Пространственное соединение — это операция, при которой данные из двух различных геометрических наборов связываются на основе их пространственного отношения. В контексте геовизуализации это означает сопоставление объектов не по ключам или идентификаторам, а по их положению в пространстве.
В экосистеме Deck.gl пространственные соединения используются для анализа и визуального синтеза слоёв: например, объединение точек событий с полигонами районов, сопоставление маршрутов с дорожной сетью, агрегация значений точечных данных внутри ячеек сетки.
Ключевые отношения, используемые в пространственных соединениях:
Deck.gl сам по себе не навязывает единую систему пространственных соединений, но предоставляет инструменты, позволяющие эффективно реализовывать их на разных уровнях:
В типичной архитектуре:
Наиболее распространённый случай — сопоставление точек с административными границами.
Пример структуры данных:
const points = [
{ position: [71.4304, 51.1281], value: 10 },
{ position: [71.4500, 51.1400], value: 25 }
];
const polygons = [
{
polygon: [
[71.40, 51.10],
[71.50, 51.10],
[71.50, 51.20],
[71.40, 51.20]
],
id: "district-1"
}
];
Наиболее прямолинейный подход — выполнение проверки принадлежности на этапе подготовки данных.
function pointInPolygon(point, polygon) {
const [x, y] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0], yi = polygon[i][1];
const xj = polygon[j][0], yj = polygon[j][1];
const intersect =
((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi + xi));
if (intersect) inside = !inside;
}
return inside;
}
Далее выполняется join:
const joined = points.map(p => {
const polygon = polygons.find(poly =>
pointInPolygon(p.position, poly.polygon)
);
return {
...p,
district: polygon ? polygon.id : null
};
});
Характеристика подхода:
При увеличении объёма данных линейный перебор становится неэффективным. Используются структуры:
Пример с R-tree:
import RBush from "rbush";
const tree = new RBush();
const indexedPolygons = polygons.map(p => ({
minX: Math.min(...p.polygon.map(d => d[0])),
minY: Math.min(...p.polygon.map(d => d[1])),
maxX: Math.max(...p.polygon.map(d => d[0])),
maxY: Math.max(...p.polygon.map(d => d[1])),
data: p
}));
tree.load(indexedPolygons);
Поиск кандидатов:
function findPolygon(point) {
const candidates = tree.search({
minX: point[0],
minY: point[1],
maxX: point[0],
maxY: point[1]
});
return candidates.find(c =>
pointInPolygon(point, c.data.polygon)
);
}
Deck.gl позволяет переносить часть логики в GPU, особенно при работе с визуальными слоями.
Используются:
GPUGridLayerHexagonLayerScreenGridLayerCompositeLayer с shader logicПример концепции агрегации:
import { HexagonLayer } from "@deck.gl/aggregation-layers";
const layer = new HexagonLayer({
id: "hex-layer",
data: points,
getPosition: d => d.position,
getWeight: d => d.value,
radius: 1000,
elevationScale: 50
});
Здесь spatial join происходит неявно: каждая точка автоматически ассоциируется с hex-ячейкой.
H3 индексирование (шестиугольная сетка) позволяет заменить сложные геометрические операции простыми lookup-операциями.
import { h3ToGeo, geoToH3 } from "h3-js";
const resolution = 7;
const enriched = points.map(p => {
const h3Index = geoToH3(p.position[1], p.position[0], resolution);
return {
...p,
cell: h3Index
};
});
Далее join с агрегированными данными:
const aggregated = {};
for (const p of enriched) {
aggregated[p.cell] = (aggregated[p.cell] || 0) + p.value;
}
Использование H3 даёт:
При построении сложных визуализаций spatial join часто
инкапсулируется в CompositeLayer.
import { CompositeLayer } from "@deck.gl/core";
class JoinedLayer extends CompositeLayer {
renderLayers() {
const { points, polygons } = this.props;
const enriched = points.map(p => {
const match = polygons.find(poly =>
pointInPolygon(p.position, poly.polygon)
);
return {
...p,
region: match?.id
};
});
return [
new ScatterplotLayer({
id: "points",
data: enriched,
getPosition: d => d.position,
getFillColor: d => d.region ? [0, 200, 100] : [200, 0, 0]
})
];
}
}
Такая структура позволяет:
Spatial join часто комбинируется с фильтрацией:
const filtered = points
.filter(p => p.value > 10)
.filter(p => p.position[0] > 71.4);
В Deck.gl фильтрация может выполняться на GPU через
DataFilterExtension, что снижает нагрузку на CPU.
Помимо вложенности и пересечений часто используется расстояние.
function distance(a, b) {
const dx = a[0] - b[0];
const dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
function nearestPolygon(point, polygons) {
let best = null;
let bestDist = Infinity;
for (const poly of polygons) {
const centroid = computeCentroid(poly.polygon);
const d = distance(point, centroid);
if (d < bestDist) {
bestDist = d;
best = poly;
}
}
return best;
}
Этот подход применяется:
Ключевые ограничения:
Оптимизации:
При работе с потоками данных (real-time feeds):
Пример:
function addPoint(streamPoint) {
const cell = geoToH3(
streamPoint.position[1],
streamPoint.position[0],
7
);
incrementCell(cell, streamPoint.value);
}
Такой подход позволяет:
В реальных приложениях spatial joins редко используются изолированно. Часто они комбинируются:
Пример цепочки:
points
.map(assignH3)
.filter(inViewport)
.reduce(aggregateByCell, {})
Такие цепочки формируют основу аналитических слоёв Deck.gl, где визуализация является прямым отражением пространственных вычислений.