Работа с геоданными | MapGL | 2GIS Documentation
MapGL JS API

Работа с геоданными

При работе с геоданными вы можете столкнуться с тем, что их невозможно сразу отобразить на карте из-за их разнообразия, больших объёмов и не всегда оптимального представления. Часто необходимо выполнить предварительные расчёты или преобразования, например:

  • рассчитать границы расположения геоданных так, чтобы спозиционировать на них карту;
  • упростить геометрии, чтобы получить более плавные контуры и увеличить производительность отрисовки;
  • преобразовать полигон в точки или наоборот.

Карта 2ГИС сама по себе не содержит функциональности по обработке геоданных. Однако вы можете использовать сторонние библиотеки с открытым исходным кодом, чтобы восполнить этот пробел. Базовым обменным форматом, своеобразным lingua franca, между этими библиотеками и MapGL JS API выступает GeoJSON.

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

Формат GeoJSON (подмножество формата JSON) поддерживается картой напрямую. Геоданные в нём представлены в виде объектов Feature, каждый из которых содержит геометрию в поле geometry и, опционально, метаданные в поле properties.

{
   "type": "FeatureCollection",
   "features": [
      {
         "geometry": {
            "type": "LineString",
            "coordinates": [
               [100.0, 0.0],
               [101.0, 1.0]
            ],
         },
         "properties": {
            "title": "A sample line"
         }
      },
   ]
}

Подробнее о работе с этим форматом в карте вы можете узнать в примере Работа с GeoJSON.

Формат GeoJSON описывается стандартом RFC 7946.

Так как формат является подмножеством JSON, он не требует каких-либо дополнительных библиотек для кодирования/декодирования.

Well-known text представляет векторные геометрии в виде строки. Формат хранит только геометрии, без метаданных.

Примеры геометрий в данном формате:

POINT (30 10)
LINESTRING (30 10, 10 30, 40 40)
POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))
POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))

В вебе вы можете работать с данным форматом при помощи библиотеки wellknown.

const wktGeometries = [
    'POINT (30 10)',
    'LINESTRING (30 10, 10 30, 40 40)',
    'POLYGON ((40 10, 60 40, 30 40, 20 20, 40 10))',
    'POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))',
];

const geojsonGeometries = wktGeometries.map((wkt) => wellknown.parse(wkt));

Формат WKT описан в стандарте OGC Simple Feature Access.

Широко используется в API 2ГИС, например в Routing API и Places API (см. поля geometry).

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Display Wkt geometries with MapGL</title>
        <meta name="description" content="Display Wkt geometries with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script>
            // Workaround for wellknown module
            var module = {};
        </script>
        <script src="https://unpkg.com/wellknown@0.5.0/index.js"></script>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            var wellknown = module.exports;
            const geojson = turf.geometryCollection([
                'POINT (30 10)',
                'LINESTRING (30 10, 10 30, 40 40)',
                // 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))',
                'POLYGON ((40 10, 60 40, 30 40, 20 20, 40 10))',
                'POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))'
            ].map(wellknown.parse));

            const center = turf.center(geojson);
            const map = new mapgl.Map('container', {
                center: turf.getCoord(center),
                zoom: 3,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

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

let source = null;

shapefile
    .open('/geodata/mapgl/ne_110m_coastline.shp')
    .then(async (source) => {
        const features = [];
        let result = await source.read();
        while (result && !result.done) {
            features.push(result.value);
            result = await source.read();
        }
        source = new mapgl.GeoJsonSource(map, {
            data: { type: 'FeatureCollection', features },
        });
    })
    .catch((error) => console.error(error.stack));

Формат поддерживается компанией Esri и описан в стандарте ESRI Shapefile Technical Description.

В вебе вы можете работать с данным форматом при помощи библиотеки shapefile.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Display Shp geometries with MapGL</title>
        <meta name="description" content="Display Shp geometries with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }

            button {
                position: absolute;
                top: 10px;
                left: 10px;
                z-index: 2;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/shapefile@0.6"></script>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            shapefile.open('https://disk.2gis.com/digital-twin/naturalearth/ne_110m_coastline.shp')
                .then(async (source) => {
                    const features = [];
                    let result = await source.read();
                    while (result && !result.done) {
                        features.push(result.value);
                        result = await source.read();
                    }
                    new mapgl.GeoJsonSource(map, {
                        data: { type: 'FeatureCollection', features },
                        attributes: { foo: 'bar' }
                    });
                })
                .catch(error => console.error(error.stack));

            
            const map = new mapgl.Map('container', {
                center: [108.9219, -6.8670],
                zoom: 4,
                key: 'Your API access key',
            });
            
            map.on('styleload', () => {
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#FF9387',
                        width: 2
                    }
                });
            })
        </script>
    </body>
</html>

Для манипуляций с GeoJSON рекомендуется использовать библиотеку turf.js. Ниже описывается использование данной библиотеки в связке с MapGL JS API.


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

const line = turf.lineString([
    [34, 40],
    [36, 41],
    [41, 37],
    [48, 42],
    [42, 35],
]);
const bbox = turf.bbox(line); // [minX, minY, maxX, maxY]

Документация на метод: @turf/bbox

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

  • Спозиционировать карту так, чтобы все объекты поместились на экран при помощи метода map.fitBounds.

    map.fitBounds({ southWest: bbox.slice(0, 2), northEast: bbox.slice(2, 4) });
    
  • Запросить дополнительные данные к имеющимся. Например, имея полигоны муниципальных районов, запросить близлежащие объекты застройки.

Граничный прямоугольник является наиболее распространенным и быстрым способом определить границы нахождения объектов. Обратной стороной является то, что границы таким образом определяются достаточно грубо. Если нужна большая точность, воспользуйтесь выпуклой оболочкой.


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/bbox with MapGL</title>
        <meta name="description" content="Using @turf/bbox with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const line = turf.lineString([[34, 40], [36, 41], [41, 37], [48, 42], [42, 35]]);
            const bbox = turf.bbox(line);

            const geojson = turf.featureCollection([line, turf.bboxPolygon(bbox)]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Метод turf.convex принимает на вход объект или группу объектов и создаёт на их основе выпуклую оболочку.

const line = turf.lineString([
    [34, 40],
    [36, 41],
    [41, 37],
    [48, 42],
    [42, 35],
]);
const convexHull = turf.convex(line);

Документация на метод: @turf/convex

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

Выпуклая оболочка — более точный по сравнению с граничным прямоугольником метод определения границ объекта. Обратной стороной является большая вычислительная сложность данного метода.


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/convex with MapGL</title>
        <meta name="description" content="Using @turf/convex with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const line = turf.lineString([[34, 40], [36, 41], [41, 37], [48, 42], [42, 35]]);
            const convexHull = turf.convex(line);

            const geojson = turf.featureCollection([line, convexHull]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Метод turf.buffer позволяет пропорционально увеличить или уменьшить объект.

const line = turf.lineString([
    [34, 40],
    [36, 41],
    [41, 37],
    [48, 42],
    [42, 35],
]);
const buffer = turf.buffer(line, 40);

Документация на метод: @turf/buffer

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

  • Отобразить различного рода охранные зоны, полосы отчуждения и т.п.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/buffer and MapGL</title>
        <meta name="description" content="Using @turf/buffer and MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const line = turf.lineString([[34, 40], [36, 41], [41, 37], [48, 42], [42, 35]]);
            const buffer = turf.buffer(line, 40);

            const geojson = turf.featureCollection([line, buffer]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

  • Показать на карте места, равноудаленные от какого-либо объекта.

    Однако это работает хорошо только на малых расстояниях (до 50-100 м). При расчёте больших расстояний начинает сказываться тот факт, что turf.buffer рассчитывает удалённость чисто геометрически, а в действительности на кратчайшем геометрическом пути обычно содержатся препятствия: дома, реки, заборы, которые требуется обходить/объезжать. В этом случае нужно применять более точный и специализированный инструмент: изохроны.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Drawing buffers around points with @turf/buffer and MapGL</title>
        <meta name="description" content="Drawing buffers around points with @turf/buffer and MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const points = [
                [33.63825,34.917647],
                [33.638238,34.914459],
                [33.634526,34.912492],
                [33.628846,34.914821],
                [33.628997,34.917202],
                [33.625677,34.916229]
            ];
            const geojson = turf.featureCollection([]);
            for (const pt of points) {
                const feature = turf.point(pt);
                geojson.features.push(feature);
                geojson.features.push(turf.buffer(feature, 0.1));
            }

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 15.5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Методы turf.center, turf.centroid и turf.centerOfMass позволяют получить тот или иной центр объекта или группы объектов.

const polygon = turf.polygon([
    [
        [34, 40],
        [36, 41],
        [41, 37],
        [48, 42],
        [42, 35],
        [34, 40],
    ],
]);
const center = turf.center(polygon);
const centroid = turf.centroid(polygon);
const centerOfMass = turf.centerOfMass(polygon);

Документация на методы:

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

Есть 3 метода получения центра:

  • center получает центр граничного прямоугольника объекта или группы.
  • centroid вычисляет геометрический центр объекта или группы.
  • centerOfMass получает центр масс фигуры.

На правильных и симметричных полигонах между этими методами практически нет разницы. В случае невыпуклых полигонов или других полигонов неправильной формы лучше всего работает centerOfMass.

Эти методы удобны для расстановки подписей объектов.


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/[center,centroid,centerOfMass] and MapGL</title>
        <meta name="description" content="Using @turf/[center,centroid,centerOfMass] and MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const polygon = turf.polygon([[[34, 40], [36, 41], [41, 37], [48, 42], [42, 35], [34, 40]]]);
            const center = turf.center(polygon, { properties: { name: 'Center' } });
            const centroid = turf.centroid(polygon, { properties: { name: 'Centeroid' } });
            const centerOfMass = turf.centerOfMass(polygon, { properties: { name: 'Center of mass' } });

            const geojson = turf.featureCollection([polygon, center, centroid, centerOfMass]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 7.5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100,
                        textFont: 'Noto_Sans',
                        textFontSize: 12,
                        textField: ['get', 'name'],
                        textPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Метод turf.nearestPoint получает ближайшую точку из множества точек.

const line = turf.lineString([
    [34, 40],
    [36, 41],
    [41, 37],
    [48, 42],
    [42, 35],
]);
const target = turf.point([38, 38]);

const points = turf.explode(line);
const nearest = turf.nearestPoint(target, points);

Документация на метод: @turf/nearestPoint

Особенностью метода является работа только с множеством точек. Чтобы преобразовать произвольный объект в множество точек, воспользуйтесь методом @turf/explode.

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

Найти ближайший к заданному объект.


Метод turf.nearestPointOnLine получает ближайшую к заданной точку на линии или на группе линий.

const line = turf.lineString([
    [34, 40],
    [36, 41],
    [41, 37],
    [48, 42],
    [42, 35],
]);
const target = turf.point([38, 38], { name: 'Target' });

const nearestOnLine = turf.nearestPointOnLine(line, target);

Документация на метод: @turf/nearestPointOnLine

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

Найти ближайшую точку на маршруте или дороге. Как и в случае с методом turf.buffer, это работает только в самых простых случаях, когда не требуется учитывать препятствия на местности. В остальных случаях воспользуйтесь API маршрутизации.


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Finding nearest with Turf.js and MapGL</title>
        <meta name="description" content="Finding nearest with Turf.js and MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const line = turf.lineString([[34, 40], [36, 41], [41, 37], [48, 42], [42, 35]]);
            const target = turf.point([38, 38], { name: 'Target' });

            const nearestOnLine = turf.nearestPointOnLine(line, target);
            nearestOnLine.properties = { name: 'Nearest Point On Line' };

            const points = turf.explode(line);
            const nearest = turf.nearestPoint(target, points);
            nearest.properties = { name: 'Nearest Point' };

            const geojson = turf.featureCollection([line, target, nearest, nearestOnLine]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 7,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100,
                        textFont: 'Noto_Sans',
                        textFontSize: 12,
                        textField: ['get', 'name'],
                        textPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Метод turf.union объединяет несколько фигур в одну.

const points = [
    [10, 10],
    [12, 10],
];
const circleA = turf.buffer(turf.point([10, 10]), 300);
const circleB = turf.buffer(turf.point([12, 10]), 300);
const united = turf.union(circleA, circleB);

Документация на метод: @turf/union

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

  • Отобразить на карте область, которую составляют несколько объектов, как один объект.
  • Собрать излишне фрагментированные объекты, что иногда случается при обработке геоданных.

Метод turf.intersect находит фигуру, образованную пересечением заданных фигур.

const points = [
    [10, 10],
    [12, 10],
];
const circleA = turf.buffer(turf.point([10, 10]), 300);
const circleB = turf.buffer(turf.point([12, 10]), 300);
const intersected = turf.intersect(circleA, circleB);

Документация на метод: @turf/intersect

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

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


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/union and @turf/intersect with MapGL</title>
        <meta name="description" content="Using @turf/union and @turf/intersect with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const points = [[10, 10], [12, 10]];
            const circleA = turf.buffer(turf.point([10, 10]), 300);
            const circleB = turf.buffer(turf.point([12, 10]), 300);
            const united = turf.transformTranslate(turf.union(circleA, circleB), 1200, 90);
            const intersected = turf.transformTranslate(turf.intersect(circleA, circleB), 1200, -90);

            const geojson = turf.featureCollection([circleA, circleB, united, intersected]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DDA0',
                        width: 3
                    }
                });
                map.addLayer({
                    id: 'points',
                    type: 'point',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        iconImage: 'ent_i',
                        iconWidth: 16,
                        iconPriority: 100
                    }
                });
            })
        </script>
    </body>
</html>

Метод turf.mask позволяет «вывернуть наизнанку» полигон: превратить остальное пространство вокруг него в полигон.

const poly = turf.polygon([
    [
        [0, 0],
        [0, 2],
        [2, 2],
        [2, 0],
        [0, 0],
    ],
]);
turf.mask(poly);

Документация на метод: @turf/mask

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

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


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/mask with MapGL</title>
        <meta name="description" content="Using @turf/mask with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const aPlace = turf.polygon([[
                [105.92429031266958, -5.262701049754213],
                [106.28471551378776, -5.195701693969994],
                [106.64994638418318, -5.214845098030779],
                [107.03920560108905, -5.305768259535483],
                [107.58705190638044, -5.4158151922751046],
                [107.74083332350821, -5.721926046845098],
                [107.8369467063646, -6.371857054158838],
                [107.82252969773305, -6.558086239663282],
                [106.65475205317988, -7.569142136398696],
                [106.01079236773299, -7.5834332864075975],
                [105.86181661939993, -7.535794279073713],
                [105.49178008359819, -7.430970061978684],
                [104.78488126325243, -6.791043334337019],
                [104.8233266147578, -6.456892553102861],
                [105.0299703919453, -6.026947405124446],
                [105.16933480114257, -5.816626441233922],
                [105.30389354084993, -5.644487005567058],
                [105.92429031266958, -5.262701049754213],
            ]]);
            const bbox = turf.bbox(aPlace);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(aPlace)),
                zoom: 7.5,
                key: 'Your API access key',
            });

            new mapgl.GeoJsonSource(map, {
                data: turf.mask(aPlace),
                attributes: {
                    foo: 'bar'
                }
            });

            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD50'
                    }
                });
            })
        </script>
    </body>
</html>



Метод turf.simplify упрощает геометрию при помощи алгоритма Рамера-Дугласа-Пекера.

const gpsTrack = turf.lineString([
    [0, 0],
    [0, 0.1],
    [0.5, 0.5],
    [1, 0.5],
]);
const simplifiedTrack = turf.simplify(gpsTrack, { tolerance: 0.2 });
// { ... geometry: { ..., coordinates: [[0, 0], [0.5, 0.5], [1, 0.5]] } }

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

Документация на метод: @turf/simplify

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

  • Получить менее сложные геометрии, которые будут отрисовываться быстрее.
  • Отфильтровать данные, когда известно, что их точность не может превышать определённое значение.
  • Отобразить точные данные на большом масштабе: упрощённые геометрии имеют более эстетичный внешний вид и плавные линии без визуального шума.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Using @turf/simplify with MapGL</title>
        <meta name="description" content="Using @turf/simplify with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const gpsTrack = turf.lineString([[0, 0], [0, 0.1], [0.5, 0.5], [1, 0.5]]);
            const simplifiedTrack = turf.simplify(gpsTrack, { tolerance: 0.2 });
            simplifiedTrack.properties.simplified = true;
            const bbox = turf.bbox(gpsTrack);

            console.log(gpsTrack, simplifiedTrack);
            const geojson = turf.featureCollection([gpsTrack, simplifiedTrack]);

            const map = new mapgl.Map('container', {
                center: turf.getCoord(turf.center(geojson)),
                zoom: 5,
                key: 'Your API access key',
            });
            map.fitBounds(
                { southWest: bbox.slice(0, 2), northEast: bbox.slice(2, 4) },
                { padding: { left: 20, top: 20, right: 20, bottom: 20 } }
            );

            new mapgl.GeoJsonSource(map, {
                data: geojson,
                attributes: {
                    foo: 'bar'
                }
            });
            map.on('styleload', () => {
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: ['match', 
                            ['get', 'simplified'], 
                            [true], '#FF9387',
                            '#00B2DDA0'
                        ],
                        width: ['match', 
                            ['get', 'simplified'], 
                            [true], 3,
                            5
                        ]
                    }
                });
            })
        </script>
    </body>
</html>


Карта позволяет работать с достаточно большим объёмом данных. Для работы с GeoJSON она использует библиотеку geojson-vt, которая обеспечивает приемлемое быстродействие на GeoJSON объёмом 100 Мб и содержащем 5.4 млн точек.

Пример ниже загружает и отображает на карте дорожную сеть из данных проекта Natural Earth Data. Данные представлены в виде Shp-файла объемом 15 Мб, содержащего порядка 700 тыс. точек.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Display road network from large shapefile with MapGL</title>
        <meta name="description" content="Display road network from large shapefile with MapGL" />
        <style>
            html,
            body,
            #container {
                margin: 0;
                width: 100%;
                height: 100%;
                overflow: hidden;
            }

            button {
                position: absolute;
                top: 10px;
                left: 10px;
                z-index: 2;

                font-family: sans-serif;
                font-size: 10pt;
                font-weight: bolder;
                background: white;
                border: none;
                border-radius: 4px;
                box-shadow: 0 1px 3px 0 rgba(38, 38, 38, 0.5);
                color: #505050;
                padding: 8px;
                margin: 0;
                box-sizing: border-box;
            }

            #status {
                font-family: sans-serif;
                font-size: 10pt;
                position: absolute;
                display: inline-block;
                background-color: white;
                padding: 2px;
                border-radius: 2px;
                top: 10px;
                left: 170px;
                z-index: 2;
            }
        </style>
    </head>
    <body>
        <button onclick="loadShp()">Load Roads Network</button> <span id="status"></span>
        <div id="container"></div>
        <script src="https://unpkg.com/shapefile@0.6"></script>
        <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
        <script src="https://mapgl.2gis.com/api/js/v1"></script>
        <script>
            const statusEl = document.querySelector('#status');
            function loadShp () {
                statusEl.innerText = 'Loading...';
                shapefile.open(
                    'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/10m_cultural/ne_10m_roads.shp'
                )
                .then(async (source) => {
                    const features = [];
                    let vertices = 0;
                    let result = await source.read();
                    while (result && !result.done) {
                        features.push(result.value);
                        vertices += turf.coordAll(result.value).length;
                        result = await source.read();
                    }
                    statusEl.innerText = `${features.length} features, ${vertices} points loaded.`;
                    new mapgl.GeoJsonSource(map, {
                        data: { type: 'FeatureCollection', features },
                        attributes: { foo: 'bar' }
                    });
                })
                .catch(error => console.error(error.stack));
            }
            
            const map = new mapgl.Map('container', {
                center: [18.687, 26.081],
                zoom: 2,
                key: 'Your API access key',
            });

            map.on('styleload', () => {
                map.addLayer({
                    id: 'polygons',
                    type: 'polygon',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: '#00B2DD90'
                    }
                });
                map.addLayer({
                    id: 'lines',
                    type: 'line',
                    filter: ['==', ['sourceAttr', 'foo'], 'bar'],
                    style: {
                        color: ['match',
                            ['get', 'type'],
                            ['Major Highway'], '#FF5733',
                            ['Secondary Highway'], '#F39C12',
                            ['Ferry Route', 'Ferry, seasonal'], '#A569BD',
                            '#85929E'
                        ],
                        width: ['interpolate', 
                            ['exponential', 1.5], ['zoom'],
                            4, 1,
                            9, 3
                        ]
                    }
                });
            })
        </script>
    </body>
</html>

Таким образом, объем данных обычно оказываются неприемлем не с точки зрения скорости их обработки/отрисовки, а с точки зрения скорости сети: передача данных объемом 20-100 Мб может занимать долгое время, особенно если используется мобильный интернет в отдаленных регионах.

Тем не менее, несколько миллионов точек — далеко не предел для геоданных. Чтобы эффективно обрабатывать любые их объёмы, данные следует преобразовать в тайловое представление: разбить на небольшие фрагменты, которые можно загружать для визуализации по отдельности. Для этих целей предназначен инструмент 2ГИС Про.