geogrid 0.0.5 → 1.1.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/README.md +3 -7
- package/dist/index.min.js +1 -1
- package/package.json +1 -1
- package/src/operator/linestogrid.js +68 -89
- package/src/operator/linestogrid_save.js +124 -0
- package/src/operator/pointstogrid.js +37 -41
- package/src/operator/polygonstogrid.js +68 -86
- package/src/operator/polygonstogrid_save.js +144 -0
package/package.json
CHANGED
|
@@ -1,129 +1,108 @@
|
|
|
1
1
|
import { bbox } from "@turf/bbox";
|
|
2
2
|
import { lineSplit } from "@turf/line-split";
|
|
3
|
-
import { length
|
|
4
|
-
import { booleanIntersects } from "@turf/boolean-intersects";
|
|
3
|
+
import { length } from "@turf/length";
|
|
5
4
|
import RBush from "rbush";
|
|
6
|
-
import { min, max, median, mean, sum } from "d3-array";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* @function linestogrid
|
|
10
|
-
* @description
|
|
11
|
-
*
|
|
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
|
|
12
14
|
* @param {object} opts
|
|
15
|
+
* @property {object} [lines] - GeoJSON lines to assign
|
|
13
16
|
* @property {object} [grid] - GeoJSON grid
|
|
14
|
-
* @property {
|
|
15
|
-
* @property {
|
|
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
|
|
17
|
+
* @property {string|Array} [var] - Field(s) to compute weighted sums (optional)
|
|
18
|
+
* @property {boolean} [values=false] - Include array of raw lines properties
|
|
22
19
|
*/
|
|
23
20
|
export function linestogrid(opts = {}) {
|
|
24
21
|
const {
|
|
25
22
|
grid,
|
|
26
23
|
lines,
|
|
24
|
+
grid_id = "index",
|
|
27
25
|
var: varField,
|
|
28
26
|
values: includeValues = false,
|
|
29
|
-
sum: calcSum = true,
|
|
30
|
-
median: calcMedian = false,
|
|
31
|
-
min: calcMin = false,
|
|
32
|
-
max: calcMax = false,
|
|
33
|
-
mean: calcMean = false,
|
|
34
27
|
} = opts;
|
|
35
28
|
|
|
36
29
|
const t0 = performance.now();
|
|
37
|
-
const gridFeatures = grid.features;
|
|
38
|
-
const lineFeatures = lines.features;
|
|
39
|
-
const hasVar = varField !== undefined && varField !== null;
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
// --- Normalize varField to array ---
|
|
32
|
+
const varFields = varField
|
|
33
|
+
? Array.isArray(varField)
|
|
34
|
+
? varField
|
|
35
|
+
: [varField]
|
|
36
|
+
: [];
|
|
37
|
+
|
|
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" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const cell of grid.features) {
|
|
44
|
+
cell.properties.count = 0;
|
|
45
|
+
for (const v of varFields) {
|
|
46
|
+
cell.properties[v] = 0;
|
|
47
|
+
}
|
|
48
|
+
if (includeValues) cell.properties.values = [];
|
|
49
|
+
}
|
|
42
50
|
|
|
43
|
-
//
|
|
51
|
+
// --- 2. Build spatial index (RBush) on the grid ---
|
|
44
52
|
const tree = new RBush();
|
|
45
|
-
const items =
|
|
46
|
-
const [minX, minY, maxX, maxY] = bbox(
|
|
47
|
-
return { minX, minY, maxX, maxY,
|
|
53
|
+
const items = grid.features.map((cell) => {
|
|
54
|
+
const [minX, minY, maxX, maxY] = bbox(cell);
|
|
55
|
+
return { minX, minY, maxX, maxY, cell };
|
|
48
56
|
});
|
|
49
57
|
tree.load(items);
|
|
50
58
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
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;
|
|
59
|
+
// --- 3. Loop over lines ---
|
|
60
|
+
for (const line of lines.features) {
|
|
63
61
|
const [minX, minY, maxX, maxY] = bbox(line);
|
|
64
62
|
const candidates = tree.search({ minX, minY, maxX, maxY });
|
|
65
63
|
|
|
66
64
|
for (const cand of candidates) {
|
|
67
|
-
const
|
|
68
|
-
if (!booleanIntersects(g, line)) continue;
|
|
65
|
+
const cell = cand.cell;
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
// split line by cell polygon
|
|
68
|
+
const splitLines = lineSplit(line, cell);
|
|
69
|
+
if (!splitLines.features.length) continue;
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
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" });
|
|
75
|
+
}
|
|
76
|
+
if (totalSegLength === 0) continue;
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stats.countSet.add(i); // unique line IDs
|
|
78
|
+
// --- update cell statistics ---
|
|
79
|
+
cell.properties.count += 1; // one line per cell
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
for (const v of varFields) {
|
|
82
|
+
const value = parseFloat(line.properties[v]);
|
|
83
|
+
cell.properties[v] += !isNaN(value)
|
|
84
|
+
? value * (totalSegLength / line.properties.length_total)
|
|
85
|
+
: 0;
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
if (includeValues) {
|
|
89
|
+
cell.properties.values.push({ ...line.properties });
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
}
|
|
92
|
+
}
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
features:
|
|
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),
|
|
94
|
+
// --- 4. Filter out cells with count == 0 ---
|
|
95
|
+
const filteredGrid = {
|
|
96
|
+
...grid,
|
|
97
|
+
features: grid.features.filter((cell) => cell.properties.count > 0),
|
|
124
98
|
};
|
|
125
99
|
|
|
126
100
|
const t1 = performance.now();
|
|
127
|
-
console.log(
|
|
128
|
-
|
|
101
|
+
console.log(
|
|
102
|
+
`Line intersection completed for ${filteredGrid.features.length} cells — ${(
|
|
103
|
+
t1 - t0
|
|
104
|
+
).toFixed(2)} ms`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return filteredGrid;
|
|
129
108
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
*/
|
|
18
|
+
export function linestogrid(opts = {}) {
|
|
19
|
+
const {
|
|
20
|
+
grid,
|
|
21
|
+
lines,
|
|
22
|
+
var: varField,
|
|
23
|
+
values: includeValues = false,
|
|
24
|
+
sum: calcSum = true,
|
|
25
|
+
median: calcMedian = false,
|
|
26
|
+
min: calcMin = false,
|
|
27
|
+
max: calcMax = false,
|
|
28
|
+
mean: calcMean = false,
|
|
29
|
+
} = opts;
|
|
30
|
+
|
|
31
|
+
const t0 = performance.now();
|
|
32
|
+
const gridFeatures = grid.features;
|
|
33
|
+
const lineFeatures = lines.features;
|
|
34
|
+
const hasVar = varField !== undefined && varField !== null;
|
|
35
|
+
|
|
36
|
+
const gridbyindex = new Map(gridFeatures.map((d, i) => [i, d]));
|
|
37
|
+
|
|
38
|
+
// ---- 1. Spatial index RBush ----
|
|
39
|
+
const tree = new RBush();
|
|
40
|
+
const items = gridFeatures.map((g, i) => {
|
|
41
|
+
const [minX, minY, maxX, maxY] = bbox(g);
|
|
42
|
+
return { minX, minY, maxX, maxY, i };
|
|
43
|
+
});
|
|
44
|
+
tree.load(items);
|
|
45
|
+
|
|
46
|
+
// ---- 2. Prepare stats storage per cell ----
|
|
47
|
+
const gridStats = new Map();
|
|
48
|
+
gridFeatures.forEach((g, i) => {
|
|
49
|
+
gridStats.set(i, { countSet: new Set(), valuesList: [], numericList: [] });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---- 3. Loop over lines ----
|
|
53
|
+
lineFeatures.forEach((line, i) => {
|
|
54
|
+
const totalLen = turfLength(line);
|
|
55
|
+
if (totalLen === 0) return;
|
|
56
|
+
|
|
57
|
+
const val = hasVar ? parseFloat(line.properties?.[varField]) || 0 : 1;
|
|
58
|
+
const [minX, minY, maxX, maxY] = bbox(line);
|
|
59
|
+
const candidates = tree.search({ minX, minY, maxX, maxY });
|
|
60
|
+
|
|
61
|
+
for (const cand of candidates) {
|
|
62
|
+
const g = gridbyindex.get(cand.i);
|
|
63
|
+
if (!booleanIntersects(g, line)) continue;
|
|
64
|
+
|
|
65
|
+
const split = lineSplit(line, g);
|
|
66
|
+
if (!split || split.features.length === 0) continue;
|
|
67
|
+
|
|
68
|
+
let lenInside = 0;
|
|
69
|
+
split.features.forEach((seg) => {
|
|
70
|
+
const segLen = turfLength(seg);
|
|
71
|
+
if (segLen > 0 && booleanIntersects(g, seg)) {
|
|
72
|
+
lenInside += segLen;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (lenInside > 0) {
|
|
77
|
+
const stats = gridStats.get(cand.i);
|
|
78
|
+
stats.countSet.add(i); // unique line IDs
|
|
79
|
+
|
|
80
|
+
if (includeValues)
|
|
81
|
+
stats.valuesList.push(hasVar ? line.properties[varField] : i);
|
|
82
|
+
|
|
83
|
+
if (hasVar) stats.numericList.push(val * (lenInside / totalLen));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- 4. Build final GeoJSON ----
|
|
89
|
+
const result = {
|
|
90
|
+
type: "FeatureCollection",
|
|
91
|
+
features: gridFeatures
|
|
92
|
+
.map((g, i) => {
|
|
93
|
+
const stats = gridStats.get(i);
|
|
94
|
+
const numericValues = stats.numericList;
|
|
95
|
+
const values = stats.valuesList;
|
|
96
|
+
const count = stats.countSet.size;
|
|
97
|
+
|
|
98
|
+
if (count === 0) return null;
|
|
99
|
+
|
|
100
|
+
const cellProps = { count };
|
|
101
|
+
|
|
102
|
+
if (hasVar && numericValues.length > 0) {
|
|
103
|
+
if (calcSum) cellProps.sum = sum(numericValues);
|
|
104
|
+
if (calcMean) cellProps.mean = mean(numericValues);
|
|
105
|
+
if (calcMedian) cellProps.median = median(numericValues);
|
|
106
|
+
if (calcMin) cellProps.min = min(numericValues);
|
|
107
|
+
if (calcMax) cellProps.max = max(numericValues);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (includeValues) cellProps.values = values;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
type: g.type,
|
|
114
|
+
properties: { ...g.properties, ...cellProps },
|
|
115
|
+
geometry: g.geometry,
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.filter((f) => f !== null),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const t1 = performance.now();
|
|
122
|
+
console.log(`Execution time: ${(t1 - t0).toFixed(2)} ms`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
@@ -1,43 +1,35 @@
|
|
|
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 { min, max, median, mean, sum } from "d3-array";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* @function pointstogrid
|
|
8
|
-
* @description Assigns points to grid cells and computes
|
|
7
|
+
* @description Assigns points to grid cells and computes sums per cell.
|
|
8
|
+
* Supports multiple variables and stores point properties if values=true
|
|
9
9
|
* @param {object} opts
|
|
10
10
|
* @property {object} [points] - GeoJSON points
|
|
11
11
|
* @property {object} [grid] - GeoJSON grid (polygons)
|
|
12
|
-
* @property {string} [var] - Field for
|
|
13
|
-
* @property {boolean} [values=false] - Include array of raw
|
|
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
|
|
12
|
+
* @property {string|Array} [var] - Field(s) for summing values
|
|
13
|
+
* @property {boolean} [values=false] - Include array of raw point properties
|
|
19
14
|
*/
|
|
20
15
|
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;
|
|
16
|
+
const { points, grid, var: varField, values: includeValues = false } = opts;
|
|
32
17
|
|
|
33
18
|
const t0 = performance.now();
|
|
19
|
+
|
|
34
20
|
const gridFeatures = grid.features;
|
|
35
21
|
const pointFeatures = points.features;
|
|
36
|
-
|
|
22
|
+
|
|
23
|
+
// --- Normalize varField to array ---
|
|
24
|
+
const varFields = varField
|
|
25
|
+
? Array.isArray(varField)
|
|
26
|
+
? varField
|
|
27
|
+
: [varField]
|
|
28
|
+
: [];
|
|
37
29
|
|
|
38
30
|
const gridbyindex = new Map(gridFeatures.map((d, i) => [i, d]));
|
|
39
31
|
|
|
40
|
-
//
|
|
32
|
+
// --- 1. Spatial index for polygons ---
|
|
41
33
|
const tree = new RBush();
|
|
42
34
|
const items = gridFeatures.map((g, i) => {
|
|
43
35
|
const [minX, minY, maxX, maxY] = bbox(g);
|
|
@@ -45,13 +37,14 @@ export function pointstogrid(opts = {}) {
|
|
|
45
37
|
});
|
|
46
38
|
tree.load(items);
|
|
47
39
|
|
|
48
|
-
//
|
|
40
|
+
// --- 2. Prepare stats storage per cell ---
|
|
49
41
|
const gridStats = new Map();
|
|
50
42
|
gridFeatures.forEach((g, i) => {
|
|
51
|
-
gridStats.set(i, { countSet: new Set(), valuesList: [],
|
|
43
|
+
gridStats.set(i, { countSet: new Set(), valuesList: [], numericLists: {} });
|
|
44
|
+
varFields.forEach((v) => (gridStats.get(i).numericLists[v] = []));
|
|
52
45
|
});
|
|
53
46
|
|
|
54
|
-
//
|
|
47
|
+
// --- 3. Loop over points ---
|
|
55
48
|
pointFeatures.forEach((pt, i) => {
|
|
56
49
|
const x = pt.geometry.coordinates[0];
|
|
57
50
|
const y = pt.geometry.coordinates[1];
|
|
@@ -65,38 +58,40 @@ export function pointstogrid(opts = {}) {
|
|
|
65
58
|
const stats = gridStats.get(cand.i);
|
|
66
59
|
stats.countSet.add(i);
|
|
67
60
|
|
|
68
|
-
|
|
61
|
+
// --- handle numeric variables ---
|
|
62
|
+
varFields.forEach((v) => {
|
|
63
|
+
const val = parseFloat(pt.properties[v]);
|
|
64
|
+
if (!isNaN(val)) stats.numericLists[v].push(val);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// --- handle values option ---
|
|
68
|
+
if (includeValues) stats.valuesList.push({ ...pt.properties });
|
|
69
69
|
|
|
70
|
-
if (includeValues)
|
|
71
|
-
stats.valuesList.push(hasVar ? pt.properties[varField] : i);
|
|
72
|
-
if (hasVar) stats.numericList.push(parseFloat(val) || 0);
|
|
73
70
|
break; // point counted once per cell
|
|
74
71
|
}
|
|
75
72
|
});
|
|
76
73
|
|
|
77
|
-
//
|
|
74
|
+
// --- 4. Build final GeoJSON ---
|
|
78
75
|
const result = {
|
|
79
76
|
type: "FeatureCollection",
|
|
80
77
|
features: gridFeatures
|
|
81
78
|
.map((g, i) => {
|
|
82
79
|
const stats = gridStats.get(i);
|
|
83
|
-
const numericValues = stats.numericList;
|
|
84
|
-
const values = stats.valuesList;
|
|
85
80
|
const count = stats.countSet.size;
|
|
86
|
-
|
|
87
81
|
if (count === 0) return null;
|
|
88
82
|
|
|
89
83
|
const cellProps = { count };
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
// sum each variable
|
|
86
|
+
varFields.forEach((v) => {
|
|
87
|
+
const numericValues = stats.numericLists[v];
|
|
88
|
+
cellProps[v] =
|
|
89
|
+
numericValues.length > 0
|
|
90
|
+
? numericValues.reduce((a, b) => a + b, 0)
|
|
91
|
+
: 0;
|
|
92
|
+
});
|
|
98
93
|
|
|
99
|
-
if (includeValues) cellProps.values =
|
|
94
|
+
if (includeValues) cellProps.values = stats.valuesList;
|
|
100
95
|
|
|
101
96
|
return {
|
|
102
97
|
type: g.type,
|
|
@@ -109,5 +104,6 @@ export function pointstogrid(opts = {}) {
|
|
|
109
104
|
|
|
110
105
|
const t1 = performance.now();
|
|
111
106
|
console.log(`Execution time: ${(t1 - t0).toFixed(2)} ms`);
|
|
107
|
+
|
|
112
108
|
return result;
|
|
113
109
|
}
|
|
@@ -1,126 +1,108 @@
|
|
|
1
|
-
import { featureCollection } from "@turf/helpers";
|
|
2
1
|
import { bbox } from "@turf/bbox";
|
|
3
2
|
import { intersect } from "@turf/intersect";
|
|
4
|
-
import
|
|
3
|
+
import { geoPath } from "d3-geo";
|
|
5
4
|
import RBush from "rbush";
|
|
6
|
-
import { min, max, median, mean, sum } from "d3-array";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* @function polygonstogrid
|
|
10
|
-
* @description Assign polygons to a grid and compute
|
|
8
|
+
* @description Assign polygons to a grid and compute weighted sums per cell.
|
|
9
|
+
* Uses a spatial index to speed up calculations
|
|
11
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
14
|
* @param {object} opts
|
|
15
|
+
* @property {object} [polygons] - GeoJSON polygons or multi polygons to assign
|
|
13
16
|
* @property {object} [grid] - GeoJSON grid
|
|
14
|
-
* @property {
|
|
15
|
-
* @property {
|
|
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
|
|
17
|
+
* @property {string|Array} [var] - Field(s) ton compute weighted sums (optional)
|
|
18
|
+
* @property {boolean} [values=false] - Include array of raw polygons properties
|
|
22
19
|
*/
|
|
23
20
|
export function polygonstogrid(opts = {}) {
|
|
24
21
|
const {
|
|
25
22
|
grid,
|
|
26
23
|
polygons,
|
|
24
|
+
grid_id = "index",
|
|
27
25
|
var: varField,
|
|
28
26
|
values: includeValues = false,
|
|
29
|
-
sum: calcSum = true,
|
|
30
|
-
median: calcMedian = false,
|
|
31
|
-
min: calcMin = false,
|
|
32
|
-
max: calcMax = false,
|
|
33
|
-
mean: calcMean = false,
|
|
34
27
|
} = opts;
|
|
35
28
|
|
|
36
29
|
const t0 = performance.now();
|
|
30
|
+
const path = geoPath();
|
|
31
|
+
|
|
32
|
+
// --- Normalize varField to array ---
|
|
33
|
+
const varFields = varField
|
|
34
|
+
? Array.isArray(varField)
|
|
35
|
+
? varField
|
|
36
|
+
: [varField]
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
// --- 1. Compute planar areas for polygons and grid cells ---
|
|
40
|
+
for (const poly of polygons.features) {
|
|
41
|
+
poly.properties.area_plan = path.area(poly);
|
|
42
|
+
}
|
|
43
|
+
for (const cell of grid.features) {
|
|
44
|
+
cell.properties.area_plan = path.area(cell);
|
|
45
|
+
|
|
46
|
+
// initialize statistics
|
|
47
|
+
cell.properties.count = 0;
|
|
48
|
+
for (const v of varFields) {
|
|
49
|
+
cell.properties[v] = 0;
|
|
50
|
+
}
|
|
51
|
+
if (includeValues) cell.properties.values = [];
|
|
52
|
+
}
|
|
37
53
|
|
|
38
|
-
|
|
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 ----
|
|
54
|
+
// --- 2. Build spatial index (RBush) on the grid ---
|
|
45
55
|
const tree = new RBush();
|
|
46
|
-
const items =
|
|
47
|
-
const [minX, minY, maxX, maxY] = bbox(
|
|
48
|
-
return { minX, minY, maxX, maxY,
|
|
56
|
+
const items = grid.features.map((cell) => {
|
|
57
|
+
const [minX, minY, maxX, maxY] = bbox(cell);
|
|
58
|
+
return { minX, minY, maxX, maxY, cell };
|
|
49
59
|
});
|
|
50
60
|
tree.load(items);
|
|
51
61
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
gridStats.set(i, { countSet: new Set(), valuesList: [], numericList: [] });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ---- 3. Loop over polygons ----
|
|
59
|
-
polys.forEach((p, i) => {
|
|
60
|
-
const polygonArea = area(p);
|
|
61
|
-
const val = hasVar ? parseFloat(p.properties?.[varField]) || 0 : 1;
|
|
62
|
-
|
|
63
|
-
const [minX, minY, maxX, maxY] = bbox(p);
|
|
62
|
+
// --- 3. Loop over polygons ---
|
|
63
|
+
for (const poly of polygons.features) {
|
|
64
|
+
const [minX, minY, maxX, maxY] = bbox(poly);
|
|
64
65
|
const candidates = tree.search({ minX, minY, maxX, maxY });
|
|
65
66
|
|
|
66
67
|
for (const cand of candidates) {
|
|
67
|
-
const
|
|
68
|
-
const f = intersect(featureCollection([p, g]));
|
|
69
|
-
if (!f) continue;
|
|
68
|
+
const cell = cand.cell;
|
|
70
69
|
|
|
71
|
-
const
|
|
72
|
-
|
|
70
|
+
const inter = intersect({
|
|
71
|
+
type: "FeatureCollection",
|
|
72
|
+
features: [poly, cell],
|
|
73
|
+
});
|
|
74
|
+
if (!inter) continue;
|
|
73
75
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
+
const areaPlan = path.area(inter);
|
|
77
|
+
const pctAreaPlan = areaPlan / poly.properties.area_plan;
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// update cell statistics
|
|
80
|
+
cell.properties.count += 1;
|
|
81
|
+
|
|
82
|
+
for (const v of varFields) {
|
|
83
|
+
const value = parseFloat(poly.properties[v]);
|
|
84
|
+
cell.properties[v] += !isNaN(value) ? value * pctAreaPlan : 0;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
// add properties of intersected polygons
|
|
88
|
+
if (includeValues) {
|
|
89
|
+
cell.properties.values.push({ ...poly.properties });
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
|
-
}
|
|
92
|
+
}
|
|
86
93
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
features:
|
|
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),
|
|
94
|
+
// --- 4. Filter out cells with count == 0 ---
|
|
95
|
+
const filteredGrid = {
|
|
96
|
+
...grid,
|
|
97
|
+
features: grid.features.filter((cell) => cell.properties.count > 0),
|
|
120
98
|
};
|
|
121
99
|
|
|
122
100
|
const t1 = performance.now();
|
|
123
|
-
console.log(
|
|
101
|
+
console.log(
|
|
102
|
+
`Intersection completed for ${filteredGrid.features.length} cells — ${(
|
|
103
|
+
t1 - t0
|
|
104
|
+
).toFixed(2)} ms`
|
|
105
|
+
);
|
|
124
106
|
|
|
125
|
-
return
|
|
107
|
+
return filteredGrid;
|
|
126
108
|
}
|