Support slightly more complicated variable substitution in
[potlatch2.git] / net / systemeD / potlatch2 / mapfeatures / Feature.as
1 package net.systemeD.potlatch2.mapfeatures {
2     import flash.events.Event;
3     import flash.events.EventDispatcher;
4     import flash.net.*;
5     import flash.utils.ByteArray;
6     
7     import mx.core.BitmapAsset;
8     import mx.graphics.codec.PNGEncoder;
9     
10     import net.systemeD.halcyon.ImageBank;
11     import net.systemeD.halcyon.connection.Entity;
12     import net.systemeD.potlatch2.utils.CachedDataLoader;
13
14
15     /** 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
16      * entities can be recognised. It also contains optional keys, with associated editing controls, that are defined as being appropriate
17      * for the feature. */
18         public class Feature extends EventDispatcher {
19         private var mapFeatures:MapFeatures;
20         private var _xml:XML;
21         // match ${foo|bar|baz|...} - see makeHTMLIcon()
22         private static var variablesPattern:RegExp = /\$\{([^|}]+)\|?([^|}]+)?\|?([^|}]+)?\|?([^|}]+)?\|?([^|}]+)?\|?([^}]+)?\}/g;
23         private var _tags:Array;
24         private var _withins:Array;
25         private var _editors:Array;
26
27         [Embed(source="../../../../embedded/missing_icon.png")]
28         [Bindable]
29         public var missingIconCls:Class;
30
31
32         /** Create this Feature from an XML subtree. */
33         public function Feature(mapFeatures:MapFeatures, _xml:XML) {
34             this.mapFeatures = mapFeatures;
35             this._xml = _xml;
36             parseConditions();
37             parseEditors();
38         }
39
40         private function parseConditions():void {
41             _tags = [];
42            _withins = [];
43
44                         // parse tags
45             for each(var tag:XML in definition.tag) {
46                 _tags.push( { k:String(tag.@k), v:String(tag.@v), vmatch:String(tag.@vmatch)} );
47             }
48
49                         // parse 'within'
50             for each(var within:XML in definition.within) {
51                                 var obj:Object= { entity:within.@entity, k:within.@k };
52                                 if (within.attribute('v'      ).length()>0) { obj['v'      ]=within.@v;       }
53                                 if (within.attribute('minimum').length()>0) { obj['minimum']=within.@minimum; }
54                                 if (within.attribute('role'   ).length()>0) { obj['role'   ]=within.@role;    }
55                 _withins.push(obj);
56             }
57         }
58
59         private function parseEditors():void {
60             _editors = new Array();
61
62             addEditors(definition);
63
64             _editors.sortOn(["sortOrder", "name"], [Array.DESCENDING | Array.NUMERIC, Array.CASEINSENSITIVE]);
65         }
66
67         private function addEditors(xml:XML):void {
68             var inputXML:XML;
69
70             for each(var inputSetRef:XML in xml.inputSet) {
71                 var setName:String = String(inputSetRef.@ref);
72                 // Go on then, someone tell me why this stopped working. Namespaces?:
73                 //for each (inputXML in mapFeatures.definition.inputSet.(@id == setName)) {
74                 for each (inputXML in mapFeatures.definition.inputSet) {
75                     if (inputXML.@id == setName) {
76                         addEditors(inputXML);
77                     }
78                 }
79             }
80
81             for each(inputXML in xml.input) {
82                 addEditor(inputXML);
83             }
84         }
85
86         private function addEditor(inputXML:XML):void {
87             var inputType:String = inputXML.@type;
88             var presenceStr:String = inputXML.@presence;
89             var sortOrderStr:String = inputXML.@priority;
90             var editor:EditorFactory = EditorFactory.createFactory(inputType, inputXML);
91             if ( editor != null ) {
92                 editor.presence = Presence.getPresence(presenceStr);
93                 editor.sortOrder = editor.getPriority(sortOrderStr);
94                 _editors.push(editor);
95             }
96         }
97
98         /** List of editing controls associated with this feature. */
99         public function get editors():Array {
100             return _editors;
101         }
102
103         /** The XML subtree that this feature was loaded from. */
104         public function get definition():XML {
105             return _xml;
106         }
107
108         [Bindable(event="nameChanged")]
109         /** The human-readable name of the feature (name), or null if none. */
110         public function get name():String {
111                         if (_xml.attribute('name').length()>0) { return _xml.@name; }
112                         return null;
113         }
114
115         [Bindable(event="descriptionChanged")]
116         /** The human-readable description of the feature, or null if none. */
117         public function get description():String {
118             var desc:XMLList = _xml.description
119             if (desc.length()>0) { return desc[0]; }
120             return null;
121         }
122
123         /** Returns the icon defined for the feature.
124         * This uses the "image" property of the feature's icon element. If no image property is defined, returns a default "missing icon".
125         */
126         [Bindable(event="imageChanged")]
127         public function get image():ByteArray {
128             return getImage();
129         }
130
131         /** Returns the drag+drop override-icon defined for the feature.
132         * This uses the "dnd" property of the feature's icon element, or if there is no override-icon it falls back to the standard image.
133         */
134         [Bindable(event="imageChanged")]
135         public function get dndimage():ByteArray {
136             return getImage(true);
137         }
138
139         /** Fetches the feature's image, as defined by the icon element in the feature definition.
140         * @param dnd if true, overrides the normal image and returns the one defined by the dnd property instead. */
141         private function getImage(dnd:Boolean = false):ByteArray {
142             var icon:XMLList = _xml.icon;
143             var imageURL:String;
144
145             if ( dnd && icon.length() > 0 && icon[0].hasOwnProperty("@dnd") ) {
146                 imageURL = icon[0].@dnd;
147             } else if ( icon.length() > 0 && icon[0].hasOwnProperty("@image") ) {
148                 imageURL = icon[0].@image;
149             }
150
151             if ( imageURL ) {
152                                 if (ImageBank.getInstance().hasImage(imageURL)) {
153                                         return ImageBank.getInstance().getAsByteArray(imageURL)
154                                 } else {
155                         return CachedDataLoader.loadData(imageURL, imageLoaded);
156                                 }
157             }
158             var bitmap:BitmapAsset = new missingIconCls() as BitmapAsset;
159             return new PNGEncoder().encode(bitmap.bitmapData);
160         }
161         
162         /** Can this feature be drag-and-dropped from the side panel? By default, any "point" feature can,
163         *   unless it has <point draganddrop="no"/> 
164         * */
165         public function canDND():Boolean {
166                 var point:XMLList = _xml.elements("point");
167                 return point.length() > 0 && !(XML(point[0]).attribute("draganddrop")[0] == "no");
168         }
169
170         private function imageLoaded(url:String, data:ByteArray):void {
171             dispatchEvent(new Event("imageChanged"));
172         }
173
174         public function htmlDetails(entity:Entity):String {
175             var icon:XMLList = _xml.icon;
176             return makeHTMLIcon(icon, entity);
177         }
178
179         /** Convert the contents of the "icon" tag as an HTML string, with variable substitution:
180         *   ${highway} shows the value of the highway key
181         *   ${name|operator|network} - if there's no name value, show operator, or network, or nothing.
182         *   (${ref}) - renders as nothing if $ref is valueless.
183         */
184         public static function makeHTMLIcon(icon:XMLList, entity:Entity):String {
185             if ( icon == null )
186                 return "";
187
188             var txt:String = icon.children().toXMLString();
189             // Args to this function: "string matched", "substring 1", "substring 2"..., index of match, whole string
190             var replaceTag:Function = function():String {
191                 var matchnum=0;
192                 var args=arguments;
193                 var value:String = null;
194                 while ((value == null || value == "") && matchnum < args.length - 3  ) {
195                   value = entity.getTag(args[matchnum + 1]);
196                   matchnum++;
197                 }
198                 return value == null ? "" : htmlEscape(value);
199             };
200             txt = txt.replace(variablesPattern, replaceTag);
201             // a slightly hacky way of making "${name} (${ref})" look ok even if ref is undefined.
202             txt = txt.replace("()", ""); 
203             return txt;
204         }
205
206         /** Basic HTML escaping. */
207         public static function htmlEscape(str:String):String {
208             var newStr:String = str.replace(/&/g, "&amp;");
209             newStr = newStr.replace(/</g, "&lt;");
210             newStr = newStr.replace(/>/g, "&gt;");
211             newStr = newStr.replace(/"/g, "&quot;");    // "
212             newStr = newStr.replace(/'/g, "&apos;");    // '
213             return newStr;
214         }
215
216         /** Whether this feature belongs to the given category or not, as defined by its definition in the XML file. */
217         public function isInCategory(category:String):Boolean {
218             var cats:XMLList = _xml.category;
219             if ( cats.length() == 0 )
220                 return false;
221
222             for each( var cat:XML in cats )
223                 if ( cat.text()[0] == category )
224                     return true;
225             return false;
226         }
227
228
229         /** List of {k, v} pairs that define the feature. */
230         public function get tags():Array {
231             return _tags;
232         }
233
234         /** List of "withins" which further restrict the applicability of the feature. Each within is a {entity, k, ?v, ?minimum, ?role} object. */
235         public function get withins():Array {
236             return _withins;
237         }
238
239         /** The first category that the feature belongs to, as defined by the order of the map features XML file. */
240         public function findFirstCategory():Category {
241             for each( var cat:Category in mapFeatures.categories ) {
242                 if ( isInCategory(cat.id) )
243                     return cat;
244             }
245             return null;
246         }
247
248         /** Whether the feature is of the given type (point, line/area, relation). */
249         public function isType(type:String):Boolean {
250             if (type=='area') {
251                             return (_xml.elements(type).length() > 0) || (_xml.elements('line').length() > 0);
252             } else {
253                             return _xml.elements(type).length() > 0;
254                         }
255         }
256
257         /** Whether there is a help string defined or one can be derived from tags. */
258         public function hasHelpURL():Boolean {
259             return _xml.help.length() > 0 || _tags.length > 0;
260         }
261
262         /** The defined help string, if any. If none, generate one from tags on the feature, pointing to the OSM wiki. */
263         public function get helpURL():String {
264                 if (_xml.help.length() > 0)
265                 return _xml.help;
266             else if (_tags.length > 0) {
267                 if (_tags[0].v == "*")
268                     return "http://www.openstreetmap.org/wiki/Key:" + _tags[0].k;
269                 else
270                     return "http://www.openstreetmap.org/wiki/Tag:" + _tags[0].k + "=" + _tags[0].v;                
271             } else
272                 return "";
273
274         }
275     }
276 }
277