- PVSM.RU - https://www.pvsm.ru -

Подключаем онлайн-карты к навигатору на смартфоне. Часть 3 — OverpassTurbo

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

Содержание:

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

Что такое OverpassTurbo?

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

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

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

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

https://overpass-turbo.eu/s/z95 [5]

Подключаем онлайн-карты к навигатору на смартфоне. Часть 3 — OverpassTurbo - 1

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

http://overpass-turbo.eu/s/KXU [6]

Подключаем онлайн-карты к навигатору на смартфоне. Часть 3 — OverpassTurbo - 2

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

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

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

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

[bbox:{{bbox}}];

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

out;>;out skel qt;

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

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

Подключаем онлайн-карты к навигатору на смартфоне. Часть 3 — OverpassTurbo - 3

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

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). Как их найти было описано в предыдущей статье [2].

Далее мы получаем координаты. Только на этот раз помимо координат центра нужны еще и координаты границ тайла (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}}];

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

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

Заключение

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

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

Автор: nnngrach

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/node-js/324856

Ссылки в тексте:

[1] Вступление. Стандартные растровые карты: https://habr.com/ru/post/461031/

[2] Продолжение. Пишем простой растеризатор для векторных карт: https://habr.com/ru/post/461053/

[3] OverpassTurbo.eu: https://overpass-turbo.eu/

[4] OpenSteerMap.org: https://www.openstreetmap.org

[5] https://overpass-turbo.eu/s/z95: https://overpass-turbo.eu/s/z95

[6] http://overpass-turbo.eu/s/KXU: http://overpass-turbo.eu/s/KXU

[7] здесь: https://wiki.openstreetmap.org/wiki/RU:Overpass_API/Language_Guide

[8] этой ссылке: https://nakarte.me/#m=16/55.62400/37.57031&l=-cseyJuYW1lIjoiTWFwc2hvdGVyIEdyYXZlbFJ1blRyYWlscyIsInVybCI6Imh0dHBzOi8vYW55Z2lzLmhlcm9rdWFwcC5jb20vbWFwc2hvdGVyL292ZXJwYXNzL3t4fS97eX0ve3p9LzE0P3NjcmlwdD1zL0tUcCIsInRtcyI6ZmFsc2UsInNjYWxlRGVwZW5kZW50IjpmYWxzZSwibWF4Wm9vbSI6MTgsImlzT3ZlcmxheSI6ZmFsc2UsImlzVG9wIjp0cnVlfQ==

[9] AnyGIS: https://nnngrach.github.io/AnyGIS_maps/Web/Html/DownloadPage_ru

[10] Источник: https://habr.com/ru/post/461073/?utm_campaign=461073&utm_source=habrahabr&utm_medium=rss