Mapbox является американским поставщиком пользовательских онлайн-карт для веб-сайтов и приложений. С 2010 года он быстро расширил нишу пользовательских карт, в ответ на ограниченный выбор, предложенный поставщиками карт, такими как Google Maps. На данный момент остается достойным продуктом на фоне большого количества конкурентов.


Deck.gl это платформа на базе WEB API WebGL для визуального исследовательского анализа больших наборов данных (не только для визуализации географических данных). Создана и поддерживается Uber. Изначально разработана с использованием Mapbox. Также поддерживаются Google Maps, но возможности будут ограничены.


Теперь мы можем переходить к практике, которую разделим на несколько пунктов. Напомню, что для реализации поставленных задач будем использовать React.

1. Отображение карты

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

Переходим к установке зависимостей: yarn add mapbox-gl @urbica/react-map-gl

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

import * as React from "react";
import MapGL from "@urbica/react-map-gl";

import "mapbox-gl/dist/mapbox-gl.css";

const App = () => {
 const viewport = {
   latitude: 0,
   longitude: 0,
   zoom: 1,
 };

 return (
   <MapGL
     style={{ width: "100vw", height: "100vh" }}
     accessToken={TOKEN}
     {...viewport}
   />
 );
};

2. Отображение точек

Будем использовать Deck.gl слой IconLayer.

Подробнее о нем можно почитать здесь.

Установим зависимости: yarn add @deck.gl/mapbox @deck.gl/layers
Атлас с иконками возьмем по ссылке.

В объекте ICON_MAPPING описаны все виды иконок и их координаты в атласе выше.

Массив для двух точек будет выглядеть следующим образом. Обращаю внимание, что нужно в ключе icon указать вид иконки из объекта ICON_MAPPING.

const iconsData = [
 {
   id: 1,
   name: "First Point",
   size: 5,
   icon: "marker",
   coordinates: [-100.12097640000002, 35.449965],
   color: [0, 0, 128],
 },
 {
   id: 2,
   name: "Second Point",
   size: 5,
   icon: "marker",
   coordinates: [-100.0893059, 40.39611790000001],
   color: [255, 0, 0],
 },
];

Итоговый код:

import * as React from "react";
import MapGL, { CustomLayer } from "@urbica/react-map-gl";
import { MapboxLayer } from "@deck.gl/mapbox";
import { IconLayer } from "@deck.gl/layers";

import "mapbox-gl/dist/mapbox-gl.css";

import Atlas from "../src/img/icon-atlas.png";
import iconsData from "./layersData/iconsData";

const ICON_MAPPING = {
 marker: { x: 0, y: 0, width: 140, height: 148, mask: true },
 circle: { x: 0, y: 130, width: 120, height: 120, mask: true },
};

const App = () => {
 const [viewport, setViewport] = React.useState({
   latitude: 0,
   longitude: 0,
   zoom: 1,
 });

 const iconsLayer = new MapboxLayer({
   id: "icon-layer",
   type: IconLayer,
   data: iconsData,
   iconAtlas: Atlas,
   sizeScale: 10,
   iconMapping: ICON_MAPPING,
   getIcon: (d) => d.icon,
   getPosition: (d) => d.coordinates,
   getSize: (d) => d.size,
   getColor: (d) => d.color,
 });

 return (
   <MapGL
     style={{ width: "100vw", height: "100vh" }}
     accessToken={TOKEN}
     onViewportChange={setViewport}
     {...viewport}
   >
     <CustomLayer layer={iconsLayer} />
   </MapGL>
 );
};



3. Построение маршрутов

Для этой задачи используем TripsLayer слой.

В качестве данных я создаю объект с уже готовыми массивами, поэтому мне не нужно дополнительно использовать метод map при создании слоя и передачи координат с временем как в примере по ссылке. Хотелось бы отметить, что каждой координате соответствует свое время (unix timestamp).

const tripsData = [
 {
   coordinates: [
     [-100.149639, 35.440481],
     [-100.151832, 35.439649],
     [-100.152752, 35.439323],
     [-100.154222, 35.439699],
     [-100.154293, 35.439301],
     [-100.15539, 35.438506],
     [-100.155745, 35.43833],
     [-100.156138, 35.4382],
     [-100.157342, 35.43783],
     [-100.157729, 35.437787],
     [-100.15931, 35.438335],
     [-100.159402, 35.438002],
     [-100.159333, 35.437877],
     [-100.159702, 35.437776],
     [-100.160215, 35.437766],
     [-100.160195, 35.43783],
     [-100.160439, 35.43806],
     [-100.160269, 35.437808],
     [-100.160124, 35.438099],
     [-100.143509, 35.441997],
     [-100.142375, 35.442401],
     [-100.141645, 35.442632],
     [-100.141291, 35.442684],
     [-100.141841, 35.442592],
     [-100.139913, 35.443157],
     [-100.139481, 35.443198],
     [-100.138995, 35.443288],
     [-100.138686, 35.443415],
   ],
   timestamps: [
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009520,
     1556009880,
     1556009880,
     1556010060,
     1556011440,
     1556011440,
     1556011500,
     1556011500,
     1556011500,
     1556011680,
     1556012100,
     1556012160,
     1556012160,
     1556012160,
     1556012160,
     1556012160,
     1556012160,
     1556012160,
     1556012160,
   ],
   color: [18, 83, 2],
 },
];

Итоговый код. Обращаю внимание, что в ключе currentTime данного слоя я указал для примера последнее время unix timestamp из массива выше.

import * as React from "react";
import MapGL, { CustomLayer } from "@urbica/react-map-gl";
import { MapboxLayer } from "@deck.gl/mapbox";
import { TripsLayer } from "@deck.gl/geo-layers";

import "mapbox-gl/dist/mapbox-gl.css";

import tripsData from "./layersData/tripsData";

const App = () => {
 const [viewport, setViewport] = React.useState({
   latitude: 0,
   longitude: 0,
   zoom: 1,
 });

 const tripsLayer = new MapboxLayer({
   id: "trips-layer",
   type: TripsLayer,
   data: tripsData,
   widthMinPixels: 3,
   rounded: true,
   trailLength: 200,
   currentTime: 1556012340,
   getPath: (d) => d.coordinates,
   getTimestamps: (d) => d.timestamps,
   getColor: (d) => d.color,
 });

 return (
   <MapGL
     style={{ width: "100vw", height: "100vh" }}
     accessToken={TOKEN}
     onViewportChange={setViewport}
     {...viewport}
   >
     <CustomLayer layer={tripsLayer} />
   </MapGL>
 );
};



4. Кластеризация

Для ее реализации нам потребуется установить зависимости: yarn add supercluster @urbica/react-map-gl-cluster

Подробнее можно почитать здесь.

Данные для точек запишем следующим образом:

const clusterData = [
 {
   id: 1,
   name: "First Point",
   coordinates: [-110.12097640000002, 35.449965],
 },
 {
   id: 2,
   name: "Second Point",
   coordinates: [-112.0893059, 35.39611790000001],
 },
];

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



import React from "react";
import { Marker } from "@urbica/react-map-gl";

import { pointStyle } from "./App";

const ClusterMarker = (props) => (
 <Marker longitude={props.longitude} latitude={props.latitude}>
   <div style={pointStyle}>{props.pointCount}</div>
 </Marker>
);

Главная компонента выглядит так. Для надежности добавил дополнительную проверку координат:

import * as React from "react";
import MapGL, { Marker } from "@urbica/react-map-gl";
import Cluster from "@urbica/react-map-gl-cluster";

import "mapbox-gl/dist/mapbox-gl.css";

import ClusterMarker from "./ClusterMarker";
import clusterData from "./layersData/clusterData";

export const pointStyle = {
 display: "flex",
 justifyContent: "center",
 alignItems: "center",
 width: "32px",
 height: "32px",
 borderRadius: "50%",
 border: "1px solid black",
 backgroundColor: "white",
};

const App = () => {
 const [viewport, setViewport] = React.useState({
   latitude: 0,
   longitude: 0,
   zoom: 1,
 });

 const clusterLayerData = React.useMemo(
   () =>
     clusterData.map((point) => {
       const [fCoordinate, sCoordinate] = point.coordinates;
       const lng =
         sCoordinate > -90 && sCoordinate < 90 ? fCoordinate : sCoordinate;
       const lat = lng === fCoordinate ? sCoordinate : fCoordinate;

       return (
         <Marker key={point.id} longitude={lng} latitude={lat}>
           <div style={pointStyle}>1</div>
         </Marker>
       );
     }),
   []
 );

 return (
   <MapGL
     style={{ width: "100vw", height: "100vh" }}
     accessToken={TOKEN}
     onViewportChange={setViewport}
     {...viewport}
   >
     <Cluster
       radius={40}
       extent={512}
       nodeSize={64}
       component={ClusterMarker}
       children={clusterLayerData}
     />
   </MapGL>
 );
};





Заключение

На сайте Deck.gl еще собрано множество разнообразных слоев. Но как вы могли заметить, с помощью Deck.gl и Mapbox можно с легкостью реализовывать достаточно сложный на первый взгляд функционал. Успехов!