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