@maplibre/geojson-vt 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).geojsonvt=e()}(this,(function(){"use strict";function t(n,o,i,s){let r=s;const l=o+(i-o>>1);let u,a=i-o;const f=n[o],c=n[o+1],m=n[i],d=n[i+1];for(let t=o+3;t<i;t+=3){const o=e(n[t],n[t+1],f,c,m,d);if(o>r)u=t,r=o;else if(o===r){const e=Math.abs(t-l);e<a&&(u=t,a=e)}}r>s&&(u-o>3&&t(n,o,u,s),n[u+2]=r,i-u>3&&t(n,u,i,s))}function e(t,e,n,o,i,s){let r=i-n,l=s-o;if(0!==r||0!==l){const u=((t-n)*r+(e-o)*l)/(r*r+l*l);u>1?(n=i,o=s):u>0&&(n+=r*u,o+=l*u)}return r=t-n,l=e-o,r*r+l*l}function n(t,e,n,i){const s={id:null==t?null:t,type:e,geometry:n,tags:i,minX:1/0,minY:1/0,maxX:-1/0,maxY:-1/0};if("Point"===e||"MultiPoint"===e||"LineString"===e)o(s,n);else if("Polygon"===e)o(s,n[0]);else if("MultiLineString"===e)for(const t of n)o(s,t);else if("MultiPolygon"===e)for(const t of n)o(s,t[0]);return s}function o(t,e){for(let n=0;n<e.length;n+=3)t.minX=Math.min(t.minX,e[n]),t.minY=Math.min(t.minY,e[n+1]),t.maxX=Math.max(t.maxX,e[n]),t.maxY=Math.max(t.maxY,e[n+1])}function i(t,e){const n=[];if("FeatureCollection"===t.type)for(let o=0;o<t.features.length;o++)s(n,t.features[o],e,o);else"Feature"===t.type?s(n,t,e):s(n,{geometry:t},e);return n}function s(t,e,o,i){if(!e.geometry)return;const a=e.geometry.coordinates;if(a&&0===a.length)return;const f=e.geometry.type,c=Math.pow(o.tolerance/((1<<o.maxZoom)*o.extent),2);let m=[],d=e.id;if(o.promoteId?d=e.properties[o.promoteId]:o.generateId&&(d=i||0),"Point"===f)r(a,m);else if("MultiPoint"===f)for(const t of a)r(t,m);else if("LineString"===f)l(a,m,c,!1);else if("MultiLineString"===f){if(o.lineMetrics){for(const o of a)m=[],l(o,m,c,!1),t.push(n(d,"LineString",m,e.properties));return}u(a,m,c,!1)}else if("Polygon"===f)u(a,m,c,!0);else{if("MultiPolygon"!==f){if("GeometryCollection"===f){for(const n of e.geometry.geometries)s(t,{id:d,geometry:n,properties:e.properties},o,i);return}throw new Error("Input data is not a valid GeoJSON object.")}for(const t of a){const e=[];u(t,e,c,!0),m.push(e)}}t.push(n(d,f,m,e.properties))}function r(t,e){e.push(a(t[0]),f(t[1]),0)}function l(e,n,o,i){let s,r,l=0;for(let t=0;t<e.length;t++){const o=a(e[t][0]),u=f(e[t][1]);n.push(o,u,0),t>0&&(l+=i?(s*u-o*r)/2:Math.sqrt(Math.pow(o-s,2)+Math.pow(u-r,2))),s=o,r=u}const u=n.length-3;n[2]=1,o>0&&t(n,0,u,o),n[u+2]=1,n.size=Math.abs(l),n.start=0,n.end=n.size}function u(t,e,n,o){for(let i=0;i<t.length;i++){const s=[];l(t[i],s,n,o),e.push(s)}}function a(t){return t/360+.5}function f(t){const e=Math.sin(t*Math.PI/180),n=.5-.25*Math.log((1+e)/(1-e))/Math.PI;return n<0?0:n>1?1:n}function c(t,e,o,i,s,r,l,u){if(i/=e,r>=(o/=e)&&l<i)return t;if(l<o||r>=i)return null;const a=[];for(const e of t){const t=e.geometry;let r=e.type;const l=0===s?e.minX:e.minY,f=0===s?e.maxX:e.maxY;if(l>=o&&f<i){a.push(e);continue}if(f<o||l>=i)continue;let c=[];if("Point"===r||"MultiPoint"===r)m(t,c,o,i,s);else if("LineString"===r)d(t,c,o,i,s,!1,u.lineMetrics);else if("MultiLineString"===r)p(t,c,o,i,s,!1);else if("Polygon"===r)p(t,c,o,i,s,!0);else if("MultiPolygon"===r)for(const e of t){const t=[];p(e,t,o,i,s,!0),t.length&&c.push(t)}if(c.length){if(u.lineMetrics&&"LineString"===r){for(const t of c)a.push(n(e.id,r,t,e.tags));continue}"LineString"!==r&&"MultiLineString"!==r||(1===c.length?(r="LineString",c=c[0]):r="MultiLineString"),"Point"!==r&&"MultiPoint"!==r||(r=3===c.length?"Point":"MultiPoint"),a.push(n(e.id,r,c,e.tags))}}return a.length?a:null}function m(t,e,n,o,i){for(let s=0;s<t.length;s+=3){const r=t[s+i];r>=n&&r<=o&&g(e,t[s],t[s+1],t[s+2])}}function d(t,e,n,o,i,s,r){let l=h(t);const u=0===i?x:M;let a,f,c=t.start;for(let m=0;m<t.length-3;m+=3){const d=t[m],p=t[m+1],x=t[m+2],M=t[m+3],y=t[m+4],P=0===i?d:p,S=0===i?M:y;let z=!1;r&&(a=Math.sqrt(Math.pow(d-M,2)+Math.pow(p-y,2))),P<n?S>n&&(f=u(l,d,p,M,y,n),r&&(l.start=c+a*f)):P>o?S<o&&(f=u(l,d,p,M,y,o),r&&(l.start=c+a*f)):g(l,d,p,x),S<n&&P>=n&&(f=u(l,d,p,M,y,n),z=!0),S>o&&P<=o&&(f=u(l,d,p,M,y,o),z=!0),!s&&z&&(r&&(l.end=c+a*f),e.push(l),l=h(t)),r&&(c+=a)}let m=t.length-3;const d=t[m],p=t[m+1],y=t[m+2],P=0===i?d:p;P>=n&&P<=o&&g(l,d,p,y),m=l.length-3,s&&m>=3&&(l[m]!==l[0]||l[m+1]!==l[1])&&g(l,l[0],l[1],l[2]),l.length&&e.push(l)}function h(t){const e=[];return e.size=t.size,e.start=t.start,e.end=t.end,e}function p(t,e,n,o,i,s){for(const r of t)d(r,e,n,o,i,s,!1)}function g(t,e,n,o){t.push(e,n,o)}function x(t,e,n,o,i,s){const r=(s-e)/(o-e);return g(t,s,n+(i-n)*r,1),r}function M(t,e,n,o,i,s){const r=(s-n)/(i-n);return g(t,e+(o-e)*r,s,1),r}function y(t,e){const n=e.buffer/e.extent;let o=t;const i=c(t,1,-1-n,n,0,-1,2,e),s=c(t,1,1-n,2+n,0,-1,2,e);return(i||s)&&(o=c(t,1,-n,1+n,0,-1,2,e)||[],i&&(o=P(i,1).concat(o)),s&&(o=o.concat(P(s,-1)))),o}function P(t,e){const o=[];for(let i=0;i<t.length;i++){const s=t[i],r=s.type;let l;if("Point"===r||"MultiPoint"===r||"LineString"===r)l=S(s.geometry,e);else if("MultiLineString"===r||"Polygon"===r){l=[];for(const t of s.geometry)l.push(S(t,e))}else if("MultiPolygon"===r){l=[];for(const t of s.geometry){const n=[];for(const o of t)n.push(S(o,e));l.push(n)}}o.push(n(s.id,r,l,s.tags))}return o}function S(t,e){const n=[];n.size=t.size,void 0!==t.start&&(n.start=t.start,n.end=t.end);for(let o=0;o<t.length;o+=3)n.push(t[o]+e,t[o+1],t[o+2]);return n}function z(t,e){if(t.transformed)return t;const n=1<<t.z,o=t.x,i=t.y;for(const s of t.features){const t=s.geometry,r=s.type;if(s.geometry=[],1===r)for(let r=0;r<t.length;r+=2)s.geometry.push(Y(t[r],t[r+1],e,n,o,i));else for(let r=0;r<t.length;r++){const l=[];for(let s=0;s<t[r].length;s+=2)l.push(Y(t[r][s],t[r][s+1],e,n,o,i));s.geometry.push(l)}}return t.transformed=!0,t}function Y(t,e,n,o,i,s){return[Math.round(n*(t*o-i)),Math.round(n*(e*o-s))]}function b(t,e,n,o,i){const s=e===i.maxZoom?0:i.tolerance/((1<<e)*i.extent),r={features:[],numPoints:0,numSimplified:0,numFeatures:t.length,source:null,x:n,y:o,z:e,transformed:!1,minX:2,minY:1,maxX:-1,maxY:0};for(const e of t)v(r,e,s,i);return r}function v(t,e,n,o){const i=e.geometry,s=e.type,r=[];if(t.minX=Math.min(t.minX,e.minX),t.minY=Math.min(t.minY,e.minY),t.maxX=Math.max(t.maxX,e.maxX),t.maxY=Math.max(t.maxY,e.maxY),"Point"===s||"MultiPoint"===s)for(let e=0;e<i.length;e+=3)r.push(i[e],i[e+1]),t.numPoints++,t.numSimplified++;else if("LineString"===s)w(r,i,t,n,!1,!1);else if("MultiLineString"===s||"Polygon"===s)for(let e=0;e<i.length;e++)w(r,i[e],t,n,"Polygon"===s,0===e);else if("MultiPolygon"===s)for(let e=0;e<i.length;e++){const o=i[e];for(let e=0;e<o.length;e++)w(r,o[e],t,n,!0,0===e)}if(r.length){let n=e.tags||null;if("LineString"===s&&o.lineMetrics){n={};for(const t in e.tags)n[t]=e.tags[t];n.mapbox_clip_start=i.start/i.size,n.mapbox_clip_end=i.end/i.size}const l={geometry:r,type:"Polygon"===s||"MultiPolygon"===s?3:"LineString"===s||"MultiLineString"===s?2:1,tags:n};null!==e.id&&(l.id=e.id),t.features.push(l)}}function w(t,e,n,o,i,s){const r=o*o;if(o>0&&e.size<(i?r:o))return void(n.numPoints+=e.length/3);const l=[];for(let t=0;t<e.length;t+=3)(0===o||e[t+2]>r)&&(n.numSimplified++,l.push(e[t],e[t+1])),n.numPoints++;i&&function(t,e){let n=0;for(let e=0,o=t.length,i=o-2;e<o;i=e,e+=2)n+=(t[e]-t[i])*(t[e+1]+t[i+1]);if(n>0===e)for(let e=0,n=t.length;e<n/2;e+=2){const o=t[e],i=t[e+1];t[e]=t[n-2-e],t[e+1]=t[n-1-e],t[n-2-e]=o,t[n-1-e]=i}}(l,s),t.push(l)}function X(t,e,n){const o=function(t){if(!t)return{};const e={};return e.removeAll=t.removeAll,e.remove=new Set(t.remove||[]),e.add=new Map(t.add?.map((t=>[t.id,t]))),e.update=new Map(t.update?.map((t=>[t.id,t]))),e}(e);let s=[];if(o.removeAll&&(s=t,t=[]),o.remove.size||o.add.size){const e=[];for(const n of t){const{id:t}=n;(o.remove.has(t)||o.add.has(t))&&e.push(n)}if(e.length){s.push(...e);const n=new Set(e.map((t=>t.id)));t=t.filter((t=>!n.has(t.id)))}if(o.add.size){let e=i({type:"FeatureCollection",features:Array.from(o.add.values())},n);e=y(e,n),s.push(...e),t.push(...e)}}if(o.update.size)for(const[e,i]of o.update){const o=t.findIndex((t=>t.id===e));if(-1===o)continue;const r=t[o],l=L(r,i,n);l&&(s.push(r,l),t[o]=l)}return{affected:s,source:t}}function L(t,e,n){const o=!!e.newGeometry,s=e.removeAllProperties||e.removeProperties?.length>0||e.addOrUpdateProperties?.length>0;if(!o&&!s)return null;if(o){let o=i({type:"FeatureCollection",features:[{type:"Feature",id:t.id,geometry:e.newGeometry,properties:s?I(t.tags,e):t.tags}]},n);return o=y(o,n),o[0]}if(s){const n={...t};return n.tags=I(n.tags,e),n}return null}function I(t,e){if(e.removeAllProperties)return{};const n={...t||{}};if(e.removeProperties)for(const t of e.removeProperties)delete n[t];if(e.addOrUpdateProperties)for(const{key:t,value:o}of e.addOrUpdateProperties)n[t]=o;return n}const Z={maxZoom:14,indexMaxZoom:5,indexMaxPoints:1e5,tolerance:3,extent:4096,buffer:64,lineMetrics:!1,promoteId:null,generateId:!1,updateable:!1,debug:0};class E{constructor(t,e){const n=(e=this.options=function(t,e){for(const n in e)t[n]=e[n];return t}(Object.create(Z),e)).debug;if(n&&console.time("preprocess data"),e.maxZoom<0||e.maxZoom>24)throw new Error("maxZoom should be in the 0-24 range");if(e.promoteId&&e.generateId)throw new Error("promoteId and generateId cannot be used together.");let o=i(t,e);this.tiles={},this.tileCoords=[],n&&(console.timeEnd("preprocess data"),console.log("index: maxZoom: %d, maxPoints: %d",e.indexMaxZoom,e.indexMaxPoints),console.time("generate tiles"),this.stats={},this.total=0),o=y(o,e),o.length&&this.splitTile(o,0,0,0),e.updateable&&(this.source=o),n&&(o.length&&console.log("features: %d, points: %d",this.tiles[0].numFeatures,this.tiles[0].numPoints),console.timeEnd("generate tiles"),console.log("tiles generated:",this.total,JSON.stringify(this.stats)))}splitTile(t,e,n,o,i,s,r){const l=[t,e,n,o],u=this.options,a=u.debug;for(;l.length;){o=l.pop(),n=l.pop(),e=l.pop(),t=l.pop();const f=1<<e,m=C(e,n,o);let d=this.tiles[m];if(!d&&(a>1&&console.time("creation"),d=this.tiles[m]=b(t,e,n,o,u),this.tileCoords.push({z:e,x:n,y:o,id:m}),a)){a>1&&(console.log("tile z%d-%d-%d (features: %d, points: %d, simplified: %d)",e,n,o,d.numFeatures,d.numPoints,d.numSimplified),console.timeEnd("creation"));const t=`z${e}`;this.stats[t]=(this.stats[t]||0)+1,this.total++}if(d.source=t,null==i){if(e===u.indexMaxZoom||d.numPoints<=u.indexMaxPoints)continue}else{if(e===u.maxZoom||e===i)continue;if(null!=i){const t=i-e;if(n!==s>>t||o!==r>>t)continue}}if(d.source=null,0===t.length)continue;a>1&&console.time("clipping");const h=.5*u.buffer/u.extent,p=.5-h,g=.5+h,x=1+h;let M=null,y=null,P=null,S=null;const z=c(t,f,n-h,n+g,0,d.minX,d.maxX,u),Y=c(t,f,n+p,n+x,0,d.minX,d.maxX,u);z&&(M=c(z,f,o-h,o+g,1,d.minY,d.maxY,u),y=c(z,f,o+p,o+x,1,d.minY,d.maxY,u)),Y&&(P=c(Y,f,o-h,o+g,1,d.minY,d.maxY,u),S=c(Y,f,o+p,o+x,1,d.minY,d.maxY,u)),a>1&&console.timeEnd("clipping"),l.push(M||[],e+1,2*n,2*o),l.push(y||[],e+1,2*n,2*o+1),l.push(P||[],e+1,2*n+1,2*o),l.push(S||[],e+1,2*n+1,2*o+1)}}getTile(t,e,n){t=+t,e=+e,n=+n;const o=this.options,{extent:i,debug:s}=o;if(t<0||t>24)return null;const r=1<<t,l=C(t,e=e+r&r-1,n);if(this.tiles[l])return z(this.tiles[l],i);s>1&&console.log("drilling down to z%d-%d-%d",t,e,n);let u,a=t,f=e,c=n;for(;!u&&a>0;)a--,f>>=1,c>>=1,u=this.tiles[C(a,f,c)];return u&&u.source?(s>1&&(console.log("found parent tile z%d-%d-%d",a,f,c),console.time("drilling down")),this.splitTile(u.source,a,f,c,t,e,n),s>1&&console.timeEnd("drilling down"),this.tiles[l]?z(this.tiles[l],i):null):null}invalidateTiles(t){const e=this.options,{debug:n}=e;let o=1/0,i=-1/0,s=1/0,r=-1/0;for(const e of t)o=Math.min(o,e.minX),i=Math.max(i,e.maxX),s=Math.min(s,e.minY),r=Math.max(r,e.maxY);const l=e.buffer/e.extent,u=new Set;for(const e in this.tiles){const a=this.tiles[e],f=1<<a.z,c=(a.x-l)/f,m=(a.x+1+l)/f,d=(a.y-l)/f,h=(a.y+1+l)/f;if(i<c||o>=m||r<d||s>=h)continue;let p=!1;for(const e of t)if(e.maxX>=c&&e.minX<m&&e.maxY>=d&&e.minY<h){p=!0;break}if(p){if(n){n>1&&console.log("invalidate tile z%d-%d-%d (features: %d, points: %d, simplified: %d)",a.z,a.x,a.y,a.numFeatures,a.numPoints,a.numSimplified);const t=`z${a.z}`;this.stats[t]=(this.stats[t]||0)-1,this.total--}delete this.tiles[e],u.add(e)}}u.size&&(this.tileCoords=this.tileCoords.filter((t=>!u.has(t.id))))}updateData(t){const e=this.options,n=e.debug;if(!e.updateable)throw new Error("to update tile geojson `updateable` option must be set to true");const{affected:o,source:i}=X(this.source,t,e);if(!o.length)return;this.source=i,n>1&&(console.log("invalidating tiles"),console.time("invalidating")),this.invalidateTiles(o),n>1&&console.timeEnd("invalidating");const[s,r,l]=[0,0,0],u=b(this.source,s,r,l,this.options);u.source=this.source;const a=C(s,r,l);if(this.tiles[a]=u,this.tileCoords.push({z:s,x:r,y:l,id:a}),n){const t=`z${s}`;this.stats[t]=(this.stats[t]||0)+1,this.total++}}}function C(t,e,n){return 32*((1<<t)*n+e)+t}return function(t,e){return new E(t,e)}}));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@maplibre/geojson-vt",
3
+ "version": "5.0.0",
4
+ "description": "Slice GeoJSON data into vector tiles efficiently",
5
+ "type": "module",
6
+ "exports": "./src/index.js",
7
+ "sideEffects": false,
8
+ "keywords": [
9
+ "spatial",
10
+ "geojson",
11
+ "tiles",
12
+ "geometry"
13
+ ],
14
+ "module": "src/index.js",
15
+ "main": "dist/geojson-vt-dev.js",
16
+ "jsdelivr": "dist/geojson-vt.js",
17
+ "unpkg": "dist/geojson-vt.js",
18
+ "devDependencies": {
19
+ "@rollup/plugin-terser": "^0.4.4",
20
+ "benchmark": "^2.1.4",
21
+ "eslint": "^9.38.0",
22
+ "eslint-config-mourner": "^4.1.0",
23
+ "rollup": "^4.18.0"
24
+ },
25
+ "license": "ISC",
26
+ "scripts": {
27
+ "pretest": "eslint src/*.js test/*.js debug/viz.js",
28
+ "test": "node --test",
29
+ "build": "rollup -c",
30
+ "watch": "rollup -cw",
31
+ "bench": "node bench/benchmark.js",
32
+ "prepublishOnly": "npm run test && npm run build"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/maplibre/geojson-vt"
41
+ }
42
+ }
package/src/clip.js ADDED
@@ -0,0 +1,200 @@
1
+
2
+ import createFeature from './feature.js';
3
+
4
+ /* clip features between two vertical or horizontal axis-parallel lines:
5
+ * | |
6
+ * ___|___ | /
7
+ * / | \____|____/
8
+ * | |
9
+ *
10
+ * k1 and k2 are the line coordinates
11
+ * axis: 0 for x, 1 for y
12
+ * minAll and maxAll: minimum and maximum coordinate value for all features
13
+ */
14
+ export default function clip(features, scale, k1, k2, axis, minAll, maxAll, options) {
15
+ k1 /= scale;
16
+ k2 /= scale;
17
+
18
+ if (minAll >= k1 && maxAll < k2) return features; // trivial accept
19
+ else if (maxAll < k1 || minAll >= k2) return null; // trivial reject
20
+
21
+ const clipped = [];
22
+
23
+ for (const feature of features) {
24
+ const geometry = feature.geometry;
25
+ let type = feature.type;
26
+
27
+ const min = axis === 0 ? feature.minX : feature.minY;
28
+ const max = axis === 0 ? feature.maxX : feature.maxY;
29
+
30
+ if (min >= k1 && max < k2) { // trivial accept
31
+ clipped.push(feature);
32
+ continue;
33
+ } else if (max < k1 || min >= k2) { // trivial reject
34
+ continue;
35
+ }
36
+
37
+ let newGeometry = [];
38
+
39
+ if (type === 'Point' || type === 'MultiPoint') {
40
+ clipPoints(geometry, newGeometry, k1, k2, axis);
41
+
42
+ } else if (type === 'LineString') {
43
+ clipLine(geometry, newGeometry, k1, k2, axis, false, options.lineMetrics);
44
+
45
+ } else if (type === 'MultiLineString') {
46
+ clipLines(geometry, newGeometry, k1, k2, axis, false);
47
+
48
+ } else if (type === 'Polygon') {
49
+ clipLines(geometry, newGeometry, k1, k2, axis, true);
50
+
51
+ } else if (type === 'MultiPolygon') {
52
+ for (const polygon of geometry) {
53
+ const newPolygon = [];
54
+ clipLines(polygon, newPolygon, k1, k2, axis, true);
55
+ if (newPolygon.length) {
56
+ newGeometry.push(newPolygon);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (newGeometry.length) {
62
+ if (options.lineMetrics && type === 'LineString') {
63
+ for (const line of newGeometry) {
64
+ clipped.push(createFeature(feature.id, type, line, feature.tags));
65
+ }
66
+ continue;
67
+ }
68
+
69
+ if (type === 'LineString' || type === 'MultiLineString') {
70
+ if (newGeometry.length === 1) {
71
+ type = 'LineString';
72
+ newGeometry = newGeometry[0];
73
+ } else {
74
+ type = 'MultiLineString';
75
+ }
76
+ }
77
+ if (type === 'Point' || type === 'MultiPoint') {
78
+ type = newGeometry.length === 3 ? 'Point' : 'MultiPoint';
79
+ }
80
+
81
+ clipped.push(createFeature(feature.id, type, newGeometry, feature.tags));
82
+ }
83
+ }
84
+
85
+ return clipped.length ? clipped : null;
86
+ }
87
+
88
+ function clipPoints(geom, newGeom, k1, k2, axis) {
89
+ for (let i = 0; i < geom.length; i += 3) {
90
+ const a = geom[i + axis];
91
+
92
+ if (a >= k1 && a <= k2) {
93
+ addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]);
94
+ }
95
+ }
96
+ }
97
+
98
+ function clipLine(geom, newGeom, k1, k2, axis, isPolygon, trackMetrics) {
99
+
100
+ let slice = newSlice(geom);
101
+ const intersect = axis === 0 ? intersectX : intersectY;
102
+ let len = geom.start;
103
+ let segLen, t;
104
+
105
+ for (let i = 0; i < geom.length - 3; i += 3) {
106
+ const ax = geom[i];
107
+ const ay = geom[i + 1];
108
+ const az = geom[i + 2];
109
+ const bx = geom[i + 3];
110
+ const by = geom[i + 4];
111
+ const a = axis === 0 ? ax : ay;
112
+ const b = axis === 0 ? bx : by;
113
+ let exited = false;
114
+
115
+ if (trackMetrics) segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
116
+
117
+ if (a < k1) {
118
+ // ---|--> | (line enters the clip region from the left)
119
+ if (b > k1) {
120
+ t = intersect(slice, ax, ay, bx, by, k1);
121
+ if (trackMetrics) slice.start = len + segLen * t;
122
+ }
123
+ } else if (a > k2) {
124
+ // | <--|--- (line enters the clip region from the right)
125
+ if (b < k2) {
126
+ t = intersect(slice, ax, ay, bx, by, k2);
127
+ if (trackMetrics) slice.start = len + segLen * t;
128
+ }
129
+ } else {
130
+ addPoint(slice, ax, ay, az);
131
+ }
132
+ if (b < k1 && a >= k1) {
133
+ // <--|--- | or <--|-----|--- (line exits the clip region on the left)
134
+ t = intersect(slice, ax, ay, bx, by, k1);
135
+ exited = true;
136
+ }
137
+ if (b > k2 && a <= k2) {
138
+ // | ---|--> or ---|-----|--> (line exits the clip region on the right)
139
+ t = intersect(slice, ax, ay, bx, by, k2);
140
+ exited = true;
141
+ }
142
+
143
+ if (!isPolygon && exited) {
144
+ if (trackMetrics) slice.end = len + segLen * t;
145
+ newGeom.push(slice);
146
+ slice = newSlice(geom);
147
+ }
148
+
149
+ if (trackMetrics) len += segLen;
150
+ }
151
+
152
+ // add the last point
153
+ let last = geom.length - 3;
154
+ const ax = geom[last];
155
+ const ay = geom[last + 1];
156
+ const az = geom[last + 2];
157
+ const a = axis === 0 ? ax : ay;
158
+ if (a >= k1 && a <= k2) addPoint(slice, ax, ay, az);
159
+
160
+ // close the polygon if its endpoints are not the same after clipping
161
+ last = slice.length - 3;
162
+ if (isPolygon && last >= 3 && (slice[last] !== slice[0] || slice[last + 1] !== slice[1])) {
163
+ addPoint(slice, slice[0], slice[1], slice[2]);
164
+ }
165
+
166
+ // add the final slice
167
+ if (slice.length) {
168
+ newGeom.push(slice);
169
+ }
170
+ }
171
+
172
+ function newSlice(line) {
173
+ const slice = [];
174
+ slice.size = line.size;
175
+ slice.start = line.start;
176
+ slice.end = line.end;
177
+ return slice;
178
+ }
179
+
180
+ function clipLines(geom, newGeom, k1, k2, axis, isPolygon) {
181
+ for (const line of geom) {
182
+ clipLine(line, newGeom, k1, k2, axis, isPolygon, false);
183
+ }
184
+ }
185
+
186
+ function addPoint(out, x, y, z) {
187
+ out.push(x, y, z);
188
+ }
189
+
190
+ function intersectX(out, ax, ay, bx, by, x) {
191
+ const t = (x - ax) / (bx - ax);
192
+ addPoint(out, x, ay + (by - ay) * t, 1);
193
+ return t;
194
+ }
195
+
196
+ function intersectY(out, ax, ay, bx, by, y) {
197
+ const t = (y - ay) / (by - ay);
198
+ addPoint(out, ax + (bx - ax) * t, y, 1);
199
+ return t;
200
+ }
package/src/convert.js ADDED
@@ -0,0 +1,139 @@
1
+
2
+ import simplify from './simplify.js';
3
+ import createFeature from './feature.js';
4
+
5
+ // converts GeoJSON feature into an intermediate projected JSON vector format with simplification data
6
+
7
+ export default function convert(data, options) {
8
+ const features = [];
9
+ if (data.type === 'FeatureCollection') {
10
+ for (let i = 0; i < data.features.length; i++) {
11
+ convertFeature(features, data.features[i], options, i);
12
+ }
13
+
14
+ } else if (data.type === 'Feature') {
15
+ convertFeature(features, data, options);
16
+
17
+ } else {
18
+ // single geometry or a geometry collection
19
+ convertFeature(features, {geometry: data}, options);
20
+ }
21
+
22
+ return features;
23
+ }
24
+
25
+ function convertFeature(features, geojson, options, index) {
26
+ if (!geojson.geometry) return;
27
+
28
+ const coords = geojson.geometry.coordinates;
29
+ if (coords && coords.length === 0) return;
30
+
31
+ const type = geojson.geometry.type;
32
+ const tolerance = Math.pow(options.tolerance / ((1 << options.maxZoom) * options.extent), 2);
33
+ let geometry = [];
34
+ let id = geojson.id;
35
+ if (options.promoteId) {
36
+ id = geojson.properties[options.promoteId];
37
+ } else if (options.generateId) {
38
+ id = index || 0;
39
+ }
40
+ if (type === 'Point') {
41
+ convertPoint(coords, geometry);
42
+
43
+ } else if (type === 'MultiPoint') {
44
+ for (const p of coords) {
45
+ convertPoint(p, geometry);
46
+ }
47
+
48
+ } else if (type === 'LineString') {
49
+ convertLine(coords, geometry, tolerance, false);
50
+
51
+ } else if (type === 'MultiLineString') {
52
+ if (options.lineMetrics) {
53
+ // explode into linestrings to be able to track metrics
54
+ for (const line of coords) {
55
+ geometry = [];
56
+ convertLine(line, geometry, tolerance, false);
57
+ features.push(createFeature(id, 'LineString', geometry, geojson.properties));
58
+ }
59
+ return;
60
+ }
61
+ convertLines(coords, geometry, tolerance, false);
62
+
63
+ } else if (type === 'Polygon') {
64
+ convertLines(coords, geometry, tolerance, true);
65
+
66
+ } else if (type === 'MultiPolygon') {
67
+ for (const polygon of coords) {
68
+ const newPolygon = [];
69
+ convertLines(polygon, newPolygon, tolerance, true);
70
+ geometry.push(newPolygon);
71
+ }
72
+ } else if (type === 'GeometryCollection') {
73
+ for (const singleGeometry of geojson.geometry.geometries) {
74
+ convertFeature(features, {
75
+ id,
76
+ geometry: singleGeometry,
77
+ properties: geojson.properties
78
+ }, options, index);
79
+ }
80
+ return;
81
+ } else {
82
+ throw new Error('Input data is not a valid GeoJSON object.');
83
+ }
84
+
85
+ features.push(createFeature(id, type, geometry, geojson.properties));
86
+ }
87
+
88
+ function convertPoint(coords, out) {
89
+ out.push(projectX(coords[0]), projectY(coords[1]), 0);
90
+ }
91
+
92
+ function convertLine(ring, out, tolerance, isPolygon) {
93
+ let x0, y0;
94
+ let size = 0;
95
+
96
+ for (let j = 0; j < ring.length; j++) {
97
+ const x = projectX(ring[j][0]);
98
+ const y = projectY(ring[j][1]);
99
+
100
+ out.push(x, y, 0);
101
+
102
+ if (j > 0) {
103
+ if (isPolygon) {
104
+ size += (x0 * y - x * y0) / 2; // area
105
+ } else {
106
+ size += Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2)); // length
107
+ }
108
+ }
109
+ x0 = x;
110
+ y0 = y;
111
+ }
112
+
113
+ const last = out.length - 3;
114
+ out[2] = 1;
115
+ if (tolerance > 0) simplify(out, 0, last, tolerance);
116
+ out[last + 2] = 1;
117
+
118
+ out.size = Math.abs(size);
119
+ out.start = 0;
120
+ out.end = out.size;
121
+ }
122
+
123
+ function convertLines(rings, out, tolerance, isPolygon) {
124
+ for (let i = 0; i < rings.length; i++) {
125
+ const geom = [];
126
+ convertLine(rings[i], geom, tolerance, isPolygon);
127
+ out.push(geom);
128
+ }
129
+ }
130
+
131
+ function projectX(x) {
132
+ return x / 360 + 0.5;
133
+ }
134
+
135
+ function projectY(y) {
136
+ const sin = Math.sin(y * Math.PI / 180);
137
+ const y2 = 0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
138
+ return y2 < 0 ? 0 : y2 > 1 ? 1 : y2;
139
+ }
@@ -0,0 +1,180 @@
1
+ import convert from './convert.js'; // GeoJSON conversion and preprocessing
2
+ import wrap from './wrap.js'; // date line processing
3
+
4
+ // This file provides a set of helper functions for managing "diffs" (changes)
5
+ // to GeoJSON data structures. These diffs describe additions, removals,
6
+ // and updates of features in a GeoJSON source in an efficient way.
7
+
8
+ // GeoJSON Source Diff:
9
+ // {
10
+ // removeAll: true, // If true, clear all existing features
11
+ // remove: [featureId, ...], // Array of feature IDs to remove
12
+ // add: [feature, ...], // Array of GeoJSON features to add
13
+ // update: [GeoJSON Feature Diff, ...] // Array of per-feature updates
14
+ // }
15
+
16
+ // GeoJSON Feature Diff:
17
+ // {
18
+ // id: featureId, // ID of the feature being updated
19
+ // newGeometry: GeoJSON.Geometry, // Optional new geometry
20
+ // removeAllProperties: true, // Remove all properties if true
21
+ // removeProperties: [key, ...], // Specific properties to delete
22
+ // addOrUpdateProperties: [ // Properties to add or update
23
+ // { key: "name", value: "New name" }
24
+ // ]
25
+ // }
26
+
27
+ /* eslint @stylistic/comma-spacing: 0, no-shadow: 0 */
28
+
29
+ // applies a diff to the geojsonvt source simplified features array
30
+ // returns an object with the affected features and new source array for invalidation
31
+ export function applySourceDiff(source, dataDiff, options) {
32
+
33
+ // convert diff to sets/maps for o(1) lookups
34
+ const diff = diffToHashed(dataDiff);
35
+
36
+ // collection for features that will be affected by this update
37
+ let affected = [];
38
+
39
+ // full removal - clear everything before applying diff
40
+ if (diff.removeAll) {
41
+ affected = source;
42
+ source = [];
43
+ }
44
+
45
+ // remove/add features and collect affected ones
46
+ if (diff.remove.size || diff.add.size) {
47
+ const removeFeatures = [];
48
+
49
+ // collect source features to be removed
50
+ for (const feature of source) {
51
+ const {id} = feature;
52
+
53
+ // explicit feature removal
54
+ if (diff.remove.has(id)) {
55
+ removeFeatures.push(feature);
56
+ // feature with duplicate id being added
57
+ } else if (diff.add.has(id)) {
58
+ removeFeatures.push(feature);
59
+ }
60
+ }
61
+
62
+ // collect affected and remove from source
63
+ if (removeFeatures.length) {
64
+ affected.push(...removeFeatures);
65
+
66
+ const removeIds = new Set(removeFeatures.map(f => f.id));
67
+ source = source.filter(f => !removeIds.has(f.id));
68
+ }
69
+
70
+ // convert and add new features
71
+ if (diff.add.size) {
72
+ // projects and adds simplification info
73
+ let addFeatures = convert({type: 'FeatureCollection', features: Array.from(diff.add.values())}, options);
74
+
75
+ // wraps features (ie extreme west and extreme east)
76
+ addFeatures = wrap(addFeatures, options);
77
+
78
+ affected.push(...addFeatures);
79
+ source.push(...addFeatures);
80
+ }
81
+ }
82
+
83
+ if (diff.update.size) {
84
+ for (const [id, update] of diff.update) {
85
+ const featureIndex = source.findIndex(f => f.id === id);
86
+ if (featureIndex === -1) continue;
87
+
88
+ const feature = source[featureIndex];
89
+
90
+ // get updated geojsonvt simplified feature
91
+ const updatedFeature = getUpdatedFeature(feature, update, options);
92
+ if (!updatedFeature) continue;
93
+
94
+ // track both features for invalidation
95
+ affected.push(feature, updatedFeature);
96
+
97
+ // replace old feature with updated feature
98
+ source[featureIndex] = updatedFeature;
99
+ }
100
+ }
101
+
102
+ return {affected, source};
103
+ }
104
+
105
+ // return an updated geojsonvt simplified feature
106
+ function getUpdatedFeature(vtFeature, update, options) {
107
+ const changeGeometry = !!update.newGeometry;
108
+
109
+ const changeProps =
110
+ update.removeAllProperties ||
111
+ update.removeProperties?.length > 0 ||
112
+ update.addOrUpdateProperties?.length > 0;
113
+
114
+ // nothing to do
115
+ if (!changeGeometry && !changeProps) return null;
116
+
117
+ // if geometry changed, need to create new geojson feature and convert to simplified format
118
+ if (changeGeometry) {
119
+ const geojsonFeature = {
120
+ type: 'Feature',
121
+ id: vtFeature.id,
122
+ geometry: update.newGeometry,
123
+ properties: changeProps ? applyPropertyUpdates(vtFeature.tags, update) : vtFeature.tags
124
+ };
125
+
126
+ // projects and adds simplification info
127
+ let features = convert({type: 'FeatureCollection', features: [geojsonFeature]}, options);
128
+
129
+ // wraps features (ie extreme west and extreme east)
130
+ features = wrap(features, options);
131
+
132
+ return features[0];
133
+ }
134
+
135
+ // only properties changed - update tags directly
136
+ if (changeProps) {
137
+ const feature = {...vtFeature};
138
+ feature.tags = applyPropertyUpdates(feature.tags, update);
139
+ return feature;
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ // helper to apply property updates from a diff update object to a properties object
146
+ function applyPropertyUpdates(tags, update) {
147
+ if (update.removeAllProperties) {
148
+ return {};
149
+ }
150
+
151
+ const properties = {...tags || {}};
152
+
153
+ if (update.removeProperties) {
154
+ for (const key of update.removeProperties) {
155
+ delete properties[key];
156
+ }
157
+ }
158
+
159
+ if (update.addOrUpdateProperties) {
160
+ for (const {key, value} of update.addOrUpdateProperties) {
161
+ properties[key] = value;
162
+ }
163
+ }
164
+
165
+ return properties;
166
+ }
167
+
168
+ // Convert a GeoJSON Source Diff to an idempotent hashed representation using Sets and Maps
169
+ export function diffToHashed(diff) {
170
+ if (!diff) return {};
171
+
172
+ const hashed = {};
173
+
174
+ hashed.removeAll = diff.removeAll;
175
+ hashed.remove = new Set(diff.remove || []);
176
+ hashed.add = new Map(diff.add?.map(feature => [feature.id, feature]));
177
+ hashed.update = new Map(diff.update?.map(update => [update.id, update]));
178
+
179
+ return hashed;
180
+ }
package/src/feature.js ADDED
@@ -0,0 +1,43 @@
1
+
2
+ export default function createFeature(id, type, geom, tags) {
3
+ const feature = {
4
+ id: id == null ? null : id,
5
+ type,
6
+ geometry: geom,
7
+ tags,
8
+ minX: Infinity,
9
+ minY: Infinity,
10
+ maxX: -Infinity,
11
+ maxY: -Infinity
12
+ };
13
+
14
+ if (type === 'Point' || type === 'MultiPoint' || type === 'LineString') {
15
+ calcLineBBox(feature, geom);
16
+
17
+ } else if (type === 'Polygon') {
18
+ // the outer ring (ie [0]) contains all inner rings
19
+ calcLineBBox(feature, geom[0]);
20
+
21
+ } else if (type === 'MultiLineString') {
22
+ for (const line of geom) {
23
+ calcLineBBox(feature, line);
24
+ }
25
+
26
+ } else if (type === 'MultiPolygon') {
27
+ for (const polygon of geom) {
28
+ // the outer ring (ie [0]) contains all inner rings
29
+ calcLineBBox(feature, polygon[0]);
30
+ }
31
+ }
32
+
33
+ return feature;
34
+ }
35
+
36
+ function calcLineBBox(feature, geom) {
37
+ for (let i = 0; i < geom.length; i += 3) {
38
+ feature.minX = Math.min(feature.minX, geom[i]);
39
+ feature.minY = Math.min(feature.minY, geom[i + 1]);
40
+ feature.maxX = Math.max(feature.maxX, geom[i]);
41
+ feature.maxY = Math.max(feature.maxY, geom[i + 1]);
42
+ }
43
+ }