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