Difference-операции представляют собой механизм вычисления геометрической разности между пространственными объектами. В результате выполнения такой операции из одной геометрии вырезается область пересечения с другой геометрией. Подобные вычисления широко применяются в геоинформационных системах для анализа территорий, подготовки тематических карт, обработки зон покрытия, исключения перекрывающихся областей и построения сложных геометрических фигур.
В экосистеме MapLibre GL JS difference-операции не входят в состав встроенного API рендеринга карты. Для выполнения геометрических вычислений обычно используются специализированные библиотеки пространственного анализа, наиболее распространённой из которых является Turf.js. После вычисления результата новая геометрия отображается на карте через стандартные источники данных и слои MapLibre.
Пусть существуют два полигона:
Операция Difference вычисляет:
A − B
Результатом становится часть полигона A, не пересекающаяся с полигоном B.
Графически процесс выглядит следующим образом:
До операции:
+-------------------+
| A |
| +---------+ |
| | B | |
| +---------+ |
+-------------------+
После Difference:
+-------------------+
| |
| вырезанная |
| область |
| |
+-------------------+
Внутренняя часть полигона B удаляется из полигона A.
Для выполнения 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';
Исходная карта:
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
}
});
Теперь можно видеть как исходные объекты, так и результат вычислений.
Часто геометрия содержит десятки или сотни вершин.
Пример полигона сложной формы:
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 корректно отображает такие объекты без дополнительной настройки.
Результатом 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('Геометрия полностью удалена');
}
При изменении данных можно пересчитывать результат в режиме реального времени.
Функция обновления:
function updateDifference(a, b) {
const result = turf.difference(
turf.featureCollection([a, b])
);
if (result) {
map.getSource('difference-area')
.setData(result);
}
}
Вызов:
updateDifference(polygonA, polygonB);
Подобный подход применяется в интерактивных редакторах карт.
После создания полигона пользователем можно вырезать запрещённые зоны.
Пример сценария:
Схема вычислений:
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
])
);
На карте отображается только реально функционирующая зона обслуживания.
При необходимости вырезать несколько зон операция выполняется последовательно.
Исходные области:
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 относится к вычислительно затратным операциям.
Основные причины:
Практические рекомендации:
Упрощать геометрию перед вычислениями
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-операции.