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