Разность полигонов

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

Операция широко применяется в геоинформационных системах:

  • исключение запрещённых зон из территории;
  • удаление водоёмов из земельных участков;
  • формирование охранных зон;
  • расчёт оставшейся площади после пересечений;
  • подготовка картографических данных для визуализации.

В Turf.js данная операция реализована функцией difference.


Математическая сущность операции

Пусть существуют два полигона:

  • Полигон A — исходная область.
  • Полигон B — область, которую необходимо удалить.

Тогда результат вычисляется следующим образом:

A − B

В итоговую геометрию попадают все точки полигона A, которые не принадлежат полигону B.

Возможны несколько сценариев:

Частичное пересечение

A = большой прямоугольник
B = круг внутри него

После вычитания внутри прямоугольника появится отверстие.

Полное перекрытие

Если B полностью покрывает A:

A ⊂ B

Результат отсутствует.

Функция возвращает:

null

Отсутствие пересечения

Если полигоны не пересекаются, результат совпадает с исходным полигоном A.


Подключение функции

Современный вариант импорта:

import { difference } from "@turf/turf";

Импорт отдельного пакета:

import difference from "@turf/difference";

Создание исходных полигонов

Рассмотрим простой пример.

import * as turf from "@turf/turf";

const polygon1 = turf.polygon([
    [
        [0, 0],
        [10, 0],
        [10, 10],
        [0, 10],
        [0, 0]
    ]
]);

const polygon2 = turf.polygon([
    [
        [5, 5],
        [15, 5],
        [15, 15],
        [5, 15],
        [5, 5]
    ]
]);

Первый полигон представляет квадрат 10×10.

Второй квадрат перекрывает его правую верхнюю часть.


Выполнение разности

Для вычисления результата используется функция:

const result = turf.difference(
    turf.featureCollection([
        polygon1,
        polygon2
    ])
);

Результат:

{
    type: "Feature",
    geometry: {
        type: "Polygon",
        ...
    }
}

Полученная геометрия содержит область первого квадрата без перекрывающейся части второго.


Почему используется FeatureCollection

В новых версиях Turf.js операция принимает коллекцию объектов.

Пример:

const result = turf.difference(
    turf.featureCollection([
        polygonA,
        polygonB
    ])
);

Первый объект считается базовым.

Все последующие объекты последовательно вычитаются из него.

Логически операция выглядит так:

(((A - B) - C) - D)

Вычитание нескольких полигонов

Допустим, имеется земельный участок и несколько зон, которые необходимо исключить.

const area = turf.polygon([
    [
        [0, 0],
        [20, 0],
        [20, 20],
        [0, 20],
        [0, 0]
    ]
]);

const lake = turf.polygon([
    [
        [3, 3],
        [8, 3],
        [8, 8],
        [3, 8],
        [3, 3]
    ]
]);

const forest = turf.polygon([
    [
        [12, 12],
        [18, 12],
        [18, 18],
        [12, 18],
        [12, 12]
    ]
]);

const industrialZone = turf.polygon([
    [
        [5, 12],
        [10, 12],
        [10, 18],
        [5, 18],
        [5, 12]
    ]
]);

Вычисление:

const result = turf.difference(
    turf.featureCollection([
        area,
        lake,
        forest,
        industrialZone
    ])
);

В результате получится участок без трёх исключённых областей.


Образование отверстий

Одним из наиболее распространённых результатов операции является появление внутренних колец.

Исходный полигон:

const territory = turf.polygon([
    [
        [0, 0],
        [20, 0],
        [20, 20],
        [0, 20],
        [0, 0]
    ]
]);

Вычитаемый полигон полностью находится внутри:

const restrictedZone = turf.polygon([
    [
        [5, 5],
        [15, 5],
        [15, 15],
        [5, 15],
        [5, 5]
    ]
]);

Разность:

const result = turf.difference(
    turf.featureCollection([
        territory,
        restrictedZone
    ])
);

Геометрия результата будет содержать:

{
    type: "Polygon",
    coordinates: [
        [...внешний контур...],
        [...внутреннее кольцо...]
    ]
}

Второй массив координат представляет отверстие.


Получение MultiPolygon

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

Пример:

const base = turf.polygon([
    [
        [0, 0],
        [20, 0],
        [20, 10],
        [0, 10],
        [0, 0]
    ]
]);

const cutter = turf.polygon([
    [
        [8, -1],
        [12, -1],
        [12, 11],
        [8, 11],
        [8, -1]
    ]
]);

Вычитаемая область полностью разрезает исходную фигуру.

После выполнения операции:

const result = turf.difference(
    turf.featureCollection([
        base,
        cutter
    ])
);

Результат может иметь тип:

MultiPolygon

Структура:

{
    type: "MultiPolygon",
    coordinates: [...]
}

Каждая часть представляет отдельный полигон.


Проверка результата

Перед дальнейшей обработкой желательно убедиться, что результат существует.

const result = turf.difference(
    turf.featureCollection([
        polygon1,
        polygon2
    ])
);

if (result) {
    console.log("Разность вычислена");
}

Если первый полигон полностью исчезнет:

if (result === null) {
    console.log("Объект полностью удалён");
}

Работа с площадью после вычитания

Очень часто операция используется совместно с функцией вычисления площади.

const result = turf.difference(
    turf.featureCollection([
        polygon1,
        polygon2
    ])
);

const area = turf.area(result);

console.log(area);

Так можно определить площадь территории после исключения определённых зон.


Исключение водоёмов из земельного участка

Практический пример.

const land = turf.polygon([...]);
const river = turf.polygon([...]);
const lake = turf.polygon([...]);

Удаление водных объектов:

const cleanLand = turf.difference(
    turf.featureCollection([
        land,
        river,
        lake
    ])
);

После этого:

const usefulArea = turf.area(cleanLand);

Будет получена площадь участка без водоёмов.


Исключение буферных зон

Часто требуется удалить область вокруг объекта.

Сначала создаётся буфер:

const road = turf.lineString([
    [0, 0],
    [10, 0]
]);

const buffer = turf.buffer(road, 1, {
    units: "kilometers"
});

Затем буфер вычитается:

const result = turf.difference(
    turf.featureCollection([
        territory,
        buffer
    ])
);

Так формируется территория без зоны влияния дороги.


Комбинация с intersect

Нередко операция пересечения используется перед вычитанием.

Например, необходимо убедиться, что объекты действительно пересекаются.

const overlap = turf.intersect(
    turf.featureCollection([
        polygon1,
        polygon2
    ])
);

Если пересечение существует:

if (overlap) {
    const result = turf.difference(
        turf.featureCollection([
            polygon1,
            polygon2
        ])
    );
}

Такой подход позволяет избегать лишних вычислений в сложных системах.


Комбинация с union

Иногда несколько объектов предварительно объединяют.

const united = turf.union(
    turf.featureCollection([
        zone1,
        zone2,
        zone3
    ])
);

После этого выполняют единственную операцию вычитания:

const result = turf.difference(
    turf.featureCollection([
        territory,
        united
    ])
);

Подобная схема часто оказывается быстрее и проще для анализа.


Визуализация результата

Полученный объект является обычным GeoJSON.

Его можно напрямую отображать в картографических библиотеках.

Пример для Leaflet:

L.geoJSON(result).addTo(map);

Пример для MapLibre:

map.addSource("result", {
    type: "geojson",
    data: result
});

Поскольку результат может быть как Polygon, так и MultiPolygon, большинство современных картографических движков корректно обрабатывают оба варианта автоматически.


Возможные ошибки

Незамкнутый контур

Неверно:

[
    [0, 0],
    [10, 0],
    [10, 10],
    [0, 10]
]

Правильно:

[
    [0, 0],
    [10, 0],
    [10, 10],
    [0, 10],
    [0, 0]
]

Первая и последняя точки должны совпадать.


Самопересекающиеся полигоны

Проблемный пример:

[
    [0, 0],
    [10, 10],
    [0, 10],
    [10, 0],
    [0, 0]
]

Такой контур образует фигуру типа «бантик».

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


Полное уничтожение исходного полигона

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

const result = turf.difference(
    turf.featureCollection([
        smallPolygon,
        hugePolygon
    ])
);

Результат:

null

Этот случай необходимо обрабатывать отдельно.


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

Скорость выполнения операции зависит от:

  • количества вершин;
  • числа пересечений контуров;
  • количества полигонов;
  • наличия отверстий;
  • сложности геометрии.

Для больших наборов данных рекомендуется:

  • предварительно объединять вычитаемые области через union;
  • удалять лишние вершины;
  • выполнять пространственную фильтрацию перед вычислениями;
  • использовать упрощение геометрии через simplify.

Пример:

const optimized = turf.simplify(complexPolygon, {
    tolerance: 0.0001,
    highQuality: false
});

После упрощения операция разности выполняется заметно быстрее на сложных геоданных.


Практический сценарий: подготовка территории строительства

Имеются:

  • граница строительной площадки;
  • санитарная зона;
  • водоохранная зона;
  • существующие строения.

Исходные данные:

const constructionSite = turf.polygon([...]);

const sanitaryZone = turf.polygon([...]);

const waterProtection = turf.polygon([...]);

const buildings = turf.polygon([...]);

Вычисление пригодной территории:

const availableArea = turf.difference(
    turf.featureCollection([
        constructionSite,
        sanitaryZone,
        waterProtection,
        buildings
    ])
);

Определение площади:

const squareMeters =
    turf.area(availableArea);

Полученная геометрия представляет все участки, пригодные для строительства после исключения ограничивающих объектов.