Merge branch 'master' into history
[potlatch2.git] / net / systemeD / potlatch2 / mapfeatures / MapFeatures.as
1 package net.systemeD.potlatch2.mapfeatures {
2
3     import flash.events.Event;
4     import flash.events.EventDispatcher;
5     import flash.net.*;
6     
7     import net.systemeD.halcyon.NestedXMLLoader;
8     import net.systemeD.halcyon.connection.*;
9
10     /** All the information about all available map features that can be selected by the user or matched against entities in the map.
11     * The list of map features is populated from an XML file the first time the MapFeatures instance is accessed.
12     *
13     * <p>There are four "types" of features: point, line, area, relation. However, the autocomplete functions refer to these as node,
14     * way (line/area) and relation.</p>
15     */
16         public class MapFeatures extends EventDispatcher {
17         private static var instance:MapFeatures;
18
19         /** Instantiates MapFeatures by loading it if required. */
20         public static function getInstance():MapFeatures {
21             if ( instance == null ) {
22                 instance = new MapFeatures();
23                 instance.loadFeatures();
24             }
25             return instance;
26         }
27
28         private var xml:XML = null;
29         private var _features:Array = null;
30         private var _categories:Array = null;
31 //              private var _keys:Array = null;
32                 private var _tags:Object = null;
33
34         /** Loads list of map features from XML file which it first retrieves. */
35         protected function loadFeatures():void {
36             var xmlLoader:NestedXMLLoader = new NestedXMLLoader();
37             xmlLoader.addEventListener(Event.COMPLETE, onFeatureLoad);
38             xmlLoader.load("map_features.xml");
39         }
40
41         /** The loaded source XML file itself. */
42         internal function get definition():XML {
43             return xml;
44         }
45
46         /** Load XML file, then trawl over it, setting up convenient indexes into the list of map features. */
47         private function onFeatureLoad(event:Event):void {
48                         var f:Feature;
49
50             xml = NestedXMLLoader(event.target).xml;
51             _features = [];
52             _tags = { relation:{}, way:{}, node:{} };
53
54             for each(var feature:XML in xml..feature) {
55                 f=new Feature(this,feature);
56                 _features.push(f);
57                 for each (var tag:Object in f.tags) {
58                     if (f.isType('line') || f.isType('area')) { addToTagList('way',tag); }
59                     if (f.isType('relation'))                 { addToTagList('relation',tag); }
60                     if (f.isType('point'))                    { addToTagList('node',tag); }
61                 }
62
63                 for each (var inputSet:XML in feature..inputSet) {
64                     var inputSetName:String=inputSet.@ref;      // Flex 4 breaks if this is included directly in the line below
65                     tagsFromInputSet(definition.inputSet.(@id == inputSetName), f);
66                 }
67             }
68
69             _categories = new Array();
70             for each(var catXML:XML in xml.category) {
71                 if ( catXML.child("category").length() == 0 )
72                   _categories.push(new Category(this, catXML.@name, catXML.@id, _categories.length));
73             }
74             dispatchEvent(new Event("featuresLoaded"));
75         }
76
77         private function tagsFromInputSet(inputSet:XMLList, f:Feature):void {
78             for each (var input:XML in inputSet.input) {
79                 // Take all the k/v pairs from inputs that have choice
80                 for each (var choice:XML in input..choice ) {
81                     if (f.isType('line') || f.isType('area')) { addToTagList('way', {k:String(input.@key), v:String(choice.@value)}); }
82                     if (f.isType('relation'))                 { addToTagList('relation',{k:String(input.@key), v:String(choice.@value)}); }
83                     if (f.isType('point'))                    { addToTagList('node',{k:String(input.@key), v:String(choice.@value)}); }
84                 }
85
86                 if (input.@type == 'freetext') {
87                     if (f.isType('line') || f.isType('area')) { addToTagList('way', {k:String(input.@key), v:''}); }
88                     if (f.isType('relation'))                 { addToTagList('relation',{k:String(input.@key), v:''}); }
89                     if (f.isType('point'))                    { addToTagList('node',{k:String(input.@key), v:''}); }
90                 }
91             }
92
93             // inputSets can have their own inputSets, so recurse
94             for each (var i:XML in inputSet.inputSet) {
95                 tagsFromInputSet(definition.inputSet.(@id == String(i.@ref)), f);
96             }
97         }
98
99         /** Add one item to tagList index, which will end up being a list like: ["way"]["highway"]["residential"] */
100                 private function addToTagList(type:String,tag:Object):void {
101                         if (tag.v=='*') { return; }
102                         if (!_tags[type][tag.k]) { _tags[type][tag.k]=new Array(); }
103                         if (_tags[type][tag.k].indexOf(tag.v)==-1) { _tags[type][tag.k].push(tag.v); }
104                 }
105
106         /** Indicates whether the XML file has finished being loaded. */
107         public function hasLoaded():Boolean {
108             return xml != null;
109         }
110
111         /** Find the first Feature (template) that matches the given Entity (actual existing object in the map).
112          *
113          * This is done to provide appropriate editing controls that correspond to the selected Entity.
114          *
115          * @param entity The Entity to try and match against.
116          * @return The first suitable Feature, or null. */
117
118         public function findMatchingFeature(entity:Entity):Feature {
119             if ( xml == null )
120                 return null;
121
122             for each(var feature:Feature in features) {
123                 var match:Boolean = true;
124
125                 // check for matching tags
126                 // the "match" attribute lets you specify other values that will match this feature
127                 // but won't affect the default value assigned. format is "*" or a regex.
128                 for each(var tag:Object in feature.tags) {
129                     var entityTag:String = entity.getTag(tag.k);
130                     if (entityTag == null) { match = false; break; }
131                     match = 
132                         tag.v == entityTag 
133                      || tag.v == "*"
134                      || tag.vmatch == "*"
135                      || tag.vmatch != "" && entityTag.match(new RegExp("^" + tag.vmatch + "$"));
136                     if ( !match ) break;
137                 }
138
139                 // check for matching withins
140                 if (match) {
141                     for each (var within:Object in feature.withins) {
142                         match = entity.countParentObjects(within) >= (within.minimum ? within.minimum : 1);
143                         if (!match) { break; }
144                     }
145                 }
146
147                 if (match) {
148                     return feature;
149                 }
150             }
151             return null;
152         }
153
154
155         /** Array of every Category found in the map features file. */
156         [Bindable(event="featuresLoaded")]
157         public function get categories():Array {
158             if ( xml == null )
159                 return null;
160             return _categories;
161         }
162
163         /** Categories that contain at least one Feature corresponding to a certain type, such as "area" or "point".
164         *
165         * @return Filtered Array of Category objects, possibly empty. null if XML file is not yet processed.
166         */
167         [Bindable(event="featuresLoaded")]
168         public function getCategoriesForType(type:String):Array {
169             if ( xml == null )
170                 return null;
171             if ( type == null || type == "" )
172                 return []; //_categories;
173
174             var filteredCategories:Array = new Array();
175             for each( var cat:Category in _categories ) {
176                 if ( cat.getFeaturesForType(type).length > 0 )
177                     filteredCategories.push(cat);
178             }
179             return filteredCategories;
180         }
181
182         /** All features.
183         *
184         * @return null if XML file not yet processed. */
185         [Bindable(event="featuresLoaded")]
186         public function get features():Array {
187             if ( xml == null )
188                 return null;
189             return _features;
190         }
191
192         /** All Features of type "point".
193         *
194         * @return null if XML file not yet processed.
195         */
196         [Bindable(event="featuresLoaded")]
197         public function get pois():Array {
198             if (xml == null )
199                 return null;
200             var pois:Array = [];
201             var counter:int = 0;
202             for each ( var feature:Feature in _features ) {
203                   if (feature.isType("point")) {
204                   pois.push(feature);
205                   }
206             }
207             return pois;
208         }
209
210         /** A list of all Keys for all features of the given type, sorted.
211          * @example <listing version="3.0">getAutoCompleteKeys ("way")</listing>
212          * Returns: [{name: "building"}, {name: "highway"}...]
213          */
214         [Bindable(event="featuresLoaded")]
215         public function getAutoCompleteKeys(type:String):Array {
216             var list:Array=[];
217             var a:Array=[];
218
219             for (var k:String in _tags[type]) { list.push(k); }
220             list.sort();
221
222             for each (k in list) { a.push( { name: k } ); }
223             return a;
224         }
225
226         /** Get all the possible values that could go with a given key and type.
227         * TODO: Include values previously entered by the user, but not existent in XML file.
228         *
229         * @example <listing version="3.0">getAutoCompleteValues("way", "highway")</listing>
230         * Returns: [{name: "motorway"}, {name: "residential"}...]
231         */
232         [Bindable(event="featuresLoaded")]
233         public function getAutoCompleteValues(type:String,key:String):Array {
234             var a:Array=[];
235             if (_tags[type][key]) {
236                 _tags[type][key].sort();
237                 for each (var v:String in _tags[type][key]) { a.push( { name: v } ); }
238             }
239             return a;
240         }
241
242     }
243
244 }
245
246