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