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