Deck.gl, Mapbox и React: отображение точек, маршрутов и кластеризация на карте

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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 можно с легкостью реализовывать достаточно сложный на первый взгляд функционал. Успехов!
Источник: https://habr.com/ru/post/553126/


Интересные статьи

Интересные статьи

Привет! Я работаю старшим сетевым инженером в компании DataLine, занимаюсь сетями с 2009 года и успел со стороны понаблюдать, как компании подвергались атакам из-за уязви...
Badoo регулярно участвует со стендом в выставках IT-конференций. Поэтому каждый год мы с коллегами — инженерами и деврелами — придумываем, что бы такого айтишного сделать, чтобы не скучать в пере...
Однажды, в понедельник, мне пришла в голову мысль — "а покопаюсь ка я в новом ядре" (новым относительно, но об этом позже). Мысль не появилась на ровном месте, а предпосылками для нее стали: ...
Здравствуйте. Я уже давно не пишу на php, но то и дело натыкаюсь на интернет-магазины на системе управления сайтами Битрикс. И я вспоминаю о своих исследованиях. Битрикс не любят примерно так,...
Если Вы используете в своих проектах инфоблоки 2.0 и таблицы InnoDB, то есть шанс в один прекрасный момент столкнуться с ошибкой MySQL «SQL Error (1118): Row size too large. The maximum row si...