[Из песочницы] Динамическая таблица поверх Google Maps

Habrahabr 6

Введение

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

К сожалению (или к счастью?), готовых решений я не нашёл. Google Карты позволяют накладывать маркеры и фигуры на карты, но эти способы представляют слишком мало информации. С Яндекс картами оказалось не лучше. Но Карты Гугл имеют механизм пользовательских наложений с HTML-содержанием. И для инкапсуляции этой работы с картами и наложениями я создал JavaScript библиотеку GMapsTable. Возможно, кому-нибудь она окажется интересной или полезной. Рабочий пример.

screen0

screen1

Условности

Чтобы не возникло путаницы, параметр zoom будем называть приближением карты, а scale — масштабом. Первый относится к Google Maps API, а второй к описываемой библиотеке.

Задача в целом

Итак, что у нас есть? Какой-нибудь источник данных (например, сервер с базой данных, обрабатывающий и посылающий данные в формате JSON) и веб-страничка с JavaScript, которая запрашивает данные и визуализирует их на Картах Гугл.

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

Основное содержание HTML страницы для GMapsTable:

 ..в <head>:
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY"></script>
<script src="http://www.aivanf.com/static/gmt/gmapstable.js"></script>

 ..в <body>:
<div id="map"></div>

GMapsTable позволяет абстрагироваться от взаимодействия с GoogleMaps API. Вам нужно лишь предоставить подходящий объект с данными. Время перейти к JavaScript'y! Чтобы использовать GMapsTable, нужно всего лишь получить объект DataContainer для Вашего div'a карты:

// Аргумент: ID div'а
//   и словарь параметров GoogleMaps,
//   это не обязательно
var container = new DataContainer("map", {
    zoom: 9,
    center: {lat: 55.7558, lng: 37.6173},
    mapTypeId: 'roadmap'
});

И затем нужно передать парочку функции:

container.dataLoader = function (scale, borders) {
    ... вызвать container.processData(some_data);
}

container.scaler = function (zoom) {
    ... return какое-нибудь число;
}

Но что именно писать внутри функций?.. Для начала разберёмся, как работает GMapsTable.

Data для DataContainer

DataContainer занимается отображением Ваших данных и заботится о том, когда оно должно быть обновлено. В самом начале и когда изменяются приближение и границы "камеры", он пробует использовать сохранённые данные, а если их нет, то вызывает функцию dataLoader. Вам нужно сгенерировать объект с данными и передать его функции DataContainer.processData. Структура объекта должна быть такая:

data: {
    minLat: float,
    difLat: float,
    minLon: float,
    difLon: float,
    scale: int,
    table: [
        [value, value, ...],
        [value, value, ...],
        ...
    ],
    tocache: boolean
}

Значением (value) может быть число, строка или любой объект, если вы укажите собственную функцию форматирования ячейки таблицы. Масштаб (sale) это целое число, говорящее, на сколько частей должны делиться единицы широты и долготы. Параметр tocache указывает, должны ли данные для текущего масштаба быть сохранены и более не запрашиваться.

Пример объекта данных
data: {
    minLat: 55.0,
    difLat: 2.0,
    minLon: 37.0,
    difLon: 1.0,
    scale: 2,
    table: [
        [1, 3, 0, 1],
        [0, 1, 2, 0]
    ],
    tocache: true
}

Здесь данные покрывают область от 55.0, 37.0 до 57.0, 38.0 и делят каждую единицу широты и долготы на 2 части (получается, одна клетка широты-долготы делится на 4 части). Также здесь указано, что для данного масштаба это полные данные, и они должны быть сохранены для использования в дальнейшем.

Перевод приближения в масштаб

Приближение (zoom) это параметр Google Maps API, целое число между 1 (карта мира) и 22 (улица). Запрашивать и хранить данные для каждой единицы приближения неудобно и нецелесообразно, поэтому GMapsTable переводит их в масштаб (scale) — число, указывающее, на сколько частей нужно делить единицу широты и долготы.

Сохранение данных

Чтобы отображение при изменении масштаба было моментальным, GMapsTable хранит наборы данных для некоторых (либо всех) масштабов. Например, у меня была база данных с координатами почти со всей России — около 42 тысяч ячеек для масштаба 10 (500 КБ, довольно легко хранится и обрабатывается у меня в десктопном браузере) и 17 миллионов для масштаба 200 (несколько МБ, вызывает значительные подвисания). Поэтому сервер оценивает число ячеек всех данных, и если их немного, отправляет данные из всей БД, иначе только для запрошенного региона. Получается такой алгоритм:

алгоритм обновления таблицы GMapsTable

Границы (bounds) — это объект JavaScript с полями minlat, maxlat, minlon, maxlon — текущими границами Google Maps и хорошим отступом про запас.

В Вашей реализации dataLoader Вы можете смело игнорировать аргументы, если нет нужды использовать разную детализацию для разных масштабов или если Ваши данные не покрывают такой большой регион. Просто передайте данные и их границы по широте и долготе и scale, на сколько разбиваете единицы широты-долготы. Но для полноты картины я предлагаю такое поведение функции dataLoader (или сервера, к которому она обращается):

алгоритм генерации объекта данных для GMapsTable

Список всех параметров

Вы можете указать такие параметры для DataContainer:

1) scaler(zoom) — переводит приближение из GoogleMaps в масштаб для GMapsTable. Оба целые числа.

2) dataLoader(scale, borders) — вызывается, когда нужны новые данные. Должен передать объект данных в DataContainer.processData(data).

Аргумент borders это объект JavaScript с полями minlat, maxlat, minlon, maxlon — текущими границами Google Maps и хорошим отступом про запас.

3) tableBeforeInit(map, table, data) — вызывается перед тем, как таблица начинает заполняться ячейками. Аргумент map это объект Google Maps, table — HTML элемент созданной таблицы, а data — предоставленный Вами объект данных для текущего масштаба. Здесь можно настроить таблицу, какие-нибудь переменные в используя актуальные данные или текущие параметры карты.

4) cellFormatter(td, val) — вызывается для заполнения ячейки. td это HTML element, ячейка таблицы. val это данные из Вашего объекта данных. Здесь можно легко настроить вывод нескольких значений или заливку цветом в соответствии с какими-либо параметрами.

5) boundsChangedListener(zoom) — вызывается, когда изменяются границы Google Maps.

6) minZoomLevel, maxZoomLevel — переменные для минимального и максимального приближения карты. Целые числа между 1 (карта мира) и 22 (улица).

Для успешной работы DataContainer необходимы только первые две функции.

Пример и исходники

Полный и хорошо прокомментированный пример использования: HTML-страничка и JS-код. А также есть GMapsTable в GitHub.