Простейший редактор для Google Maps

В предыдущей заметке про Google Maps я рассказывал, как выполнять основные действия с картой и маркерами. Теперь же попробуем создать полноценный редактор, в котором можно будет добавлять, редактировать и удалять маркеры, а также выводить их список рядом с картой и перемещаться к обозначенному маркером месту по щелчку. Кроме того, редактор будет сохранять созданные маркеры между сеансами работы с картой (но сохранять в localStorage, иначе пришлось бы писать серверную часть).
Общая инициализация в целом остается такой же, как в прошлом примере. В HTML кроме divа для самой карты создадим еще список, в котором будем выводить названия и координаты маркеров и используем flex-верстку, чтобы разместить его рядом с картой, а карту сделать расширяемой:
<div id="all" style="display: flex"> <div style="flex-grow: 1; min-width: 210px"> <ul id="point_list"> </ul> </div> <div id="map_canvas" style="flex-grow: 4;min-width:600px; height:500px"></div> </div>
JavaScript-код инициализации остается без изменений:
<script type="text/javascript"><!-- MyMap = function () {     self = this; // запоминаем this, указывающий на MyMap в self,     // это нам пригодится, чтобы обращаться к содержимому MyMap из всяких обработчиков событий, у которых this указывает на другие объекты     this.map = new google.maps.Map(document.getElementById("map_canvas"), {       zoom: 10,       center: { lat: 55.763585, lng: 37.560883 }, // коордианты можно задавать и в таком виде       mapTypeId: google.maps.MapTypeId.ROADMAP     });     this.markers = new Array; // здесь будут храниться все созданные маркеры // функция moveMapTo порождает замыкание, которое запоминает переданные координаты и возвращает функцию для сдвига карты на них     this.moveMapTo = function(lat,lng) {         return function(e) {           self.map.panTo(new google.maps.LatLng(lat,lng));         }     };     // сюда будем вставлять функции, описанные дальше }; myMap = new MyMap; google.maps.event.addDomListener(window, 'load', myMap); --></script>

Первое, что нам потребуется — это функция создания нового маркера makeMarker. Она будет вызываться в двух случаях: при двойном щелчке по карте или при ее загрузке и получении списка маркеров из сохраненных данных и получать в качестве параметров координаты и название маркера. Маркеры можно будет свободно переносить по карте в любой момент, для чего сразу ставим свойство draggble в true. Но перенос маркера нужно будет отслежвать, а кроме того, предусмотреть возможность его редактирования и удаления. Поэтому с помощью google.maps.event.addDomListener добавим два обработчика событий: dragend — окончание перетаскивания (в нем будем обновлять координаты в списке слева), dblclick — двойное нажатие, по которому будем редактировать название. Особенность этой функции в том, что она может принимать как обычные DOM-элементы, так и объекты Google Maps, такие как маркеры или даже саму карту.
    this.makeMarker=function (coords,m_title) {       var marker = new google.maps.Marker({         position: coords, map: self.map, title: m_title, draggable:true       });       google.maps.event.addDomListener(marker,'dblclick',self.editMarker);       google.maps.event.addDomListener(marker,'dragend',self.onMarkerMove);
Далее создадим элемент li для списка слева, в котором выводятся все маркеры, и сохраним ссылку на него в самом маркере, в поле list_item. Эта ссылка будет использоваться в обработчиках выше, чтобы изменять координаты и название при редактировании маркера. Кроме того, на событие onclick элемента списка повесим функцию, которая будет передвигать карту к маркеру, который связан с этим элементом. В общем случае можно было бы добавить в элемент li ссылку на маркер, и использвать ее в обработчике для определения координат и перемещения карты. Но в данном случае оказалось проще воспользоваться уже готовой функцией moveMapTo. Эта функция использует прием под названием замыкание: возвращает функцию, внутри которой «запомнены» координаты точки, на которую нужно передвинуть карту.
    var elm=document.createElement('li'); // создаем элемент для списка       marker.list_item = elm; // запоминаем ссылку на него в маркере       // здесь же можно было бы присвоить в marker дополнительные данные об объекте, если бы они у нас были:       // marker.data = { field1 : "значение1", field2: "значение2" };       elm.innerHTML=m_title+" <span>("+coords.lat.toFixed(2)+" "+coords.lng.toFixed(2)+")</span>"; // задаем выводимый в элементе текст       elm.onclick=self.moveMapTo(coords.lat,coords.lng); // задаем обработчик для перехода к маркеру на карте       document.getElementById('point_list').appendChild(elm); // добавляем элемент в список слева
Далее запоминаем созданный маркер в масив маркеров и вызываем функцию onUpdate, которая отвечает за сохранение всех созданных элементов. На этом функция makeMarker готова!
      self.markers.push(marker);       self.onUpdate();
Теперь нам нужно создать обработчик, который будет вызывать ее при двойном щелчке по карте. Он создается так же, как любой другой обработчик. Основное отличие — в параметре e приходит существенно больше информации, чем для простых DOM-элементов. В частности, в свойстве latLng приходят географические координаты точки на карте, по которой был сделан щелчок. Далее вешаем этот обработчик на событие dblclick на самой карте с помощью уже упоминавшейся google.maps.event.addDomListener:
    this.onMapDblClick=function (e) {       var title = window.prompt("Введите название точки"); // запрашиваем у пользователя название       self.makeMarker({lat:e.latLng.lat(),lng:e.latLng.lng()},title); // и передаем название и координаты, преобразовав их в хеш, содержащий обычные number     };     google.maps.event.addDomListener(this.map,'dblclick', self.onMapDblClick); // вешаем обработчик на двойной щелчок
Далее нужно создать обработчик двойного щелчка по маркеру. Он будет запрашивать новое его название,  если оно не пустое, запоминать его в маркере и списке, если пустое — удалять соответствующий маркер. Именно в этом обработчике нам и пригодится ссылка на элемент списка слева: по ней мы будем обращаться к нему для изменения или удаления.
    this.editMarker=function(e) {       var pos=self.markers.indexOf(this); // ищем номер текущего маркера в массиве всех созданных маркеров       var new_title = window.prompt("Введите новое название точки или оставьте пустым для удаления",this.title); // запрашиваем новое название       if (new_title=="") { // если оно пустое, удаляем маркер         document.getElementById('point_list').removeChild(this.list_item); // сначала удаляем элемент из списка слева         this.setMap(null); // чтобы убрать маркер с карты, нужно сообщить ему, что теперь он принадлежит несуществующей карте         delete self.markers[pos]; // удаляем маркер из массива запомненных       }       else { // иначе запоминаем новое название и обновляем в списке слева         this.title=new_title;         this.list_item.innerText = new_title+" <span>"+e.latLng.lat().toFixed(2)+", "+e.latLng.lng().toFixed(2)+")</span>"; // вот и пригодилась ссылка на li!       }       self.onUpdate();     };
Теперь обработчик перемещения маркера. Тут ничего сложного или интересного: выводим новые координаты в списке и заменяем обработчик-замыкание для перемещения на новые координаты. В конце всех обработчиков вызываемonUpdate, чтобы сохранить сделанные изменения.
    this.onMarkerMove=function(e) {       this.list_item.innerHTML = this.title+" <span>"+e.latLng.lat().toFixed(2)+", "+e.latLng.lng().toFixed(2)+")</span>"; // обновляем координаты в списке       elm.onclick=self.moveMapTo(e.latLng.lat(),e.latLng.lng()); // вешаем новый обработчик для перемещения карты       self.onUpdate(); // сохраняем изменения     };
И вот теперь пришло время объявить саму функцию onUpdate. По идее, в ней должна производиться отправка данных через AJAX на сервер, чтобы сохранить их там (самый простой вариант — пересылать их в AJAX). Но в данном примере серверной части не предусмотрено, поэтому сохраняем их в локальное хранилище броузера — localStorage. Но объект marker содержит большое количество разной информации, в то время как нам для восстановления состояния в дальнейшем нужны только координаты маркера и его название. Поэтому сначала мы создадим вспомогательный массив из хешей, в которых отберем только долготу, широту и название маркера. Если бы у нас было поле data с дополнительными данными (см. makeMarker), его тоже имело бы смысл добавить в этот хеш. В принципе, заполнение массива можно было бы сделать обычным циклом, но более красивым решением является использование функционального стиля, в частности функции map. Ей передается функция обработчик, которой поочередно передается каждый элемент массива, а из возвращенных ею значений собирается новый массив, который и является результатом работы функции map.
Далее следует учесть, что в localStorage можно сохранять только строки, поэтому получившийся массив сначала преобразуем в JSON. Получаем такой код:
    this.onUpdate=function() {       var data = self.markers.map(function (item) {         return { lat: item.position.lat(), lng: item.position.lng(),title: item.title /*, сюда добавить data: item.data, если бы у нас были дополнительные данные об объекте */ }       });     // тут должен был бы быть XMLHttpRequest для отправки на сервер       window.localStorage['XDemoMap']=JSON.stringify(data); // преобразуем данные в JSON-строку и отправляем в хранилище     };
Теперь остается добавить только код загрузки уже сохраненных данных. Я решил сделать так: если данных нет, создается три новых маркера с фиксированными точками. Выглядит этот код так:
    // если в хранилище нет данных, заполняем карту начальными маркерами     if (window.localStorage['XDemoMap']==undefined) {       self.makeMarker({lat:55.763525,lng:37.560893},"Москва");       self.makeMarker({lat:59.939956,lng:30.344911},"Санкт-Петербург");       self.makeMarker({lat:56.325418,lng:43.935667},"Нижний Новгород");     }     else { // иначе декодируем данные из JSON и для каждого элемента там создаем маркер       JSON.parse(window.localStorage['XDemoMap']).forEach(item=>self.makeMarker(item,item.title)); // используем стрелочную запись функции для экономии места, forEach вызывает ее для каждого элемента массива     }
Таким образом, получился редактор карты, в котором есть все базовые операции: создание, перемещение, редактирование, удаление, сохранение и загрузка объектов. Для его дальнейшего развития нужно заменить вызов prompt на показ формы для редактирования данных и обеспечить их сохранение в поле data. (В этом случае обработчики editMarker и onMapDblClick потребуется разделить на две части: одна будет показывать форму по двойному щелчку, вторая — создавать/изменять данные по нажатию кнопки «Сохранить».) Работающий пример можно посмотреть по ссылке. Остальные заметки серии про работу с Картами Google доступны по тегу Google Maps.