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