Время прочтения: 6 мин.

Предположим, что перед пользователем стоит задача обработать большой список адресов/координат, а именно, посчитать расстояния между точками по автомобильным дорогам и определить координаты внушительного списка объектов. Тогда-то он и столкнется с отсутствием возможности пакетной обработки в пользовательском интерфейсе сервиса. Однако, Яндекс об этом позаботился, предоставив в условно-бесплатное пользование JavaScript API.

Разработчики на JavaScript API получают техническую поддержку, подробную документацию, инструментарий для тестирования кода в “песочнице” на сайте проекта

Итак, попробуем воспользоваться этим инструментом для решения задачи геокодирования и построения маршрутов. Понадобится учетная запись Яндекс и API ключ для сервиса “JavaScript API и HTTP Геокодер”, который можно создать в Кабинете разработчика . Не будем подробно останавливаться на этом моменте, в сети достаточно информации, чтобы самостоятельно разобраться.

JavaScript API работает только в браузере, поэтому для выполнения кода нужно разработать web-страничку. В данном случае можно обойтись статичным html-файлом, т.е. читать исходные данные и записывать результат в html-элементы, т.е. для упрощения обойдемся без использования серверной части. Итак, запускаем текстовый редактор и создаем заготовку будущей web-страницы.

<!DOCTYPE html>
<html>
  <head>
    <title>JavaScript API Yndex.Map</title>
    <meta charset="utf-8" />
    <script src="https://api-maps.yandex.ru/2.1/?apikey=<ваш API-ключ >=ru_RU" type="text/javascript"></script>
   <script type="text/javascript"></script>
  </head>
  <body style="font-family: Arial, Helvetica, sans-serif;">
    <textarea id="data" class="area" rows=5 ></textarea>
    <div><button id="process_button" class="button process" onclick="start_process()">Start</button></div>
    <textarea id="target" class="area" rows=5 ></textarea>
    <div><button id="clear_button" class="button clear" onclick="clear_result()">Clear</button></div>
  </body>
</html>

Тут будет два многострочных текстовых поля, первое для ввода исходных данных (data) и второе для записи результата (target), и две кнопки для запуска процесса обработки и очистки поля с результатом.

Обратите внимание, в заголовке страницы в элементе script подключается так необходимый нам API от Яндекса, а в параметрах url атрибута src необходимо указать тот самый секретный api-ключ, полученный в кабинете разработчика.

После загрузки страницы, в глобальном контексте выполнения JavaScript станет доступен объект ymaps, через который получаем доступ к API, в том числе и к необходимым функциям геокодирования и маршрутизации. Напишем две функции “router” и “geocoder”, которые, как не сложно догадаться, реализуют построение маршрута и выполняют геокодирование. Конечно же JavaScript API реализуют значительно больше картографических сервисов и даже получаемые тут данные о маршруте и местоположении являются далеко не полными. Полное описание используемых методов доступно по ссылке, указанной в начале этого материала. Но вернемся к поставленной задаче и разберемся сначала с функцией “router”:

      function router(from, to){
        return ymaps.route([from,to], {multiRoute: false, routingMode: "auto"})
          .then(r => {return {from: from, to: to, dist: r.getLength()}} )
          .catch(e => {console.log(e); return {from: from, to: to, dist: 'route error'}});
      }

В качестве аргументов она принимает пункты отправления и назначения, которыми могут быть как строками с адресом, так и массивами [широта, долгота]. Эти аргументы передаются в метод route объекта ymaps вместе с параметрами построения маршрута, в данном случае routingMode: “auto” – это указание строить именно автомобильный маршрут. Метод возвращает promise-объект, содержащий в том числе протяженность маршрута в метрах, который затем и возвращает функция.

Очередь функции “geocoder”:

      function geocoder(place){
          return ymaps.geocode(place, {results: 1}).then(
            r => {
              let geo = r.geoObjects.toArray()[0];
              let data = geo.properties.get('metaDataProperty').GeocoderMetaData;
              let point = geo.geometry.getCoordinates()
              let result = {point: point, place: place, kind: data.kind, precision: data.precision, text: data.text};  
              return result;}
          ).catch(e => {console.log(e); return {place: place, point: 'geocoding error', text: null, kind:null, precision: null}})
      }

Функция принимает адрес строкой или массив [широта, долгота]. Этот параметр передается в метод geocode объекта ymaps вместе с параметрами геокодирования, в данном случае results: 1 – указание вернуть только один, наиболее точный, результат. Функция вернет promise-объект, содержащий координаты, тип, наименование, точность геокодирования.

Promise (обещание) – специальный объект JavaScript, который используется для написания и использования асинхронного кода”

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

(async ()=>{
 console.log(await geocoder(“Москва”));
 console.log(await router(“Москва”,”Санкт-Петербург”));
})()

то в консоль браузера будут записаны результат геокодирования объекта “Москва” и информация о протяженности автомобильного маршрута “Москва -> Санкт-Петербург”

Остается только реализовать построчную обработку содержимого элемента “data” и запись результата в элемент “target”. Ниже приводится итоговое содержимое html-файла, в котором это реализуется функцией “start_process”.

<!DOCTYPE html>
<html>
  <head>
    <title>JavaScript API Yndex.Map</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="css/style.css">
    <script src="https://api-maps.yandex.ru/2.1/?apikey=<ваш API-ключ >&lang=ru_RU" type="text/javascript"></script>
    <script type="text/javascript">
      function clear_result() {
        document.getElementById("target").value= "";
      }
      async function start_process() {
        let data = document.getElementById("data").value.split("\n")
        let target = document.getElementById("target")
        for (row of data) {
          await new Promise(r => setTimeout(r, 50));
          let arr = row.split(";")
          let result = (
            arr.length > 1
            ? await (()=>{
              let from = iscoord(arr[0])? arr[0].split(",",2): arr[0] ;
              let to = iscoord(arr[1])? arr[1].split(",",2): arr[1] ;
              return router(from,to).then(r => {
                return `${r.from};${r.to};${r.dist}`;
              })})()
            : await (()=>{
              let place = iscoord(arr[0]) ? arr[0].split(",",2) : arr[0];
              return geocoder(place).then(r => {
                return `${r.place};${r.point};${r.text}`;
            })})()
          )
          target.value += result + "\n"
        }
      }
      function router(from, to){
        return ymaps.route([from,to], {multiRoute: false, routingMode: "auto"})
          .then(r => {return {from: from, to: to, dist: r.getLength()}} )
          .catch(e => {console.log(e); return {from: from, to: to, dist: 'route error'}});
      }
      function geocoder(place){
          return ymaps.geocode(place, {results: 1}).then(
            r => {
              let geo = r.geoObjects.toArray()[0];
              let data = geo.properties.get('metaDataProperty').GeocoderMetaData;
              let point = geo.geometry.getCoordinates()
              let result = {point: point, place: place, kind: data.kind, precision: data.precision, text: data.text};  
              return result;}
          ).catch(e => {console.log(e); return {place: place, point: 'geocoding error', text: null, kind:null, precision: null}})
      }
      function iscoord(value) {
        let regexp = /\d+(?:\.\d+)*,\d+(?:\.\d+)*/ ;
        return regexp.test(value) 
      }
    </script>    
  </head>
  <body style="font-family: Arial, Helvetica, sans-serif;">
    <textarea id="data" class="area" rows=5 ></textarea>
    <div><button id="process_button" class="button process" onclick="start_process()">Start</button></div>
    <textarea id="target" class="area" rows=5 ></textarea>
    <div><button id="clear_button" class="button clear" onclick="clear_result()">Clear</button></div>
  </body>
</html>

Страница в браузере с результатами обработки выглядит примерно следующим образом:

Верхнее текстовое поле для вставки исходных данных, в нижнее пишется полученный результат. Если в строке исходных данных встречается разделитель (разделителем между пунктом отправления и прибытия является “;”), то осуществляется расчет маршрута, в противном случае выполняется геокодирование сроки (в примере на картинке выше “Лондон”). Также регулярным выражением определяется, являются ли исходные данные географическими координатами (широта и долгота разделены запятой) или строкой адреса, при этом адреса и координаты можно комбинировать при построении маршрута.

Внимательному читателю наверняка сразу бросилась в глаза вот эта конструкция в листинге кода:

await new Promise(r => setTimeout(r, 50));

Тут делаем короткую (50 мсек) паузу перед обработкой следующей строки, т.е. искусственно устанавливается предел максимальной скорости обработки в 20 строк в секунду. Спросите, зачем? И тут настало время упомянуть о бесплатном сыре технических лимитах бесплатного использования JavaScript API от Яндекса. А их два, но они довольно «вкусные»:

  • Суточное ограничениедо 25000 запросов к API (каждый вызов метода geocode или route считается за 1). Информация о расходовании суточного лимита доступна в Кабинете разработчика.
  • Кол-во запросов в секундуне более 50

Т.е. пауза между вызовами API нужна, чтобы не превысить «скоростной лимит», установленный Яндексом.

На этом, пожалуй, всё, “за кадром” осталась css-таблица стилей, а также разработка backend на Node.js, но это — уже совсем другая история.