1 package net.systemeD.halcyon {
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;
11 /** Manages the drawing of map entities, allocating their sprites etc. */
12 public class MapPaint extends Sprite {
14 /** Parent Map - required for finding out bounds and scale */
17 /** Source data for this MapPaint layer */
18 public var connection:Connection;
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={};
39 /** The url of the style in use */
40 public var style:String = '';
42 private const VERYBIG:Number=Math.pow(2,16);
43 private static const NO_LAYER:int=-99999; // same as NodeUI
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>
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.
57 public function MapPaint(map:Map, connection:Connection, styleurl:String, minlayer:int, maxlayer:int) {
61 this.connection=connection;
62 this.minlayer=minlayer;
63 this.maxlayer=maxlayer;
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);
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
83 s.addChild(getPaintSprite()); // | 1 casing
84 var t:Sprite = getPaintSprite(); // | 2 stroke
85 t.addChild(getPaintSprite()); // | | sublayer
87 s.addChild(getPaintSprite()); // | 3 names
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
100 /** Returns the paint surface for the given layer. */
101 public function getPaintSpriteAt(l:int):Sprite {
102 return getChildAt(l-minlayer) as Sprite;
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;
110 /** Is ruleset loaded? */
111 public function get ready():Boolean {
112 if (!ruleset) { return false; }
113 if (!ruleset.loaded) { return false; }
117 public function sublayer(layer:int,spritetype:uint,sublayer:Number):Sprite {
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) {
128 if (ix>sublayer && ix<lowestAbove) {
130 lowestAbovePos=sublayerIndex[index];
134 if (lowestAbovePos==-1) { lowestAbovePos=indexLength; }
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
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) {
149 if (ix>sublayer) { toUpdate.push(index); }
151 for each (index in toUpdate) { sublayerIndex[index]++; }
152 sublayerIndex[sublayer]=lowestAbovePos;
155 l=getChildAt(layer-minlayer);
156 o=(l as Sprite).getChildAt(spritetype);
157 return ((o as Sprite).getChildAt(sublayerIndex[sublayer]) as Sprite);
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.
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)
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);
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
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(); }
190 for each (poi in o.poisInside) {
191 if (!nodeuis[poi.id]) { createNodeUI(poi); }
192 else if (redraw) { nodeuis[poi.id].redraw(); }
196 for each (poi in o.poisOutside) {
197 if (nodeuis[poi.id] && !nodeuis[poi.id].purgable) {
198 if (redraw) { nodeuis[poi.id].redraw(); }
205 for each (marker in o.markersInside) {
206 if (!markeruis[marker.id]) { createMarkerUI(marker); }
207 else if (redraw) { markeruis[marker.id].redraw(); }
211 for each (marker in o.markersOutside) {
212 if (markeruis[marker.id] && !markeruis[marker.id].purgable) {
213 if (redraw) { markeruis[marker.id].redraw(); }
215 deleteMarkerUI(marker);
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);
227 wayuis[way.id].redraw();
229 return wayuis[way.id];
232 /** Respond to event by removing the WayUI. */
233 public function wayDeleted(event:EntityEvent):void {
234 deleteWayUI(event.entity as Way);
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];
247 for (var i:uint=0; i<way.length; i++) {
248 var node:Node=way.getNode(i);
249 if (nodeuis[node.id]) { deleteNodeUI(node); }
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 };
265 /** Make a UI object representing a node. */
266 public function createNodeUI(node:Node,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):NodeUI {
267 if (!nodeuis[node.id]) {
268 nodeuis[node.id]=new NodeUI(node,this,rotation,layer,stateClasses);
269 node.addEventListener(Connection.NODE_DELETED, nodeDeleted);
271 for (var state:String in stateClasses) {
272 nodeuis[node.id].setStateClass(state,stateClasses[state]);
274 nodeuis[node.id].redraw();
276 return nodeuis[node.id];
279 /** Respond to event by deleting NodeUI. */
280 public function nodeDeleted(event:EntityEvent):void {
281 deleteNodeUI(event.entity as Node);
284 /** Remove a node's UI object. */
285 public function deleteNodeUI(node:Node):void {
286 node.removeEventListener(Connection.NODE_DELETED, nodeDeleted);
287 if (!nodeuis[node.id]) { return; }
288 nodeuis[node.id].removeSprites();
289 nodeuis[node.id].removeEventListeners();
290 nodeuis[node.id].removeListenSprite();
291 delete nodeuis[node.id];
294 /** Make a UI object representing a marker. */
295 public function createMarkerUI(marker:Marker,rotation:Number=0,layer:int=NO_LAYER,stateClasses:Object=null):MarkerUI {
296 if (!markeruis[marker.id]) {
297 markeruis[marker.id]=new MarkerUI(marker,this,rotation,layer,stateClasses);
298 marker.addEventListener(Connection.NODE_DELETED, markerDeleted);
300 for (var state:String in stateClasses) {
301 markeruis[marker.id].setStateClass(state,stateClasses[state]);
303 markeruis[marker.id].redraw();
305 return markeruis[marker.id];
308 /** Respond to event by deleting MarkerUI. */
309 public function markerDeleted(event:EntityEvent):void {
310 deleteMarkerUI(event.entity as Marker);
313 /** Remove a marker's UI object. */
314 public function deleteMarkerUI(marker:Marker):void {
315 marker.removeEventListener(Connection.NODE_DELETED, markerDeleted);
316 if (!markeruis[marker.id]) { return; }
317 markeruis[marker.id].removeSprites();
318 markeruis[marker.id].removeEventListeners();
319 markeruis[marker.id].removeListenSprite();
320 delete markeruis[marker.id];
323 public function renumberWayUI(way:Way,oldID:Number):void {
324 if (!wayuis[oldID]) { return; }
325 wayuis[way.id]=wayuis[oldID];
326 delete wayuis[oldID];
329 public function renumberNodeUI(node:Node,oldID:Number):void {
330 if (!nodeuis[oldID]) { return; }
331 nodeuis[node.id]=nodeuis[oldID];
332 delete nodeuis[oldID];
335 /** Make a new sprite for painting on */
336 private function getPaintSprite():Sprite {
337 var s:Sprite = new Sprite();
338 s.mouseEnabled = false;
339 s.mouseChildren = false;
343 private function getHitSprite():Sprite {
344 var s:Sprite = new Sprite();
348 /** Redraw all entities */
349 public function redraw():void {
350 for each (var w:WayUI in wayuis) { w.recalculate(); w.invalidateStyleList(); w.redraw(); }
351 /* sometimes (e.g. in Map.setStyle) Mappaint.redrawPOIs() is called immediately afterwards anyway. FIXME? */
352 for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
353 for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
356 /** Redraw nodes and markers only */
357 public function redrawPOIs():void {
358 for each (var p:NodeUI in nodeuis) { p.invalidateStyleList(); p.redraw(); }
359 for each (var m:MarkerUI in markeruis) { m.invalidateStyleList(); m.redraw(); }
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();
371 /** Switch to new MapCSS. */
372 public function setStyle(url:String):void {
374 ruleset=new RuleSet(map.MINSCALE,map.MAXSCALE,redraw,redrawPOIs);
375 ruleset.loadFromCSS(url);
378 /** Does an entity belong to this layer? */
379 public function sameConnection(entity:Entity):Boolean {
380 return entity.connection==this.connection;
383 // ==================== Start of code moved from Map.as
385 // Listeners for Connection events
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; }
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; }
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);
405 private function wayRenumberedListener(event:EntityRenumberedEvent):void {
406 var way:Way = event.entity as Way;
407 renumberWayUI(way,event.oldID);
410 private function nodeRenumberedListener(event:EntityRenumberedEvent):void {
411 var node:Node = event.entity as Node;
412 renumberNodeUI(node,event.oldID);
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); }
421 public function setHighlightOnNodes(way:Way, settings:Object):void {
422 if (wayuis[way.id]) wayuis[way.id].setHighlightOnNodes(settings);
425 public function protectWay(way:Way):void {
426 if (wayuis[way.id]) wayuis[way.id].protectSprites();
429 public function unprotectWay(way:Way):void {
430 if (wayuis[way.id]) wayuis[way.id].unprotectSprites();
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();
440 /** Protect Entities and EntityUIs against purging. This prevents the currently selected items
441 from being purged even though they're off-screen. */
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; }
454 } else if ( entity is Node && nodeuis[entity.id]) {
455 nodeuis[entity.id].purgable=purgable;
460 // ==================== End of code moved from Map.as
462 /** Find all ways whose WayUI passes a given screen co-ordinate. */
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); }
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
478 * @return either the newly created entity, or null
480 public function pullThrough(entity:Entity, target:MapPaint):Entity {
481 // TODO - check the entity actually resides in this layer.
483 var action:CompositeUndoableAction = new CompositeUndoableAction("pull through");
485 // copy way through to main layer
486 var oldWay:Way=Way(entity);
487 var nodemap:Object={};
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);
497 newNode = target.connection.createNode(oldNode.getTagsCopy(), oldNode.lat, oldNode.lon, action.push);
499 nodemap[oldNode.id]=newNode;
501 oldWay.remove(action.push);
502 var newWay:Way=target.connection.createWay(oldWay.getTagsCopy(), nodes, action.push);
503 MainUndoStack.getGlobalStack().addAction(action);
506 } else if (entity is Node && !entity.hasParentWays) {
508 oldNode=Node(entity);
510 var newPoiAction:CreatePOIAction = new CreatePOIAction(
511 target.connection, oldNode.getTagsCopy(), oldNode.lat, oldNode.lon);
512 action.push(newPoiAction);
514 oldNode.remove(action.push);
516 MainUndoStack.getGlobalStack().addAction(action);
517 return newPoiAction.getNode();