├── .gitignore ├── LICENSE ├── README.md ├── bin └── light-map.js ├── example ├── basic.html ├── controls.html ├── fonts │ ├── light-map.eot │ ├── light-map.svg │ ├── light-map.ttf │ └── light-map.woff ├── index.html ├── light-map.min.js ├── proxy.php ├── retina.html └── style.css ├── img ├── light-map.jpg └── retina.jpg ├── map ├── Controls.js ├── Map.js ├── MapUtils.js ├── Rect.js ├── Tile.js └── mercator.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | node_modules/* 4 | /tmp 5 | .npmignore 6 | /javascripts 7 | /params.json 8 | /stylesheets 9 | /example/vendor 10 | /map/Controls.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2015 Mathew Groves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | light map 2 | ============= 3 | 4 | minimal, lightweight, self contained XYZ tile map viewer with a 2d canvas renderer. 5 | 6 | ### live example ### 7 | - [an example with the most common methods](http://nicoptere.github.io/light-map/example/) let's you set the lat/lon/zoom, change the width/height of the canvas and monitor the load progress. 8 | - [a basic example of controls](http://nicoptere.github.io/light-map/example/controls.html) you should be able to drag the map around. 9 | - [an example of retina support](http://nicoptere.github.io/light-map/example/retina.html) first I thought it wasn't much but it seems to be a big deal. it's based on the devicePixelRatio, ideally you should use a @2x provider but I did an internal nearest neighbour resize. 10 | - [an example with a vignette](http://nicoptere.github.io/light-map/example/basic.html) as the map output is a Canvas2D, this shows how to post process the output. 11 | 12 | ### more info ### 13 | [explanation and examples](http://nicoptere.github.io/light-map/) 14 | 15 | ### basic example ### 16 | 17 | ```js 18 | 19 | 20 | 27 | ``` 28 | 29 | ### vignette example ### 30 | 31 | ```js 32 | 33 | 34 | 114 | 115 | ``` 116 | 117 | ### npm module installation ### 118 | ``` 119 | npm install light-map --save 120 | ``` 121 | 122 | ### related ### 123 | [Python library to perform Mercator conversions](http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/) 124 | 125 | [Quad keys explained](https://msdn.microsoft.com/en-us/library/bb259689.aspx) 126 | 127 | npm [globalMercator](https://github.com/davvo/globalmercator/blob/master/globalmercator.js) 128 | 129 | ### License ### 130 | 131 | This content is released under the [MIT License](http://opensource.org/licenses/MIT). 132 | -------------------------------------------------------------------------------- /bin/light-map.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Map = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { 142 | this._events[type].warned = true; 143 | console.error('(node) warning: possible EventEmitter memory ' + 144 | 'leak detected. %d listeners added. ' + 145 | 'Use emitter.setMaxListeners() to increase limit.', 146 | this._events[type].length); 147 | if (typeof console.trace === 'function') { 148 | // not supported in IE 10 149 | console.trace(); 150 | } 151 | } 152 | } 153 | 154 | return this; 155 | }; 156 | 157 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 158 | 159 | EventEmitter.prototype.once = function(type, listener) { 160 | if (!isFunction(listener)) 161 | throw TypeError('listener must be a function'); 162 | 163 | var fired = false; 164 | 165 | function g() { 166 | this.removeListener(type, g); 167 | 168 | if (!fired) { 169 | fired = true; 170 | listener.apply(this, arguments); 171 | } 172 | } 173 | 174 | g.listener = listener; 175 | this.on(type, g); 176 | 177 | return this; 178 | }; 179 | 180 | // emits a 'removeListener' event iff the listener was removed 181 | EventEmitter.prototype.removeListener = function(type, listener) { 182 | var list, position, length, i; 183 | 184 | if (!isFunction(listener)) 185 | throw TypeError('listener must be a function'); 186 | 187 | if (!this._events || !this._events[type]) 188 | return this; 189 | 190 | list = this._events[type]; 191 | length = list.length; 192 | position = -1; 193 | 194 | if (list === listener || 195 | (isFunction(list.listener) && list.listener === listener)) { 196 | delete this._events[type]; 197 | if (this._events.removeListener) 198 | this.emit('removeListener', type, listener); 199 | 200 | } else if (isObject(list)) { 201 | for (i = length; i-- > 0;) { 202 | if (list[i] === listener || 203 | (list[i].listener && list[i].listener === listener)) { 204 | position = i; 205 | break; 206 | } 207 | } 208 | 209 | if (position < 0) 210 | return this; 211 | 212 | if (list.length === 1) { 213 | list.length = 0; 214 | delete this._events[type]; 215 | } else { 216 | list.splice(position, 1); 217 | } 218 | 219 | if (this._events.removeListener) 220 | this.emit('removeListener', type, listener); 221 | } 222 | 223 | return this; 224 | }; 225 | 226 | EventEmitter.prototype.removeAllListeners = function(type) { 227 | var key, listeners; 228 | 229 | if (!this._events) 230 | return this; 231 | 232 | // not listening for removeListener, no need to emit 233 | if (!this._events.removeListener) { 234 | if (arguments.length === 0) 235 | this._events = {}; 236 | else if (this._events[type]) 237 | delete this._events[type]; 238 | return this; 239 | } 240 | 241 | // emit removeListener for all listeners on all events 242 | if (arguments.length === 0) { 243 | for (key in this._events) { 244 | if (key === 'removeListener') continue; 245 | this.removeAllListeners(key); 246 | } 247 | this.removeAllListeners('removeListener'); 248 | this._events = {}; 249 | return this; 250 | } 251 | 252 | listeners = this._events[type]; 253 | 254 | if (isFunction(listeners)) { 255 | this.removeListener(type, listeners); 256 | } else { 257 | // LIFO order 258 | while (listeners.length) 259 | this.removeListener(type, listeners[listeners.length - 1]); 260 | } 261 | delete this._events[type]; 262 | 263 | return this; 264 | }; 265 | 266 | EventEmitter.prototype.listeners = function(type) { 267 | var ret; 268 | if (!this._events || !this._events[type]) 269 | ret = []; 270 | else if (isFunction(this._events[type])) 271 | ret = [this._events[type]]; 272 | else 273 | ret = this._events[type].slice(); 274 | return ret; 275 | }; 276 | 277 | EventEmitter.listenerCount = function(emitter, type) { 278 | var ret; 279 | if (!emitter._events || !emitter._events[type]) 280 | ret = 0; 281 | else if (isFunction(emitter._events[type])) 282 | ret = 1; 283 | else 284 | ret = emitter._events[type].length; 285 | return ret; 286 | }; 287 | 288 | function isFunction(arg) { 289 | return typeof arg === 'function'; 290 | } 291 | 292 | function isNumber(arg) { 293 | return typeof arg === 'number'; 294 | } 295 | 296 | function isObject(arg) { 297 | return typeof arg === 'object' && arg !== null; 298 | } 299 | 300 | function isUndefined(arg) { 301 | return arg === void 0; 302 | } 303 | 304 | },{}],2:[function(require,module,exports){ 305 | var Mercator = require( './Mercator' ); 306 | var utils = require( './MapUtils' ); 307 | var Rect = require( './Rect' ); 308 | var Tile = require( './Tile' ); 309 | var events = require('events'); 310 | 311 | module.exports = function(){ 312 | 313 | function Map( provider, domains, width, height, minZoom, maxZoom ){ 314 | 315 | 316 | //sets the scale of the map, handles retina display 317 | this.mercator = new Mercator( 256 ); 318 | 319 | //events 320 | this.eventEmitter = new events.EventEmitter(); 321 | 322 | //zoom bounds 323 | this._minZoom = Math.max( 0, minZoom ); 324 | this._maxZoom = Math.max( 1, maxZoom ); 325 | 326 | //map center 327 | this.latitude = 0; 328 | this.longitude = 0; 329 | 330 | //map zoom level 331 | this.zoom = 0; 332 | 333 | //loading 334 | this.tiles = []; 335 | this.keys = []; 336 | this.loadedTiles = []; 337 | 338 | //create domElement 339 | this.canvas = document.createElement("canvas"); 340 | this.ctx = this.canvas.getContext("2d"); 341 | 342 | //viewRect 343 | this.setSize( width, height ); 344 | 345 | //providers 346 | this.setProvider( provider, domains, 256 * window.devicePixelRatio ); 347 | 348 | //makes the math utils available 349 | this.utils = utils; 350 | } 351 | 352 | //getters / setters 353 | Map.prototype = { 354 | 355 | get width(){return this._width; }, set width( value ){this._width = value; this.setSize(this.width, this.height ); }, 356 | get height(){return this._height; }, set height( value ){this._height = value; this.setSize(this.width, this.height ); }, 357 | get minZoom(){return this._minZoom; }, set minZoom( value ){this._minZoom = value; }, 358 | get maxZoom(){return this._maxZoom; }, set maxZoom( value ){this._maxZoom = value; } 359 | 360 | }; 361 | /** 362 | * changes the Tile provider 363 | * @param provider url 364 | * @param domains sub domains 365 | * @param tileSize 366 | */ 367 | function setProvider( provider, domains, tileSize ) 368 | { 369 | if( this.provider == provider )return; 370 | 371 | this.dispose(); 372 | 373 | this.provider = provider || "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 374 | this.domains = domains || ["a","b","c"]; 375 | this.scale = tileSize / 256; 376 | 377 | this.mercator = new Mercator( 256 * this.scale ); 378 | 379 | this.setSize( this._width, this._height ); 380 | 381 | //this.setView(); 382 | 383 | } 384 | /** 385 | * sets the view rect size 386 | * @param w 387 | * @param h 388 | * @param apply boolean to apply the transform only 389 | */ 390 | function setSize( w,h ) 391 | { 392 | this._width = w || 256; 393 | this._height = h || 256; 394 | 395 | this.viewRect = new Rect( 0,0,this._width,this._height ); 396 | 397 | if( this.canvas != null ){ 398 | 399 | // set the scaled resolution 400 | this.canvas.width = this.viewRect.w * this.scale; 401 | this.canvas.height = this.viewRect.h * this.scale; 402 | 403 | // and the actual size 404 | this.canvas.style.width = ( this.viewRect.w ) + 'px'; 405 | this.canvas.style.height = ( this.viewRect.h ) + 'px'; 406 | } 407 | 408 | } 409 | 410 | function renderTiles() { 411 | 412 | this.ctx.clearRect( 0, 0, this.canvas.width, this.canvas.height ); 413 | 414 | var c = this.getViewRectCenterPixels(); 415 | var ctx = this.ctx; 416 | var zoom = this.zoom; 417 | var viewRect = this.viewRect; 418 | var scale = this.scale; 419 | this.loadedTiles.forEach(function(tile) 420 | { 421 | if( tile.zoom == zoom ) 422 | { 423 | var px = viewRect.w / 2 * scale + ( tile.px - c[ 0 ] ); 424 | var py = viewRect.h / 2 * scale + ( tile.py - c[ 1 ] ); 425 | 426 | ctx.drawImage( tile.img, Math.round( px ), Math.round( py ) ); 427 | 428 | } 429 | }); 430 | 431 | this.eventEmitter.emit( Map.ON_TEXTURE_UPDATE, this.ctx ); 432 | 433 | } 434 | 435 | /** 436 | * returns an array of the latitude/longitude in degrees 437 | * @returns {*[]} 438 | */ 439 | function getLatLng(){ 440 | 441 | return [ this.latitude, this.longitude ]; 442 | } 443 | 444 | /** 445 | * returns the bounds of the view rect as an array of the latitude/longitude in degrees 446 | * @returns [ top left lat, top left lon, bottom right lat, bottom right lon] 447 | */ 448 | function viewRectToLatLng( lat, lng, zoom ){ 449 | 450 | var c = this.mercator.latLngToPixels( -lat, lng, zoom ); 451 | var w = this.viewRect.w * this.scale; 452 | var h = this.viewRect.h * this.scale; 453 | var tl = this.mercator.pixelsToLatLng( c[ 0 ] - w / 2 * this.scale, c[ 1 ] - h / 2 * this.scale, zoom ); 454 | var br = this.mercator.pixelsToLatLng( c[ 0 ] + w / 2 * this.scale, c[ 1 ] + h / 2 * this.scale, zoom ); 455 | return [ -tl[ 0 ], tl[ 1 ], -br[ 0 ], br[ 1 ] ]; 456 | 457 | } 458 | 459 | /** 460 | * returns the bounds of the view rect as an object of the latitude/longitude in degrees 461 | * @returns {left lon, top lat, right lon, bottom lat} 462 | */ 463 | function getViewPortBounds() 464 | { 465 | var bounds = this.viewRectToLatLng( this.latitude, this.longitude, this.zoom ); 466 | return{ 467 | left: bounds[1], 468 | top: bounds[0], 469 | right: bounds[3], 470 | bottom: bounds[2] 471 | }; 472 | 473 | } 474 | 475 | /** 476 | * converts local X & Y coordinates and zoom level to latitude and longitude 477 | * @param x X position on the canvas 478 | * @param y Y position on the canvas 479 | * @param zoom zoom level (optional, falls back to the map's current zoom level) 480 | * @returns {*} array[ lat, lon ] in degrees 481 | */ 482 | function pixelsToLatLon( x,y, zoom ) 483 | { 484 | 485 | var c = this.mercator.latLngToPixels( -this.latitude, this.longitude, zoom || this.zoom ); 486 | var pos = map.mercator.pixelsToLatLng( c[ 0 ] - this.width / 2 + x, c[ 1 ] - this.height / 2 + y, zoom || this.zoom ); 487 | pos[0] *= -1; 488 | return pos; 489 | 490 | } 491 | 492 | 493 | /** 494 | * converts latitude/longitude at a given zoom level to a local XY position 495 | * @param lat latitude in degrees 496 | * @param lon longitude in degress 497 | * @param zoom zoom level (optional, falls back to the map's current zoom level) 498 | * @returns {*} array[ X, Y ] in pixels 499 | */ 500 | function latLonToPixels( lat,lon, zoom ) 501 | { 502 | 503 | var c = this.mercator.latLngToPixels( -this.latitude, this.longitude, zoom || this.zoom ); 504 | var p = this.mercator.latLngToPixels( -lat, lon, zoom || this.zoom ); 505 | return [ (p[0]-c[0]) + this.width / 2, ( p[1] - c[1] ) + this.height / 2 ]; 506 | 507 | } 508 | 509 | /** 510 | * returns the bounds of the view rect as rectangle (Rect object) of pixels in absolute coordinates 511 | * @returns new Rect( absolute x, absolute y, width, height ) 512 | */ 513 | function canvasPixelToLatLng( px, py, zoom ){ 514 | 515 | var c = this.mercator.latLngToPixels( -ma, lng, zoom || map.zoom ); 516 | return new Rect( c[ 0 ]-this.viewRect.w/2, c[ 1 ]-this.viewRect.h/2, this.viewRect.w, this.viewRect.h ); 517 | } 518 | 519 | /** 520 | * returns an array of the latitude/longitude of the viewrect center in degrees 521 | * @returns {*[]} 522 | */ 523 | function getViewRectCenter() 524 | { 525 | var bounds = this.viewRectToLatLng( this.latitude, this.longitude, this.zoom, this.viewRect); 526 | return [ utils.map(.5,0,1, -bounds[0], -bounds[2] ), utils.map(.5,0,1, bounds[1], bounds[3] )]; 527 | } 528 | 529 | /** 530 | * returns an array of the absolute x/y coordinates of the viewrect center in pixels 531 | * @returns {*[]} 532 | */ 533 | function getViewRectCenterPixels() 534 | { 535 | return this.mercator.latLngToPixels( -this.latitude, this.longitude, this.zoom ); 536 | } 537 | 538 | /** 539 | * retrieves a list of tiles (loaded or not) that lie within the viewrect 540 | * @param zoom 541 | * @returns {Array} 542 | */ 543 | function viewRectTiles( zoom ) 544 | { 545 | zoom = zoom || this.zoom; 546 | var bounds = viewRectToLatLng( this.latitude, this.longitude, zoom, this.viewRect); 547 | var tl = this.mercator.latLonToTile(-bounds[0], bounds[1], zoom); 548 | var br = this.mercator.latLonToTile(-bounds[2], bounds[3], zoom); 549 | var u = 0; 550 | var v = 0; 551 | var tiles = []; 552 | for (var i = tl[0]; i <= br[0] ; i++) 553 | { 554 | v = 0; 555 | for (var j = tl[1]; j <= br[1]; j++) 556 | { 557 | var key = this.mercator.tileXYToQuadKey(i, j, zoom); 558 | var exists = false; 559 | for (var k = 0; k < this.loadedTiles.length; k++){ 560 | 561 | if (this.loadedTiles[k].key == key ){ 562 | 563 | this.loadedTiles[k].viewRectPosition[0] = u; 564 | this.loadedTiles[k].viewRectPosition[1] = v; 565 | tiles.push(this.loadedTiles[k]); 566 | exists = true; 567 | break; 568 | } 569 | } 570 | if( exists == false ) { 571 | 572 | for (k = 0; k < this.tiles.length; k++) { 573 | 574 | if (this.tiles[k].key == key) { 575 | 576 | this.tiles[k].viewRectPosition[0] = u; 577 | this.tiles[k].viewRectPosition[1] = v; 578 | tiles.push(this.tiles[k]); 579 | exists = true; 580 | break; 581 | } 582 | } 583 | } 584 | v++; 585 | } 586 | u++; 587 | } 588 | return tiles; 589 | } 590 | 591 | /** 592 | * gets a list of the tiles that are displayed in the viewrect at the given lat/lon/zoom 593 | * @param lat latitude in degrees 594 | * @param lng longitude in degrees 595 | * @param zoom zoom level 596 | * @param viewRect 597 | * @returns {Array} an array of Tile objects 598 | */ 599 | function getVisibleTiles( lat, lng, zoom, viewRect ) 600 | { 601 | 602 | var bounds = this.viewRectToLatLng( lat, lng, zoom, viewRect ); 603 | var tl = this.mercator.latLonToTile( -bounds[0], bounds[1], zoom ); 604 | var br = this.mercator.latLonToTile( -bounds[2], bounds[3], zoom ); 605 | 606 | var tiles = []; 607 | for( var i = tl[ 0 ]; i <= br[ 0 ]; i++ ) 608 | { 609 | for( var j = tl[ 1 ]; j <= br[ 1 ]; j++ ) 610 | { 611 | var key = this.mercator.tileXYToQuadKey( i, j, zoom ); 612 | 613 | //check if the tile was already loaded/being loaded 614 | if( this.keys.indexOf( key ) == -1 ) 615 | { 616 | var tile = new Tile( this, key ); 617 | tiles.push( tile ); 618 | this.keys.push( key ); 619 | } 620 | } 621 | } 622 | return tiles; 623 | 624 | } 625 | 626 | /** 627 | * sets the map view 628 | * @param lat 629 | * @param lng 630 | * @param zoom 631 | */ 632 | function setView( lat, lng, zoom ) 633 | { 634 | this.latitude = lat || this.latitude; 635 | this.longitude = lng || this.longitude; 636 | this.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, zoom || this.zoom ) ); 637 | 638 | this.load(); 639 | this.renderTiles(); 640 | } 641 | 642 | /** 643 | * loads the visibles tiles 644 | */ 645 | function load() 646 | { 647 | 648 | var tiles = this.getVisibleTiles( this.latitude, this.longitude, this.zoom, this.viewRect ); 649 | 650 | if( tiles.length == 0 && this.tiles.length == 0 ) 651 | { 652 | this.eventEmitter.emit( Map.ON_LOAD_COMPLETE, -1 ); 653 | return; 654 | } 655 | 656 | for ( var i = 0; i < tiles.length; i++ ) { 657 | tiles[ i ].eventEmitter.on( Tile.ON_TILE_LOADED, this.appendTile ); 658 | tiles[ i ].load(); 659 | } 660 | 661 | this.tiles = this.tiles.concat( tiles ); 662 | 663 | } 664 | 665 | /** 666 | * adds a loaded tile to the pool 667 | * @param tile 668 | */ 669 | function appendTile( tile ) 670 | { 671 | 672 | var scope = tile.map; 673 | scope.tiles.splice( scope.tiles.indexOf( tile ), 1 ); 674 | 675 | var img = tile.img; 676 | scope.loadedTiles.push( tile ); 677 | scope.renderTiles( true ); 678 | 679 | scope.eventEmitter.emit( Map.ON_TILE_LOADED, tile ); 680 | 681 | if( scope.tiles.length == 0 ){ 682 | 683 | scope.eventEmitter.emit( Map.ON_LOAD_COMPLETE, 0 ); 684 | } 685 | } 686 | 687 | /** 688 | * gets the map resolution in pixels at a given zoom level 689 | * @param zoom 690 | * @returns {*} 691 | */ 692 | function resolution( zoom ) 693 | { 694 | zoom = zoom || this.zoom; 695 | return this.mercator.resolution( zoom ); 696 | } 697 | 698 | /** 699 | * destroys all the tiles 700 | */ 701 | function dispose() 702 | { 703 | var scope = this; 704 | this.tiles.forEach( function(tile){ tile.eventEmitter.removeListener( Tile.ON_TILE_LOADED, scope.appendTile ); tile.valid = false; tile.dispose(); }); 705 | this.loadedTiles.forEach( function(tile){ tile.dispose(); }); 706 | 707 | this.tiles = []; 708 | this.loadedTiles = []; 709 | this.keys = []; 710 | 711 | } 712 | 713 | /** 714 | * returns the bounds of the view rect as rectangle (Rect object) of pixels in absolute coordinates 715 | * @returns {*[absolute x, absolute y, width, height ]} 716 | */ 717 | function viewRectToPixels( lat, lng, zoom ){ 718 | 719 | var c = this.mercator.latLngToPixels( -lat, lng, zoom ); 720 | return new Rect( c[ 0 ], c[ 1 ], this.viewRect.w, this.viewRect.h ); 721 | } 722 | 723 | //Map constants 724 | Map.ON_LOAD_COMPLETE = 0; 725 | Map.ON_TILE_LOADED = 1; 726 | Map.ON_TEXTURE_UPDATE = 2; 727 | 728 | 729 | var _p = Map.prototype; 730 | _p.constructor = Map; 731 | 732 | _p.setProvider = setProvider; 733 | _p.setSize = setSize; 734 | _p.renderTiles = renderTiles; 735 | _p.pixelsToLatLon = pixelsToLatLon; 736 | _p.latLonToPixels = latLonToPixels; 737 | _p.viewRectToLatLng = viewRectToLatLng; 738 | _p.getViewPortBounds = getViewPortBounds; 739 | _p.getViewRectCenter = getViewRectCenter; 740 | _p.getViewRectCenterPixels = getViewRectCenterPixels; 741 | _p.setView = setView; 742 | _p.viewRectTiles = viewRectTiles; 743 | _p.getVisibleTiles = getVisibleTiles; 744 | _p.load = load; 745 | _p.appendTile = appendTile; 746 | _p.resolution = resolution; 747 | _p.dispose = dispose; 748 | _p.viewRectToPixels = viewRectToPixels; 749 | 750 | return Map; 751 | 752 | }(); 753 | 754 | 755 | },{"./MapUtils":3,"./Mercator":4,"./Rect":5,"./Tile":6,"events":1}],3:[function(require,module,exports){ 756 | 757 | module.exports.MapUtils = function( exports ) 758 | { 759 | var RAD = Math.PI / 180; 760 | 761 | function isPowerOfTwo( value ){ 762 | 763 | return ( (value & -value) == value ); 764 | } 765 | 766 | function powerTwo(val){ 767 | 768 | if( isPowerOfTwo( val ) )return val 769 | var b = 1; 770 | while ( b < ~~( val ) )b = b << 1; 771 | return b; 772 | } 773 | 774 | //http://www.movable-type.co.uk/scripts/latlong.html 775 | function latLngDistance(lat1, lng1, lat2, lng2) 776 | { 777 | var R = EARTH_RADIUS; // km 778 | var p1 = lat1 * RAD; 779 | var p2 = lat2 * RAD; 780 | var tp = (lat2-lat1) * RAD; 781 | var td = (lng2-lng1) * RAD; 782 | 783 | var a = Math.sin(tp/2) * Math.sin(tp/2) + 784 | Math.cos(p1) * Math.cos(p2) * 785 | Math.sin(td/2) * Math.sin(td/2); 786 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 787 | return R * c; 788 | } 789 | function lerp ( t, a, b ){ return a + t * ( b - a ); } 790 | function norm( t, a, b ){return ( t - a ) / ( b - a );} 791 | function map( t, a0, b0, a1, b1 ){ return lerp( norm( t, a0, b0 ), a1, b1 );} 792 | 793 | // methods 794 | 795 | exports.lerp = lerp; 796 | exports.norm = norm; 797 | exports.map = map; 798 | 799 | exports.isPowerOfTwo = isPowerOfTwo; 800 | exports.powerTwo = powerTwo; 801 | exports.latLngDistance = latLngDistance; 802 | 803 | return exports; 804 | 805 | }( module.exports ); 806 | 807 | },{}],4:[function(require,module,exports){ 808 | /* 809 | 810 | //from http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ 811 | 812 | #!/usr/bin/env python 813 | ############################################################################### 814 | # $Id$ 815 | # 816 | # Project: GDAL2Tiles, Google Summer of Code 2007 & 2008 817 | # Global Map Tiles Classes 818 | # Purpose: Convert a raster into TMS tiles, create KML SuperOverlay EPSG:4326, 819 | # generate a simple HTML viewers based on Google Maps and OpenLayers 820 | # Author: Klokan Petr Pridal, klokan at klokan dot cz 821 | # Web: http://www.klokan.cz/projects/gdal2tiles/ 822 | # 823 | ############################################################################### 824 | # Copyright (c) 2008 Klokan Petr Pridal. All rights reserved. 825 | # 826 | # Permission is hereby granted, free of charge, to any person obtaining a 827 | # copy of this software and associated documentation files (the "Software"), 828 | # to deal in the Software without restriction, including without limitation 829 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 830 | # and/or sell copies of the Software, and to permit persons to whom the 831 | # Software is furnished to do so, subject to the following conditions: 832 | # 833 | # The above copyright notice and this permission notice shall be included 834 | # in all copies or substantial portions of the Software. 835 | # 836 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 837 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 838 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 839 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 840 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 841 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 842 | # DEALINGS IN THE SOFTWARE. 843 | ############################################################################### 844 | 845 | globalmaptiles.py 846 | 847 | Global Map Tiles as defined in Tile Map Service (TMS) Profiles 848 | ============================================================== 849 | 850 | Functions necessary for generation of global tiles used on the web. 851 | It contains classes implementing coordinate conversions for: 852 | 853 | - GlobalMercator (based on EPSG:900913 = EPSG:3785) 854 | for Google Maps, Yahoo Maps, Microsoft Maps compatible tiles 855 | - GlobalGeodetic (based on EPSG:4326) 856 | for OpenLayers Base Map and Google Earth compatible tiles 857 | 858 | More info at: 859 | 860 | http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification 861 | http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation 862 | http://msdn.microsoft.com/en-us/library/bb259689.aspx 863 | http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates 864 | 865 | Created by Klokan Petr Pridal on 2008-07-03. 866 | Google Summer of Code 2008, project GDAL2Tiles for OSGEO. 867 | 868 | In case you use this class in your product, translate it to another language 869 | or find it usefull for your project please let me know. 870 | My email: klokan at klokan dot cz. 871 | I would like to know where it was used. 872 | 873 | Class is available under the open-source GDAL license (www.gdal.org). 874 | 875 | */ 876 | 877 | module.exports = function() { 878 | 879 | function Mercator( tile_size, earth_radius ) 880 | { 881 | this.init(tile_size, earth_radius); 882 | } 883 | 884 | function init(tile_size, earth_radius) 885 | { 886 | //Initialize the TMS Global Mercator pyramid 887 | this.tileSize = tile_size || 256; 888 | 889 | this.earthRadius = earth_radius || 6378137; 890 | 891 | // 156543.03392804062 for tileSize 256 pixels 892 | this.initialResolution = 2 * Math.PI * this.earthRadius / this.tileSize; 893 | 894 | // 20037508.342789244 895 | this.originShift = 2 * Math.PI * this.earthRadius / 2.0; 896 | 897 | } 898 | 899 | 900 | //converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913 901 | function latLonToMeters( lat, lon ) 902 | { 903 | var mx = lon * this.originShift / 180.0; 904 | var my = Math.log( Math.tan((90 + lat ) * Math.PI / 360.0)) / (Math.PI / 180.0); 905 | my = my * this.originShift / 180.0; 906 | return [mx, my]; 907 | } 908 | 909 | //converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum 910 | function metersToLatLon( mx, my ) 911 | { 912 | var lon = (mx / this.originShift) * 180.0; 913 | var lat = (my / this.originShift) * 180.0; 914 | lat = 180 / Math.PI * ( 2 * Math.atan( Math.exp(lat * Math.PI / 180.0 ) ) - Math.PI / 2.0); 915 | return [lat, lon]; 916 | } 917 | 918 | //converts pixel coordinates in given zoom level of pyramid to EPSG:900913 919 | function pixelsToMeters( px, py, zoom ) 920 | { 921 | var res = this.resolution(zoom); 922 | var mx = px * res - this.originShift; 923 | var my = py * res - this.originShift; 924 | return [mx, my]; 925 | } 926 | 927 | //converts EPSG:900913 to pyramid pixel coordinates in given zoom level 928 | function metersToPixels( mx, my, zoom ) 929 | { 930 | var res = this.resolution( zoom ); 931 | var px = (mx + this.originShift) / res; 932 | var py = (my + this.originShift) / res; 933 | return [px, py]; 934 | } 935 | 936 | //returns tile for given mercator coordinates 937 | function metersToTile( mx, my, zoom ) 938 | { 939 | var pxy = this.metersToPixels(mx, my, zoom); 940 | return this.pixelsToTile(pxy[0], pxy[1]); 941 | } 942 | 943 | //returns a tile covering region in given pixel coordinates 944 | function pixelsToTile( px, py) 945 | { 946 | var tx = parseInt( Math.ceil( px / parseFloat( this.tileSize ) ) - 1); 947 | var ty = parseInt( Math.ceil( py / parseFloat( this.tileSize ) ) - 1); 948 | return [tx, ty]; 949 | } 950 | 951 | //returns a tile covering region the given lat lng coordinates 952 | function latLonToTile( lat, lng, zoom ) 953 | { 954 | var px = this.latLngToPixels( lat, lng, zoom ); 955 | return this.pixelsToTile( px[ 0 ], px[ 1 ] ); 956 | } 957 | 958 | //Move the origin of pixel coordinates to top-left corner 959 | function pixelsToRaster( px, py, zoom ) 960 | { 961 | var mapSize = this.tileSize << zoom; 962 | return [px, mapSize - py]; 963 | } 964 | 965 | //returns bounds of the given tile in EPSG:900913 coordinates 966 | function tileMetersBounds( tx, ty, zoom ) 967 | { 968 | var min = this.pixelsToMeters(tx * this.tileSize, ty * this.tileSize, zoom); 969 | var max = this.pixelsToMeters((tx + 1) * this.tileSize, (ty + 1) * this.tileSize, zoom); 970 | return [ min[0], min[1], max[0], max[1] ]; 971 | } 972 | 973 | //returns bounds of the given tile in pixels 974 | function tilePixelsBounds( tx, ty, zoom ) 975 | { 976 | var bounds = this.tileMetersBounds( tx, ty, zoom ); 977 | var min = this.metersToPixels(bounds[0], bounds[1], zoom ); 978 | var max = this.metersToPixels(bounds[2], bounds[3], zoom ); 979 | return [ min[0], min[1], max[0], max[1] ]; 980 | } 981 | 982 | //returns bounds of the given tile in latutude/longitude using WGS84 datum 983 | function tileLatLngBounds( tx, ty, zoom ) 984 | { 985 | var bounds = this.tileMetersBounds( tx, ty, zoom ); 986 | var min = this.metersToLatLon(bounds[0], bounds[1]); 987 | var max = this.metersToLatLon(bounds[2], bounds[3]); 988 | return [ min[0], min[1], max[0], max[1] ]; 989 | } 990 | 991 | //resolution (meters/pixel) for given zoom level (measured at Equator) 992 | function resolution( zoom ) 993 | { 994 | return this.initialResolution / Math.pow( 2, zoom ); 995 | } 996 | 997 | /** 998 | * untested... 999 | * @param pixelSize 1000 | * @returns {number} 1001 | * @constructor 1002 | */ 1003 | function zoomForPixelSize( pixelSize ) 1004 | { 1005 | var i = 30; 1006 | while( pixelSize > this.resolution(i) ) 1007 | { 1008 | i--; 1009 | if( i <= 0 )return 0; 1010 | } 1011 | return i; 1012 | } 1013 | 1014 | //returns the lat lng of a pixel X/Y coordinates at given zoom level 1015 | function pixelsToLatLng( px, py, zoom ) 1016 | { 1017 | var meters = this.pixelsToMeters( px, py, zoom ); 1018 | return this.metersToLatLon( meters[ 0 ], meters[ 1 ] ); 1019 | } 1020 | 1021 | //returns the pixel X/Y coordinates at given zoom level from a given lat lng 1022 | function latLngToPixels( lat, lng, zoom ) 1023 | { 1024 | var meters = this.latLonToMeters( lat, lng, zoom ); 1025 | return this.metersToPixels( meters[ 0 ], meters[ 1 ], zoom ); 1026 | } 1027 | 1028 | //retrieves a given tile from a given lat lng 1029 | function latLngToTile( lat, lng, zoom ) 1030 | { 1031 | var meters = this.latLonToMeters( lat, lng ); 1032 | return this.metersToTile( meters[ 0 ], meters[ 1 ], zoom ); 1033 | } 1034 | 1035 | //encodes the tlie X / Y coordinates & zoom level into a quadkey 1036 | function tileXYToQuadKey( tx,ty,zoom ) 1037 | { 1038 | var quadKey = ''; 1039 | for ( var i = zoom; i > 0; i-- ) 1040 | { 1041 | var digit = 0; 1042 | var mask = 1 << ( i - 1 ); 1043 | if( ( tx & mask ) != 0 ) 1044 | { 1045 | digit++; 1046 | } 1047 | if( ( ty & mask ) != 0 ) 1048 | { 1049 | digit++; 1050 | digit++; 1051 | } 1052 | quadKey += digit; 1053 | } 1054 | return quadKey; 1055 | } 1056 | 1057 | //decodes the tlie X / Y coordinates & zoom level into a quadkey 1058 | function quadKeyToTileXY( quadKeyString ) 1059 | { 1060 | var tileX = 0; 1061 | var tileY = 0; 1062 | var quadKey = quadKeyString.split( '' ); 1063 | var levelOfDetail = quadKey.length; 1064 | 1065 | for( var i = levelOfDetail; i > 0; i--) 1066 | { 1067 | var mask = 1 << ( i - 1 ); 1068 | switch( quadKey[ levelOfDetail - i ] ) 1069 | { 1070 | case '0': 1071 | break; 1072 | 1073 | case '1': 1074 | tileX |= mask; 1075 | break; 1076 | 1077 | case '2': 1078 | tileY |= mask; 1079 | break; 1080 | 1081 | case '3': 1082 | tileX |= mask; 1083 | tileY |= mask; 1084 | break; 1085 | 1086 | default: 1087 | return null; 1088 | } 1089 | } 1090 | return { tx:tileX, ty:tileY, zoom:levelOfDetail }; 1091 | } 1092 | 1093 | 1094 | // public methods 1095 | 1096 | var _p = Mercator.prototype; 1097 | _p.constructor = Mercator; 1098 | 1099 | _p.init = init; 1100 | _p.latLonToMeters = latLonToMeters; 1101 | _p.metersToLatLon = metersToLatLon; 1102 | _p.pixelsToMeters = pixelsToMeters; 1103 | _p.metersToPixels = metersToPixels; 1104 | _p.metersToTile = metersToTile; 1105 | _p.pixelsToTile = pixelsToTile; 1106 | _p.latLonToTile = latLonToTile; 1107 | _p.pixelsToRaster = pixelsToRaster; 1108 | _p.tileMetersBounds = tileMetersBounds; 1109 | _p.tilePixelsBounds = tilePixelsBounds; 1110 | _p.tileLatLngBounds = tileLatLngBounds; 1111 | _p.resolution = resolution; 1112 | _p.zoomForPixelSize = zoomForPixelSize; 1113 | _p.pixelsToLatLng = pixelsToLatLng; 1114 | _p.latLngToPixels = latLngToPixels; 1115 | _p.latLngToTile = latLngToTile; 1116 | _p.tileXYToQuadKey = tileXYToQuadKey; 1117 | _p.quadKeyToTileXY = quadKeyToTileXY; 1118 | 1119 | return Mercator; 1120 | 1121 | 1122 | }(); 1123 | },{}],5:[function(require,module,exports){ 1124 | 1125 | module.exports = function() 1126 | { 1127 | /** 1128 | * the physical representation of the map box 1129 | * @param x 1130 | * @param y 1131 | * @param _w 1132 | * @param _h 1133 | * @constructor 1134 | */ 1135 | 1136 | function Rect( x,y,w,h ) { 1137 | 1138 | this.x = x; 1139 | this.y = y; 1140 | this.w = w; 1141 | this.h = h; 1142 | 1143 | } 1144 | Rect.prototype = 1145 | { 1146 | get x(){ return this._x;}, set x( value ){ this._x = value; return this._x; }, 1147 | get y(){ return this._y;}, set y( value ){ this._y = value; return this._y; }, 1148 | get w(){ return this._w;}, set w( value ){ this._w = value; return this._w; }, 1149 | get h(){ return this._h;}, set h( value ){ this._h = value; return this._h; } 1150 | }; 1151 | 1152 | function containsPoint( _x, _y ) 1153 | { 1154 | if( _x < this.x ) return false; 1155 | if( _y < this.y ) return false; 1156 | if( _x > this.x + this.w ) return false; 1157 | return _y <= this.y + this.h; 1158 | } 1159 | 1160 | function isContained( _x, _y, _w, _h ) 1161 | { 1162 | return ( this.x >= _x 1163 | && this.y >= _y 1164 | && this.x + this.w <= _x + _w 1165 | && this.y + this.h <= _y + _h ); 1166 | } 1167 | 1168 | function intersect( _x, _y, _w, _h ) 1169 | { 1170 | return !( _x > this.x + this.w || _x+_w < this.x || _y > this.y + this.h || _y+_h< this.y ); 1171 | } 1172 | 1173 | function intersection( other ) 1174 | { 1175 | if( this.intersect( other.x, other.y, other.x + other.w, other.y + other.h ) ) 1176 | { 1177 | var x = Math.max( this.x, other.x ); 1178 | var y = Math.max( this.y, other.y ); 1179 | var w = Math.min( this.x + this.w, other.x + other.w ) - x; 1180 | var h = Math.min( this.y + this.h, other.y + other.h ) - y; 1181 | return new Rect( x,y,w,h ); 1182 | } 1183 | return null; 1184 | } 1185 | 1186 | var _p = Rect.prototype; 1187 | 1188 | _p.constructor = Rect; 1189 | _p.containsPoint = containsPoint; 1190 | _p.isContained = isContained; 1191 | _p.intersect = intersect; 1192 | _p.intersection = intersection; 1193 | 1194 | return Rect; 1195 | 1196 | }(); 1197 | },{}],6:[function(require,module,exports){ 1198 | /* 1199 | * Tile object, holds reference to: 1200 | * 1201 | * tile top left lat/lon 1202 | * tile id 1203 | * tile X/Y 1204 | * tile pixel X/Y 1205 | * the tile's DOM element 1206 | * 1207 | * + some helper methods ( contains, isContained, intersect ) 1208 | * 1209 | * @param map the map this tile is bound to 1210 | * @param quadKey optional, can be set with initFromQuadKey() 1211 | * @constructor 1212 | */ 1213 | 1214 | var events = require( 'events' ); 1215 | module.exports = function() 1216 | { 1217 | 1218 | var undef; 1219 | 1220 | /** 1221 | * @param map Map instance this tile is associated with 1222 | * @param map the map this tile is bound to 1223 | * @param quadKey the QuadKey of this Tile 1224 | * @constructor 1225 | */ 1226 | function Tile( map, quadKey ) 1227 | { 1228 | 1229 | if( map == null )throw new Error( "Tile: no map associated to Tile." ); 1230 | this.map = map; 1231 | 1232 | this.valid = true; 1233 | this.loaded = false; 1234 | 1235 | this.key = ""; 1236 | this.id = -1; 1237 | 1238 | this.tx = -1; 1239 | this.ty = -1; 1240 | this.zoom = -1; 1241 | 1242 | this.lat = -1; 1243 | this.lng = -1; 1244 | 1245 | this.meterBounds = undef; 1246 | this.mx = -1; 1247 | this.my = -1; 1248 | 1249 | this.pixelBounds = undef; 1250 | this.px = -1; 1251 | this.py = -1; 1252 | 1253 | //raster position (tile position relative to the canvas) 1254 | this.rx = 0; 1255 | this.ry = 0; 1256 | 1257 | this.viewRectPosition = [0,0]; 1258 | this.latLngBounds = undef; 1259 | 1260 | this.img = new Image(); 1261 | this.url = ""; 1262 | 1263 | this.eventEmitter = new events.EventEmitter(); 1264 | 1265 | this.quadKey = this.key = quadKey; 1266 | if( this.quadKey != undef ) 1267 | { 1268 | this.initFromQuadKey( this.quadKey ); 1269 | } 1270 | 1271 | } 1272 | 1273 | 1274 | 1275 | function initFromTileXY(x, y, zoom) 1276 | { 1277 | var quadKey = this.map.mercator.tileXYToQuadKey(x, y, zoom); 1278 | initFromQuadKey(quadKey); 1279 | } 1280 | 1281 | function initFromQuadKey(quadKey) 1282 | { 1283 | 1284 | var tile = this.map.mercator.quadKeyToTileXY(quadKey); 1285 | if ( tile == undef) { 1286 | this.valid = false; 1287 | return; 1288 | } 1289 | 1290 | var center = this.map.mercator.tileLatLngBounds(this.tx + .5, this.ty + .5, this.zoom); 1291 | this.lat = -center[0]; 1292 | this.lng = center[1]; 1293 | 1294 | this.key = quadKey; 1295 | this.id = parseInt(quadKey); 1296 | 1297 | this.tx = tile.tx; 1298 | this.ty = tile.ty; 1299 | this.zoom = tile.zoom; 1300 | 1301 | this.meterBounds = this.map.mercator.tileMetersBounds( this.tx, this.ty, this.zoom); 1302 | this.mx = this.meterBounds[0]; 1303 | this.my = this.meterBounds[1]; 1304 | 1305 | this.pixelBounds = this.map.mercator.tilePixelsBounds(this.tx, this.ty, this.zoom); 1306 | this.px = this.pixelBounds[0]; 1307 | this.py = this.pixelBounds[1]; 1308 | 1309 | this.latLngBounds = this.map.mercator.tileLatLngBounds(this.tx, this.ty, this.zoom); 1310 | this.latLngBounds[0] *= -1; 1311 | this.latLngBounds[2] *= -1; 1312 | 1313 | this.url = this.getMapUrl(this.tx, this.ty, this.zoom); 1314 | 1315 | } 1316 | 1317 | function load( callback ) 1318 | { 1319 | if( !this.valid ) 1320 | { 1321 | console.log( "invalid tile, not loading"); 1322 | return; 1323 | } 1324 | 1325 | var scope = this; 1326 | 1327 | this.img.tile = this; 1328 | this.img.crossOrigin = 'anonymous'; 1329 | this.img.onload = function (e) { 1330 | 1331 | if( scope.map == undef ) 1332 | { 1333 | console.warn( 'loaded Tile has no associated map > ', scope.key, scope.zoom ); 1334 | return; 1335 | } 1336 | 1337 | if( scope.map.mercator.tileSize != e.target.width ){ 1338 | scope.rescaleImage( e.target, scope.map.mercator.tileSize, window.devicePixelRatio ); 1339 | } 1340 | 1341 | scope.loaded = true; 1342 | scope.eventEmitter.emit( Tile.ON_TILE_LOADED, scope ); 1343 | 1344 | }; 1345 | this.img.setAttribute("key", this.key); 1346 | this.img.src = this.url; 1347 | } 1348 | 1349 | /** 1350 | * rescales the image so that it fits the map's scale 1351 | * @param img input image 1352 | * @param tileSize destination tileSize 1353 | * @param scale scale factor 1354 | */ 1355 | function rescaleImage( img, tileSize, scale ) 1356 | { 1357 | 1358 | var canvas = document.createElement( 'canvas' ); 1359 | var w = canvas.width = img.width ; 1360 | var h = canvas.height = img.height; 1361 | 1362 | //collect image data 1363 | var ctx = canvas.getContext("2d"); 1364 | ctx.drawImage( img, 0,0,w,h ); 1365 | var srcData = ctx.getImageData( 0,0,w,h).data; 1366 | 1367 | //result 1368 | var imgOut = ctx.createImageData(tileSize,tileSize); 1369 | var out = imgOut.data; 1370 | 1371 | //nearest neighbours upscale 1372 | for( var i = 0; i < srcData.length; i+=4 ) 1373 | { 1374 | var x = ( i/4 % w ) * scale; 1375 | var y = ~~( i/4 / w ) * scale; 1376 | for( var j = x; j <= x + scale; j++ ) 1377 | { 1378 | if( x >= tileSize )continue; 1379 | for( var k = y; k <= y + scale; k++ ) { 1380 | 1381 | var id = ( ~~( j ) + ~~( k ) * tileSize ) * 4; 1382 | out[id] = srcData[i ]; 1383 | out[id + 1] = srcData[i+1]; 1384 | out[id + 2] = srcData[i+2]; 1385 | out[id + 3] = srcData[i+3]; 1386 | } 1387 | } 1388 | } 1389 | canvas.width = canvas.height = tileSize; 1390 | imgOut.data = out; 1391 | ctx.putImageData(imgOut,0,0); 1392 | 1393 | //replace img with canvas 1394 | delete this.img; 1395 | this.img = canvas; 1396 | 1397 | } 1398 | 1399 | function getMapUrl(x, y, zl) 1400 | { 1401 | var url = this.map.provider; 1402 | url = url.replace(/\{x\}/, x); 1403 | url = url.replace(/\{y\}/, y); 1404 | url = url.replace(/\{z\}/, zl); 1405 | 1406 | if( url.lastIndexOf("{s}") != -1 ){ 1407 | var domains = this.map.domains || [""]; 1408 | url = url.replace(/\{s\}/, domains[ parseInt( Math.random() * domains.length ) ] ); 1409 | } 1410 | return url; 1411 | } 1412 | 1413 | function containsLatLng(lat, lng){ 1414 | 1415 | if (lat > this.latLngBounds[0]) return false; 1416 | if (lng < this.latLngBounds[1]) return false; 1417 | if (lat < this.latLngBounds[2]) return false; 1418 | if (lat > this.latLngBounds[3]) return false; 1419 | return true; 1420 | } 1421 | 1422 | function isContained(latLngBound){ 1423 | 1424 | return this.latLngBounds[0] >= latLngBound[0] 1425 | && this.latLngBounds[1] >= latLngBound[1] 1426 | && this.latLngBounds[2] <= latLngBound[2] 1427 | && this.latLngBounds[3] <= latLngBound[3]; 1428 | } 1429 | 1430 | function intersect(latLngBound){ 1431 | 1432 | return !( latLngBound[0] < this.latLngBounds[2] || 1433 | latLngBound[1] > this.latLngBounds[3] || 1434 | latLngBound[2] > this.latLngBounds[0] || 1435 | latLngBound[3] < this.latLngBounds[1] ); 1436 | } 1437 | 1438 | function dispose() { 1439 | 1440 | this.map = undef; 1441 | delete this.map; 1442 | this.meterBounds = undef; 1443 | delete this.meterBounds; 1444 | this.pixelBounds = undef; 1445 | delete this.pixelBounds; 1446 | 1447 | delete this.viewRectPosition; 1448 | delete this.latLngBounds; 1449 | delete this.img; 1450 | 1451 | this.eventEmitter.removeAllListeners(); 1452 | delete this.eventEmitter; 1453 | 1454 | } 1455 | 1456 | var _p = Tile.prototype; 1457 | _p.constructor = Tile; 1458 | 1459 | _p.initFromTileXY = initFromTileXY; 1460 | _p.initFromQuadKey = initFromQuadKey; 1461 | _p.load = load; 1462 | _p.rescaleImage = rescaleImage; 1463 | _p.getMapUrl = getMapUrl; 1464 | _p.containsLatLng = containsLatLng; 1465 | _p.isContained = isContained; 1466 | _p.intersect = intersect; 1467 | _p.dispose = dispose; 1468 | 1469 | Tile.ON_TILE_LOADED = 0; 1470 | 1471 | return Tile; 1472 | 1473 | }(); 1474 | 1475 | },{"events":1}]},{},[2])(2) 1476 | }); -------------------------------------------------------------------------------- /example/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 122 | 123 | -------------------------------------------------------------------------------- /example/controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | light map utils 7 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 235 | 236 |
237 | 238 |

239 |

240 | 241 | 242 |
243 | 244 | 245 | -------------------------------------------------------------------------------- /example/fonts/light-map.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoptere/light-map/7e34aaff9836b282ad1f3ff8acc9da76124e5773/example/fonts/light-map.eot -------------------------------------------------------------------------------- /example/fonts/light-map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/fonts/light-map.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoptere/light-map/7e34aaff9836b282ad1f3ff8acc9da76124e5773/example/fonts/light-map.ttf -------------------------------------------------------------------------------- /example/fonts/light-map.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoptere/light-map/7e34aaff9836b282ad1f3ff8acc9da76124e5773/example/fonts/light-map.woff -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | settings 36 | 37 |
38 | 47 | 48 |
49 | copyright 50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 | 62 |
63 | 64 | 65 |
66 | 67 | 68 |
69 | 70 |
71 | 72 | 73 |
74 | 75 | 76 |
77 |
78 | 79 | loading status 80 | 81 | 82 |
83 | 84 |
85 | 86 | 173 | 174 | -------------------------------------------------------------------------------- /example/light-map.min.js: -------------------------------------------------------------------------------- 1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Map=t()}}(function(){return function t(e,i,s){function n(o,h){if(!i[o]){if(!e[o]){var a="function"==typeof require&&require;if(!h&&a)return a(o,!0);if(r)return r(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var u=i[o]={exports:{}};e[o][0].call(u.exports,function(t){var i=e[o][1][t];return n(i?i:t)},u,u.exports,t,e,i,s)}return i[o].exports}for(var r="function"==typeof require&&require,o=0;ot||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},i.prototype.emit=function(t){var e,i,n,h,a,l;if(this._events||(this._events={}),"error"===t&&(!this._events.error||r(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(i=this._events[t],o(i))return!1;if(s(i))switch(arguments.length){case 1:i.call(this);break;case 2:i.call(this,arguments[1]);break;case 3:i.call(this,arguments[1],arguments[2]);break;default:for(n=arguments.length,h=new Array(n-1),a=1;n>a;a++)h[a-1]=arguments[a];i.apply(this,h)}else if(r(i)){for(n=arguments.length,h=new Array(n-1),a=1;n>a;a++)h[a-1]=arguments[a];for(l=i.slice(),n=l.length,a=0;n>a;a++)l[a].apply(this,h)}return!0},i.prototype.addListener=function(t,e){var n;if(!s(e))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,s(e.listener)?e.listener:e),this._events[t]?r(this._events[t])?this._events[t].push(e):this._events[t]=[this._events[t],e]:this._events[t]=e,r(this._events[t])&&!this._events[t].warned){var n;n=o(this._maxListeners)?i.defaultMaxListeners:this._maxListeners,n&&n>0&&this._events[t].length>n&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},i.prototype.on=i.prototype.addListener,i.prototype.once=function(t,e){function i(){this.removeListener(t,i),n||(n=!0,e.apply(this,arguments))}if(!s(e))throw TypeError("listener must be a function");var n=!1;return i.listener=e,this.on(t,i),this},i.prototype.removeListener=function(t,e){var i,n,o,h;if(!s(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(i=this._events[t],o=i.length,n=-1,i===e||s(i.listener)&&i.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(r(i)){for(h=o;h-->0;)if(i[h]===e||i[h].listener&&i[h].listener===e){n=h;break}if(0>n)return this;1===i.length?(i.length=0,delete this._events[t]):i.splice(n,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},i.prototype.removeAllListeners=function(t){var e,i;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(i=this._events[t],s(i))this.removeListener(t,i);else for(;i.length;)this.removeListener(t,i[i.length-1]);return delete this._events[t],this},i.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?s(this._events[t])?[this._events[t]]:this._events[t].slice():[]},i.listenerCount=function(t,e){var i;return i=t._events&&t._events[e]?s(t._events[e])?1:t._events[e].length:0}},{}],2:[function(t,e){var i=t("./Mercator"),s=t("./MapUtils"),n=t("./Rect"),r=t("./Tile"),o=t("events");e.exports=function(){function t(t,e,n,r,h,a){this.mercator=new i(256),this.eventEmitter=new o.EventEmitter,this._minZoom=Math.max(0,h),this._maxZoom=Math.max(1,a),this.latitude=0,this.longitude=0,this.zoom=0,this.tiles=[],this.keys=[],this.loadedTiles=[],this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.setSize(n,r),this.setProvider(t,e,256*window.devicePixelRatio),this.utils=s}function e(t,e,s){this.provider!=t&&(this.dispose(),this.provider=t||"http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",this.domains=e||["a","b","c"],this.scale=s/256,this.mercator=new i(256*this.scale),this.setSize(this._width,this._height))}function h(t,e){this._width=t||256,this._height=e||256,this.viewRect=new n(0,0,this._width,this._height),null!=this.canvas&&(this.canvas.width=this.viewRect.w*this.scale,this.canvas.height=this.viewRect.h*this.scale,this.canvas.style.width=this.viewRect.w+"px",this.canvas.style.height=this.viewRect.h+"px")}function a(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);var e=this.getViewRectCenterPixels(),i=this.ctx,s=this.zoom,n=this.viewRect,r=this.scale;this.loadedTiles.forEach(function(t){if(t.zoom==s){var o=n.w/2*r+(t.px-e[0]),h=n.h/2*r+(t.py-e[1]);i.drawImage(t.img,Math.round(o),Math.round(h))}}),this.eventEmitter.emit(t.ON_TEXTURE_UPDATE,this.ctx)}function l(t,e,i){var s=this.mercator.latLngToPixels(-t,e,i),n=this.viewRect.w*this.scale,r=this.viewRect.h*this.scale,o=this.mercator.pixelsToLatLng(s[0]-n/2*this.scale,s[1]-r/2*this.scale,i),h=this.mercator.pixelsToLatLng(s[0]+n/2*this.scale,s[1]+r/2*this.scale,i);return[-o[0],o[1],-h[0],h[1]]}function u(){var t=this.viewRectToLatLng(this.latitude,this.longitude,this.zoom);return{left:t[1],top:t[0],right:t[3],bottom:t[2]}}function c(t,e,i){var s=this.mercator.latLngToPixels(-this.latitude,this.longitude,i||this.zoom),n=map.mercator.pixelsToLatLng(s[0]-this.width/2+t,s[1]-this.height/2+e,i||this.zoom);return n[0]*=-1,n}function d(t,e,i){var s=this.mercator.latLngToPixels(-this.latitude,this.longitude,i||this.zoom),n=this.mercator.latLngToPixels(-t,e,i||this.zoom);return[n[0]-s[0]+this.width/2,n[1]-s[1]+this.height/2]}function m(){var t=this.viewRectToLatLng(this.latitude,this.longitude,this.zoom,this.viewRect);return[s.map(.5,0,1,-t[0],-t[2]),s.map(.5,0,1,t[1],t[3])]}function f(){return this.mercator.latLngToPixels(-this.latitude,this.longitude,this.zoom)}function v(t){t=t||this.zoom;for(var e=l(this.latitude,this.longitude,t,this.viewRect),i=this.mercator.latLonToTile(-e[0],e[1],t),s=this.mercator.latLonToTile(-e[2],e[3],t),n=0,r=0,o=[],h=i[0];h<=s[0];h++){r=0;for(var a=i[1];a<=s[1];a++){for(var u=this.mercator.tileXYToQuadKey(h,a,t),c=!1,d=0;di;)i<<=1;return i}function s(t,e,i,s){var n=EARTH_RADIUS,r=t*h,o=i*h,a=(i-t)*h,l=(s-e)*h,u=Math.sin(a/2)*Math.sin(a/2)+Math.cos(r)*Math.cos(o)*Math.sin(l/2)*Math.sin(l/2),c=2*Math.atan2(Math.sqrt(u),Math.sqrt(1-u));return n*c}function n(t,e,i){return e+t*(i-e)}function r(t,e,i){return(t-e)/(i-e)}function o(t,e,i,s,o){return n(r(t,e,i),s,o)}var h=Math.PI/180;return t.lerp=n,t.norm=r,t.map=o,t.isPowerOfTwo=e,t.powerTwo=i,t.latLngDistance=s,t}(e.exports)},{}],4:[function(t,e){e.exports=function(){function t(t,e){this.init(t,e)}function e(t,e){this.tileSize=t||256,this.earthRadius=e||6378137,this.initialResolution=2*Math.PI*this.earthRadius/this.tileSize,this.originShift=2*Math.PI*this.earthRadius/2}function i(t,e){var i=e*this.originShift/180,s=Math.log(Math.tan((90+t)*Math.PI/360))/(Math.PI/180);return s=s*this.originShift/180,[i,s]}function s(t,e){var i=t/this.originShift*180,s=e/this.originShift*180;return s=180/Math.PI*(2*Math.atan(Math.exp(s*Math.PI/180))-Math.PI/2),[s,i]}function n(t,e,i){var s=this.resolution(i),n=t*s-this.originShift,r=e*s-this.originShift;return[n,r]}function r(t,e,i){var s=this.resolution(i),n=(t+this.originShift)/s,r=(e+this.originShift)/s;return[n,r]}function o(t,e,i){var s=this.metersToPixels(t,e,i);return this.pixelsToTile(s[0],s[1])}function h(t,e){var i=parseInt(Math.ceil(t/parseFloat(this.tileSize))-1),s=parseInt(Math.ceil(e/parseFloat(this.tileSize))-1);return[i,s]}function a(t,e,i){var s=this.latLngToPixels(t,e,i);return this.pixelsToTile(s[0],s[1])}function l(t,e,i){var s=this.tileSize<this.resolution(e);)if(e--,0>=e)return 0;return e}function v(t,e,i){var s=this.pixelsToMeters(t,e,i);return this.metersToLatLon(s[0],s[1])}function p(t,e,i){var s=this.latLonToMeters(t,e,i);return this.metersToPixels(s[0],s[1],i)}function g(t,e,i){var s=this.latLonToMeters(t,e);return this.metersToTile(s[0],s[1],i)}function L(t,e,i){for(var s="",n=i;n>0;n--){var r=0,o=1<0;r--){var o=1<this.x+this.w?!1:e<=this.y+this.h}function i(t,e,i,s){return this.x>=t&&this.y>=e&&this.x+this.w<=t+i&&this.y+this.h<=e+s}function s(t,e,i,s){return!(t>this.x+this.w||t+ithis.y+this.h||e+s ",e.key,e.zoom):(e.map.mercator.tileSize!=i.target.width&&e.rescaleImage(i.target,e.map.mercator.tileSize,window.devicePixelRatio),e.loaded=!0,void e.eventEmitter.emit(t.ON_TILE_LOADED,e))},this.img.setAttribute("key",this.key),this.img.src=this.url}function r(t,e,i){var s=document.createElement("canvas"),n=s.width=t.width,r=s.height=t.height,o=s.getContext("2d");o.drawImage(t,0,0,n,r);for(var h=o.getImageData(0,0,n,r).data,a=o.createImageData(e,e),l=a.data,u=0;u=m;m++)if(!(c>=e))for(var f=d;d+i>=f;f++){var v=4*(~~m+~~f*e);l[v]=h[u],l[v+1]=h[u+1],l[v+2]=h[u+2],l[v+3]=h[u+3]}s.width=s.height=e,a.data=l,o.putImageData(a,0,0),delete this.img,this.img=s}function o(t,e,i){var s=this.map.provider;if(s=s.replace(/\{x\}/,t),s=s.replace(/\{y\}/,e),s=s.replace(/\{z\}/,i),-1!=s.lastIndexOf("{s}")){var n=this.map.domains||[""];s=s.replace(/\{s\}/,n[parseInt(Math.random()*n.length)])}return s}function h(t,e){return t>this.latLngBounds[0]?!1:ethis.latLngBounds[3]?!1:!0}function a(t){return this.latLngBounds[0]>=t[0]&&this.latLngBounds[1]>=t[1]&&this.latLngBounds[2]<=t[2]&&this.latLngBounds[3]<=t[3]}function l(t){return!(t[0]this.latLngBounds[3]||t[2]>this.latLngBounds[0]||t[3] -------------------------------------------------------------------------------- /example/retina.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | light map utils 6 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'light-map'; 3 | src:url('fonts/light-map.eot?-ezg7ro'); 4 | src:url('fonts/light-map.eot?#iefix-ezg7ro') format('embedded-opentype'), 5 | url('fonts/light-map.ttf?-ezg7ro') format('truetype'), 6 | url('fonts/light-map.woff?-ezg7ro') format('woff'), 7 | url('fonts/light-map.svg?-ezg7ro#light-map') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="light-map-"], [class*=" light-map-"] { 13 | font-family: 'light-map'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .light-map-location:before { 27 | content: "\e600"; 28 | } 29 | .light-map-plus:before { 30 | content: "\e601"; 31 | } 32 | .light-map-minus:before { 33 | content: "\e602"; 34 | } 35 | .light-map-up:before { 36 | content: "\e603"; 37 | } 38 | .light-map-right:before { 39 | content: "\e604"; 40 | } 41 | .light-map-down:before { 42 | content: "\e605"; 43 | } 44 | .light-map-left:before { 45 | content: "\e606"; 46 | } 47 | .light-map-circle-up:before { 48 | content: "\e607"; 49 | } 50 | .light-map-circle-right:before { 51 | content: "\e608"; 52 | } 53 | .light-map-circle-down:before { 54 | content: "\e609"; 55 | } 56 | .light-map-circle-left:before { 57 | content: "\e60a"; 58 | } 59 | -------------------------------------------------------------------------------- /img/light-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoptere/light-map/7e34aaff9836b282ad1f3ff8acc9da76124e5773/img/light-map.jpg -------------------------------------------------------------------------------- /img/retina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoptere/light-map/7e34aaff9836b282ad1f3ff8acc9da76124e5773/img/retina.jpg -------------------------------------------------------------------------------- /map/Controls.js: -------------------------------------------------------------------------------- 1 | module.exports = function() 2 | { 3 | function Controls( map ){ 4 | this.map = map; 5 | this.mercator = map.mercator; 6 | } 7 | 8 | 9 | var _p = Controls.prototype; 10 | 11 | 12 | return Controls; 13 | 14 | }(); 15 | 16 | -------------------------------------------------------------------------------- /map/Map.js: -------------------------------------------------------------------------------- 1 | var Mercator = require( './Mercator' ); 2 | var utils = require( './MapUtils' ); 3 | var Rect = require( './Rect' ); 4 | var Tile = require( './Tile' ); 5 | var events = require('events'); 6 | 7 | module.exports = function(){ 8 | 9 | function Map( provider, domains, width, height, minZoom, maxZoom ){ 10 | 11 | 12 | //sets the scale of the map, handles retina display 13 | this.mercator = new Mercator( 256 ); 14 | 15 | //events 16 | this.eventEmitter = new events.EventEmitter(); 17 | 18 | //zoom bounds 19 | this._minZoom = Math.max( 0, minZoom ); 20 | this._maxZoom = Math.max( 1, maxZoom ); 21 | 22 | //map center 23 | this.latitude = 0; 24 | this.longitude = 0; 25 | 26 | //map zoom level 27 | this.zoom = 0; 28 | 29 | //loading 30 | this.tiles = []; 31 | this.keys = []; 32 | this.loadedTiles = []; 33 | 34 | //create domElement 35 | this.canvas = document.createElement("canvas"); 36 | this.ctx = this.canvas.getContext("2d"); 37 | 38 | //viewRect 39 | this.setSize( width, height ); 40 | 41 | //providers 42 | this.setProvider( provider, domains, 256 * window.devicePixelRatio ); 43 | 44 | //makes the math utils available 45 | this.utils = utils; 46 | } 47 | 48 | //getters / setters 49 | Map.prototype = { 50 | 51 | get width(){return this._width; }, set width( value ){this._width = value; this.setSize(this.width, this.height ); }, 52 | get height(){return this._height; }, set height( value ){this._height = value; this.setSize(this.width, this.height ); }, 53 | get minZoom(){return this._minZoom; }, set minZoom( value ){this._minZoom = value; }, 54 | get maxZoom(){return this._maxZoom; }, set maxZoom( value ){this._maxZoom = value; } 55 | 56 | }; 57 | /** 58 | * changes the Tile provider 59 | * @param provider url 60 | * @param domains sub domains 61 | * @param tileSize 62 | */ 63 | function setProvider( provider, domains, tileSize ) 64 | { 65 | if( this.provider == provider )return; 66 | 67 | this.dispose(); 68 | 69 | this.provider = provider || "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 70 | this.domains = domains || ["a","b","c"]; 71 | this.scale = tileSize / 256; 72 | 73 | this.mercator = new Mercator( 256 * this.scale ); 74 | 75 | this.setSize( this._width, this._height ); 76 | 77 | //this.setView(); 78 | 79 | } 80 | /** 81 | * sets the view rect size 82 | * @param w 83 | * @param h 84 | * @param apply boolean to apply the transform only 85 | */ 86 | function setSize( w,h ) 87 | { 88 | this._width = w || 256; 89 | this._height = h || 256; 90 | 91 | this.viewRect = new Rect( 0,0,this._width,this._height ); 92 | 93 | if( this.canvas != null ){ 94 | 95 | // set the scaled resolution 96 | this.canvas.width = this.viewRect.w * this.scale; 97 | this.canvas.height = this.viewRect.h * this.scale; 98 | 99 | // and the actual size 100 | this.canvas.style.width = ( this.viewRect.w ) + 'px'; 101 | this.canvas.style.height = ( this.viewRect.h ) + 'px'; 102 | } 103 | 104 | } 105 | 106 | function renderTiles() { 107 | 108 | this.ctx.clearRect( 0, 0, this.canvas.width, this.canvas.height ); 109 | 110 | var c = this.getViewRectCenterPixels(); 111 | var ctx = this.ctx; 112 | var zoom = this.zoom; 113 | var viewRect = this.viewRect; 114 | var scale = this.scale; 115 | this.loadedTiles.forEach(function(tile) 116 | { 117 | if( tile.zoom == zoom ) 118 | { 119 | var px = viewRect.w / 2 * scale + ( tile.px - c[ 0 ] ); 120 | var py = viewRect.h / 2 * scale + ( tile.py - c[ 1 ] ); 121 | 122 | ctx.drawImage( tile.img, Math.round( px ), Math.round( py ) ); 123 | 124 | } 125 | }); 126 | 127 | this.eventEmitter.emit( Map.ON_TEXTURE_UPDATE, this.ctx ); 128 | 129 | } 130 | 131 | /** 132 | * returns an array of the latitude/longitude in degrees 133 | * @returns {*[]} 134 | */ 135 | function getLatLng(){ 136 | 137 | return [ this.latitude, this.longitude ]; 138 | } 139 | 140 | /** 141 | * returns the bounds of the view rect as an array of the latitude/longitude in degrees 142 | * @returns [ top left lat, top left lon, bottom right lat, bottom right lon] 143 | */ 144 | function viewRectToLatLng( lat, lng, zoom ){ 145 | 146 | var c = this.mercator.latLngToPixels( -lat, lng, zoom ); 147 | var w = this.viewRect.w * this.scale; 148 | var h = this.viewRect.h * this.scale; 149 | var tl = this.mercator.pixelsToLatLng( c[ 0 ] - w / 2 * this.scale, c[ 1 ] - h / 2 * this.scale, zoom ); 150 | var br = this.mercator.pixelsToLatLng( c[ 0 ] + w / 2 * this.scale, c[ 1 ] + h / 2 * this.scale, zoom ); 151 | return [ -tl[ 0 ], tl[ 1 ], -br[ 0 ], br[ 1 ] ]; 152 | 153 | } 154 | 155 | /** 156 | * returns the bounds of the view rect as an object of the latitude/longitude in degrees 157 | * @returns {left lon, top lat, right lon, bottom lat} 158 | */ 159 | function getViewPortBounds() 160 | { 161 | var bounds = this.viewRectToLatLng( this.latitude, this.longitude, this.zoom ); 162 | return{ 163 | left: bounds[1], 164 | top: bounds[0], 165 | right: bounds[3], 166 | bottom: bounds[2] 167 | }; 168 | 169 | } 170 | 171 | /** 172 | * converts local X & Y coordinates and zoom level to latitude and longitude 173 | * @param x X position on the canvas 174 | * @param y Y position on the canvas 175 | * @param zoom zoom level (optional, falls back to the map's current zoom level) 176 | * @returns {*} array[ lat, lon ] in degrees 177 | */ 178 | function pixelsToLatLon( x,y, zoom ) 179 | { 180 | 181 | var c = this.mercator.latLngToPixels( -this.latitude, this.longitude, zoom || this.zoom ); 182 | var pos = map.mercator.pixelsToLatLng( c[ 0 ] - this.width / 2 + x, c[ 1 ] - this.height / 2 + y, zoom || this.zoom ); 183 | pos[0] *= -1; 184 | return pos; 185 | 186 | } 187 | 188 | 189 | /** 190 | * converts latitude/longitude at a given zoom level to a local XY position 191 | * @param lat latitude in degrees 192 | * @param lon longitude in degress 193 | * @param zoom zoom level (optional, falls back to the map's current zoom level) 194 | * @returns {*} array[ X, Y ] in pixels 195 | */ 196 | function latLonToPixels( lat,lon, zoom ) 197 | { 198 | 199 | var c = this.mercator.latLngToPixels( -this.latitude, this.longitude, zoom || this.zoom ); 200 | var p = this.mercator.latLngToPixels( -lat, lon, zoom || this.zoom ); 201 | return [ (p[0]-c[0]) + this.width / 2, ( p[1] - c[1] ) + this.height / 2 ]; 202 | 203 | } 204 | 205 | /** 206 | * returns the bounds of the view rect as rectangle (Rect object) of pixels in absolute coordinates 207 | * @returns new Rect( absolute x, absolute y, width, height ) 208 | */ 209 | function canvasPixelToLatLng( px, py, zoom ){ 210 | 211 | var c = this.mercator.latLngToPixels( -ma, lng, zoom || map.zoom ); 212 | return new Rect( c[ 0 ]-this.viewRect.w/2, c[ 1 ]-this.viewRect.h/2, this.viewRect.w, this.viewRect.h ); 213 | } 214 | 215 | /** 216 | * returns an array of the latitude/longitude of the viewrect center in degrees 217 | * @returns {*[]} 218 | */ 219 | function getViewRectCenter() 220 | { 221 | var bounds = this.viewRectToLatLng( this.latitude, this.longitude, this.zoom, this.viewRect); 222 | return [ utils.map(.5,0,1, -bounds[0], -bounds[2] ), utils.map(.5,0,1, bounds[1], bounds[3] )]; 223 | } 224 | 225 | /** 226 | * returns an array of the absolute x/y coordinates of the viewrect center in pixels 227 | * @returns {*[]} 228 | */ 229 | function getViewRectCenterPixels() 230 | { 231 | return this.mercator.latLngToPixels( -this.latitude, this.longitude, this.zoom ); 232 | } 233 | 234 | /** 235 | * retrieves a list of tiles (loaded or not) that lie within the viewrect 236 | * @param zoom 237 | * @returns {Array} 238 | */ 239 | function viewRectTiles( zoom ) 240 | { 241 | zoom = zoom || this.zoom; 242 | var bounds = viewRectToLatLng( this.latitude, this.longitude, zoom, this.viewRect); 243 | var tl = this.mercator.latLonToTile(-bounds[0], bounds[1], zoom); 244 | var br = this.mercator.latLonToTile(-bounds[2], bounds[3], zoom); 245 | var u = 0; 246 | var v = 0; 247 | var tiles = []; 248 | for (var i = tl[0]; i <= br[0] ; i++) 249 | { 250 | v = 0; 251 | for (var j = tl[1]; j <= br[1]; j++) 252 | { 253 | var key = this.mercator.tileXYToQuadKey(i, j, zoom); 254 | var exists = false; 255 | for (var k = 0; k < this.loadedTiles.length; k++){ 256 | 257 | if (this.loadedTiles[k].key == key ){ 258 | 259 | this.loadedTiles[k].viewRectPosition[0] = u; 260 | this.loadedTiles[k].viewRectPosition[1] = v; 261 | tiles.push(this.loadedTiles[k]); 262 | exists = true; 263 | break; 264 | } 265 | } 266 | if( exists == false ) { 267 | 268 | for (k = 0; k < this.tiles.length; k++) { 269 | 270 | if (this.tiles[k].key == key) { 271 | 272 | this.tiles[k].viewRectPosition[0] = u; 273 | this.tiles[k].viewRectPosition[1] = v; 274 | tiles.push(this.tiles[k]); 275 | exists = true; 276 | break; 277 | } 278 | } 279 | } 280 | v++; 281 | } 282 | u++; 283 | } 284 | return tiles; 285 | } 286 | 287 | /** 288 | * gets a list of the tiles that are displayed in the viewrect at the given lat/lon/zoom 289 | * @param lat latitude in degrees 290 | * @param lng longitude in degrees 291 | * @param zoom zoom level 292 | * @param viewRect 293 | * @returns {Array} an array of Tile objects 294 | */ 295 | function getVisibleTiles( lat, lng, zoom, viewRect ) 296 | { 297 | 298 | var bounds = this.viewRectToLatLng( lat, lng, zoom, viewRect ); 299 | var tl = this.mercator.latLonToTile( -bounds[0], bounds[1], zoom ); 300 | var br = this.mercator.latLonToTile( -bounds[2], bounds[3], zoom ); 301 | 302 | var tiles = []; 303 | for( var i = tl[ 0 ]; i <= br[ 0 ]; i++ ) 304 | { 305 | for( var j = tl[ 1 ]; j <= br[ 1 ]; j++ ) 306 | { 307 | var key = this.mercator.tileXYToQuadKey( i, j, zoom ); 308 | 309 | //check if the tile was already loaded/being loaded 310 | if( this.keys.indexOf( key ) == -1 ) 311 | { 312 | var tile = new Tile( this, key ); 313 | tiles.push( tile ); 314 | this.keys.push( key ); 315 | } 316 | } 317 | } 318 | return tiles; 319 | 320 | } 321 | 322 | /** 323 | * sets the map view 324 | * @param lat 325 | * @param lng 326 | * @param zoom 327 | */ 328 | function setView( lat, lng, zoom ) 329 | { 330 | this.latitude = lat || this.latitude; 331 | this.longitude = lng || this.longitude; 332 | this.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, zoom || this.zoom ) ); 333 | 334 | this.load(); 335 | this.renderTiles(); 336 | } 337 | 338 | /** 339 | * loads the visibles tiles 340 | */ 341 | function load() 342 | { 343 | 344 | var tiles = this.getVisibleTiles( this.latitude, this.longitude, this.zoom, this.viewRect ); 345 | 346 | if( tiles.length == 0 && this.tiles.length == 0 ) 347 | { 348 | this.eventEmitter.emit( Map.ON_LOAD_COMPLETE, -1 ); 349 | return; 350 | } 351 | 352 | for ( var i = 0; i < tiles.length; i++ ) { 353 | tiles[ i ].eventEmitter.on( Tile.ON_TILE_LOADED, this.appendTile ); 354 | tiles[ i ].load(); 355 | } 356 | 357 | this.tiles = this.tiles.concat( tiles ); 358 | 359 | } 360 | 361 | /** 362 | * adds a loaded tile to the pool 363 | * @param tile 364 | */ 365 | function appendTile( tile ) 366 | { 367 | 368 | var scope = tile.map; 369 | scope.tiles.splice( scope.tiles.indexOf( tile ), 1 ); 370 | 371 | var img = tile.img; 372 | scope.loadedTiles.push( tile ); 373 | scope.renderTiles( true ); 374 | 375 | scope.eventEmitter.emit( Map.ON_TILE_LOADED, tile ); 376 | 377 | if( scope.tiles.length == 0 ){ 378 | 379 | scope.eventEmitter.emit( Map.ON_LOAD_COMPLETE, 0 ); 380 | } 381 | } 382 | 383 | /** 384 | * gets the map resolution in pixels at a given zoom level 385 | * @param zoom 386 | * @returns {*} 387 | */ 388 | function resolution( zoom ) 389 | { 390 | zoom = zoom || this.zoom; 391 | return this.mercator.resolution( zoom ); 392 | } 393 | 394 | /** 395 | * destroys all the tiles 396 | */ 397 | function dispose() 398 | { 399 | var scope = this; 400 | this.tiles.forEach( function(tile){ tile.eventEmitter.removeListener( Tile.ON_TILE_LOADED, scope.appendTile ); tile.valid = false; tile.dispose(); }); 401 | this.loadedTiles.forEach( function(tile){ tile.dispose(); }); 402 | 403 | this.tiles = []; 404 | this.loadedTiles = []; 405 | this.keys = []; 406 | 407 | } 408 | 409 | /** 410 | * returns the bounds of the view rect as rectangle (Rect object) of pixels in absolute coordinates 411 | * @returns {*[absolute x, absolute y, width, height ]} 412 | */ 413 | function viewRectToPixels( lat, lng, zoom ){ 414 | 415 | var c = this.mercator.latLngToPixels( -lat, lng, zoom ); 416 | return new Rect( c[ 0 ], c[ 1 ], this.viewRect.w, this.viewRect.h ); 417 | } 418 | 419 | //Map constants 420 | Map.ON_LOAD_COMPLETE = 0; 421 | Map.ON_TILE_LOADED = 1; 422 | Map.ON_TEXTURE_UPDATE = 2; 423 | 424 | 425 | var _p = Map.prototype; 426 | _p.constructor = Map; 427 | 428 | _p.setProvider = setProvider; 429 | _p.setSize = setSize; 430 | _p.renderTiles = renderTiles; 431 | _p.pixelsToLatLon = pixelsToLatLon; 432 | _p.latLonToPixels = latLonToPixels; 433 | _p.viewRectToLatLng = viewRectToLatLng; 434 | _p.getViewPortBounds = getViewPortBounds; 435 | _p.getViewRectCenter = getViewRectCenter; 436 | _p.getViewRectCenterPixels = getViewRectCenterPixels; 437 | _p.setView = setView; 438 | _p.viewRectTiles = viewRectTiles; 439 | _p.getVisibleTiles = getVisibleTiles; 440 | _p.load = load; 441 | _p.appendTile = appendTile; 442 | _p.resolution = resolution; 443 | _p.dispose = dispose; 444 | _p.viewRectToPixels = viewRectToPixels; 445 | 446 | return Map; 447 | 448 | }(); 449 | 450 | -------------------------------------------------------------------------------- /map/MapUtils.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports.MapUtils = function( exports ) 3 | { 4 | var RAD = Math.PI / 180; 5 | 6 | function isPowerOfTwo( value ){ 7 | 8 | return ( (value & -value) == value ); 9 | } 10 | 11 | function powerTwo(val){ 12 | 13 | if( isPowerOfTwo( val ) )return val 14 | var b = 1; 15 | while ( b < ~~( val ) )b = b << 1; 16 | return b; 17 | } 18 | 19 | //http://www.movable-type.co.uk/scripts/latlong.html 20 | function latLngDistance(lat1, lng1, lat2, lng2) 21 | { 22 | var R = EARTH_RADIUS; // km 23 | var p1 = lat1 * RAD; 24 | var p2 = lat2 * RAD; 25 | var tp = (lat2-lat1) * RAD; 26 | var td = (lng2-lng1) * RAD; 27 | 28 | var a = Math.sin(tp/2) * Math.sin(tp/2) + 29 | Math.cos(p1) * Math.cos(p2) * 30 | Math.sin(td/2) * Math.sin(td/2); 31 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 32 | return R * c; 33 | } 34 | function lerp ( t, a, b ){ return a + t * ( b - a ); } 35 | function norm( t, a, b ){return ( t - a ) / ( b - a );} 36 | function map( t, a0, b0, a1, b1 ){ return lerp( norm( t, a0, b0 ), a1, b1 );} 37 | 38 | // methods 39 | 40 | exports.lerp = lerp; 41 | exports.norm = norm; 42 | exports.map = map; 43 | 44 | exports.isPowerOfTwo = isPowerOfTwo; 45 | exports.powerTwo = powerTwo; 46 | exports.latLngDistance = latLngDistance; 47 | 48 | return exports; 49 | 50 | }( module.exports ); 51 | -------------------------------------------------------------------------------- /map/Rect.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() 3 | { 4 | /** 5 | * the physical representation of the map box 6 | * @param x 7 | * @param y 8 | * @param _w 9 | * @param _h 10 | * @constructor 11 | */ 12 | 13 | function Rect( x,y,w,h ) { 14 | 15 | this.x = x; 16 | this.y = y; 17 | this.w = w; 18 | this.h = h; 19 | 20 | } 21 | Rect.prototype = 22 | { 23 | get x(){ return this._x;}, set x( value ){ this._x = value; return this._x; }, 24 | get y(){ return this._y;}, set y( value ){ this._y = value; return this._y; }, 25 | get w(){ return this._w;}, set w( value ){ this._w = value; return this._w; }, 26 | get h(){ return this._h;}, set h( value ){ this._h = value; return this._h; } 27 | }; 28 | 29 | function containsPoint( _x, _y ) 30 | { 31 | if( _x < this.x ) return false; 32 | if( _y < this.y ) return false; 33 | if( _x > this.x + this.w ) return false; 34 | return _y <= this.y + this.h; 35 | } 36 | 37 | function isContained( _x, _y, _w, _h ) 38 | { 39 | return ( this.x >= _x 40 | && this.y >= _y 41 | && this.x + this.w <= _x + _w 42 | && this.y + this.h <= _y + _h ); 43 | } 44 | 45 | function intersect( _x, _y, _w, _h ) 46 | { 47 | return !( _x > this.x + this.w || _x+_w < this.x || _y > this.y + this.h || _y+_h< this.y ); 48 | } 49 | 50 | function intersection( other ) 51 | { 52 | if( this.intersect( other.x, other.y, other.x + other.w, other.y + other.h ) ) 53 | { 54 | var x = Math.max( this.x, other.x ); 55 | var y = Math.max( this.y, other.y ); 56 | var w = Math.min( this.x + this.w, other.x + other.w ) - x; 57 | var h = Math.min( this.y + this.h, other.y + other.h ) - y; 58 | return new Rect( x,y,w,h ); 59 | } 60 | return null; 61 | } 62 | 63 | var _p = Rect.prototype; 64 | 65 | _p.constructor = Rect; 66 | _p.containsPoint = containsPoint; 67 | _p.isContained = isContained; 68 | _p.intersect = intersect; 69 | _p.intersection = intersection; 70 | 71 | return Rect; 72 | 73 | }(); -------------------------------------------------------------------------------- /map/Tile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tile object, holds reference to: 3 | * 4 | * tile top left lat/lon 5 | * tile id 6 | * tile X/Y 7 | * tile pixel X/Y 8 | * the tile's DOM element 9 | * 10 | * + some helper methods ( contains, isContained, intersect ) 11 | * 12 | * @param map the map this tile is bound to 13 | * @param quadKey optional, can be set with initFromQuadKey() 14 | * @constructor 15 | */ 16 | 17 | var events = require( 'events' ); 18 | module.exports = function() 19 | { 20 | 21 | var undef; 22 | 23 | /** 24 | * @param map Map instance this tile is associated with 25 | * @param map the map this tile is bound to 26 | * @param quadKey the QuadKey of this Tile 27 | * @constructor 28 | */ 29 | function Tile( map, quadKey ) 30 | { 31 | 32 | if( map == null )throw new Error( "Tile: no map associated to Tile." ); 33 | this.map = map; 34 | 35 | this.valid = true; 36 | this.loaded = false; 37 | 38 | this.key = ""; 39 | this.id = -1; 40 | 41 | this.tx = -1; 42 | this.ty = -1; 43 | this.zoom = -1; 44 | 45 | this.lat = -1; 46 | this.lng = -1; 47 | 48 | this.meterBounds = undef; 49 | this.mx = -1; 50 | this.my = -1; 51 | 52 | this.pixelBounds = undef; 53 | this.px = -1; 54 | this.py = -1; 55 | 56 | //raster position (tile position relative to the canvas) 57 | this.rx = 0; 58 | this.ry = 0; 59 | 60 | this.viewRectPosition = [0,0]; 61 | this.latLngBounds = undef; 62 | 63 | this.img = new Image(); 64 | this.url = ""; 65 | 66 | this.eventEmitter = new events.EventEmitter(); 67 | 68 | this.quadKey = this.key = quadKey; 69 | if( this.quadKey != undef ) 70 | { 71 | this.initFromQuadKey( this.quadKey ); 72 | } 73 | 74 | } 75 | 76 | 77 | 78 | function initFromTileXY(x, y, zoom) 79 | { 80 | var quadKey = this.map.mercator.tileXYToQuadKey(x, y, zoom); 81 | initFromQuadKey(quadKey); 82 | } 83 | 84 | function initFromQuadKey(quadKey) 85 | { 86 | 87 | var tile = this.map.mercator.quadKeyToTileXY(quadKey); 88 | if ( tile == undef) { 89 | this.valid = false; 90 | return; 91 | } 92 | 93 | var center = this.map.mercator.tileLatLngBounds(this.tx + .5, this.ty + .5, this.zoom); 94 | this.lat = -center[0]; 95 | this.lng = center[1]; 96 | 97 | this.key = quadKey; 98 | this.id = parseInt(quadKey); 99 | 100 | this.tx = tile.tx; 101 | this.ty = tile.ty; 102 | this.zoom = tile.zoom; 103 | 104 | this.meterBounds = this.map.mercator.tileMetersBounds( this.tx, this.ty, this.zoom); 105 | this.mx = this.meterBounds[0]; 106 | this.my = this.meterBounds[1]; 107 | 108 | this.pixelBounds = this.map.mercator.tilePixelsBounds(this.tx, this.ty, this.zoom); 109 | this.px = this.pixelBounds[0]; 110 | this.py = this.pixelBounds[1]; 111 | 112 | this.latLngBounds = this.map.mercator.tileLatLngBounds(this.tx, this.ty, this.zoom); 113 | this.latLngBounds[0] *= -1; 114 | this.latLngBounds[2] *= -1; 115 | 116 | this.url = this.getMapUrl(this.tx, this.ty, this.zoom); 117 | 118 | } 119 | 120 | function load( callback ) 121 | { 122 | if( !this.valid ) 123 | { 124 | console.log( "invalid tile, not loading"); 125 | return; 126 | } 127 | 128 | var scope = this; 129 | 130 | this.img.tile = this; 131 | this.img.crossOrigin = 'anonymous'; 132 | this.img.onload = function (e) { 133 | 134 | if( scope.map == undef ) 135 | { 136 | console.warn( 'loaded Tile has no associated map > ', scope.key, scope.zoom ); 137 | return; 138 | } 139 | 140 | if( scope.map.mercator.tileSize != e.target.width ){ 141 | scope.rescaleImage( e.target, scope.map.mercator.tileSize, window.devicePixelRatio ); 142 | } 143 | 144 | scope.loaded = true; 145 | scope.eventEmitter.emit( Tile.ON_TILE_LOADED, scope ); 146 | 147 | }; 148 | this.img.setAttribute("key", this.key); 149 | this.img.src = this.url; 150 | } 151 | 152 | /** 153 | * rescales the image so that it fits the map's scale 154 | * @param img input image 155 | * @param tileSize destination tileSize 156 | * @param scale scale factor 157 | */ 158 | function rescaleImage( img, tileSize, scale ) 159 | { 160 | 161 | var canvas = document.createElement( 'canvas' ); 162 | var w = canvas.width = img.width ; 163 | var h = canvas.height = img.height; 164 | 165 | //collect image data 166 | var ctx = canvas.getContext("2d"); 167 | ctx.drawImage( img, 0,0,w,h ); 168 | var srcData = ctx.getImageData( 0,0,w,h).data; 169 | 170 | //result 171 | var imgOut = ctx.createImageData(tileSize,tileSize); 172 | var out = imgOut.data; 173 | 174 | //nearest neighbours upscale 175 | for( var i = 0; i < srcData.length; i+=4 ) 176 | { 177 | var x = ( i/4 % w ) * scale; 178 | var y = ~~( i/4 / w ) * scale; 179 | for( var j = x; j <= x + scale; j++ ) 180 | { 181 | if( x >= tileSize )continue; 182 | for( var k = y; k <= y + scale; k++ ) { 183 | 184 | var id = ( ~~( j ) + ~~( k ) * tileSize ) * 4; 185 | out[id] = srcData[i ]; 186 | out[id + 1] = srcData[i+1]; 187 | out[id + 2] = srcData[i+2]; 188 | out[id + 3] = srcData[i+3]; 189 | } 190 | } 191 | } 192 | canvas.width = canvas.height = tileSize; 193 | imgOut.data = out; 194 | ctx.putImageData(imgOut,0,0); 195 | 196 | //replace img with canvas 197 | delete this.img; 198 | this.img = canvas; 199 | 200 | } 201 | 202 | function getMapUrl(x, y, zl) 203 | { 204 | var url = this.map.provider; 205 | url = url.replace(/\{x\}/, x); 206 | url = url.replace(/\{y\}/, y); 207 | url = url.replace(/\{z\}/, zl); 208 | 209 | if( url.lastIndexOf("{s}") != -1 ){ 210 | var domains = this.map.domains || [""]; 211 | url = url.replace(/\{s\}/, domains[ parseInt( Math.random() * domains.length ) ] ); 212 | } 213 | return url; 214 | } 215 | 216 | function containsLatLng(lat, lng){ 217 | 218 | if (lat > this.latLngBounds[0]) return false; 219 | if (lng < this.latLngBounds[1]) return false; 220 | if (lat < this.latLngBounds[2]) return false; 221 | if (lat > this.latLngBounds[3]) return false; 222 | return true; 223 | } 224 | 225 | function isContained(latLngBound){ 226 | 227 | return this.latLngBounds[0] >= latLngBound[0] 228 | && this.latLngBounds[1] >= latLngBound[1] 229 | && this.latLngBounds[2] <= latLngBound[2] 230 | && this.latLngBounds[3] <= latLngBound[3]; 231 | } 232 | 233 | function intersect(latLngBound){ 234 | 235 | return !( latLngBound[0] < this.latLngBounds[2] || 236 | latLngBound[1] > this.latLngBounds[3] || 237 | latLngBound[2] > this.latLngBounds[0] || 238 | latLngBound[3] < this.latLngBounds[1] ); 239 | } 240 | 241 | function dispose() { 242 | 243 | this.map = undef; 244 | delete this.map; 245 | this.meterBounds = undef; 246 | delete this.meterBounds; 247 | this.pixelBounds = undef; 248 | delete this.pixelBounds; 249 | 250 | delete this.viewRectPosition; 251 | delete this.latLngBounds; 252 | delete this.img; 253 | 254 | this.eventEmitter.removeAllListeners(); 255 | delete this.eventEmitter; 256 | 257 | } 258 | 259 | var _p = Tile.prototype; 260 | _p.constructor = Tile; 261 | 262 | _p.initFromTileXY = initFromTileXY; 263 | _p.initFromQuadKey = initFromQuadKey; 264 | _p.load = load; 265 | _p.rescaleImage = rescaleImage; 266 | _p.getMapUrl = getMapUrl; 267 | _p.containsLatLng = containsLatLng; 268 | _p.isContained = isContained; 269 | _p.intersect = intersect; 270 | _p.dispose = dispose; 271 | 272 | Tile.ON_TILE_LOADED = 0; 273 | 274 | return Tile; 275 | 276 | }(); 277 | -------------------------------------------------------------------------------- /map/mercator.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | //from http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ 4 | 5 | #!/usr/bin/env python 6 | ############################################################################### 7 | # $Id$ 8 | # 9 | # Project: GDAL2Tiles, Google Summer of Code 2007 & 2008 10 | # Global Map Tiles Classes 11 | # Purpose: Convert a raster into TMS tiles, create KML SuperOverlay EPSG:4326, 12 | # generate a simple HTML viewers based on Google Maps and OpenLayers 13 | # Author: Klokan Petr Pridal, klokan at klokan dot cz 14 | # Web: http://www.klokan.cz/projects/gdal2tiles/ 15 | # 16 | ############################################################################### 17 | # Copyright (c) 2008 Klokan Petr Pridal. All rights reserved. 18 | # 19 | # Permission is hereby granted, free of charge, to any person obtaining a 20 | # copy of this software and associated documentation files (the "Software"), 21 | # to deal in the Software without restriction, including without limitation 22 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 23 | # and/or sell copies of the Software, and to permit persons to whom the 24 | # Software is furnished to do so, subject to the following conditions: 25 | # 26 | # The above copyright notice and this permission notice shall be included 27 | # in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 30 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 32 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 34 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 35 | # DEALINGS IN THE SOFTWARE. 36 | ############################################################################### 37 | 38 | globalmaptiles.py 39 | 40 | Global Map Tiles as defined in Tile Map Service (TMS) Profiles 41 | ============================================================== 42 | 43 | Functions necessary for generation of global tiles used on the web. 44 | It contains classes implementing coordinate conversions for: 45 | 46 | - GlobalMercator (based on EPSG:900913 = EPSG:3785) 47 | for Google Maps, Yahoo Maps, Microsoft Maps compatible tiles 48 | - GlobalGeodetic (based on EPSG:4326) 49 | for OpenLayers Base Map and Google Earth compatible tiles 50 | 51 | More info at: 52 | 53 | http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification 54 | http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation 55 | http://msdn.microsoft.com/en-us/library/bb259689.aspx 56 | http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates 57 | 58 | Created by Klokan Petr Pridal on 2008-07-03. 59 | Google Summer of Code 2008, project GDAL2Tiles for OSGEO. 60 | 61 | In case you use this class in your product, translate it to another language 62 | or find it usefull for your project please let me know. 63 | My email: klokan at klokan dot cz. 64 | I would like to know where it was used. 65 | 66 | Class is available under the open-source GDAL license (www.gdal.org). 67 | 68 | */ 69 | 70 | module.exports = function() { 71 | 72 | function Mercator( tile_size, earth_radius ) 73 | { 74 | this.init(tile_size, earth_radius); 75 | } 76 | 77 | function init(tile_size, earth_radius) 78 | { 79 | //Initialize the TMS Global Mercator pyramid 80 | this.tileSize = tile_size || 256; 81 | 82 | this.earthRadius = earth_radius || 6378137; 83 | 84 | // 156543.03392804062 for tileSize 256 pixels 85 | this.initialResolution = 2 * Math.PI * this.earthRadius / this.tileSize; 86 | 87 | // 20037508.342789244 88 | this.originShift = 2 * Math.PI * this.earthRadius / 2.0; 89 | 90 | } 91 | 92 | 93 | //converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913 94 | function latLonToMeters( lat, lon ) 95 | { 96 | var mx = lon * this.originShift / 180.0; 97 | var my = Math.log( Math.tan((90 + lat ) * Math.PI / 360.0)) / (Math.PI / 180.0); 98 | my = my * this.originShift / 180.0; 99 | return [mx, my]; 100 | } 101 | 102 | //converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum 103 | function metersToLatLon( mx, my ) 104 | { 105 | var lon = (mx / this.originShift) * 180.0; 106 | var lat = (my / this.originShift) * 180.0; 107 | lat = 180 / Math.PI * ( 2 * Math.atan( Math.exp(lat * Math.PI / 180.0 ) ) - Math.PI / 2.0); 108 | return [lat, lon]; 109 | } 110 | 111 | //converts pixel coordinates in given zoom level of pyramid to EPSG:900913 112 | function pixelsToMeters( px, py, zoom ) 113 | { 114 | var res = this.resolution(zoom); 115 | var mx = px * res - this.originShift; 116 | var my = py * res - this.originShift; 117 | return [mx, my]; 118 | } 119 | 120 | //converts EPSG:900913 to pyramid pixel coordinates in given zoom level 121 | function metersToPixels( mx, my, zoom ) 122 | { 123 | var res = this.resolution( zoom ); 124 | var px = (mx + this.originShift) / res; 125 | var py = (my + this.originShift) / res; 126 | return [px, py]; 127 | } 128 | 129 | //returns tile for given mercator coordinates 130 | function metersToTile( mx, my, zoom ) 131 | { 132 | var pxy = this.metersToPixels(mx, my, zoom); 133 | return this.pixelsToTile(pxy[0], pxy[1]); 134 | } 135 | 136 | //returns a tile covering region in given pixel coordinates 137 | function pixelsToTile( px, py) 138 | { 139 | var tx = parseInt( Math.ceil( px / parseFloat( this.tileSize ) ) - 1); 140 | var ty = parseInt( Math.ceil( py / parseFloat( this.tileSize ) ) - 1); 141 | return [tx, ty]; 142 | } 143 | 144 | //returns a tile covering region the given lat lng coordinates 145 | function latLonToTile( lat, lng, zoom ) 146 | { 147 | var px = this.latLngToPixels( lat, lng, zoom ); 148 | return this.pixelsToTile( px[ 0 ], px[ 1 ] ); 149 | } 150 | 151 | //Move the origin of pixel coordinates to top-left corner 152 | function pixelsToRaster( px, py, zoom ) 153 | { 154 | var mapSize = this.tileSize << zoom; 155 | return [px, mapSize - py]; 156 | } 157 | 158 | //returns bounds of the given tile in EPSG:900913 coordinates 159 | function tileMetersBounds( tx, ty, zoom ) 160 | { 161 | var min = this.pixelsToMeters(tx * this.tileSize, ty * this.tileSize, zoom); 162 | var max = this.pixelsToMeters((tx + 1) * this.tileSize, (ty + 1) * this.tileSize, zoom); 163 | return [ min[0], min[1], max[0], max[1] ]; 164 | } 165 | 166 | //returns bounds of the given tile in pixels 167 | function tilePixelsBounds( tx, ty, zoom ) 168 | { 169 | var bounds = this.tileMetersBounds( tx, ty, zoom ); 170 | var min = this.metersToPixels(bounds[0], bounds[1], zoom ); 171 | var max = this.metersToPixels(bounds[2], bounds[3], zoom ); 172 | return [ min[0], min[1], max[0], max[1] ]; 173 | } 174 | 175 | //returns bounds of the given tile in latutude/longitude using WGS84 datum 176 | function tileLatLngBounds( tx, ty, zoom ) 177 | { 178 | var bounds = this.tileMetersBounds( tx, ty, zoom ); 179 | var min = this.metersToLatLon(bounds[0], bounds[1]); 180 | var max = this.metersToLatLon(bounds[2], bounds[3]); 181 | return [ min[0], min[1], max[0], max[1] ]; 182 | } 183 | 184 | //resolution (meters/pixel) for given zoom level (measured at Equator) 185 | function resolution( zoom ) 186 | { 187 | return this.initialResolution / Math.pow( 2, zoom ); 188 | } 189 | 190 | /** 191 | * untested... 192 | * @param pixelSize 193 | * @returns {number} 194 | * @constructor 195 | */ 196 | function zoomForPixelSize( pixelSize ) 197 | { 198 | var i = 30; 199 | while( pixelSize > this.resolution(i) ) 200 | { 201 | i--; 202 | if( i <= 0 )return 0; 203 | } 204 | return i; 205 | } 206 | 207 | //returns the lat lng of a pixel X/Y coordinates at given zoom level 208 | function pixelsToLatLng( px, py, zoom ) 209 | { 210 | var meters = this.pixelsToMeters( px, py, zoom ); 211 | return this.metersToLatLon( meters[ 0 ], meters[ 1 ] ); 212 | } 213 | 214 | //returns the pixel X/Y coordinates at given zoom level from a given lat lng 215 | function latLngToPixels( lat, lng, zoom ) 216 | { 217 | var meters = this.latLonToMeters( lat, lng, zoom ); 218 | return this.metersToPixels( meters[ 0 ], meters[ 1 ], zoom ); 219 | } 220 | 221 | //retrieves a given tile from a given lat lng 222 | function latLngToTile( lat, lng, zoom ) 223 | { 224 | var meters = this.latLonToMeters( lat, lng ); 225 | return this.metersToTile( meters[ 0 ], meters[ 1 ], zoom ); 226 | } 227 | 228 | //encodes the tlie X / Y coordinates & zoom level into a quadkey 229 | function tileXYToQuadKey( tx,ty,zoom ) 230 | { 231 | var quadKey = ''; 232 | for ( var i = zoom; i > 0; i-- ) 233 | { 234 | var digit = 0; 235 | var mask = 1 << ( i - 1 ); 236 | if( ( tx & mask ) != 0 ) 237 | { 238 | digit++; 239 | } 240 | if( ( ty & mask ) != 0 ) 241 | { 242 | digit++; 243 | digit++; 244 | } 245 | quadKey += digit; 246 | } 247 | return quadKey; 248 | } 249 | 250 | //decodes the tlie X / Y coordinates & zoom level into a quadkey 251 | function quadKeyToTileXY( quadKeyString ) 252 | { 253 | var tileX = 0; 254 | var tileY = 0; 255 | var quadKey = quadKeyString.split( '' ); 256 | var levelOfDetail = quadKey.length; 257 | 258 | for( var i = levelOfDetail; i > 0; i--) 259 | { 260 | var mask = 1 << ( i - 1 ); 261 | switch( quadKey[ levelOfDetail - i ] ) 262 | { 263 | case '0': 264 | break; 265 | 266 | case '1': 267 | tileX |= mask; 268 | break; 269 | 270 | case '2': 271 | tileY |= mask; 272 | break; 273 | 274 | case '3': 275 | tileX |= mask; 276 | tileY |= mask; 277 | break; 278 | 279 | default: 280 | return null; 281 | } 282 | } 283 | return { tx:tileX, ty:tileY, zoom:levelOfDetail }; 284 | } 285 | 286 | 287 | // public methods 288 | 289 | var _p = Mercator.prototype; 290 | _p.constructor = Mercator; 291 | 292 | _p.init = init; 293 | _p.latLonToMeters = latLonToMeters; 294 | _p.metersToLatLon = metersToLatLon; 295 | _p.pixelsToMeters = pixelsToMeters; 296 | _p.metersToPixels = metersToPixels; 297 | _p.metersToTile = metersToTile; 298 | _p.pixelsToTile = pixelsToTile; 299 | _p.latLonToTile = latLonToTile; 300 | _p.pixelsToRaster = pixelsToRaster; 301 | _p.tileMetersBounds = tileMetersBounds; 302 | _p.tilePixelsBounds = tilePixelsBounds; 303 | _p.tileLatLngBounds = tileLatLngBounds; 304 | _p.resolution = resolution; 305 | _p.zoomForPixelSize = zoomForPixelSize; 306 | _p.pixelsToLatLng = pixelsToLatLng; 307 | _p.latLngToPixels = latLngToPixels; 308 | _p.latLngToTile = latLngToTile; 309 | _p.tileXYToQuadKey = tileXYToQuadKey; 310 | _p.quadKeyToTileXY = quadKeyToTileXY; 311 | 312 | return Mercator; 313 | 314 | 315 | }(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "light-map", 3 | "version": "0.0.6", 4 | "description": "a light-weight XYZ tile map viewer with a Canvas2D renderer", 5 | "main": "bin/light-map.js", 6 | "scripts": { 7 | "test": "new Map( provider, domains, 512,512, 0,10 )" 8 | }, 9 | "keywords": [ 10 | "map", 11 | "maps", 12 | "tms", 13 | "WGS84", 14 | "canvas", 15 | "self-contained", 16 | "mercator" 17 | ], 18 | "author": "Nicolas Barradeau", 19 | "maintainers": [ 20 | { 21 | "name": "Nicolas Barradeau", 22 | "email": "nicoptere@gmail.com", 23 | "web": "http://www.barradeau.com/blog" 24 | } 25 | ], 26 | 27 | "dependencies" :{ 28 | "events":"1.0.2" 29 | }, 30 | "devDependencies": { 31 | "browserify" : "^11.0.0", 32 | "uglifyjs": "^2.4.10" 33 | }, 34 | 35 | "scripts": { 36 | "debug": "browserify map/Map.js --standalone Map -o example/light-map.min.js", 37 | "bundle": "browserify map/Map.js --standalone Map -o bin/light-map.js", 38 | "uglify": "uglifyjs bin/light-map.js -m -c -screw-ie8 > example/light-map.min.js", 39 | "build": "npm run bundle && npm run uglify" 40 | }, 41 | 42 | "demos": [ 43 | "./example/index.html" 44 | ], 45 | 46 | "readme": "light map\n=============\n\nminimal, lightweight, self contained XYZ tile map viewer with a 2d canvas renderer.\n\n### live example ###\n- [an example with the most common methods](http://nicoptere.github.io/light-map/example/) let's you set the lat/lon/zoom, change the width/height of the canvas and monitor the load progress.\n- [a basic example of controls](http://nicoptere.github.io/light-map/example/controls.html) you should be able to drag the map around.\n- [an example of retina support](http://nicoptere.github.io/light-map/example/retina.html) first I thought it wasn't much but it seems to be a big deal. it's based on the devicePixelRatio, ideally you should use a @2x provider but I did an internal nearest neighbour resize.\n- [an example with a vignette](http://nicoptere.github.io/light-map/example/basic.html) as the map output is a Canvas2D, this shows how to post process the output.\n\n### more info ###\n[explanation and examples](http://nicoptere.github.io/light-map/)\n\n### basic example ###\n\n```js\n\n\n\n```\n\n### vignette example ###\n\n```js\n\n\n\n\n```\n\n### npm module installation ###\n```\nnpm install light-map --save\n```\n\n### related ###\n[Python library to perform Mercator conversions](http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/)\n\n[Quad keys explained](https://msdn.microsoft.com/en-us/library/bb259689.aspx)\n\nnpm [globalMercator](https://github.com/davvo/globalmercator/blob/master/globalmercator.js)\n\n### License ###\n\nThis content is released under the [MIT License](http://opensource.org/licenses/MIT).\n", 47 | 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/nicoptere/light-map.git" 51 | }, 52 | "license": "MIT", 53 | 54 | "bugs": { 55 | "url": "https://github.com/nicoptere/light-map/issues" 56 | }, 57 | 58 | "homepage": "https://github.com/nicoptere/light-map" 59 | } 60 | --------------------------------------------------------------------------------