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