Kami menghubungkan peta online ke navigator di smartphone. Bagian 2 - kartu vektor

Kami sedang menulis aplikasi server yang akan menghasilkan ubin raster PNG berdasarkan peta vektor online. Gunakan memo web dengan Dalang untuk mendapatkan data peta.


Konten:


1 - Pendahuluan. Peta raster standar
2 - Lanjutan. Menulis rasterizer sederhana untuk peta vektor
3 - Kasus khusus. Kami menghubungkan kartu OverpassTurbo


Lanjutan


Jadi kita sampai pada topik yang paling menarik. Bayangkan kita menemukan situs dengan peta yang ingin kita tambahkan ke navigator kita. Kami melakukan segalanya sesuai dengan instruksi dari bagian sebelumnya . Kami membuka tampilan konten situs, dan tidak ada gambar! Tentu saja Nah, beberapa ikon dan hanya itu. Dan beberapa file teks lainnya dengan daftar koordinat.


Selamat, kami menemukan peta vektor. Secara kasar, ini diterjemahkan secara real time oleh browser Anda. Jadi dia tidak membutuhkan ubin yang disiapkan sama sekali. Di satu sisi, sejauh ini tidak banyak peta vektor. Tetapi teknologi ini sangat menjanjikan dan seiring waktu mereka dapat menjadi berkali-kali lipat. Yah, kita sudah menemukannya. Namun, apa yang kita lakukan sekarang?


Pertama, Anda dapat mencoba mengunduh peramban dari versi yang sangat, sangat lama. Satu yang tidak mendukung fungsi yang diperlukan untuk membuat peta. Mungkin saja Anda akan diperlihatkan versi situs yang berbeda. Dengan peta raster. Nah, apa yang perlu Anda lakukan dengannya sudah Anda ketahui.


Namun, jika trik ini tidak berhasil, tetapi Anda masih benar-benar ingin mendapatkan kartu ini, dan, lebih lagi, tidak di browser ponsel cerdas, yaitu di navigator Anda, maka ada caranya.


Ide utama


Kami akan melanjutkan dari kenyataan bahwa kami ingin mendapatkan peta yang dapat dibuka di salah satu navigator. Maka kita membutuhkan adaptor - sejenis perantara yang akan menghasilkan ubin untuk kita dalam format PNG.


Ternyata yang Anda butuhkan menciptakan sepeda kembangkan mesin lain untuk memvisualisasikan data vektor. Nah, atau Anda dapat menulis skrip yang akan pergi ke situs, membiarkannya menggambar peta vektor sendiri. Dan kemudian dia akan menunggu unduhan, mengambil tangkapan layar, memotong dan kembali ke pengguna. Mungkin saya akan memilih opsi kedua.


Untuk mengambil tangkapan layar, saya akan menggunakan "browser kendali jarak jauh" - Chrome Tanpa Kepala. Anda dapat mengontrolnya menggunakan pustaka simpul node js. Anda dapat mempelajari dasar-dasar bekerja dengan perpustakaan ini dari artikel ini .


Halo Dunia! Atau buat dan sesuaikan proyek


Jika Anda belum menginstal Node.js, buka halaman ini atau ini , pilih sistem operasi Anda dan selesaikan instalasi sesuai dengan instruksi.


Buat folder baru untuk proyek dan buka di terminal.


$ cd /Mapshoter_habr 

Kami memulai manajer menciptakan proyek baru


 $ npm init 

Di sini Anda dapat menentukan nama proyek ( nama paket ), nama file untuk memasukkan aplikasi ( titik masuk ) dan nama penulis ( penulis ). Untuk semua permintaan lainnya, kami menyetujui parameter default: kami tidak memasukkan apa pun dan cukup tekan Enter . Di akhir - tekan y dan Enter .


Selanjutnya, instal kerangka kerja yang diperlukan untuk pekerjaan. Ekspres untuk membuat server dan Dalang untuk bekerja dengan browser.


 $ npm install express $ npm i puppeteer 

Akibatnya, file file konfigurasi proyek package.json muncul di folder proyek. Dalam kasus saya, ini:


 { "name": "mapshoter_habr", "version": "1.0.0", "description": "", "main": "router.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "nnngrach", "license": "ISC", "dependencies": { "express": "^4.17.1", "puppeteer": "^1.18.1" } } 

Saya akan menambahkan baris awal ke bagian skrip untuk meluncurkan aplikasi kami dengan lebih nyaman.


 "scripts": { "start": "node router.js", "test": "echo \"Error: no test specified\" && exit 1" }, 

Sekarang buat dua file dengan implementasi fungsi dasar. File pertama adalah titik masuk ke aplikasi. Dalam kasus saya, router.js . Dia akan membuat server dan melakukan routing.


 //        const express = require( 'express' ) const mapshoter = require( './mapshoter' ) //  ,       const PORT = process.env.PORT || 5000 //     const app = express() app.listen( PORT, () => { console.log( '    ', PORT ) }) //       // http://siteName.com/x/y/z app.get( '/:x/:y/:z', async ( req, res, next ) => { //      const x = req.params.x const y = req.params.y const z = req.params.z //      const screenshot = await mapshoter.makeTile( x, y, z ) //        const imageBuffer = Buffer.from( screenshot, 'base64' ) //    res.writeHead( 200, { 'Content-Type': 'image/png', 'Content-Length': imageBuffer.length }) //    res.end( imageBuffer ) }) 

Sekarang buat file kedua. Dia akan mengontrol browser dan mengambil screenshot. Saya menyebutnya mapshoter.js .


 const puppeteer = require( 'puppeteer' ) async function makeTile( x, y, z ) { //   const browser = await puppeteer.launch() //       const page = await browser.newPage() await page.goto( 'https://www.google.ru/' ) //    const screenshot = await page.screenshot() //      await browser.close() return screenshot } module.exports.makeTile = makeTile 

Jalankan skrip kami dan periksa kinerjanya. Untuk melakukan ini, ketik konsol:


$ npm start


Muncul pesan yang mengatakan "Server dibuat pada port 5000". Sekarang buka browser di komputer Anda dan pergi ke alamat lokal server kami. Alih-alih koordinat x, y, z, Anda dapat memasukkan angka apa saja. Saya memasukkan 1, 2, 3.


http://localhost:5000/1/2/3


Jika semuanya dilakukan dengan benar, tangkapan layar situs Google akan muncul.


gambar


Tekan Ctrl + C di konsol untuk menghentikan skrip kami.


Selamat, dasar dari aplikasi kita sudah siap! Kami membuat server yang menerima permintaan html kami, mengambil tangkapan layar, dan mengembalikan gambar kepada kami. Sekarang saatnya beralih ke implementasi detail.


Hitung koordinatnya


Idenya adalah browser akan membuka situs dengan peta dan memasukkan koordinat tempat yang kita butuhkan di bilah pencarian. Setelah mengklik tombol "Temukan", tempat ini akan persis di tengah layar. Jadi akan mudah untuk memotong area yang kita butuhkan.


Tetapi pertama-tama, Anda perlu menghitung koordinat pusat ubin berdasarkan nomor seri. Saya akan melakukan ini berdasarkan rumus untuk menemukan sudut kiri atas. Saya memasukkannya ke dalam fungsi getCoordinates () .


Dan karena untuk beberapa situs, selain pusat ubin, Anda juga perlu menentukan perbatasannya, maka saya akan mencarinya juga. Baiklah, mari kita buat modul terpisah untuk perhitungan ini dengan nama geoTools.js . Ini kodenya:


 //   -   function getCoordinates( x, y, z ) { const n = Math.pow( 2, z ) const lon = x / n * 360.0 - 180.0 const lat = 180.0 * ( Math.atan( Math.sinh( Math.PI * ( 1 - 2 * y / n) ) ) ) / Math.PI return { lat: lat, lon: lon } } //          function getCenter( left, rigth, top, bottom ) { let lat = ( left + rigth ) / 2 let lon = ( top + bottom ) / 2 return { lat: lat, lon: lon } } //        function getAllCoordinates( stringX, stringY, stringZ ) { //      const x = Number( stringX ) const y = Number( stringY ) const z = Number( stringZ ) //     //    -  -  const topLeft = getCoordinates( x, y, z ) const bottomRight = getCoordinates( x+1, y+1, z ) //   const center = getCenter( topLeft.lat, bottomRight.lat, topLeft.lon, bottomRight.lon ) //   const bBox = { latMin: bottomRight.lat, lonMin: topLeft.lon, latMax: topLeft.lat, lonMax: bottomRight.lon } return { bBox: bBox, center: center } } module.exports.getAllCoordinates = getAllCoordinates 

Sekarang kita siap untuk mulai mengimplementasikan skrip untuk bekerja dengan browser. Mari kita lihat beberapa skenario bagaimana ini bisa dilakukan.


Skenario 1 - Pencarian API


Mari kita mulai dengan case paling sederhana, ketika Anda cukup memasukkan koordinat di URL halaman peta. Misalnya, seperti ini:


https://nakarte.me/#m=5/50.28144/89.30666&l=O/Wp


Mari kita lihat skripnya. Cukup ganti, hapus seluruh konten file mapshoter.js dan rekatkan kode di bawah ini.


Dalam versi ini, ketika memulai browser, kami menentukan parameter tambahan yang akan memungkinkannya untuk memulai dan bekerja di server Linux, seperti Heroku. Juga sekarang kita akan mengurangi ukuran jendela sehingga ubin peta sesedikit mungkin pas di layar. Dengan demikian, kami meningkatkan kecepatan pemuatan halaman.


Selanjutnya, kami menghitung koordinat tengah ubin yang diinginkan. Kami menempelkannya ke URL dan mengkliknya. Ubin muncul tepat di tengah layar. Potong sepotong 256x256 piksel. Ini akan menjadi ubin yang kami butuhkan. Tetap hanya mengembalikannya ke pengguna.


Sebelum beralih ke kode, saya perhatikan bahwa untuk kejelasan, semua penanganan kesalahan telah dihapus dari skrip.


 const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) { //    ,    Heroku const herokuDeploymentParams = {'args' : ['--no-sandbox', '--disable-setuid-sandbox']} const browser = await puppeteer.launch( herokuDeploymentParams ) //        //       const page = await browser.newPage() await page.setViewport( { width: 660, height: 400 } ) //         URL const coordinates = geoTools.getAllCoordinates( x, y, z ) const centerCoordinates = `${z}/${coordinates.center.lat}/${coordinates.center.lon}&l=` const pageUrl = 'https://nakarte.me/#m=' + centerCoordinates + "O/Wp" //   URL  ,    await page.goto( pageUrl, { waitUntil: 'networkidle0', timeout: 20000 } ) //    const cropOptions = { fullPage: false, clip: { x: 202, y: 67, width: 256, height: 256 } } const screenshot = await page.screenshot( cropOptions ) //      await browser.close() return screenshot } module.exports.makeTile = makeTile 

Sekarang jalankan skrip kami dan lihat peta untuk bagian ini.


http://localhost:5000/24/10/5


Jika semuanya dilakukan dengan benar, maka server harus mengembalikan ubin seperti itu:



Untuk memastikan kami tidak mencampur apa pun saat memotong, bandingkan ubin kami dengan yang asli dari OpenStreetMaps.org



Skenario 2 - Cari menggunakan antarmuka situs


Namun, tidak selalu memungkinkan untuk mengontrol kartu melalui saluran peramban. Nah, dalam kasus seperti itu, skrip kami akan berperilaku seperti pengguna yang nyata. Dia akan mencetak koordinat di kotak pencarian dan klik tombol Cari. Setelah itu, ia akan menghapus penanda titik yang ditemukan, yang biasanya muncul di tengah layar. Dan kemudian dia akan mengklik tombol untuk menambah atau mengurangi skala sampai dia mencapai yang diinginkan. Kemudian akan mengambil tangkapan layar dan mengembalikannya kepada pengguna.


Saya perhatikan bahwa biasanya setelah pencarian skala yang sama ditetapkan. 15, misalnya. Dalam contoh kita, ini tidak selalu terjadi. Karenanya, kami akan mengenali level zoom dari parameter elemen html pada halaman.


Juga dalam contoh ini, kita akan mencari elemen antarmuka menggunakan pemilih XPath. Tapi bagaimana Anda mengenalinya?


Untuk melakukan ini, buka halaman yang diperlukan di browser dan buka toolbar pengembang ( Ctll + Alt + I untuk Google Chrome). Tekan tombol untuk memilih item. Kami mengklik elemen yang Anda minati (saya mengklik bidang pencarian).



Daftar item menggulir ke item yang Anda klik dan itu disorot dengan warna biru. Klik tombol dengan tiga titik di sebelah kiri nama.


Dari menu pop-up, pilih Salin. Selanjutnya, jika Anda membutuhkan pemilih biasa, lalu klik Salin pemilih . Tetapi untuk contoh yang sama, kami akan menggunakan item Salin XPath .



Sekarang ganti konten file mapshoter.js dengan kode ini. Di dalamnya, saya telah mengumpulkan pemilih untuk semua elemen antarmuka yang diperlukan.


 const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) { //      const searchFieldXPath = '//*[@id="map"]/div[1]/div[1]/div/input' const zoomPlusXPath = '//*[@id="map"]/div[2]/div[2]/div[4]/div[1]/a[1]' const zoomMinusXPath = '//*[@id="map"]/div[2]/div[2]/div[4]/div[1]/a[2]' const directionButonXPath = '//*[@id="gtm-poi-card-get-directions"]' const deletePinButonXPatch = '//*[@id="map"]/div[1]/div/div/div[1]/div[2]/div/div[4]/div/div[4]' //         () const coordinates = geoTools.getAllCoordinates( x, y, z ) const centerCoordinates = `lat=${coordinates.center.lat} lng=${coordinates.center.lon}` //      const herokuDeploymentParams = {'args' : ['--no-sandbox', '--disable-setuid-sandbox']} const browser = await puppeteer.launch( herokuDeploymentParams ) const page = await browser.newPage() await page.setViewport( { width: 1100, height: 450 } ) //         const pageUrl = 'https://www.waze.com/en/livemap?utm_campaign=waze_website' await page.goto( pageUrl, { waitUntil: 'networkidle2', timeout: 10000 } ) //    ,      await click( searchFieldXPath, page ) //        await page.keyboard.type( centerCoordinates ) //  Enter    page.keyboard.press( 'Enter' ); //  500     await page.waitFor( 500 ) //       //       await click( directionButonXPath, page ) await page.waitFor( 100 ) await click( deletePinButonXPatch, page ) await page.waitFor( 100 ) //       //        while( z > await fetchCurrentZoom( page )) { await click( zoomPlusXPath, page ) await page.waitFor( 300 ) } while( z < await fetchCurrentZoom( page )) { await click( zoomMinusXPath, page ) await page.waitFor( 300 ) } //    const cropOptions = { fullPage: false, clip: { x: 422, y: 97, width: 256, height: 256 } } const screenshot = await page.screenshot( cropOptions ) //   await browser.close() return screenshot } //  : //        async function click( xPathSelector, page ) { await page.waitForXPath( xPathSelector ) const foundedElements = await page.$x( xPathSelector ) if ( foundedElements.length > 0 ) { await foundedElements[0].click() } else { throw new Error( "XPath element not found: ", xPathSelector ) } } //         html  async function fetchCurrentZoom( page ) { const xPathSelector = '//*[@id="map"]/div[2]' await page.waitForXPath( xPathSelector ) const elems = await page.$x(xPathSelector) const elementParams = await page.evaluate((...elems) => { return elems.map(e => e.className); }, ...elems); const zoom = elementParams[0].split('--zoom-').pop() return zoom } module.exports.makeTile = makeTile 

Jalankan skrip kami dan ikuti tautannya. Jika semuanya dilakukan dengan benar, maka skrip akan kembali kepada kami seperti ubin ini.


http://localhost:5000/1237/640/11



Optimasi


Pada prinsipnya, dua metode yang dijelaskan di atas cukup untuk terhubung ke banyak situs dengan peta vektor. Tetapi jika Anda tiba-tiba membutuhkan akses ke beberapa peta baru, Anda hanya perlu sedikit memodifikasi skrip dalam file mapshoter.js. Artinya, metode ini membuatnya sangat mudah untuk menambahkan kartu baru. Ini dari kelebihannya.


Namun ada juga kekurangannya. Dan yang utama adalah kecepatan kerja. Bandingkan saja. Rata-rata, dibutuhkan sekitar 0,5 detik untuk mengunduh satu ubin raster biasa. Saat menerima satu ubin dari skrip kami saat ini membutuhkan waktu sekitar 8 detik.


Tapi itu belum semuanya! Kami menggunakan simpul single-threaded js dan permintaan panjang kami pada akhirnya akan memblokir utas utama, yang dari luar akan terlihat seperti antrian sinkron biasa. Dan ketika kami mencoba mengunduh peta untuk seluruh layar (di mana, misalnya, 24 ubin ditempatkan), yaitu, ada risiko menemukan masalah.


Dan satu hal lagi. Beberapa navigator memiliki batas waktu: mereka akan berhenti memuat setelah 30 detik. Dan ini berarti bahwa dengan implementasi saat ini hanya 3-4 ubin akan memiliki waktu untuk memuat. Baiklah, mari kita lihat apa yang bisa kita lakukan.


Mungkin cara yang paling jelas adalah dengan hanya menambah jumlah server tempat skrip kita dijalankan. Misalnya, jika kita memiliki 10 server, maka mereka akan memiliki waktu untuk memproses ubin untuk seluruh layar dalam 30 detik. (Jika Anda tidak ingin membayar banyak uang, Anda bisa mendapatkannya dengan mendaftarkan beberapa akun gratis di Heroku)


Kedua, masih mungkin untuk mengimplementasikan multithreading pada node js menggunakan modul worker_threads . Menurut pengamatan saya, pada server dengan prosesor single-core pada akun Heroku gratis, saya berhasil memulai tiga utas. Tiga aliran dengan browser terpisah di masing-masing, yang dapat bekerja secara bersamaan tanpa saling menghalangi. Dalam keadilan, saya perhatikan bahwa sebagai hasil dari peningkatan beban pada prosesor, kecepatan unduhan satu ubin bahkan sedikit meningkat. Namun, jika Anda mencoba mengunduh peta untuk seluruh layar, maka setelah 30 detik lebih dari setengah peta akan memiliki waktu untuk memuat. Lebih dari 12 ubin. Sudah lebih baik.


Ketiga Dalam implementasi skrip saat ini, dengan setiap permintaan, kami menghabiskan waktu mengunduh browser Chrome, dan kemudian menyelesaikannya. Sekarang kita akan membuat browser terlebih dahulu dan akan mentransfer tautan ke sana di mapshoter.js. Akibatnya, kecepatan tidak akan berubah untuk permintaan pertama. Tetapi untuk semua kecepatan unduhan berikutnya dari satu ubin dikurangi menjadi 4 detik. Dan setelah 30 detik seluruh peta memiliki waktu untuk memuat - semua 24 ubin yang ditempatkan di layar saya.


Nah, jika Anda menerapkan semua ini, maka skrip dapat menjadi sangat layak. Jadi mari kita mulai. Untuk pekerjaan yang lebih sederhana dengan multithreading, saya akan menggunakan modul node-worker-threads-pool - semacam wrapper over worker_threads. Mari kita instal.


$ npm install node-worker-threads-pool --save


Perbaiki file router.js. Tambahkan padanya penciptaan kumpulan utas. Utas akan menjadi 3 buah. Kode mereka akan dijelaskan dalam file worker.js , kita akan melihatnya nanti. Sementara itu, hapus peluncuran modul tangkapan layar secara langsung. Sebagai gantinya, kami akan menambahkan tugas baru ke kumpulan utas. Mereka akan mulai memprosesnya ketika ada utas yang dibebaskan.


 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', async ( req, res, next ) => { const x = req.params.x const y = req.params.y const z = req.params.z //       //       const screenshot = await workersPool.exec( { x, y, z } ) const imageBuffer = Buffer.from( screenshot, 'base64' ) res.writeHead( 200, { 'Content-Type': 'image/png', 'Content-Length': imageBuffer.length }) res.end( imageBuffer ) }) 

Sekarang lihat file pekerja.js . Setiap kali tugas baru tiba, metode parentPort.on () akan diluncurkan. Sayangnya, ia tidak dapat menangani fungsi async / menunggu. Jadi kita akan menggunakan fungsi adaptor dalam bentuk metode doMyAsyncCode () .


Di dalamnya dalam format yang mudah dibaca, kita akan menempatkan logika pekerja. Yaitu, luncurkan browser (jika belum berjalan) dan aktifkan metode untuk mengambil tangkapan layar. Saat memulai, kami akan memberikan tautan ke browser yang berjalan ke metode ini.


 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/aswit //     async function doMyAsyncCode( params ) { //      await prepareEnviroment() //     const screenshot = await mapshoter.makeTile( params.x, params.y, params.z, browser ) return screenshot } //  .     ,    async function prepareEnviroment( ) { if ( browser === "empty" ) { const herokuDeploymentParams = {'args' : ['--no-sandbox', '--disable-setuid-sandbox']} browser = await puppeteer.launch( herokuDeploymentParams ) } } 

Untuk lebih jelasnya, mari kita kembali ke versi pertama mapshoter.js . Itu tidak akan banyak berubah. Sekarang pada parameter input, ia akan menerima tautan ke browser, dan ketika skrip berakhir, itu tidak akan mematikan browser, tetapi cukup tutup tab yang dibuat.


 const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z, browserLink ) { //      const browser = await browserLink //      const page = await browser.newPage() await page.setViewport( { width: 660, height: 400 } ) const coordinates = geoTools.getAllCoordinates( x, y, z ) const centerCoordinates = `${z}/${coordinates.center.lat}/${coordinates.center.lon}&l=` const pageUrl = 'https://nakarte.me/#m=' + centerCoordinates + "O/Wp" await page.goto( pageUrl, { waitUntil: 'networkidle0', timeout: 20000 } ) const cropOptions = { fullPage: false, clip: { x: 202, y: 67, width: 256, height: 256 } } const screenshot = await page.screenshot( cropOptions ) //   .   . await page.close() return screenshot } module.exports.makeTile = makeTile 

Pada prinsipnya, itu saja. Sekarang Anda dapat mengunggah hasilnya ke server dengan cara apa pun yang nyaman bagi Anda. Misalnya melalui buruh pelabuhan. Jika Anda ingin melihat hasil yang sudah selesai, Anda dapat mengklik tautan ini . Anda juga dapat menemukan kode proyek lengkap di GitHub saya.


Kesimpulan


Sekarang mari kita evaluasi hasilnya. Di satu sisi, meskipun semua trik dilakukan, kecepatan unduh masih sangat rendah. Selain itu, karena rem, kartu seperti itu tidak nyaman untuk digulir.


Di sisi lain, skrip ini berupaya dengan kartu yang sebelumnya tidak mungkin terhubung ke navigator di smartphone. Tidak mungkin solusi ini akan diterapkan sebagai metode utama untuk memperoleh data kartografi. Tapi di sini sebagai tambahan, dengan bantuan yang, jika perlu, akan mungkin untuk membuka beberapa kartu eksotis - sangat mungkin.


Juga, keuntungan dari skrip ini termasuk fakta bahwa mudah untuk bekerja dengannya. Mudah untuk menulis. Dan, yang paling penting, dapat dengan mudah dikerjakan ulang untuk menghubungkan kartu online lainnya.


Nah, pada artikel selanjutnya saya akan membahas hal itu saja. Saya akan mengubah skrip menjadi semacam API untuk bekerja dengan peta interaktif OverpassTurbo.

Source: https://habr.com/ru/post/id461053/


All Articles