Draw new markers when they are created, and save the data loaders from worrying about...
[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                 /** Parent Map - required for finding out bounds and scale */
14                 public var map:Map;
15
16                 /** Source data for this MapPaint layer */
17                 public var connection:Connection;
18
19                 /** Lowest OSM layer that can be displayed */
20                 public var minlayer:int;
21                 /** Highest OSM layer that can be displayed */
22                 public var maxlayer:int;
23                 /** The MapCSS rules used for drawing entities. */
24                 public var ruleset:RuleSet;                                             
25                 /** WayUI objects attached to Way entities that are currently visible. */
26                 public var wayuis:Object=new Object();                  // sprites for ways and (POI/tagged) nodes
27                 /** NodeUI objects attached to POI/tagged node entities that are currently visible. */
28                 // confirm this - it seems to me all nodes in ways get nodeuis? --Steve B
29                 public var nodeuis:Object=new Object();
30                 /** MarkerUI objects attached to Marker entities that are currently visible. */
31         public var markeruis:Object=new Object();
32         /** Is this a background layer or the core paint object? */
33                 public var isBackground:Boolean = true;
34                 /** Hash of index->position */
35                 public var sublayerIndex:Object={};
36
37                 private const VERYBIG:Number=Math.pow(2,16);
38                 private static const NO_LAYER:int=-99999;               // same as NodeUI
39
40                 // Set up layering
41
42                 /** Creates paint sprites and hit sprites for all layers in range. This object ends up with a series of child sprites
43                  * 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.
44                  * <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>  
45                  * <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>
46                  * 
47                  * @param map The Map this is attached to. (Required for finding out bounds and scale.)
48                  * @param connection The Connection containing the data for this layer.
49                  * @param minlayer The lowest OSM layer to display.
50                  * @param maxlayer The highest OSM layer to display.
51                  * */ 
52                 public function MapPaint(map:Map, connection:Connection, styleurl:String, minlayer:int, maxlayer:int) {
53                         mouseEnabled=false;
54
55                         this.map=map;
56                         this.connection=connection;
57                         this.minlayer=minlayer;
58                         this.maxlayer=maxlayer;
59                         sublayerIndex[1]=0;
60                         var s:Sprite, l:int;
61
62                         // Set up stylesheet
63                         setStyle(styleurl);
64
65                         // Listen for changes on this Connection
66             connection.addEventListener(Connection.NEW_WAY, newWayCreatedListener);
67             connection.addEventListener(Connection.NEW_POI, newPOICreatedListener);
68             connection.addEventListener(Connection.WAY_RENUMBERED, wayRenumberedListener);
69             connection.addEventListener(Connection.NODE_RENUMBERED, nodeRenumberedListener);
70             connection.addEventListener(Connection.NEW_MARKER, newMarkerCreatedListener);
71
72                         // Add paint sprites
73                         for (l=minlayer; l<=maxlayer; l++) {                    // each layer (10 is +5, 0 is -5)
74                                 s = getPaintSprite();                                           //      |
75                                 s.addChild(getPaintSprite());                           //      | 0 fill
76                                 s.addChild(getPaintSprite());                           //      | 1 casing
77                                 var t:Sprite = getPaintSprite();                        //      | 2 stroke
78                                 t.addChild(getPaintSprite());                           //      |  | sublayer
79                                 s.addChild(t);                                                          //      |  |
80                                 s.addChild(getPaintSprite());                           //      | 3 names
81                                 addChild(s);                                                            //      |
82                         }
83                         
84                         // Add hit sprites
85                         for (l=minlayer; l<=maxlayer; l++) {                    // each layer (21 is +5, 11 is -5)
86                                 s = getHitSprite();                                                     //      |
87                                 s.addChild(getHitSprite());                                     //      | 0 way hit tests
88                                 s.addChild(getHitSprite());                                     //      | 1 node hit tests
89                                 addChild(s);
90                         }
91                 }
92                 
93                 /** Returns the paint surface for the given layer. */
94                 public function getPaintSpriteAt(l:int):Sprite {
95                         return getChildAt(l-minlayer) as Sprite;
96                 }
97
98                 /** Returns the hit sprite for the given layer. */
99                 public function getHitSpriteAt(l:int):Sprite {
100                         return getChildAt((l-minlayer) + (maxlayer-minlayer+1)) as Sprite;
101                 }
102                 
103                 /** Is ruleset loaded? */
104                 public function get ready():Boolean {
105                         if (!ruleset) { return false; }
106                         if (!ruleset.loaded) { return false; }
107                         return true;
108                 }
109
110                 public function sublayer(layer:int,sublayer:Number):Sprite {
111                         var l:DisplayObject;
112                         var o:DisplayObject;
113                         var index:String, ix:Number;
114                         if (!sublayerIndex.hasOwnProperty(sublayer)) {
115                                 // work out which position to add at
116                                 var lowestAbove:Number=VERYBIG;
117                                 var lowestAbovePos:int=-1;
118                                 var indexLength:uint=0;
119                                 for (index in sublayerIndex) {
120                                         ix=Number(index);
121                                         if (ix>sublayer && ix<lowestAbove) {
122                                                 lowestAbove=ix;
123                                                 lowestAbovePos=sublayerIndex[index];
124                                         }
125                                         indexLength++;
126                                 }
127                                 if (lowestAbovePos==-1) { lowestAbovePos=indexLength; }
128                         
129                                 // add sprites
130                                 for (var i:int=minlayer; i<=maxlayer; i++) {
131                                         l=getChildAt(i-minlayer);
132                                         o=(l as Sprite).getChildAt(2);
133                                         (o as Sprite).addChildAt(getPaintSprite(),lowestAbovePos);
134                                 }
135                         
136                                 // update index
137                                 // (we do it in this rather indirect way because if you alter sublayerIndex directly
138                                 //      within the loop, it confuses the iterator)
139                                 var toUpdate:Array=[];
140                                 for (index in sublayerIndex) {
141                                         ix=Number(index);
142                                         if (ix>sublayer) { toUpdate.push(index); }
143                                 }
144                                 for each (index in toUpdate) { sublayerIndex[index]++; }
145                                 sublayerIndex[sublayer]=lowestAbovePos;
146                         }
147
148                         l=getChildAt(layer-minlayer);
149                         o=(l as Sprite).getChildAt(2);
150                         return ((o as Sprite).getChildAt(sublayerIndex[sublayer]) as Sprite);
151                 }
152
153         /**
154         * Update, and if necessary, create / remove UIs for the current viewport.
155         * Flags control redrawing existing entities and removing UIs from entities no longer in view.
156         *
157         * @param redraw If true, all UIs for entities on "inside" lists will be redrawn
158         * @param remove If true, all UIs for entites on "outside" lists will be removed. The purgable flag on UIs
159                         can override this, for example for selected objects.
160         * fixme? add smarter behaviour for way nodes - remove NodeUIs from way nodes off screen, create them for ones
161         * that scroll onto screen (for highlights etc)
162         */
163                 public function updateEntityUIs(redraw:Boolean, remove:Boolean):void {
164                         var way:Way, poi:Node, marker:Marker;
165                         var o:Object = connection.getObjectsByBbox(map.edge_l,map.edge_r,map.edge_t,map.edge_b);
166
167                         for each (way in o.waysInside) {
168                                 if (!wayuis[way.id]) { createWayUI(way); }
169                                 else if (redraw) { wayuis[way.id].recalculate(); wayuis[way.id].redraw(); }
170                                 else wayuis[way.id].updateHighlights();//dubious
171                         }
172
173                         if (remove) {
174                                 for each (way in o.waysOutside) {
175                                         if (wayuis[way.id] && !wayuis[way.id].purgable) {
176                                                 if (redraw) { wayuis[way.id].recalculate(); wayuis[way.id].redraw(); }
177                                         } else {
178                                                 deleteWayUI(way);
179                                         }
180                                 }
181                         }
182
183                         for each (poi in o.poisInside) {
184                                 if (!nodeuis[poi.id]) { createNodeUI(poi); }
185                                 else if (redraw) { nodeuis[poi.id].redraw(); }
186                         }
187
188                         if (remove) {
189                                 for each (poi in o.poisOutside) { 
190                                         if (nodeuis[poi.id] && !nodeuis[poi.id].purgable) {
191                                                 if (redraw) { nodeuis[poi.id].redraw(); }
192                                         } else {
193                                                 deleteNodeUI(poi);
194                                         }
195                                 }
196                         }
197
198             for each (marker in o.markersInside) {
199                 if (!markeruis[marker.id]) { createMarkerUI(marker); }
200                 else if (redraw) { markeruis[marker.id].redraw(); }
201             }
202
203             if (remove) {
204                 for each (marker in o.markersOutside) {
205                     if (markeruis[marker.id] && !markeruis[marker.id].purgable) {
206                         if (redraw) { markeruis[marker.id].redraw(); }
207                     } else {
208                         deleteMarkerUI(marker);
209                     }
210                 }
211             }
212                 }
213
214                 /** Make a UI object representing a way. */
215                 public function createWayUI(way:Way):WayUI {
216                         if (!wayuis[way.id]) {
217                                 wayuis[way.id]=new WayUI(way,this);
218                                 way.addEventListener(Connection.WAY_DELETED, wayDeleted);
219                         } else {
220                                 wayuis[way.id].redraw();
221                         }
222                         return wayuis[way.id];
223                 }
224
225                 /** Respond to event by removing the WayUI. */
226                 public function wayDeleted(event:EntityEvent):void {
227                         deleteWayUI(event.entity as Way);
228                 }
229
230                 /** Remove a way's UI object. */
231                 public function deleteWayUI(way:Way):void {
232                         way.removeEventListener(Connection.WAY_DELETED, wayDeleted);
233                         if (wayuis[way.id]) {
234                                 wayuis[way.id].redrawMultis();
235                                 wayuis[way.id].removeSprites();
236                                 wayuis[way.id].removeEventListeners();
237                                 wayuis[way.id].removeListenSprite();
238                                 delete wayuis[way.id];
239                         }
240                         for (var i:uint=0; i<way.length; i++) {
241                                 var node:Node=way.getNode(i);
242                                 if (nodeuis[node.id]) { deleteNodeUI(node); }
243                         }
244                 }
245
246                 /** Make a UI object representing a node. */
247                 public function createNodeUI(node:Node,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):NodeUI {
248                         if (!nodeuis[node.id]) {
249                                 nodeuis[node.id]=new NodeUI(node,this,rotation,layer,stateClasses);
250                                 node.addEventListener(Connection.NODE_DELETED, nodeDeleted);
251                         } else {
252                                 for (var state:String in stateClasses) {
253                                         nodeuis[node.id].setStateClass(state,stateClasses[state]);
254                                 }
255                                 nodeuis[node.id].redraw();
256                         }
257                         return nodeuis[node.id];
258                 }
259
260                 /** Respond to event by deleting NodeUI. */
261                 public function nodeDeleted(event:EntityEvent):void {
262                         deleteNodeUI(event.entity as Node);
263                 }
264
265                 /** Remove a node's UI object. */
266                 public function deleteNodeUI(node:Node):void {
267                         node.removeEventListener(Connection.NODE_DELETED, nodeDeleted);
268                         if (!nodeuis[node.id]) { return; }
269                         nodeuis[node.id].removeSprites();
270                         nodeuis[node.id].removeEventListeners();
271                         nodeuis[node.id].removeListenSprite();
272                         delete nodeuis[node.id];
273                 }
274
275         /** Make a UI object representing a marker. */
276         public function createMarkerUI(marker:Marker,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):MarkerUI {
277             if (!markeruis[marker.id]) {
278                 markeruis[marker.id]=new MarkerUI(marker,this,rotation,layer,stateClasses);
279                 marker.addEventListener(Connection.NODE_DELETED, markerDeleted);
280             } else {
281                 for (var state:String in stateClasses) {
282                     markeruis[marker.id].setStateClass(state,stateClasses[state]);
283                 }
284                 markeruis[marker.id].redraw();
285             }
286             return markeruis[marker.id];
287         }
288
289         /** Respond to event by deleting MarkerUI. */
290         public function markerDeleted(event:EntityEvent):void {
291             deleteMarkerUI(event.entity as Marker);
292         }
293
294         /** Remove a marker's UI object. */
295         public function deleteMarkerUI(marker:Marker):void {
296             marker.removeEventListener(Connection.NODE_DELETED, markerDeleted);
297             if (!markeruis[marker.id]) { return; }
298             markeruis[marker.id].removeSprites();
299             markeruis[marker.id].removeEventListeners();
300             markeruis[marker.id].removeListenSprite();
301             delete markeruis[marker.id];
302         }
303                 
304                 public function renumberWayUI(way:Way,oldID:Number):void {
305                         if (!wayuis[oldID]) { return; }
306                         wayuis[way.id]=wayuis[oldID];
307                         delete wayuis[oldID];
308                 }
309
310                 public function renumberNodeUI(node:Node,oldID:Number):void {
311                         if (!nodeuis[oldID]) { return; }
312                         nodeuis[node.id]=nodeuis[oldID];
313                         delete nodeuis[oldID];
314                 }
315
316                 /** Make a new sprite for painting on */
317                 private function getPaintSprite():Sprite {
318                         var s:Sprite = new Sprite();
319                         s.mouseEnabled = false;
320                         s.mouseChildren = false;
321                         return s;
322                 }
323
324                 private function getHitSprite():Sprite {
325                         var s:Sprite = new Sprite();
326                         return s;
327                 }
328
329                 public function redraw():void {
330                         for each (var w:WayUI in wayuis) { w.recalculate(); w.invalidateStyleList(); w.redraw(); }
331                         /* sometimes (e.g. in Map.setStyle) Mappaint.redrawPOIs() is called immediately afterwards anyway. FIXME? */
332                         for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
333             for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
334                 }
335
336                 /** Redraw nodes and markers */
337                 public function redrawPOIs():void {
338                         for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
339             for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
340                 }
341                 
342                 /** Switch to new MapCSS. */
343                 public function setStyle(url:String):void {
344                         ruleset=new RuleSet(map.MINSCALE,map.MAXSCALE,redraw,redrawPOIs);
345                         ruleset.loadFromCSS(url);
346         }
347
348                 // >>>> REFACTOR: remove this
349                 public function findSource():VectorLayer {
350 //                      var v:VectorLayer;
351 //                      for each (v in map.vectorlayers) {
352 //                              if (v.paint==this) { return v; }
353 //                      }
354                         return null;
355                 }
356
357                 // ==================== Start of code moved from Map.as
358
359                 // Listeners for Connection events
360
361         private function newWayCreatedListener(event:EntityEvent):void {
362             var way:Way = event.entity as Way;
363                         if (!way.loaded || !way.within(map.edge_l, map.edge_r, map.edge_t, map.edge_b)) { return; }
364                         createWayUI(way);
365         }
366
367         private function newPOICreatedListener(event:EntityEvent):void {
368             var node:Node = event.entity as Node;
369                         if (!node.within(map.edge_l, map.edge_r, map.edge_t, map.edge_b)) { return; }
370                         createNodeUI(node);
371         }
372
373         private function newMarkerCreatedListener(event:EntityEvent):void {
374             var marker:Marker = event.entity as Marker;
375             if (!marker.within(map.edge_l, map.edge_r, map.edge_t, map.edge_b)) { return; }
376             createMarkerUI(marker);
377         }
378
379                 private function wayRenumberedListener(event:EntityRenumberedEvent):void {
380             var way:Way = event.entity as Way;
381                         renumberWayUI(way,event.oldID);
382                 }
383
384                 private function nodeRenumberedListener(event:EntityRenumberedEvent):void {
385             var node:Node = event.entity as Node;
386                         renumberNodeUI(node,event.oldID);
387                 }
388
389         /** Visually mark an entity as highlighted. */
390         public function setHighlight(entity:Entity, settings:Object):void {
391                         if      ( entity is Way  && wayuis[entity.id] ) { wayuis[entity.id].setHighlight(settings);  }
392                         else if ( entity is Node && nodeuis[entity.id]) { nodeuis[entity.id].setHighlight(settings); }
393         }
394
395         public function setHighlightOnNodes(way:Way, settings:Object):void {
396                         if (wayuis[way.id]) wayuis[way.id].setHighlightOnNodes(settings);
397         }
398
399                 public function protectWay(way:Way):void {
400                         if (wayuis[way.id]) wayuis[way.id].protectSprites();
401                 }
402
403                 public function unprotectWay(way:Way):void {
404                         if (wayuis[way.id]) wayuis[way.id].unprotectSprites();
405                 }
406                 
407                 public function limitWayDrawing(way:Way,except:Number=NaN,only:Number=NaN):void {
408                         if (!wayuis[way.id]) return;
409                         wayuis[way.id].drawExcept=except;
410                         wayuis[way.id].drawOnly  =only;
411                         wayuis[way.id].redraw();
412                 }
413
414                 /** Protect Entities and EntityUIs against purging. This prevents the currently selected items
415                    from being purged even though they're off-screen. */
416
417                 public function setPurgable(entities:Array, purgable:Boolean):void {
418                         for each (var entity:Entity in entities) {
419                                 entity.locked=!purgable;
420                                 if ( entity is Way  ) {
421                                         var way:Way=entity as Way;
422                                         if (wayuis[way.id]) { wayuis[way.id].purgable=purgable; }
423                                         for (var i:uint=0; i<way.length; i++) {
424                                                 var node:Node=way.getNode(i)
425                                                 node.locked=!purgable;
426                                                 if (nodeuis[node.id]) { nodeuis[node.id].purgable=purgable; }
427                                         }
428                                 } else if ( entity is Node && nodeuis[entity.id]) { 
429                                         nodeuis[entity.id].purgable=purgable;
430                                 }
431                         }
432                 }
433
434                 // ==================== End of code moved from Map.as
435
436         }
437 }