Merge branch 'master' into snapshotserver
[potlatch2.git] / net / systemeD / halcyon / connection / Connection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.Event;
4     import flash.events.EventDispatcher;
5     import flash.net.*;
6     
7     import net.systemeD.halcyon.AttentionEvent;
8     import net.systemeD.halcyon.MapEvent;
9     import net.systemeD.halcyon.connection.actions.*;
10     import net.systemeD.halcyon.Globals;
11
12         public class Connection extends EventDispatcher {
13
14                 public var name:String;
15                 public var statusFetcher:StatusFetcher;
16                 public var inlineStatus:Boolean = false;
17         protected var apiBaseURL:String;
18         protected var policyURL:String;
19         protected var params:Object;
20
21                 public function Connection(cname:String,api:String,policy:String,initparams:Object=null) {
22                         initparams = (initparams!=null ? initparams:{});
23                         name=cname;
24                         apiBaseURL=api;
25                         policyURL=policy;
26                         params=initparams;
27                 }
28
29         public function getParam(name:String, defaultValue:String):String {
30                         if (params[name]) return params[name];
31                         if (Globals.vars.flashvars[name]) return Globals.vars.flashvars[name];  // REFACTOR - given the profusion of connections, should this be removed?
32                         return defaultValue;
33         }
34
35         public function get apiBase():String {
36             return apiBaseURL;
37         }
38
39         public function get serverName():String {
40             return getParam("serverName", "Localhost");
41         }
42
43                 public function getEnvironment(responder:Responder):void {}
44
45         // connection events
46         public static var LOAD_STARTED:String = "load_started";
47         public static var LOAD_COMPLETED:String = "load_completed";
48         public static var SAVE_STARTED:String = "save_started";
49         public static var SAVE_COMPLETED:String = "save_completed";
50         public static var DATA_DIRTY:String = "data_dirty";
51         public static var DATA_CLEAN:String = "data_clean";
52         public static var NEW_CHANGESET:String = "new_changeset";
53         public static var NEW_CHANGESET_ERROR:String = "new_changeset_error";
54         public static var NEW_NODE:String = "new_node";
55         public static var NEW_WAY:String = "new_way";
56         public static var NEW_RELATION:String = "new_relation";
57         public static var NEW_POI:String = "new_poi";
58         public static var NEW_MARKER:String = "new_marker";
59         public static var NODE_RENUMBERED:String = "node_renumbered";
60         public static var WAY_RENUMBERED:String = "way_renumbered";
61         public static var RELATION_RENUMBERED:String = "relation_renumbered";
62         public static var TAG_CHANGED:String = "tag_changed";
63         public static var STATUS_CHANGED:String = "status_changed";
64         public static var NODE_MOVED:String = "node_moved";
65         public static var NODE_ALTERED:String = "node_altered";
66         public static var WAY_NODE_ADDED:String = "way_node_added";
67         public static var WAY_NODE_REMOVED:String = "way_node_removed";
68         public static var WAY_REORDERED:String = "way_reordered";
69         public static var ENTITY_DRAGGED:String = "entity_dragged";
70                 public static var NODE_DELETED:String = "node_deleted";
71                 public static var WAY_DELETED:String = "way_deleted";
72                 public static var RELATION_DELETED:String = "relation_deleted";
73                 public static var RELATION_MEMBER_ADDED:String = "relation_member_added";
74                 public static var RELATION_MEMBER_REMOVED:String = "relation_member_deleted";
75                 public static var ADDED_TO_RELATION:String = "added_to_relation";
76                 public static var REMOVED_FROM_RELATION:String = "removed_from_relation";
77                 public static var SUSPEND_REDRAW:String = "suspend_redraw";
78                 public static var RESUME_REDRAW:String = "resume_redraw";
79         public static var TRACES_LOADED:String = "traces_loaded";
80
81         // store the data we download
82         private var negativeID:Number = -1;
83         private var nodes:Object = {};
84         private var ways:Object = {};
85         private var relations:Object = {};
86         private var markers:Object = {};
87         private var pois:Array = [];
88         private var changeset:Changeset = null;
89                 private var changesetUpdated:Number;
90                 private var modified:Boolean = false;
91                 public var nodecount:int=0;
92                 public var waycount:int=0;
93                 public var relationcount:int=0;
94         private var traces:Array = [];
95         private var nodePositions:Object = {};
96         protected var traces_loaded:Boolean = false;
97                 private var loadedBboxes:Array = [];
98
99                 /** maximum number of ways to keep in memory before purging */
100                 protected const MAXWAYS:uint=3000;
101
102         protected function get nextNegative():Number {
103             return negativeID--;
104         }
105
106         protected function setNode(node:Node, queue:Boolean):void {
107                         if (!nodes[node.id]) { nodecount++; }
108             nodes[node.id] = node;
109             addDupe(node);
110             if (node.loaded) { sendEvent(new EntityEvent(NEW_NODE, node),queue); }
111         }
112
113         protected function setWay(way:Way, queue:Boolean):void {
114                         if (!ways[way.id] && way.loaded) { waycount++; }
115             ways[way.id] = way;
116             if (way.loaded) { sendEvent(new EntityEvent(NEW_WAY, way),queue); }
117         }
118
119         protected function setRelation(relation:Relation, queue:Boolean):void {
120                         if (!relations[relation.id]) { relationcount++; }
121             relations[relation.id] = relation;
122             if (relation.loaded) { sendEvent(new EntityEvent(NEW_RELATION, relation),queue); }
123         }
124
125                 protected function setOrUpdateNode(newNode:Node, queue:Boolean):void {
126                 if (nodes[newNode.id]) {
127                                 var wasDeleted:Boolean=nodes[newNode.id].isDeleted();
128                                 nodes[newNode.id].update(newNode.version, newNode.getTagsHash(), true, newNode.parentsLoaded, newNode.lat, newNode.lon, newNode.uid, newNode.timestamp);
129                                 if (wasDeleted) sendEvent(new EntityEvent(NEW_NODE, nodes[newNode.id]), false);
130                         } else {
131                                 setNode(newNode, queue);
132                         }
133                 }
134
135                 protected function renumberNode(oldID:Number, newID:Number, version:uint):void {
136                         var node:Node=nodes[oldID];
137                         if (oldID!=newID) { removeDupe(node); }
138                         node.renumber(newID, version);
139                         if (oldID==newID) return;                                       // if only a version change, return
140                         nodes[newID]=node;
141                         addDupe(node);
142                         if (node.loaded) { sendEvent(new EntityRenumberedEvent(NODE_RENUMBERED, node, oldID),false); }
143                         delete nodes[oldID];
144                 }
145
146                 protected function renumberWay(oldID:Number, newID:Number, version:uint):void {
147                         var way:Way=ways[oldID];
148                         way.renumber(newID, version);
149                         if (oldID==newID) return;
150                         ways[newID]=way;
151                         if (way.loaded) { sendEvent(new EntityRenumberedEvent(WAY_RENUMBERED, way, oldID),false); }
152                         delete ways[oldID];
153                 }
154
155                 protected function renumberRelation(oldID:Number, newID:Number, version:uint):void {
156                         var relation:Relation=relations[oldID];
157                         relation.renumber(newID, version);
158                         if (oldID==newID) return;
159                         relations[newID] = relation;
160                         if (relation.loaded) { sendEvent(new EntityRenumberedEvent(RELATION_RENUMBERED, relation, oldID),false); }
161                         delete relations[oldID];
162                 }
163
164
165                 public function sendEvent(e:*,queue:Boolean):void {
166                         // queue is only used for AMFConnection
167                         dispatchEvent(e);
168                 }
169
170         public function registerPOI(node:Node):void {
171             if ( pois.indexOf(node) < 0 ) {
172                 pois.push(node);
173                 sendEvent(new EntityEvent(NEW_POI, node),false);
174             }
175         }
176
177         public function unregisterPOI(node:Node):void {
178             var index:uint = pois.indexOf(node);
179             if ( index >= 0 ) {
180                 pois.splice(index,1);
181             }
182         }
183
184         public function getNode(id:Number):Node {
185             return nodes[id];
186         }
187
188         public function getWay(id:Number):Way {
189             return ways[id];
190         }
191
192         public function getRelation(id:Number):Relation {
193             return relations[id];
194         }
195
196         public function getMarker(id:Number):Marker {
197             return markers[id];
198         }
199
200                 protected function findEntity(type:String, id:*):Entity {
201                         var i:Number=Number(id);
202                         switch (type.toLowerCase()) {
203                                 case 'node':     return getNode(id);
204                                 case 'way':      return getWay(id);
205                                 case 'relation': return getRelation(id);
206                                 default:         return null;
207                         }
208                 }
209
210                 // Remove data from Connection
211                 // These functions are used only internally to stop redundant data hanging around
212                 // (either because it's been deleted on the server, or because we have panned away
213                 //  and need to reduce memory usage)
214
215                 protected function killNode(id:Number):void {
216                         if (!nodes[id]) return;
217             nodes[id].dispatchEvent(new EntityEvent(Connection.NODE_DELETED, nodes[id]));
218                         removeDupe(nodes[id]);
219                         if (nodes[id].parentRelations.length>0) {
220                                 nodes[id].nullify();
221                         } else {
222                                 delete nodes[id];
223                         }
224                         nodecount--;
225                 }
226
227                 protected function killWay(id:Number):void {
228                         if (!ways[id]) return;
229             ways[id].dispatchEvent(new EntityEvent(Connection.WAY_DELETED, ways[id]));
230                         if (ways[id].parentRelations.length>0) {
231                                 ways[id].nullify();
232                         } else {
233                                 delete ways[id];
234                         }
235                         waycount--;
236                 }
237
238                 protected function killRelation(id:Number):void {
239                         if (!relations[id]) return;
240             relations[id].dispatchEvent(new EntityEvent(Connection.RELATION_DELETED, relations[id]));
241                         if (relations[id].parentRelations.length>0) {
242                                 relations[id].nullify();
243                         } else {
244                                 delete relations[id];
245                         }
246                         relationcount--;
247                 }
248
249                 protected function killWayWithNodes(id:Number):void {
250                         var way:Way=ways[id];
251                         var node:Node;
252                         for (var i:uint=0; i<way.length; i++) {
253                                 node=way.getNode(i);
254                                 if (node.isDirty) { continue; }
255                                 if (node.parentWays.length>1) {
256                                         node.removeParent(way);
257                                 } else {
258                                         killNode(node.id);
259                                 }
260                         }
261                         killWay(id);
262                 }
263                 
264                 protected function killEntity(entity:Entity):void {
265                         if (entity is Way) { killWay(entity.id); }
266                         else if (entity is Node) { killNode(entity.id); }
267                         else if (entity is Relation) { killRelation(entity.id); }
268                 }
269
270         public function createNode(tags:Object, lat:Number, lon:Number, performCreate:Function):Node {
271             var node:Node = new Node(this, nextNegative, 0, tags, true, lat, lon);
272             performCreate(new CreateEntityAction(node, setNode));
273             return node;
274         }
275
276         public function createWay(tags:Object, nodes:Array, performCreate:Function):Way {
277             var way:Way = new Way(this, nextNegative, 0, tags, true, nodes.concat());
278             performCreate(new CreateEntityAction(way, setWay));
279             return way;
280         }
281
282         public function createRelation(tags:Object, members:Array, performCreate:Function):Relation {
283             var relation:Relation = new Relation(this, nextNegative, 0, tags, true, members.concat());
284             performCreate(new CreateEntityAction(relation, setRelation));
285             return relation;
286         }
287
288         /** Create a new marker. This can't be done as part of a Composite Action. */
289         // REFACTOR  This needs renaming and/or refactoring to behave more similarly to n/w/r
290         public function createMarker(tags:Object,lat:Number,lon:Number,id:Number=NaN):Marker {
291             if (!id) {
292               id = negativeID;
293               negativeID--;
294             }
295             var marker:Marker = markers[id];
296             if (marker == null) {
297               marker = new Marker(this, id, 0, tags, true, lat, lon);
298               markers[id]=marker;
299               sendEvent(new EntityEvent(NEW_MARKER, marker),false);
300             }
301             return marker;
302         }
303
304         public function getAllNodeIDs():Array {
305             var list:Array = [];
306             for each (var node:Node in nodes)
307                 list.push(node.id);
308             return list;
309         }
310
311         public function getAllWayIDs():Array {
312             var list:Array = [];
313             for each (var way:Way in ways)
314                 list.push(way.id);
315             return list;
316         }
317
318         public function getAllRelationIDs():Array {
319             var list:Array = [];
320             for each (var relation:Relation in relations)
321                 list.push(relation.id);
322             return list;
323         }
324
325                 public function getAllLoadedEntities():Array {
326                         var list:Array = []; var entity:Entity;
327                         for each (entity in relations) { if (entity.loaded && !entity.deleted) list.push(entity); }
328                         for each (entity in ways     ) { if (entity.loaded && !entity.deleted) list.push(entity); }
329                         for each (entity in nodes    ) { if (entity.loaded && !entity.deleted) list.push(entity); }
330                         return list;
331                 }
332
333         /** Returns all available relations that match all of {k1: [v1,v2,...], k2: [v1...] ...} 
334         * where p1 is an array [v1, v2, v3...] */
335         public function getMatchingRelationIDs(match:Object):Array {
336             var list:Array = [];
337             for each (var relation:Relation in relations) {
338                 var ok: Boolean = true;
339                                 if (relation.deleted) { continue; }
340                                 for (var k:String in match) {
341                                         var v:String = relation.getTagsHash()[k];
342                                         if (!v || match[k].indexOf(v) < 0) { 
343                                            ok = false; break;  
344                                         }
345                                 }
346                                 if (ok) { list.push(relation.id); }
347                         }
348             return list;
349         }
350
351                 public function getObjectsByBbox(left:Number, right:Number, top:Number, bottom:Number):Object {
352                         var o:Object = { poisInside: [], poisOutside: [], waysInside: [], waysOutside: [],
353                               markersInside: [], markersOutside: [] };
354                         for each (var way:Way in ways) {
355                                 if (way.within(left,right,top,bottom)) { o.waysInside.push(way); }
356                                                                   else { o.waysOutside.push(way); }
357                         }
358                         for each (var poi:Node in pois) {
359                                 if (poi.within(left,right,top,bottom)) { o.poisInside.push(poi); }
360                                                                   else { o.poisOutside.push(poi); }
361                         }
362             for each (var marker:Marker in markers) {
363                 if (marker.within(left,right,top,bottom)) { o.markersInside.push(marker); }
364                                                      else { o.markersOutside.push(marker); }
365             }
366                         return o;
367                 }
368
369                 public function purgeOutside(left:Number, right:Number, top:Number, bottom:Number):void {
370                         for each (var way:Way in ways) {
371                                 if (!way.within(left,right,top,bottom) && !way.isDirty && !way.locked && !way.hasLockedNodes()) {
372                                         killWayWithNodes(way.id);
373                                 }
374                         }
375                         for each (var poi:Node in pois) {
376                                 if (!poi.within(left,right,top,bottom) && !poi.isDirty && !poi.locked) {
377                                         killNode(poi.id);
378                                 }
379                         }
380                         // ** should purge relations too, if none of their members are on-screen
381                 }
382
383                 public function markDirty():void {
384             if (!modified) { dispatchEvent(new Event(DATA_DIRTY)); }
385                         modified=true;
386                 }
387                 public function markClean():void {
388             if (modified) { dispatchEvent(new Event(DATA_CLEAN)); }
389                         modified=false;
390                 }
391                 public function get isDirty():Boolean {
392                         return modified;
393                 }
394
395                 // Keep track of the bboxes we've loaded
396
397                 /** Has the data within this bbox already been loaded? */
398                 protected function isBboxLoaded(left:Number,right:Number,top:Number,bottom:Number):Boolean {
399                         var l:Number,r:Number,t:Number,b:Number;
400                         for each (var box:Array in loadedBboxes) {
401                                 l=box[0]; r=box[1]; t=box[2]; b=box[3];
402                                 if (left>=l && left<=r && right>=l && right<=r && top>=b && top<=t && bottom>=b && bottom<=t) {
403                                         return true;
404                                 }
405                         }
406                         return false;
407                 }
408                 /** Mark that bbox is loaded */
409                 protected function markBboxLoaded(left:Number,right:Number,top:Number,bottom:Number):void {
410                         if (isBboxLoaded(left,right,top,bottom)) return;
411                         loadedBboxes.push([left,right,top,bottom]);
412                 }
413                 /** Purge all data if number of ways exceeds limit */
414                 public function purgeIfFull(left:Number,right:Number,top:Number,bottom:Number):void {
415                         if (waycount<=MAXWAYS) return;
416                         purgeOutside(left,right,top,bottom);
417                         loadedBboxes=[[left,right,top,bottom]];
418                 }
419
420                 // Changeset tracking
421
422         protected function setActiveChangeset(changeset:Changeset):void {
423             this.changeset = changeset;
424                         changesetUpdated = new Date().getTime();
425             sendEvent(new EntityEvent(NEW_CHANGESET, changeset),false);
426         }
427
428                 protected function freshenActiveChangeset():void {
429                         changesetUpdated = new Date().getTime();
430                 }
431                 
432                 protected function closeActiveChangeset():void {
433                         changeset = null;
434                 }
435         
436         public function getActiveChangeset():Changeset {
437                         if (changeset && (new Date().getTime()) > (changesetUpdated+58*60*1000)) {
438                                 closeActiveChangeset();
439                         }
440             return changeset;
441         }
442
443         public function addTrace(t:Object):void {
444             traces.push(t);
445         }
446
447         protected function clearTraces():void {
448             traces = [];
449         }
450
451                 public function findTrace(id:int):Trace {
452                         for each (var t:Trace in traces) {
453                                 if (t.id == id) return t;
454                         }
455                         return null;
456                 }
457
458         public function getTraces():Array {
459             return traces;
460         }
461
462         public function addDupe(node:Node):void {
463             if (getNode(node.id) != node) { return; } // make sure it's on this connection
464             var a:String = node.lat+","+node.lon;
465             if(!nodePositions[a]) {
466               nodePositions[a] = [];
467             }
468             nodePositions[a].push(node);
469             if (nodePositions[a].length > 1) { // don't redraw if it's the only node in town
470               for each (var n:Node in nodePositions[a]) {
471                 n.dispatchEvent(new Event(Connection.NODE_ALTERED));
472               }
473             }
474         }
475
476         public function removeDupe(node:Node):void {
477             if (getNode(node.id) != node) { return; } // make sure it's on this connection
478             var a:String = node.lat+","+node.lon;
479             var redraw:Boolean=node.isDupe();
480             var dupes:Array = [];
481             for each (var dupe:Node in nodePositions[a]) {
482               if (dupe!=node) { dupes.push(dupe); }
483             }
484             nodePositions[a] = dupes;
485             for each (var n:Node in nodePositions[a]) { // redraw any nodes remaining
486               n.dispatchEvent(new Event(Connection.NODE_ALTERED));
487             }
488             if (redraw) { node.dispatchEvent(new Event(Connection.NODE_ALTERED)); } //redraw the one being moved
489         }
490
491         public function nodesAtPosition(lat:Number, lon:Number):uint {
492             if (nodePositions[lat+","+lon]) {
493               return nodePositions[lat+","+lon].length;
494             }
495             return 0;
496         }
497
498         public function getNodesAtPosition(lat:Number, lon:Number):Array {
499             if (nodePositions[lat+","+lon]) {
500               return nodePositions[lat+","+lon];
501             }
502             return [];
503         }
504
505                 public function identicalNode(node:Node):Node {
506                         for each (var dupe:Node in nodePositions[node.lat+","+node.lon]) {
507                                 if (node.lat==dupe.lat && node.lon==dupe.lon && node.sameTags(dupe)) return dupe;
508                         }
509                         return null;
510                 }
511
512                 // Error-handling
513                 
514                 protected function throwConflictError(entity:Entity,serverVersion:uint,message:String):void {
515                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
516                                 message: "An item you edited has been changed by another mapper. Download their version and try again? (The server said: "+message+")",
517                                 yes: function():void { revertBeforeUpload(entity) },
518                                 no: cancelUpload }));
519                         // ** FIXME: this should also offer the choice of 'overwrite?'
520                 }
521                 protected function throwAlreadyDeletedError(entity:Entity,message:String):void {
522                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
523                                 message: "You tried to delete something that's already been deleted. Forget it and try again? (The server said: "+message+")",
524                                 yes: function():void { deleteBeforeUpload(entity) },
525                                 no: cancelUpload }));
526                 }
527                 protected function throwInUseError(entity:Entity,message:String):void {
528                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
529                                 message: "You tried to delete something that's since been used elsewhere. Restore it and try again? (The server said: "+message+")",
530                                 yes: function():void { revertBeforeUpload(entity) },
531                                 no: cancelUpload }));
532                 }
533                 protected function throwEntityError(entity:Entity,message:String):void {
534                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
535                                 message: "There is a problem with your changes which needs to be fixed before you can save: "+message+". Click 'OK' to see the offending item.",
536                                 ok: function():void { goToEntity(entity) } }));
537                 }
538                 protected function throwChangesetError(message:String):void {
539                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
540                                 message: "The changeset in which you're saving changes is no longer valid. Start a new one and retry? (The server said: "+message+")",
541                                 yes: retryUploadWithNewChangeset,
542                                 no: cancelUpload }));
543                 }
544                 protected function throwBugError(message:String):void {
545                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
546                                 message: "An unexpected error occurred, probably due to a bug in Potlatch 2. Do you want to retry? (The server said: "+message+")",
547                                 yes: retryUpload,
548                                 no: cancelUpload }));
549                 }
550                 protected function throwServerError(message:String):void {
551                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
552                                 message: "A server error occurred. Do you want to retry? (The server said: "+message+")",
553                                 yes: retryUpload,
554                                 no: cancelUpload }));
555                 }
556
557                 public function retryUpload(e:Event=null):void { 
558                         removeEventListener(LOAD_COMPLETED,retryUpload);
559                         uploadChanges(); 
560                 }
561                 public function cancelUpload():void {
562                         return;
563                 }
564                 public function retryUploadWithNewChangeset():void { 
565                         // ** FIXME: we need to move the create-changeset-then-upload logic out of SaveDialog
566                 }
567                 public function goToEntity(entity:Entity):void { 
568                         dispatchEvent(new AttentionEvent(AttentionEvent.ATTENTION, entity));
569                 }
570                 public function revertBeforeUpload(entity:Entity):void { 
571                         addEventListener(LOAD_COMPLETED,retryUpload);
572                         loadEntity(entity);
573                 }
574                 public function deleteBeforeUpload(entity:Entity):void {
575             var a:CompositeUndoableAction = new CompositeUndoableAction("Delete refs");            
576             entity.remove(a.push);
577             a.doAction();
578                         killEntity(entity);
579                         uploadChanges();
580                 }
581
582         // these are functions that the Connection implementation is expected to
583         // provide. This class has some generic helpers for the implementation.
584         /**
585         * Load data for the bounding box given. Usually called in response to pan / zoom requests
586         */
587                 public function loadBbox(left:Number, right:Number,
588                                                                 top:Number, bottom:Number):void {
589             }
590             public function loadEntityByID(type:String, id:Number):void {}
591             public function setAuthToken(id:Object):void {}
592         public function setAccessToken(key:String, secret:String):void {}
593             public function createChangeset(tags:Object):void {}
594                 public function closeChangeset():void {}
595         public function uploadChanges():* {}
596         public function fetchUserTraces(refresh:Boolean=false):void {}
597         public function fetchTrace(id:Number, callback:Function):void {}
598         public function hasAccessToken():Boolean { return false; }
599
600                 public function loadEntity(entity:Entity):void {
601                         loadEntityByID(entity.getType(),entity.id);
602                 }
603
604     }
605
606 }
607