Difference операции

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

В экосистеме MapLibre GL JS difference-операции не входят в состав встроенного API рендеринга карты. Для выполнения геометрических вычислений обычно используются специализированные библиотеки пространственного анализа, наиболее распространённой из которых является Turf.js. После вычисления результата новая геометрия отображается на карте через стандартные источники данных и слои MapLibre.


Концепция геометрической разности

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

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

Операция Difference вычисляет:

A − B

Результатом становится часть полигона A, не пересекающаяся с полигоном B.

Графически процесс выглядит следующим образом:

До операции:

+-------------------+
|        A          |
|   +---------+     |
|   |    B    |     |
|   +---------+     |
+-------------------+

После Difference:

+-------------------+
|                   |
|    вырезанная     |
|      область      |
|                   |
+-------------------+

Внутренняя часть полигона B удаляется из полигона A.


Подключение Turf.js

Для выполнения Difference обычно подключается библиотека Turf.

Через CDN:

<script src="https://unpkg.com/@turf/turf@latest/turf.min.js"></script>

Либо через npm:

npm install @turf/turf

Импорт в проекте:

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

Простая операция Difference

Исходная карта:

const map = new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [37.62, 55.75],
    zoom: 9
});

Создание двух полигонов:

const polygonA = turf.polygon([
    [
        [37.4, 55.6],
        [37.9, 55.6],
        [37.9, 55.9],
        [37.4, 55.9],
        [37.4, 55.6]
    ]
]);

const polygonB = turf.polygon([
    [
        [37.55, 55.68],
        [37.75, 55.68],
        [37.75, 55.82],
        [37.55, 55.82],
        [37.55, 55.68]
    ]
]);

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

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

Полученная геометрия содержит полигон A без области пересечения с полигоном B.


Отображение результата на карте

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

map.on('load', () => {

    map.addSource('difference-area', {
        type: 'geojson',
        data: result
    });

    map.addLayer({
        id: 'difference-fill',
        type: 'fill',
        source: 'difference-area',
        paint: {
            'fill-color': '#0080ff',
            'fill-opacity': 0.5
        }
    });

});

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


Визуализация исходных объектов

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

Источник данных:

map.addSource('source-polygons', {
    type: 'geojson',
    data: {
        type: 'FeatureCollection',
        features: [polygonA, polygonB]
    }
});

Слой контура:

map.addLayer({
    id: 'polygon-borders',
    type: 'line',
    source: 'source-polygons',
    paint: {
        'line-color': '#ff0000',
        'line-width': 2
    }
});

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


Difference для сложных полигонов

Часто геометрия содержит десятки или сотни вершин.

Пример полигона сложной формы:

const region = turf.polygon([
    [
        [30, 10],
        [40, 40],
        [20, 45],
        [10, 20],
        [30, 10]
    ]
]);

Полигон исключения:

const exclusionZone = turf.polygon([
    [
        [20, 20],
        [35, 20],
        [35, 35],
        [20, 35],
        [20, 20]
    ]
]);

Вычисление:

const result = turf.difference(
    turf.featureCollection([
        region,
        exclusionZone
    ])
);

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


Формирование полигонов с отверстиями

Difference часто приводит к появлению внутренних пустот.

Например:

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

const inner = turf.polygon([
    [
        [3, 3],
        [7, 3],
        [7, 7],
        [3, 7],
        [3, 3]
    ]
]);

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

const result = turf.difference(
    turf.featureCollection([
        outer,
        inner
    ])
);

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

MapLibre GL JS корректно отображает такие объекты без дополнительной настройки.


Работа с MultiPolygon

Результатом Difference может быть не только Polygon, но и MultiPolygon.

Пример:

console.log(result.geometry.type);

Возможный вывод:

MultiPolygon

Это происходит, если вырезаемая область разделяет исходный полигон на несколько независимых частей.

Например:

Исходный объект

██████████████

После вычитания

██████  ██████

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

MapLibre автоматически обрабатывает такой результат.


Проверка наличия пересечения

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

const intersects = turf.booleanIntersects(
    polygonA,
    polygonB
);

Если результат равен:

true

можно выполнять Difference.

Пример:

if (intersects) {

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

}

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


Обработка пустого результата

Иногда один полигон полностью покрывает другой.

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

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

null

Поэтому рекомендуется выполнять проверку:

if (result) {

    map.getSource('difference-area')
        .setData(result);

}

Или:

if (result === null) {
    console.log('Геометрия полностью удалена');
}

Динамическое обновление Difference

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

Функция обновления:

function updateDifference(a, b) {

    const result = turf.difference(
        turf.featureCollection([a, b])
    );

    if (result) {
        map.getSource('difference-area')
            .setData(result);
    }

}

Вызов:

updateDifference(polygonA, polygonB);

Подобный подход применяется в интерактивных редакторах карт.


Использование вместе с инструментами рисования

После создания полигона пользователем можно вырезать запрещённые зоны.

Пример сценария:

  1. Пользователь рисует участок.
  2. Загружается слой ограничений.
  3. Выполняется Difference.
  4. Отображается разрешённая территория.

Схема вычислений:

const allowedArea = turf.difference(
    turf.featureCollection([
        userPolygon,
        restrictedZone
    ])
);

Такой механизм часто используется в кадастровых системах, логистике и городском планировании.


Исключение водоёмов из территории

Распространённая задача геоаналитики — получение площади суши без внутренних водных объектов.

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

const landPolygon = ...;
const lakesPolygon = ...;

Вычисление:

const dryLand = turf.difference(
    turf.featureCollection([
        landPolygon,
        lakesPolygon
    ])
);

Полученный объект содержит только сухопутную территорию.


Исключение зон покрытия

Difference активно применяется при анализе покрытия сетей связи.

Например:

const coverageArea = ...;
const outageArea = ...;

Расчёт доступной сети:

const activeCoverage = turf.difference(
    turf.featureCollection([
        coverageArea,
        outageArea
    ])
);

На карте отображается только реально функционирующая зона обслуживания.


Difference нескольких объектов

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

Исходные области:

const zone1 = ...;
const zone2 = ...;
const zone3 = ...;

Вычисления:

let result = turf.difference(
    turf.featureCollection([
        mainPolygon,
        zone1
    ])
);

result = turf.difference(
    turf.featureCollection([
        result,
        zone2
    ])
);

result = turf.difference(
    turf.featureCollection([
        result,
        zone3
    ])
);

После каждого шага площадь становится меньше.


Оптимизация производительности

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

Основные причины:

  • поиск пересечений рёбер;
  • построение новых контуров;
  • создание дополнительных вершин;
  • обработка отверстий;
  • работа с MultiPolygon.

Практические рекомендации:

Упрощать геометрию перед вычислениями

const simplified = turf.simplify(polygon, {
    tolerance: 0.0005,
    highQuality: false
});

Проверять пересечение заранее

turf.booleanIntersects(a, b);

Избегать повторных вычислений

cache[id] = result;

Переносить тяжёлые расчёты в Web Worker

worker.postMessage({
    polygonA,
    polygonB
});

Это предотвращает блокировку интерфейса карты.


Типичные проблемы

Невалидная геометрия

Самопересекающиеся полигоны способны привести к ошибкам.

Проверка:

turf.booleanValid(polygon);

Нарушенный порядок координат

Формат GeoJSON требует порядка:

[longitude, latitude]

Правильно:

[37.62, 55.75]

Неправильно:

[55.75, 37.62]

Отсутствие результата

Если полигоны не пересекаются:

const intersects =
    turf.booleanIntersects(a, b);

Difference может вернуть исходную геометрию без изменений либо не дать ожидаемого эффекта, поэтому логика проверки пересечений должна присутствовать всегда.


Практический пример: вырезание запрещённой зоны

Создание основной территории:

const territory = turf.circle(
    [37.62, 55.75],
    10,
    { units: 'kilometers' }
);

Создание зоны ограничения:

const restriction = turf.circle(
    [37.67, 55.75],
    3,
    { units: 'kilometers' }
);

Вычисление:

const availableArea = turf.difference(
    turf.featureCollection([
        territory,
        restriction
    ])
);

Отображение:

map.addSource('available-area', {
    type: 'geojson',
    data: availableArea
});

map.addLayer({
    id: 'available-fill',
    type: 'fill',
    source: 'available-area',
    paint: {
        'fill-color': '#00aa44',
        'fill-opacity': 0.6
    }
});

В результате отображается круговая территория с автоматически вырезанным внутренним сектором ограничения, сформированным на основе Difference-операции.