1 package net.systemeD.halcyon {
3 import flash.text.TextField;
4 import flash.geom.Rectangle;
5 import flash.display.DisplayObjectContainer;
6 import flash.display.Loader;
7 import flash.display.Sprite;
8 import flash.display.Shape;
9 import flash.display.Stage;
10 import flash.display.BitmapData;
11 import flash.display.LoaderInfo;
12 import flash.text.Font;
13 import flash.utils.ByteArray;
14 import flash.events.*;
16 import flash.external.ExternalInterface;
18 import net.systemeD.halcyon.connection.*;
19 import net.systemeD.halcyon.connection.EntityEvent;
20 import net.systemeD.halcyon.styleparser.*;
21 import net.systemeD.halcyon.Globals;
22 import flash.ui.Keyboard;
24 // for experimental export function:
25 // import flash.net.FileReference;
26 // import com.adobe.images.JPGEncoder;
28 /** The representation of part of the map on the screen, including information about coordinates, background imagery, paint properties etc. */
29 public class Map extends Sprite {
31 /** master map scale - how many Flash pixels in 1 degree longitude (for Landsat, 5120) */
32 public const MASTERSCALE:Number=5825.4222222222;
34 /** don't zoom out past this */
35 public const MINSCALE:uint=13;
36 /** don't zoom in past this */
37 public const MAXSCALE:uint=23;
39 /** sprite for ways and (POI/tagged) nodes in core layer */
40 public var paint:MapPaint;
41 /** sprite for vector background layers */
42 public var vectorbg:Sprite;
45 public var scale:uint=14;
46 /** current scaling factor for lon/latp */
47 public var scalefactor:Number=MASTERSCALE;
48 public var bigedge_l:Number= 999999; // area of largest whichways
49 public var bigedge_r:Number=-999999; // |
50 public var bigedge_b:Number= 999999; // |
51 public var bigedge_t:Number=-999999; // |
53 public var edge_l:Number; // current bounding box
54 public var edge_r:Number; // |
55 public var edge_t:Number; // |
56 public var edge_b:Number; // |
57 public var centre_lat:Number; // centre lat/lon
58 public var centre_lon:Number; // |
60 /** urllon-xradius/masterscale; */
61 public var baselon:Number;
62 /** lat2lat2p(urllat)+yradius/masterscale; */
63 public var basey:Number;
64 /** width (Flash pixels) */
65 public var mapwidth:uint;
66 /** height (Flash pixels) */
67 public var mapheight:uint;
69 /** Is the map being panned */
70 public var dragstate:uint=NOT_DRAGGING; // dragging map (panning)
71 /** Can the map be panned */
72 private var _draggable:Boolean=true; // |
73 private var lastxmouse:Number; // |
74 private var lastymouse:Number; // |
75 private var downX:Number; // |
76 private var downY:Number; // |
77 private var downTime:Number; // |
78 public const NOT_DRAGGING:uint=0; // |
79 public const NOT_MOVED:uint=1; // |
80 public const DRAGGING:uint=2; // |
81 /** How far the map can be dragged without actually triggering a pan. */
82 public const TOLERANCE:uint=7; // |
84 /** object containing HTML page parameters */
85 public var initparams:Object;
87 /** reference to backdrop sprite */
88 public var backdrop:Object;
89 /** background tile object */
90 public var tileset:TileSet;
91 /** background tile URL, name and scheme */
92 private var tileparams:Object={ url:'' };
93 /** internal style URL */
94 private var styleurl:String='';
95 /** show all objects, even if unstyled? */
96 public var showall:Boolean=true;
98 /** server connection */
99 public var connection:Connection;
100 /** VectorLayer objects */
101 public var vectorlayers:Object={};
103 // ------------------------------------------------------------------------------------------
104 /** Map constructor function */
105 public function Map(initparams:Object) {
107 this.initparams=initparams;
108 connection = Connection.getConnection(initparams);
109 connection.addEventListener(Connection.NEW_WAY, newWayCreated);
110 connection.addEventListener(Connection.NEW_POI, newPOICreated);
111 connection.addEventListener(Connection.WAY_RENUMBERED, wayRenumbered);
112 connection.addEventListener(Connection.NODE_RENUMBERED, nodeRenumbered);
113 gotEnvironment(null);
115 addEventListener(Event.ENTER_FRAME, everyFrame);
116 scrollRect=new Rectangle(0,0,800,600);
119 public function gotEnvironment(r:Object):void {
120 var loader:Loader = new Loader();
121 loader.contentLoaderInfo.addEventListener(Event.COMPLETE, gotFont);
122 loader.load(new URLRequest("FontLibrary.swf"));
125 public function gotFont(r:Event):void {
126 var FontLibrary:Class = r.target.applicationDomain.getDefinition("FontLibrary") as Class;
127 Font.registerFont(FontLibrary.DejaVu);
129 if (initparams['lat'] != null) {
130 // parameters sent from HTML
131 init(initparams['lat'],
136 // somewhere innocuous
137 init(53.09465,-2.56495,17);
141 // ------------------------------------------------------------------------------------------
142 /** Initialise map at a given lat/lon */
143 public function init(startlat:Number, startlon:Number, startscale:uint=0):void {
144 while (numChildren) { removeChildAt(0); }
146 tileset=new TileSet(this); // 0 - 900913 background
147 if (initparams['tileblocks']) { // | option to block dodgy tile sources
148 tileset.blocks=initparams['tileblocks'];// |
150 addChild(tileset); // |
151 tileset.init(tileparams, false,
152 initparams['background_dim'] ==null ? true : initparams['background_dim'],
153 initparams['background_sharpen']==null ? false : initparams['background_sharpen']);
155 vectorbg = new Sprite(); // 1 - vector background layers
156 addChild(vectorbg); // |
158 paint = new MapPaint(this,-5,5); // 2 - core paint object
159 addChild(paint); // |
160 paint.isBackground=false; // |
162 if (styleurl) { // if we've only just set up paint, then setStyle won't have created the RuleSet
163 paint.ruleset=new RuleSet(MINSCALE,MAXSCALE,redraw,redrawPOIs);
164 paint.ruleset.loadFromCSS(styleurl);
168 this.dispatchEvent(new MapEvent(MapEvent.SCALE, {scale:scale}));
171 scalefactor=MASTERSCALE/Math.pow(2,13-scale);
172 baselon =startlon -(mapwidth /2)/scalefactor;
173 basey =lat2latp(startlat)+(mapheight/2)/scalefactor;
174 addDebug("Baselon "+baselon+", basey "+basey);
176 this.dispatchEvent(new Event(MapEvent.INITIALISED));
179 if (ExternalInterface.available) {
180 ExternalInterface.addCallback("setPosition", function (lat:Number,lon:Number,zoom:uint):void {
181 updateCoordsFromLatLon(lat, lon);
187 // ------------------------------------------------------------------------------------------
188 /** Recalculate co-ordinates from new Flash origin */
190 public function updateCoords(tx:Number,ty:Number):void {
191 setScrollRectXY(tx,ty);
193 edge_t=coord2lat(-ty );
194 edge_b=coord2lat(-ty+mapheight);
195 edge_l=coord2lon(-tx );
196 edge_r=coord2lon(-tx+mapwidth );
202 /** Move the map to centre on a given latitude/longitude. */
203 public function updateCoordsFromLatLon(lat:Number,lon:Number):void {
204 var cy:Number=-(lat2coord(lat)-mapheight/2);
205 var cx:Number=-(lon2coord(lon)-mapwidth/2);
209 private function setScrollRectXY(tx:Number,ty:Number):void {
210 var w:Number=scrollRect.width;
211 var h:Number=scrollRect.height;
212 scrollRect=new Rectangle(-tx,-ty,w,h);
214 private function setScrollRectSize(width:Number,height:Number):void {
215 var sx:Number=scrollRect.x ? scrollRect.x : 0;
216 var sy:Number=scrollRect.y ? scrollRect.y : 0;
217 scrollRect=new Rectangle(sx,sy,width,height);
220 private function getX():Number { return -scrollRect.x; }
221 private function getY():Number { return -scrollRect.y; }
223 private function setCentre():void {
224 centre_lat=coord2lat(-getY()+mapheight/2);
225 centre_lon=coord2lon(-getX()+mapwidth/2);
226 this.dispatchEvent(new MapEvent(MapEvent.MOVE, {lat:centre_lat, lon:centre_lon, scale:scale, minlon:edge_l, maxlon:edge_r, minlat:edge_b, maxlat:edge_t}));
229 /** Sets the offset between the background imagery and the map. */
230 public function nudgeBackground(x:Number,y:Number):void {
231 this.dispatchEvent(new MapEvent(MapEvent.NUDGE_BACKGROUND, { x: x, y: y }));
234 private function moveMap(dx:Number,dy:Number):void {
235 updateCoords(getX()+dx,getY()+dy);
236 updateEntityUIs(false, false);
240 /** Recentre map at given lat/lon, updating the UI and downloading entities. */
241 public function moveMapFromLatLon(lat:Number,lon:Number):void {
242 updateCoordsFromLatLon(lat,lon);
243 updateEntityUIs(false,false);
247 // Co-ordinate conversion functions
249 public function latp2coord(a:Number):Number { return -(a-basey)*scalefactor; }
250 public function coord2latp(a:Number):Number { return a/-scalefactor+basey; }
251 public function lon2coord(a:Number):Number { return (a-baselon)*scalefactor; }
252 public function coord2lon(a:Number):Number { return a/scalefactor+baselon; }
254 public function latp2lat(a:Number):Number { return 180/Math.PI * (2 * Math.atan(Math.exp(a*Math.PI/180)) - Math.PI/2); }
255 public function lat2latp(a:Number):Number { return 180/Math.PI * Math.log(Math.tan(Math.PI/4+a*(Math.PI/180)/2)); }
257 public function lat2coord(a:Number):Number { return -(lat2latp(a)-basey)*scalefactor; }
258 public function coord2lat(a:Number):Number { return latp2lat(a/-scalefactor+basey); }
261 // ------------------------------------------------------------------------------------------
262 /** Resize map size based on current stage and height */
264 public function updateSize(w:uint, h:uint):void {
265 mapwidth = w; centre_lon=coord2lon(-getX()+w/2);
266 mapheight= h; centre_lat=coord2lat(-getY()+h/2);
267 setScrollRectSize(w,h);
269 this.dispatchEvent(new MapEvent(MapEvent.RESIZE, {width:w, height:h}));
271 if ( backdrop != null ) {
272 backdrop.width=mapwidth;
273 backdrop.height=mapheight;
275 if ( mask != null ) {
277 mask.height=mapheight;
281 /** Download map data. Data is downloaded for the connection and the vector layers, where supported.
282 * The bounding box for the download is taken from the current map edges.
284 public function download():void {
285 this.dispatchEvent(new MapEvent(MapEvent.DOWNLOAD, {minlon:edge_l, maxlon:edge_r, maxlat:edge_t, minlat:edge_b} ));
287 if (edge_l>=bigedge_l && edge_r<=bigedge_r &&
288 edge_b>=bigedge_b && edge_t<=bigedge_t) { return; } // we have already loaded this area, so ignore
289 bigedge_l=edge_l; bigedge_r=edge_r;
290 bigedge_b=edge_b; bigedge_t=edge_t;
291 if (connection.waycount>1000) {
292 connection.purgeOutside(edge_l,edge_r,edge_t,edge_b);
294 addDebug("Calling download with "+edge_l+"-"+edge_r+", "+edge_t+"-"+edge_b);
295 connection.loadBbox(edge_l,edge_r,edge_t,edge_b);
297 // Do the same for vector layers
298 for each (var layer:VectorLayer in vectorlayers) {
299 layer.loadBbox(edge_l,edge_r,edge_t,edge_b);
303 private function newWayCreated(event:EntityEvent):void {
304 var way:Way = event.entity as Way;
305 if (!way.loaded || !way.within(edge_l,edge_r,edge_t,edge_b)) { return; }
306 paint.createWayUI(way);
309 private function newPOICreated(event:EntityEvent):void {
310 var node:Node = event.entity as Node;
311 if (!node.within(edge_l,edge_r,edge_t,edge_b)) { return; }
312 paint.createNodeUI(node);
315 private function wayRenumbered(event:EntityRenumberedEvent):void {
316 var way:Way = event.entity as Way;
317 paint.renumberWayUI(way,event.oldID);
320 private function nodeRenumbered(event:EntityRenumberedEvent):void {
321 var node:Node = event.entity as Node;
322 paint.renumberNodeUI(node,event.oldID);
325 /** Visually mark an entity as highlighted. */
326 public function setHighlight(entity:Entity, settings:Object):void {
327 if ( entity is Way && paint.wayuis[entity.id] ) { paint.wayuis[entity.id].setHighlight(settings); }
328 else if ( entity is Node && paint.nodeuis[entity.id]) { paint.nodeuis[entity.id].setHighlight(settings); }
331 public function setHighlightOnNodes(way:Way, settings:Object):void {
332 if (paint.wayuis[way.id]) paint.wayuis[way.id].setHighlightOnNodes(settings);
335 public function protectWay(way:Way):void {
336 if (paint.wayuis[way.id]) paint.wayuis[way.id].protectSprites();
339 public function unprotectWay(way:Way):void {
340 if (paint.wayuis[way.id]) paint.wayuis[way.id].unprotectSprites();
343 public function limitWayDrawing(way:Way,except:Number=NaN,only:Number=NaN):void {
344 if (!paint.wayuis[way.id]) return;
345 paint.wayuis[way.id].drawExcept=except;
346 paint.wayuis[way.id].drawOnly =only;
347 paint.wayuis[way.id].redraw();
350 /** Protect Entities and EntityUIs against purging. This prevents the currently selected items
351 from being purged even though they're off-screen. */
353 public function setPurgable(entities:Array, purgable:Boolean):void {
354 for each (var entity:Entity in entities) {
355 entity.locked=!purgable;
356 if ( entity is Way ) {
357 var way:Way=entity as Way;
358 if (paint.wayuis[way.id]) { paint.wayuis[way.id].purgable=purgable; }
359 for (var i:uint=0; i<way.length; i++) {
360 var node:Node=way.getNode(i)
361 node.locked=!purgable;
362 if (paint.nodeuis[node.id]) { paint.nodeuis[node.id].purgable=purgable; }
364 } else if ( entity is Node && paint.nodeuis[entity.id]) {
365 paint.nodeuis[entity.id].purgable=purgable;
370 // Handle mouse events on ways/nodes
371 private var mapController:MapController = null;
373 /** Assign map controller. */
374 public function setController(controller:MapController):void {
375 this.mapController = controller;
378 public function entityMouseEvent(event:MouseEvent, entity:Entity):void {
379 if ( mapController != null )
380 mapController.entityMouseEvent(event, entity);
384 // ------------------------------------------------------------------------------------------
387 public function addVectorLayer(layer:VectorLayer):void {
388 vectorlayers[layer.name]=layer;
389 vectorbg.addChild(layer.paint);
392 // ------------------------------------------------------------------------------------------
393 // Redraw all items, zoom in and out
395 public function updateEntityUIs(redraw:Boolean,remove:Boolean):void {
396 paint.updateEntityUIs(connection.getObjectsByBbox(edge_l, edge_r, edge_t, edge_b), redraw, remove);
397 for each (var v:VectorLayer in vectorlayers) {
398 v.paint.updateEntityUIs(v.getObjectsByBbox(edge_l, edge_r, edge_t, edge_b), redraw, remove);
401 /** Redraw everything, including in every vector layer. */
402 public function redraw():void {
404 for each (var v:VectorLayer in vectorlayers) { v.paint.redraw(); }
406 /** Redraw POI's, including in every vector layer. */
407 public function redrawPOIs():void {
409 for each (var v:VectorLayer in vectorlayers) { v.paint.redrawPOIs(); }
412 /** Increase scale. */
413 public function zoomIn():void {
414 if (scale==MAXSCALE) { return; }
415 changeScale(scale+1);
418 /** Decrease scale. */
419 public function zoomOut():void {
420 if (scale==MINSCALE) { return; }
421 changeScale(scale-1);
424 private function changeScale(newscale:uint):void {
425 addDebug("new scale "+newscale);
427 this.dispatchEvent(new MapEvent(MapEvent.SCALE, {scale:scale}));
428 scalefactor=MASTERSCALE/Math.pow(2,13-scale);
429 updateCoordsFromLatLon((edge_t+edge_b)/2,(edge_l+edge_r)/2); // recentre
430 tileset.changeScale(scale);
431 updateEntityUIs(true,true);
435 private function reportPosition():void {
436 addDebug("lon "+coord2lon(mouseX)+", lat "+coord2lat(mouseY));
439 /** Switch to new MapCSS. */
440 public function setStyle(url:String):void {
443 paint.ruleset=new RuleSet(MINSCALE,MAXSCALE,redraw,redrawPOIs);
444 paint.ruleset.loadFromCSS(url);
448 /** Select a new background imagery. */
449 public function setBackground(bg:Object):void {
451 if (tileset) { tileset.init(bg, bg.url!=''); }
454 /** Set background dimming on/off. */
455 public function setDimming(dim:Boolean):void {
456 if (tileset) { tileset.setDimming(dim); }
459 /** Return background dimming. */
460 public function getDimming():Boolean {
461 if (tileset) { return tileset.getDimming(); }
465 /** Set background sharpening on/off. */
466 public function setSharpen(sharpen:Boolean):void {
467 if (tileset) { tileset.setSharpen(sharpen); }
469 /** Return background sharpening. */
470 public function getSharpen():Boolean {
471 if (tileset) { return tileset.getSharpen(); }
475 // ------------------------------------------------------------------------------------------
476 // Export (experimental)
477 // ** just a bit of fun for now!
478 // really needs to take a bbox, and make sure that the image is correctly cropped/resized
479 // to that area (will probably require creating a new DisplayObject with a different origin
482 public function export():void {
483 addDebug("size is "+this.width+","+this.height);
484 var jpgSource:BitmapData = new BitmapData(800,800); // (this.width, this.height);
485 jpgSource.draw(this);
486 var jpgEncoder:JPGEncoder = new JPGEncoder(85);
487 var jpgStream:ByteArray = jpgEncoder.encode(jpgSource);
488 var fileRef:FileReference = new FileReference();
489 // fileRef.save(jpgStream,'map.jpeg');
494 // ==========================================================================================
497 // ------------------------------------------------------------------------------------------
500 /** Should map be allowed to pan? */
501 public function set draggable(draggable:Boolean):void {
502 _draggable=draggable;
503 dragstate=NOT_DRAGGING;
506 /** Prepare for being dragged by recording start time and location of mouse. */
507 public function mouseDownHandler(event:MouseEvent):void {
508 if (!_draggable) { return; }
510 lastxmouse=stage.mouseX; downX=stage.mouseX;
511 lastymouse=stage.mouseY; downY=stage.mouseY;
512 downTime=new Date().getTime();
515 /** Respond to mouse up by possibly moving map. */
516 public function mouseUpHandler(event:MouseEvent=null):void {
517 if (dragstate==DRAGGING) { moveMap(x,y); }
518 dragstate=NOT_DRAGGING;
521 /** Respond to mouse movement, dragging the map if tolerance threshold met. */
522 public function mouseMoveHandler(event:MouseEvent):void {
523 if (!_draggable) { return; }
524 if (dragstate==NOT_DRAGGING) { return; }
526 if (dragstate==NOT_MOVED) {
527 if (new Date().getTime()-downTime<300) {
528 if (Math.abs(downX-stage.mouseX)<=TOLERANCE && Math.abs(downY-stage.mouseY)<=TOLERANCE ) return;
530 if (Math.abs(downX-stage.mouseX)<=TOLERANCE/2 && Math.abs(downY-stage.mouseY)<=TOLERANCE/2) return;
535 setScrollRectXY(getX()+stage.mouseX-lastxmouse,getY()+stage.mouseY-lastymouse);
536 lastxmouse=stage.mouseX; lastymouse=stage.mouseY;
540 // ------------------------------------------------------------------------------------------
543 private function everyFrame(event:Event):void {
544 if (tileset) { tileset.serviceQueue(); }
547 // ------------------------------------------------------------------------------------------
548 // Miscellaneous events
550 /** Respond to cursor movements and zoom in/out.*/
551 public function keyUpHandler(event:KeyboardEvent):void {
552 if (event.target is TextField) return; // not meant for us
553 switch (event.keyCode) {
554 case Keyboard.PAGE_UP: zoomIn(); break; // Page Up - zoom in
555 case Keyboard.PAGE_DOWN: zoomOut(); break; // Page Down - zoom out
556 case Keyboard.LEFT: moveMap(mapwidth/2,0); break; // left cursor
557 case Keyboard.UP: moveMap(0,mapheight/2); break; // up cursor
558 case Keyboard.RIGHT: moveMap(-mapwidth/2,0); break; // right cursor
559 case Keyboard.DOWN: moveMap(0,-mapheight/2); break; // down cursor
560 // case 76: reportPosition(); break; // L - report lat/long
564 /** What to do if an error with the network connection happens. */
565 public function connectionError(err:Object=null): void {
566 addDebug("got error");
569 // ------------------------------------------------------------------------------------------
572 public function clearDebug():void {
573 if (!Globals.vars.hasOwnProperty('debug')) return;
574 Globals.vars.debug.text='';
577 public function addDebug(text:String):void {
579 if (!Globals.vars.hasOwnProperty('debug')) return;
580 if (!Globals.vars.debug.visible) return;
581 Globals.vars.debug.appendText(text+"\n");
582 Globals.vars.debug.scrollV=Globals.vars.debug.maxScrollV;