1 package net.systemeD.halcyon.connection {
3 import flash.events.Event;
4 import flash.events.EventDispatcher;
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;
12 public class Connection extends EventDispatcher {
14 public var name:String;
15 protected var apiBaseURL:String;
16 protected var policyURL:String;
17 protected var params:Object;
19 public function Connection(cname:String,api:String,policy:String,initparams:Object=null) {
20 initparams = (initparams!=null ? initparams:{});
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];
33 public function get apiBase():String {
37 public function get serverName():String {
38 return getParam("serverName", "Localhost");
41 public function getEnvironment(responder:Responder):void {}
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";
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 = [];
96 /** maximum number of ways to keep in memory before purging */
97 protected const MAXWAYS:uint=3000;
99 protected function get nextNegative():Number {
103 protected function setNode(node:Node, queue:Boolean):void {
104 if (!nodes[node.id]) { nodecount++; }
105 nodes[node.id] = node;
107 if (node.loaded) { sendEvent(new EntityEvent(NEW_NODE, node),queue); }
110 protected function setWay(way:Way, queue:Boolean):void {
111 if (!ways[way.id] && way.loaded) { waycount++; }
113 if (way.loaded) { sendEvent(new EntityEvent(NEW_WAY, way),queue); }
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); }
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);
128 setNode(newNode, queue);
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
139 if (node.loaded) { sendEvent(new EntityRenumberedEvent(NODE_RENUMBERED, node, oldID),false); }
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;
148 if (way.loaded) { sendEvent(new EntityRenumberedEvent(WAY_RENUMBERED, way, oldID),false); }
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];
162 public function sendEvent(e:*,queue:Boolean):void {
163 // queue is only used for AMFConnection
167 public function registerPOI(node:Node):void {
168 if ( pois.indexOf(node) < 0 ) {
170 sendEvent(new EntityEvent(NEW_POI, node),false);
174 public function unregisterPOI(node:Node):void {
175 var index:uint = pois.indexOf(node);
177 pois.splice(index,1);
181 public function getNode(id:Number):Node {
185 public function getWay(id:Number):Way {
189 public function getRelation(id:Number):Relation {
190 return relations[id];
193 public function getMarker(id:Number):Marker {
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;
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)
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) {
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) {
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();
241 delete relations[id];
246 protected function killWayWithNodes(id:Number):void {
247 var way:Way=ways[id];
249 for (var i:uint=0; i<way.length; i++) {
251 if (node.isDirty) { continue; }
252 if (node.parentWays.length>1) {
253 node.removeParent(way);
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); }
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));
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));
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));
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 {
292 var marker:Marker = markers[id];
293 if (marker == null) {
295 marker = new Marker(this, id, 0, tags, true, lat, lon);
297 sendEvent(new EntityEvent(NEW_MARKER, marker),false);
302 public function getAllNodeIDs():Array {
304 for each (var node:Node in nodes)
309 public function getAllWayIDs():Array {
311 for each (var way:Way in ways)
316 public function getAllRelationIDs():Array {
318 for each (var relation:Relation in relations)
319 list.push(relation.id);
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 {
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) {
336 if (ok) { list.push(relation.id); }
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); }
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); }
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); }
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);
365 for each (var poi:Node in pois) {
366 if (!poi.within(left,right,top,bottom) && !poi.isDirty && !poi.locked) {
370 // ** should purge relations too, if none of their members are on-screen
373 public function markDirty():void {
374 if (!modified) { dispatchEvent(new Event(DATA_DIRTY)); }
377 public function markClean():void {
378 if (modified) { dispatchEvent(new Event(DATA_CLEAN)); }
381 public function get isDirty():Boolean {
385 // Keep track of the bboxes we've loaded
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) {
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]);
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]];
410 // Changeset tracking
412 protected function setActiveChangeset(changeset:Changeset):void {
413 this.changeset = changeset;
414 changesetUpdated = new Date().getTime();
415 sendEvent(new EntityEvent(NEW_CHANGESET, changeset),false);
418 protected function freshenActiveChangeset():void {
419 changesetUpdated = new Date().getTime();
422 protected function closeActiveChangeset():void {
426 public function getActiveChangeset():Changeset {
427 if (changeset && (new Date().getTime()) > (changesetUpdated+58*60*1000)) {
428 closeActiveChangeset();
433 protected function addTrace(t:Object):void {
437 protected function clearTraces():void {
441 public function getTraces():Array {
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] = [];
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));
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); }
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));
471 if (redraw) { node.dispatchEvent(new Event(Connection.NODE_ALTERED)); } //redraw the one being moved
474 public function nodesAtPosition(lat:Number, lon:Number):uint {
475 if (nodePositions[lat+","+lon]) {
476 return nodePositions[lat+","+lon].length;
481 public function getNodesAtPosition(lat:Number, lon:Number):Array {
482 if (nodePositions[lat+","+lon]) {
483 return nodePositions[lat+","+lon];
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?'
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 }));
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 }));
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) } }));
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 }));
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+")",
524 no: cancelUpload }));
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+")",
530 no: cancelUpload }));
533 public function retryUpload(e:Event=null):void {
534 removeEventListener(LOAD_COMPLETED,retryUpload);
537 public function cancelUpload():void {
540 public function retryUploadWithNewChangeset():void {
541 // ** FIXME: we need to move the create-changeset-then-upload logic out of SaveDialog
543 public function goToEntity(entity:Entity):void {
544 dispatchEvent(new AttentionEvent(AttentionEvent.ATTENTION, entity));
546 public function revertBeforeUpload(entity:Entity):void {
547 addEventListener(LOAD_COMPLETED,retryUpload);
550 public function deleteBeforeUpload(entity:Entity):void {
551 var a:CompositeUndoableAction = new CompositeUndoableAction("Delete refs");
552 entity.remove(a.push);
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 {
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; }
573 public function loadEntity(entity:Entity):void {
574 loadEntityByID(entity.getType(),entity.id);