Введение


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


К сожалению (или к счастью?), готовых решений я не нашёл. 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.

Поделиться с друзьями
-->

Комментарии (8)


  1. prostofilya
    15.06.2017 04:31

    И всё-таки я не понял, чем плохи те же маркеры с текстом, к тому же они с гео-привязкой. Вообще, зачем что-то кидать на карту, не имеющее геопривязку?) В какой- нибудь kml бы сконвертили табличку и всё.


    1. AivanF
      15.06.2017 07:17

      В смысле??) В статье же написано, что данные имеют геопривязку, в этом и смысл работы библиотеки: «понадобилось отображать заказы сгруппированные по широте и долготе», «Каждой области можно поставить в соответствие: число заказов, клиентов и среднюю сумму. Поэтому данные могут и должны отображаться с разной детализацией для разных приближений».

      А маркеры плохи тем, что ими не передашь несколько параметров наглядно, у меня же легко настраивается кастомное оформление ячеек. И в приложении, которое сделал по работе с этой библиотекой, в каждой ячейке как раз показаны и число заказов, и средняя сумма заказов, и число клиентов; а цвет фона вообще по другим параметрам задаётся. Да, можно было бы скриншот этого дела показать для наглядности) или в пример встроить, что я сейчас сделаю.


      1. prostofilya
        15.06.2017 07:26

        Вот у вас ячейка, что ей можно покрыть с такой точностью? регион? район? геопривязка ведь не точная (взять в сравнении с полигонами этих регионов/районов).

        А маркеры плохи тем, что ими не передашь несколько параметров наглядно

        Маркер имеет цвет, подпись, размер. у ячейки же произвольный размер отпадает. Я уж не говорю про полигоны.
        А ещё точки можно в кластеры объединять


        1. AivanF
          15.06.2017 08:56

          Да, интересный этот OpenLayers, много разных клёвых возможностей. Но всё же для моей задачи больше подходит отображение данных на сетке, с заливкой фона в зависимости от заданного условия. Произвольная форма как раз не нужна, размер же у меня указывается легко. Да, точность позиционирования ячеек вряд ли 100-процентная, но опять же, наглядность вывода важнее, и в целом поставленную задачу GMapsTable решает идеально)


  1. snakendead
    15.06.2017 08:01

    Делал подобное, только для iOS. Мы выбрали Mapbox — более удобный и с инструментами overlay, etc. К тому же понадобилось прикрутить кластеризацию сетки, и сама сетка была основана на шестиугольниках. Крайне интересная задача :)


  1. gaploid
    15.06.2017 15:36

    А Leaflet не смотрели http://leafletjs.com? Очень похоже, что там задача кластеризации уже решена уже и он очень оптимизирован под большое количество обьектов. Вот к примеру https://github.com/Leaflet/Leaflet.markercluster и вот на базе него https://github.com/SINTEF-9012/PruneCluster и в самой библиотеке есть куча всего.


    1. AivanF
      15.06.2017 17:14

      Классная вещь! Но как и в комментарии выше, это не совсем то, что мне было надо. Цель моей библиотеки не в кластеризации данных, а в их детализированном отображении (cама же кластеризация происходит на сервере с БД). Поэтому заказы здесь должны отображаться не как независимые маркеры, а в виде таблицы с разной детализацией и с выводом в ячейках нескольких параметров.


  1. dmitryredkin
    21.06.2017 19:39

    А почему не тепловая карта?