Агрегация пространственных данных в Turf.js строится вокруг задачи сопоставления точечных объектов с полигональными зонами и последующего вычисления статистик по группам. Базовый принцип заключается в выполнении пространственного соединения (spatial join), после которого данные агрегируются средствами JavaScript или специализированными утилитами Turf.
Ключевая операция пространственной агрегации — определение, попадает
ли точка внутрь полигона. Для этого используется
booleanPointInPolygon.
Функция принимает точку и полигон и возвращает логическое значение:
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import { point, polygon } from "@turf/helpers";
const pt = point([30.5, 50.5]);
const poly = polygon([[
[30, 50],
[31, 50],
[31, 51],
[30, 51],
[30, 50]
]]);
booleanPointInPolygon(pt, poly);
На этом уровне формируется основа для последующей группировки данных: каждая точка либо относится к полигону, либо исключается из набора.
При наличии множества точек и множества полигонов применяется итеративное сопоставление. Каждый объект из набора точек проверяется на принадлежность каждому полигону.
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
function spatialJoin(points, polygons) {
const result = polygons.features.map(poly => ({
...poly,
properties: {
...poly.properties,
joined: []
}
}));
points.features.forEach(pt => {
result.forEach(poly => {
if (booleanPointInPolygon(pt, poly)) {
poly.properties.joined.push(pt.properties.value);
}
});
});
return result;
}
Такой подход формирует промежуточную структуру, в которой каждый полигон содержит набор значений, принадлежащих его области.
В Turf.js предусмотрена утилита collect, предназначенная
для извлечения значений свойств точек и их переноса в полигоны.
import collect from "@turf/collect";
import { featureCollection, polygon, point } from "@turf/helpers";
const polys = featureCollection([
polygon([[
[0, 0],
[10, 0],
[10, 10],
[0, 10],
[0, 0]
]], { id: 1 })
]);
const pts = featureCollection([
point([1, 1], { population: 10 }),
point([2, 2], { population: 20 }),
point([20, 20], { population: 30 })
]);
collect(polys, pts, "population", "values");
Результатом работы становится добавление массива значений
values в свойства полигонов, содержащих попавшие в них
точки.
После формирования массивов значений выполняется статистическая обработка: суммирование, вычисление среднего, минимального и максимального значения.
function aggregate(polygons, propertyName) {
polygons.features.forEach(poly => {
const values = poly.properties[propertyName] || [];
const sum = values.reduce((acc, v) => acc + v, 0);
const count = values.length;
const mean = count > 0 ? sum / count : 0;
const min = Math.min(...values);
const max = Math.max(...values);
poly.properties.sum = sum;
poly.properties.count = count;
poly.properties.mean = mean;
poly.properties.min = min;
poly.properties.max = max;
});
return polygons;
}
Такой этап превращает пространственную группировку в полноценную аналитическую модель, где каждый полигон представляет агрегированную статистическую единицу.
При наличии нескольких атрибутов возможно вычисление взвешенных метрик. Например, суммарная численность населения с учетом коэффициентов:
function weightedSum(values, weights) {
return values.reduce((acc, v, i) => acc + v * (weights[i] || 1), 0);
}
Применение подобных функций особенно актуально в задачах территориального анализа, где каждый объект имеет различную значимость.
Вместо вложения массивов в полигоны может использоваться внешняя структура Map, где ключом выступает идентификатор полигона.
function groupByPolygon(points, polygons) {
const groups = new Map();
polygons.features.forEach(p => {
groups.set(p.properties.id, []);
});
points.features.forEach(pt => {
polygons.features.forEach(poly => {
if (booleanPointInPolygon(pt, poly)) {
groups.get(poly.properties.id).push(pt);
}
});
});
return groups;
}
Такой подход упрощает последующую обработку и снижает связанность данных.
При большом количестве объектов линейная проверка становится неэффективной. Для оптимизации используется пространственное индексирование: предварительная фильтрация по bounding box.
import bbox from "@turf/bbox";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
function indexedJoin(points, polygons) {
const indexed = polygons.features.map(poly => ({
poly,
bbox: bbox(poly),
values: []
}));
points.features.forEach(pt => {
const [x, y] = pt.geometry.coordinates;
indexed.forEach(item => {
const [minX, minY, maxX, maxY] = item.bbox;
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
if (booleanPointInPolygon(pt, item.poly)) {
item.values.push(pt.properties.value);
}
}
});
});
return indexed;
}
Фильтрация по bounding box значительно сокращает количество геометрических проверок.
Альтернативный подход заключается в разбиении пространства на регулярную сетку и последующем накоплении значений внутри ячеек.
import squareGrid from "@turf/square-grid";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import bbox from "@turf/bbox";
const grid = squareGrid(bbox(points), 1);
grid.features.forEach(cell => {
cell.properties.sum = 0;
cell.properties.count = 0;
points.features.forEach(pt => {
if (booleanPointInPolygon(pt, cell)) {
cell.properties.sum += pt.properties.value;
cell.properties.count += 1;
}
});
});
Подобная модель применяется в тепловых картах и плотностном анализе.
При наличии комплексных данных агрегация может охватывать сразу несколько показателей:
function multiAggregate(polygons, points) {
polygons.features.forEach(poly => {
const values = {
income: [],
population: []
};
points.features.forEach(pt => {
if (booleanPointInPolygon(pt, poly)) {
values.income.push(pt.properties.income);
values.population.push(pt.properties.population);
}
});
poly.properties.income_avg =
values.income.reduce((a, b) => a + b, 0) / (values.income.length || 1);
poly.properties.population_sum =
values.population.reduce((a, b) => a + b, 0);
});
return polygons;
}
При агрегации часто встречаются отсутствующие или некорректные значения. Их обработка выполняется на этапе подготовки данных:
const clean = values.filter(v => typeof v === "number" && !isNaN(v));
Игнорирование невалидных значений предотвращает искажение статистики.
При наличии вложенных административных уровней (район → город → область) агрегация выполняется поэтапно: сначала нижний уровень, затем перенос результатов на верхний.
function hierarchical(polygonsLevel1, polygonsLevel2) {
polygonsLevel2.features.forEach(parent => {
const children = polygonsLevel1.features.filter(child =>
booleanPointInPolygon(child.properties.centroid, parent)
);
parent.properties.total = children.reduce(
(acc, c) => acc + (c.properties.sum || 0),
0
);
});
return polygonsLevel2;
}
Точность пространственной агрегации зависит от геометрической корректности входных данных. Ошибки в порядке координат, самопересечения полигонов и несогласованные системы координат приводят к неверным результатам.
Стандартизация данных до GeoJSON с единым пространственным контекстом является обязательным условием стабильной агрегации.