geogrid 0.0.1 → 0.0.4

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,129 @@
1
+ import { bbox } from "@turf/bbox";
2
+ import { lineSplit } from "@turf/line-split";
3
+ import { length as turfLength } from "@turf/length";
4
+ import { booleanIntersects } from "@turf/boolean-intersects";
5
+ import RBush from "rbush";
6
+ import { min, max, median, mean, sum } from "d3-array";
7
+
8
+ /**
9
+ * @function linestogrid
10
+ * @description Assigns lines to a grid and computes statistics per cell.
11
+ * Supports weighted lengths or simple counts.
12
+ * @param {object} opts
13
+ * @property {object} [grid] - GeoJSON grid
14
+ * @property {object} [lines] - GeoJSON lines (LineString or MultiLineString)
15
+ * @property {string} [var] - Field for weighting length (optional)
16
+ * @property {boolean} [values=false] - Include array of raw values/IDs
17
+ * @property {boolean} [sum=true] - Compute sum of weighted lengths
18
+ * @property {boolean} [median=false] - Compute median
19
+ * @property {boolean} [min=false] - Compute minimum
20
+ * @property {boolean} [max=false] - Compute maximum
21
+ * @property {boolean} [mean=false] - Compute mean
22
+ */
23
+ export function linestogrid(opts = {}) {
24
+ const {
25
+ grid,
26
+ lines,
27
+ var: varField,
28
+ values: includeValues = false,
29
+ sum: calcSum = true,
30
+ median: calcMedian = false,
31
+ min: calcMin = false,
32
+ max: calcMax = false,
33
+ mean: calcMean = false,
34
+ } = opts;
35
+
36
+ const t0 = performance.now();
37
+ const gridFeatures = grid.features;
38
+ const lineFeatures = lines.features;
39
+ const hasVar = varField !== undefined && varField !== null;
40
+
41
+ const gridbyindex = new Map(gridFeatures.map((d, i) => [i, d]));
42
+
43
+ // ---- 1. Spatial index RBush ----
44
+ const tree = new RBush();
45
+ const items = gridFeatures.map((g, i) => {
46
+ const [minX, minY, maxX, maxY] = bbox(g);
47
+ return { minX, minY, maxX, maxY, i };
48
+ });
49
+ tree.load(items);
50
+
51
+ // ---- 2. Prepare stats storage per cell ----
52
+ const gridStats = new Map();
53
+ gridFeatures.forEach((g, i) => {
54
+ gridStats.set(i, { countSet: new Set(), valuesList: [], numericList: [] });
55
+ });
56
+
57
+ // ---- 3. Loop over lines ----
58
+ lineFeatures.forEach((line, i) => {
59
+ const totalLen = turfLength(line);
60
+ if (totalLen === 0) return;
61
+
62
+ const val = hasVar ? parseFloat(line.properties?.[varField]) || 0 : 1;
63
+ const [minX, minY, maxX, maxY] = bbox(line);
64
+ const candidates = tree.search({ minX, minY, maxX, maxY });
65
+
66
+ for (const cand of candidates) {
67
+ const g = gridbyindex.get(cand.i);
68
+ if (!booleanIntersects(g, line)) continue;
69
+
70
+ const split = lineSplit(line, g);
71
+ if (!split || split.features.length === 0) continue;
72
+
73
+ let lenInside = 0;
74
+ split.features.forEach((seg) => {
75
+ const segLen = turfLength(seg);
76
+ if (segLen > 0 && booleanIntersects(g, seg)) {
77
+ lenInside += segLen;
78
+ }
79
+ });
80
+
81
+ if (lenInside > 0) {
82
+ const stats = gridStats.get(cand.i);
83
+ stats.countSet.add(i); // unique line IDs
84
+
85
+ if (includeValues)
86
+ stats.valuesList.push(hasVar ? line.properties[varField] : i);
87
+
88
+ if (hasVar) stats.numericList.push(val * (lenInside / totalLen));
89
+ }
90
+ }
91
+ });
92
+
93
+ // ---- 4. Build final GeoJSON ----
94
+ const result = {
95
+ type: "FeatureCollection",
96
+ features: gridFeatures
97
+ .map((g, i) => {
98
+ const stats = gridStats.get(i);
99
+ const numericValues = stats.numericList;
100
+ const values = stats.valuesList;
101
+ const count = stats.countSet.size;
102
+
103
+ if (count === 0) return null;
104
+
105
+ const cellProps = { count };
106
+
107
+ if (hasVar && numericValues.length > 0) {
108
+ if (calcSum) cellProps.sum = sum(numericValues);
109
+ if (calcMean) cellProps.mean = mean(numericValues);
110
+ if (calcMedian) cellProps.median = median(numericValues);
111
+ if (calcMin) cellProps.min = min(numericValues);
112
+ if (calcMax) cellProps.max = max(numericValues);
113
+ }
114
+
115
+ if (includeValues) cellProps.values = values;
116
+
117
+ return {
118
+ type: g.type,
119
+ properties: { ...g.properties, ...cellProps },
120
+ geometry: g.geometry,
121
+ };
122
+ })
123
+ .filter((f) => f !== null),
124
+ };
125
+
126
+ const t1 = performance.now();
127
+ console.log(`Execution time: ${(t1 - t0).toFixed(2)} ms`);
128
+ return result;
129
+ }
@@ -1,49 +1,113 @@
1
+ import RBush from "rbush";
2
+ import { bbox } from "@turf/bbox";
1
3
  import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
4
+ import { min, max, median, mean, sum } from "d3-array";
2
5
 
3
6
  /**
4
- * @function op.pointstogrid
5
- * @description The `op.pointstogrid()` function allows to count dots in polygons (e.g. grid cells)
6
- * @property {object} [points] - dots geoJSON
7
- * @property {object} [grid] - grid
8
- * @property {string} [var = undefined] - field (absolute quantitative data only)
7
+ * @function pointstogrid
8
+ * @description Assigns points to grid cells and computes statistics per cell.
9
+ * @param {object} opts
10
+ * @property {object} [points] - GeoJSON points
11
+ * @property {object} [grid] - GeoJSON grid (polygons)
12
+ * @property {string} [var] - Field for weighting points (optional)
13
+ * @property {boolean} [values=false] - Include array of raw values/IDs
14
+ * @property {boolean} [sum=true] - Compute sum
15
+ * @property {boolean} [median=false] - Compute median
16
+ * @property {boolean} [min=false] - Compute minimum
17
+ * @property {boolean} [max=false] - Compute maximum
18
+ * @property {boolean} [mean=false] - Compute mean
9
19
  */
10
- export function pointstogrid(
11
- opts = {
12
- points: undefined,
13
- grid: undefined,
14
- var: undefined,
15
- }
16
- ) {
17
- let polys = opts.grid.features;
18
- let points = opts.dots.features;
19
- let count = new Array(polys.length).fill(0);
20
- let nb = points.length;
21
- let test = new Array(nb).fill(true);
22
- polys.forEach((p, i) => {
23
- points.forEach((d, j) => {
24
- if (test[j]) {
25
- if (booleanPointInPolygon(d, p)) {
26
- if (opts.var == undefined) {
27
- count[i] = count[i] + 1;
28
- } else {
29
- count[i] = count[i] + parseFloat(d.properties[opts.var]);
30
- }
31
- test[j] = false;
32
- }
33
- }
34
- });
20
+ export function pointstogrid(opts = {}) {
21
+ const {
22
+ points,
23
+ grid,
24
+ var: varField,
25
+ values: includeValues = false,
26
+ sum: calcSum = true,
27
+ median: calcMedian = false,
28
+ min: calcMin = false,
29
+ max: calcMax = false,
30
+ mean: calcMean = false,
31
+ } = opts;
32
+
33
+ const t0 = performance.now();
34
+ const gridFeatures = grid.features;
35
+ const pointFeatures = points.features;
36
+ const hasVar = varField !== undefined && varField !== null;
37
+
38
+ const gridbyindex = new Map(gridFeatures.map((d, i) => [i, d]));
39
+
40
+ // ---- 1. Spatial index for polygons ----
41
+ const tree = new RBush();
42
+ const items = gridFeatures.map((g, i) => {
43
+ const [minX, minY, maxX, maxY] = bbox(g);
44
+ return { minX, minY, maxX, maxY, i };
45
+ });
46
+ tree.load(items);
47
+
48
+ // ---- 2. Prepare stats storage per cell ----
49
+ const gridStats = new Map();
50
+ gridFeatures.forEach((g, i) => {
51
+ gridStats.set(i, { countSet: new Set(), valuesList: [], numericList: [] });
52
+ });
53
+
54
+ // ---- 3. Loop over points ----
55
+ pointFeatures.forEach((pt, i) => {
56
+ const x = pt.geometry.coordinates[0];
57
+ const y = pt.geometry.coordinates[1];
58
+
59
+ const candidates = tree.search({ minX: x, minY: y, maxX: x, maxY: y });
60
+
61
+ for (const cand of candidates) {
62
+ const poly = gridbyindex.get(cand.i);
63
+ if (!booleanPointInPolygon(pt, poly)) continue;
64
+
65
+ const stats = gridStats.get(cand.i);
66
+ stats.countSet.add(i);
67
+
68
+ const val = hasVar ? pt.properties[varField] : 1;
69
+
70
+ if (includeValues)
71
+ stats.valuesList.push(hasVar ? pt.properties[varField] : i);
72
+ if (hasVar) stats.numericList.push(parseFloat(val) || 0);
73
+ break; // point counted once per cell
74
+ }
35
75
  });
36
76
 
37
- // Rebuild grid
38
- let output = polys
39
- .map((d, i) => ({
40
- type: d.type,
41
- geometry: d.geometry,
42
- properties: { ...d.properties, count: count[i] },
43
- }))
44
- .filter((d) => d.properties.count !== 0);
45
-
46
- //const endTime = performance.now();
47
- //const elapsedTime = endTime - startTime;
48
- return { type: "FeatureCollection", features: output };
77
+ // ---- 4. Build final GeoJSON ----
78
+ const result = {
79
+ type: "FeatureCollection",
80
+ features: gridFeatures
81
+ .map((g, i) => {
82
+ const stats = gridStats.get(i);
83
+ const numericValues = stats.numericList;
84
+ const values = stats.valuesList;
85
+ const count = stats.countSet.size;
86
+
87
+ if (count === 0) return null;
88
+
89
+ const cellProps = { count };
90
+
91
+ if (hasVar && numericValues.length > 0) {
92
+ if (calcSum) cellProps.sum = sum(numericValues);
93
+ if (calcMean) cellProps.mean = mean(numericValues);
94
+ if (calcMedian) cellProps.median = median(numericValues);
95
+ if (calcMin) cellProps.min = min(numericValues);
96
+ if (calcMax) cellProps.max = max(numericValues);
97
+ }
98
+
99
+ if (includeValues) cellProps.values = values;
100
+
101
+ return {
102
+ type: g.type,
103
+ properties: { ...g.properties, ...cellProps },
104
+ geometry: g.geometry,
105
+ };
106
+ })
107
+ .filter((f) => f !== null),
108
+ };
109
+
110
+ const t1 = performance.now();
111
+ console.log(`Execution time: ${(t1 - t0).toFixed(2)} ms`);
112
+ return result;
49
113
  }
@@ -1,68 +1,126 @@
1
1
  import { featureCollection } from "@turf/helpers";
2
+ import { bbox } from "@turf/bbox";
2
3
  import { intersect } from "@turf/intersect";
3
- import { geoPath } from "d3-geo";
4
- import { groups } from "d3-array";
5
- const d3 = Object.assign({}, { geoPath, groups });
4
+ import area from "@turf/area";
5
+ import RBush from "rbush";
6
+ import { min, max, median, mean, sum } from "d3-array";
6
7
 
7
8
  /**
8
- * @function op.polygonstogrid
9
- * @description The `op.polygonstogrid()` function allows to affect polygons values tu grid cells.
10
- * @property {object} [polygons] - polygons geoJSON
11
- * @property {object} [grid] - grid geoJSON
12
- * @property {string} [var = undefined] - field (absolute quantitative data only)
9
+ * @function polygonstogrid
10
+ * @description Assign polygons to a grid and compute statistics per cell.
11
+ * Optimized and removes cells with count = 0
12
+ * @param {object} opts
13
+ * @property {object} [grid] - GeoJSON grid
14
+ * @property {object} [polygons] - GeoJSON polygons to assign
15
+ * @property {string} [var] - Field for weighting (optional)
16
+ * @property {boolean} [values=false] - Include array of raw values
17
+ * @property {boolean} [sum=true] - Compute sum
18
+ * @property {boolean} [median=false] - Compute median
19
+ * @property {boolean} [min=false] - Compute minimum
20
+ * @property {boolean} [max=false] - Compute maximum
21
+ * @property {boolean} [mean=false] - Compute mean
13
22
  */
23
+ export function polygonstogrid(opts = {}) {
24
+ const {
25
+ grid,
26
+ polygons,
27
+ var: varField,
28
+ values: includeValues = false,
29
+ sum: calcSum = true,
30
+ median: calcMedian = false,
31
+ min: calcMin = false,
32
+ max: calcMax = false,
33
+ mean: calcMean = false,
34
+ } = opts;
14
35
 
15
- export function polygonstogrid(
16
- opts = {
17
- grid: undefined,
18
- polygons: undefined,
19
- var: undefined,
20
- }
21
- ) {
22
- let grid = opts.grid.features;
23
- let polys = opts.polygons.features;
24
- let gridbyindex = new Map(grid.map((d, i) => [i, d]));
25
-
26
- let arr = [];
36
+ const t0 = performance.now();
37
+
38
+ const gridFeatures = grid.features;
39
+ const polys = polygons.features;
40
+ const hasVar = varField !== undefined && varField !== null;
41
+
42
+ const gridbyindex = new Map(gridFeatures.map((d, i) => [i, d]));
43
+
44
+ // ---- 1. Spatial index RBush ----
45
+ const tree = new RBush();
46
+ const items = gridFeatures.map((g, i) => {
47
+ const [minX, minY, maxX, maxY] = bbox(g);
48
+ return { minX, minY, maxX, maxY, i };
49
+ });
50
+ tree.load(items);
51
+
52
+ // ---- 2. Prepare stats storage per cell ----
53
+ const gridStats = new Map();
54
+ gridFeatures.forEach((g, i) => {
55
+ gridStats.set(i, { countSet: new Set(), valuesList: [], numericList: [] });
56
+ });
57
+
58
+ // ---- 3. Loop over polygons ----
27
59
  polys.forEach((p, i) => {
28
- const area = d3.geoPath().area(p);
29
- grid.forEach((g, j) => {
60
+ const polygonArea = area(p);
61
+ const val = hasVar ? parseFloat(p.properties?.[varField]) || 0 : 1;
62
+
63
+ const [minX, minY, maxX, maxY] = bbox(p);
64
+ const candidates = tree.search({ minX, minY, maxX, maxY });
65
+
66
+ for (const cand of candidates) {
67
+ const g = gridbyindex.get(cand.i);
30
68
  const f = intersect(featureCollection([p, g]));
31
- if (f !== null) {
32
- let areapiece = d3.geoPath().area(f);
33
- arr.push([i, j, areapiece / area]);
69
+ if (!f) continue;
70
+
71
+ const areapiece = area(f);
72
+ const fraction = areapiece / polygonArea;
73
+
74
+ const stats = gridStats.get(cand.i);
75
+ stats.countSet.add(i);
76
+
77
+ if (includeValues) {
78
+ stats.valuesList.push(hasVar ? p.properties[varField] : 1);
79
+ }
80
+
81
+ if (hasVar) {
82
+ stats.numericList.push(val * fraction);
34
83
  }
35
- });
84
+ }
36
85
  });
37
86
 
38
- let accessor;
39
- if (opts.var) {
40
- accessor = new Map(
41
- polys.map((d, i) => [i, parseFloat(d.properties[opts.var]) || 0])
42
- );
43
- } else {
44
- accessor = new Map(polys.map((d, i) => [i, 1]));
45
- }
46
-
47
- let datagrid = d3.groups(arr, (d) => d[1]);
48
-
49
- function getsum(cell) {
50
- let sum = 0;
51
- cell[1].forEach((d) => {
52
- sum += accessor.get(d[0]) * d[2];
53
- });
54
- return sum == 0 ? undefined : sum;
55
- }
56
-
57
- return {
87
+ // ---- 4. Build final GeoJSON ----
88
+ const result = {
58
89
  type: "FeatureCollection",
59
- features: datagrid.map((d) => {
60
- let tmp = gridbyindex.get(d[0]);
61
- return {
62
- type: tmp.type,
63
- properties: { ...tmp.properties, sum: getsum(d) },
64
- geometry: tmp.geometry,
65
- };
66
- }),
90
+ features: gridFeatures
91
+ .map((g, i) => {
92
+ const stats = gridStats.get(i);
93
+ const numericValues = stats.numericList;
94
+ const values = stats.valuesList;
95
+ const count = stats.countSet.size;
96
+
97
+ if (count === 0) return null;
98
+
99
+ const cellProps = { count };
100
+
101
+ if (hasVar && numericValues.length > 0) {
102
+ if (calcSum) cellProps.sum = sum(numericValues);
103
+ if (calcMean) cellProps.mean = mean(numericValues);
104
+ if (calcMedian) cellProps.median = median(numericValues);
105
+ if (calcMin) cellProps.min = min(numericValues);
106
+ if (calcMax) cellProps.max = max(numericValues);
107
+ }
108
+
109
+ if (includeValues) {
110
+ cellProps.values = values;
111
+ }
112
+
113
+ return {
114
+ type: g.type,
115
+ properties: { ...g.properties, ...cellProps },
116
+ geometry: g.geometry,
117
+ };
118
+ })
119
+ .filter((f) => f !== null),
67
120
  };
121
+
122
+ const t1 = performance.now();
123
+ console.log(`Optimized execution time: ${(t1 - t0).toFixed(2)} ms`);
124
+
125
+ return result;
68
126
  }
@@ -0,0 +1,100 @@
1
+ import { featureCollection } from "@turf/helpers";
2
+ import { bbox } from "@turf/bbox";
3
+ import { intersect } from "@turf/intersect";
4
+ import RBush from "rbush";
5
+ import { geoPath } from "d3-geo";
6
+ import { groups } from "d3-array";
7
+
8
+ const d3 = Object.assign({}, { geoPath, groups });
9
+
10
+ /**
11
+ * @function polygonstogrid
12
+ * @description Répartit des polygones (optionnellement pondérés) sur une grille polygonale.
13
+ * @property {object} [polygons] - GeoJSON de polygones
14
+ * @property {object} [grid] - GeoJSON de grille
15
+ * @property {string} [var] - Champ de pondération (facultatif)
16
+ */
17
+ export function polygonstogrid(
18
+ opts = { grid: undefined, polygons: undefined, var: undefined }
19
+ ) {
20
+ const t0 = performance.now();
21
+
22
+ const grid = opts.grid.features;
23
+ const polys = opts.polygons.features;
24
+ const gridbyindex = new Map(grid.map((d, i) => [i, d]));
25
+
26
+ // ---- 1. Créer l’index spatial RBush ----
27
+ const tree = new RBush();
28
+ const items = grid.map((g, i) => {
29
+ const [minX, minY, maxX, maxY] = bbox(g);
30
+ return { minX, minY, maxX, maxY, i };
31
+ });
32
+ tree.load(items);
33
+
34
+ const arr = [];
35
+ const path = d3.geoPath();
36
+
37
+ // ---- 2. Boucle sur les polygones ----
38
+ polys.forEach((p, i) => {
39
+ const area = path.area(p);
40
+ const [minX, minY, maxX, maxY] = bbox(p);
41
+
42
+ // 3. Chercher uniquement les cellules qui peuvent intersecter
43
+ const candidates = tree.search({ minX, minY, maxX, maxY });
44
+
45
+ // 4. Tester seulement les candidats
46
+ for (const cand of candidates) {
47
+ const g = gridbyindex.get(cand.i);
48
+ const f = intersect(featureCollection([p, g]));
49
+ if (f) {
50
+ const areapiece = path.area(f);
51
+ arr.push([i, cand.i, areapiece / area]);
52
+ }
53
+ }
54
+ });
55
+
56
+ // ---- 5. Calcul des valeurs ----
57
+ const hasVar = opts.var !== undefined && opts.var !== null;
58
+
59
+ const accessor = new Map(
60
+ polys.map((d, i) => [
61
+ i,
62
+ hasVar ? parseFloat(d.properties[opts.var]) || 0 : 1,
63
+ ])
64
+ );
65
+
66
+ const datagrid = d3.groups(arr, (d) => d[1]);
67
+
68
+ function getsum(cell) {
69
+ const vals = cell[1];
70
+ if (hasVar) {
71
+ // Cas pondéré
72
+ let sum = 0;
73
+ vals.forEach((d) => {
74
+ sum += accessor.get(d[0]) * d[2];
75
+ });
76
+ return sum === 0 ? undefined : sum;
77
+ } else {
78
+ // Cas non pondéré : juste compter le nombre de polygones intersectés
79
+ const uniquePolygons = new Set(vals.map((d) => d[0]));
80
+ return uniquePolygons.size;
81
+ }
82
+ }
83
+
84
+ // ---- 6. Assemblage du résultat ----
85
+ const result = {
86
+ type: "FeatureCollection",
87
+ features: datagrid.map(([key, vals]) => {
88
+ const tmp = gridbyindex.get(key);
89
+ return {
90
+ type: tmp.type,
91
+ properties: { ...tmp.properties, sum: getsum([key, vals]) },
92
+ geometry: tmp.geometry,
93
+ };
94
+ }),
95
+ };
96
+
97
+ const t1 = performance.now();
98
+ console.log(`Temps d'exécution: ${(t1 - t0).toFixed(2)} ms`);
99
+ return result;
100
+ }
@@ -1,45 +0,0 @@
1
- /**
2
- * @function tool/rewind
3
- * @description The `tool.rewind` function allows to generate compliant Polygon and MultiPolygon geometries. Adapted from MapBox geojson-rewind code (https://github.com/mapbox/grojson-rewind) under ISC license
4
- * @see {@link https://observablehq.com/@neocartocnrs/handle-geometries}
5
- * @property {object} data - a GeoJSON FeatureCollection
6
- * @property {boolean} [options.outer = false] - rewind Rings Outer
7
- * @property {boolean} [options.mutate = false] - mutate the Input geoJSON
8
- */
9
-
10
- export function rewind(data, options = {}) {
11
- data = JSON.parse(JSON.stringify(data));
12
- let outer = options.outer === false ? false : true;
13
- let mutate = options.mutate === false ? false : true;
14
- let geo = mutate === true ? data : JSON.parse(JSON.stringify(x));
15
- for (let i = 0; i < geo.features.length; i++) {
16
- if (geo.features[i].geometry.type === "Polygon") {
17
- rewindRings(geo.features[i].geometry.coordinates, outer);
18
- } else if (geo.features[i].geometry.type === "MultiPolygon") {
19
- for (let j = 0; j < geo.features[i].geometry.coordinates.length; j++) {
20
- rewindRings(geo.features[i].geometry.coordinates[j], outer);
21
- }
22
- }
23
- }
24
- return geo;
25
- }
26
-
27
- function rewindRings(rings, outer) {
28
- if (rings.length === 0) return;
29
- rewindRing(rings[0], outer);
30
- for (let i = 1; i < rings.length; i++) {
31
- rewindRing(rings[i], !outer);
32
- }
33
- }
34
-
35
- function rewindRing(ring, dir) {
36
- let tArea = 0;
37
- let err = 0;
38
- for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
39
- const k = (ring[i][0] - ring[j][0]) * (ring[j][1] + ring[i][1]);
40
- const m = tArea + k;
41
- err += Math.abs(tArea) >= Math.abs(k) ? tArea - m + k : k - m + tArea;
42
- tArea = m;
43
- }
44
- if (tArea + err >= 0 !== !!dir) ring.reverse();
45
- }