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