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