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