Это началось как любопытство, а затем превратилось в решение, которое уже работает. Для справки, я всегда рекомендую идти по пути наименьшего сопротивления. Если библиотека компонентов React вокруг Mapbox, такая как response-map-gl, работает для вас, придерживайтесь ее! Это, безусловно, хорошо служило мне в прошлом.

Просто эта маленькая функция получения текущего местоположения пользователя у меня никогда не работала? Ничего не произойдет при открытии примера на их демонстрационном сайте и в моих приложениях, моя карта зависнет после нажатия кнопки «Геолокация»?

Я не мог найти решения, поэтому решил попробовать другую библиотеку, исходя из того, что у меня работает одна конкретная функция.

Простая карта кластера, которую я уже реализовал, не казалась слишком сложной для воссоздания, и мне было любопытно, как это будет происходить. Так. Если вам также интересно реализовать в своем проекте эту библиотеку, не поддерживающую React, читайте дальше.

Репозиторий Github:



Обратите внимание, что accessToken не подойдет вам, так как я обновил его перед публикацией этой статьи. Чтобы получить собственный токен, создайте учетную запись Mapbox.

1. Установка

Установите mapbox-gl

npm install mapbox-gl --save

Вставить стили mapbox

Добавьте это в <Head> своей страницы или pages/_template.js, если все ваши страницы используют карту.

<link href='https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css' rel='stylesheet' />

2. Добавление карты

Mapbox отображает приведенный ниже фрагмент кода для добавления на наш сайт.

var mapboxgl = require('mapbox-gl/dist/mapbox-gl.js');

mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
  var map = new mapboxgl.Map({
  container: 'YOUR_CONTAINER_ELEMENT_ID',
  style: 'mapbox://styles/mapbox/streets-v11'
});

Переключите переменную на const и вставьте div id в наш pages/index.js файл.

Теперь у нас есть что-то вроде этого:

pages / index.js

import Head from "next/head";
import styles from "../styles/Home.module.css";
const mapboxgl = require("mapbox-gl/dist/mapbox-gl.js");

mapboxgl.accessToken =
  "YOUR_ACCESS_TOKEN";
const map = new mapboxgl.Map({
  container: "my-map",
  style: "mapbox://styles/mapbox/streets-v11",
});

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
        <link
          href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css"
          rel="stylesheet"
        />
      </Head>

      <main className={styles.main}>
        <div id="my-map" />
...

Запускаем его с npm run dev, и мы обнаруживаем ошибку.

TypeError: Cannot read property "getElementById" of undefined.

Наша const map пытается найти div # my-map на странице, которая еще не существует. Определим map только после того, как страница будет смонтирована.

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

const [pageIsMounted, setPageIsMounted] = useState(false)

...

useEffect(() => {
    setPageIsMounted(true)
      const map = new mapboxgl.Map({
        container: "my-map",
        style: "mapbox://styles/mapbox/streets-v11",
      });
}, [])

Запускаем, и ошибок не будет. Но где карта? Добавьте размеры в свой div.

3. Добавление элемента управления геолокацией

Теперь по той причине, по которой мы пришли сюда. (Или, по крайней мере, причина, по которой я начал это путешествие).

Добавьте следующее к тому же useEffect, где мы убедились, что страница смонтирована:

useEffect(() => {
  const map = new mapboxgl.Map({
    container: "my-map",
    style: "mapbox://styles/mapbox/streets-v11",
  });

  map.addControl(
    new mapboxgl.GeolocateControl({
      positionOptions: {
        enableHighAccuracy: true,
      },
      trackUserLocation: true,
    })
  );
}, []);

Теперь мы видим кнопку «Геолокация». Нажмите на нее, и она действительно РАБОТАЕТ, перетащив вас в ваше текущее местоположение. ✈️

4. Добавление кластеров

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

Технология задействовала компоненты react-map-gl, useSupercluster и React в качестве контактов и всплывающих надписей (не показаны). При использовании этих пакетов я обнаружил несколько проблем:

  • Пины отображались неточно: местоположение на удаленных уровнях масштабирования выглядело некорректно.
  • Пины были неуклюжими: такой подход не обеспечивал очень плавное панорамирование, особенно на мобильных устройствах.
  • Пины будут оставаться: в неправильных местах. Если бы я установил границу в координатах, по которым пользователи могли панорамировать, переход к крайним краям заставил бы булавки обнимать край экрана и перемещаться в места, которым они не принадлежали.
  • Всплывающие окна будут отображаться частично скрытыми: Хорошо. Так. Это определенно для меня, так как я создал свой собственный компонент всплывающего окна, но когда я нажимал на булавку к краю экрана, он постоянно отображался справа от булавки и не был достаточно умен, чтобы знать, что он попадает край области просмотра

¯ \ (ツ) / ¯ Если это было на мне или нет, я упомянул все эти проблемы вам, потому что они исчезли с этой новой реализацией.

Итак, кластеры. Для этого нам понадобятся данные. Для этой демонстрации я создам конечную точку api/liveMusic, которая будет возвращать образец полезной нагрузки GeoJSON.

Обратитесь к примеру Создание и стилизация кластеров из Mapbox и примените его к useEffect, над которым мы работали.

Вот большой кусок кода, который они нам дают:

map.on("load", function () {
  map.addSource("earthquakes", {
    type: "geojson",
    // Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
    // from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
    data:
      "https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson",
    cluster: true,
    clusterMaxZoom: 14, // Max zoom to cluster points on
    clusterRadius: 50, // Radius of each cluster when clustering points (defaults to 50)
  });

  map.addLayer({
    id: "clusters",
    ...
  });

  map.addLayer({
    id: "cluster-count",
    ...
  });

  map.addLayer({
    id: "unclustered-point",
    ...
  });

  // inspect a cluster on click
  map.on("click", "clusters", function (e) {
    var features = map.queryRenderedFeatures(e.point, {
      layers: ["clusters"],
    });
    var clusterId = features[0].properties.cluster_id;
    map
      .getSource("earthquakes")
      .getClusterExpansionZoom(clusterId, function (err, zoom) {
        if (err) return;

        map.easeTo({
          center: features[0].geometry.coordinates,
          zoom: zoom,
        });
      });
  });

  // When a click event occurs on a feature in
  // the unclustered-point layer, open a popup at
  // the location of the feature, with
  // description HTML from its properties.
  map.on("click", "unclustered-point", function (e) {
    var coordinates = e.features[0].geometry.coordinates.slice();
    var mag = e.features[0].properties.mag;
    var tsunami;

    if (e.features[0].properties.tsunami === 1) {
      tsunami = "yes";
    } else {
      tsunami = "no";
    }

    // Ensure that if the map is zoomed out such that
    // multiple copies of the feature are visible, the
    // popup appears over the copy being pointed to.
    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    new mapboxgl.Popup()
      .setLngLat(coordinates)
      .setHTML(
        "magnitude: " + mag + "<br>Was there a tsunami?: " + tsunami
      )
      .addTo(map);
  });

  map.on("mouseenter", "clusters", function () {
    map.getCanvas().style.cursor = "pointer";
  });
  map.on("mouseleave", "clusters", function () {
    map.getCanvas().style.cursor = "";
  });
});

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

4.А. Изменить область просмотра

const map = new mapboxgl.Map({
  container: "my-map",
  style: "mapbox://styles/mapbox/streets-v11",
  center: [-77.02, 38.887],
  zoom: 12.5,
  pitch: 45,
  maxBounds: [
    [-77.875588, 38.50705], // Southwest coordinates
    [-76.15381, 39.548764], // Northeast coordinates
  ],

Шаг за шагом. Во-первых, поскольку наши данные состоят из мест проведения из Вашингтона, округ Колумбия, мы продолжим и изменим область просмотра для нашей карты, указав свойства center, zoom, pitch и maxBounds вокруг Капитолия.

4.B. Изменить источник данных

Теперь переключим источник данных. В настоящее время код ссылается на статический файл GeoJSON, предоставленный Mapbox. Наша фиктивная конечная точка тоже возвращает те же данные, но что, если мы захотим использовать API, который вместо этого возвращает часто меняющийся GeoJSON? Мы будем использовать swr, чтобы постоянно и автоматически получать поток обновлений данных.

Установить swr

npm i swr

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

Настроить swr

async function fetcher(params) {
  try {
    const response = await fetch(params);
    const responseJSON = await response.json();
    return responseJSON;
  } catch (error) {
    console.error("Fetcher error: " + error);
    return {};
  }
}

Создайте сборщик. Мы используем fetch, поскольку Next.js позаботится о соответствующих полифилах.

Затем использование хука:

const { data, error } = useSWR("/api/liveMusic", fetcher);

Переименуйте источник «землетрясения» в свой собственный, заменив его URL на data.

map.addSource("dcmusic.live", {
  type: "geojson",
  data: data,
  cluster: true,
  clusterMaxZoom: 14, 
  clusterRadius: 50, 
});

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

Что происходит? Если вы console.log(data) в функции map.on ('load'), вы увидите, что данные на самом деле отображаются как undefined. Он не загрузился вовремя для карты.

Что мы можем сделать? Запустить изменение исходных данных и слоев нашей карты, когда наши данные изменились и карта загрузилась.

4.C. Реструктуризация слоев данных

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

Извлеките любые addSource и addLayer функции в отдельную функцию в addDataLayer.js файле. В этом файле мы проверим, существует ли источник данных, и обновим данные. В противном случае мы продолжим и создадим его.

map / addDataLayer.js

export function addDataLayer(map, data) {
  map.addSource("dcmusic.live", {
    type: "geojson",
    data: data,
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 50,
  });

  map.addLayer({
    id: "data",
     ...
  });

  map.addLayer({
    id: "cluster-count",
    ...
  });

  map.addLayer({
    id: "unclustered-point",
    ...
  });
}

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

Слушатели щелчка и мыши, а также функция addControl могут быть помещены в файл initializeMap.js для наглядности.

map / initializeMap.js

export function initializeMap(mapboxgl, map) {
  map.on("click", "data", function (e) {
    var features = map.queryRenderedFeatures(e.point, {
      layers: ["data"],
    });
    var clusterId = features[0].properties.cluster_id;
    map
      .getSource("dcmusic.live")
      .getClusterExpansionZoom(clusterId, function (err, zoom) {
        if (err) return;
        map.easeTo({
          center: features[0].geometry.coordinates,
          zoom: zoom,
        });
      });
  });

  map.on("click", "unclustered-point", function (e) {
    var coordinates = e.features[0].geometry.coordinates.slice();
    var mag = e.features[0].properties.mag;
    var tsunami;
    if (e.features[0].properties.tsunami === 1) {
      tsunami = "yes";
    } else {
      tsunami = "no";
    }
    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }
    new mapboxgl.Popup()
      .setLngLat(coordinates)
      .setHTML("magnitude: " + mag + "<br>Was there a tsunami?: " + tsunami)
      .addTo(map);
  });
  map.addControl(
    new mapboxgl.GeolocateControl({
      positionOptions: {
        enableHighAccuracy: true,
      },
      trackUserLocation: true,
    })
  );

  map.on("mouseenter", "data", function () {
    map.getCanvas().style.cursor = "pointer";
  });
  map.on("mouseleave", "data", function () {
    map.getCanvas().style.cursor = "";
  });
}

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

const [Map, setMap] = useState()

Теперь внесем несколько изменений в pages/index.js:

  1. Вызовите функцию initializeMap в useEffect, где мы устанавливаем переменную pageIsMounted.
  2. Здесь также установите переменную Map.
  3. В новом useEffect добавьте событие «load» и вызовите функцию addDataLayer, если pageIsMounted, а у нас data.

pages / index.js

useEffect(() => {
    setPageIsMounted(true);

    let map = new mapboxgl.Map({
      container: "my-map",
      style: "mapbox://styles/mapbox/streets-v11",
      center: [-77.02, 38.887],
      zoom: 12.5,
      pitch: 45,
      maxBounds: [
        [-77.875588, 38.50705], // Southwest coordinates
        [-76.15381, 39.548764], // Northeast coordinates
      ],
    });

    initializeMap(mapboxgl, map);
    setMap(map);
  }, []);

  useEffect(() => {
    if (pageIsMounted && data) {
      Map.on("load", function () {
        addDataLayer(Map, data);
      });
    }
  }, [pageIsMounted, setMap, data, Map]);

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

5. Настройте стили кластера.

Если вы посмотрите на предоставленные данные geoJSON, вы увидите, что на самом деле мы сами немного группируем, присваивая каждому месту проведения event_count свойство. Это позволяет нам отправлять меньше данных во внешний интерфейс. Отсюда мы можем легко агрегировать информацию из точек кластера geoJSON с помощью clusterProperties.

Когда мы добавляем наш источник в map/addDataLayer.js, мы указываем эту агрегацию с помощью специального синтаксиса массива:

clusterProperties: {
  sum: ["+", ["get", "event_count"]],
}

Это позволяет нам модифицировать наш слой с id: cluster-count, чтобы использовать sum:

map.addLayer({
  id: "cluster-count",
  type: "symbol",
  source: "dcmusic.live",
  filter: ["has", "point_count"],
  layout: {
    "text-field": "{sum}",
    "text-font": ["Open Sans Bold"],
    "text-size": 16,
  },
  paint: {
    "text-color": "white",
  },
});

Кроме того, мы можем добавить новый слой для обозначения наших unclustered-point:

map.addLayer({
  id: "event-count",
  type: "symbol",
  source: "dcmusic.live",
  filter: ["!", ["has", "point_count"]],
  layout: {
    "text-field": "{event_count}",
    "text-font": ["Open Sans Bold"],
    "text-size": 16,
  },
  paint: {
    "text-color": "white",
  },
});

Наконец, мы удалим выражение step, которое различает цвет круга, и оставим его однородным.

6. Добавление всплывающего окна

При создании всплывающего окна в Mapbox у вас есть несколько вариантов для изменения содержимого. В своем примере отображать всплывающее окно при щелчке они используют setHTML. Поскольку мне нужна гибкость использования моего собственного компонента React, мы будем использовать вместо него setDOMContent.

map / initializeMap.js

map.on("click", "unclustered-point", function (e) {
  const coordinates = e.features[0].geometry.coordinates.slice();
  const venue_title = e.features[0].properties.title;

  while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
  }

  let placeholder = document.createElement("div");

  ReactDOM.render(<VenuePopup title={venue_title} />, placeholder);

  new mapboxgl.Popup({ offset: 25 })
    .setLngLat(coordinates)
    .setDOMContent(placeholder)
    .addTo(map);
});

В демонстрационных целях вот компонент Popup:

map / VenuePopup.js

export const VenuePopup = ({ title }) => {
  return (
    <div>
      <strong>{title}</strong>
    </div>
  );
};

После изменения наших функций щелчка и слушателей мыши для ссылки на наши слои clusters и unclustered-point, у нас есть как функция масштабирования расширения, предоставляемая примером кластера Mapbox, так и всплывающее окно, которое ссылается на наши собственные данные в компоненте React.

final: map / initializeMap.js

import ReactDOM from "react-dom";
import { VenuePopup } from "./VenuePopup";

export function initializeMap(mapboxgl, map) {
  map.on("click", "clusters", function (e) {
    var features = map.queryRenderedFeatures(e.point, {
      layers: ["clusters"],
    });
    var clusterId = features[0].properties.cluster_id;
    map
      .getSource("dcmusic.live")
      .getClusterExpansionZoom(clusterId, function (err, zoom) {
        if (err) return;

        map.easeTo({
          center: features[0].geometry.coordinates,
          zoom: zoom,
        });
      });
  });

  map.on("click", "unclustered-point", function (e) {
    const coordinates = e.features[0].geometry.coordinates.slice();
    const venue_title = e.features[0].properties.title;

    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    let placeholder = document.createElement("div");

    ReactDOM.render(<VenuePopup title={venue_title} />, placeholder);

    new mapboxgl.Popup({ offset: 25 })
      .setLngLat(coordinates)
      .setDOMContent(placeholder)
      .addTo(map);
  });

  map.addControl(
    new mapboxgl.GeolocateControl({
      positionOptions: {
        enableHighAccuracy: true,
      },
      trackUserLocation: true,
    })
  );

  map.on("mouseenter", "clusters", function () {
    map.getCanvas().style.cursor = "pointer";
  });
  map.on("mouseleave", "clusters", function () {
    map.getCanvas().style.cursor = "";
  });

  map.on("mouseenter", "unclustered-point", function () {
    map.getCanvas().style.cursor = "pointer";
  });
  map.on("mouseleave", "unclustered-point", function () {
    map.getCanvas().style.cursor = "";
  });
}

Готово! Вы только что интегрировали mapbox-gl-js в проект Next.js с кластеризацией и геолокацией. Если у вас есть вопросы или вы хотите предложить другой подход, сообщите нам в комментариях!

Примечания

  • Чтобы изменить сам контейнер Mapbox Popup, вам нужно будет использовать css и либо переопределить их классы, либо предоставить свои собственные классы через свойство className.
  • Вы можете следовать этому руководству вместе с ветками для этого репозитория Github. Последовательность коммитов в части 4. Добавление кластеров может быть трудным для понимания, поскольку я возился с решением. Я бы порекомендовал вместо этого посмотреть последний коммит этой ветки.

использованная литература

Пример Mapbox: поиск пользователя
Пример Mapbox: создание и стиль кластеров
Пример Mapbox: отображение всплывающего окна при нажатии
SWR: Обзор
Mapbox API: setData
API Mapbox: setDOMContent
API Mapbox: всплывающее окно

Первоначально опубликовано на https://dev.to 2 октября 2020 г.