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

Я знал несколько классных библиотек карт для React, но никогда не использовал их вместе с инструментами визуализации данных. С этими целями я решил начать небольшой проект, в основном используя react-map-gl и deck.gl от Uber. На данный момент у меня есть инструменты, но… какие данные я могу использовать и откуда их взять? 🤔

Я давно использую историю местоположений Google, поэтому я знал, что у меня есть данные о моих поездках на их платформе. Что было новым для меня, так это способ извлечения этих данных, поскольку у них нет для этого API. Единственный способ, который я нашел, - это загрузка всех данных о моем местоположении с помощью их проекта по освобождению данных под названием Google Takeout. С его помощью вы можете загружать собственные данные о некоторых продуктах Google. В моем случае мне просто нужна история местоположений для этого проекта. В результате получился JSON-файл размером 32 МБ, который невозможно использовать для простого проекта без его размещения в базе данных.

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

{
    "timestampMs" : "1514242208538",
    "latitudeE7" : 378673894,
    "longitudeE7" : -47359558,
    "accuracy" : 19,
    "altitude" : 177,
    "verticalAccuracy" : 2,
    "activity" : [ {
      "timestampMs" : "1514242476975",
      "activity" : [ {
        "type" : "STILL",
        "confidence" : 100
      } ]
    } ]
  }

Давайте углубимся в технологии

Я начал простой проект реагирования, используя create-react-app инструмент. Для сложных проектов у меня есть собственный шаблон с моими настраиваемыми конфигурациями, касающимися Redux или маршрутизации. В подобных случаях я предпочитаю использовать create-react-app, так как не хочу беспокоиться о конфигурациях, подобных тем, которые используются для Webpack или Babel, а проект настолько прост, что я m не собираюсь использовать какие-либо другие распространенные библиотеки.

Имея свое базовое приложение в качестве отправной точки проекта, я удалил некоторые элементы шаблона, такие как примеры компонентов и стилей, которые я получил как результат create-react-app. Затем я установил react-map-gl библиотеку, чтобы начать рендеринг некоторых карт. В процессе настройки мне пришлось создать бесплатный проект на веб-сайте Mapbox, чтобы получить токен доступа, поскольку от него зависит библиотека Uber.

react-map-gl имеет простую документацию, поэтому я легко смог протестировать большинство компонентов и стили. Но в конце я использовал несколько базовых стилей и только дополнительный компонент для управления навигацией. Итак, функция render возвращает следующее:

return (
      <div>
        <ReactMapGL
          {...viewport}
          mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN}
          mapStyle="mapbox://styles/mapbox/dark-v9"
          onViewportChange={vp => this.onViewportChange(vp)}
        >
          <Controller
            onViewportChange={vp => this.onViewportChange(vp)}
          />
        </ReactMapGL>
      </div>
    )

Последним и самым интересным шагом была визуализация данных с помощью библиотеки Deck.gl. У меня были все данные и карта, поэтому я с нетерпением ждал, когда мои данные будут отображены поверх них. Документы Deck.gl сложнее, чем использовавшаяся ранее библиотека карт, поэтому я просто просмотрел вкладку "Начало работы", поскольку мне не нужны были какие-либо настраиваемые слои или интерактивность.

Для всех точек расположения я использовал класс GridLayer, так как он был лучшим в соответствии с моими потребностями. Производительность оказалась лучше, чем я ожидал, поскольку я рендерил более 60 тыс. Точек местоположения, прочитанных непосредственно из файла JSON. Чтобы реализовать это, нужно добавить только один компонент к ReactMapGL, и он выглядит так:

<DeckGL
  {...viewport}
  layers={[
    new GridLayer({
      id: "grid-layer",
      data: showLocations ? gridData : []
    }),
  ]}
/>

И карта отрисована…

На карте просматривается множество точек, поэтому поездки было легко идентифицировать, но… как насчет полетов? ✈️

Для этого потребовались дополнительные вычисления. Изучив данные, я увидел, что Google сохраняет большинство точек местоположения с помощью свойства активности, которое представляет собой массив с различными типами активности и точками уверенности для каждого из них. Таким образом вы сможете рассчитать, как вы двигаетесь во время путешествия. Это действительно полезно, если я хочу отображать свои автомобильные или железнодорожные поездки на другом слое, но не о рейсах. Я думаю, что в моем случае это было проще, так как я выключаю свой мобильный телефон или включаю режим полета во время полета, поэтому точки местоположения не должны отображаться.

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

Давай попробуем! 💪 Чтобы сравнить две последовательные точки с точки зрения расстояния, мне нужно было преобразовать две пары широты и долготы в расстояние в км. Это был момент для использования формулы Хаверсина, которая определяет расстояние между двумя точками на сфере с учетом их долготы и широты. Реализация javascript:

function calcDistance(latitude1, longitude1, latitude2, longitude2){
  const R = 6371 // km
  const dLat = toRad(latitude2 - latitude1)
  const dLon = toRad(longitude2 - longitude1)
  const lat1 = toRad(latitude1)
  const lat2 = toRad(latitude2)

  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
  return R * c
}

// Converts numeric degrees to radians
function toRad(Value) {
  return Value * Math.PI / 180
}

Как видите, я также создал функцию toRad, которая просто преобразует градусы в радианы. Функция calcDistance использовалась функцией getFlightsTrips, которая имеет в качестве параметра данные locationHistory и возвращает массив объектов с информацией о рейсах.

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

В совокупности результат был ожидаемым. Полеты отображались правильно, поэтому предположения соответствовали моим данным. Вот как это выглядит после добавления последнего слоя:

С таким результатом я считал проект завершенным. Библиотеки, которые я использовал, действительно хороши и для более сложных проектов, поэтому я обязательно сохраню их в своем списке на будущее. Я разместил демо-версию этого проекта в своем профиле на github, так что не стесняйтесь посещать его и оставлять комментарии!