7466e2e76817e3c246249a91289db4f70f662341
[potlatch2.git] / net / systemeD / potlatch2 / mapfeatures / Feature.as
1 package net.systemeD.potlatch2.mapfeatures {
2
3     import flash.events.Event;
4     import flash.events.EventDispatcher;
5     import flash.net.*;
6     import flash.utils.ByteArray;
7     
8     import mx.core.BitmapAsset;
9     import mx.graphics.codec.PNGEncoder;
10     
11     import net.systemeD.halcyon.connection.Entity;
12     import net.systemeD.potlatch2.utils.CachedDataLoader;
13
14         /** A "map feature" is sort of a template for a map entity. It consists of a few crucial key/value pairs that define the feature, so that
15          * entities can be recognised. It also contains optional keys, with associated editing controls, that are defined as being appropriate
16          * for the feature. */
17         public class Feature extends EventDispatcher {
18         private var mapFeatures:MapFeatures;
19         private var _xml:XML;
20         private static var variablesPattern:RegExp = /[$][{]([^}]+)[}]/g;
21         private var _tags:Array;
22         private var _withins:Array;
23         private var _editors:Array;
24
25         [Embed(source="../../../../embedded/missing_icon.png")]
26         [Bindable]
27         public var missingIconCls:Class;
28
29
30         /** Create this Feature from an XML subtree. */
31         public function Feature(mapFeatures:MapFeatures, _xml:XML) {
32             this.mapFeatures = mapFeatures;
33             this._xml = _xml;
34             parseConditions();
35             parseEditors();
36         }
37
38         private function parseConditions():void {
39             _tags = [];
40            _withins = [];
41
42                         // parse tags
43             for each(var tag:XML in definition.tag) {
44                 _tags.push( { k:String(tag.@k), v:String(tag.@v), vmatch:String(tag.@vmatch)} );
45             }
46
47                         // parse 'within'
48             for each(var within:XML in definition.within) {
49                                 var obj:Object= { entity:within.@entity, k:within.@k };
50                                 if (within.attribute('v'      ).length()>0) { obj['v'      ]=within.@v;       }
51                                 if (within.attribute('minimum').length()>0) { obj['minimum']=within.@minimum; }
52                                 if (within.attribute('role'   ).length()>0) { obj['role'   ]=within.@role;    }
53                 _withins.push(obj);
54             }
55         }
56
57         private function parseEditors():void {
58             _editors = new Array();
59
60             addEditors(definition);
61
62             _editors.sortOn(["sortOrder", "name"], [Array.DESCENDING | Array.NUMERIC, Array.CASEINSENSITIVE]);
63         }
64
65         private function addEditors(xml:XML):void {
66             var inputXML:XML;
67
68             for each(var inputSetRef:XML in xml.inputSet) {
69                 var setName:String = String(inputSetRef.@ref);
70                 for each (inputXML in mapFeatures.definition.inputSet.(@id==setName)) {
71                     addEditors(inputXML);
72                 }
73             }
74
75             for each(inputXML in xml.input) {
76                 addEditor(inputXML);
77             }
78         }
79
80         private function addEditor(inputXML:XML):void {
81             var inputType:String = inputXML.@type;
82             var presenceStr:String = inputXML.@presence;
83             var sortOrderStr:String = inputXML.@priority;
84 //          _tags.push( { k:String(inputXML.@key) } ); /* add the key to tags so that e.g. addr:housenumber shows up on autocomplete */
85             var editor:EditorFactory = EditorFactory.createFactory(inputType, inputXML);
86             if ( editor != null ) {
87                 editor.presence = Presence.getPresence(presenceStr);
88                 editor.sortOrder = EditorFactory.getPriority(sortOrderStr);
89                 _editors.push(editor);
90             }
91         }
92
93         /** List of editing controls associated with this feature. */
94         public function get editors():Array {
95             return _editors;
96         }
97
98         /** The XML subtree that this feature was loaded from. */
99         public function get definition():XML {
100             return _xml;
101         }
102
103         [Bindable(event="nameChanged")]
104         /** The human-readable name of the feature (@name), or null if none. */
105         public function get name():String {
106                         if (_xml.attribute('name').length()>0) { return _xml.@name; }
107                         return null;
108         }
109
110         [Bindable(event="imageChanged")]
111         /** An icon for the feature (from icons[0]/@image). If none is defined, return default "missing icon". */
112         public function get image():ByteArray {
113             var icon:XMLList = _xml.icon;
114             var imageURL:String = null;
115             var img:ByteArray;
116
117             
118             if ( icon.length() > 0 && icon[0].hasOwnProperty("@image") )
119                 imageURL = icon[0].@image;
120
121             if ( imageURL != null ) {
122                 img = CachedDataLoader.loadData(imageURL, imageLoaded);
123             }
124             if (img) {
125               return img;
126             }
127             var bitmap:BitmapAsset = new missingIconCls() as BitmapAsset;
128             return new PNGEncoder().encode(bitmap.bitmapData);
129         }
130         
131         /** Can this feature be drag-and-dropped from the side panel? By default, any "point" feature can,
132         *   unless it has <point draganddrop="no"/> 
133         * */
134         public function canDND():Boolean {
135                 var point:XMLList = _xml.elements("point");
136                 return point.length() > 0 && !(XML(point[0]).attribute("draganddrop")[0] == "no");
137         }
138
139         private function imageLoaded(url:String, data:ByteArray):void {
140             dispatchEvent(new Event("imageChanged"));
141         }
142
143         public function htmlDetails(entity:Entity):String {
144             var icon:XMLList = _xml.icon;
145             return makeHTMLIcon(icon, entity);
146         }
147
148         /** Convert the contents of the "icon" tag as an HTML string, with variable substitution. */
149         public static function makeHTMLIcon(icon:XMLList, entity:Entity):String {
150             if ( icon == null )
151                 return "";
152
153             var txt:String = icon.children().toXMLString();
154             var replaceTag:Function = function():String {
155                 var value:String = entity.getTag(arguments[1]);
156                 return value == null ? "" : htmlEscape(value);
157             };
158             txt = txt.replace(variablesPattern, replaceTag);
159             return txt;
160         }
161
162         /** Basic HTML escaping. */
163         public static function htmlEscape(str:String):String {
164             var newStr:String = str.replace(/&/g, "&amp;");
165             newStr = newStr.replace(/</g, "&lt;");
166             newStr = newStr.replace(/>/g, "&gt;");
167             newStr = newStr.replace(/"/g, "&quot;");    // "
168             newStr = newStr.replace(/'/g, "&apos;");    // '
169             return newStr;
170         }
171
172         /** Whether this feature belongs to the given category or not, as defined by its definition in the XML file. */
173         public function isInCategory(category:String):Boolean {
174             var cats:XMLList = _xml.category;
175             if ( cats.length() == 0 )
176                 return false;
177
178             for each( var cat:XML in cats )
179                 if ( cat.text()[0] == category )
180                     return true;
181             return false;
182         }
183
184
185         /** List of {k, v} pairs that define the feature. */
186         public function get tags():Array {
187             return _tags;
188         }
189
190         /** List of "withins" which further restrict the applicability of the feature. Each within is a {entity, k, ?v, ?minimum, ?role} object. */
191         public function get withins():Array {
192             return _withins;
193         }
194
195         /** The first category that the feature belongs to, as defined by the order of the map features XML file. */
196         public function findFirstCategory():Category {
197             for each( var cat:Category in mapFeatures.categories ) {
198                 if ( isInCategory(cat.id) )
199                     return cat;
200             }
201             return null;
202         }
203
204         /** Whether the feature is of the given type (point, line/area, relation). */
205         public function isType(type:String):Boolean {
206             if (type=='area') {
207                             return (_xml.elements(type).length() > 0) || (_xml.elements('line').length() > 0);
208             } else {
209                             return _xml.elements(type).length() > 0;
210                         }
211         }
212
213         /** Whether there is a help string defined or one can be derived from tags. */
214         public function hasHelpURL():Boolean {
215             return _xml.help.length() > 0 || _tags.length > 0;
216         }
217
218         /** The defined help string, if any. If none, generate one from tags on the feature, pointing to the OSM wiki. */
219         public function get helpURL():String {
220                 if (_xml.help.length() > 0)
221                 return _xml.help;
222             else if (_tags.length > 0) {
223                 if (_tags[0].v == "*")
224                     return "http://www.openstreetmap.org/wiki/Key:" + _tags[0].k;
225                 else
226                     return "http://www.openstreetmap.org/wiki/Tag:" + _tags[0].k + "=" + _tags[0].v;                
227             } else
228                 return "";
229
230         }
231     }
232 }
233