В прошлой статье я кратко рассказала о возможностях 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]