Merge branch 'master' into history
[potlatch2.git] / net / systemeD / halcyon / connection / Entity.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.Event;
4     import flash.events.EventDispatcher;
5     import flash.utils.Dictionary;
6     
7     import net.systemeD.halcyon.connection.actions.*;
8
9     /** An Entity is an object stored in the map database, and therefore uploaded and downloaded. This includes Nodes, Ways, 
10     *   Relations but also Changesets etc. */
11     public class Entity extends EventDispatcher {
12                 private var _connection:Connection;
13         private var _id:Number;
14         private var _version:uint;
15         private var _uid:Number;
16         private var _timestamp:String;
17         private var _user:String;
18         private var tags:Object = {};
19         private var modified:Boolean = false;
20         private var _loaded:Boolean = true;
21         private var parents:Dictionary = new Dictionary();
22                 public var status:String;
23         /** Lock against purging when off-screen */
24         public var locked:Boolean = false;
25         public var deleted:Boolean = false;
26         /** Have all its parents (ie, relations that contain this object as a member, ways that contain this node) been loaded into memory */
27         public var parentsLoaded:Boolean = true;
28
29         public function Entity(connection:Connection, id:Number, version:uint, tags:Object, loaded:Boolean, uid:Number, timestamp:String, user:String) {
30                         this._connection = connection;
31             this._id = id;
32             this._version = version;
33             this._uid = uid;
34             this._timestamp = timestamp;
35             this._user = user
36             if (connection.cssTransform) tags=connection.cssTransform.run(this,tags);
37             this.tags = tags;
38                         this._loaded = loaded;
39             modified = id < 0;
40         }
41
42         /** OSM ID. */
43         public function get id():Number {
44             return _id;
45         }
46
47         /** Current version number. */
48         public function get version():uint {
49             return _version;
50         }
51
52         /** User ID who last edited this entity (from OSM API). */
53         public function get uid():Number {
54             return _uid;
55         }
56
57                 /** Is entity fully loaded, or is it just a placeholder reference (as a relation member)? */
58         public function get loaded():Boolean {
59             return _loaded;
60         }
61
62                 /** List of entities. Overridden by EntityCollection. */
63                 public function get entities():Array {
64                         return [this];
65                 }
66
67         /** Most recent modification of the entity (from OSM API). */
68         public function get timestamp():String {
69             return _timestamp;
70         }
71
72         /** The username who last edited this entity (from OSM API). */
73         public function get user():String {
74             return _user;
75         }
76
77                 /** Connection to which this entity belongs. */
78                 public function get connection():Connection {
79                         return _connection;
80                 }
81
82         /** Set a bunch of properties in one hit. Implicitly makes entity not deleted. */
83         public function updateEntityProperties(version:uint, tags:Object, loaded:Boolean, parentsLoaded:Boolean, uid:Number, timestamp:String, user:String):void {
84             _version=version; this.tags=tags; _loaded=loaded; this.parentsLoaded=parentsLoaded; _uid = uid; _timestamp = timestamp; _user = user;
85             deleted=false;
86         }
87
88         /** Assign a new ID and version. */
89         public function renumber(newID:Number, newVersion:uint):void {
90             this._id = newID;
91             this._version = newVersion;
92         }
93
94                 // Tag-handling methods
95
96         /** Whether the entity has > 0 tags. */
97         public function hasTags():Boolean {
98             for (var key:String in tags)
99                 return true;
100             return false;
101         }
102
103         /** Whether the entity has any tags other than meta-tags (attribution, created_by, source, tiger:...) */
104         public function hasInterestingTags():Boolean {
105             for (var key:String in tags) {
106               if (key != "attribution" && key != "created_by" && key != "source" && key.indexOf('tiger:') != 0) {
107                 //trace(key);
108                 return true;
109               }
110             }
111             return false;
112         }
113
114                 /** Compare tags between two entities. */
115                 public function sameTags(entity:Entity):Boolean {
116                         var o:Object=entity.getTagsHash();
117                         for (var k:String in tags)
118                                 if (!o[k] || o[k]!=tags[k]) return false;
119                         for (k in o)
120                                 if (!tags[k] || tags[k]!=o[k]) return false;
121                         return true;
122                 }
123
124         /** Rough function to detect entities untouched since TIGER import. */
125         public function isUneditedTiger():Boolean {
126             // todo: make this match the rules from the tiger edited map
127             // http://github.com/MapQuest/TIGER-Edited-map/blob/master/inc/layer-tiger.xml.inc
128             if (this is Way && (uid == 7168 || uid == 15169 || uid == 20587)) {//todo fixme etc
129               return true;
130             }
131             return false;
132         }
133
134         /** Retrieve a tag by key. */
135         public function getTag(key:String):String {
136             return tags[key];
137         }
138
139                 /** Retrieve a key matching a regex. */
140                 public function getTagByRegex(regex:RegExp):String {
141                         for (var k:String in tags) {
142                                 if (k.match(regex)) return tags[k];
143                         }
144                         return null;
145                 }
146
147         /** @return true if there exists key=value */
148         public function tagIs(key:String,value:String):Boolean {
149             if (!tags[key]) { return false; }
150             return tags[key]==value;
151         }
152
153         /** Set key=value, with optional undoability.
154          * @param key Name of key to set
155          * @param value Value to set tag to
156          * @param performAction Single-argument function to pass a SetTagAction to.
157          * @example setTag("highway", "residential", MainUndoStack.getGlobalStack().addAction);
158          */
159         public function setTag(key:String, value:String, performAction:Function):void {
160             performAction(new SetTagAction(this, key, value));
161         }
162
163         /** Change oldKey=[value] to newKey=[value], with optional undoability.
164          * @param oldKey Name of key to rename
165          * @param newKey New name of key
166          * @param performAction Single-argument function to pass a SetTagKeyAction to.
167          * @example renameTag("building", "amenity", MainUndoStack.getGlobalStack().addAction);
168          */
169         public function renameTag(oldKey:String, newKey:String, performAction:Function):void {
170             performAction(new SetTagKeyAction(this, oldKey, newKey));
171         }
172
173         public function getTagList():TagList {
174             return new TagList(tags);
175         }
176
177         /** Returns an object that duplicates the tags on this entity. */
178         public function getTagsCopy():Object {
179             var copy:Object = {};
180             for (var key:String in tags )
181                 copy[key] = tags[key];
182             return copy;
183         }
184
185         public function getTagsHash():Object {
186             // hm, not sure we should be doing this, but for read-only purposes
187             // it's faster than using getTagsCopy
188             return tags;
189         }
190
191         /** Returns an array that duplicates the tags on this entity. */
192         public function getTagArray():Array {
193             var copy:Array = [];
194             for (var key:String in tags )
195                 copy.push(new Tag(this, key, tags[key]));
196             return copy;
197         }
198
199                 /** Change entity status. */
200                 public function setStatus(s:String):void {
201                         if (s=='') s=null;
202                         if (s==status) return;
203                         status=s;
204                         dispatchEvent(new EntityEvent(Connection.STATUS_CHANGED,this));
205                 }
206
207                 // Clean/dirty methods
208
209         /** Check if entity is modified since last markClean(). */
210         public function get isDirty():Boolean {
211             return modified;
212         }
213
214         /** Reset modified flag. You should not be calling this directly, instead you should be calling markClean from your UndoableEntityAction */
215         public function markClean():void {
216             modified = false;
217         }
218
219         /** Set entity as modified. You should not be calling this directly, instead you should be calling markDirty from your UndoableEntityAction */
220         internal function markDirty():void {
221             modified = true;
222         }
223
224
225         /** Delete entity - must be overridden. */
226         public function remove(performAction:Function):void {
227             // to be overridden
228         }
229
230         /** Whether entity is marked deleted. */
231         public function isDeleted():Boolean {
232             return deleted;
233         }
234
235         /** Mark entity as deleted. */
236         public function setDeletedState(isDeleted:Boolean):void {
237             deleted = isDeleted;
238             if (this is Node) {
239                 var n:Node = Node(this);
240                 if (isDeleted) {
241                     connection.removeDupe(n);
242                 } else {
243                     connection.addDupe(n);
244                 }
245             }
246         }
247
248         /** Whether entity is "empty" - to be overridden by subclass. */
249         internal function isEmpty():Boolean {
250             return false;
251         }
252
253         /** Free up memory by converting entity to a dummy entity, for entities that we no longer need
254         *  but which are part of a still-in-memory relation */
255         public function nullify():void {
256             nullifyEntity();
257         }
258
259         /** Implement nullifybehaviour: delete tags, etc. */
260         protected function nullifyEntity():void {
261             // this is the common nullify behaviour for all entity types (we'd call this by super() if ActionScript let us)
262             _version=0;
263             _loaded=false;
264             tags={};
265         }
266
267
268         public function within(left:Number,right:Number,top:Number,bottom:Number):Boolean {
269             return true;        // to be overridden
270         }
271
272         public function removeFromParents(performAction:Function):void {
273             for (var o:Object in parents) {
274                 if (o is Relation) { Relation(o).removeMember(this, performAction); }
275                 else if (o is Way) { Way(o).removeNode(Node(this), performAction); }
276                 if (o.isEmpty()) { o.remove(performAction); }
277             }
278         }
279
280         // Parent handling
281
282         /** Create parent link from this entity to another. */
283         public function addParent(parent:Entity):void {
284             parents[parent]=true;
285
286             if ( parent is Relation )
287                 dispatchEvent(new RelationMemberEvent(Connection.ADDED_TO_RELATION, this, parent as Relation, -1));
288         }
289
290         /** Remove parent link. */
291         public function removeParent(parent:Entity):void {
292             delete parents[parent];
293
294             if ( parent is Relation )
295                 dispatchEvent(new RelationMemberEvent(Connection.REMOVED_FROM_RELATION, this, parent as Relation, -1));
296         }
297
298         /** Get array of all Ways of which this object (presumably a node) is a child. */
299         public function get parentWays():Array {
300             var a:Array=[];
301             for (var o:Object in parents) {
302                 if (o is Way) { a.push(o); }
303             }
304             return a;
305         }
306
307         /** Whether this entity has any parents. */
308         public function get hasParents():Boolean {
309             for (var o:Object in parents) { return true; }
310             return false;
311         }
312
313         /** Whether this entity has any parents that are Ways. */
314         public function get hasParentWays():Boolean {
315             for (var o:Object in parents) {
316                 if (o is Way) { return true; }
317             }
318             return false;
319         }
320
321         /** How many parents are Ways? */
322         public function get numParentWays():uint {
323             var i:uint=0;
324             for (var o:Object in parents) {
325                 if (o is Way) { i++; }
326             }
327             return i;
328         }
329
330         /** All parents that are Relations */
331         public function get parentRelations():Array {
332             var a:Array=[];
333             for (var o:Object in parents) {
334                 if (o is Relation) { a.push(o); }
335             }
336             return a;
337         }
338
339         /** Returns parents that are relations, and of the specified type, and of which this entity is the correct role (if provided).
340         *
341         * @example entity.findParentRelationsOfType('multipolygon','inner');
342         */
343         public function findParentRelationsOfType(type:String, role:String=null):Array {
344             var a:Array=[];
345             for (var o:Object in parents) {
346                 if (o is Relation && Relation(o).tagIs('type',type) && (role==null || Relation(o).hasMemberInRole(this,role))) {
347                     a.push(o);
348                 }
349             }
350             return a;
351         }
352
353                 public function getRelationMemberships():Array {
354                         var memberships:Array = [];
355                         for each( var rel:Relation in parentRelations ) {
356                                 for each( var memberIndex:int in rel.findEntityMemberIndexes(this)) {
357                                         memberships.push({
358                                                 relation: rel,
359                                                 id: rel.id,
360                                                 index: memberIndex,
361                                                 role: rel.getMember(memberIndex).role,
362                                                 description: rel.getDescription(),
363                                                 id_idx: rel.id + "/"+memberIndex });
364                                 }
365                         }
366                         return memberships;
367                 }
368
369         /** How many parents does this entity have that satisfy the "within" constraint? */
370         public function countParentObjects(within:Object):uint {
371             var count:uint=0;
372             for (var o:Object in parents) {
373                 if (o.getType()==within.entity && o.getTag(within.k)) {
374                     if (within.v && within.v!=o.getTag(within.k)) { break; }
375                     if (within.role && !Relation(o).hasMemberInRole(this,within.role)) { break; }
376                     count++;
377                 }
378             }
379             return count;
380         }
381
382         /** All parents of this entity. */
383         public function get parentObjects():Array {
384             var a:Array=[];
385             for (var o:Object in parents) { a.push(o); }
386                 return a;
387             }
388
389         /** Whether 'entity' is a parent of this Entity. */
390         public function hasParent(entity:Entity):Boolean {
391             return parents[entity] == true;
392         }
393
394             /** Returns all relations that this Entity is part of, as array of {relation, position, role}, sorted by position. */
395             public function get memberships():Array {
396                         var list:Array=[];
397                         for (var o:Object in parents) {
398                                 if (o is Relation) {
399                                         for (var i:uint=0; i<o.length; i++) {
400                                                 if (o.getMember(i).entity==this) {
401                                                         list.push( { relation:o, position:i, role: o.getMember(i).role } );
402                                                 }
403                                         }
404                                 }
405                         }
406             // it's useful to return in a sorted order, even if the relations are interleaved
407             // e.g. [{r0 p1},{r1 p1},{r0 p4}]
408                         return list.sortOn("position");
409                 }
410
411
412
413                 /** Temporarily prevent redrawing of the object. */
414                 public function suspend():void {
415                         dispatchEvent(new EntityEvent(Connection.SUSPEND_REDRAW, this));
416                 }
417                 /** Resume redrawing of the object */
418                 public function resume():void {
419                         dispatchEvent(new EntityEvent(Connection.RESUME_REDRAW, this));
420                 }
421
422
423                 /** Basic description of Entity - should be overriden by subclass. */
424                 public function getDescription():String {
425                         var basic:String=this.getType()+" "+_id;
426                         if (tags['ref'] && tags['name']) { return tags['ref']+' '+tags['name']+' ('+basic+')'; }
427                         if (tags['ref']) { return tags['ref']+' ('+basic+')'; }
428                         if (tags['name']) { return tags['name']+' ('+basic+')'; }
429                         return basic;
430                 }
431
432         /** The type of Entity (node, way etc). By default, returns ''. */
433         public function getType():String {
434             return '';
435         }
436
437                 /** Compare type against supplied string */
438                 public function isType(str:String):Boolean {
439                         return getType()==str;
440                 }
441                 
442         /** Copy tags from another entity into this one, creating "key=value1; value2" pairs if necessary.
443         * * @return Array of keys that require manual merging, in order to warn the user. */ 
444         public function mergeTags(source: Entity, performAction:Function):Boolean {
445             var sourcetags:Object = source.getTagsHash();
446             var conflict:Boolean = false;
447             for (var k:String in sourcetags) {
448                 var v1:String = tags[k];
449                 var v2:String = sourcetags[k];
450                 if ( v1 && v1 != v2) {
451                     setTag(k, v1+"; "+v2, performAction);
452                     conflict=true;
453                 } else {
454                     setTag(k, v2, performAction);
455                 }
456             }
457             return conflict;
458         }
459
460     }
461
462 }
463