diff --git a/package.json b/package.json index ef70259..20448a1 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,11 @@ }, "homepage": "https://github.com/SpatialServer/Leaflet.MapboxVectorTile", "dependencies": { - "vector-tile": "~0.1.2", "pbf": "0.0.2", "point-geometry": "0.0.0", - "request": "~2.44.0" + "rbush": "^1.4.0", + "request": "~2.44.0", + "vector-tile": "~0.1.2" }, "devDependencies": { "browserify": "~5.9.1", diff --git a/src/MVTFeature.js b/src/MVTFeature.js index 16d741b..9f23b37 100755 --- a/src/MVTFeature.js +++ b/src/MVTFeature.js @@ -87,7 +87,7 @@ function ajaxCallback(self, response) { } self._setStyle(self.mvtLayer.style); - redrawTiles(self); + this.redraw(); } MVTFeature.prototype._setStyle = function(styleFn) { @@ -156,8 +156,12 @@ MVTFeature.prototype.draw = function(canvasID) { }; MVTFeature.prototype.getPathsForTile = function(canvasID) { - //Get the info from the parts list - return this.tiles[canvasID].paths; + var tile = this.tiles[canvasID]; + if (tile) { + return tile.paths; + } else { + return []; + } }; MVTFeature.prototype.addTileFeature = function(vtf, ctx) { @@ -195,20 +199,15 @@ MVTFeature.prototype.clearTileFeatures = function(zoom) { /** * Redraws all of the tiles associated with a feature. Useful for * style change and toggling. - * - * @param self */ -function redrawTiles(self) { +MVTFeature.prototype.redraw = function() { //Redraw the whole tile, not just this vtf - var tiles = self.tiles; - var mvtLayer = self.mvtLayer; - - for (var id in tiles) { + for (var id in this.tiles) { var tileZoom = parseInt(id.split(':')[0]); - var mapZoom = self.map.getZoom(); + var mapZoom = this.map.getZoom(); if (tileZoom === mapZoom) { //Redraw the tile - mvtLayer.redrawTile(id); + this.mvtLayer.redrawTile(id); } } } @@ -224,7 +223,7 @@ MVTFeature.prototype.toggle = function() { MVTFeature.prototype.select = function() { this.selected = true; this.mvtSource.featureSelected(this); - redrawTiles(this); + this.redraw(); var linkedFeature = this.linkedFeature(); if (linkedFeature && linkedFeature.staticLabel && !linkedFeature.staticLabel.selected) { linkedFeature.staticLabel.select(); @@ -234,7 +233,7 @@ MVTFeature.prototype.select = function() { MVTFeature.prototype.deselect = function() { this.selected = false; this.mvtSource.featureDeselected(this); - redrawTiles(this); + this.redraw(); var linkedFeature = this.linkedFeature(); if (linkedFeature && linkedFeature.staticLabel && linkedFeature.staticLabel.selected) { linkedFeature.staticLabel.deselect(); diff --git a/src/MVTLayer.js b/src/MVTLayer.js index 9723ca3..b76c2c6 100755 --- a/src/MVTLayer.js +++ b/src/MVTLayer.js @@ -4,6 +4,7 @@ /** Forked from https://gist.github.com/DGuidi/1716010 **/ var MVTFeature = require('./MVTFeature'); var Util = require('./MVTUtil'); +var rbush = require('rbush'); module.exports = L.TileLayer.Canvas.extend({ @@ -103,9 +104,11 @@ module.exports = L.TileLayer.Canvas.extend({ }, _initializeFeaturesHash: function(ctx){ - this._canvasIDToFeatures[ctx.id] = {}; - this._canvasIDToFeatures[ctx.id].features = []; - this._canvasIDToFeatures[ctx.id].canvas = ctx.canvas; + this._canvasIDToFeatures[ctx.id] = { + features: [], + canvas: ctx.canvas, + index: rbush(9) + }; }, _draw: function(ctx) { @@ -154,8 +157,6 @@ module.exports = L.TileLayer.Canvas.extend({ //See if we can pluck the child tile from this PBF tile layer based on the master layer's tile id. layerCtx.canvas = self._tiles[tilePoint.x + ":" + tilePoint.y]; - - //Initialize this tile's feature storage hash, if it hasn't already been created. Used for when filters are updated, and features are cleared to prepare for a fresh redraw. if (!this._canvasIDToFeatures[layerCtx.id]) { this._initializeFeaturesHash(layerCtx); @@ -165,6 +166,7 @@ module.exports = L.TileLayer.Canvas.extend({ } var features = vtl.parsedFeatures; + var toIndex = []; for (var i = 0, len = features.length; i < len; i++) { var vtf = features[i]; //vector tile feature vtf.layer = vtl; @@ -187,6 +189,12 @@ module.exports = L.TileLayer.Canvas.extend({ var uniqueID = self.options.getIDForLayerFeature(vtf) || i; var mvtFeature = self.features[uniqueID]; + /** + * Index the feature by bounding box into rbush. + */ + var box = bbox(vtf, layerCtx.tileSize, uniqueID); + toIndex.push(box); + /** * Use layerOrdering function to apply a zIndex property to each vtf. This is defined in * TileLayer.MVTSource.js. Used below to sort features.npm @@ -212,9 +220,10 @@ module.exports = L.TileLayer.Canvas.extend({ } //Associate & Save this feature with this tile for later - if(layerCtx && layerCtx.id) self._canvasIDToFeatures[layerCtx.id]['features'].push(mvtFeature); + self._canvasIDToFeatures[layerCtx.id].features.push(mvtFeature); } + self._canvasIDToFeatures[layerCtx.id].index.load(toIndex); /** * Apply sorting (zIndex) on feature if there is a function defined in the options object @@ -287,25 +296,22 @@ module.exports = L.TileLayer.Canvas.extend({ this.setLowestCount(count); }, - //This is the old way. It works, but is slow for mouseover events. Fine for click events. - handleClickEvent: function(evt, cb) { - //Click happened on the GroupLayer (Manager) and passed it here - var tileID = evt.tileID.split(":").slice(1, 3).join(":"); - var zoom = evt.tileID.split(":")[0]; - var canvas = this._tiles[tileID]; - if(!canvas) (cb(evt)); //break out - var x = evt.layerPoint.x - canvas._leaflet_pos.x; - var y = evt.layerPoint.y - canvas._leaflet_pos.y; + featureAt: function(tileID, tilePixelPoint) { + if (!this._canvasIDToFeatures[tileID]) return null; // break out + + var zoom = tileID.split(":")[0]; + var x = tilePixelPoint.x; + var y = tilePixelPoint.y; - var tilePoint = {x: x, y: y}; - var features = this._canvasIDToFeatures[evt.tileID].features; + var index = this._canvasIDToFeatures[tileID].index; var minDistance = Number.POSITIVE_INFINITY; var nearest = null; var j, paths, distance; - for (var i = 0; i < features.length; i++) { - var feature = features[i]; + var matches = index.search([x, y, x, y]); + for (var i = 0; i < matches.length; i++) { + var feature = this.features[matches[i].id]; switch (feature.type) { case 1: //Point - currently rendered as circular paths. Intersect with that. @@ -319,7 +325,7 @@ module.exports = L.TileLayer.Canvas.extend({ radius = feature.style.radius; } - paths = feature.getPathsForTile(evt.tileID); + paths = feature.getPathsForTile(tileID); for (j = 0; j < paths.length; j++) { //Builds a circle of radius feature.style.radius (assuming circular point symbology). if(in_circle(paths[j][0].x, paths[j][0].y, radius, x, y)){ @@ -330,10 +336,10 @@ module.exports = L.TileLayer.Canvas.extend({ break; case 2: //LineString - paths = feature.getPathsForTile(evt.tileID); + paths = feature.getPathsForTile(tileID); for (j = 0; j < paths.length; j++) { if (feature.style) { - var distance = this._getDistanceFromLine(tilePoint, paths[j]); + var distance = this._getDistanceFromLine(tilePixelPoint, paths[j]); var thickness = (feature.selected && feature.style.selected ? feature.style.selected.size : feature.style.size); if (distance < thickness / 2 + this.options.lineClickTolerance && distance < minDistance) { nearest = feature; @@ -344,9 +350,9 @@ module.exports = L.TileLayer.Canvas.extend({ break; case 3: //Polygon - paths = feature.getPathsForTile(evt.tileID); + paths = feature.getPathsForTile(tileID); for (j = 0; j < paths.length; j++) { - if (this._isPointInPoly(tilePoint, paths[j])) { + if (this._isPointInPoly(tilePixelPoint, paths[j])) { nearest = feature; minDistance = 0; // point is inside the polygon, so distance is zero } @@ -356,11 +362,7 @@ module.exports = L.TileLayer.Canvas.extend({ if (minDistance == 0) break; } - if (nearest && nearest.toggleEnabled) { - nearest.toggle(); - } - evt.feature = nearest; - cb(evt); + return nearest; }, clearTile: function(id) { @@ -377,8 +379,10 @@ module.exports = L.TileLayer.Canvas.extend({ context.clearRect(0, 0, canvas.width, canvas.height); }, - clearTileFeatureHash: function(canvasID){ - this._canvasIDToFeatures[canvasID] = { features: []}; //Get rid of all saved features + clearTileFeatureHash: function(canvasID) { + // Get rid of all saved features + this._canvasIDToFeatures[canvasID].features = []; + this._canvasIDToFeatures[canvasID].index = rbush(9); }, clearLayerFeatureHash: function(){ @@ -420,14 +424,6 @@ module.exports = L.TileLayer.Canvas.extend({ } }, - _resetCanvasIDToFeatures: function(canvasID, canvas) { - - this._canvasIDToFeatures[canvasID] = {}; - this._canvasIDToFeatures[canvasID].features = []; - this._canvasIDToFeatures[canvasID].canvas = canvas; - - }, - linkedLayer: function() { if(this.mvtSource.layerLink) { var linkName = this.mvtSource.layerLink(this.name); @@ -444,6 +440,29 @@ module.exports = L.TileLayer.Canvas.extend({ }); +function bbox(vtf, tileSize, id) { + var divisor = vtf.extent / tileSize; + + var minX = Number.POSITIVE_INFINITY; + var maxX = Number.NEGATIVE_INFINITY; + var minY = Number.POSITIVE_INFINITY; + var maxY = Number.NEGATIVE_INFINITY; + vtf.coordinates.forEach(function(coordinates) { + coordinates.forEach(function(coordinate) { + var x = coordinate.x / divisor; + var y = coordinate.y / divisor; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + }); + }); + + var box = [minX, minY, maxX, maxY]; + box.id = id; + return box; +} + function removeLabels(self) { var features = self.featuresWithLabels; @@ -494,4 +513,4 @@ function waitFor(testFx, onReady, timeOutMillis) { } } }, 50); //< repeat check every 50ms -}; \ No newline at end of file +}; diff --git a/src/MVTSource.js b/src/MVTSource.js index e405a82..50229dc 100644 --- a/src/MVTSource.js +++ b/src/MVTSource.js @@ -12,7 +12,7 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ url: "", //URL TO Vector Tile Source, getIDForLayerFeature: function() {}, tileSize: 256, - visibleLayers: [] + visibleLayers: null }, layers: {}, //Keep a list of the layers contained in the PBFs processedTiles: {}, //Keep a list of tiles that have been processed already @@ -83,7 +83,7 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ */ this.zIndex = options.zIndex; - if (typeof options.style === 'function') { + if (typeof options.style === 'function' || typeof options.style === 'object') { this.style = options.style; } @@ -212,9 +212,14 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ var arrayBuffer = new Uint8Array(xhr.response); var buf = new Protobuf(arrayBuffer); var vt = new VectorTile(buf); - //Check the current map layer zoom. If fast zooming is occurring, then short circuit tiles that are for a different zoom level than we're currently on. - if(self.map && self.map.getZoom() != ctx.zoom) { - console.log("Fetched tile for zoom level " + ctx.zoom + ". Map is at zoom level " + self._map.getZoom()); + // Check the attachment status of the layer. + if (!self.map) { + console.log("Fetched tile for removed map."); + return; + } + // Check the current map layer zoom. If fast zooming is occurring, then short circuit tiles that are for a different zoom level than we're currently on. + if (self.map.getZoom() != ctx.zoom) { + console.log("Fetched tile for zoom level " + ctx.zoom + ". Map is at zoom level " + self.map.getZoom()); return; } self.checkVectorTileLayers(parseVT(vt), ctx); @@ -247,19 +252,23 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ var self = this; //Check if there are specified visible layers - if(self.options.visibleLayers && self.options.visibleLayers.length > 0){ - //only let thru the layers listed in the visibleLayers array - for(var i=0; i < self.options.visibleLayers.length; i++){ - var layerName = self.options.visibleLayers[i]; - if(vt.layers[layerName]){ - //Proceed with parsing - self.prepareMVTLayers(vt.layers[layerName], layerName, ctx, parsed); - } + var visibleLayers = self.options.visibleLayers; + if (!visibleLayers) { + visibleLayers = Object.keys(vt.layers); + } + + var layerMapping = visibleLayers; + if (Array.isArray(visibleLayers)) { + layerMapping = {}; + for (var i=0; i < visibleLayers.length; i++) { + layerMapping[visibleLayers[i]] = visibleLayers[i]; } - }else{ - //Parse all vt.layers - for (var key in vt.layers) { - self.prepareMVTLayers(vt.layers[key], key, ctx, parsed); + } + + for (var key in layerMapping) { + var lyr = vt.layers[layerMapping[key]]; + if (lyr) { + self.prepareMVTLayers(lyr, key, ctx, parsed); } } }, @@ -291,11 +300,16 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ getIDForLayerFeature = Util.getIDForLayerFeature; } + var style = self.style; + if (typeof style === 'object') { + style = style[key]; + } + var options = { getIDForLayerFeature: getIDForLayerFeature, filter: self.options.filter, layerOrdering: self.options.layerOrdering, - style: self.style, + style: style, name: key, asynch: true }; @@ -317,8 +331,13 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ hideLayer: function(id) { if (this.layers[id]) { this._map.removeLayer(this.layers[id]); - if(this.options.visibleLayers.indexOf("id") > -1){ - this.visibleLayers.splice(this.options.visibleLayers.indexOf("id"), 1); + var visibleLayers = this.options.visibleLayers; + if (visibleLayers) { + if (Array.isArray(visibleLayers) && visibleLayers.indexOf(id) > -1) { + visibleLayers.splice(visibleLayers.indexOf(id), 1); + } else { + delete visibleLayers[id]; + } } } }, @@ -326,8 +345,13 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ showLayer: function(id) { if (this.layers[id]) { this._map.addLayer(this.layers[id]); - if(this.options.visibleLayers.indexOf("id") == -1){ - this.visibleLayers.push(id); + var visibleLayers = this.options.visibleLayers; + if (visibleLayers) { + if (Array.isArray(visibleLayers)) { + visibleLayers.push(id); + } else { + visibleLayers[id] = id; + } } } //Make sure manager layer is always in front @@ -343,11 +367,14 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ }, addChildLayers: function(map) { - var self = this; - if(self.options.visibleLayers.length > 0){ - //only let thru the layers listed in the visibleLayers array - for(var i=0; i < self.options.visibleLayers.length; i++){ - var layerName = self.options.visibleLayers[i]; + var visibleLayers = this.visibleLayers; + if (visibleLayers) { + //only let thru the layers listed in the visibleLayers array or object + if (!Array.isArray(visibleLayers)) { + visibleLayers = Object.keys(visibleLayers); + } + for(var i=0; i < visibleLayers.length; i++){ + var layerName = visibleLayers[i]; var layer = this.layers[layerName]; if(layer){ //Proceed with parsing @@ -370,6 +397,28 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ this._eventHandlers[eventType] = callback; }, + featureAtLatLng: function(latlng) { + return this.featureAtContainerPoint(this.map.latLngToContainerPoint(latlng)); + }, + + featureAtContainerPoint: function(containerPoint) { + return this._featureAt(containerPoint, this.layers); + }, + + _featureAt: function(containerPoint, layers) { + var tilePoint = this._getTilePoint(containerPoint); + + // TODO: Z-ordering? Clickable? + for (var key in layers) { + var layer = layers[key]; + var feature = layer.featureAt(tilePoint.tileID, tilePoint); + if (feature) { + return feature; + } + } + return null; + }, + _onClick: function(evt) { //Here, pass the event on to the child MVTLayer and have it do the hit test and handle the result. var self = this; @@ -377,33 +426,28 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ var clickableLayers = self.options.clickableLayers; var layers = self.layers; - evt.tileID = getTileURL(evt.latlng.lat, evt.latlng.lng, this.map.getZoom()); - // We must have an array of clickable layers, otherwise, we just pass // the event to the public onClick callback in options. - - if(!clickableLayers){ - clickableLayers = Object.keys(self.layers); - } - - if (clickableLayers && clickableLayers.length > 0) { + if (clickableLayers) { + layers = {}; for (var i = 0, len = clickableLayers.length; i < len; i++) { var key = clickableLayers[i]; - var layer = layers[key]; + var layer = self.layers[key]; if (layer) { - layer.handleClickEvent(evt, function(evt) { - if (typeof onClick === 'function') { - onClick(evt); - } - }); + layers[key] = layer; } } - } else { - if (typeof onClick === 'function') { - onClick(evt); - } } + var feature = this._featureAt(evt.layerPoint, layers); + if (feature && feature.toggleEnabled) { + feature.toggle(); + } + + evt.feature = feature; + if (typeof onClick === 'function') { + onClick(evt); + } }, setFilter: function(filterFunction, layerName) { @@ -485,6 +529,17 @@ module.exports = L.TileLayer.MVTSource = L.TileLayer.Canvas.extend({ onTilesLoaded(this); } self._triggerOnTilesLoadedEvent = true; //reset - if redraw() is called with the optinal 'false' parameter to temporarily disable the onTilesLoaded event from firing. This resets it back to true after a single time of firing as 'false'. + }, + + _getTilePoint: function(containerPoint) { + var tileSize = this.options.tileSize; + var globalPoint = this.map.containerPointToLayerPoint(containerPoint) + .add(this.map.getPixelOrigin()); + + var tileIndexPoint = globalPoint.divideBy(tileSize).floor(); + var tilePoint = globalPoint.subtract(tileIndexPoint.multiplyBy(tileSize)); + tilePoint.tileID = "" + this.map.getZoom() + ":" + tileIndexPoint.x + ":" + tileIndexPoint.y; + return tilePoint; } }); @@ -496,12 +551,6 @@ if (typeof(Number.prototype.toRad) === "undefined") { } } -function getTileURL(lat, lon, zoom) { - var xtile = parseInt(Math.floor( (lon + 180) / 360 * (1<