Превращаем созданный ранее скрипт в API для просмотра интерактивной карты с сайта OverpassTurbo.eu через навигационное приложение смартфона.


Содержание:


1 – Вступление. Стандартные растровые карты
2 – Продолжение. Пишем простой растеризатор для векторных карт
3 – Частный случай. Подключаем карту OverpassTurbo


Что такое OverpassTurbo?


Итак. Существует такая база картографических данных, как OpenStreetMaps. В ней собрано все: моря, контуры материков, горы, леса, дороги, здания, детские площадки и даже лежачие полицейские. У каждого объекта есть название, координаты и свойства. Например, у дороги – материал покрытия, у здания – количество этажей и так далее.


Так вот. Большинство карт, которые сегодня представлены в интернете генерируются на основе именно этой базы данных. Но что, если нам не подходят все эти готовые карты? Можно сделать собственную! Ну, или хотя бы дополнить уже существующую, что в разы проще.


Именно этим и занимается сайт OverpassTurbo.eu. Он представляет из себя онлайн IDE. С его помощью можно составить запрос к базе данных OSM. Нажимаем на кнопку Старт, запрос уходит к базе, а к нам через некоторое время возвращаются данные. OverpassTurbo визуализирует эти данные в виде векторных маркеров и линий, располагающихся поверх фонового слоя — карты с сайта OpenSteerMap.org.


Посмотрим на пример. Этот скрипт, который показывает различные источники питьевой воды и их название (автор – Erelen). Чтобы его запустить просто перейдите по ссылке и нажмите Старт. (Если же сайт выдаст ошибку, то зайдите через VPN и попробуйте еще раз)


https://overpass-turbo.eu/s/z95



А вот этот скрипт выделяет оранжевым гравийные дорожки, подходящие для пробежек.


http://overpass-turbo.eu/s/KXU



Фактически, с помощью этого инструмента можно дополнить карту какими угодно данными. И, замечу, что это весьма и весьма довольно увлекательно. Но эта статья не об этом. Если вас заинтересовала данная тема, то можете ознакомиться о основами Overpass здесь.


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


Инструкция для пользователей: как пользоваться нашим API


Итак. Допустим у вас есть готовый скрипт для OverpassTurbo, результаты работы которого вы хотите видеть в своем смартфоне. И притом не в браузере, а именно в навигаторе. Для этого приведите свой скрипт к следующему формату.


[bbox:{{bbox}}];

(
  // Поместите сюда ваш запрос
  node[amenity=waste_basket];
);

out;>;out skel qt;

В особенности, нас интересует первая строчка: наше приложение будет ее заменять.


После этого нажмите на кнопку Поделиться. Обязательно снимите галочку Включить состояние отображаемой карты.



После этого скопируйте ссылку. Для примера, будем считать, что ваша скопированная ссылка выглядит так:


http://overpass-turbo.eu/s/KEy


Теперь посмотрим на наше API


https://anygis.herokuapp.com/mapshoter/overpass/{x}/{y}/{z}/{crossZoom}?script={script}


С {x} ,{y} и {z} все, вроде бы, понятно: это координаты искомого тайла.


На место {script} нужно подставлять ID вашего скрипта. В нашем примере — s/Key.


Но что такое {crossZoom}? Допустим у вас это 15. Тогда если вы будете запросите тайл для зума меньше 15, то сервер не будет делать медленный запрос к OverpassTurbo, а просто перенаправит вас на карту OpenStreetMaps (которая загрузится практически моментально). Такой подход нужен для того, чтобы в случае необходимости можно было отдалить карту, быстро проскролить ее до интересующего места, приблизить и ждать. Ждать пока OverpassTurbo сгенерирует карту с результатами выдачи.


Надеюсь, основной принцип понятен. А теперь посмотрите на заполненный URL для нашего запроса. Думаю теперь пользоваться нашим API для вас не составит труда: просто заменяйте s/KEy на ID вашего скрипта.


https://anygis.herokuapp.com/mapshoter/overpass/{x}/{y}/{z}/15?script=s/KEy


А мы, же тем временем, посмотрим, как можно реализовать такое приложение.


Сценарий 3 – Поиск с помощью URL и кэша браузера


Итак. Начнем с файла router.js. Сделаем, чтобы наш метод принимал параметры crossZoom и script. А затем передадим их воркеру. Так же добавим опцию, которая будет прерывать скрипт и перенаправлять пользователя на другой сайт, если запрашиваемый зум слишком низкий.


const express = require( 'express' )

const PORT = process.env.PORT || 5000
const app = express()
app.listen( PORT, () => {
    console.log( 'Сервер создан на порту ', PORT )
})

const { StaticPool } = require( 'node-worker-threads-pool' )
const worker = "./worker.js"
const workersPool = new StaticPool({
  size: 3,
  task: worker,
  workerData: "no"
})

// Добавляем новые входные параметры
app.get( '/:x/:y/:z/:crossZoom', async ( req, res, next ) => {

    const x = req.params.x
    const y = req.params.y
    const z = req.params.z
    const crossZoom = req.params.crossZoom
     const scriptName = req.query.script

    // Досрочный выход из скрипта
    if ( Number( z ) < Number( crossZoom ) ) {
        res.redirect( `http://tile.openstreetmap.org/${z}/${x}/${y}.png` )
    }

    // Запускаем задачу с новым параметром
    const screenshot = await workersPool.exec( { x, y, z, scriptName } )

    const imageBuffer = Buffer.from( screenshot, 'base64' )

    res.writeHead( 200, {
        'Content-Type': 'image/png',
        'Content-Length': imageBuffer.length
    })

    res.end( imageBuffer )
})

Файл worker.js практически не изменился. Просто пробрасываем новые переменные дальше.


const { parentPort, workerData } = require( 'worker_threads' );
const puppeteer = require( 'puppeteer' )
const mapshoter = require( './mapshoter' )

var browser = "empty"

parentPort.on( "message", ( params ) => {
    doMyAsyncCode( params )
    .then( ( result ) => { parentPort.postMessage( result ) })
})

async function doMyAsyncCode( params ) {
    await prepareEnviroment()

    // Добавляем параметр
    const screenshot = await mapshoter.makeTile( params.x, params.y, params.z, params.scriptName, browser )
    return screenshot
}

async function prepareEnviroment( ) {
    if ( browser === "empty" ) {
        const herokuDeploymentParams = {'args' : ['--no-sandbox', '--disable-setuid-sandbox']}
        browser = await puppeteer.launch( herokuDeploymentParams )
    }
}

Теперь займемся mapshoter.js. Для начала посмотрим на код:


const puppeteer = require( 'puppeteer' )
const geoTools = require( './geoTools' )

async function makeTile( x, y, z, scriptName, browserLink ) {

    // Селекторы для выбора элементов интерфейса
    const runButtonSelector = '#navs > div > div.buttons > div:nth-child(1) > a:nth-child(1)'
    const codeEditorSelector = '#editor > div.CodeMirror.CodeMirror-wrap > div:nth-child(1) > textarea'

    // Рассчитать координаты краев и центра области для загрузки тайла
    const coordinates = geoTools.getAllCoordinates( x, y, z )
    const bBox = `[bbox:${coordinates.bBox.latMin}, ${coordinates.bBox.lonMin}, ${coordinates.bBox.latMax}, ${coordinates.bBox.lonMax}];`
    const centerCoordinates = `${coordinates.center.lat};${coordinates.center.lon};${z}`

    // Запустить и настроить страницу браузера
    const browser = await browserLink
    const page = await browser.newPage()
    await page.setViewport( { width: 850, height: 450 } )

    // Подождем немного, чтобы не забанили:
    // запросы должны приходить немного вразнобой
    await page.waitFor( randomInt( 0, 500 ) )

    // Призумить к нужному месту с помощью параметров URL
    var pageUrl = `http://overpass-turbo.eu/?C=${centerCoordinates}`
    await page.goto( pageUrl, { waitUntil: 'networkidle2', timeout: 10000 } )

    // Загрузить текст скрипта с помощью параметров URL
    pageUrl = 'http://overpass-turbo.eu/' + scriptName
    await page.goto( pageUrl, { waitUntil: 'networkidle0', timeout: 20000 } )

    // Кликнуть на окно редактора кода
    await page.focus( codeEditorSelector )

    // Вписать вместо первой строчки новую область для поиска,
    // совпадающую с границами тайла 
    await page.keyboard.type( bBox + ' //' )

    // Дождаться, когда онлайн-IDE распознает синтаксис
    await page.waitFor( 100 )

    // Нажать на кнопку загрузки гео-данных
    await page.click( runButtonSelector )

    // Дождаться, когда скроется окно с индикатором загрузки. 
    // И еще немного для надежности.
    await page.waitForFunction(() => !document.querySelector('body > div.modal > div > ul > li:nth-child(1)'), {polling: 'mutation'});
    await page.waitFor( 1000 )

    // Сделать кадрированный скриншот
    const cropOptions = {
      fullPage: false,
      clip: { x: 489, y: 123, width: 256, height: 256 }
    }
    const screenshot = await page.screenshot( cropOptions )

    // Завершение работы
    await page.close()
    return screenshot
}

// Вспомогательная функция для поиска рандомного числа
function randomInt( low, high ) {
  return Math.floor( Math.random() * ( high - low ) + low )
}

module.exports.makeTile = makeTile

Начнем с того, что в данном скрипте мы ради разнообразия будем работать с обычными селекторами элементов (которые не XPath). Как их найти было описано в предыдущей статье.


Далее мы получаем координаты. Только на этот раз помимо координат центра нужны еще и координаты границ тайла (bBox).


Далее запускаем браузер. Тут все типично. Но прежде чем перейти к загрузке страницы заставим скрипт подождать рандомный промежуток времени от 0 до 500 мс. Чтобы на сайт от нас не пришло одновременно слишком много одинаковых запросов и нас не забанили.


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


После этого переходим по еще одному URL. На этот раз с ID нашего скрипта. В результате в тексте редактора кода появится наш скрипт.


(Обратите внимание, что если бы в меню Поделиться при копировании URL для нашего скрипта мы бы не сняли галочку Сохранять состояние карты, то карта бы сместилась. А нам этого совсем не нужно)


А теперь отвечу резонный на вопрос: зачем мы целых два раза переходим по URL, то есть дважды тратим время на загрузку этого сайта? Отвечаю. Потому, что, во первых, мне не удалось найти, как совместить в одном URL запросе и загрузку скрипта и переход к указанным координатам. Во вторых, потому, что по каким-то причинам Puppeteer крайне медленно печатает текст и работает с элементами интерфейса на этом сайте. Полторы минуты может печатать! Так что от идеи вставить координаты в поле поиска, а потом покликать по кнопкам зума, как мы делали в прошлой статье, было решено отказаться. В итоге, дважды перейти по ссылке получилось быстрее, чем проделывать все это. Возможно, это баг и его рано или поздно исправят, но пока работаем с тем, что есть.


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


[bbox:{{bbox}}];

Мы же заменим ее на координаты границ тайла. Это чтобы не тратить время на загрузку из базы лишнего. Так что скрипт впечатает в первую строчку примерно такой текст:


[bbox:55.6279, 37.5622, 55.6341, 37.5732]; //

А чтобы не пришлось стирать изначальную строчку (много раз нажимая для этого Delete) мы просто ее закомментируем. Таким образом мы максимально сократим и время, затрачиваемое на ввод текста, и время загрузки из базы данных. В результате, первая строчка будет выглядеть следующим образом:


[bbox:55.6279, 37.5622, 55.6341, 37.5732]; //[bbox:{{bbox}}];

После этого нашему скрипту остается кликнуть на кнопку Старт, немного подождать, сделать скриншот карты и отправить его пользователю. И все: задача выполнена!


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


Заключение


Чтож, как не трудно предположить, эта версия скрипта будет работать еще медленней предыдущих. Ведь теперь сайт тратит время на запрос из сторонней базы данных. Да и сам по себе он работает не слишком быстро. Однако этот метод позволяет крайне легко (пусть и медленно) получить уникальную, настроенную под себя карту. И, притом, на основе самых свежих данных. А это, порой, может оказаться весьма полезно. Так что стоит иметь такой способ в виду.


А на этом все. На всякий случай напоминаю, что на моем сайте AnyGIS собран архив уже готовых пресетов для навигаторов Locus, OsmAnd и GuruMaps. Там есть как растровые карты, так и векторные карты, для просмотра которых используется описанное в этих статьях приложение. Заходите и пользуйтесь.

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


  1. sshikov
    23.07.2019 19:57

    Это отличный инструмент, давно собирался о нем написать, но в итоге вы молодец, а я лентяй :)

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

    Кстати, вдруг кто-то прокомментирует — есть ли опыт установки overpass-turbo локально, возможно ли это вообще?


    1. Zverik
      25.07.2019 18:56
      +1

      Overpass Turbo — это всего лишь JS-фронтенд, у него нет никаких ограничений. А сервер Overpass API, конечно, можно установить локально. Например, докером: github.com/mediasuitenz/docker-overpass-api


      1. sshikov
        25.07.2019 20:10

        Ага, спасибо. Ну т.е. если докер уже есть — то одним пинком, фактически?