geogrid 1.1.1 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geogrid",
3
- "version": "1.1.1",
3
+ "version": "1.2.2",
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,95 @@
1
+ import booleanIntersects from "@turf/boolean-intersects";
2
+
3
+ /**
4
+ * @function square_sph
5
+ * @summary Build a global square grid in lon/lat.
6
+ * @description
7
+ * Generates a grid of square polygons covering the whole globe in WGS84 coordinates.
8
+ * - Longitudes: start at -180, add `step` until reaching +180 (last cell may be smaller)
9
+ * - Latitudes: start at +90, subtract `step` until reaching -90 (last cell may be smaller)
10
+ * - Cells never overlap; borders align exactly
11
+ * - EPS is used to avoid coordinates exactly at ±180 and ±90 to prevent rewind/topology issues
12
+ *
13
+ * @param {object} options
14
+ * @param {number} [options.step=1] - Grid cell size in degrees
15
+ * @param {GeoJSON} [options.domain=null] - Optional GeoJSON mask; only intersecting cells are kept
16
+ * @returns {GeoJSON.FeatureCollection} A FeatureCollection of square polygons
17
+ */
18
+ export function square_sph({ step = 1, domain = null } = {}) {
19
+ const LON_MIN = -180;
20
+ const LON_MAX = 180;
21
+ const LAT_MIN = -90;
22
+ const LAT_MAX = 90;
23
+ const EPS = 1e-2; // small offset to avoid exact ±180/±90 coordinates
24
+
25
+ if (step <= 0) throw new Error("step must be > 0");
26
+
27
+ const features = [];
28
+ let index = 0;
29
+
30
+ // --- Longitudes: from -180 (west) to +180 (east), incrementing by step ---
31
+ let lonWest = LON_MIN;
32
+ while (lonWest < LON_MAX - 1e-12) {
33
+ let lonEast = lonWest + step;
34
+ if (lonEast > LON_MAX) lonEast = LON_MAX;
35
+
36
+ // Apply EPS to avoid exact ±180
37
+ const lonW = Math.max(lonWest, LON_MIN + EPS);
38
+ const lonE = Math.min(lonEast, LON_MAX - EPS);
39
+
40
+ // --- Latitudes: from +90 (north) to -90 (south), decrementing by step ---
41
+ let latNorth = LAT_MAX;
42
+ while (latNorth > LAT_MIN + 1e-12) {
43
+ let latSouth = latNorth - step;
44
+ if (latSouth < LAT_MIN) latSouth = LAT_MIN;
45
+
46
+ // Apply EPS to avoid exact ±90
47
+ const latN = Math.min(latNorth, LAT_MAX - EPS);
48
+ const latS = Math.max(latSouth, LAT_MIN + EPS);
49
+
50
+ // Skip degenerate cells (can happen with EPS near poles)
51
+ if (lonE - lonW > 1e-12 && latN - latS > 1e-12) {
52
+ features.push({
53
+ type: "Feature",
54
+ geometry: {
55
+ type: "Polygon",
56
+ coordinates: [
57
+ [
58
+ [lonW, latN],
59
+ [lonE, latN],
60
+ [lonE, latS],
61
+ [lonW, latS],
62
+ [lonW, latN],
63
+ ],
64
+ ],
65
+ },
66
+ properties: { index: index++ },
67
+ });
68
+ }
69
+
70
+ // Move to the next row (southward)
71
+ latNorth = latSouth;
72
+ }
73
+
74
+ // Move to the next column (eastward)
75
+ lonWest = lonEast;
76
+ }
77
+
78
+ // --- Optional filtering by domain ---
79
+ const filtered = domain
80
+ ? features.filter((f) => {
81
+ try {
82
+ return booleanIntersects(f, domain);
83
+ } catch {
84
+ return false;
85
+ }
86
+ })
87
+ : features;
88
+
89
+ return {
90
+ type: "FeatureCollection",
91
+ grid: "square_sph",
92
+ geo: true,
93
+ features: filtered,
94
+ };
95
+ }
@@ -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,54 +1,67 @@
1
1
  import { bbox } from "@turf/bbox";
2
2
  import { lineSplit } from "@turf/line-split";
3
3
  import { length } from "@turf/length";
4
+ import { booleanWithin } from "@turf/boolean-within";
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 linestogrid
8
- * @description Assign lines to a grid and compute weighted sums per cell.
9
- * Uses a spatial index to speed up calculations.
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 line properties
14
- * @param {object} opts
15
- * @property {object} [lines] - GeoJSON lines to assign
16
- * @property {object} [grid] - GeoJSON grid
17
- * @property {string|Array} [var] - Field(s) to compute weighted sums (optional)
18
- * @property {boolean} [values=false] - Include array of raw lines properties
11
+ * @description Assign lines (LineString or MultiLineString) to a grid and compute weighted sums per cell.
12
+ * MultiLineStrings are treated as a single feature for count and proportional distribution.
19
13
  */
14
+
20
15
  export function linestogrid(opts = {}) {
21
- const {
16
+ let {
22
17
  grid,
23
18
  lines,
24
- grid_id = "index",
25
19
  var: varField,
26
20
  values: includeValues = false,
21
+ spherical = false,
22
+ debug = false,
27
23
  } = opts;
28
24
 
29
25
  const t0 = performance.now();
30
26
 
31
- // --- Normalize varField to array ---
27
+ if (spherical) grid = unstitch(grid);
28
+
32
29
  const varFields = varField
33
30
  ? Array.isArray(varField)
34
31
  ? varField
35
32
  : [varField]
36
33
  : [];
37
34
 
38
- // --- 1. Compute total lengths for lines and initialize grid cells ---
39
- for (const line of lines.features) {
40
- line.properties.length_total = length(line, { units: "meters" });
35
+ // --- Add stable ID to grid cells ---
36
+ grid.features.forEach((cell, i) => (cell._id = i));
37
+
38
+ // --- Compute total length per line feature ---
39
+ for (const f of lines.features) {
40
+ if (f.geometry.type === "LineString") {
41
+ f.properties.length_total = length(f, { units: "meters" });
42
+ } else if (f.geometry.type === "MultiLineString") {
43
+ let total = 0;
44
+ for (const coords of f.geometry.coordinates) {
45
+ const part = {
46
+ type: "Feature",
47
+ geometry: { type: "LineString", coordinates: coords },
48
+ };
49
+ total += length(part, { units: "meters" });
50
+ }
51
+ f.properties.length_total = total;
52
+ } else {
53
+ f.properties.length_total = 0;
54
+ }
41
55
  }
42
56
 
57
+ // --- Initialize grid cells ---
43
58
  for (const cell of grid.features) {
44
59
  cell.properties.count = 0;
45
- for (const v of varFields) {
46
- cell.properties[v] = 0;
47
- }
60
+ for (const v of varFields) cell.properties[v] = 0;
48
61
  if (includeValues) cell.properties.values = [];
49
62
  }
50
63
 
51
- // --- 2. Build spatial index (RBush) on the grid ---
64
+ // --- Build spatial index ---
52
65
  const tree = new RBush();
53
66
  const items = grid.features.map((cell) => {
54
67
  const [minX, minY, maxX, maxY] = bbox(cell);
@@ -56,53 +69,100 @@ export function linestogrid(opts = {}) {
56
69
  });
57
70
  tree.load(items);
58
71
 
59
- // --- 3. Loop over lines ---
60
- for (const line of lines.features) {
61
- const [minX, minY, maxX, maxY] = bbox(line);
72
+ // --- Track counted features for debug ---
73
+ const countedLines = new Set();
74
+
75
+ // --- Process each line feature ---
76
+ for (const f of lines.features) {
77
+ const totalLength = f.properties.length_total || 0;
78
+ if (totalLength === 0) continue;
79
+
80
+ const [minX, minY, maxX, maxY] = bbox(f);
62
81
  const candidates = tree.search({ minX, minY, maxX, maxY });
82
+ const touched = new Set();
83
+ let totalSegLen = 0;
63
84
 
64
85
  for (const cand of candidates) {
65
86
  const cell = cand.cell;
87
+ let segLen = 0;
88
+
89
+ if (f.geometry.type === "LineString") {
90
+ segLen = safeSegLength(f, cell);
91
+ } else if (f.geometry.type === "MultiLineString") {
92
+ for (const coords of f.geometry.coordinates) {
93
+ const part = {
94
+ type: "Feature",
95
+ geometry: { type: "LineString", coordinates: coords },
96
+ };
97
+ segLen += safeSegLength(part, cell);
98
+ }
99
+ }
66
100
 
67
- // split line by cell polygon
68
- const splitLines = lineSplit(line, cell);
69
- if (!splitLines.features.length) continue;
101
+ if (segLen === 0) continue;
102
+ totalSegLen += segLen;
70
103
 
71
- // compute total length of segments inside the cell
72
- let totalSegLength = 0;
73
- for (const seg of splitLines.features) {
74
- totalSegLength += length(seg, { units: "meters" });
104
+ if (!touched.has(cell._id)) {
105
+ cell.properties.count += 1;
106
+ touched.add(cell._id);
75
107
  }
76
- if (totalSegLength === 0) continue;
77
-
78
- // --- update cell statistics ---
79
- cell.properties.count += 1; // one line per cell
80
108
 
81
109
  for (const v of varFields) {
82
- const value = parseFloat(line.properties[v]);
110
+ const value = parseFloat(f.properties[v]);
83
111
  cell.properties[v] += !isNaN(value)
84
- ? value * (totalSegLength / line.properties.length_total)
112
+ ? value * (segLen / totalLength)
85
113
  : 0;
86
114
  }
87
115
 
88
- if (includeValues) {
89
- cell.properties.values.push({ ...line.properties });
90
- }
116
+ if (includeValues) cell.properties.values.push({ ...f.properties });
91
117
  }
118
+
119
+ if (totalSegLen > 0) countedLines.add(f.properties.id || f.id || f);
92
120
  }
93
121
 
94
- // --- 4. Filter out cells with count == 0 ---
122
+ // --- Filter empty cells ---
95
123
  const filteredGrid = {
96
124
  ...grid,
97
- features: grid.features.filter((cell) => cell.properties.count > 0),
125
+ features: grid.features.filter((c) => c.properties.count > 0),
98
126
  };
99
127
 
100
128
  const t1 = performance.now();
101
129
  console.log(
102
- `Line intersection completed for ${filteredGrid.features.length} cells — ${(
130
+ `Line aggregation completed for ${filteredGrid.features.length} cells — ${(
103
131
  t1 - t0
104
132
  ).toFixed(2)} ms`
105
133
  );
106
134
 
107
- return filteredGrid;
135
+ // --- Debug report ---
136
+ if (debug) {
137
+ const totalLines = lines.features.length;
138
+ const missing = totalLines - countedLines.size;
139
+ console.log(
140
+ `Counted ${countedLines.size}/${totalLines} line features (${missing} missing)`
141
+ );
142
+ }
143
+
144
+ return spherical ? stitchmerge(filteredGrid) : filteredGrid;
145
+ }
146
+
147
+ /**
148
+ * @function safeSegLength
149
+ * @description Returns total segment length of intersection between a line and polygon cell,
150
+ * including case where line is entirely contained in the cell.
151
+ */
152
+ function safeSegLength(line, cell) {
153
+ try {
154
+ if (booleanWithin(line, cell)) {
155
+ return length(line, { units: "meters" });
156
+ }
157
+
158
+ const split = lineSplit(line, cell);
159
+ let sum = 0;
160
+ for (const s of split.features) {
161
+ const l = length(s, { units: "meters" });
162
+ if (l > 0.001) sum += l; // ignore sub-mm artifacts
163
+ }
164
+ return sum;
165
+ } catch (e) {
166
+ return 0;
167
+ }
108
168
  }
@@ -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
- const { points, grid, var: varField, values: includeValues = false } = opts;
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
  }