Enlarge requested bbox by 20% on each side to make for easier panning; keep list...
[potlatch2.git] / net / systemeD / halcyon / MapPaint.as
1 package net.systemeD.halcyon {
2
3         import flash.display.Sprite;
4         import flash.display.DisplayObject;
5         import net.systemeD.halcyon.NodeUI;
6         import net.systemeD.halcyon.WayUI;
7         import net.systemeD.halcyon.connection.*;
8         import net.systemeD.halcyon.styleparser.RuleSet;
9
10         /** Manages the drawing of map entities, allocating their sprites etc. */
11         public class MapPaint extends Sprite {
12                 
13                 /** Access Map object */
14                 public var map:Map;
15                 /** Entities on layers below minlayer will not be shown by this paint object. (Confirm?) */
16                 public var minlayer:int;
17                 /** Entities on layers above maxlayer will not be shown by this paint object. (Confirm?) */
18                 public var maxlayer:int;
19                 /** The MapCSS rules used for drawing entities. */
20                 public var ruleset:RuleSet;                                             
21                 /** WayUI objects attached to Way entities that are currently visible. */
22                 public var wayuis:Object=new Object();                  // sprites for ways and (POI/tagged) nodes
23                 /** NodeUI objects attached to POI/tagged node entities that are currently visible. */
24                 // confirm this - it seems to me all nodes in ways get nodeuis? --Steve B
25                 public var nodeuis:Object=new Object();
26                 /** MarkerUI objects attached to Marker entities that are currently visible. */
27         public var markeruis:Object=new Object();
28         /** Is this a background layer or the core paint object? */
29                 public var isBackground:Boolean = true;
30                 /** Hash of index->position */
31                 public var sublayerIndex:Object={};
32
33                 private const VERYBIG:Number=Math.pow(2,16);
34                 private static const NO_LAYER:int=-99999;               // same as NodeUI
35
36                 // Set up layering
37
38                 /** Creates paint sprites and hit sprites for all layers in range. This object ends up with a series of child sprites
39                  * as follows: p0,p1,p2..px, h0,h1,h2..hx where p are "paint sprites" and "h" are "hit sprites". There is one of each type for each layer.
40                  * <p>Each paint sprite has 4 child sprites (fill, casing, stroke, names). Each hit sprite has 2 child sprites (way hit tests, node hit tests).</p>  
41                  * <p>Thus if layers range from -5 to +5, there will be 11 top level paint sprites followed by 11 top level hit sprites.</p>
42                  * 
43                  * @param map The map to be rendered.
44                  * @param minlayer The lowest layer in that map that will be rendered.
45                  * @param maxlayer The top layer in that map that will be rendered.
46                  * */ 
47                 public function MapPaint(map:Map,minlayer:int,maxlayer:int) {
48                         mouseEnabled=false;
49
50                         this.map=map;
51                         this.minlayer=minlayer;
52                         this.maxlayer=maxlayer;
53                         sublayerIndex[1]=0;
54                         var s:Sprite, l:int;
55
56                         // Add paint sprites
57                         for (l=minlayer; l<=maxlayer; l++) {                    // each layer (10 is +5, 0 is -5)
58                                 s = getPaintSprite();                                           //      |
59                                 s.addChild(getPaintSprite());                           //      | 0 fill
60                                 s.addChild(getPaintSprite());                           //      | 1 casing
61                                 var t:Sprite = getPaintSprite();                        //      | 2 stroke
62                                 t.addChild(getPaintSprite());                           //      |  | sublayer
63                                 s.addChild(t);                                                          //      |  |
64                                 s.addChild(getPaintSprite());                           //      | 3 names
65                                 addChild(s);                                                            //      |
66                         }
67                         
68                         // Add hit sprites
69                         for (l=minlayer; l<=maxlayer; l++) {                    // each layer (21 is +5, 11 is -5)
70                                 s = getHitSprite();                                                     //      |
71                                 s.addChild(getHitSprite());                                     //      | 0 way hit tests
72                                 s.addChild(getHitSprite());                                     //      | 1 node hit tests
73                                 addChild(s);
74                         }
75                 }
76                 
77                 /** Returns the paint surface for the given layer. */
78                 public function getPaintSpriteAt(l:int):Sprite {
79                         return getChildAt(l-minlayer) as Sprite;
80                 }
81
82                 /** Returns the hit sprite for the given layer. */
83                 public function getHitSpriteAt(l:int):Sprite {
84                         return getChildAt((l-minlayer) + (maxlayer-minlayer+1)) as Sprite;
85                 }
86                 
87                 /** Is ruleset loaded? */
88                 public function get ready():Boolean {
89                         if (!ruleset) { return false; }
90                         if (!ruleset.loaded) { return false; }
91                         return true;
92                 }
93
94                 public function sublayer(layer:int,sublayer:Number):Sprite {
95                         var l:DisplayObject;
96                         var o:DisplayObject;
97                         var index:String, ix:Number;
98                         if (!sublayerIndex.hasOwnProperty(sublayer)) {
99                                 // work out which position to add at
100                                 var lowestAbove:Number=VERYBIG;
101                                 var lowestAbovePos:int=-1;
102                                 var indexLength:uint=0;
103                                 for (index in sublayerIndex) {
104                                         ix=Number(index);
105                                         if (ix>sublayer && ix<lowestAbove) {
106                                                 lowestAbove=ix;
107                                                 lowestAbovePos=sublayerIndex[index];
108                                         }
109                                         indexLength++;
110                                 }
111                                 if (lowestAbovePos==-1) { lowestAbovePos=indexLength; }
112                         
113                                 // add sprites
114                                 for (var i:int=minlayer; i<=maxlayer; i++) {
115                                         l=getChildAt(i-minlayer);
116                                         o=(l as Sprite).getChildAt(2);
117                                         (o as Sprite).addChildAt(getPaintSprite(),lowestAbovePos);
118                                 }
119                         
120                                 // update index
121                                 // (we do it in this rather indirect way because if you alter sublayerIndex directly
122                                 //      within the loop, it confuses the iterator)
123                                 var toUpdate:Array=[];
124                                 for (index in sublayerIndex) {
125                                         ix=Number(index);
126                                         if (ix>sublayer) { toUpdate.push(index); }
127                                 }
128                                 for each (index in toUpdate) { sublayerIndex[index]++; }
129                                 sublayerIndex[sublayer]=lowestAbovePos;
130                         }
131
132                         l=getChildAt(layer-minlayer);
133                         o=(l as Sprite).getChildAt(2);
134                         return ((o as Sprite).getChildAt(sublayerIndex[sublayer]) as Sprite);
135                 }
136
137         /**
138         * Update, and if necessary, create / remove UIs for the given objects.
139         * The object is effectively lists of objects split into inside/outside pairs, e.g.
140         * { waysInside: [], waysOutside: [] } where each is a array of entities either inside
141         * or outside this current view window. UIs for the entities on "inside" lists will be created if necessary.
142         * Flags control redrawing existing entities and removing UIs from entities no longer in view.
143         *
144         * @param o The object containing all the relevant entites.
145         * @param redraw If true, all UIs for entities on "inside" lists will be redrawn
146         * @param remove If true, all UIs for entites on "outside" lists will be removed. The purgable flag on UIs
147                         can override this, for example for selected objects.
148         * fixme? add smarter behaviour for way nodes - remove NodeUIs from way nodes off screen, create them for ones
149         * that scroll onto screen (for highlights etc)
150         */
151                 public function updateEntityUIs(o:Object, redraw:Boolean, remove:Boolean):void {
152                         var way:Way, poi:Node, marker:Marker;
153
154                         for each (way in o.waysInside) {
155                                 if (!wayuis[way.id]) { createWayUI(way); }
156                                 else if (redraw) { wayuis[way.id].recalculate(); wayuis[way.id].redraw(); }
157                                 else wayuis[way.id].updateHighlights();//dubious
158                         }
159
160                         if (remove) {
161                                 for each (way in o.waysOutside) {
162                                         if (wayuis[way.id] && !wayuis[way.id].purgable) {
163                                                 if (redraw) { wayuis[way.id].recalculate(); wayuis[way.id].redraw(); }
164                                         } else {
165                                                 deleteWayUI(way);
166                                         }
167                                 }
168                         }
169
170                         for each (poi in o.poisInside) {
171                                 if (!nodeuis[poi.id]) { createNodeUI(poi); }
172                                 else if (redraw) { nodeuis[poi.id].redraw(); }
173                         }
174
175                         if (remove) {
176                                 for each (poi in o.poisOutside) { 
177                                         if (nodeuis[poi.id] && !nodeuis[poi.id].purgable) {
178                                                 if (redraw) { nodeuis[poi.id].redraw(); }
179                                         } else {
180                                                 deleteNodeUI(poi);
181                                         }
182                                 }
183                         }
184
185             for each (marker in o.markersInside) {
186                 if (!markeruis[marker.id]) { createMarkerUI(marker); }
187                 else if (redraw) { markeruis[marker.id].redraw(); }
188             }
189
190             if (remove) {
191                 for each (marker in o.markersOutside) {
192                     if (markeruis[marker.id] && !markeruis[marker.id].purgable) {
193                         if (redraw) { markeruis[marker.id].redraw(); }
194                     } else {
195                         deleteMarkerUI(marker);
196                     }
197                 }
198             }
199                 }
200
201                 /** Make a UI object representing a way. */
202                 public function createWayUI(way:Way):WayUI {
203                         if (!wayuis[way.id]) {
204                                 wayuis[way.id]=new WayUI(way,this);
205                                 way.addEventListener(Connection.WAY_DELETED, wayDeleted);
206                         } else {
207                                 wayuis[way.id].redraw();
208                         }
209                         return wayuis[way.id];
210                 }
211
212                 /** Respond to event by removing the WayUI. */
213                 public function wayDeleted(event:EntityEvent):void {
214                         deleteWayUI(event.entity as Way);
215                 }
216
217                 /** Remove a way's UI object. */
218                 public function deleteWayUI(way:Way):void {
219                         way.removeEventListener(Connection.WAY_DELETED, wayDeleted);
220                         if (wayuis[way.id]) {
221                                 wayuis[way.id].redrawMultis();
222                                 wayuis[way.id].removeSprites();
223                                 wayuis[way.id].removeEventListeners();
224                                 wayuis[way.id].removeListenSprite();
225                                 delete wayuis[way.id];
226                         }
227                         for (var i:uint=0; i<way.length; i++) {
228                                 var node:Node=way.getNode(i);
229                                 if (nodeuis[node.id]) { deleteNodeUI(node); }
230                         }
231                 }
232
233                 /** Make a UI object representing a node. */
234                 public function createNodeUI(node:Node,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):NodeUI {
235                         if (!nodeuis[node.id]) {
236                                 nodeuis[node.id]=new NodeUI(node,this,rotation,layer,stateClasses);
237                                 node.addEventListener(Connection.NODE_DELETED, nodeDeleted);
238                         } else {
239                                 for (var state:String in stateClasses) {
240                                         nodeuis[node.id].setStateClass(state,stateClasses[state]);
241                                 }
242                                 nodeuis[node.id].redraw();
243                         }
244                         return nodeuis[node.id];
245                 }
246
247                 /** Respond to event by deleting NodeUI. */
248                 public function nodeDeleted(event:EntityEvent):void {
249                         deleteNodeUI(event.entity as Node);
250                 }
251
252                 /** Remove a node's UI object. */
253                 public function deleteNodeUI(node:Node):void {
254                         node.removeEventListener(Connection.NODE_DELETED, nodeDeleted);
255                         if (!nodeuis[node.id]) { return; }
256                         nodeuis[node.id].removeSprites();
257                         nodeuis[node.id].removeEventListeners();
258                         nodeuis[node.id].removeListenSprite();
259                         delete nodeuis[node.id];
260                 }
261
262         /** Make a UI object representing a marker. */
263         public function createMarkerUI(marker:Marker,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):MarkerUI {
264             if (!markeruis[marker.id]) {
265                 markeruis[marker.id]=new MarkerUI(marker,this,rotation,layer,stateClasses);
266                 marker.addEventListener(Connection.NODE_DELETED, markerDeleted);
267             } else {
268                 for (var state:String in stateClasses) {
269                     markeruis[marker.id].setStateClass(state,stateClasses[state]);
270                 }
271                 markeruis[marker.id].redraw();
272             }
273             return markeruis[marker.id];
274         }
275
276         /** Respond to event by deleting MarkerUI. */
277         public function markerDeleted(event:EntityEvent):void {
278             deleteMarkerUI(event.entity as Marker);
279         }
280
281         /** Remove a marker's UI object. */
282         public function deleteMarkerUI(marker:Marker):void {
283             marker.removeEventListener(Connection.NODE_DELETED, markerDeleted);
284             if (!markeruis[marker.id]) { return; }
285             markeruis[marker.id].removeSprites();
286             markeruis[marker.id].removeEventListeners();
287             markeruis[marker.id].removeListenSprite();
288             delete markeruis[marker.id];
289         }
290                 
291                 public function renumberWayUI(way:Way,oldID:Number):void {
292                         if (!wayuis[oldID]) { return; }
293                         wayuis[way.id]=wayuis[oldID];
294                         delete wayuis[oldID];
295                 }
296
297                 public function renumberNodeUI(node:Node,oldID:Number):void {
298                         if (!nodeuis[oldID]) { return; }
299                         nodeuis[node.id]=nodeuis[oldID];
300                         delete nodeuis[oldID];
301                 }
302
303                 /** Make a new sprite for painting on */
304                 private function getPaintSprite():Sprite {
305                         var s:Sprite = new Sprite();
306                         s.mouseEnabled = false;
307                         s.mouseChildren = false;
308                         return s;
309                 }
310
311                 private function getHitSprite():Sprite {
312                         var s:Sprite = new Sprite();
313                         return s;
314                 }
315
316                 public function redraw():void {
317                         for each (var w:WayUI in wayuis) { w.recalculate(); w.invalidateStyleList(); w.redraw(); }
318                         /* sometimes (e.g. in Map.setStyle) Mappaint.redrawPOIs() is called immediately afterwards anyway. FIXME? */
319                         for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
320             for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
321                 }
322
323                 /** Redraw nodes and markers */
324                 public function redrawPOIs():void {
325                         for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
326             for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
327                 }
328                 
329                 public function findSource():VectorLayer {
330                         var v:VectorLayer;
331                         for each (v in map.vectorlayers) {
332                                 if (v.paint==this) { return v; }
333                         }
334                         return null;
335                 }
336         }
337 }