commit bf9c82181a52f22135020690fbc997f06878c6f9
parent 45e00f9ea3ea0b3e81839ec938ff33ba5a90335b
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Sat, 19 Nov 2016 14:06:17 +0300
wikimapia layer
Diffstat:
4 files changed, 507 insertions(+), 1 deletion(-)
diff --git a/src/config.js b/src/config.js
@@ -4,5 +4,5 @@ export default {
bingKey: 'AhZy06XFi8uAADPQvWNyVseFx4NHYAOH-7OTMKDPctGtYo86kMfx2T0zUrF5AAaM',
westraDataBaseUrl: 'http://nakarte.tk/westraPasses/',
CORSProxyUrl: 'http://proxy.nakarte.tk/',
- elevationsServer: 'http://elevation.nakarte.tk/',
+ elevationsServer: 'http://elevation.nakarte.tk/'
}
diff --git a/src/lib/leaflet.layer.wikimapia/style.css b/src/lib/leaflet.layer.wikimapia/style.css
@@ -0,0 +1,13 @@
+.wikimapia-tooltip {
+ background-color: #FFFFA3;
+ font-size: 10pt;
+ line-height: 1;
+ border: 1px solid #777;
+ border-radius: 4px;
+ white-space: nowrap;
+ padding: 4px 6px;
+ position: absolute;
+ z-index: 10000;
+ max-width: 500px;
+ box-sizing: border-box;
+}
+\ No newline at end of file
diff --git a/src/lib/leaflet.layer.wikimapia/wikimapia.js b/src/lib/leaflet.layer.wikimapia/wikimapia.js
@@ -0,0 +1,313 @@
+import L from 'leaflet';
+import wmUtils from './wm-utils';
+
+
+function isPointInPolygon(polygon, p) {
+ var inside = false;
+ var prevNode = polygon[polygon.length - 1],
+ node, i;
+ for (i = 0; i < polygon.length; i++) {
+ node = polygon[i];
+ if ((node[0] <= p[0] && p[0] < prevNode[0] || prevNode[0] <= p[0] && p[0] < node[0]) && p[1] < (prevNode[1] - node[1]) * (p[0] - node[0]) / (prevNode[0] - node[0]) + node[1]) {
+ inside = !inside;
+ }
+ prevNode = node;
+ }
+ return inside;
+}
+
+const Label = L.Class.extend({
+ initialize: function(text, latlng) {
+ this.text = text;
+ this.latlng = latlng;
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+ this._container = L.DomUtil.create('div', 'leaflet-marker-icon leaflet-zoom-animated wikimapia-tooltip');
+ this._container.innerHTML = this.text;
+
+ map._container.appendChild(this._container);
+ map.on('viewreset', this._updatePosition, this);
+ map.on('mousemove', this.onMouseMove, this);
+ this._updatePosition();
+ },
+
+ onRemove: function(map) {
+ map.off('viewreset', this._updatePosition, this);
+ map.off('mousemove', this.onMouseMove, this);
+ map._container.removeChild(this._container);
+ },
+
+ onMouseMove: function(e) {
+ this.latlng = e.latlng;
+ this._updatePosition();
+ },
+
+ _updatePosition: function() {
+ var pos = this._map.latLngToContainerPoint(this.latlng);
+ var right = pos.x + this._container.clientWidth + 16 + 2;
+ var x, y;
+ y = pos.y - 16;
+ x = pos.x;
+ if (right > this._map._container.clientWidth) {
+ x -= this._container.clientWidth + 16 + 2;
+ } else {
+ x += 16;
+ }
+ L.Util.extend(this._container.style, {
+ top: y + 'px',
+ left: x + 'px'
+ }
+ );
+ }
+ }
+);
+
+// TODO: как-то посылать remove() в очередь загрузок при событии tileunload
+L.Layer.Wikimapia = L.GridLayer.extend({
+ options: {
+ tileSize: 512,
+ updateWhenIdle: true,
+ cacheSize: 30
+ },
+
+ initialize: function(options) {
+ L.GridLayer.prototype.initialize.call(this, options);
+ this._cache = {};
+ this._cachePriority = [];
+ this._downloading = {};
+ },
+
+ onAdd: function(map) {
+ L.GridLayer.prototype.onAdd.call(this, map);
+ map.on('mousemove', this.highlightPlace, this);
+ // map.on('click', this.showPlaceDetails, this);
+ },
+
+ onRemove: function(map) {
+ map.off('mousemove', this.highlightPlace, this);
+ // map.off('click', this.showPlaceDetails, this);
+ // if (this.highlightedPlace) {
+ // this._map.removeLayer(this.highlightedPlace.polygon);
+ // this._map.removeLayer(this.highlightedPlace.label);
+ // this.highlightedPlace = null;
+ // }
+ L.TileLayer.prototype.onRemove.call(this, map);
+
+ },
+
+ raiseCachePriority: function(tileId) {
+ const i = this._cachePriority.indexOf(tileId);
+ if (i > -1) {
+ this._cachePriority.splice(i, 1);
+ }
+ this._cachePriority.unshift(tileId);
+ },
+
+ getWikimapiaDataFromCache: function(tileId) {
+ console.log('getWikimapiaDataFromCache', tileId);
+ if (tileId in this._cache) {
+ console.log('Cache hit', tileId);
+ this.raiseCachePriority(tileId);
+ return this._cache[tileId];
+ }
+ while (tileId.length > 1) {
+ tileId = tileId.substr(0, tileId.length - 1);
+ let tile = this._cache[tileId];
+ if (tile && !tile.hasChildren) {
+ console.log('Cache hit (parent)', tileId);
+ this.raiseCachePriority(tileId);
+ return tile;
+ }
+ }
+ console.log('Cache miss', tileId);
+ return null;
+ },
+
+ putWikimapiaDataToCache: function(data) {
+ const tileId = data.tileId;
+ console.log('putWikimapiaDataToCache', tileId);
+ this._cache[tileId] = data;
+ this.raiseCachePriority(tileId);
+ while (this._cachePriority.length > this.options.cacheSize) {
+ let tileId = this._cachePriority.pop();
+ delete this._cache[tileId];
+ }
+ },
+
+ // TODO: каждые 50-100 мс отдавать управление
+ projectPlacesForZoom: function(tile, viewZoom) {
+ if (viewZoom < tile.coords.z) {
+ throw new Error('viewZoom < tile.zoom');
+ }
+ if (tile.places && tile.places[0] && tile.places[0].projectedPolygons &&
+ tile.places[0].projectedPolygons[viewZoom]) {
+ return;
+ }
+ const virtualTileSize = 256 * (2 ** (viewZoom - tile.coords.z));
+ const p1 = L.point([tile.coords.x * virtualTileSize, tile.coords.y * virtualTileSize]),
+ p2 = L.point([(tile.coords.x + 1) * virtualTileSize, (tile.coords.y + 1) * virtualTileSize]),
+ latlng1 = this._map.unproject(p1, viewZoom),
+ latlng2 = this._map.unproject(p2, viewZoom),
+ lat0 = latlng1.lat,
+ lng0 = latlng1.lng;
+ const qx = (p2.x - p1.x) / (latlng2.lng - latlng1.lng),
+ qy = (p2.y - p1.y) / (latlng2.lat - latlng1.lat);
+
+ const offsets = [],
+ offsetsStep = virtualTileSize / 64;
+ const y0 = p1.y;
+ for (let y = -virtualTileSize; y <= 2 * virtualTileSize; y += offsetsStep) {
+ let lat = y / qy + lat0;
+ let offset = this._map.project([lat, lng0], viewZoom).y - y0 - y;
+ offsets.push(offset);
+ }
+
+ let ll, offset, offsetPos, offsetIndex, offsetIndexDelta, x, y;
+ const x0 = p1.x;
+ for (let place of tile.places) {
+ let polygon = place.polygon;
+ if (polygon.length < 3) {
+ continue;
+ }
+ let projectedPolygon = [];
+ for (let i = 0; i < polygon.length; i++) {
+ ll = polygon[i];
+ x = (ll[1] - lng0) * qx;
+ y = (ll[0] - lat0) * qy;
+ offsetPos = (y + virtualTileSize) / offsetsStep;
+ offsetIndex = Math.floor(offsetPos);
+ offsetIndexDelta = offsetPos - offsetIndex;
+ offset = offsets[offsetIndex] * (1 - offsetIndexDelta) + offsets[offsetIndex + 1] * offsetIndexDelta;
+ projectedPolygon.push([x + x0, y + offset + y0]);
+ }
+ projectedPolygon = projectedPolygon.map(L.point);
+ projectedPolygon = L.LineUtil.simplify(projectedPolygon, 1.5);
+ if (!place.projectedPolygons) {
+ place.projectedPolygons = [];
+ }
+ place.projectedPolygons[viewZoom] = projectedPolygon;
+ }
+ },
+
+ getWikimapiaDataFromServer: async function(tileId) {
+ console.log('getWikimapiaDataFromServer', tileId);
+ let tile;
+
+ try {
+ tile = await wmUtils.fetchTile(tileId)
+ } catch (e) {
+ return {error: e.message};
+ } finally {
+ delete this._downloading[tileId];
+ }
+ try {
+ tile = wmUtils.parseTile(tile)
+ } catch (e) {
+ console.log('Error parsing tile', e);
+ return {'error': `Malformed wikimapia data for tile ${tileId}`}
+ }
+
+ this.putWikimapiaDataToCache(tile);
+ return tile;
+ },
+
+ getWikimapiaDataForTile: function(coords) {
+ console.log('getWikimapiaDataForTile', coords);
+ coords = wmUtils.getWikimapiaTileCoords(coords, this.options.tileSize);
+ const tileId = wmUtils.getTileId(coords);
+ if (tileId in this._downloading) {
+ console.log('Already downloading', coords, tileId);
+ return this._downloading[tileId];
+ }
+ let data = this.getWikimapiaDataFromCache(tileId);
+ if (data) {
+ return Promise.resolve(data);
+ }
+ let dataPromise = this.getWikimapiaDataFromServer(tileId);
+ this._downloading[tileId] = dataPromise;
+ return dataPromise;
+ },
+
+
+ drawTile: function(coords, canvas, data) {
+ if (!this._map) {
+ return;
+ }
+ console.log('drawTile', coords, data);
+ if (!data.places) {
+ return;
+ }
+ const canvasCtx = canvas.getContext('2d');
+ canvasCtx.strokeStyle = '#CFA600';
+ canvasCtx.lineWidth = 1;
+ this.projectPlacesForZoom(data, coords.z);
+ const x0 = coords.x * this.options.tileSize,
+ y0 = coords.y * this.options.tileSize;
+ for (let place of data.places) {
+ if (!place.projectedPolygons) {
+ continue;
+ }
+ let polygon = place.projectedPolygons[coords.z];
+ canvasCtx.moveTo(polygon[0].x - x0, polygon[0].y - y0);
+ let p;
+ for (let i = 1; i < polygon.length; i++) {
+ p = polygon[i];
+ canvasCtx.lineTo(p.x - x0, p.y - y0);
+ }
+ canvasCtx.lineTo(polygon[0].x - x0, polygon[0].y - y0);
+ }
+ canvasCtx.stroke();
+
+ },
+
+ createTile: function(coords, done) {
+ console.log('createTile', coords);
+ const canvas = L.DomUtil.create('canvas', 'leaflet-tile');
+ canvas.width = this.options.tileSize;
+ canvas.height = this.options.tileSize;
+ this.getWikimapiaDataForTile(coords)
+ .then((data) => {
+ let t1 = Date.now();
+ this.drawTile(coords, canvas, data)
+ let t2 = Date.now();
+ console.log('Tile drawn', t2 - t1);
+ }
+ )
+ .then(() => {
+ done(null, canvas)
+ }
+ );
+ return canvas;
+ },
+
+ getTileAtLayerPoint: function(x, y, z) {
+ for (let tileId of this._cachePriority) {
+ let tile = this._cache[tileId];
+
+ }
+ // var i, tile;
+
+ // for (i = 0; i < this.tileCache.length; i++) {
+ // tile = this.tileCache[i];
+ // if (x >= tile.x0 && x < tile.x0 + 1024 && y >= tile.y0 && y < tile.y0 + 1024) {
+ // return tile;
+ // }
+ // }
+ },
+
+ getPlaceAtLayerPoint: function(x, y, places) {
+ var j, bounds, place;
+ for (j = places.length - 1; j >= 0; j--) {
+ place = places[j];
+ bounds = place.localBoundsWESN;
+ if (x >= bounds[0] && x <= bounds[1] && y >= bounds[3] && y <= bounds[2] && isPointInPolygon(place.localPolygon, [x, y])) {
+ return place;
+ }
+ }
+ }
+
+
+ }
+);
diff --git a/src/lib/leaflet.layer.wikimapia/wm-utils.js b/src/lib/leaflet.layer.wikimapia/wm-utils.js
@@ -0,0 +1,178 @@
+import urlViaCorsProxy from 'lib/CORSProxy';
+import 'lib/xhr-promise';
+
+
+// (1233,130,5) -> "032203"
+function getTileId({x, y, z}) {
+ let id = [];
+ y = (1 << z) - y - 1;
+ z += 1;
+ while (z) {
+ id.push((x & 1) + (y & 1) * 2);
+ x >>= 1;
+ y >>= 1;
+ z--;
+ }
+ return id.reverse().join('');
+}
+
+
+function tileIdToCoords(tileId) {
+ const z = tileId.length - 1;
+ let x = 0,
+ y = 0;
+ for (let i = 0; i < tileId.length; i++) {
+ let c = parseInt(tileId[i], 10);
+ x <<= 1;
+ y <<= 1;
+ x += c & 1;
+ y += c >> 1;
+ }
+ y = (1 << z) - y - 1;
+ return {x, y, z};
+}
+
+function getWikimapiaTileCoords(coords, viewTileSize) {
+ let z = coords.z - 2;
+ if (z < 0) {
+ z = 0;
+ }
+ if (z > 15) {
+ z = 15;
+ }
+ const q = 2 ** (z - coords.z + Math.log2(viewTileSize / 256));
+ let x = Math.floor(coords.x * q),
+ y = Math.floor(coords.y * q);
+ return {x, y, z};
+}
+
+function fetchTile(tileId) {
+ let url = tileId.replace(/(\d{3})(?!$)/g, '$1/'); // "033331022" -> "033/331/022"
+ url = 'http://wikimapia.org/z1/itiles/' + url + '.xy?342342';
+
+ url = urlViaCorsProxy(url);
+ let {promise, requestId} = window.xmlHttpRequestQueue.put(url, {timeout: 30000});
+ promise = promise.then((xhr) => {
+ if (xhr.status === 0) {
+ throw new Error(`Network error while fetching wikimapia data, request "${url}"`);
+ }
+ if (xhr.status !== 200) {
+ throw new Error(`Wikimapia server responded with status ${xhr.status}, request "${url}"`);
+ }
+ return xhr.response;
+ }
+ );
+ const abort = () => {
+ window.xmlHttpRequestQueue.remove(requestId);
+ };
+ // return {promise, abort};
+ return promise;
+
+}
+
+function decodeTitles(s) {
+ const titles = {};
+ for (let title of s.split('\x1f')) {
+ if (title.length > 2) {
+ let langCode = title.charCodeAt(0) - 32;
+ titles[langCode.toString()] = title.substring(1);
+ }
+ }
+ return titles;
+}
+
+function chooseTitle(titles) {
+ var popularLanguages = ['1', '0', '3', '2', '5', '4', '9', '28', '17', '27'];
+ for (let langCode of popularLanguages) {
+ if (langCode in titles) {
+ return titles[langCode];
+ }
+ }
+ for (let langCode of Object.keys(titles)) {
+ return titles[langCode];
+ }
+}
+
+function decodePolygon(s) {
+ var i = 0,
+ coords = [],
+ lat = 0,
+ lng = 0;
+ while (i < s.length) {
+ var p, l = 0,
+ c = 0;
+ do {
+ p = s.charCodeAt(i++) - 63;
+ c |= (p & 31) << l;
+ l += 5;
+ } while (p >= 32);
+ lng += c & 1 ? ~(c >> 1) : c >> 1;
+ l = 0;
+ c = 0;
+ do {
+ p = s.charCodeAt(i++) - 63;
+ c |= (p & 31) << l;
+ l += 5;
+ } while (p >= 32);
+ lat += c & 1 ? ~(c >> 1) : c >> 1;
+ coords.push([lat / 1e6, lng / 1e6]);
+ }
+ return coords;
+}
+
+function parseTile(s) {
+ const tile = {};
+ const places = tile.places = [];
+ const lines = s.split('\n');
+ if (lines.length < 1) {
+ throw new Error('No data in tile');
+ }
+ const fields = lines[0].split('|');
+ const tileId = fields[0];
+ if (!tileId || !tileId.match(/^[0-3]+$/)) {
+ throw new Error('Invalid tile header');
+ }
+ tile.tileId = tileId;
+ tile.coords = tileIdToCoords(tileId);
+ tile.hasChildren = fields[1] === '1';
+
+ //FIXME: ignore some errors
+ for (let line of lines.slice(2)) {
+ const place = {};
+ const fields = line.split('|');
+ if (fields.length < 6) {
+ continue;
+ }
+ let placeId = fields[0];
+ if (!placeId.match(/^\d+$/)) {
+ // throw new Error('Invalid place id');
+ continue;
+ }
+ place.id = parseInt(placeId, 10);
+ place.title = chooseTitle(decodeTitles(fields[5]));
+ if (fields[6] !== '1') {
+ throw new Error(`Unknown wikimapia polygon encoding type: "${fields[6]}"`);
+ }
+
+ let bounds = fields[2].match(/^([-\d]+),([-\d]+),([-\d]+),([-\d]+)$/);
+ if (!bounds) {
+ throw new Error('Invalid place bounds');
+ }
+ place.boundsWESN = bounds.slice(1).map((x) => {
+ return parseInt(x, 10) / 1e7
+ }
+ );
+
+ let coords = fields.slice(7).join('|');
+
+ coords = decodePolygon(coords);
+ if (coords.length < 3) {
+ throw new Error(`Polygon has ${coords.length} points`);
+ }
+ place.polygon = coords;
+ places.push(place);
+ }
+ return tile;
+}
+
+export default {getTileId, getWikimapiaTileCoords, fetchTile, parseTile}
+\ No newline at end of file