commit 5897c861e1eed55b3efc6f350c73a7a9de402590
parent 199d71b4223e0e051abbdf25b50396c9f66f2b64
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Tue, 31 Jan 2017 00:03:12 +0300
[wikimapia] use new loader module
Diffstat:
7 files changed, 393 insertions(+), 355 deletions(-)
diff --git a/src/layers.js b/src/layers.js
@@ -7,6 +7,7 @@ import 'lib/leaflet.layer.soviet-topomaps-grid';
 import 'lib/leaflet.layer.westraPasses';
 import 'lib/leaflet.layer.nordeskart';
 import 'lib/leaflet.layer.tracks-collection';
+import 'lib/leaflet.layer.wikimapia';
 
 export default function getLayers() {
     const layers = [
@@ -195,13 +196,13 @@ export default function getLayers() {
                     isDefault: true,
                     layer: new L.Layer.SovietTopoGrid({code: 'Ng'})
                 },
-                // {
-                //     title: 'Wikimapia',
-                //     order: 1130,
-                //     isOverlay: true,
-                //     isDefault: true,
-                //     layer: new L.Wikimapia({code: 'W', zIndexOffset: 10000}),
-                // },
+                {
+                    title: 'Wikimapia',
+                    order: 1130,
+                    isOverlay: true,
+                    isDefault: true,
+                    layer: new L.Wikimapia({code: 'W'}),
+                },
                 {
                     title: 'Mountain passes (Westra)',
                     order: 1140,
diff --git a/src/lib/leaflet.layer.wikimapia/index.js b/src/lib/leaflet.layer.wikimapia/index.js
@@ -0,0 +1,207 @@
+import L from 'leaflet';
+import {WikimapiaLoader} from './wikimapia-loader';
+import './style.css';
+
+
+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;
+}
+
+L.Wikimapia = L.GridLayer.extend({
+        options: {
+            tileSize: 1024,
+            updateWhenIdle: true,
+        },
+
+        initialize: function(options) {
+            L.GridLayer.prototype.initialize.call(this, options);
+            this.loader = null;
+        },
+
+        onAdd: function(map) {
+            if (!this.loader) {
+                this.loader = new WikimapiaLoader(map.project.bind(map));
+            }
+            L.GridLayer.prototype.onAdd.call(this, map);
+            this.on('tileunload', this.onTileUnload, this);
+            map.on('mousemove', this.onMouseMove, this);
+            map.on('click', this.onClick, this);
+        },
+
+        onRemove: function(map) {
+            map.off('mousemove', this.onMouseMove, this);
+            map.off('click', this.onClick, 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);
+            this.off('tileunload', this.onTileUnload, this);
+
+        },
+
+        drawTile: function(canvas) {
+            if (!this._map) {
+                return;
+            }
+            const
+                tileData = canvas._tileData,
+                adjustment = canvas._adjustment;
+            if (!tileData) {
+                return;
+            }
+
+            const canvasCtx = canvas.getContext('2d');
+            canvasCtx.beginPath();
+            canvasCtx.strokeStyle = '#CFA600';
+            canvasCtx.lineWidth = 1;
+            for (let place of tileData.places) {
+                let polygon = place.localPolygon;
+                if (!polygon) {
+                    continue;
+                }
+                if (adjustment) {
+                    let {multiplier, offsetX, offsetY} = adjustment,
+                        polygon2 = [];
+                    for (let i = 0; i < polygon.length; i++) {
+                        let p = polygon[i];
+                        polygon2.push({
+                                x: p.x * multiplier - offsetX,
+                                y: p.y * multiplier - offsetY
+                            }
+                        );
+                    }
+                    polygon = polygon2;
+                }
+                canvasCtx.moveTo(polygon[0].x, polygon[0].y);
+                let p;
+                for (let i = 1; i < polygon.length; i++) {
+                    p = polygon[i];
+                    canvasCtx.lineTo(p.x, p.y);
+                }
+                canvasCtx.lineTo(polygon[0].x, polygon[0].y);
+            }
+            canvasCtx.stroke();
+
+        },
+
+        createTile: function(coords, done) {
+            const canvas = L.DomUtil.create('canvas', 'leaflet-tile');
+            canvas.width = this.options.tileSize;
+            canvas.height = this.options.tileSize;
+
+            let {dataPromise, abortLoading} = this.loader.requestTileData(coords);
+            dataPromise.then((data) => {
+                    canvas._tileData = data.tileData;
+                    canvas._adjustment = data.adjustment;
+                    this.drawTile(canvas);
+                    this._tileOnLoad(done, canvas)
+                }
+            );
+
+            canvas._abortLoading = abortLoading;
+            return canvas;
+        },
+
+        _tileOnLoad: function(done, tile) {
+            // For https://github.com/Leaflet/Leaflet/issues/3332
+            if (L.Browser.ielt9) {
+                setTimeout(L.bind(done, this, null, tile), 0);
+            } else {
+                done(null, tile);
+            }
+        },
+
+        onTileUnload: function(e) {
+            const tile = e.tile;
+            tile._abortLoading();
+            delete tile._tileData;
+            delete tile._adjustment;
+        },
+
+        _tileCoordsFromEvent: function(e) {
+            const layerPoint = this._map.getPixelOrigin().add(e.layerPoint);
+            const tileSize = this.options.tileSize;
+            let coords = {
+                x: Math.floor(layerPoint.x / tileSize),
+                y: Math.floor(layerPoint.y / tileSize),
+                z: this._map.getZoom()
+            };
+
+            return coords;
+        },
+
+        getPlaceAtMousePos: function(e) {
+            const tileCoords = this._tileCoordsFromEvent(e);
+            let tile = this._tiles[this._tileCoordsToKey(tileCoords)];
+            if (!tile) {
+                return null;
+            }
+            const tileData = tile.el._tileData;
+            if (!tileData) {
+                return null;
+            }
+            return this.getPlaceAtLatlng(e.latlng, tileData.places);
+        },
+
+        onMouseMove: function(e) {
+            const place = this.getPlaceAtMousePos(e);
+            if (this.highlightedPlace && (!place || this.highlightedPlace.id != place.id)) {
+                this._map.removeLayer(this.highlightedPlace.polygon);
+                this._map.removeLayer(this.highlightedPlace.label);
+                this.highlightedPlace = null;
+            }
+
+            if (place && !this.highlightedPlace) {
+                this.highlightedPlace = {
+                    id: place.id,
+                    polygon: L.polygon(place.polygon, {
+                            weight: 0,
+                            color: '#E6B800'
+                        }
+                    ),
+                    label: L.tooltip({className: 'wikimapia-tooltip'}, null)
+                };
+                this.highlightedPlace.label.setLatLng(e.latlng);
+                this.highlightedPlace.polygon.addTo(this._map);
+                this.highlightedPlace.label.setContent(place.title);
+                this._map.addLayer(this.highlightedPlace.label);
+            }
+            if (this.highlightedPlace) {
+                this.highlightedPlace.label.setLatLng(e.latlng);
+            }
+        },
+
+        onClick: function(e) {
+
+        },
+
+        getPlaceAtLatlng: function(latlng, places) {
+            let {lat, lng} = latlng,
+                bounds, place;
+            for (let i = places.length - 1; i >= 0; i--) {
+                place = places[i];
+                bounds = place.boundsWESN;
+                if (lng >= bounds[0] && lng <= bounds[1] && lat >= bounds[2] && lat <= bounds[3] &&
+                    isPointInPolygon(place.polygon, [lat, lng])) {
+                    return place;
+                }
+            }
+        }
+
+    }
+);
diff --git a/src/lib/leaflet.layer.wikimapia/style.css b/src/lib/leaflet.layer.wikimapia/style.css
@@ -10,4 +10,14 @@
     z-index: 10000;
     max-width: 500px;
     box-sizing: border-box;
-}
-\ No newline at end of file
+    margin-top: -6px;
+    /*box-shadow: none !important;*/
+}
+
+.wikimapia-tooltip:before {
+    border: none !important;
+}
+
+.leaflet-tooltip-right {
+    margin-left: 16px !important;
+}
diff --git a/src/lib/leaflet.layer.wikimapia/wikimapia-loader.js b/src/lib/leaflet.layer.wikimapia/wikimapia-loader.js
@@ -0,0 +1,80 @@
+import {TiledDataLoader} from 'lib/tiled-data-loader';
+import wmUtils from './wm-utils'
+import urlViaCorsProxy from 'lib/CORSProxy';
+
+class WikimapiaLoader extends TiledDataLoader {
+    maxZoom = 15;
+    tileSize = 1024;
+
+    constructor(projectFunc) {
+        super();
+        this._project = projectFunc;
+
+    }
+
+    getFromCache(dataTileCoords) {
+        dataTileCoords = Object.assign({}, dataTileCoords);
+        let exactMatch = true;
+        while (dataTileCoords.z >= 0) {
+            let key = this.makeTileKey(dataTileCoords);
+            let res = this._cache.get(key);
+            if (res.found) {
+                if (exactMatch || !res.value.hasChildren) {
+                    res['coords'] = dataTileCoords;
+                    return res;
+                }
+                break;
+            }
+            dataTileCoords.z -= 1;
+            dataTileCoords.x = Math.floor(dataTileCoords.x / 2);
+            dataTileCoords.y = Math.floor(dataTileCoords.y / 2);
+            exactMatch = false;
+        }
+        return {found: false};
+    }
+
+    layerTileToDataTileCoords(layerTileCoords) {
+        let z = layerTileCoords.z - 2;
+        if (z > this.maxZoom) {
+            let z2 = this.maxZoom,
+                multiplier = 1 << (z - z2);
+            return {
+                x: Math.floor(layerTileCoords.x / multiplier),
+                y: Math.floor(layerTileCoords.y / multiplier),
+                z: z2
+            }
+        } else {
+            return {z, x: layerTileCoords.x, y: layerTileCoords.y}
+        }
+    }
+
+    makeRequestData(dataTileCoords) {
+        let url = wmUtils.makeTileUrl(dataTileCoords);
+        url = urlViaCorsProxy(url);
+        return {
+            url,
+            options: {timeout: 20000}
+        }
+    }
+
+    processResponse(xhr) {
+        const tileData = wmUtils.parseTile(xhr.response, this._project);
+        return {
+            tileData,
+            coords: tileData.coords
+        }
+    }
+
+    calcAdjustment(layerTileCoords, dataTileCoords) {
+        const adjustment = super.calcAdjustment(
+            {x: layerTileCoords.x, y: layerTileCoords.y, z: layerTileCoords.z - 2},
+            dataTileCoords);
+        if (adjustment) {
+            adjustment.offsetX *= 1024;
+            adjustment.offsetY *= 1024;
+        }
+        return adjustment;
+    }
+}
+
+export {WikimapiaLoader};
+\ No newline at end of file
diff --git a/src/lib/leaflet.layer.wikimapia/wikimapia.js b/src/lib/leaflet.layer.wikimapia/wikimapia.js
@@ -1,313 +0,0 @@
-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
@@ -1,7 +1,3 @@
-import urlViaCorsProxy from 'lib/CORSProxy';
-import 'lib/xhr-promise';
-
-
 // (1233,130,5) -> "032203"
 function getTileId({x, y, z}) {
     let id = [];
@@ -16,6 +12,12 @@ function getTileId({x, y, z}) {
     return id.reverse().join('');
 }
 
+function makeTileUrl(coords) {
+    const
+        tileId = getTileId(coords),
+        urlPath = tileId.replace(/(\d{3})(?!$)/g, '$1/'); // "033331022" -> "033/331/022"
+    return `http://wikimapia.org/z1/itiles/${urlPath}.xy?342342`;
+}
 
 function tileIdToCoords(tileId) {
     const z = tileId.length - 1;
@@ -46,29 +48,6 @@ function getWikimapiaTileCoords(coords, viewTileSize) {
     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 = {};
@@ -120,7 +99,23 @@ function decodePolygon(s) {
     return coords;
 }
 
-function parseTile(s) {
+function makeCoordsLocal(line, tileCoords, projectFunc) {
+    const {x: tileX, y: tileY, z: tileZ} = tileCoords,
+        x0 = tileX * 1024,
+        y0 = tileY * 1024,
+        localCoords = [];
+    let latlon, p;
+    for (let i = 0; i < line.length; i++) {
+        latlon = line[i];
+        p = projectFunc(latlon, tileZ + 2);
+        p.x -= x0;
+        p.y -= y0;
+        localCoords.push(p);
+    }
+    return localCoords;
+}
+
+function parseTile(s, projectFunc) {
     const tile = {};
     const places = tile.places = [];
     const lines = s.split('\n');
@@ -170,9 +165,67 @@ function parseTile(s) {
             throw new Error(`Polygon has ${coords.length} points`);
         }
         place.polygon = coords;
+        place.localPolygon = makeCoordsLocal(coords, tile.coords, projectFunc);
+        // place.localBoundsWESN = makeBoundsLocal(place.boundsWESN);
         places.push(place);
     }
     return tile;
 }
 
-export default {getTileId, getWikimapiaTileCoords, fetchTile, parseTile}
-\ No newline at end of file
+// быстрое проектирование полигонов
+// 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;
+//     }
+// },
+
+export default {getTileId, getWikimapiaTileCoords, parseTile, makeTileUrl}
+\ No newline at end of file
diff --git a/src/lib/tiled-data-loader/index.js b/src/lib/tiled-data-loader/index.js
@@ -41,9 +41,9 @@ class TiledDataLoader {
     }
 
     calcAdjustment(layerTileCoords, dataTileCoords) {
-        if (layerTileCoords.x == dataTileCoords.x &&
-            layerTileCoords.y == dataTileCoords.y &&
-            layerTileCoords.z == dataTileCoords.z) {
+        if (layerTileCoords.x === dataTileCoords.x &&
+            layerTileCoords.y === dataTileCoords.y &&
+            layerTileCoords.z === dataTileCoords.z) {
             return null;
         }
         if (dataTileCoords.z > layerTileCoords.z) {