Работа со статистикой по зонам в геопространственном анализе опирается на идею пространственной агрегации: набор точек, линий или объектов объединяется в заранее определённые области (зоны), после чего внутри каждой зоны вычисляются численные характеристики. В Turf.js этот подход реализуется через комбинацию предикатов принадлежности, функций перебора геометрий и статистических операций над наборами данных.
Зоны обычно представлены полигонами, которые могут соответствовать административным границам, сетке тайлов, пользовательским областям интереса или результатам кластеризации. Точки — это наблюдения: события, координаты объектов, измерения. Главная задача заключается в сопоставлении каждой точки с зоной и последующем вычислении агрегированных метрик.
Формально процесс состоит из трёх этапов:
Определение зон Зоны задаются как
FeatureCollection<Polygon | MultiPolygon>.
Привязка объектов к зонам Каждая точка проверяется на принадлежность полигону.
Агрегация значений Внутри каждой зоны вычисляются статистики: сумма, среднее, минимум, максимум, количество.
Ключевое отличие Turf.js от серверных ГИС-систем заключается в том, что все операции выполняются в памяти браузера или Node.js, без пространственного индекса по умолчанию. Это влияет на производительность и требует аккуратного выбора алгоритмов при больших данных.
Основой зональной статистики является операция point-in-polygon. В Turf.js она реализуется через:
booleanPointInPolygonАлгоритм использует лучевой метод (ray casting), определяя, пересекает ли горизонтальный луч границы полигона нечётное число раз.
Пример базовой проверки:
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
const inside = booleanPointInPolygon(point, polygon);
Эта операция становится ядром всех последующих агрегаций, поскольку определяет, в какую зону попадает каждая наблюдаемая точка.
Типичная структура входных данных:
const zones = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { id: 1, name: "Zone A" },
geometry: {
type: "Polygon",
coordinates: [...]
}
}
]
};
const points = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { value: 10 },
geometry: {
type: "Point",
coordinates: [30.5, 50.2]
}
}
]
};
Каждая зона может содержать собственные свойства, в которые будут записаны результаты статистики.
Самая базовая метрика — количество точек в зоне. Она вычисляется перебором всех точек и проверкой их принадлежности:
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
function countPointsInZones(zones, points) {
return {
...zones,
features: zones.features.map(zone => {
let count = 0;
for (const pt of points.features) {
if (booleanPointInPolygon(pt, zone)) {
count++;
}
}
return {
...zone,
properties: {
...zone.properties,
count
}
};
})
};
}
Такой подход имеет сложность O(n × m), где n — количество зон, m — количество точек.
Часто точки содержат измеряемые параметры: температуру, количество пользователей, интенсивность события. Тогда требуется вычисление суммы, среднего и других метрик.
function zoneSumAndMean(zones, points, property) {
return {
...zones,
features: zones.features.map(zone => {
let sum = 0;
let count = 0;
for (const pt of points.features) {
if (booleanPointInPolygon(pt, zone)) {
sum += pt.properties[property];
count++;
}
}
return {
...zone,
properties: {
...zone.properties,
sum,
mean: count > 0 ? sum / count : 0
}
};
})
};
}
Такой подход позволяет строить тепловые карты на уровне зон, а не отдельных точек.
collect для группировки значенийВ Turf.js существует функция collect, предназначенная
для извлечения значений из точек внутри полигонов.
import collect from "@turf/collect";
Она позволяет агрегировать свойства точек по зонам без ручного перебора.
Принцип работы:
После этого статистика вычисляется уже по массиву.
const collected = collect(zones, points, "value", "values");
Далее можно вычислить метрики:
collected.features.forEach(zone => {
const values = zone.properties.values || [];
const sum = values.reduce((a, b) => a + b, 0);
const mean = values.length ? sum / values.length : 0;
zone.properties.sum = sum;
zone.properties.mean = mean;
});
При большом количестве точек проверка каждого объекта на каждый полигон становится узким местом. Для оптимизации используется предварительное ограничение через bounding box.
Turf.js предоставляет:
bboxbooleanDisjointpointsWithinPolygonpointsWithinPolygonimport pointsWithinPolygon from "@turf/points-within-polygon";
const pts = pointsWithinPolygon(points, polygon);
Эта функция возвращает только те точки, которые гарантированно находятся внутри полигона, снижая количество проверок.
Частый сценарий — разбиение территории на сетку (grid) и расчет статистики по каждой ячейке.
import squareGrid from "@turf/square-grid";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
Процесс:
const grid = squareGrid(bbox, cellSize);
grid.features.forEach(cell => {
let count = 0;
for (const pt of points.features) {
if (booleanPointInPolygon(pt, cell)) {
count++;
}
}
cell.properties.count = count;
});
Такой подход используется для heatmap-аналитики, оценки плотности и кластерного анализа.
Если каждая точка имеет вес (например, количество пользователей или интенсивность события), применяется взвешенная агрегация.
function weightedSum(zones, points, valueField, weightField) {
return {
...zones,
features: zones.features.map(zone => {
let sum = 0;
let weightSum = 0;
for (const pt of points.features) {
if (booleanPointInPolygon(pt, zone)) {
const value = pt.properties[valueField];
const weight = pt.properties[weightField];
sum += value * weight;
weightSum += weight;
}
}
return {
...zone,
properties: {
...zone.properties,
weightedMean: weightSum ? sum / weightSum : 0
}
};
})
};
}
Взвешенные метрики особенно важны в демографии, логистике и анализе трафика.
Зональная статистика часто выполняется не по одной, а по нескольким категориям данных одновременно.
Пример: в каждой зоне нужно посчитать:
function multiStats(zones, points) {
return zones.features.map(zone => {
let count = 0;
let sum = 0;
const categories = new Set();
for (const pt of points.features) {
if (booleanPointInPolygon(pt, zone)) {
count++;
sum += pt.properties.value;
categories.add(pt.properties.type);
}
}
return {
...zone,
properties: {
...zone.properties,
count,
sum,
uniqueCategories: categories.size
}
};
});
}
Такой подход используется в аналитических панелях, где зоны выступают как единицы визуализации.
Turf.js не включает встроенный R-tree, но его можно комбинировать с внешними структурами данных. Однако даже без них можно снизить нагрузку через предварительный bbox-фильтр:
import bboxPolygon from "@turf/bbox-polygon";
import booleanIntersects from "@turf/boolean-intersects";
Сначала проверяется пересечение bounding box:
if (!booleanIntersects(point, zoneBboxPolygon)) {
continue;
}
Только затем выполняется точная проверка принадлежности.
Иногда зоны вложены: страна → регион → город → район. В этом случае статистика рассчитывается по нескольким уровням.
Подход:
function aggregateHierarchy(level1, level2) {
level2.features.forEach(parent => {
parent.properties.count = 0;
for (const child of level1.features) {
if (booleanPointInPolygon(child, parent)) {
parent.properties.count += child.properties.count || 1;
}
}
});
}
Такая модель позволяет строить каскадные отчёты.
Ключевые факторы производительности:
booleanPointInPolygonОптимизации:
При масштабах десятки тысяч точек и сотни зон чистый перебор становится неэффективным, и требуется внешняя индексация или серверная обработка.
Особенно критичен последний случай, поскольку Turf.js может трактовать границу как принадлежность в зависимости от реализации алгоритма.
В реальных приложениях обычно строится единый pipeline:
Такой конвейер обеспечивает предсказуемость вычислений и совместимость с картографическими библиотеками (Mapbox, Leaflet, OpenLayers).