Определение принадлежности точки полигону — одна из базовых геометрических операций в геоинформационных системах, картографических приложениях и сервисах пространственного анализа. Задача заключается в проверке, находится ли заданная точка внутри контура полигона, на его границе или за его пределами.
В экосистеме MapLibre GL JS такая операция часто требуется при:
Поскольку MapLibre GL JS отвечает главным образом за визуализацию карты, для геометрических вычислений обычно используются собственные алгоритмы или дополнительные библиотеки, работающие с форматом GeoJSON.
Имеется полигон:
const polygon = [
[30, 10],
[40, 40],
[20, 40],
[10, 20],
[30, 10]
];
И точка:
const point = [25, 25];
Необходимо определить:
Графически задача выглядит следующим образом:
*
/ \
/ \
/ • \
/ \
*---------*
где:
• представляет тестируемую точку.В большинстве случаев данные в MapLibre GL JS представлены в формате GeoJSON.
Полигон описывается объектом типа Polygon.
const polygonFeature = {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[
[30, 10],
[40, 40],
[20, 40],
[10, 20],
[30, 10]
]]
}
};
Точка:
const pointFeature = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [25, 25]
}
};
Координаты полигона всегда содержат массив колец.
Для простого полигона структура выглядит так:
coordinates: [
[
[x1, y1],
[x2, y2],
...
]
]
Первое кольцо является внешним контуром.
Самый распространённый способ проверки попадания точки внутрь полигона — алгоритм пересечения лучей (Ray Casting Algorithm).
Основная идея:
Пример:
*
/|
/ |
/ |
•------|-------->
\ |
\ |
\|
*
Луч начинается в тестируемой точке и продолжается бесконечно вправо.
Простейшая реализация:
function isPointInsidePolygon(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];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect =
((yi > y) !== (yj > y)) &&
(
x <
((xj - xi) * (y - yi)) /
(yj - yi) +
xi
);
if (intersect) {
inside = !inside;
}
}
return inside;
}
Использование:
const result = isPointInsidePolygon(
[25, 25],
polygon
);
console.log(result);
Результат:
true
Каждое ребро полигона проверяется на пересечение с горизонтальным лучом.
Условие:
(yi > y) !== (yj > y)
означает, что горизонтальная линия точки проходит между вершинами ребра.
Затем вычисляется координата пересечения:
((xj - xi) * (y - yi)) /
(yj - yi) +
xi
Если пересечение находится справа от точки:
x < intersectionX
счётчик пересечений изменяет состояние переменной:
inside = !inside;
Каждое пересечение переключает значение между:
false
и
true
Частый сценарий в MapLibre GL JS — определение области, по которой кликнул пользователь.
Получение координат клика:
map.on("click", (event) => {
const lng = event.lngLat.lng;
const lat = event.lngLat.lat;
console.log(lng, lat);
});
Проверка попадания в полигон:
map.on("click", (event) => {
const point = [
event.lngLat.lng,
event.lngLat.lat
];
const inside =
isPointInsidePolygon(
point,
polygon
);
if (inside) {
console.log("Внутри полигона");
} else {
console.log("Снаружи полигона");
}
});
Для производственных проектов предпочтительнее использовать готовые геометрические библиотеки.
Наиболее популярное решение — Turf.js.
Подключение:
<script src="https://unpkg.com/@turf/turf@latest/turf.min.js"></script>
Создание объектов:
const point = turf.point([25, 25]);
const polygon = turf.polygon([[
[30, 10],
[40, 40],
[20, 40],
[10, 20],
[30, 10]
]]);
Проверка:
const result =
turf.booleanPointInPolygon(
point,
polygon
);
console.log(result);
Результат:
true
Функция корректно обрабатывает:
Предположим, на карте уже существует источник:
map.addSource("regions", {
type: "geojson",
data: regionsGeoJSON
});
Получение объекта:
const source =
map.getSource("regions");
Если исходный GeoJSON хранится отдельно:
const feature =
regionsGeoJSON.features[0];
Проверка точки:
const inside =
turf.booleanPointInPolygon(
pointFeature,
feature
);
GeoJSON поддерживает внутренние кольца.
Пример:
{
type: "Polygon",
coordinates: [
[
[0,0],
[10,0],
[10,10],
[0,10],
[0,0]
],
[
[3,3],
[7,3],
[7,7],
[3,7],
[3,3]
]
]
}
Первое кольцо:
+----------+
| |
| |
| |
+----------+
Второе кольцо:
+----+
| |
+----+
представляет отверстие.
Точка внутри отверстия не считается принадлежащей полигону.
turf.booleanPointInPolygon(
point,
polygon
);
вернёт:
false
если точка находится внутри внутреннего кольца.
Некоторые объекты состоят из нескольких независимых частей.
Например:
GeoJSON:
{
type: "MultiPolygon",
coordinates: [
[
[
[0,0],
[10,0],
[10,10],
[0,10],
[0,0]
]
],
[
[
[20,20],
[30,20],
[30,30],
[20,30],
[20,20]
]
]
]
}
Turf автоматически проверяет все части:
const inside =
turf.booleanPointInPolygon(
point,
multiPolygon
);
Нередко требуется определить принадлежность большого количества объектов одной области.
Например:
const points = [
[20, 20],
[25, 30],
[80, 50],
[15, 10]
];
Фильтрация:
const result = points.filter(point =>
isPointInsidePolygon(
point,
polygon
)
);
Результат:
[
[20, 20],
[25, 30]
]
Если проверяется несколько тысяч точек, полный обход всех рёбер становится дорогой операцией.
Полезно предварительно вычислить ограничивающий прямоугольник.
function getBoundingBox(polygon) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
polygon.forEach(([x, y]) => {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
});
return {
minX,
minY,
maxX,
maxY
};
}
Проверка прямоугольника:
function insideBoundingBox(
point,
bbox
) {
const [x, y] = point;
return (
x >= bbox.minX &&
x <= bbox.maxX &&
y >= bbox.minY &&
y <= bbox.maxY
);
}
Использование:
const bbox =
getBoundingBox(polygon);
if (
insideBoundingBox(point, bbox)
) {
const inside =
isPointInsidePolygon(
point,
polygon
);
}
Такой подход существенно снижает количество дорогостоящих геометрических вычислений.
Если необходимо определить попадание в уже отображаемый полигон, можно использовать механизм выборки объектов карты.
map.on("click", (event) => {
const features =
map.queryRenderedFeatures(
event.point,
{
layers: ["regions-layer"]
}
);
if (features.length > 0) {
console.log(
"Полигон найден"
);
}
});
Преимущества метода:
Однако этот способ определяет попадание в визуализированный объект, а не выполняет универсальный геометрический анализ произвольных координат.
Отдельный случай — точка лежит непосредственно на ребре.
Например:
const point = [20, 40];
Такая точка совпадает с вершиной полигона.
В зависимости от реализации возможны разные результаты:
true
или
false
Для строгого контроля обычно сначала выполняется проверка принадлежности отрезку.
Пример проверки:
function pointOnSegment(
point,
start,
end
) {
const [px, py] = point;
const [x1, y1] = start;
const [x2, y2] = end;
const cross =
(py - y1) * (x2 - x1) -
(px - x1) * (y2 - y1);
if (Math.abs(cross) > 1e-10) {
return false;
}
const dot =
(px - x1) * (x2 - x1) +
(py - y1) * (y2 - y1);
if (dot < 0) {
return false;
}
const lengthSquared =
(x2 - x1) ** 2 +
(y2 - y1) ** 2;
return dot <= lengthSquared;
}
После проверки границы выполняется стандартный алгоритм определения внутренней области.
if (
turf.booleanPointInPolygon(
userPoint,
zone
)
) {
activateZone();
}
districts.features.find(feature =>
turf.booleanPointInPolygon(
point,
feature
)
);
const visibleObjects =
objects.filter(object =>
turf.booleanPointInPolygon(
turf.point(object.coords),
selectedPolygon
)
);
routePoints.forEach(point => {
const inside =
turf.booleanPointInPolygon(
turf.point(point),
protectedArea
);
if (inside) {
console.log(
"Вход в охраняемую зону"
);
}
});
map.on("click", event => {
const inside =
turf.booleanPointInPolygon(
turf.point([
event.lngLat.lng,
event.lngLat.lat
]),
selectedRegion
);
if (inside) {
showInformationPanel();
}
});
Определение вхождения точки в полигон является фундаментальной операцией пространственного анализа в MapLibre GL JS. На практике используются два основных подхода: собственная реализация алгоритма Ray Casting для максимального контроля над вычислениями и применение Turf.js для работы со сложными геометриями GeoJSON, включая полигоны с отверстиями и объекты типа MultiPolygon. В интерфейсах карт эта операция лежит в основе геозон, выбора объектов, пространственной фильтрации и обработки пользовательских взаимодействий с картой.