habrahabr

Интерактивная карта для веб-приложения за пару часов

  • вторник, 11 сентября 2018 г. в 00:18:52
https://habr.com/post/422759/
  • Разработка веб-сайтов
  • Геоинформационные сервисы
  • Визуализация данных
  • ReactJS
  • JavaScript


В прошлой статье я кратко рассказала о возможностях kepler.gl — нового Open Source инструмента для визуализации и анализа больших наборов гео-данных.


Варианты карт, созданных с помощью kepler.gl
Рисунок 1. Варианты карт, созданных с помощью kepler.gl (by Uber)


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


Сравниваем альтернативные варианты


Вся «магия» kepler.gl происходит на клиенте, поэтому приложение предлагает только 2 способа поделиться своими результатами с другими:


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

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


Примечание. Обрабатывая и агрегируя данные на лету, kepler.gl зачастую требует достаточно много ресурсов. Поэтому стоит быть особенно внимательным при его интеграции в приложения, заточенные под мобильные.


Такой вариант использования kepler.gl позволит:


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

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


Создаем демо-приложение


Пришло время перейти от теории к практике. Для того, чтобы познакомить вас с основными этапами интеграции kepler.gl в ваш код, я сделала небольшое демо-приложение.


Оно позволяет пользователю посмотреть информацию о платных московских парковках в одном из двух режимов – общем или агрегированном. При этом приложение дает возможность только просматривать созданные нами визуализации, переключаться между ними и работать с картой в read-only режиме. Весь исходный код и live-версия доступна на GitHub.


Демо приложение о платных парковках Москвы
Рисунок 2. Два режима просмотра карты, предоставляемых демо-приложением


Для создания этого демо я использовала свой собственных шаблон проекта. Однако, если вы решите самостоятельно «поиграть» с kepler.gl, но у вас еще не сложилось личных предпочтений, рекомендую вам использовать create-react-app, который заметно уменьшит время на создание основы вашего будущего приложения.


Добавляем kepler.gl в проект


Kepler.gl представляет собой React-компонент, использующий Redux для хранения и управления своим состоянием. Для того, чтобы добавить его к проекту, достаточно установить соответствующий npm-package:


npm install --save kepler.gl

Данный npm-пакет включает в себя:


  • набор UI компонентов и фабрик, позволяющих их переопределять своими собственными компонентами
  • предопределенные методов для добавления/изменения используемых данных и способов их отображения
  • необходимый для их работы редьюсер Redux

Настраиваем Redux хранилище для работы kepler.gl


Kepler.gl использует Redux для управления своим состоянием в процессе создания и обновления карт. Поэтому перед использованием компонента KeplerGl нам потребуется добавить соответствующий редьюсер к редьюсеру приложения.


import { combineReducers } from 'redux';

import keplerGlReducer from 'kepler.gl/reducers';
import appReducer from './appReducer';

const reducers = combineReducers({
  keplerGl: mapReducer,
  app: appReducer,
});

export default reducers;

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


const mapReducer = keplerGlReducer
    .initialState({
         uiState: {
              readOnly: true,
              mapControls: {
                  visibleLayers: {
                      show: false
                  },
                  toggle3d: {
                       show: false
                  },
                  splitMap: {
                       show: true
                  },        
                  mapLegend: {
                       show: true,
                       active: false
                  }
             }
        }
   });

Так же нам нужно будет установить react-palm, который kepler.gl использует для управления side-эффектами и добавить taskMiddleware из этого npm-пакета к Redux хранилищу своего приложения:


import {createStore, applyMiddleware, compose} from 'redux';
import {taskMiddleware} from 'react-palm';

import reducers from './reducers';

const initialState = { };
const middlewares = [ taskMiddleware ];
const enhancers = [applyMiddleware(...middlewares)];

export default createStore(
  reducers,
  initialState,
  compose(...enhancers)
);

Примечание. В настоящее время команда Uber Engineering активно работает над новой версией kepler.gl в которой не будет зависимости от react-palm.


По умолчанию kepler.gl ожидает, что его объект-состояние будет расположен на верхнем уровне состояния всего приложения и доступен по имени keplerGl. Если же конфигурация Redux хранилища отличается от ожидаемой, то для корректной работы соответствующего React-компонента достаточно явно указать место расположение его состояния в иерархии с помощью свойства getState.


Встраиваем React-компонент KeplerGl


Для быстрого рендеринга карт с большим количеством отображаемых элементов (до миллионов гео-точек!) kepler.gl использует desk.gl – WebGL фреймворк для визуализации данных, и MapBox – Open Source гео-платформу, предоставляющая удобное API и широкие возможности по кастомизации создаваемых карт. Поэтому, одним из обязательных параметров, передаваемых компоненту KeplerGl, является API токен для доступа к сервису MapBox.


Для получения токена нужно зарегистрироваться на сайте www.mapbox.com. MapBox предоставляет на выбор несколько различных тарифных планов, но для небольших приложений будет достаточно бесплатной версии с 50,000 просмотрами в месяц.


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


Устанавливаем полученный токен в соответствующую переменную среды:


export MapboxAccessToken=<your_mapBox_token>

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


import React from 'react';
import KeplerGl from 'kepler.gl';

const mapboxAccessToken = process.env.MapboxAccessToken;

const ParkingMap = (props) => (
    <KeplerGl
          id="parking_map"
          mapboxApiAccessToken={mapboxAccessToken}
          width={ props.width }
          height={ props.height }         
    />
);

export default ParkingMap;

Добавляем ParkingMap в приложение. На данном этапе вместо информации о парковках просто отобразиться карта без каких-либо сведений, ведь мы еще не передали данных, на основе которых строятся наши визуализации.


Загружаем данные и конфигурации карты


Для того, чтобы отобразить свои данные на карте, нужно передать KeplerGl набор данных, на основе которых будет создана карта, и желаемую конфигурацию финальной визуализации. Это можно сделать воспользовавшись одним из двух методов — addDataToMap или updateVisData.


Первый метод позволяет не только загрузить необходимый набор данных и полностью задать/обновить конфигурацию соответствующего экземпляра компонента KeplerGl, включая настройки визуализации(visState) и карты(mapState), а также стиль используемой карты(mapStyle).


В качестве параметра метода addDataToMap принимает объект, содержащий следующие сведения:


  • используемые наборы данных для построения визуализации
  • дополнительные параметры конфигурации (options)
  • данные конфигурации, включающие в себя mapState, mapStyle, visState
    addDataToMap({
        datasets: { … }
        options: { … }
        config: {
            mapState { … }, 
            mapStyle { … },
            visState: { … }
        }
    });

Примечание. Данные из объекта конфигураций всегда имеют более высокий приоритет по сравнению с настройками, переданными в объекте “options”.


Метод updateVisData позволяет обновить только используемые наборы данных без полного изменения конфигурации используемого компонента. В качестве параметра он так же, как и первый метод принимает объект, который содержит сведения о новом наборе или наборах данных и параметр «options» для обновления некоторых настроек отображения карты.


Инициализация карты


Таким образом, для первичной загрузки данных нам потребуется метод addDataToMap. В создаваемом демо-приложении база данных о платных парковках Москвы загружается при первом обращении к приложению отдельным запросом. Полученные исходные данные необходимо подготовить для загрузки в KeplerGl. Для этого в большинстве случаев достаточно одного из предопределенных процессоров, которые портируют csv / json данные в поддерживаемый kepler.gl формат данных.


export function loadParkingData(mapMode) {
        return (dispatch) => {
            dispatch( requestParkingData() );
            fetch(demoDataUrl)
                    .then(response => response.text())
                    .then(source => {
                            dispatch( getParkingData() );

                const data = Processors.processCsvData(source);
                const config = getMapConfig(mapMode);
                const datasets = [{ info, data }]; 

                dispatch(
                    wrapTo('parking_map',
                        addDataToMap({
                            datasets,
                            config
                        })
                    )); 
            }); 
        };
}

Переключение между режимами


Для переключения между режимами просмотра карты нам нужно определить еще одну функцию действия. Так как в текущей версии KeplerGl нет простого способа изменить только конфигурацию карты, не затрагивая при этом данные, то наиболее подходящим методом для переключения между режимами также будет метод addDataToMap:


export function toggleMapMode(mode) {
    return (dispatch, getState) => {
            const config = getMapConfig( mode );
            const datasets =  getDatasets(getState());
            dispatch(
               wrapTo('parking_map',
                     addDataToMap({
                          datasets,
                          config
                     })

               )); 
            dispatch( setMapMode(mode) );
        };
}

Параметр dataset является обязательным, поэтому каждый раз при переключении режима просмотра карты мы будем повторно передавать исходный набор данных, загруженный при старте приложения. Сведения о конфигурации карты при этом каждый раз будут обновляется. В этой статье я не буду подробно останавливатся на том, как реализованы вспомогательные методы getMapConfig и getDatasets, с исходным кодом которых вы можете ознакомится на GitHub.


Примечание. В настоящее время API KeplerGl очень ограничено и расчитано на самые базовые случаи (добавление и обновление данных). При этом сами разработчики признают, что текущая версия не предусматривает эффективного метода для обновления только конфигураций или для real-time обновления данных. Однако, не стоит забывать, что проект находится в стадии активной разработки и есть надежда на скорое расширение его функциональных возможностей.


Кастомизируем элементы карты


KeplerGl включает в себя не только контейнер с гео-визуализацией, но и элементы управления картой, тултип, боковую панель для управления отображаемыми данными, диалоговое окно загрузки данных в формате csv, json или geojson и т.д. При этом, каждый и перечисленных компонентов можно легко заменить на собственную версию с помощью системы внедрения зависимостей (dependency injection).


Для того, чтобы заменить базовый компонент на его кастомизированную версию достаточно:


  • импортировать дефолтную фабрику компонента
  • определить новую фабрику, возвращающую кастомный компонент
  • встроить новую фабрику с помощью метода injectComponents

В создаваемом нами демо-приложении мы не хотим давать пользователю возможности самостоятельно настраивать режим просмотра, фильтровать существующую или загружать новые данные.


В теории для этого достаточно указать, что компонент KeplerGl находится в read only режиме, который появился только в версии 0.0.27 Однако даже в этой версии все элементы управления все равно отображаются пользователю в течении нескольких первых мгновений до загрузки карты, а лишь потом скрываются. Чтобы избежать этого, мы можем явно заменить нежелательные компоненты на null-компонент, воспользовавшись методом injectComponents:


import React from 'react';

import { 
    injectComponents, 
    ModalContainerFactory, 
    SidePanelFactory,
} from 'kepler.gl/components';

// define null factory to don not render any unnecessary components
const NullComponent = () => null;
const nullComponentFactory = () => NullComponent;

const KeplerGl = injectComponents([
   [ModalContainerFactory, nullComponentFactory],
   [SidePanelFactory, nullComponentFactory],
]);

export default KeplerGl;

Удобно, что KeplerGl не только позволяет заменить базовые компоненты на кастомизированные, но с помощью метода withState дает возможность добавить дополнительные действия и параметры состояния для новых компонентов.


Как использовать несколько карт одновременно


Если вы планируете использовать несколько различных компонентов KeplerGL в рамках одного приложения, то каждому из них в параметрах обязательно нужно задать уникальный id, необходимый для добавления/обновление данных и конфигураций каждой из карт:


const wrapToParking = wrapTo(' parking_map');

dispatch(
    wrapToParking(
        addDataToMap({
            datasets,
            config
        })
    ));

Альтернативным вариантом является использование функции connect из Redux и функции forwardTo из kepler.gl. В этом случае достаточно просто для соответствующей функции диспетчера указать id соответствующей карты:


import KeplerGl from 'kepler.gl';
import { forwardTo, toggleFullScreen } from 'kepler.gl/actions';
import {connect} from 'react-redux';

const MapContainer = props => (
  <div>
        <button onClick=() => props.keplerGlDispatch(toggleFullScreen())/>
        <KeplerGl id="foo" />
  </div>
)

const mapStateToProps = state => state
const mapDispatchToProps = (dispatch, props) => ({
    dispatch,
    keplerGlDispatch: forwardTo(‘foo’, dispatch)
});

Заключение


KeplerGl позволяет добавить в веб-приложение, созданное на основе React, красочные интерактивные карты. Благодаря использованию фреймворка desk.gl компонент он может легко отобразить миллионы гео-точек в удобном для их просмотра и анализа формате.


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


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


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


Полезные ссылки


  1. Полный код демо-приложения
  2. Вводная статья о Kepler.Gl на Habr
  3. Репозиторий kepler.gl на GitHub
  4. Официальная документация по kepler.gl [en]
  5. Туториал по kepler.gl на Vis.Academy [en]