nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

commit 45e00f9ea3ea0b3e81839ec938ff33ba5a90335b
parent 5255342df86821e12a99dca801b38bf294f50a90
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Tue, 24 Jan 2017 00:21:47 +0300

added tiled tracks layer

Diffstat:
Mpackage.json | 1+
Msrc/layers.js | 21++++++++++++++++++---
Asrc/lib/cache/index.js | 32++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.layer.tracks-collection/index.js | 425+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.layer.tracks-collection/style.css | 4++++
Asrc/lib/leaflet.layer.tracks-collection/track_tile_proto.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/leaflet.layer.tracks-collection/update_proto.js | 2++
7 files changed, 533 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json @@ -57,6 +57,7 @@ "knockout": "^3.4.0", "leaflet": "1.0.3", "load-script": "^1.0.0", + "pbf": "^3.0.5", "rbush": "^2.0.1", "utf8": "^2.1.2" }, diff --git a/src/layers.js b/src/layers.js @@ -6,6 +6,7 @@ import config from './config'; import 'lib/leaflet.layer.soviet-topomaps-grid'; import 'lib/leaflet.layer.westraPasses'; import 'lib/leaflet.layer.nordeskart'; +import 'lib/leaflet.layer.tracks-collection'; export default function getLayers() { const layers = [ @@ -212,7 +213,21 @@ export default function getLayers() { scaleDependent: true } ) - }] + }, + { + title: 'Tracks', + order: 1150, + isOverlay: true, + isDefault: true, + layer: new L.TracksCollection({ + tms: true, + maxNativeZoom: 12, + code: 'Tc', + print: false, + } + ) + }, + ] }, @@ -294,7 +309,7 @@ export default function getLayers() { isOverlay: true, isDefault: false, layer: new L.TileLayer.Nordeskart('http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=toporaster3&zoom={z}&x={x}&y={y}&gkt={baatToken}', - {code: 'Np', maxNativeZoom: 16, tms: false, print: true, jnx: true, scaleDependent: true} + {code: 'Np', maxNativeZoom: 16, tms: false, print: true, jnx: true, scaleDependent: true} ) }, { @@ -303,7 +318,7 @@ export default function getLayers() { isOverlay: true, isDefault: false, layer: new L.TileLayer.Nordeskart('http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=topo2&zoom={z}&x={x}&y={y}&gkt={baatToken}', - {code: 'Nm', tms: false, print: true, jnx: true, scaleDependent: true} + {code: 'Nm', tms: false, print: true, jnx: true, scaleDependent: true} ) }, { diff --git a/src/lib/cache/index.js b/src/lib/cache/index.js @@ -0,0 +1,31 @@ +class Cache { + constructor(maxSize) { + this._maxSize = maxSize; + this._store = {}; + this._size = 0; + } + + get(key) { + if (key in this._store) { + const value = this._store[key]; + delete this._store[key]; + this._store[key] = value; + return value; + } + } + + put(key, value) { + if (key in this._store) { + delete this._store[key]; + } else { + this._size += 1; + } + this._store[key] = value; + if (this._size > this._maxSize) { + delete this._store[Object.keys(this._store)[0]]; + this._size -= 1; + } + } +} + +export {Cache} +\ No newline at end of file diff --git a/src/lib/leaflet.layer.tracks-collection/index.js b/src/lib/leaflet.layer.tracks-collection/index.js @@ -0,0 +1,424 @@ +import L from 'leaflet'; +import {fetch} from 'lib/xhr-promise'; +import Pbf from 'pbf'; +import {Tile as TileProto} from './track_tile_proto'; +import {Cache} from 'lib/cache'; +import './style.css'; + +class TileCache extends Cache { + makeKey(coords) { + return `${coords.x}:${coords.y}:${coords.z}`; + } + + get(coords) { + return super.get(this.makeKey(coords)); + } + + put(coords, value) { + super.put(this.makeKey(coords), value); + } +} + +class TrackTilesLoader { + constructor(url, maxZoom) { + this.url = url; + this.maxZoom = maxZoom; + this.cache = new TileCache(50); + } + + getTileUrl(coords) { + const data = { + x: coords.x, + z: coords.z, + y: (2 ** coords.z) - coords.y - 1 + }; + return L.Util.template(this.url, data) + } + + unpackTileData(dataArray) { + const + pbf = new Pbf(new Uint8Array(dataArray)), + tileData = TileProto.read(pbf); + + const tracksById = {}; + for (let track of tileData.tracks) { + const + ar = track.coordinates, + ar_len = ar.length, + ar2 = new Float64Array(ar_len); + for (let i = 0; i < ar_len; i++) { + ar2[i] = ar[i] / 255 * 256; + } + track.coordinates = ar2; + tracksById[track.trackId] = track; + } + tileData.tracksById = tracksById; + return tileData; + } + + requestTileData(coords) { + let {x, y, z} = coords; + let adjustments; + if (z > this.maxZoom) { + let z2 = this.maxZoom, + multiplier = 1 << (z - z2), + x2 = Math.floor(x / multiplier), + y2 = Math.floor(y / multiplier); + adjustments = { + multiplier: multiplier, + offsetX: (x - x2 * multiplier) * 256, + offsetY: (y - y2 * multiplier) * 256 + }; + [x, y, z] = [x2, y2, z2]; + } + + let tileData = this.cache.get({x, y, z}); + if (tileData !== undefined) { + return { + dataPromise: Promise.resolve({tileData, adjustments}), + abortLoading: () => { + } + } + } + let url = this.getTileUrl({x, y, z}); + const fetchPromise = fetch(url, { + responseType: 'arraybuffer', + timeout: 10000, + isResponseSuccess: (xhr) => xhr.status === 200 || xhr.status === 204 + } + ); + //TODO: handle errors + const dataPromise = fetchPromise.then((xhr) => { + let tileData; + if (xhr.status === 200 && xhr.response) { + tileData = this.unpackTileData(xhr.response) + } else { + tileData = null; + } + this.cache.put({x, y, z}, tileData); + return {tileData, adjustments}; + } + ); + return {dataPromise, abortLoading: fetchPromise.abort.bind(fetchPromise)}; + } +} + +function sqDistancePointToSegment(p, p1, p2) { + var x = p1.x, + y = p1.y, + dx = p2.x - x, + dy = p2.y - y, + dot = dx * dx + dy * dy, + t; + + if (dot > 0) { + t = ((p.x - x) * dx + (p.y - y) * dy) / dot; + + if (t > 1) { + x = p2.x; + y = p2.y; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.x - x; + dy = p.y - y; + + return dx * dx + dy * dy; +} + +function isPointOnTrack(track, p, tolerance) { + let ar = track.coordinates, + pos = 0, + p1, p2; + for (let segmentLength of track.segmentsLengths) { + p1 = { + x: ar[pos], + y: ar[pos + 1] + }; + pos += 2; + for (let i = 1; i < segmentLength; i++) { + p2 = { + x: ar[pos], + y: ar[pos + 1] + }; + if (sqDistancePointToSegment(p, p1, p2) < tolerance) { + return true; + } + pos += 2; + p1 = p2; + } + } + return false; +} + +L.ProtobufTileLines = L.GridLayer.extend({ + options: { + url: 'http://tracks-collection/gd/tile/{x}/{y}/{z}', + tileSize: 256, + trackFilter: 1 + 2, + noWrap: true, + bounds: L.latLngBounds([[-90, -180], [90, 180]]) + }, + + initialize: function(options) { + L.GridLayer.prototype.initialize.call(this, options); + // FIXME: maxZoom mus be from arguments + this.loader = new TrackTilesLoader(this.options.url, 12); + }, + + onAdd: function(map) { + L.GridLayer.prototype.onAdd.call(this, map); + this.on('tileunload', this.onTileUnload, this); + map.on('mousemove', this.onMouseMove, this); + L.DomUtil.addClass(this._map._container, 'tile-tracks-active'); + }, + + onRemove: function(map) { + L.DomUtil.removeClass(this._map._container, 'tile-tracks-active'); + this.off('tileunload', this.onTileUnload, this); + map.off('mousemove', this.onMouseMove, this); + L.GridLayer.prototype.onRemove.call(this, map); + + }, + + createTile: function(coords, done) { + const tile = document.createElement('div'); + const canvas = document.createElement('canvas'); + const overlayCanvas = document.createElement('canvas'); + tile.appendChild(canvas); + tile.appendChild(overlayCanvas); + canvas.width = 256; + canvas.height = 256; + overlayCanvas.width = 256; + overlayCanvas.height = 256; + let {dataPromise, abortLoading} = this.loader.requestTileData(coords); + dataPromise.then((tileData) => { + tile._tileData = tileData.tileData; + tile._adjustments = tileData.adjustments; + this.drawTile(canvas, tileData); + this._tileOnLoad(done, tile) + } + ); + + tile._canvas = canvas; + tile._overlayCanvas = overlayCanvas; + tile._abortLoading = abortLoading; + return tile; + }, + + drawTile: function(canvas, {tileData, adjustments}) { + if (!tileData) { + return + } + let rastersCombinedFilter = 0; + for (let raster of tileData.rasters) { + if (this.options.trackFilter & raster.filter) { + rastersCombinedFilter |= raster.filter; + } + } + canvas._rastersCombinedFilter = rastersCombinedFilter; + if (tileData.rasters.length) { + this.drawRasterTile(canvas, tileData.rasters); + } + this.drawVectorTile(canvas, tileData.tracks, adjustments); + }, + + drawRasterTile: function(canvas, rasters) { + const color = 'rgba(0, 0, 255, 0.004)'; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 256, 256); + const imageData = ctx.getImageData(0, 0, 256, 256); + const data = imageData.data; + let ind, c1, c2; + for (let raster of rasters) { + if (raster.filter & this.options.trackFilter) { + let rasterBytes = raster.raster; + for (let i = 0; i < 65536; i++) { + ind = i * 4 + 3; + c1 = data[ind]; + c2 = rasterBytes[i]; + data[ind] = (c1 < c2) ? c2 : c1; + } + } + } + ctx.putImageData(imageData, 0, 0); + canvas._imageData = data; + }, + + drawTrackLines: function(context, track, adjustments) { + let ar = track.coordinates, + x, y; + if (adjustments) { + const + ar_len = ar.length, + ar2 = new Float64Array(ar_len), + {multiplier, offsetX, offsetY} = adjustments; + let i = 0; + while (i < ar_len) { + ar2[i] = ar[i] * multiplier - offsetX; + ar2[i + 1] = ar[i + 1] * multiplier - offsetY; + i += 2; + } + ar = ar2; + } + let pos = 0; + for (let segmentLength of track.segmentsLengths) { + x = ar[pos]; + y = ar[pos + 1]; + pos += 2; + context.moveTo(x, y); + for (let i = 1; i < segmentLength; i++) { + x = ar[pos]; + y = ar[pos + 1]; + context.lineTo(x, y); + pos += 2; + } + } + }, + + drawVectorTile: function(canvas, tracks, adjustments) { + const ctx = canvas.getContext('2d'); + let hasDrawn = false; + ctx.strokeStyle = '#0000FF'; + ctx.beginPath(); + for (let track of tracks) { + if (track.filter & this.options.trackFilter & ~canvas._rastersCombinedFilter) { + hasDrawn = true; + this.drawTrackLines(ctx, track, adjustments); + } + } + if (hasDrawn) { + ctx.stroke(); + } + }, + + _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._promise; + delete tile._features; + }, + + + highlightTracks: function(trackIds) { + console.log(trackIds); + for (let tile of Object.values(this._tiles)) { + if (tile.current) { + let tileData = tile.el._tileData; + if (!tileData) { + continue; + } + let canvas = tile.el._overlayCanvas; + let ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, 256, 256); + ctx.beginPath(); + ctx.strokeStyle = '#FFAA00'; + ctx.lineWidth = 2; + for (let trackId of trackIds) { + let track = tileData.tracksById[trackId]; + if (track) { + this.drawTrackLines(ctx, track, tile.el._adjustments); + } + } + ctx.stroke(); + } + } + }, + + _tileCoordsFromEvent: function(e) { + const layerPoint = this._map.getPixelOrigin().add(e.layerPoint); + let coords = { + x: Math.floor(layerPoint.x / 256), + y: Math.floor(layerPoint.y / 256), + z: this._map.getZoom() + }; + + return { + tileCoords: coords, + pixelCoords: { + x: layerPoint.x % 256, + y: layerPoint.y % 256 + } + }; + }, + + pointNearRasterLine: function(imageData, pixel, tolerance) { + let minX = pixel.x - tolerance, + maxX = pixel.x + tolerance, + minY = pixel.y - tolerance, + maxY = pixel.y + tolerance; + for (let y = minY; y <= maxY; y++){ + for (let x = minX; x <= maxX; x++){ + if (imageData[(y * 256 + x) * 4 + 3] > 127) { + return true; + } + } + } + return false; + }, + + getTracksIdsForPoint: function(tile, pixel) { + const + tileData = tile._tileData, + adjustments = tile._adjustments; + let + tolerance = 5; + if (!tileData) { + return []; + } + if (adjustments) { + pixel.x = (pixel.x + adjustments.offsetX) / adjustments.multiplier; + pixel.y = (pixel.y + adjustments.offsetY) / adjustments.multiplier; + tolerance /= adjustments.multiplier; + } + if (tile._canvas._imageData) { // tile was drawn as raster + let trackIds = []; + if (this.pointNearRasterLine(tile._canvas._imageData, pixel, tolerance)) { + trackIds = 'RASTER'; + } + return trackIds; + } + let sqTolerance = tolerance * tolerance, + trackIds = {}; + for (let track of tileData.tracks) { + if (track.filter & this.options.trackFilter) { + if (isPointOnTrack(track, pixel, sqTolerance)) { + trackIds[track.trackId] = 1; + } + } + } + return Object.keys(trackIds); + }, + + onMouseMove: function(e) { + const + {tileCoords, pixelCoords} = this._tileCoordsFromEvent(e), + key = this._tileCoordsToKey(tileCoords); + let tile = this._tiles[key]; + if (!tile) { + return; + } + tile = tile.el; + if (!tile) { + return; + } + const ids = this.getTracksIdsForPoint(tile, pixelCoords); + this.highlightTracks(ids); + } + } +); + +L.TracksCollection = L.ProtobufTileLines.extend({}); +\ No newline at end of file diff --git a/src/lib/leaflet.layer.tracks-collection/style.css b/src/lib/leaflet.layer.tracks-collection/style.css @@ -0,0 +1,3 @@ +.tile-tracks-active { + cursor: default; +} +\ No newline at end of file diff --git a/src/lib/leaflet.layer.tracks-collection/track_tile_proto.js b/src/lib/leaflet.layer.tracks-collection/track_tile_proto.js @@ -0,0 +1,51 @@ +'use strict'; // code generated by pbf v3.0.5 + +// TrackLines ======================================== + +var TrackLines = exports.TrackLines = {}; + +TrackLines.read = function (pbf, end) { + return pbf.readFields(TrackLines._readField, {trackId: 0, filter: 0, segmentsLengths: [], coordinates: null}, end); +}; +TrackLines._readField = function (tag, obj, pbf) { + if (tag === 1) obj.trackId = pbf.readVarint(); + else if (tag === 2) obj.filter = pbf.readVarint(); + else if (tag === 3) pbf.readPackedVarint(obj.segmentsLengths, true); + else if (tag === 4) obj.coordinates = pbf.readBytes(); +}; + +// TrackRaster ======================================== + +var TrackRaster = exports.TrackRaster = {}; + +TrackRaster.read = function (pbf, end) { + return pbf.readFields(TrackRaster._readField, {filter: 0, raster: null}, end); +}; +TrackRaster._readField = function (tag, obj, pbf) { + if (tag === 1) obj.filter = pbf.readVarint(); + else if (tag === 2) obj.raster = pbf.readBytes(); +}; + +// Tile ======================================== + +var Tile = exports.Tile = {}; + +Tile.read = function (pbf, end) { + return pbf.readFields(Tile._readField, {rasters: [], tracks: []}, end); +}; +Tile._readField = function (tag, obj, pbf) { + if (tag === 1) obj.rasters.push(TrackRaster.read(pbf, pbf.readVarint() + pbf.pos)); + else if (tag === 2) obj.tracks.push(TrackLines.read(pbf, pbf.readVarint() + pbf.pos)); +}; + +// TileRawLines ======================================== + +var TileRawLines = exports.TileRawLines = {}; + +TileRawLines.read = function (pbf, end) { + return pbf.readFields(TileRawLines._readField, {rasters: [], tracks: []}, end); +}; +TileRawLines._readField = function (tag, obj, pbf) { + if (tag === 1) obj.rasters.push(TrackRaster.read(pbf, pbf.readVarint() + pbf.pos)); + else if (tag === 2) obj.tracks.push(pbf.readBytes()); +}; diff --git a/src/lib/leaflet.layer.tracks-collection/update_proto.js b/src/lib/leaflet.layer.tracks-collection/update_proto.js @@ -0,0 +1,2 @@ +#!/bin/bash +../../../node_modules/pbf/bin/pbf ~/projects/tracks-catalog2/scripts/lib/track_tile.proto --no-write > track_tile_proto.js