support reverting individual entities - makes conflict resolution work
[potlatch2.git] / net / systemeD / halcyon / connection / Connection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.net.*;
4
5     import flash.events.EventDispatcher;
6     import flash.events.Event;
7         import net.systemeD.halcyon.Globals;
8         import net.systemeD.halcyon.connection.actions.*;
9         import net.systemeD.halcyon.AttentionEvent;
10         import net.systemeD.halcyon.MapEvent;
11
12         public class Connection extends EventDispatcher {
13
14         private static var connectionInstance:Connection = null;
15
16         protected static var policyURL:String;
17         protected static var apiBaseURL:String;
18         protected static var params:Object;
19
20         public static function getConnection(initparams:Object=null):Connection {
21             if ( connectionInstance == null ) {
22             
23                 params = initparams == null ? new Object() : initparams;
24                 policyURL = getParam("policy", "http://127.0.0.1:3000/api/crossdomain.xml");
25                 apiBaseURL = getParam("api", "http://127.0.0.1:3000/api/0.6/");
26                 var connectType:String = getParam("connection", "XML");
27                 
28                 if ( connectType == "XML" )
29                     connectionInstance = new XMLConnection();
30                 else if ( connectType == "OSM" )
31                     connectionInstance = new OSMConnection();
32                 else
33                     connectionInstance = new AMFConnection();
34             }
35             return connectionInstance;
36         }
37
38         public static function getParam(name:String, defaultValue:String):String {
39             return params[name] == null ? defaultValue : params[name];
40         }
41
42         public function get apiBase():String {
43             return apiBaseURL;
44         }
45
46         public static function get serverName():String {
47             return getParam("serverName", "Localhost");
48         }
49                 
50                 public static function getConnectionInstance():Connection {
51             return connectionInstance;
52                 }
53
54                 public function getEnvironment(responder:Responder):void {}
55
56         // connection events
57         public static var LOAD_STARTED:String = "load_started";
58         public static var LOAD_COMPLETED:String = "load_completed";
59         public static var SAVE_STARTED:String = "save_started";
60         public static var SAVE_COMPLETED:String = "save_completed";
61         public static var DATA_DIRTY:String = "data_dirty";
62         public static var DATA_CLEAN:String = "data_clean";
63         public static var NEW_CHANGESET:String = "new_changeset";
64         public static var NEW_CHANGESET_ERROR:String = "new_changeset_error";
65         public static var NEW_NODE:String = "new_node";
66         public static var NEW_WAY:String = "new_way";
67         public static var NEW_RELATION:String = "new_relation";
68         public static var NEW_POI:String = "new_poi";
69         public static var NODE_RENUMBERED:String = "node_renumbered";
70         public static var WAY_RENUMBERED:String = "way_renumbered";
71         public static var RELATION_RENUMBERED:String = "relation_renumbered";
72         public static var TAG_CHANGED:String = "tag_change";
73         public static var NODE_MOVED:String = "node_moved";
74         public static var NODE_ALTERED:String = "node_altered";
75         public static var WAY_NODE_ADDED:String = "way_node_added";
76         public static var WAY_NODE_REMOVED:String = "way_node_removed";
77         public static var WAY_REORDERED:String = "way_reordered";
78         public static var ENTITY_DRAGGED:String = "entity_dragged";
79                 public static var NODE_DELETED:String = "node_deleted";
80                 public static var WAY_DELETED:String = "way_deleted";
81                 public static var RELATION_DELETED:String = "relation_deleted";
82                 public static var RELATION_MEMBER_ADDED:String = "relation_member_added";
83                 public static var RELATION_MEMBER_REMOVED:String = "relation_member_deleted";
84                 public static var ADDED_TO_RELATION:String = "added_to_relation";
85                 public static var REMOVED_FROM_RELATION:String = "removed_from_relation";
86                 public static var SUSPEND_REDRAW:String = "suspend_redraw";
87                 public static var RESUME_REDRAW:String = "resume_redraw";
88         public static var TRACES_LOADED:String = "traces_loaded";
89
90         // store the data we download
91         private var negativeID:Number = -1;
92         private var nodes:Object = {};
93         private var ways:Object = {};
94         private var relations: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:Array = [];
103         private var nodePositions:Object = {};
104         protected var traces_loaded:Boolean = false;
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                 protected function findEntity(type:String, id:*):Entity {
201                         var i:Number=Number(id);
202                         switch (type.toLowerCase()) {
203                                 case 'node':     return getNode(id);
204                                 case 'way':      return getWay(id);
205                                 case 'relation': return getRelation(id);
206                                 default:         return null;
207                         }
208                 }
209
210                 // Remove data from Connection
211                 // These functions are used only internally to stop redundant data hanging around
212                 // (either because it's been deleted on the server, or because we have panned away
213                 //  and need to reduce memory usage)
214
215                 protected function killNode(id:Number):void {
216                         if (!nodes[id]) return;
217             nodes[id].dispatchEvent(new EntityEvent(Connection.NODE_DELETED, nodes[id]));
218                         removeDupe(nodes[id]);
219                         if (nodes[id].parentRelations.length>0) {
220                                 nodes[id].nullify();
221                         } else {
222                                 delete nodes[id];
223                         }
224                         nodecount--;
225                 }
226
227                 protected function killWay(id:Number):void {
228                         if (!ways[id]) return;
229             ways[id].dispatchEvent(new EntityEvent(Connection.WAY_DELETED, ways[id]));
230                         if (ways[id].parentRelations.length>0) {
231                                 ways[id].nullify();
232                         } else {
233                                 delete ways[id];
234                         }
235                         waycount--;
236                 }
237
238                 protected function killRelation(id:Number):void {
239                         if (!relations[id]) return;
240             relations[id].dispatchEvent(new EntityEvent(Connection.RELATION_DELETED, relations[id]));
241                         if (relations[id].parentRelations.length>0) {
242                                 relations[id].nullify();
243                         } else {
244                                 delete relations[id];
245                         }
246                         relationcount--;
247                 }
248
249                 protected function killWayWithNodes(id:Number):void {
250                         var way:Way=ways[id];
251                         var node:Node;
252                         for (var i:uint=0; i<way.length; i++) {
253                                 node=way.getNode(i);
254                                 if (node.isDirty) { continue; }
255                                 if (node.parentWays.length>1) {
256                                         node.removeParent(way);
257                                 } else {
258                                         killNode(node.id);
259                                 }
260                         }
261                         killWay(id);
262                 }
263                 
264                 protected function killEntity(entity:Entity):void {
265                         if (entity is Way) { killWay(entity.id); }
266                         else if (entity is Node) { killNode(entity.id); }
267                         else if (entity is Relation) { killRelation(entity.id); }
268                 }
269
270         public function createNode(tags:Object, lat:Number, lon:Number, performCreate:Function):Node {
271             var node:Node = new Node(nextNegative, 0, tags, true, lat, lon);
272             performCreate(new CreateEntityAction(node, setNode));
273                         //markDirty();
274             return node;
275         }
276
277         public function createWay(tags:Object, nodes:Array, performCreate:Function):Way {
278             var way:Way = new Way(nextNegative, 0, tags, true, nodes.concat());
279             performCreate(new CreateEntityAction(way, setWay));
280                         //markDirty();
281             return way;
282         }
283
284         public function createRelation(tags:Object, members:Array, performCreate:Function):Relation {
285             var relation:Relation = new Relation(nextNegative, 0, tags, true, members.concat());
286             performCreate(new CreateEntityAction(relation, setRelation));
287                         //markDirty();
288             return relation;
289         }
290
291         public function getAllNodeIDs():Array {
292             var list:Array = [];
293             for each (var node:Node in nodes)
294                 list.push(node.id);
295             return list;
296         }
297
298         public function getAllWayIDs():Array {
299             var list:Array = [];
300             for each (var way:Way in ways)
301                 list.push(way.id);
302             return list;
303         }
304
305         public function getAllRelationIDs():Array {
306             var list:Array = [];
307             for each (var relation:Relation in relations)
308                 list.push(relation.id);
309             return list;
310         }
311
312         public function getMatchingRelationIDs(match:Object):Array {
313             var list:Array = [];
314                         var ok:Boolean;
315             for each (var relation:Relation in relations) {
316                                 ok=true;
317                                 if (relation.deleted) { ok=false; }
318                                 for (var k:String in match) {
319                                         if (!relation.getTagsHash()[k] || relation.getTagsHash()[k]!=match[k]) { ok=false; }
320                                 }
321                                 if (ok) { list.push(relation.id); }
322                         }
323             return list;
324         }
325
326                 public function getObjectsByBbox(left:Number, right:Number, top:Number, bottom:Number):Object {
327                         var o:Object = { poisInside: [], poisOutside: [], waysInside: [], waysOutside: [] };
328                         for each (var way:Way in ways) {
329                                 if (way.within(left,right,top,bottom)) { o.waysInside.push(way); }
330                                                                   else { o.waysOutside.push(way); }
331                         }
332                         for each (var poi:Node in pois) {
333                                 if (poi.within(left,right,top,bottom)) { o.poisInside.push(poi); }
334                                                                   else { o.poisOutside.push(poi); }
335                         }
336                         return o;
337                 }
338
339                 public function purgeOutside(left:Number, right:Number, top:Number, bottom:Number):void {
340                         for each (var way:Way in ways) {
341                                 if (!way.within(left,right,top,bottom) && !way.isDirty && !way.locked && !way.hasLockedNodes()) {
342                                         killWayWithNodes(way.id);
343                                 }
344                         }
345                         for each (var poi:Node in pois) {
346                                 if (!poi.within(left,right,top,bottom) && !poi.isDirty && !poi.locked) {
347                                         killNode(poi.id);
348                                 }
349                         }
350                         // ** should purge relations too, if none of their members are on-screen
351                 }
352
353                 public function markDirty():void {
354             if (!modified) { dispatchEvent(new Event(DATA_DIRTY)); }
355                         modified=true;
356                 }
357                 public function markClean():void {
358             if (modified) { dispatchEvent(new Event(DATA_CLEAN)); }
359                         modified=false;
360                 }
361                 public function get isDirty():Boolean {
362                         return modified;
363                 }
364
365                 // Changeset tracking
366
367         protected function setActiveChangeset(changeset:Changeset):void {
368             this.changeset = changeset;
369                         changesetUpdated = new Date().getTime();
370             sendEvent(new EntityEvent(NEW_CHANGESET, changeset),false);
371         }
372
373                 protected function freshenActiveChangeset():void {
374                         changesetUpdated = new Date().getTime();
375                 }
376                 
377                 protected function closeActiveChangeset():void {
378                         changeset = null;
379                 }
380         
381         public function getActiveChangeset():Changeset {
382                         if (changeset && (new Date().getTime()) > (changesetUpdated+58*60*1000)) {
383                                 closeActiveChangeset();
384                         }
385             return changeset;
386         }
387
388         protected function addTrace(t:Object):void {
389             traces.push(t);
390         }
391
392         protected function clearTraces():void {
393             traces = [];
394         }
395
396         public function getTraces():Array {
397             return traces;
398         }
399
400         public function addDupe(node:Node):void {
401             if (getNode(node.id) != node) { return; } // make sure it's on this connection
402             var a:String = node.lat+","+node.lon;
403             if(!nodePositions[a]) {
404               nodePositions[a] = [];
405             }
406             nodePositions[a].push(node);
407             if (nodePositions[a].length > 1) { // don't redraw if it's the only node in town
408               for each (var n:Node in nodePositions[a]) {
409                 n.dispatchEvent(new Event(Connection.NODE_ALTERED));
410               }
411             }
412         }
413
414         public function removeDupe(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             var redraw:Boolean=node.isDupe();
418             var dupes:Array = [];
419             for each (var dupe:Node in nodePositions[a]) {
420               if (dupe!=node) { dupes.push(dupe); }
421             }
422             nodePositions[a] = dupes;
423             for each (var n:Node in nodePositions[a]) { // redraw any nodes remaining
424               n.dispatchEvent(new Event(Connection.NODE_ALTERED));
425             }
426             if (redraw) { node.dispatchEvent(new Event(Connection.NODE_ALTERED)); } //redraw the one being moved
427         }
428
429         public function nodesAtPosition(lat:Number, lon:Number):uint {
430             if (nodePositions[lat+","+lon]) {
431               return nodePositions[lat+","+lon].length;
432             }
433             return 0;
434         }
435
436         public function getNodesAtPosition(lat:Number, lon:Number):Array {
437             if (nodePositions[lat+","+lon]) {
438               return nodePositions[lat+","+lon];
439             }
440             return [];
441         }
442
443                 // Error-handling
444                 
445                 protected function throwConflictError(entity:Entity,serverVersion:uint,message:String):void {
446                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
447                                 message: "An item you edited has been changed by another mapper. Download their version and try again? (The server said: "+message+")",
448                                 yes: function():void { revertBeforeUpload(entity) },
449                                 no: cancelUpload }));
450                         // ** FIXME: this should also offer the choice of 'overwrite?'
451                 }
452                 protected function throwAlreadyDeletedError(entity:Entity,message:String):void {
453                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
454                                 message: "You tried to delete something that's already been deleted. Forget it and try again? (The server said: "+message+")",
455                                 yes: function():void { deleteBeforeUpload(entity) },
456                                 no: cancelUpload }));
457                 }
458                 protected function throwInUseError(entity:Entity,message:String):void {
459                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
460                                 message: "You tried to delete something that's since been used elsewhere. Restore it and try again? (The server said: "+message+")",
461                                 yes: function():void { revertBeforeUpload(entity) },
462                                 no: cancelUpload }));
463                 }
464                 protected function throwEntityError(entity:Entity,message:String):void {
465                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
466                                 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.",
467                                 ok: function():void { goToEntity(entity) } }));
468                 }
469                 protected function throwChangesetError(message:String):void {
470                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
471                                 message: "The changeset in which you're saving changes is no longer valid. Start a new one and retry? (The server said: "+message+")",
472                                 yes: retryUploadWithNewChangeset,
473                                 no: cancelUpload }));
474                 }
475                 protected function throwBugError(message:String):void {
476                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
477                                 message: "An unexpected error occurred, probably due to a bug in Potlatch 2. Do you want to retry? (The server said: "+message+")",
478                                 yes: retryUpload,
479                                 no: cancelUpload }));
480                 }
481                 protected function throwServerError(message:String):void {
482                         dispatchEvent(new MapEvent(MapEvent.ERROR, {
483                                 message: "A server error occurred. Do you want to retry? (The server said: "+message+")",
484                                 yes: retryUpload,
485                                 no: cancelUpload }));
486                 }
487
488                 public function retryUpload(e:Event=null):void { 
489                         removeEventListener(LOAD_COMPLETED,retryUpload);
490                         uploadChanges(); 
491                 }
492                 public function cancelUpload():void {
493                         return;
494                 }
495                 public function retryUploadWithNewChangeset():void { 
496                         // ** FIXME: we need to move the create-changeset-then-upload logic out of SaveDialog
497                 }
498                 public function goToEntity(entity:Entity):void { 
499                         dispatchEvent(new AttentionEvent(AttentionEvent.ATTENTION, entity));
500                 }
501                 public function revertBeforeUpload(entity:Entity):void { 
502                         addEventListener(LOAD_COMPLETED,retryUpload);
503                         loadEntity(entity);
504                 }
505                 public function deleteBeforeUpload(entity:Entity):void {
506             var a:CompositeUndoableAction = new CompositeUndoableAction("Delete refs");            
507             entity.remove(a.push);
508             a.doAction();
509                         killEntity(entity);
510                         uploadChanges();
511                 }
512
513         // these are functions that the Connection implementation is expected to
514         // provide. This class has some generic helpers for the implementation.
515                 public function loadBbox(left:Number, right:Number,
516                                                                 top:Number, bottom:Number):void {
517             }
518             public function loadEntity(entity:Entity):void {}
519             public function setAuthToken(id:Object):void {}
520         public function setAccessToken(key:String, secret:String):void {}
521             public function createChangeset(tags:Object):void {}
522                 public function closeChangeset():void {}
523             public function uploadChanges():void {}
524         public function fetchUserTraces(refresh:Boolean=false):void {}
525         public function fetchTrace(id:Number, callback:Function):void {}
526         public function hasAccessToken():Boolean { return false; }
527     }
528
529 }
530