turtlestitch/src/maps.js

366 wiersze
11 KiB
JavaScript

/*
maps.js
a slippy maps tile client for morphic.js and Snap!
written by Jens Mönig
jens@moenig.org
Copyright (C) 2021 by Jens Mönig
This file is part of Snap!.
Snap! is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
prerequisites:
--------------
needs morphic.js
credits:
--------
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
*/
/*global modules, Point, newCanvas, radians, StringMorph, normalizeCanvas*/
// Global stuff ////////////////////////////////////////////////////////
modules.maps = '2021-June-15';
// WorldMap /////////////////////////////////////////////////////////////
function WorldMap(host) {
this.tileServers = {
OpenStreetMap: {
url: 'tile.openstreetmap.org',
type: 'zxy',
subdomains: ['a', 'b', 'c'],
key: null,
min: 0,
max: 19,
credits: 'Map data \u00A9 OpenStreetMap contributors' +
'CC-BY-SA, Imagery \u00A9 Mapnik'
},
Wikimedia: {
url: 'maps.wikimedia.org/osm-intl',
type: 'zxy',
subdomains: null,
key: null,
min: 0,
max: 19,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Wikimedia'
},
Watercolor: {
url: 'stamen-tiles.a.ssl.fastly.net/watercolor',
type: 'zxy',
subdomains: null,
key: null,
min: 0,
max: 20,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Stamen, CC-BY-3.0.'
},
'Toner': {
url: 'stamen-tiles.a.ssl.fastly.net/toner',
type: 'zxy',
subdomains: null,
key: null,
min: 0,
max: 20,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Stamen, CC-BY-3.0.'
},
'Terrain': {
url: 'stamen-tiles.a.ssl.fastly.net/terrain',
type: 'zxy',
subdomains: null,
key: null,
min: 0,
max: 16,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Stamen, CC-BY-3.0.'
},
Topographic: {
url: 'tile.opentopomap.org',
type: 'zxy',
subdomains: null,
key: null,
min: 0,
max: 17,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Opentopomaps'
},
Satellite: {
url: 'services.arcgisonline.com/ArcGIS/rest/services/' +
'World_Imagery/MapServer/tile',
type: 'zyx',
subdomains: null,
key: null,
min: 0,
max: 19,
credits: 'Imagery \u00A9 ArcGIS'
},
Streets: {
url: 'services.arcgisonline.com/ArcGIS/rest/services/' +
'World_Street_Map/MapServer/tile',
type: 'zyx',
subdomains: null,
key: null,
min: 0,
max: 19,
credits: 'Imagery \u00A9 ArcGIS'
},
Shading: {
url: 'services.arcgisonline.com/ArcGIS/rest/services/' +
'World_Topo_Map/MapServer/tile',
type: 'zyx',
subdomains: null,
key: null,
min: 0,
max: 19,
credits: 'Imagery \u00A9 ArcGIS'
},
'Mapbox (experimental)': {
url: 'api.tiles.mapbox.com/v4/mapbox.streets',
type: 'zxy',
subdomains: null,
key: '?access_token=' +
'pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycX' +
'BndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw',
min: 0,
max: 20,
credits: 'Map data \u00A9 OpenStreetMap contributors, ' +
'CC-BY-SA, Imagery \u00A9 Mapbox'
}
};
this.api = this.tileServers[host || 'OpenStreetMap'];
this.lon = -122.257852;
this.lat = 37.872099;
this.zoom = 13;
this.position = new Point(
this.tileXfromLon(this.lon),
this.tileYfromLat(this.lat)
);
this.extent = new Point(480, 360);
this.tileSize = 256;
this.canvas = null;
this.creditsTxt = null;
this.creditsBG = null;
this.initializeCredits();
this.loading = 0;
}
WorldMap.prototype.setHost = function (host) {
this.api = this.tileServers[host || 'Wikimedia'];
this.initializeCredits();
this.setZoom(this.zoom);
};
WorldMap.prototype.setView = function (lon, lat) {
this.lat = lat;
this.lon = lon;
this.refresh();
};
WorldMap.prototype.setZoom = function (num) {
this.zoom = Math.max(Math.min(this.api.max, Math.floor(num)), this.api.min);
this.refresh();
};
WorldMap.prototype.panBy = function (x, y) {
this.lon = this.lonFromTileX(this.position.x + (x / this.tileSize));
this.lat = this.latFromTileY(this.position.y + (y / this.tileSize));
this.refresh();
};
WorldMap.prototype.refresh = function () {
this.position = new Point(
this.wrapTile(this.tileXfromLon(this.lon)),
this.tileYfromLat(this.lat)
);
};
WorldMap.prototype.wrapTile = function (n) {
var max = Math.pow(2, this.zoom);
return n < 0 ? max + n : n % max;
};
WorldMap.prototype.tileXfromLon = function (lon) {
return (parseFloat(lon) + 180) / 360 * Math.pow(2, this.zoom);
};
WorldMap.prototype.tileYfromLat = function (lat) {
return (1 - Math.log(Math.tan(parseFloat(lat) * Math.PI / 180) + 1 /
Math.cos(parseFloat(lat) * Math.PI / 180)) / Math.PI) / 2 *
Math.pow(2, this.zoom);
};
WorldMap.prototype.lonFromTileX = function (x) {
return x / Math.pow(2, this.zoom) * 360 - 180;
};
WorldMap.prototype.latFromTileY = function (y) {
var n = Math.PI - 2 * Math.PI * y / Math.pow(2, this.zoom);
return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
};
WorldMap.prototype.lonFromSnapX = function (x) {
return this.lonFromTileX(this.position.x + (x / this.tileSize));
};
WorldMap.prototype.latFromSnapY = function (y) {
return this.latFromTileY(this.position.y - (y / this.tileSize));
};
WorldMap.prototype.snapXfromLon = function (lon) {
return (this.tileXfromLon(lon) - this.position.x) * this.tileSize;
};
WorldMap.prototype.snapYfromLat = function (lat) {
return (this.tileYfromLat(lat) - this.position.y) * -this.tileSize;
};
WorldMap.prototype.distanceInKm = function(lat1, lon1, lat2, lon2) {
// haversine formula:
var R = 6371, // radius of the earth in km
dLat = radians(+lat2 - lat1),
dLon = radians(+lon2 - lon1),
a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(radians(+lat1)) * Math.cos(radians(+lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2),
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
};
WorldMap.prototype.render = function () {
var cntr = this.extent.divideBy(2),
size = this.tileSize,
tile = this.position.floor(),
off = new Point(
this.position.x % 1,
this.position.y % 1
).multiplyBy(size),
tileOrigin = cntr.subtract(off),
tileDistance = tileOrigin.floorDivideBy(size).add(1),
tileGrid = this.extent.floorDivideBy(size).add(2),
originTile = tile.subtract(tileDistance),
mapOrigin = tileOrigin.subtract(
tileDistance.multiplyBy(size)
),
sub = 0,
myself = this,
max = Math.pow(2, this.zoom),
x, y, img, ctx, tileX, tileY, coords;
function ok() {
myself.loading -= 1;
ctx.drawImage(
this,
mapOrigin.x + (this.cx * size),
mapOrigin.y + (this.cy * size)
);
crd();
}
function err() {
myself.loading -= 1;
crd();
}
function crd() {
if (!myself.loading) {
myself.addCredits();
}
}
// create a new canvas. Note, we cannot reuse the existing canvas,
// because it could be queried while tiles are still rendering
this.canvas = newCanvas(this.extent, true);
ctx = this.canvas.getContext('2d');
for (x = 0; x < tileGrid.x; x += 1) {
for (y = 0; y < tileGrid.y; y += 1) {
tileX = this.wrapTile(originTile.x + x);
tileY = originTile.y + y;
if ((tileX >= 0 && tileX < max) && (tileY >= 0 && tileY < max)) {
img = new Image();
img.cx = x;
img.cy = y;
img.crossOrigin = ''; // anonymous
img.onload = ok;
img.onerror = err;
myself.loading += 1;
switch (this.api.type) {
case 'zxy':
coords = '' + this.zoom + '/' + tileX + '/' + tileY;
break;
case 'zyx':
coords = '' + this.zoom + '/' + tileY + '/' + tileX;
break;
case 'xyz':
coords = '' + tileX + '/' + tileY + '/' + this.zoom ;
break;
}
img.src = 'https://' +
(this.api.subdomains ?
this.api.subdomains[sub] + '.'
: '') +
this.api.url + '/' + coords + '.png' + (this.api.key || '');
if (this.api.subdomains) {
sub += 1;
if (sub === this.api.subdomains.length) {
sub = 0;
}
}
}
}
}
};
WorldMap.prototype.initializeCredits = function () {
var ctx;
this.creditsTxt = new StringMorph(
' ' + this.api.credits + ' ',
8
);
this.creditsTxt.isCachingImage = true;
this.creditsTxt.cachedImage = normalizeCanvas(
this.creditsTxt.getImage(), true
);
this.creditsBG = newCanvas(this.creditsTxt.extent(), true);
ctx = this.creditsBG.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, this.creditsBG.width, this.creditsBG.height);
};
WorldMap.prototype.addCredits = function () {
var ctx = this.canvas.getContext('2d'),
crd = this.creditsTxt.getImage();
ctx.globalAlpha = 0.5;
ctx.drawImage(
this.creditsBG,
this.canvas.width - crd.width,
this.canvas.height - crd.height
);
ctx.globalAlpha = 1;
ctx.drawImage(
crd,
this.canvas.width - crd.width,
this.canvas.height - crd.height
);
};