Рендеринг нескольких цветных линий на карте React Map с помощью полилиний

Введение

Прошлым летом я начал работать на стартап в области Интернета вещей, Blues Wireless, целью которого является упрощение разработки IoT путем предоставления предоплаченного сотового подключения к Интернету любому IoT-устройству с помощью карты Notecard, которая передает данные датчиков в формате JSON в защищенное облако Notehub.

В предыдущей статье я показал, как я использовал Next.js и React Leaflet для создания карты отслеживания активов, чтобы показать, где находится движущаяся Notecard (внутри моего автомобиля) в режиме реального времени. Это упражнение оказалось более полезным, чем я ожидал, когда машину моих родителей угнали с подъездной дорожки во время праздника Дня благодарения, а я положил нотекарту на заднее сиденье, когда гостил у них.

Хотя блокнот был обнаружен вскоре после угона и выброшен в окно, в течение короткого периода времени мы (и полиция) могли следить за машиной по городу благодаря моей карте на приборной панели, и этот опыт вдохновил меня во время хакатона для всей компании несколько месяцев спустя.

Одна вещь, которая была бы очень полезна в период, когда машина была угнана, — это если бы линии между точками на карте были какого-то другого цвета, кроме стандартного синего, чтобы легче было определить, где находилась машина после того, как ее угнали. Поэтому для хакатона я создал новую приборную панель с «режимом SOS», чтобы не только отображать линии красным цветом на карте после включения режима SOS, но и увеличить частоту снятия показаний местоположения Notecard для большей точности.

Сегодня я покажу вам, как создать карту с помощью React Leaflet в Next.js с возможностью отображения линий разного цвета по нажатию кнопки для отслеживания активов в нормальных или чрезвычайных условиях.


Настройка компонента карты в приложении Next.js

Обратите внимание: в этой статье мы не будем рассматривать настройку нового проекта Next или подробное объяснение получения данных трекера активов из беспроводной карты Blues Wireless Notecard, поскольку я уже рассказывал об этом в данном посте.

Чтобы увидеть готовый код, вы можете просмотреть мой репозиторий на GitHub здесь

Установка зависимостей проекта карты

Первое, что мы сделаем в этом руководстве, это добавим карту в проект Next. Для этого нам потребуется добавить в проект несколько новых пакетов npm: leaflet, react-leaflet и leaflet-defaulticon-compatibility.

Выполните следующие строки в терминале.

$ npm install leaflet react-leaflet leaflet-defaulticon-compatibility
Войти в полноэкранный режим Выйти из полноэкранного режима

Примечание: Вам также понадобятся react и react-dom в качестве одноранговых зависимостей, если их еще нет в вашем проекте.

Примечание по TypeScript:

Если вы используете TypeScript в своем проекте, вам также необходимо установить следующую зависимость dev, чтобы избежать ошибок TypeScript:

$ npm install @types/leaflet --save-dev 
Вход в полноэкранный режим Выход из полноэкранного режима

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

Сгенерируйте токен Mapbox для стиля отображения карты и добавьте его в проект

Для отображения карты, на которой будет находиться трекер активов, я решил использовать стили Mapbox. У него есть множество стилей на выбор, и разработчики могут создать свои собственные токены Mapbox API для доступа к этим стилям, подписавшись на бесплатный аккаунт Mapbox.

После регистрации и создания нового API-токена скопируйте значение токена. В файле next.config.js приложения Next.js в корне проекта добавьте API-токен следующим образом:

next.config.js

/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  env: {
    MAPBOX_ACCESS_TOKEN:
      "[MAPBOX_TOKEN]",
  },
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Из этого файла Next сможет обращаться к токену, когда ему понадобится вызвать конечную точку Mapbox API. Теперь мы можем приступить к созданию компонента <Map /> в нашем проекте.

Создание компонента <Map>

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

Поскольку это проект React, индивидуальные, многократно используемые компоненты — это главное, поэтому создайте новый файл с именем Map.tsx и вставьте в него следующий код.

Фактический код доступен при нажатии на название файла ниже.

Map.tsx

import {
  MapContainer,
  TileLayer,
  Marker,
  Popup,
  CircleMarker,
  Polyline,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";

const Map = ({
  coords,
  lastPosition,
  markers,
  latestTimestamp,
  sosCoords,
}: {
  coords: number[][];
  lastPosition: [number, number];
  markers: [number, number][];
  latestTimestamp: string;
  sosCoords?: number[][];
}) => {
  const geoJsonObj: any = coords;
  const sosGeoJsonObj: any = sosCoords;

  const mapMarkers = markers.map((latLng, i) => (
    <CircleMarker key={i} center={latLng} fillColor="navy" />
  ));

  return (
    <>
      <h2>Asset Tracker Map</h2>
      <MapContainer
        center={lastPosition}
        zoom={14}
        style={{ height: "100%", width: "100%" }}
      >
        <TileLayer
          url={`https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`}
        />
        <Marker position={lastPosition} draggable={true}>
          <Popup>
            Last recorded position:
            <br />
            {lastPosition[0].toFixed(6)}&#176;, 
            {lastPosition[1].toFixed(6)}&#176;
            <br />
            {latestTimestamp}
          </Popup>
          <Polyline pathOptions={{ color: "blue" }} positions={geoJsonObj} />
          <Polyline pathOptions={{ color: "red" }} positions={sosGeoJsonObj} />
          {mapMarkers}
        </Marker>
      </MapContainer>
    </>
  );
};

export default Map;
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте вкратце обсудим, что здесь происходит.

В начале файла мы импортируем все необходимые компоненты React Leaflet, CSS Leaflet, а также CSS и JS Leaflet Default Icon Compatibility (это рекомендуется для того, чтобы иконки Leaflet работали как надо).

Затем мы видим реквизиты, которые ожидает компонент Map:

Теперь обратите внимание на JSX дальше в файле.

Компонент <MapContainer /> отвечает за создание экземпляра карты Leaflet Map. Без этого компонента карта не будет работать, и мы также определяем координаты карты center, уровень масштабирования по умолчанию и основные стили для компонента.

Компонент <TileLayer /> — это место, где используется стиль Mapbox и новый токен API. Выберите любой подходящий вам стиль, замените часть строки streets-v11 и убедитесь, что токен Mapbox присутствует в файле next.config.js, который я показал в предыдущем шаге. Без этого компонента фон карты для координат не отобразится — вместо него будет просто пустой холст.

<Marker /> принимает параметр lastPosition для отображения на карте иконки последней записанной позиции трекера, и оборачивает компонент <Popup />, компоненты <Polyline /> и список компонентов <CircleMarker />.

Компонент <Popup /> представляет собой красивую всплывающую подсказку, которая может отображать информацию. Мой компонент <Popup /> показывает последние GPS координаты трекера и время, когда пользователь нажимает на него.

Компоненты <Polyline /> являются тем местом, куда передается список coords или список sosCoords GPS координат для рисования соединительных линий между маркерами карты. Объект Polyline принимает positions, который в данном случае является либо geoJsonObj, либо sosGeoJsonObj, а pathOptions определяет цвет отрисованной линии.

Примечание: Сначала я пытался использовать объект GeoJSON для рендеринга соединительных линий, но нет возможности изменить цвет линии на середине пути (как в случае, когда включен режим SOS и линии должны перейти от синего цвета к красному), поэтому несколько отдельных объектов Polyline оказались лучшим способом достижения этой цели.

И последнее, но не менее важное, компоненты <CircleMarker >/, которые отображаются в JSX этого компонента как {mapMarkers}.

Примечание: Для того чтобы все маркеры в списке отображались как отдельные круги на карте, мне пришлось создать небольшую функцию для итерации списка и генерации всех кругов, а затем внедрить ее непосредственно в JSX.

Попытка перебрать все значения внутри JSX не сработала бы.

Теперь наш компонент Map разобран, давайте перейдем к наполнению карты данными и переходу от синих линий к красным и обратно.

Рендеринг карты в приложении Next.js

Следующий шаг к тому, чтобы заставить эту карту работать в нашем приложении Next.js, — импортировать компонент Map с опцией ssr: false.

Библиотека react-leaflet работает только на стороне клиента, поэтому необходимо использовать поддержку Next’s dynamic import() без функции SSR, чтобы компонент не пытался отрисоваться на стороне сервера.

Ниже приведен код файла index.tsx, в котором будет отображаться этот компонент, сокращенный для ясности. Если вы хотите увидеть полный код на GitHub, щелкните по имени файла.

pages/index.tsx

// imports
import dynamic from "next/dynamic";
// other imports

type dataProps = {
// condensed for code brevity
};

export default function Home({ data }: { data: dataProps[] }) {
  // needed to make the Leaflet map render correctly
  const MapWithNoSSR = dynamic(() => import("../src/components/Map"), {
    ssr: false,
  });

 // logic to enable/disable sos mode and transform data into items needed to pass to map

  return (
    <div>
      {/* extra tracker app code */}
      <main>
        <h1>Notelink Tracker Dashboard</h1>
        {/* other tracker components */}
          <MapWithNoSSR
            coords={latLngMarkerPositions}
            lastPosition={lastPosition}
            markers={latLngMarkerPositions}
            latestTimestamp={latestTimestamp}
            sosCoords={sosCoords}
          />
        </div>
      </main>
    </div>
  );
}

// code to fetch tracker data: getStaticProps
Вход в полноэкранный режим Выход из полноэкранного режима

Пока не стоит слишком беспокоиться о реквизитах, передаваемых компоненту — мы настроим их в ближайшее время.

Теперь наш компонент <Map /> динамически импортируется с отключенным серверным рендерингом Next, и компонент можно использовать так же, как и любой другой в приложении.

Получение данных для карты

В своей предыдущей статье о панели отслеживания активов я подробно рассказывал о том, как создать собственный трекер активов для генерирования реальных данных для приложения с помощью оборудования Blues Wireless и получения этих данных в приложение через API облака Notehub.

Если вы захотите последовать этому примеру, чтобы создать свой собственный трекер и направить данные в Notehub, пожалуйста.

В этом посте я перейду к той части, где мы уже получаем данные в приложение через вызов API Next.js getStaticProps. Данные JSON из облака Notehub при первом получении выглядят следующим образом:

[
  {
    "uid": "d7cf7475-45ff-4d8c-b02a-64de9f15f538",
    "device_uid": "dev:864475ABCDEF",
    "file": "_track.qo",
    "captured": "2021-11-05T16:10:52Z",
    "received": "2021-11-05T16:11:29Z",
    "body": {
      "hdop": 3,
      "seconds": 90,
      "motion": 76,
      "temperature": 20.1875,
      "time": 1636123230,
      "voltage": 4.2578125
    },
    "gps_location": {
      "when": "2021-11-05T16:10:53Z",
      "name": "Sandy Springs, GA",
      "country": "US",
      "timezone": "America/New_York",
      "latitude": 33.913747500000014,
      "longitude": -84.35008984375
    }
  },
  {
    "uid": "3b1ef772-44da-455a-a846-446a85a70050",
    "device_uid": "dev:864475ABCDEF",
    "file": "_track.qo",
    "captured": "2021-11-05T22:22:18Z",
    "received": "2021-11-05T22:23:12Z",
    "body": {
      "hdop": 2,
      "motion": 203,
      "seconds": 174,
      "temperature": 22,
      "time": 1636150938,
      "voltage": 4.2265625
    },
    "gps_location": {
      "when": "2021-11-05T22:22:19Z",
      "name": "Doraville, GA",
      "country": "US",
      "timezone": "America/New_York",
      "latitude": 33.901052500000006,
      "longitude": -84.27090234375
    }
  },
  {
    "uid": "e94b0c68-b1d0-49cb-8361-d622d2d0081e",
    "device_uid": "dev:864475ABCDEF",
    "file": "_track.qo",
    "captured": "2021-11-05T22:40:04Z",
    "received": "2021-11-05T22:46:30Z",
    "body": {
      "hdop": 1,
      "motion": 50,
      "seconds": 41,
      "temperature": 21.875,
      "time": 1636152004,
      "voltage": 4.1875
    },
    "gps_location": {
      "when": "2021-11-05T22:40:05Z",
      "name": "Peachtree Corners, GA",
      "country": "US",
      "timezone": "America/New_York",
      "latitude": 33.9828325,
      "longitude": -84.21591015624999
    }
  },
  {
    "uid": "1344517c-adcb-4133-af6a-b1132ffc86ea",
    "device_uid": "dev:864475ABCDEF",
    "file": "_track.qo",
    "captured": "2021-11-06T03:04:07Z",
    "received": "2021-11-06T03:10:51Z",
    "body": {
      "hdop": 1,
      "motion": 126,
      "seconds": 218,
      "temperature": 12.5625,
      "time": 1636167847,
      "voltage": 4.1875
    },
    "gps_location": {
      "when": "2021-11-06T03:04:08Z",
      "name": "Norcross, GA",
      "country": "US",
      "timezone": "America/New_York",
      "latitude": 33.937182500000006,
      "longitude": -84.25278515625
    }
  }
]
Войти в полноэкранный режим Выход из полноэкранного режима

Каждый объект JSON в этом массиве представляет собой отдельное событие движения _track.qo, которое отображает текущее местоположение Notecard и показания датчиков. Часть объекта, которая нас интересует в данном конкретном посте, это значения gps_location: latitude, longitude, и значение captured. Эти данные понадобятся нам для карты.

Вскоре мы займемся преобразованием этих данных для соответствия реквизитам нашего компонента <Map /> — этим мы займемся сразу после создания режима SOS для приложения.

Настройка режима SOS в приложении

Кнопка SOS для включения режима SOS в приложении.

Прежде чем преобразовывать данные JSON, нам нужно дать нашему приложению возможность включить или выключить режим SOS (который изменяет цвет полилиний, отображаемых на карте).

Для этого нам понадобится новая переменная состояния, функция и кнопка в нашем файле index.tsx.

pages/index.tsx

// imports
import { useState } from "react";
// more imports

type dataProps = {
// condensed for code brevity
};

export default function Home({ data }: { data: dataProps[] }) {
  // map component imported dynamically here

  const [isSosModeEnabled, setIsSosModeEnabled] = useState<boolean>(false);

  const toggleSosMode = () => {
    const newSosState = !isSosModeEnabled;
    if (newSosState === true) {
      localStorage.setItem("sos-timestamp", new Date());
      setIsSosModeEnabled(newSosState);
    } else {
      localStorage.removeItem("sos-timestamp");
      setIsSosModeEnabled(newSosState);
    }
  };

 // logic to transform data into items needed to pass to map

  return (
    <div>
      {/* extra tracker app code */}
      <main>
        <h1>Notelink Tracker Dashboard</h1>
        <button onClick={toggleSosMode}>
          SOS Mode
        </button>
        {isSosModeEnabled ? <p>SOS Mode Currently On</p> : null}
        {/* other tracker components */}
          <MapWithNoSSR
            coords={latLngMarkerPositions}
            lastPosition={lastPosition}
            markers={latLngMarkerPositions}
            latestTimestamp={latestTimestamp}
            sosCoords={sosCoords}
          />
        </div>
      </main>
    </div>
  );
}

// code to fetch tracker data: getStaticProps
Вход в полноэкранный режим Выход из полноэкранного режима

В файле выше мы добавляем новое булево isSosModeEnabled: это позволит приложению знать, происходят ли новые события о местоположении во время отслеживания чрезвычайной ситуации или нет.

Далее мы создаем новую функцию toggleSosMode(). Эта функция будет изменять состояние isSosModeEnabled, а также сохранять временную метку sos-timestamp в локальном хранилище браузера. Я сохраняю эту временную метку в локальном хранилище, чтобы ее можно было сравнить с событиями, которые приходят в приложение после включения режима SOS, и приложение знало, нужно ли ему отрисовывать полилинии на карте красным или синим цветом. Мы перейдем к логике этой части в следующем разделе.

Наконец, в JSX для компонента мы создадим новый элемент <button> и присоединим функцию toggleSosMode() к его методу onClick(). Я также добавил тег <p> под кнопкой, чтобы отображать, когда в приложении действует режим SOS.

Нажатие на кнопку после включения режима включит его, повторное нажатие выключит.

Примечание: В настоящем репозитории также есть логика для обновления Notecard через Notehub для более частого считывания местоположения, а также для обработки ситуации, когда режим SOS уже включен, а окно браузера закрыто или соединение с приложением потеряно, но для ясности в этом руководстве я исключил оба этих соображения.

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

Изменение формы данных о событиях трекера

Нашему файлу index.tsx понадобится еще несколько переменных состояния для выполнения всех различных реквизитов данных, необходимых компоненту <Map />. Я снова сократил логику, чтобы облегчить чтение этого файла, но вы всегда можете щелкнуть по имени файла, чтобы увидеть его полное содержимое онлайн.

pages/index.tsx

// imports
import { useEffect, useState } from "react";
import dayjs from "dayjs"; // for ease of date formatting 
// more imports

type dataProps = {
// condensed for code brevity
};

export default function Home({ data }: { data: dataProps[] }) {
  // map component imported dynamically here

   const [lastPosition, setLastPosition] = useState<[number, number]>([
    33, -84,
  ]);
  const [latestTimestamp, setLatestTimestamp] = useState<string>("");
  const [latLngMarkerPositions, setLatLngMarkerPositions] = useState<
    [number, number][]
  >([]);

  // isSosEnabled boolean here
  const [sosCoords, setSosCoords] = useState<number[][]>([]);

  /* runs as soon as the location data is fetched from Notehub API 
    or when the sos mode is toggled on or off with the button */
  useEffect(() => {
    const latLngArray: [number, number][] = [];
    const sosLatLngArray: [number, number][] = [];
    if (data && data.length > 0) {
      data
        .sort((a, b) => {
          return Number(a.captured) - Number(b.captured);
        })
        .map((event) => {
          let latLngCoords: [number, number] = [];
          let sosLatLngCoords: [number, number] = [];
          if (!isSosModeEnabled) {
            latLngCoords = [
            event.gps_location.latitude,
            event.gps_location.longitude,
            ];
            latLngArray.push(latLngCoords);
          } else {
            const localSosTimestamp = localStorage.getItem("sos-timestamp");
            if (Date.parse(event.captured) >= Date.parse(localSosTimestamp)) {
                sosLatLngCoords = [
                  event.gps_location.latitude,
                  event.gps_location.longitude,
                ];
              sosLatLngArray.push(sosLatLngCoords);
            } else {
              latLngCoords = [
                event.gps_location.latitude,
                event.gps_location.longitude,
              ];
              latLngArray.push(latLngCoords);
            }
          }
        });
      const lastEvent = data.at(-1);
      let lastCoords: [number, number] = [0, 1];
      lastCoords = [
        lastEvent.gps_location.latitude,
        lastEvent.gps_location.longitude,
      ];
      setLastPosition(lastCoords);
      const timestamp = dayjs(lastEvent?.captured).format("MMM D, YYYY h:mm A");
      setLatestTimestamp(timestamp);
    }
    if (sosLatLngArray.length > 0) {
      setSosCoords(sosLatLngArray);
    }
    setLatLngMarkerPositions(latLngArray);
  }, [data, isSosModeEnabled]);

  // toggleSosMode function  

  return (
    <div>
      {/* extra tracker app code */}
      <main>
        <h1>Notelink Tracker Dashboard</h1>
        {/* other tracker components */}
          <MapWithNoSSR
            coords={latLngMarkerPositions}
            lastPosition={lastPosition}
            markers={latLngMarkerPositions}
            latestTimestamp={latestTimestamp}
            sosCoords={sosCoords}
          />
        </div>
      </main>
    </div>
  );
}

// code to fetch tracker data: getStaticProps
Вход в полноэкранный режим Выход из полноэкранного режима

В нашем основном компоненте, после получения данных из Notehub, мы устанавливаем следующие новые переменные React useState, чтобы хранить данные для передачи компоненту <Map />.

lastPosition, latestTimestamp, latLngMarkerPositions и sosCoords — это новые переменные состояния, которые нам понадобятся.

После объявления этих состояний функция useEffect() будет запускаться всякий раз, когда данные будут получены из Notehub (при монтировании компонента) или когда будет переключен режим SOS приложения. Внутри функции события из Notehub сортируются и итерируются.

Если булево isSosModeEnabled истинно, sos-timestamp извлекается из локального хранилища браузера, и дата этой временной метки сравнивается с captured временной меткой каждого события, чтобы событие можно было правильно отсортировать в список sosLatLngArray или latLngArray.

Когда эти локальные массивы собраны внутри useEffect(), они устанавливаются равными переменным состояния latLngMarkerPositions и sosCoords.

Если isSosModeEnabled равно false, то все события добавляются в список latLngArray автоматически.

Другие переменные lastPosition и latestTimestamp устанавливаются просто путем извлечения последнего события из массива отсортированных данных и извлечения из него свойств.

Затем все эти переменные передаются компоненту <Map />, и он знает, что делать дальше в отношении маркеров, всплывающих окон и цветов линий.

Проверьте это

Хорошо! Думаю, мы готовы протестировать нашу карту и разноцветные линии!

Если вы используете наши смоделированные данные вместо данных, поступающих в реальном времени из облака Notehub, самый простой способ протестировать приложение — это включить режим SOS с помощью кнопки в браузере, затем настроить временную метку в локальном хранилище DevTool браузера так, чтобы она была раньше, чем хотя бы некоторые из событий captured timestamps.

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

Когда режим SOS включен, новые события, произошедшие после его включения, отображаются в виде красных линий.

Пожалуйста, имейте в виду, что это приложение было создано за один день, поэтому оно грубовато по краям и, конечно, не готово к прайм-тайму. Я бы рекомендовал сделать форк и провести дополнительную полировку и тестирование, прежде чем выкладывать его в prod, если у вас есть такая возможность.

И вот оно: разноцветные линии на карте в приложении React. Не слишком плохо для одного дня работы.


Заключение

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

Это казалось хорошей функцией для улучшения читаемости карты в какой-либо чрезвычайной ситуации.

Next.js в сочетании с библиотекой React Leaflet сделали все это возможным, и в установленные сроки у меня был рабочий (хотя и очень грубый) прототип, который я мог показать своим коллегам. Работать над ним было очень весело, и по ходу дела я узнал много нового. Таково мое представление об успешном хакатоне.

Загляните ко мне через несколько недель — я буду писать больше о JavaScript, React, IoT или о чем-то еще, связанном с веб-разработкой.

Если вы хотите быть уверены, что никогда не пропустите ни одной моей статьи, подпишитесь на мою рассылку здесь: https://paigeniedringhaus.substack.com.

Спасибо за прочтение. Надеюсь, вам было полезно узнать, как настроить интерактивную карту в Next.js и отобразить разноцветные линии движения между различными точками в зависимости от ситуации. Счастливого картографирования!


Ссылки и дополнительные ресурсы

  • Оригинальный проект трекера активов Hackster.io
  • SOS Asset Tracker GitHub repo
  • Документация по Leaflet
  • Документация по React Leaflet
  • Сайт Mapbox
  • Сайт Blues Wireless

Оцените статью
devanswers.ru
Добавить комментарий