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