Введение

Привет! Если тебе пришлось на проекте столкнуться с геоданными, но ты ещё не знаком с этой областью или ищешь короткое введение в работу с ними — эта статья для тебя.

Я не являюсь экспертом в ГИС (геоинформациооных системах); эта статья подводит итог тому, что я сам узнал для базовой работы с геоданными.

Подготовоительная информация

Эллипсоид — математическая модель поверхности Земли

Для расчетов нам нужна некоторая поверхность, на которой мы будем отмечать точки. Математической моделью поверхности Земли (геоида) выступают эллипсоиды.
Суть в том, чтобы выбрать эллипсоид, максимально «обтекающий» реальный геоид и сводящий отклонения от его поверхности к минимуму.

Эллипсоид определяется следующими параметрами:

  • Большая (экваториальная) полуось
  • Малая (полярная) полуось

Из этих параметров можно вывести еще один — коэффициент сжатия: отношение разности большой и малой полуосей к большой полуоси.

Координаты и Coordinate Reference System (CRS)

Точки в пространстве можно задать некоторыми координатами. Они бывают:

  • Эллипсоидальными (географическими/геодезическими) — задаются угловыми величинами lat, lon, отражают положение точек на эллипсоиде. Выражаются в градусах или радианах.
  • Прямоугольными (спроецированными/метрическими) — задаются координатами x, y на плоскости (картографической проекции), выражаются в метрах или футах.

Чтобы координаты точно отражали положение точек на заданной поверхности, их необходимо «привязать» к ней с помощью системы координат.
CRS (Coordinate Reference System) — это система координат, которая однозначно задаёт, как числа (координаты) интерпретировать как реальные точки на Земле, а также определяет единицы измерения.
Координаты из разных систем отражают разные точки в пространстве.

WGS 84

WGS 84 — международный стандарт, определяющий параметры эллипсоида, датум (набор геопараметров) и эллипсоидальную систему координат.
В геоинформационных системах именно WGS 84 используется как «система координат по умолчанию», если не выбрана более специализированная.
На эту тему есть доклад FOSS4GE 2024 | WGS 84: I don’t know, I don’t care, в котором докладчик объясняет, что такое WGS 84, с какой точностью можно проводить расчеты с его помощью и на какие нюансы при работе с ним часто не обращают внимания.

SRID

SRID (Spatial Reference System Identifier) — это числовой идентификатор, указывающий на конкретную систему координат и проекцию, включая информацию об эллипсоиде, датуме и единицах измерения.
Например, SRID WGS 84 в EPSG (реестре геодезических данных) — 4326.
Согласно реестру, для WGS 84 принята двумерная эллипсоидальная система координат с градусом в качестве единицы измерения.

Simple Features Standard

SFS/SFSQL (Simple Features Standard/Simple Features for SQL) — это стандарт, разработанный Open Geospatial Consortium (OGC), который определяет:

  • Набор геометрических типов (Point, LineString, Polygon, MultiPoint и т. д.)
  • Форматы хранения и обмена (WKT — Well-Known Text, WKB — Well-Known Binary)
  • Набор пространственных операций и функций (ST_Contains, ST_Intersects, ST_Buffer и т. д.)

Ознакомиться можно по ссылке.
В следующих разделах статьи мы увидим, как этот стандарт применяется.

Well-known text (WKT)

WKT — текстовый формат для описания геоданных.
Пример: POLYGON((30 10, 40 40, 20 40, 10 20, 30 10))
Примеры других географий с картинками есть на википедии.
Удобный инструмент для визуализации WKT на карте https://wktmap.com/

GeoJSON

О спецификации

GeoJSON — это спецификация, которая задает правила передачи геопространственных данных в формате JSON и включает в себя 7 геометрических типов из SFSQL: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection.
Со спецификацией можно ознакомиться по ссылке.

Общая структура объектов GeoJSON

GeoJSON может передавать следующие типы:

  1. Geometry — определенное место в пространстве
  2. Feature — место в пространстве (Geometry) с набором свойств (произвольные key-value пары)
  3. FeatureCollection — набор Feature

Чтобы парсер понимал, с каким типом объекта он работает, GeoJSON-объекты обязательно содержат поле type.

{
  "type": "FeatureCollection",               // FeatureCollection: коллекция Feature
  "features": [                              // массив Feature-объектов
    {                                        // Feature #1
      "type": "Feature",                     // Тип объекта — Feature
      "properties": {                        // Свойства Feature
        "name": "Точка интереса"
      },
      "geometry": {                          // Geometry вложена в Feature
        "type": "Point",                     // Тип геометрии — Point
        "coordinates": [37.6173, 55.7520]    // Координаты точки (Position)
      }
    },
    {                                        // Feature #2
      "type": "Feature",
      "properties": {
        "name": "Улица"
      },
      "geometry": {                          // Geometry вложена в Feature
        "type": "LineString",                // Тип геометрии — LineString
        "coordinates": [                     // Массив Position, задающий ломаную линию
          [37.6173, 55.7520],
          [37.6200, 55.7550],
          [37.6225, 55.7540]
        ]
      }
    }
  ]
}

Geometry

Geometry — это базовый объект, описывающий форму и положение пространственного элемента. Все объекты Geometry имеют два обязательных поля:

  1. type — строка, задающая тип геометрии. Стандартные значения:

    • Point
    • MultiPoint
    • LineString
    • MultiLineString
    • Polygon
    • MultiPolygon
    • GeometryCollection
  2. coordinates — Массив чисел или вложенных массивов (в спецификации они называются Position), описывающий координаты в 2D или 3D.

Вот пример объекта типа Polygon

{
  "type": "Polygon",
  "coordinates": [
    [
      [30.0, 10.0],
      [40.0, 40.0],
      [20.0, 40.0],
      [30.0, 10.0]
    ]
  ]
}

Position

Позиция — это набор координат в системе WGS 84.

Координаты точки составляют позицию.
Набор позиций составляет более сложные сущности вроде LineString или MultiPoint
Из этих объектов могут строиться еще более сложные сущности вроде Polygon и MultiPolygon.

Типы Geometry

Название геометрии Что из себя представляет Какой набор позиций содержит Примеры из реальной жизни
Point Одна отдельная точка Одна Position Отметка местоположения кафе или дерева на карте
MultiPoint Набор независимых точек Массив Position Расположение всех автобусных остановок в городе
LineString Ломанная линия, соединяющая точки последовательно Упорядоченный массив Position Маршрут дороги или пешеходная тропа
MultiLineString Коллекция нескольких ломаных линий Массив упорядоченных массивов Position Сеть железнодорожных путей или электроснабжения
Polygon Замкнутый контур (многоугольник), может содержать «дырки» Массив колец: каждый — массив Position (первое и последнее совпадают) Граница города, форма озера или парка
MultiPolygon Набор нескольких многоугольников Массив массивов колец: массивы массивов Position Архипелаг островов или разбросанные парки
GeometryCollection Коллекция различных объектов Geometry Не имеет coordinates; содержит массив объектов Geometry в geometries Смешанные объекты: здание (Polygon), входы (Point) и подходящие дорожки (LineString)

Изображения

Point GeoJSON

Скриншот карты OpenStreetMap с геометрией Point
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

MultiPoint GeoJSON

Скриншот карты OpenStreetMap с геометрией MultiPoint
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

LineString GeoJSON

Скриншот карты OpenStreetMap с геометрией LineString
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

MultiLineString GeoJSON

Скриншот карты OpenStreetMap с геометрией MultiLineString
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

Polygon GeoJSON

Скриншот карты OpenStreetMap с геометрией Polygon
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

MultiPolygon GeoJSON

Скриншот карты OpenStreetMap с геометрией MultiPolygon
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

GeometryCollection GeoJSON (другой объект)

Скриншот карты OpenStreetMap с геометрией GeometryCollection
© Mapbox © OpenStreetMap contributors / Copyright / ODbL v1.0

Отображение GeoJSON на карте

Отображать и редактировать GeoJSON на карте можно с помощью сервиса geojson.io

Java

JTS

JTS Topology Suite — библиотека для работы с объектами из SFSQL в Java.
Библиотека предоставляет не только структуру объектов, но и операции над ними: расчет расстояния, площадей, пространственных предикатов (покрытие/пересечения и т. д.). (ссылка на документацию).

JTS оперирует плоской (евклидовой) геометрией, поэтому входные геоданные нужно заранее спроецировать в плоскую систему координат.
Для проекций можно воспользоваться библиотеками org.locationtech.proj4j:proj4j и org.locationtech.proj4j:proj4j-epsg

JTS & GeoJSON

Пакет org.locationtech.jts.io:jts-io-common предоставляет классы для сериализации/десериализации объектов GeoJSON и JTS.
Пример использования:

public class Main {
    public static void main(String[] args) {
        String geoJson = """
            {
              "type": "Feature",
              "geometry": {
                "type": "Point",
                "coordinates": [74.59, 42.87]
              },
              "properties": {
                "name": "Bishkek"
              }
            }
            """;

        GeoJsonReader reader = new GeoJsonReader();
        try {
            // Парсим GeoJSON → JTS Geometry
            Geometry geometry = reader.read(geoJson);
            System.out.println("Parsed JTS geometry: " + geometry);
            System.out.println(geometry.getSRID());
        } catch (ParseException e) {
            // Обрабатываем checked-исключение...
        }
    }
}

Output

Parsed JTS geometry: POINT (74.59 42.87)
4326 // WGS 84 — SRID, используемый в GeoJSON.

JTS & Hibernate Spatial

Hibernate Spatial позволяет использовать объекты JTS для хранения геоданных в СУБД. Помимо прямой поддержки JTS-объектов в полях сущностей (Entity-классов), он обеспечивает интеграцию с пространственными диалектами (например, PostGIS) и набором готовых пространственных функций.
Ознакомиться с документацией можно по ссылке.

PostgreSQL и PostGIS

PostGIS — это расширение для PostgreSQL, реализующее поддержку объектов геометрии и географии по спецификации OGC Simple Features (SFS) и предоставляющее набор функций для работы с этими объектами.
Полный список поддерживаемых типов геометрий доступен по ссылке.

Пространственные функции PostGIS

В PostGIS доступен набор функций для проверки пространственных отношений — вхождения, пересечения, равенства и др.
Описание с наглядными примерами и SQL-запросами можно найти в документации.

Geometry vs Geography

PostGIS позволяет работать как с плоской геометрией в произвольных проекционных CRS (тип geometry), так и с геодезическими координатами на эллипсоидах (тип geography).

Параметр Geometry Geography
Модель пространства Плоская Эллипсоид
Кратчайшее расстояние между двумя точками Прямая Дуга на эллипсоиде
Система координат Любой SRID, включая локальные CRS Только эллипсоидальные
Производительность Быстрее: вычислления на плоскости выполняются быстро Медленнее: расчёты по эллипсоиду дороже по ресурсам.
Юзкейсы Локальные и региональные задачи в спроецированных CRS. Глобальные данные в широте/долготе.

Расстояние между двумя точками

Для демонстрации разницы между geography и geometry посчитаем расстояние по прямой между Бишкеком (lat 42.87, lon 74.59) и Караколом (lat 42.49, lon 78.39). Согласно калькулятору, расстояние между точками 315 км.

Geography

Посчитаем расстояние на эллипсоиде WGS 84

SELECT
  ST_Distance(
    ST_SetSRID(ST_MakePoint(74.5698, 42.8746), 4326)::geography,
    ST_SetSRID(ST_MakePoint(78.3933, 42.4986), 4326)::geography
  ) AS distance_meters;

Output

distance_meters
316094.80038898

Получили 316 км.

Geometry

А теперь посмотрим на результат на плоскости.
Спроецируем WGS 84 в WGS 84 / Pseudo-Mercator — Spherical Mercator (EPSG:3857)

SELECT
  ST_Distance(
    ST_Transform(
      ST_SetSRID(ST_MakePoint(74.59, 42.87), 4326), -- 4326 — SRID WGS 84
      3857                                          -- 3857 — SRID WGS 84 / Pseudo-Mercator — Spherical Mercator 
    ),
    ST_Transform(
      ST_SetSRID(ST_MakePoint(78.39, 42.49), 4326),
      3857
    )
  ) AS distance_meters;

Output

distance_meters
426909.7201568989

Получили отклонение на 110 км.
Чтобы получить более точные результаты, нужно подобрать подходящую CRS.
Для Кыргызстана такой является WGS 84 / UTM zone 43N (EPSG:32643)
Посчитаем в новой проекции

SELECT
  ST_Distance(
    ST_Transform(
      ST_SetSRID(ST_MakePoint(74.59, 42.87), 4326),
      32643
    ),
    ST_Transform(
      ST_SetSRID(ST_MakePoint(78.39, 42.49), 4326),
      32643
    )
  ) AS distance_meters;

Output

distance_meters
314242.10025156994

Получили корректный результат — 314 км.

Индексы

PostgreSQL умеет индексировать пространственные данные.
Подробное описание алгоритмов и внутренних структур индексов выходит за рамки этой статьи: в интернете уже есть качественные материалы на эту тему.
Вот некоторые из них:
Доклад Павла Кислова — Павел рассказывает как устроены пространственные индексы, когда их лучше использовать, приводит бенчмарки.
PostGIS intro workshop — как устроены пространственные индексы; как функции PostGIS работают с этими индексами.

Пример использования

Представим, что у нас есть 3 магазина и 2 службы доставки, которые работают в определенных областях.
Отметим их на карте:

Скриншот карты OpenStreetMap с магазинами и областями доставки
Данные карт: © OpenStreetMap contributors / Copyright / ODbL v1.0
Векторные слои: © GeoData Show.

Создадим и наполним таблицы

CREATE TABLE shop (
  id   SERIAL PRIMARY KEY,
  name TEXT   NOT NULL,
  geom GEOMETRY(Point, 4326) NOT NULL
);

CREATE TABLE delivery_area (
  id      SERIAL    PRIMARY KEY,
  service TEXT      NOT NULL,
  area    GEOMETRY(Polygon, 4326) NOT NULL
);

INSERT INTO shop (name, geom) VALUES
  ('Магазин на Льва Толстого',  ST_SetSRID(ST_MakePoint(74.595526, 42.863348), 4326)),
  ('Магазин на Киевской',       ST_SetSRID(ST_MakePoint(74.593933, 42.875174), 4326)),
  ('Магазин на Московской',     ST_SetSRID(ST_MakePoint(74.615718, 42.869289), 4326));


INSERT INTO delivery_area (service, area) VALUES
  (
    'ProКурьер',
    ST_SetSRID(
      ST_GeomFromText(
        'POLYGON((
          74.583778 42.859294,
          74.604979 42.859294,
          74.604979 42.878166,
          74.583778 42.878166,
          74.583778 42.859294
        ))'
      ),
      4326
    )
  ),
  (
    'БыстраяДоставка',
    ST_SetSRID(
      ST_GeomFromText(
        'POLYGON((
          74.578457 42.867032,
          74.623518 42.867032,
          74.623518 42.880115,
          74.578457 42.880115,
          74.578457 42.867032
        ))'
      ),
      4326
    )
  );

Допустим, на бэкэнд пришли координаты пользователя POINT(74.600794 42.872347), и нам нужно отобразить ближайшие к нему магазины.

Скриншот карты OpenStreetMap с магазинами и областями доставки
Данные карт: © OpenStreetMap contributors / Copyright / ODbL v1.0
Векторные слои: © GeoData Show.

Запросим ближайшие магазины

SELECT
  id,
  name,
  ST_Distance(
    geom::geography,
    ST_SetSRID(ST_MakePoint(74.600794, 42.872347), 4326)::geography
  ) AS dist_m
FROM shop
ORDER BY dist_m
LIMIT 3;

Output

id name dist_m
2 Магазин на Киевской 642.57083016
1 Магазин на Льва Толстого 1088.44452992
3 Магазин на Московской 1265.89776504

А теперь проверим, какие курьерские службы могут осуществить доставку клиенту

SELECT
  id, service
FROM delivery_area
WHERE ST_Contains(
  area,
  ST_SetSRID(ST_MakePoint(74.600794, 42.872347), 4326)
);

Output

id service
1 ProКурьер
2 БыстраяДоставка

Итог

В этой статье мы рассмотрели:

  1. Эллипсоид как модель Земли для географических расчетов и конкретную эллипсоидальную систему координат WGS 84.
  2. CRS и SRID — как однозначно задать систему координат на поверхности (плоскости/эллипсоиде).
  3. Стандарт Simple Features (SFA/SFS), который задаёт набор геометрий и операций над ними.
  4. GeoJSON как формат для передачи геоданных в JSON.
  5. Основные геометрии SFA (Point, LineString, Polygon и т. д.) на примере GeoJSON
  6. Как работать с SFS-объектами в Java с помощью JTS, как получить JTS объекты из GeoJSON, как научить Hibernate работать с этими объектами.
  7. Возможности PostGIS и различие между типами geometry (плоскость) и geography (эллипсоид) на примере вычисления реального расстояния между двумя точками.

Спасибо за внимание! Подписывайтесь на мой Telegram канал чтобы не пропустить новые статьи.