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