Spatial joins

Понятие пространственного соединения

Пространственное соединение — это операция, при которой данные из двух различных геометрических наборов связываются на основе их пространственного отношения. В контексте геовизуализации это означает сопоставление объектов не по ключам или идентификаторам, а по их положению в пространстве.

В экосистеме Deck.gl пространственные соединения используются для анализа и визуального синтеза слоёв: например, объединение точек событий с полигонами районов, сопоставление маршрутов с дорожной сетью, агрегация значений точечных данных внутри ячеек сетки.

Ключевые отношения, используемые в пространственных соединениях:

  • попадание точки в полигон (point-in-polygon)
  • пересечение геометрий (intersection)
  • расстояние до объекта (proximity join)
  • принадлежность к ячейке сетки (grid indexing)

Роль Spatial Joins в архитектуре Deck.gl

Deck.gl сам по себе не навязывает единую систему пространственных соединений, но предоставляет инструменты, позволяющие эффективно реализовывать их на разных уровнях:

  • CPU-обработка (JavaScript / TypeScript)
  • GPU-ускоренные вычисления через WebGL (Transformations, shaders)
  • комбинирование с Data Filtering и Aggregation Layers

В типичной архитектуре:

  • источники данных загружаются как GeoJSON, CSV или бинарные форматы
  • выполняется предварительная пространственная индексация
  • соединение происходит либо до рендера, либо внутри слоя

Базовый сценарий: точки и полигоны

Наиболее распространённый случай — сопоставление точек с административными границами.

Пример структуры данных:

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"
  }
];

Реализация point-in-polygon через CPU

Наиболее прямолинейный подход — выполнение проверки принадлежности на этапе подготовки данных.

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
  };
});

Характеристика подхода:

  • высокая гибкость
  • низкая производительность на больших данных
  • подходит для предварительной обработки

Использование spatial indexing

При увеличении объёма данных линейный перебор становится неэффективным. Используются структуры:

  • R-tree
  • Quadtree
  • H3 (hex indexing)
  • geohash

Пример с 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)
  );
}

GPU-ускоренные spatial joins

Deck.gl позволяет переносить часть логики в GPU, особенно при работе с визуальными слоями.

Используются:

  • GPUGridLayer
  • HexagonLayer
  • ScreenGridLayer
  • кастомные CompositeLayer с 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 как основа пространственных соединений

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 даёт:

  • константное время lookup
  • масштабируемость до миллионов объектов
  • удобную агрегацию

Spatial joins внутри Deck.gl CompositeLayer

При построении сложных визуализаций 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 joins и фильтрация данных

Spatial join часто комбинируется с фильтрацией:

  • временной (time filtering)
  • пространственной (viewport filtering)
  • атрибутивной (value filtering)
const filtered = points
  .filter(p => p.value > 10)
  .filter(p => p.position[0] > 71.4);

В Deck.gl фильтрация может выполняться на GPU через DataFilterExtension, что снижает нагрузку на CPU.


Сложные joins: distance-based matching

Помимо вложенности и пересечений часто используется расстояние.

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;
}

Этот подход применяется:

  • при кластеризации событий
  • при привязке объектов к ближайшим зонам
  • при анализе покрытия

Производительность spatial joins

Ключевые ограничения:

  • O(n*m) при наивной реализации
  • стоимость пересечения сложных полигонов
  • передача данных между CPU и GPU

Оптимизации:

  • предварительная индексация (R-tree, H3)
  • упрощение геометрии (Douglas-Peucker)
  • батчинг вычислений
  • использование Web Workers
  • перенос агрегации в GPU

Spatial joins в потоковой визуализации

При работе с потоками данных (real-time feeds):

  • новые точки поступают непрерывно
  • join выполняется инкрементально
  • структура индекса обновляется динамически

Пример:

function addPoint(streamPoint) {
  const cell = geoToH3(
    streamPoint.position[1],
    streamPoint.position[0],
    7
  );

  incrementCell(cell, streamPoint.value);
}

Такой подход позволяет:

  • избегать полного пересчёта
  • поддерживать актуальные агрегации
  • работать с высокой частотой обновлений

Комбинированные пространственные операции

В реальных приложениях spatial joins редко используются изолированно. Часто они комбинируются:

  • point-in-polygon + aggregation
  • distance join + clustering
  • grid indexing + filtering + extrusion

Пример цепочки:

points
  .map(assignH3)
  .filter(inViewport)
  .reduce(aggregateByCell, {})

Такие цепочки формируют основу аналитических слоёв Deck.gl, где визуализация является прямым отражением пространственных вычислений.