1 // June 20th 2005 http://civicactions.net anselm@hook.org public domain version 0.5
7 var netscape = ( document.getElementById && !document.all ) || document.layers;
8 var defaultEngine = null; // xxx for firefox keyboard events.
10 var PI = 3.14159265358979323846;
12 var lat_range = PI, lon_range = PI;
15 // Utility - get div position - may not be accurate
18 function getCSSPositionX(parent)
20 var offset = parent.x ? parseInt(parent.x) : 0;
21 offset += parent.style.left ? parseInt(parent.style.left) : 0;
22 for(var node = parent; node ; node = node.offsetParent )
24 offset += node.offsetLeft;
29 function getCSSPositionY(parent)
31 var offset = parent.y ? parseInt(parent.y) : 0;
32 offset += parent.style.top ? parseInt(parent.style.top) : 0;
33 for(var node = parent; node ; node = node.offsetParent )
35 offset += node.offsetTop;
41 /// initialize a new tile engine object
42 /// usage: var engine = new tile_engine_new(parentdiv,stylehints,wmssource,lon,lat,zoom,optional width, optional height)
44 function tile_engine_new(parentname,hints,feedurl,url,lon,lat,zoom,w,h)
46 // NW geocoder removed for now
48 this.timestamp = new Date().getTime();
49 this.urlAttr = new Array();
51 // NW Removed navigation buttons entirely for flexibility
53 this.lonPerPixel = function()
54 { return (this.lon_quant/this.scale)/this.tilewidth; }
56 this.latPerPixel = function()
57 { return (this.lat_quant/this.scale)/this.tileheight; }
59 this.xToLon = function(x)
60 { return this.lon + (x-this.thewidth/2)*this.lonPerPixel(); }
62 this.yToLat = function(y)
63 { return normallat(this.lat - (y-this.theheight/2)
64 *this.latPerPixel()); }
66 this.lonToX = function (lon)
67 { return ((lon-this.lon)/this.lonPerPixel()) + this.thewidth/2;}
69 this.latToY = function(lat)
70 { return ((this.lat-mercatorlat(lat))/this.latPerPixel()) +
75 // it is possible that this collection is already in use - clean it
77 this.clean = function()
80 while( this.parent.hasChildNodes() )
81 this.parent.removeChild( this.parent.firstChild );
84 for(var ct=0; ct<this.parent.childNodes.length; ct++)
86 if(this.parent.childNodes[ct].id != "controls")
87 this.parent.removeChild(this.parent.childNodes[ct]);
91 // build inner tile container for theoretical speed improvement?
92 // center in parent for simplicity of math
93 // size of inner container is irrelevant since overflow is enabled
96 if( this.dragcontainer )
98 this.tiles = document.createElement('div');
99 this.tiles.style.position = 'absolute';
100 this.tiles.style.left = this.displaywidth/2 + 'px';
101 this.tiles.style.top = this.displayheight/2 + 'px';
102 this.tiles.style.width = '16px';
103 this.tiles.style.height = '16px';
106 this.tiles.style.border = 'dashed green 1px';
108 this.tiles.tile_engine = this;
109 this.parent.appendChild(this.tiles);
114 this.tiles = this.parent;
118 /// focus over specified lon/lat and zoom
119 /// user should call this.drag(0,0) after this to force an initial refresh
122 this.performzoom = function(lon,lat,z)
125 // this engine operates at * scale to try avoid tile errors thrashing
128 this.scale = 1000000;
129 this.lon_min_clamp = -180 * this.scale;
130 this.lon_max_clamp = 180 * this.scale;
131 this.lat_min_clamp = -180 * this.scale; //t
132 this.lat_max_clamp = 180 * this.scale; //t
133 this.lon_start_tile = 180 * this.scale;
134 this.lat_start_tile = 90 * this.scale; //t
136 this.lon_quant = this.lon_start_tile;
137 this.lat_quant = this.lat_start_tile;
141 // operational lat - = lat due to quirks in our engine and quirks in o
145 // divide tile size until reach requested zoom
146 // trying to guarantee consistency so as to not thrash the server side tile cache
149 this.lon_quant = this.lon_quant / this.zoom_power;
150 this.lat_quant = this.lat_quant / this.zoom_power;
153 this.lon_quant = Math.round( this.lon_quant );
154 this.lat_quant = Math.round( this.lat_quant );
156 // get user requested exact lon/lat
157 this.lon_scaled = Math.round( lon * this.scale );
158 this.lat_scaled = Math.round( lat * this.scale );
162 // convert requested exact lon/lat to quantized lon lat (rounding down
163 // or up as best suits)
164 this.lon_round = Math.round( this.lon_scaled / this.lon_quant ) *
166 this.lat_round = Math.round( this.lat_scaled / this.lat_quant ) *
169 //alert('lon_round=' + this.lon_round+ ' lat_round='+this.lat_round);
171 // calculate world extents [ this is the span of all tiles in lon/lat ]
172 this.lon_min = this.lon_round - this.lon_quant;
173 this.lon_max = this.lon_round + this.lon_quant;
174 this.lat_min = this.lat_round - this.lat_quant;
175 this.lat_max = this.lat_round + this.lat_quant;
177 // set tiled region details [ this is the span of all tiles in pixels ]
180 this.tilewidth = 256;
181 this.tileheight = 128;
182 this.left = -this.tilewidth;
183 this.right = this.tilewidth;
184 this.top = -this.tileheight;
185 this.bot = this.tileheight;
188 // adjust the current center position slightly to reflect exact lat/lon
190 this.centerx -= (this.lon_scaled-this.lon_round)/
191 (this.lon_max-this.lon_min)*(this.right-this.left);
192 this.centery -= (this.lat_scaled-this.lat_round)/
193 (this.lat_max-this.lat_min)*(this.bot-this.top);
196 this.update_perma_link = function() {
197 // because we're using mercator
198 updatelinks(this.lon,normallat(this.lat),this.zoom);
202 /// draw the spanning lon/lat range
203 /// drag is simply the mouse delta in pixels
206 this.drag = function(dragx,dragy)
210 // move the drag offset
211 this.centerx += dragx;
212 this.centery += dragy;
214 // update where we think the user is actually focused
215 this.lon = ( this.lon_round - ( this.lon_max - this.lon_min ) /
216 ( this.right - this.left ) * this.centerx ) / this.scale;
217 this.lat = - ( this.lat_round - ( this.lat_max - this.lat_min ) /
218 ( this.bot - this.top ) * this.centery ) / this.scale;
220 this.update_perma_link();
223 var helper = this.navhelp;
225 // extend exposed sections
227 while( this.left + this.centerx > -this.displaywidth/2 &&
228 this.lon_min > this.lon_min_clamp )
230 this.left -= this.tilewidth;
231 this.lon_min -= this.lon_quant;
234 while( this.right + this.centerx < this.displaywidth/2 &&
235 this.lon_max < this.lon_max_clamp )
237 this.right += this.tilewidth;
238 this.lon_max += this.lon_quant;
241 while( this.top + this.centery > -this.displayheight/2 &&
242 this.lat_min > this.lat_min_clamp )
244 this.top -= this.tileheight;
245 this.lat_min -= this.lat_quant;
249 while( this.bot + this.centery < this.displayheight/2 &&
250 this.lat_max < this.lat_max_clamp )
252 this.bot += this.tileheight;
253 this.lat_max += this.lat_quant;
258 // prepare to walk the container and assure that all nodes are correct
262 // in drag container mode we do not have to move the children all the
264 if( this.dragcontainer )
266 this.tiles.style.left = this.displaywidth / 2 + this.centerx + 'px';
267 this.tiles.style.top = this.displayheight / 2 + this.centery + 'px';
268 if( !dirty && this.tiles.hasChildNodes() )
272 containerx = this.left;
273 containery = this.top;
277 containerx = this.left + this.centerx;
278 containery = this.top + this.centery;
281 // walk all tiles and repair as needed
282 // xxx one bug is that it walks the _entire_ width and height...
284 // xxx this makes cleanup harder and perhaps a bitmap is better
286 var removehidden = 1;
287 var removecolumn = 0;
289 var containeryreset = containery;
291 for( var x = this.lon_min; x < this.lon_max ; x+= this.lon_quant )
293 // will this row be visible in the next round?
296 var rx = containerx + this.centerx;
297 if( rx > this.displaywidth / 2 )
300 // ideally i would truncate max width here
302 else if( rx + this.tilewidth < - this.displaywidth / 2 )
312 for( var y = this.lat_min; y < this.lat_max ; y+= this.lat_quant )
314 // is this column visible?
317 var ry = containery + this.centery;
318 if( ry > this.displayheight / 2 )
322 else if( ry + this.tileheight < - this.displayheight/2)
332 // convert to WMS compliant coordinate system
333 var lt = x / this.scale;
334 var rt = lt + this.lon_quant / this.scale;
335 var tp = y / this.scale;
336 var bt = tp + this.lat_quant / this.scale;
341 // modify for mercator-projected tiles:
342 tp = 180/PI * (2 * Math.atan(Math.exp(tp * PI / 180)) - PI / 2);
343 bt = 180/PI * (2 * Math.atan(Math.exp(bt * PI / 180)) - PI / 2);
346 var key = this.url + "?WIDTH="+(this.tilewidth)+"&HEIGHT="+
347 (this.tileheight)+"&BBOX="+lt+","+tp+","+rt+","+bt;
349 // see if our tile is already present
350 var node = document.getElementById(key);
352 // create if not present
357 node = document.createElement('div');
361 node = document.createElement('img');
364 node.className = 'tile';
365 node.style.position = 'absolute';
366 node.style.width = this.tilewidth + 'px';
367 node.style.height = this.tileheight + 'px';
368 node.style.left = containerx + 'px';
369 node.style.top = containery + 'px';
370 node.style.zIndex = 10; // to appear under the rss elements
371 node.tile_engine = this;
374 node.style.border = "1px solid yellow";
375 node.innerHTML = key;
378 var img = document.createElement('img');
380 node.appendChild(img);
384 var goURL = key + "&zoom=" + this.zoom;
386 for(var k in this.urlAttr)
388 goURL += "&"+k+"="+this.urlAttr[k];
393 node.alt = "loading tile..";
394 node.style.color = "#ffffff";
395 this.tiles.appendChild(node);
397 // adjust if using active style
398 else if( !this.dragcontainer ) {
399 node.style.left = containerx + 'px';
400 node.style.top = containery + 'px';
403 containery += this.tileheight;
405 containery = containeryreset;
406 containerx += this.tilewidth;
410 this.zoomTo = function(zoom)
415 if (this.zoom < this.minzoom) { this.zoom = this.minzoom; }
416 if (this.zoom > this.maxzoom) { this.zoom = this.maxzoom; }
422 /// immediately draw and or fit to feed
424 this.performzoom(this.lon,this.lat,zoom);
428 } // CLOSE ZOOM FUNCTION
430 this.setLatLon = function(lat,lon)
433 this.lat=mercatorlat(lat);
435 this.performzoom(lon,mercatorlat(lat),this.zoom);
439 this.forceRefresh = function()
442 this.performzoom(this.lon,this.lat,this.zoom);
447 /// zoom a tile group
449 this.tile_engine_zoomout = function()
452 this.zoomTo(this.zoom-1);
454 return false; // or safari falls over
458 /// zoom a tile group
460 this.tile_engine_zoomin = function()
464 this.zoomTo(this.zoom+1);
466 return false; // or safari falls over
469 this.setURL=function(url) { this.url=url; }
474 /// intercept context events to minimize out-of-browser interruptions
477 this.event_context = function(e)
486 this.event_key = function(e)
490 var hostengine = defaultEngine;
492 if( window && window.event && window.event.srcElement ) {
493 hostengine = window.event.srcElement.tile_engine;
494 } else if( e.target ) {
495 hostengine = e.target.tile_engine;
496 } else if( e.srcElement ) {
497 hostengine = e.srcElement.tile_engine;
500 if( hostengine == null ) {
501 hostengine = defaultEngine;
502 if( hostengine == null ) {
508 if( e == null && document.all ) {
522 hostengine.drag(16,0);
524 case 100: // d = right
525 hostengine.drag(-16,0);
528 hostengine.drag(0,16);
531 hostengine.drag(0,-16);
533 case 115: // s = center
534 new tile_engine_new(hostengine.parentname,
536 hostengine.feedurl, // xxx hrm, cache this?
544 case 122: // z = zoom
545 new tile_engine_new(hostengine.parentname,
547 hostengine.feedurl, // xxx hrm, cache this?
555 case 99: // c = unzoom
556 new tile_engine_new(hostengine.parentname,
558 hostengine.feedurl, // xxx hrm, cache this?
571 /// catch mouse move events
572 /// this routine _must_ return false or else the operating system outside-of-browser-scope drag and drop handler will interfere
575 this.event_mouse_move = function(e) {
577 var hostengine = null;
578 if( window && window.event && window.event.srcElement ) {
579 hostengine = window.event.srcElement.tile_engine;
580 } else if( e.target ) {
581 hostengine = e.target.tile_engine;
582 } else if( e.srcElement ) {
583 hostengine = e.srcElement.tile_engine;
587 if( hostengine && hostengine.drag ) {
588 if( hostengine.mousedown ) {
590 hostengine.mousex = parseInt(e.pageX) + 0.0;
591 hostengine.mousey = parseInt(e.pageY) + 0.0;
593 hostengine.mousex = parseInt(window.event.clientX) + 0.0;
594 hostengine.mousey = parseInt(window.event.clientY) + 0.0;
596 hostengine.drag(hostengine.mousex-hostengine.lastmousex,hostengine.mousey-hostengine.lastmousey);
598 hostengine.lastmousex = hostengine.mousex;
599 hostengine.lastmousey = hostengine.mousey;
602 // must return false to prevent operating system drag and drop from handling events
610 this.event_mouse_down = function(e) {
612 var hostengine = null;
613 if( window && window.event && window.event.srcElement ) {
614 hostengine = window.event.srcElement.tile_engine;
615 } else if( e.target ) {
616 hostengine = e.target.tile_engine;
617 } else if( e.srcElement ) {
618 hostengine = e.srcElement.tile_engine;
624 hostengine.mousex = parseInt(e.pageX) + 0.0;
625 hostengine.mousey = parseInt(e.pageY) + 0.0;
627 hostengine.mousex = parseInt(window.event.clientX) + 0.0;
628 hostengine.mousey = parseInt(window.event.clientY) + 0.0;
630 hostengine.lastmousex = hostengine.mousex;
631 hostengine.lastmousey = hostengine.mousey;
632 hostengine.mousedown = 1;
635 // must return false to prevent operating system drag and drop from handling events
640 /// catch double click (use to center map)
643 this.event_double_click = function(e) {
645 var hostengine = null;
646 if( window && window.event && window.event.srcElement ) {
647 hostengine = window.event.srcElement.tile_engine;
648 } else if( e.target ) {
649 hostengine = e.target.tile_engine;
650 } else if( e.srcElement ) {
651 hostengine = e.srcElement.tile_engine;
657 hostengine.mousex = parseInt(e.pageX) + 0.0;
658 hostengine.mousey = parseInt(e.pageY) + 0.0;
660 hostengine.mousex = parseInt(window.event.clientX) + 0.0;
661 hostengine.mousey = parseInt(window.event.clientY) + 0.0;
663 var dx = hostengine.mousex-(hostengine.displaywidth/2)-hostengine.parent_x;
664 var dy = hostengine.mousey-(hostengine.displayheight/2)-hostengine.parent_y;
665 hostengine.drag(-dx,-dy); // TODO smooth
668 // must return false to prevent operating system drag and drop from handling events
677 this.event_mouse_up = function(e) {
679 var hostengine = null;
680 if( window && window.event && window.event.srcElement ) {
681 hostengine = window.event.srcElement.tile_engine;
682 } else if( e.target ) {
683 hostengine = e.target.tile_engine;
684 } else if( e.srcElement ) {
685 hostengine = e.srcElement.tile_engine;
691 hostengine.mousex = parseInt(e.pageX) + 0.0;
692 hostengine.mousey = parseInt(e.pageY) + 0.0;
694 hostengine.mousex = parseInt(window.event.clientX) + 0.0;
695 hostengine.mousey = parseInt(window.event.clientY) + 0.0;
697 hostengine.mousedown = 0;
700 // must return false to prevent operating system drag and drop from handling events
708 this.event_mouse_out = function(e) {
710 var hostengine = null;
711 if( window && window.event && window.event.srcElement ) {
712 hostengine = window.event.srcElement.tile_engine;
713 } else if( e.target ) {
714 hostengine = e.target.tile_engine;
715 } else if( e.srcElement ) {
716 hostengine = e.srcElement.tile_engine;
722 hostengine.mousex = parseInt(e.pageX) + 0.0;
723 hostengine.mousey = parseInt(e.pageY) + 0.0;
725 hostengine.mousex = parseInt(window.event.clientX) + 0.0;
726 hostengine.mousey = parseInt(window.event.clientY) + 0.0;
728 hostengine.mousedown = 0;
731 // must return false to prevent operating system drag and drop from handling events
737 /// register new handlers to catch desired events
740 // NW removed parameter - always use parent
741 this.event_catch = function() {
743 this.parent.style.cursor = 'move';
746 window.captureEvents(Event.MOUSEMOVE);
747 window.captureEvents(Event.KEYPRESS);
750 this.parent.onmousemove = this.event_mouse_move;
751 this.parent.onmousedown = this.event_mouse_down;
752 this.parent.onmouseup = this.event_mouse_up;
753 this.parent.onkeypress = this.event_key;
754 window.ondblclick = this.event_double_click;
757 window.onmousemove = this.event_mouse_move;
758 window.onmouseup = this.event_mouse_up;
759 window.ondblclick = this.event_double_click;
764 this.setURLAttribute = function(k,v)
769 this.getURLAttribute = function(k)
771 return this.urlAttr[k];
774 this.getDownloadedTileBounds = function()
776 var bounds = new Array();
777 bounds.w=this.lon_min;
778 bounds.s=normallat(this.lat_min);
779 bounds.e=this.lon_max;
780 bounds.n=normallat(this.lat_max);
784 this.getVisibleBounds = function()
786 var bounds = new Array();
787 bounds.w = this.xToLon(0);
788 bounds.s = this.yToLat(this.theheight);
789 bounds.e = this.xToLon(this.thewidth);
790 bounds.n = this.yToLat(0);
794 // navout and navin stuff - START
795 // draw navigation buttons into the parent div
797 // ENTRY CODE BEGINS HERE....
800 // get parent div or fail
801 this.parent = document.getElementById(parentname);
802 if( this.parent == null ) {
803 alert('The tile map engine cannot find a parent container named ['
812 this.parentname = parentname;
814 this.feedurl = feedurl;
817 this.lat = mercatorlat(lat);
820 this.dragcontainer = 1;
823 // for firefox keyboard
824 defaultEngine = this;
825 document.engine = this;
829 // decide on display width and height
833 w = parseInt(this.parent.style.width);
834 h = parseInt(this.parent.style.height);
839 this.parent.style.width = w + 'px';
840 this.parent.style.height = h + 'px';
845 this.parent.style.width = parseInt(w) + 'px';
846 this.parent.style.height = parseInt(h) + 'px';
848 this.displaywidth = w;
849 this.displayheight = h;
855 // enforce parent div style?
856 // position absolute is really only required for firefox
857 // http://www.quirksmode.org/js/findpos.html
860 this.parent_x = getCSSPositionX(this.parent);
861 this.parent_y = getCSSPositionY(this.parent);
863 this.parent.style.position = 'relative';
864 this.parent.style.overflow = 'hidden';
865 this.parent.style.backgroundColor = '#000036';
868 // attach event capture parent div
878 function normallat(mercatorlat)
880 var tp = 180/PI*(2 * Math.atan(Math.exp(mercatorlat * PI / 180)) - PI / 2);
884 function mercatorlat(normallat)
886 var lpi = 3.14159265358979323846;
887 return Math.log( Math.tan( (lpi / 4.0) + (normallat / 180.0 * lpi / 2.0))) *