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

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

Вкратце моя работа состояла из:

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

Базовые API Google Maps позволяют делать все это, я только добавил небольшую библиотеку для агрегирования значков ближайших маркеров, чтобы карта оставалась читаемой.

Я не мог использовать свои исходные данные для этой статьи, поэтому я нашел в Интернете список примерно 400 муниципалитетов Лацио (Италия) с некоторыми географическими данными, чтобы воспроизвести тот же процесс, что и в моей реальной работе (демонстрационные данные были скачано с сайта ИСТАТ).

Хотя данные не те, что использовались изначально (и несмотря на то, что их количество меньше), конечный результат абсолютно идентичен.

Требования

В первую очередь вам понадобится Google Maps Javascript API.

Вам нужно просто связать их со своей страницей, добавив свой Ключ API, как указано в документации по API:

Все приложения API Карт должны загружать API Карт с помощью ключа API. Использование ключа API позволяет отслеживать использование Maps API вашим приложением и гарантирует, что Google сможет связаться с вами по поводу вашего приложения, если это необходимо. Если использование Maps API вашим приложением превышает лимиты использования, вы должны загрузить Maps API с помощью ключа API, чтобы приобрести дополнительную квоту.

Помимо Google Maps, я добавил на страницу библиотеку Marker Clusterer Plus.

Marker Clusterer является частью Библиотеки служебных программ Google Maps, Проекта с открытым исходным кодом, который должен стать центральным хранилищем служебных библиотек, которые можно использовать с Google Maps API JavaScript v3. Мы будем использовать Marker Clusterer для агрегирования значков маркеров на карте.

О демо

Для демонстрации я подготовил файл json, содержащий 378 записей обо всех муниципалитетах Лацио, Италия. В реальном мире, конечно, файл будет создаваться динамически из базы данных.

Файл json имеет следующую структуру (каждая запись представляет муниципалитет):

[
 {
 “province_code”: “RM”,
 “denomination”: “Roma”,
 “chief_town”: 1,
 “altitude_zone”: 5,
 “mountain_zone”: “P”,
 “area”: 1287.7586,
 “population”: 2617175,
 “lat”: 41.9027835,
 “lng”: 12.496365500000024
 }
]

Я использовал поля «широта» и «долгота» для размещения маркера на карте, зоны «высота» и «горы» в качестве ключей фильтрации, а все остальное отображалось как дополнительная информация при выборе отдельных маркеров. Кроме того, логическое поле `chief_town` использовалось для изменения формы значка.

Построение интерфейса

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

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

Всякий раз, когда добавляется фильтр, или когда выбирается метод окраски, или даже когда установлен флажок «агрегировать маркеры», маркеры перерисовываются.
Изменение метода окраски также обновляет небольшую легенду в нижней части формы.

Значки маркеров

Самый простой способ управлять формой и цветом значков — использовать пути SVG.
Маркеры Google Maps поддерживают отображение векторных путей с использованием обозначения пути SVG.

Это может показаться сложным, но все, что вам нужно, это создать значок с помощью Illustrator, Sketch или предпочитаемого вами редактора.

Я нарисовал свои иконки в Sketch, а затем экспортировал их в SVG.

Затем я открыл файл SVG в редакторе. Вы можете видеть, что каждая иконка состоит из уникального объекта `path` с атрибутом `d`: его значением является код, используемый для векторных путей маркеров.

Мне также нужно использовать значки в качестве изображения на моей странице (в этой демонстрации я использовал «маркер звездочки» в легенде), поэтому я заменил значки на символы и добавил на свою страницу весь код SVG, как вы можете видеть ниже. (значения `d` были сокращены для удобства):

<svg style=”display:none”>
 <symbol id=”std_marker” viewBox=”0 0 18 30">
 <path d=”M18,9.69 C18,16.98 …”></path>
 </symbol>
 <symbol id=”asterisk_marker” viewBox=”24 0 18 30">
 <path d=”M33,0 C28.11,0 24,4 …”></path>
 </symbol>
</svg>

Теперь я могу использовать значки в своем HTML-коде как символы SVG, и в то же время я смогу получить векторный путь с помощью нескольких javascript.

Запуск двигателя

При загрузке страницы сразу выполняется набор небольших задач:

  • Определены некоторые переменные:
  • объекты altitude и горные зоны (которые используются как для декодирования некоторых значений json, так и для построения содержимого полей выбора «фильтровать по…» в форме
  • список цветов в шестнадцатеричном формате, которые будут использоваться для окраски маркеров
  • ссылки на какой-то элемент на странице, который будет использоваться позже
  • создается экземпляр функции `buildLegend`. Он используется для создания легенды каждый раз, когда пользователи изменяют значение «Цвет маркера в соответствии с…».
  • над страницей добавляется оверлей `div`. Его областью действия является предотвращение доступа к форме и добавление сообщения для пользователей, пока данные не будут загружены.

Теперь мы можем загрузить наш файл json.

Загрузка данных

Есть несколько разных вариантов загрузки данных, в основном это зависит от того, как вы можете получить к ним доступ.

Если ваши данные находятся в том же домене вашего скрипта, лучше всего выполнить запрос `ajax`, но если вы должны вызывать данные из другого источника (как в моем случае), вы должны использовать CORS или JSONP.

CORS – это механизм для включения запросов на стороне клиента из разных источников, который является рекомендацией W3C с января 2014 года; он хорошо поддерживается большинством современных браузеров, но имеет некоторые проблемы с устаревшими браузерами (подробнее см. caniuse.com). Дополнительные сведения см. в статье Использование CORS на сайте HTML5 Rocks.
JSONP (JSON с отступами) — это более старый метод, позволяющий обойти политику одного и того же источника. Это чрезвычайно просто и совместимо со всеми браузерами, и, поскольку у этой демонстрации нет особых требований, я решил использовать ее.

Идея, на которой основан JSONP, очень проста. Если вы выполните `XMLHttpRequest`, вы получите объект (обычно XML или JSON), который будет передан функции обратного вызова, которая выполнит всю работу.
Проблема начинается, когда запрос адресован внешнему домену. : политика того же домена из соображений безопасности блокирует все подобные запросы.

Но мы знаем, что мы можем загрузить файл javascript из любого домена через тег script, и именно так JSONP загружает данные.

Вместо вызова `XMLHttpRequest` ваш сценарий должен выполнить инъекцию DOM и вызвать сценарий JS, который содержит вызов функции (обратный вызов), который вы уже создали. Функция передает уникальный аргумент: объект данных json, который таким образом может обойти ограничение на тот же домен.

Вот что это происходит:

Сначала мы выполняем инъекцию DOM:

var head = document.getElementsByTagName(“head”)[0],
 script = document.createElement(“script”);
 
script.type = “text/javascript”;
script.src = “http://your.remote.domain/jsonp_file.js";
head.appendChild(script);

Файл JSONP содержит вызов функции обратного вызова:

my_callback([item1, item2, …, itemN]);

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

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

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

Собираем все вместе

Давайте резюмируем, что происходит: были созданы экземпляры некоторых переменных и функций, было показано сообщение «Загрузка данных… пожалуйста, подождите», некоторые данные были вызваны через JSONP, и после того, как все они были загружены, был вызван обратный вызов.

В демо функции обратного вызова называется mapData_callback, и она сделает почти всю работу.

Во-первых, он настроил карту: с помощью функции «Геокодер» инициализируется новая карта с центром в Риме, Италия.

// this function receives the json data and performs all tasks
mapData_callback = function(map_data) {
// loading Google Maps
 var geocoder = new google.maps.Geocoder();
 geocoder.geocode( { ‘address’: ‘Rome, Italy’}, function(results, status) {
 if (status === google.maps.GeocoderStatus.OK) {
 
 // do something
 
 } else {
 alert(“Can’t draw map: \n” + status);
 }
}); // end geocoder.geocode
}; // end mapData_callback

Если все пойдет хорошо, и карту можно нарисовать (`google.maps.GeocoderStatus.OK`), мы можем приступить к выполнению всех наших действий.
Опять же, сначала создаются экземпляры некоторых переменных:

  • `mapOptions`: некоторые настройки карты, полный список всех доступных опций смотрите в спецификации объекта google.maps.MapOptions
  • `mcOptions`: параметры для кластеризатора маркеров (мы рассмотрим их позже)
  • `mc` и `markers`: значения по умолчанию для кластеров маркеров и стандартных маркеров gmaps.
  • `infoWindow`: объект Google Maps InfoWindow, он будет отображаться при щелчке маркеров.
  • `standard_marker_shape` и `chief_towns_marker_shape`: векторные формы для значков маркеров, они извлекаются из символов SVG в верхней части страницы с помощью метода `getAttribute`:
document.getElementById(‘std_marker’).querySelector(‘path’).getAttribute(‘d’)
  • `form_fields`: ссылка на все поля в форме (обратите внимание, что я всегда использую термин «форма» для обозначения набора полей, используемых в приложении, но на странице нет реального элемента `form`)
  • `map_wrapper`: контейнер карты

Наконец, создается экземпляр переменной `map` и генерируется карта:

map = new google.maps.Map(map_wrapper, mapOptions)

Теперь карта (без маркеров) отображается, и мы можем удалить сообщение о загрузке. Далее мы можем начать работать с маркерами.

Все задачи маркеров управляются с помощью функции «addMarkers». Эта функция присоединена как слушатель ко всем полям формы (которые мы ранее ссылались на переменную `form_fields`). Каждый раз, когда их значение изменяется, вызывается функция addMarkers.

//adding a listener to add fields in the form to make them call the addMarkers function on change
for( i = 0; i < form_fields.length; i++ ) {
 form_fields[i].addEventListener(‘change’, addMarkers);
}
addMarkers(); // first run

Удаление предыдущих маркеров

Прежде чем добавлять маркеры, мы должны очистить любой предыдущий.
Мы можем найти два типа маркеров: стандартные Google Maps и Marker Clusterer, поскольку у них есть разные способы удаления, мы должны удалить маркеры двумя способами.

Marker Clusterer, ссылающийся на переменную `mc`, имеет специальный метод для удаления маркеров. если `mc` не равен нулю (это означает, что кластеризатор маркеров был активирован), мы можем использовать метод `removeMarkers` для очистки карты.

Стандартные маркеры Google Maps ссылаются на массив `markers`. Чтобы удалить их, мы должны установить расположение каждого маркера на карте в «null».

Затем мы можем повторно инициализировать переменные `mc` и `markers`, установив их значения по умолчанию.

// marker clusterer removing
if(mc !== null) { 
 mc.removeMarkers(markers, false);
}
// Google Maps markers removing
for(i = 0; i<markers.length; i++){
 markers[i].setMap(null);
}
// markers variables reset
markers = []; // Google Maps
mc = null; //markerClusterer

Фильтрация данных

Функция addMarkers также содержит задачу фильтрации данных. Поскольку эта функция вызывается при изменении значений поля, мы должны каждый раз проверять, запрашивается ли фильтр данных.

Мы можем использовать метод фильтр, чтобы быстро выбрать только те данные, которые нам нужны.

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

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

Кроме того, внизу формы печатается количество отфильтрованных элементов.

// data filtering
var filtered_data = map_data.filter(function (row) {
 var test = [],
 mountain_zone_filter_value = mountain_zone_select.options[mountain_zone_select.selectedIndex].value,
 altitude_zone_filter_value = altitude_zone_select.options[altitude_zone_select.selectedIndex].value;
 if(mountain_zone_filter_value) {
 test.push( row.mountain_zone === mountain_zone_filter_value );
 }
 if(altitude_zone_filter_value) {
 test.push( row.altitude_zone === Number(altitude_zone_filter_value) );
 }
 return test.every(function (item) {return item !== false;});
});
// found items message
document.getElementById(‘found_items’).innerHTML = filtered_data.length;

Наконец, мы можем добавить маркеры.

Добавление маркеров

Теперь нам нужно циклически обрабатывать массив `filtered_data` и добавлять маркеры (из Google Maps) для каждого элемента.

Эта задача требуется всегда, даже если пользователь выбрал параметры «агрегирования маркеров»: надстройке Marker Clusterer для работы требуется набор маркеров Google Maps.

Мы должны создать объект `google.maps.Marker` для каждого элемента массива. Каждый маркер имеет следующие параметры:

  • position: его местоположение на карте.
  • title: определяет атрибут title маркера.
  • icon: определение объекта значка. Он содержит параметр путь (векторную форму значка, которую мы уже определили с помощью наших значков SVG) и некоторые другие параметры отображения (заливка, обводка, масштаб и т. д.). Полную справку см. в спецификации объекта google.maps.Symbol.

Затем маркер связывается с картой (в данном случае с помощью метода setMap) и добавляется в массив маркеров:

// markers
var this_item, this_marker,
// markers: color criterion chosen by user
colorBy_item = document.querySelector(‘.marker_color_button:checked’).value,
// conversion to array of parameter object
//Object.keys returns an array of strings even if keys are numbers
color_category_array = Object.keys(parameters[colorBy_item])
;
for(i = 0; i < filtered_data.length; i++) {
 this_item = filtered_data[i];
 
this_marker = new google.maps.Marker({
 position: new google.maps.LatLng(this_item.lat, this_item.lng),
 title: this_item.label,
 icon: {
 path: this_item.chief_town? chief_towns_marker_shape : standard_marker_shape ,
 fillColor: colors[color_category_array.indexOf(String(this_item[colorBy_item]))],
 fillOpacity: 1,
 strokeColor: ‘#000’,
 strokeWeight: 1,
 scale: 1, //18x30px
 anchor: new google.maps.Point(9,30)
 }
 });
 this_marker.setMap(map);
 makeinfowindowCallback(this_marker, this_item);
 markers.push(this_marker);
}

У каждого маркера есть `infoWindow` для отображения некоторой информации при щелчке по нему (дополнительную информацию см. в Документации по кластеру маркеров). Поскольку infoWindow построен внутри цикла, нам нужна функция обратного вызова (`makeinfowindowCallback`), чтобы она работала правильно (посмотрите на страницу закрытия MDN).

Теперь нам нужно агрегировать маркеры (если пользователь выбрал эту опцию).
Нам нужно только вызвать конструктор Marker Clusterer с тремя параметрами:

  • объект Карта (`map`)
  • массив со всеми ранее сгенерированными маркерами (`markers`)
  • объект со всеми свойствами MC (ранее определенные `mcOptions`).

Некоторые примечательные вещи о `mcOptions`:

  • `maxZoom`: максимальный уровень масштабирования, при котором включается кластеризация, другими словами, вы можете выбрать уровень масштабирования, в пределах которого объединяются маркеры.
  • `url`: URL-адрес файла изображения значка кластера. Для этой демонстрации я нарисовал значок SVG, но вы можете использовать любой формат изображения.
// Marker Clusterer
if(document.getElementById(‘aggregate’).checked) {
 mc = new MarkerClusterer(map, markers, mcOptions);
}

Полную документацию можно найти на странице MarkerClustererPlus для Google Maps V3.

Сыграй сам

Скрипт работает во всех современных браузерах, включая IE10+. Вам понадобится некоторое исправление, чтобы заставить его работать с самым старым IE. Сыграйте сами на http://codepen.io/massimo-cassandro/pen/XJGgvg/