diff --git a/build_src.js b/build_src.js index 425eb6120..347bb922b 100644 --- a/build_src.js +++ b/build_src.js @@ -27,7 +27,10 @@ module.exports = function buildSrc() { input: './modules/id.js', plugins: [ includePaths( { - paths: ['node_modules/d3/node_modules'] // npm2 or windows + paths: ['node_modules/d3/node_modules'], // npm2 or windows + include: { + 'martinez-polygon-clipping': 'node_modules/martinez-polygon-clipping/dist/martinez.umd.js' + } }), nodeResolve({ module: true, diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 01ac65148..d5a5e2fc9 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -1,17 +1,21 @@ import _find from 'lodash-es/find'; +import _isEqual from 'lodash-es/isEqual'; import _forEach from 'lodash-es/forEach'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { request as d3_request } from 'd3-request'; -import bboxClip from '@turf/bbox-clip'; +import turf_bboxClip from '@turf/bbox-clip'; +import stringify from 'fast-json-stable-stringify'; +import martinez from 'martinez-polygon-clipping'; + import Protobuf from 'pbf'; import vt from '@mapbox/vector-tile'; import { utilHashcode, utilRebind, utilTiler } from '../util'; -var tiler = utilTiler().tileSize(512); +var tiler = utilTiler().tileSize(512).margin(1); var dispatch = d3_dispatch('loadedData'); var _vtCache; @@ -21,7 +25,7 @@ function abortRequest(i) { } -function vtToGeoJSON(data, tile) { +function vtToGeoJSON(data, tile, mergeCache) { var vectorTile = new vt.VectorTile(new Protobuf(data.response)); var layers = Object.keys(vectorTile.layers); if (!Array.isArray(layers)) { layers = [layers]; } @@ -32,16 +36,58 @@ function vtToGeoJSON(data, tile) { if (layer) { for (var i = 0; i < layer.length; i++) { var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]); + var geometry = feature.geometry; if (layers.length > 1) { feature.properties.vt_layer = layerID; } - // clip to tile bounds - feature = bboxClip(feature, tile.extent.rectangle()); + + // Treat all Polygons as MultiPolygons + if (geometry.type === 'Polygon') { + geometry.type = 'MultiPolygon'; + geometry.coordinates = [geometry.coordinates]; + } + + // Clip to tile bounds + if (geometry.type === 'MultiPolygon') { + var isClipped = false; + var featureClip = turf_bboxClip(feature, tile.extent.rectangle()); + if (!_isEqual(feature.geometry, featureClip.geometry)) { + // feature = featureClip; + isClipped = true; + } + if (!feature.geometry.coordinates.length) continue; // not actually on this tile + if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile + } // force some unique id generation - feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); - feature.__propertyhash__ = utilHashcode(JSON.stringify(feature.properties || {})); + var featurehash = utilHashcode(stringify(feature)); + var propertyhash = utilHashcode(stringify(feature.properties || {})); + feature.__featurehash__ = featurehash; + feature.__propertyhash__ = propertyhash; features.push(feature); + + // Clipped Polygons at same zoom with identical properties can get merged + if (isClipped && geometry.type === 'MultiPolygon') { + var merged = mergeCache[propertyhash]; + if (merged && merged.length) { + var other = merged[0]; + var coords = martinez.union( + feature.geometry.coordinates, other.geometry.coordinates + ); + + if (!coords || !coords.length) { + continue; // something failed in martinez union + } + + merged.push(feature); + for (var j = 0; j < merged.length; j++) { // all these features get... + merged[j].geometry.coordinates = coords; // same coords + merged[j].__featurehash__ = featurehash; // same hash, so deduplication works + } + } else { + mergeCache[propertyhash] = [feature]; + } + } } } }); @@ -64,19 +110,19 @@ function loadTile(source, tile) { return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length]; }); - source.inflight[tile.id] = d3_request(url) .responseType('arraybuffer') .get(function(err, data) { - source.loaded[tile.id] = {}; + source.loaded[tile.id] = []; delete source.inflight[tile.id]; if (err || !data) return; - source.loaded[tile.id] = { - data: data, - features: vtToGeoJSON(data, tile) - }; + var z = tile.xyz[2]; + if (!source.canMerge[z]) { + source.canMerge[z] = {}; // initialize mergeCache + } + source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]); dispatch.call('loadedData'); }); } @@ -106,7 +152,7 @@ export default { addSource: function(sourceID, template) { - _vtCache[sourceID] = { template: template, inflight: {}, loaded: {} }; + _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} }; return _vtCache[sourceID]; }, @@ -120,13 +166,14 @@ export default { var results = []; for (var i = 0; i < tiles.length; i++) { - var loaded = source.loaded[tiles[i].id]; - if (!loaded || !loaded.features) continue; + var features = source.loaded[tiles[i].id]; + if (!features || !features.length) continue; - for (var j = 0; j < loaded.features.length; j++) { - var feature = loaded.features[j]; - if (seen[feature.__featurehash__]) continue; - seen[feature.__featurehash__] = true; + for (var j = 0; j < features.length; j++) { + var feature = features[j]; + var hash = feature.__featurehash__; + if (seen[hash]) continue; + seen[hash] = true; results.push(feature); } } diff --git a/modules/svg/data.js b/modules/svg/data.js index 38e0c15e0..581da8fc7 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -16,6 +16,7 @@ import { select as d3_select } from 'd3-selection'; +import stringify from 'fast-json-stable-stringify'; import toGeoJSON from '@mapbox/togeojson'; import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; @@ -133,7 +134,7 @@ export function svgData(projection, context, dispatch) { // ensure that each single Feature object has a unique ID function ensureFeatureID(feature) { if (!feature) return; - feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); + feature.__featurehash__ = utilHashcode(stringify(feature)); return feature; } diff --git a/package.json b/package.json index d87e1818d..a037d7b76 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,10 @@ "@mapbox/vector-tile": "^1.3.1", "@turf/bbox-clip": "^6.0.0", "diacritics": "1.3.0", + "fast-json-stable-stringify": "2.0.0", "lodash-es": "4.17.10", "marked": "0.5.0", + "martinez-polygon-clipping": "0.5.0", "node-diff3": "1.0.0", "osm-auth": "1.0.2", "pannellum": "2.4.1",