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