geogrid 1.1.1 → 1.2.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.
- package/dist/index.min.js +1 -1
- package/package.json +8 -2
- package/src/grid/square_sph.js +98 -0
- package/src/helpers/sphere.js +41 -0
- package/src/helpers/stitchmerge.js +144 -0
- package/src/helpers/unstitch.js +31 -0
- package/src/index.js +10 -0
- package/src/operator/linestogrid.js +10 -3
- package/src/operator/pointstogrid.js +15 -2
- package/src/operator/polygonstogrid.js +39 -26
- package/src/operator/linestogrid_save.js +0 -124
- package/src/operator/polygonstogrid_save.js +0 -144
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geogrid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Regular and irregular geoJSON grids ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -41,20 +41,26 @@
|
|
|
41
41
|
"@turf/boolean-intersects": "^7.2.0",
|
|
42
42
|
"@turf/boolean-point-in-polygon": "^7.1.0",
|
|
43
43
|
"@turf/boolean-within": "^7.2.0",
|
|
44
|
+
"@turf/centroid": "^7.2.0",
|
|
44
45
|
"@turf/helpers": "^7.1.0",
|
|
45
46
|
"@turf/intersect": "^7.1.0",
|
|
46
47
|
"@turf/length": "^7.2.0",
|
|
47
48
|
"@turf/line-split": "^7.2.0",
|
|
49
|
+
"@turf/rewind": "^7.2.0",
|
|
48
50
|
"d3-array": "^3.2.4",
|
|
49
51
|
"d3-delaunay": "^6.0.4",
|
|
50
52
|
"d3-geo": "^3.1.1",
|
|
51
53
|
"d3-geo-projection": "^4.0.0",
|
|
54
|
+
"d3-geo-voronoi": "^2.1.0",
|
|
52
55
|
"docs": "^0.3.2-canary.0",
|
|
53
56
|
"documentation": "^14.0.3",
|
|
54
57
|
"geojson2h3": "^1.2.0",
|
|
55
58
|
"h3-js": "^4.1.0",
|
|
59
|
+
"martinez-polygon-clipping": "^0.7.4",
|
|
56
60
|
"rbush": "^4.0.1",
|
|
57
|
-
"rollup": "^4.10.0"
|
|
61
|
+
"rollup": "^4.10.0",
|
|
62
|
+
"s2-geometry": "^1.2.10",
|
|
63
|
+
"s2js": "^1.43.6"
|
|
58
64
|
},
|
|
59
65
|
"devDependencies": {
|
|
60
66
|
"@jood/jsdoc-theme": "^0.1.14",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createSteppedArray } from "../helpers/createSteppedArray.js";
|
|
2
|
+
import booleanIntersects from "@turf/boolean-intersects";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @function square_sph
|
|
6
|
+
* @summary Compute a square grid in WGS84 degrees.
|
|
7
|
+
* @description Builds a square grid in latitude/longitude degrees,
|
|
8
|
+
* avoids coordinates exactly at ±180 or ±90 to prevent rewind issues.
|
|
9
|
+
* @param {object} options
|
|
10
|
+
* @param {number[]} [options.start=[-180, -90]] - Starting coordinates [lon, lat].
|
|
11
|
+
* @param {number} [options.width=360] - Width of the grid in degrees (longitude span).
|
|
12
|
+
* @param {number} [options.height=180] - Height of the grid in degrees (latitude span).
|
|
13
|
+
* @param {number} [options.step=1] - Step size in degrees.
|
|
14
|
+
* @param {GeoJSON.Feature|GeoJSON.FeatureCollection|GeoJSON.Geometry} [options.domain] -
|
|
15
|
+
* Optional GeoJSON object. Only cells that intersect this domain are kept.
|
|
16
|
+
* @returns {GeoJSON.FeatureCollection} A GeoJSON FeatureCollection of polygons.
|
|
17
|
+
*/
|
|
18
|
+
export function square_sph({
|
|
19
|
+
start = [-180, -90],
|
|
20
|
+
width = 360,
|
|
21
|
+
height = 180,
|
|
22
|
+
step = 1,
|
|
23
|
+
domain = null,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const LON_MIN = -180;
|
|
26
|
+
const LON_MAX = 180;
|
|
27
|
+
const LAT_MIN = -90;
|
|
28
|
+
const LAT_MAX = 90;
|
|
29
|
+
const EPS = 1e-2; // petit décalage pour éviter ±180 et ±90 exacts
|
|
30
|
+
|
|
31
|
+
const lonStart = Math.max(LON_MIN, start[0]);
|
|
32
|
+
const latStart = Math.max(LAT_MIN, start[1]);
|
|
33
|
+
const lonEnd = Math.min(LON_MAX, start[0] + width);
|
|
34
|
+
const latEnd = Math.min(LAT_MAX, start[1] + height);
|
|
35
|
+
|
|
36
|
+
let x = createSteppedArray(lonStart + step / 2, lonEnd - step / 2, step);
|
|
37
|
+
let y = createSteppedArray(
|
|
38
|
+
latStart + step / 2,
|
|
39
|
+
latEnd - step / 2,
|
|
40
|
+
step,
|
|
41
|
+
true
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const features = [];
|
|
45
|
+
let i = 0;
|
|
46
|
+
|
|
47
|
+
for (const lon of x) {
|
|
48
|
+
for (const lat of y) {
|
|
49
|
+
// coins initiaux
|
|
50
|
+
let lonWest = lon - step / 2;
|
|
51
|
+
let lonEast = lon + step / 2;
|
|
52
|
+
let latSouth = lat - step / 2;
|
|
53
|
+
let latNorth = lat + step / 2;
|
|
54
|
+
|
|
55
|
+
// remplacer ±180 et ±90 par des valeurs légèrement inférieures/supérieures
|
|
56
|
+
if (lonWest <= LON_MIN) lonWest = LON_MIN + EPS;
|
|
57
|
+
if (lonEast >= LON_MAX) lonEast = LON_MAX - EPS;
|
|
58
|
+
if (latSouth <= LAT_MIN) latSouth = LAT_MIN + EPS;
|
|
59
|
+
if (latNorth >= LAT_MAX) latNorth = LAT_MAX - EPS;
|
|
60
|
+
|
|
61
|
+
features.push({
|
|
62
|
+
type: "Feature",
|
|
63
|
+
geometry: {
|
|
64
|
+
type: "Polygon",
|
|
65
|
+
coordinates: [
|
|
66
|
+
[
|
|
67
|
+
[lonWest, latNorth],
|
|
68
|
+
[lonEast, latNorth],
|
|
69
|
+
[lonEast, latSouth],
|
|
70
|
+
[lonWest, latSouth],
|
|
71
|
+
[lonWest, latNorth],
|
|
72
|
+
],
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
properties: { index: i++ },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Domain
|
|
81
|
+
let filtered = features;
|
|
82
|
+
if (domain) {
|
|
83
|
+
filtered = features.filter((f) => {
|
|
84
|
+
try {
|
|
85
|
+
return booleanIntersects(f, domain);
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
type: "FeatureCollection",
|
|
94
|
+
grid: "square_sph",
|
|
95
|
+
geo: true,
|
|
96
|
+
features: filtered,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { polygon, featureCollection } from "@turf/helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Crée un polygone approximatif de la sphère projetée dans le plan SVG
|
|
5
|
+
* et le retourne dans une FeatureCollection
|
|
6
|
+
* @param {d3.GeoProjection} projection - La projection D3 utilisée pour le SVG
|
|
7
|
+
* @param {number} [stepDeg=5] - Pas en degrés pour approximer la sphère
|
|
8
|
+
* @returns {GeoJSON.FeatureCollection} - FeatureCollection contenant le polygone Turf de la sphère projetée
|
|
9
|
+
*/
|
|
10
|
+
export function sphere(projection, stepDeg = 5) {
|
|
11
|
+
const coords = [];
|
|
12
|
+
|
|
13
|
+
// contour supérieur (latitude 90°)
|
|
14
|
+
for (let lon = -180; lon <= 180; lon += stepDeg) {
|
|
15
|
+
const pt = projection([lon, 90]);
|
|
16
|
+
if (pt) coords.push(pt);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// contour droit (longitude 180°)
|
|
20
|
+
for (let lat = 90; lat >= -90; lat -= stepDeg) {
|
|
21
|
+
const pt = projection([180, lat]);
|
|
22
|
+
if (pt) coords.push(pt);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// contour inférieur (latitude -90°)
|
|
26
|
+
for (let lon = 180; lon >= -180; lon -= stepDeg) {
|
|
27
|
+
const pt = projection([lon, -90]);
|
|
28
|
+
if (pt) coords.push(pt);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// contour gauche (longitude -180°)
|
|
32
|
+
for (let lat = -90; lat <= 90; lat += stepDeg) {
|
|
33
|
+
const pt = projection([-180, lat]);
|
|
34
|
+
if (pt) coords.push(pt);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const poly = polygon([coords]);
|
|
38
|
+
|
|
39
|
+
// Retourner une FeatureCollection pour être compatible avec Turf intersect
|
|
40
|
+
return featureCollection([poly]);
|
|
41
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Takes a GeoJSON and processes each MultiPolygon feature that
|
|
3
|
+
* crosses the antimeridian, converting it into a continuous Polygon feature.
|
|
4
|
+
*/
|
|
5
|
+
export function stitchmerge(geojson) {
|
|
6
|
+
// Helper: adjust longitude relative to previous one to handle wrapping around ±180°
|
|
7
|
+
function adjustLonToPrev(lon, prevLon) {
|
|
8
|
+
if (prevLon === null || prevLon === undefined) return lon;
|
|
9
|
+
// Normalize angular difference to be within [-180, 180]
|
|
10
|
+
while (lon - prevLon > 180) lon -= 360;
|
|
11
|
+
while (prevLon - lon > 180) lon += 360;
|
|
12
|
+
return lon;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper: remove consecutive duplicate points (with a small tolerance)
|
|
16
|
+
function dedupeConsecutive(points) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (let i = 0; i < points.length; i++) {
|
|
19
|
+
const p = points[i];
|
|
20
|
+
const prev = out.length ? out[out.length - 1] : null;
|
|
21
|
+
if (
|
|
22
|
+
!prev ||
|
|
23
|
+
Math.abs(prev[0] - p[0]) > 1e-12 ||
|
|
24
|
+
Math.abs(prev[1] - p[1]) > 1e-12
|
|
25
|
+
) {
|
|
26
|
+
out.push(p);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Helper: ensure the ring is closed (first == last point)
|
|
33
|
+
function closeRing(ring) {
|
|
34
|
+
if (!ring.length) return ring;
|
|
35
|
+
const first = ring[0];
|
|
36
|
+
const last = ring[ring.length - 1];
|
|
37
|
+
if (
|
|
38
|
+
Math.abs(first[0] - last[0]) > 1e-12 ||
|
|
39
|
+
Math.abs(first[1] - last[1]) > 1e-12
|
|
40
|
+
) {
|
|
41
|
+
ring.push([first[0], first[1]]);
|
|
42
|
+
}
|
|
43
|
+
return ring;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Process one MultiPolygon geometry -> returns a merged Polygon geometry
|
|
47
|
+
function processMultiPolygonGeometry(multiCoords) {
|
|
48
|
+
// multiCoords: Array of polygons; each polygon is an array of rings.
|
|
49
|
+
// We treat the outer ring (index 0) of each polygon as a possible fragment
|
|
50
|
+
// of a single continuous outer ring that needs to be reconnected.
|
|
51
|
+
// Additional rings (holes) are collected and preserved as holes.
|
|
52
|
+
const exteriorRings = [];
|
|
53
|
+
const interiorRings = [];
|
|
54
|
+
|
|
55
|
+
for (let p = 0; p < multiCoords.length; p++) {
|
|
56
|
+
const polygon = multiCoords[p];
|
|
57
|
+
if (!polygon || !polygon.length) continue;
|
|
58
|
+
// polygon[0] is the exterior ring
|
|
59
|
+
exteriorRings.push(polygon[0].slice());
|
|
60
|
+
// if polygon has holes, collect them
|
|
61
|
+
for (let r = 1; r < polygon.length; r++) {
|
|
62
|
+
interiorRings.push(polygon[r].slice());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Unwrap all exterior rings so that longitudes are continuous
|
|
67
|
+
const unwrappedExteriors = [];
|
|
68
|
+
let prevLon = null;
|
|
69
|
+
for (let i = 0; i < exteriorRings.length; i++) {
|
|
70
|
+
const ring = exteriorRings[i];
|
|
71
|
+
const newRing = [];
|
|
72
|
+
for (let j = 0; j < ring.length; j++) {
|
|
73
|
+
let [lon, lat] = ring[j];
|
|
74
|
+
lon = adjustLonToPrev(lon, prevLon);
|
|
75
|
+
newRing.push([lon, lat]);
|
|
76
|
+
prevLon = lon;
|
|
77
|
+
}
|
|
78
|
+
const trimmed = dedupeConsecutive(newRing);
|
|
79
|
+
unwrappedExteriors.push(trimmed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Merge all unwrapped exterior rings into a single continuous ring
|
|
83
|
+
// We remove internal closures and only close at the end
|
|
84
|
+
let mergedExterior = [];
|
|
85
|
+
for (let i = 0; i < unwrappedExteriors.length; i++) {
|
|
86
|
+
const ring = unwrappedExteriors[i].slice();
|
|
87
|
+
// remove last point if it's equal to the first (closed ring)
|
|
88
|
+
if (ring.length > 1) {
|
|
89
|
+
const f = ring[0];
|
|
90
|
+
const l = ring[ring.length - 1];
|
|
91
|
+
if (Math.abs(f[0] - l[0]) < 1e-12 && Math.abs(f[1] - l[1]) < 1e-12) {
|
|
92
|
+
ring.pop();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
mergedExterior = mergedExterior.concat(ring);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Deduplicate consecutive identical points
|
|
99
|
+
mergedExterior = dedupeConsecutive(mergedExterior);
|
|
100
|
+
// Close the final merged ring
|
|
101
|
+
mergedExterior = closeRing(mergedExterior);
|
|
102
|
+
|
|
103
|
+
// Unwrap interior rings (holes) so they match the unwrapped longitude system
|
|
104
|
+
const unwrappedInteriors = interiorRings.map((ring) => {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (let i = 0; i < ring.length; i++) {
|
|
107
|
+
const anchorLon = mergedExterior.length ? mergedExterior[0][0] : null;
|
|
108
|
+
let lon = ring[i][0];
|
|
109
|
+
if (anchorLon !== null) {
|
|
110
|
+
while (lon - anchorLon > 180) lon -= 360;
|
|
111
|
+
while (anchorLon - lon > 180) lon += 360;
|
|
112
|
+
}
|
|
113
|
+
out.push([lon, ring[i][1]]);
|
|
114
|
+
}
|
|
115
|
+
return closeRing(dedupeConsecutive(out));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Return a single Polygon geometry: outer + holes
|
|
119
|
+
const coords = [mergedExterior].concat(unwrappedInteriors);
|
|
120
|
+
return { type: "Polygon", coordinates: coords };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MAIN: process all features
|
|
124
|
+
const out = {
|
|
125
|
+
...geojson,
|
|
126
|
+
features: (geojson.features || []).map((feature) => {
|
|
127
|
+
if (!feature || !feature.geometry) return feature;
|
|
128
|
+
const geom = feature.geometry;
|
|
129
|
+
if (geom.type === "MultiPolygon") {
|
|
130
|
+
try {
|
|
131
|
+
const newGeom = processMultiPolygonGeometry(geom.coordinates);
|
|
132
|
+
return { ...feature, geometry: newGeom };
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.warn("Error stitching a MultiPolygon:", e);
|
|
135
|
+
return feature; // fallback to original geometry
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Leave other geometries unchanged
|
|
139
|
+
return feature;
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { geoProject } from "d3-geo-projection";
|
|
2
|
+
import { rewind } from "geotoolbox";
|
|
3
|
+
import { geoEquirectangular } from "d3-geo";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @function unstitch
|
|
7
|
+
* @description After Matthieu Viry: https://observablehq.com/@mthh/unstitch
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function unstitch(a) {
|
|
11
|
+
a = JSON.parse(JSON.stringify(a));
|
|
12
|
+
a = geoProject(
|
|
13
|
+
a,
|
|
14
|
+
geoEquirectangular()
|
|
15
|
+
.scale(180 / Math.PI)
|
|
16
|
+
.translate([0, 0])
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
for (const f of a.features) {
|
|
20
|
+
if (f.geometry.type === "Polygon")
|
|
21
|
+
f.geometry.coordinates.forEach((ring) =>
|
|
22
|
+
ring.forEach((point) => (point[1] *= -1))
|
|
23
|
+
);
|
|
24
|
+
else if (f.geometry.type === "MultiPolygon")
|
|
25
|
+
f.geometry.coordinates.forEach((poly) =>
|
|
26
|
+
poly.forEach((ring) => ring.forEach((point) => (point[1] *= -1)))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return rewind(a, { simple: true });
|
|
31
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
// Planar grids
|
|
1
2
|
export { square } from "./grid/square.js";
|
|
2
3
|
export { triangle } from "./grid/triangle.js";
|
|
3
4
|
export { dot } from "./grid/dot.js";
|
|
4
5
|
export { random } from "./grid/random.js";
|
|
5
6
|
export { diamond } from "./grid/diamond.js";
|
|
6
7
|
export { hexbin } from "./grid/hexbin.js";
|
|
8
|
+
|
|
9
|
+
// Spherical grids
|
|
10
|
+
export { square_sph } from "./grid/square_sph.js";
|
|
7
11
|
export { h3 } from "./grid/h3.js";
|
|
12
|
+
|
|
13
|
+
// Operators
|
|
8
14
|
export { pointstogrid } from "./operator/pointstogrid.js";
|
|
9
15
|
export { polygonstogrid } from "./operator/polygonstogrid.js";
|
|
10
16
|
export { linestogrid } from "./operator/linestogrid.js";
|
|
17
|
+
|
|
18
|
+
// Helpers
|
|
11
19
|
export { project } from "./helpers/project.js";
|
|
20
|
+
export { unstitch } from "./helpers/unstitch.js";
|
|
21
|
+
export { sphere } from "./helpers/sphere.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { bbox } from "@turf/bbox";
|
|
2
2
|
import { lineSplit } from "@turf/line-split";
|
|
3
3
|
import { length } from "@turf/length";
|
|
4
|
+
import { stitchmerge } from "../helpers/stitchmerge.js";
|
|
5
|
+
import { unstitch } from "../helpers/unstitch.js";
|
|
4
6
|
import RBush from "rbush";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -18,16 +20,21 @@ import RBush from "rbush";
|
|
|
18
20
|
* @property {boolean} [values=false] - Include array of raw lines properties
|
|
19
21
|
*/
|
|
20
22
|
export function linestogrid(opts = {}) {
|
|
21
|
-
|
|
23
|
+
let {
|
|
22
24
|
grid,
|
|
23
25
|
lines,
|
|
24
|
-
grid_id = "index",
|
|
25
26
|
var: varField,
|
|
26
27
|
values: includeValues = false,
|
|
28
|
+
spherical = false,
|
|
27
29
|
} = opts;
|
|
28
30
|
|
|
29
31
|
const t0 = performance.now();
|
|
30
32
|
|
|
33
|
+
// Unstitch grids if needed
|
|
34
|
+
if (spherical) {
|
|
35
|
+
grid = unstitch(grid);
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
// --- Normalize varField to array ---
|
|
32
39
|
const varFields = varField
|
|
33
40
|
? Array.isArray(varField)
|
|
@@ -104,5 +111,5 @@ export function linestogrid(opts = {}) {
|
|
|
104
111
|
).toFixed(2)} ms`
|
|
105
112
|
);
|
|
106
113
|
|
|
107
|
-
return filteredGrid;
|
|
114
|
+
return spherical ? stitchmerge(filteredGrid) : filteredGrid;
|
|
108
115
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import RBush from "rbush";
|
|
2
2
|
import { bbox } from "@turf/bbox";
|
|
3
3
|
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
|
|
4
|
+
import { stitchmerge } from "../helpers/stitchmerge.js";
|
|
5
|
+
import { unstitch } from "../helpers/unstitch.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @function pointstogrid
|
|
@@ -11,12 +13,23 @@ import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
|
|
|
11
13
|
* @property {object} [grid] - GeoJSON grid (polygons)
|
|
12
14
|
* @property {string|Array} [var] - Field(s) for summing values
|
|
13
15
|
* @property {boolean} [values=false] - Include array of raw point properties
|
|
16
|
+
* @property {boolean} [spherical=false] - Use true if you use a spherical coordinate system
|
|
14
17
|
*/
|
|
15
18
|
export function pointstogrid(opts = {}) {
|
|
16
|
-
|
|
19
|
+
let {
|
|
20
|
+
points,
|
|
21
|
+
grid,
|
|
22
|
+
var: varField,
|
|
23
|
+
values: includeValues = false,
|
|
24
|
+
spherical = false,
|
|
25
|
+
} = opts;
|
|
17
26
|
|
|
18
27
|
const t0 = performance.now();
|
|
19
28
|
|
|
29
|
+
if (spherical) {
|
|
30
|
+
grid = unstitch(grid);
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
const gridFeatures = grid.features;
|
|
21
34
|
const pointFeatures = points.features;
|
|
22
35
|
|
|
@@ -105,5 +118,5 @@ export function pointstogrid(opts = {}) {
|
|
|
105
118
|
const t1 = performance.now();
|
|
106
119
|
console.log(`Execution time: ${(t1 - t0).toFixed(2)} ms`);
|
|
107
120
|
|
|
108
|
-
return result;
|
|
121
|
+
return spherical ? stitchmerge(result) : result;
|
|
109
122
|
}
|
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
import { bbox } from "@turf/bbox";
|
|
2
2
|
import { intersect } from "@turf/intersect";
|
|
3
|
+
import { area } from "@turf/area";
|
|
3
4
|
import { geoPath } from "d3-geo";
|
|
5
|
+
import { stitchmerge } from "../helpers/stitchmerge.js";
|
|
6
|
+
import { unstitch } from "../helpers/unstitch.js";
|
|
4
7
|
import RBush from "rbush";
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* @function polygonstogrid
|
|
8
11
|
* @description Assign polygons to a grid and compute weighted sums per cell.
|
|
9
|
-
*
|
|
10
|
-
* Optimized and removes cells with count = 0
|
|
11
|
-
* Supports multiple variables in varField (string or array of strings)
|
|
12
|
-
* Treats undefined or NaN as zero when summing
|
|
13
|
-
* If `values` is true, stores an array of intersected polygon properties
|
|
12
|
+
* Supports planar (path.area) or spherical (Turf area) surface calculation.
|
|
13
|
+
* Optimized and removes cells with count = 0.
|
|
14
|
+
* Supports multiple variables in varField (string or array of strings).
|
|
15
|
+
* Treats undefined or NaN as zero when summing.
|
|
16
|
+
* If `values` is true, stores an array of intersected polygon properties.
|
|
14
17
|
* @param {object} opts
|
|
15
|
-
* @property {object} [polygons] - GeoJSON polygons or
|
|
18
|
+
* @property {object} [polygons] - GeoJSON polygons or multipolygons to assign
|
|
16
19
|
* @property {object} [grid] - GeoJSON grid
|
|
17
|
-
* @property {string|Array} [var] - Field(s)
|
|
18
|
-
* @property {boolean} [values=false] - Include array of raw
|
|
20
|
+
* @property {string|Array} [var] - Field(s) to compute weighted sums (optional)
|
|
21
|
+
* @property {boolean} [values=false] - Include array of raw polygon properties
|
|
22
|
+
* @property {boolean} [spherical=false] - Compute areas on the sphere (Turf) instead of planar (D3)
|
|
19
23
|
*/
|
|
20
24
|
export function polygonstogrid(opts = {}) {
|
|
21
|
-
|
|
25
|
+
let {
|
|
22
26
|
grid,
|
|
23
27
|
polygons,
|
|
24
|
-
grid_id = "index",
|
|
25
28
|
var: varField,
|
|
26
29
|
values: includeValues = false,
|
|
30
|
+
spherical = false,
|
|
27
31
|
} = opts;
|
|
28
32
|
|
|
29
33
|
const t0 = performance.now();
|
|
30
34
|
const path = geoPath();
|
|
31
35
|
|
|
36
|
+
// Unstitch grids if needed
|
|
37
|
+
if (spherical) {
|
|
38
|
+
grid = unstitch(grid);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// --- Normalize varField to array ---
|
|
33
42
|
const varFields = varField
|
|
34
43
|
? Array.isArray(varField)
|
|
@@ -36,18 +45,18 @@ export function polygonstogrid(opts = {}) {
|
|
|
36
45
|
: [varField]
|
|
37
46
|
: [];
|
|
38
47
|
|
|
39
|
-
// --- 1. Compute
|
|
48
|
+
// --- 1. Compute areas for polygons and grid cells ---
|
|
40
49
|
for (const poly of polygons.features) {
|
|
41
|
-
poly.properties.
|
|
50
|
+
if (spherical) poly.properties.area_spherical = area(poly);
|
|
51
|
+
else poly.properties.area_plan = path.area(poly);
|
|
42
52
|
}
|
|
53
|
+
|
|
43
54
|
for (const cell of grid.features) {
|
|
44
|
-
cell.properties.
|
|
55
|
+
if (spherical) cell.properties.area_spherical = area(cell);
|
|
56
|
+
else cell.properties.area_plan = path.area(cell);
|
|
45
57
|
|
|
46
|
-
// initialize statistics
|
|
47
58
|
cell.properties.count = 0;
|
|
48
|
-
for (const v of varFields)
|
|
49
|
-
cell.properties[v] = 0;
|
|
50
|
-
}
|
|
59
|
+
for (const v of varFields) cell.properties[v] = 0;
|
|
51
60
|
if (includeValues) cell.properties.values = [];
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -67,27 +76,31 @@ export function polygonstogrid(opts = {}) {
|
|
|
67
76
|
for (const cand of candidates) {
|
|
68
77
|
const cell = cand.cell;
|
|
69
78
|
|
|
79
|
+
// vérifier que les deux features existent
|
|
80
|
+
if (!poly || !cell) continue;
|
|
81
|
+
|
|
82
|
+
// Turf v7: intersect via FeatureCollection contenant exactement deux géométries
|
|
70
83
|
const inter = intersect({
|
|
71
84
|
type: "FeatureCollection",
|
|
72
85
|
features: [poly, cell],
|
|
73
86
|
});
|
|
74
87
|
if (!inter) continue;
|
|
75
88
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
89
|
+
const areaVal = spherical ? area(inter) : path.area(inter);
|
|
90
|
+
const pctArea =
|
|
91
|
+
areaVal /
|
|
92
|
+
(spherical
|
|
93
|
+
? poly.properties.area_spherical
|
|
94
|
+
: poly.properties.area_plan);
|
|
78
95
|
|
|
79
96
|
// update cell statistics
|
|
80
97
|
cell.properties.count += 1;
|
|
81
|
-
|
|
82
98
|
for (const v of varFields) {
|
|
83
99
|
const value = parseFloat(poly.properties[v]);
|
|
84
|
-
cell.properties[v] += !isNaN(value) ? value *
|
|
100
|
+
cell.properties[v] += !isNaN(value) ? value * pctArea : 0;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
if (includeValues) {
|
|
89
|
-
cell.properties.values.push({ ...poly.properties });
|
|
90
|
-
}
|
|
103
|
+
if (includeValues) cell.properties.values.push({ ...poly.properties });
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
|
|
@@ -104,5 +117,5 @@ export function polygonstogrid(opts = {}) {
|
|
|
104
117
|
).toFixed(2)} ms`
|
|
105
118
|
);
|
|
106
119
|
|
|
107
|
-
return filteredGrid;
|
|
120
|
+
return spherical ? stitchmerge(filteredGrid) : filteredGrid;
|
|
108
121
|
}
|