Nous écrivons une application serveur qui générera des tuiles raster PNG basées sur des cartes vectorielles en ligne. Utilisez le grattage Web avec Puppeteer pour obtenir des données cartographiques.
Contenu:
1 - Introduction. Cartes raster standard
2 - Suite. Écrire un rasterizer simple pour les cartes vectorielles
3 - Un cas particulier. Nous connectons la carte OverpassTurbo
Continuation
Et nous sommes donc arrivés au sujet le plus intéressant. Imaginez que nous avons trouvé un site avec une carte que nous voulons vraiment ajouter à notre navigateur. Nous faisons tout conformément aux instructions de la partie précédente . Nous ouvrons la visualisation du contenu du site, et il n'y a pas de photos! Absolument. Eh bien, quelques icônes et c'est tout. Et un autre fichier texte avec une liste de coordonnées.
Félicitations, nous avons trouvé une carte vectorielle. En gros, il est rendu en temps réel par votre navigateur. Elle n'a donc pas du tout besoin de tuiles préparées. D'une part, il n'y a pas tellement de cartes vectorielles jusqu'à présent. Mais cette technologie est très prometteuse et au fil du temps, elle peut devenir plusieurs fois plus. Eh bien, nous l'avons compris. Et pourtant, que faisons-nous maintenant?
Tout d'abord, vous pouvez essayer de télécharger un navigateur d'une très, très ancienne version. Celui qui ne prend pas en charge les fonctions requises pour rendre la carte. Il est possible qu'on vous montre une version différente du site. Avec carte raster. Eh bien, ce que vous devez en faire, vous le savez déjà.
Cependant, si cette astuce n'a pas fonctionné, mais que vous souhaitez toujours obtenir cette carte, et, en outre, pas dans le navigateur du smartphone, à savoir dans votre navigateur, il existe un moyen.
Idée principale
Nous partirons du fait que nous voulons obtenir une carte qui peut être ouverte dans n'importe lequel des navigateurs. Ensuite, nous avons besoin d'un adaptateur - une sorte d'intermédiaire qui va générer pour nous des tuiles au format PNG.
Il s'avère que vous avez besoin inventer un vélo développer un autre moteur de visualisation des données vectorielles. Eh bien, ou vous pouvez écrire un script qui ira sur le site, lui permettant de dessiner sa propre carte vectorielle par lui-même. Et puis il attendra le téléchargement, fera une capture d'écran, recadrera et reviendra à l'utilisateur. Je choisirai peut-être la deuxième option.
Pour prendre des captures d'écran, j'utiliserai un "navigateur de contrôle à distance" - Chrome sans tête. Vous pouvez le contrôler à l'aide de la bibliothèque de nœuds js Puppeteer . Vous pouvez en apprendre davantage sur les bases de l'utilisation de cette bibliothèque à partir de cet article .
Bonjour tout le monde! Ou créez et personnalisez un projet
Si vous n'avez pas encore installé Node.js, rendez-vous sur telle ou telle page, sélectionnez votre système d'exploitation et terminez l'installation selon les instructions.
Créez un nouveau dossier pour le projet et ouvrez-le dans le terminal.
$ cd /Mapshoter_habr
Nous commençons le gestionnaire de création d'un nouveau projet
$ npm init
Ici, vous pouvez spécifier le nom du projet ( nom du package ), le nom du fichier pour entrer dans l'application ( point d'entrée ) et le nom de l'auteur ( auteur ). Pour toutes les autres demandes, nous acceptons les paramètres par défaut: nous n'entrons rien et nous n'appuyons que sur Entrée . À la fin - appuyez sur y et sur Entrée .
Ensuite, installez les cadres nécessaires au travail. Express pour créer un serveur et Puppeteer pour travailler avec un navigateur.
$ npm install express $ npm i puppeteer
Par conséquent, le fichier de configuration du projet package.json apparaît dans le dossier du projet. Dans mon cas, ceci:
{ "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" } }
J'ajouterai la ligne de départ à la section des scripts pour lancer notre application plus facilement.
"scripts": { "start": "node router.js", "test": "echo \"Error: no test specified\" && exit 1" },
Créez maintenant deux fichiers avec l'implémentation de la fonctionnalité de base. Le premier fichier est le point d'entrée de l'application. Dans mon cas, router.js . Il va créer un serveur et faire du routage.
Créez maintenant un deuxième fichier. Il contrôlera le navigateur et prendra des captures d'écran. Je l'ai appelé mapshoter.js .
const puppeteer = require( 'puppeteer' ) async function makeTile( x, y, z ) {
Exécutez notre script et vérifiez ses performances. Pour ce faire, tapez dans la console:
$ npm start
Un message apparaît qui dit "Le serveur est créé sur le port 5000". Ouvrez maintenant un navigateur sur votre ordinateur et accédez à l'adresse locale de notre serveur. Au lieu des coordonnées x, y, z, vous pouvez saisir n'importe quel nombre. J'ai entré 1, 2, 3.
http://localhost:5000/1/2/3
Si tout est fait correctement, une capture d'écran du site Google apparaîtra.

Appuyez sur Ctrl + C dans la console pour arrêter notre script.
Félicitations, la base de notre application est prête! Nous avons créé un serveur qui accepte nos requêtes html, prend une capture d'écran et nous renvoie une image. Il est maintenant temps de passer à la mise en œuvre des détails.
Calculez les coordonnées
L'idée est que le navigateur ouvre un site avec une carte et entre les coordonnées de l'endroit dont nous avons besoin dans la barre de recherche. Après avoir cliqué sur le bouton "Rechercher", cet endroit sera exactement au centre de l'écran. Il sera donc facile de découper la zone dont nous avons besoin.
Mais d'abord, vous devez calculer les coordonnées du centre de la tuile en fonction de son numéro de série. Je vais le faire en fonction de la formule pour trouver le coin supérieur gauche. Je l'ai mis dans la fonction getCoordinates () .
Et puisque pour certains sites, en plus du centre de la tuile, vous devez également spécifier ses bordures, je les rechercherai également. Eh bien, créons un module séparé pour ces calculs sous le nom geoTools.js . Voici son code:
Nous sommes maintenant prêts à commencer à implémenter le script pour travailler avec le navigateur. Examinons quelques scénarios sur la façon dont cela peut être fait.
Scénario 1 - Recherche d'API
Commençons par le cas le plus simple lorsque vous pouvez simplement entrer les coordonnées dans l'URL de la page de la carte. Par exemple, comme ceci:
https://nakarte.me/#m=5/50.28144/89.30666&l=O/Wp
Regardons le script. Remplacez, supprimez tout le contenu du fichier mapshoter.js et collez le code ci-dessous.
Dans cette version, lors du démarrage du navigateur, nous spécifions des paramètres supplémentaires qui lui permettront de démarrer et de fonctionner sur des serveurs Linux, tels que Heroku. De plus, nous allons maintenant réduire la taille de la fenêtre afin que le moins de tuiles de carte possible tiennent sur l'écran. Ainsi, nous augmentons la vitesse de chargement des pages.
Ensuite, nous calculons les coordonnées du centre de la tuile souhaitée. Nous les collons dans l'URL et cliquons dessus. La tuile apparaît exactement au centre de l'écran. Coupez un morceau de 256x256 pixels. Ce sera la tuile dont nous avons besoin. Il ne reste plus qu'à le rendre à l'utilisateur.
Avant de passer au code, je note que pour plus de clarté, toute la gestion des erreurs a été supprimée du script.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
Maintenant, exécutez notre script et consultez la carte de cette section.
http://localhost:5000/24/10/5
Si tout est fait correctement, le serveur doit retourner une telle tuile:

Pour nous assurer de ne rien mélanger lors du recadrage, comparez notre mosaïque avec l'original d'OpenStreetMaps.org

Scénario 2 - Recherche à l'aide de l'interface du site
Cependant, il n'est pas toujours possible de contrôler une carte via une ligne de navigateur. Eh bien, dans de tels cas, notre script se comportera comme un véritable utilisateur vivant. Il imprimera les coordonnées dans la zone de recherche et cliquera sur le bouton Rechercher. Après cela, il supprimera le marqueur du point trouvé, qui apparaît généralement au centre de l'écran. Et puis il cliquera sur les boutons pour augmenter ou diminuer l'échelle jusqu'à ce qu'il atteigne celui souhaité. Ensuite, il prendra une capture d'écran et la renverra à l'utilisateur.
Je note qu'en général, après la recherche, la même échelle est définie. 15e, par exemple. Dans notre exemple, cela ne se produit pas toujours. Par conséquent, nous reconnaîtrons le niveau de zoom à partir des paramètres des éléments html sur la page.
Dans cet exemple également, nous rechercherons des éléments d'interface à l'aide de sélecteurs XPath. Mais comment les reconnaissez-vous?
Pour ce faire, ouvrez la page requise dans le navigateur et ouvrez la barre d'outils développeur ( Ctll + Alt + I pour Google Chrome). Appuyez sur le bouton pour sélectionner les éléments. Nous cliquons sur l'élément qui vous intéresse (j'ai cliqué sur le champ de recherche).

La liste des éléments défile jusqu'à celui sur lequel vous avez cliqué et elle est surlignée en bleu. Cliquez sur le bouton avec trois points à gauche du nom.
Dans le menu contextuel, sélectionnez Copier. Ensuite, si vous avez besoin d'un sélecteur standard, cliquez sur Copier le sélecteur . Mais pour le même exemple, nous utiliserons l'élément Copy XPath .

Remplacez maintenant le contenu du fichier mapshoter.js par ce code. Dans ce document, j'ai déjà collecté des sélecteurs pour tous les éléments d'interface nécessaires.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
Exécutez notre script et suivez le lien. Si tout est fait correctement, le script nous renverra quelque chose comme cette tuile.
http://localhost:5000/1237/640/11

Optimisation
En principe, les deux méthodes décrites ci-dessus suffisent pour se connecter à de nombreux sites avec des cartes vectorielles. Mais si vous avez soudainement besoin d'accéder à une nouvelle carte, vous n'aurez qu'à modifier légèrement le script dans le fichier mapshoter.js. Autrement dit, cette méthode facilite l'ajout de nouvelles cartes. Cela vient de ses avantages.
Mais il y a aussi des inconvénients. Et le principal est la vitesse de travail. Il suffit de comparer. En moyenne, il faut environ 0,5 seconde pour télécharger une tuile raster ordinaire. La réception d'une tuile de notre script prend actuellement environ 8 secondes.
Mais ce n’est pas tout! Nous utilisons le nœud js à un seul thread et nos longues requêtes finiront par bloquer le thread principal, qui de l'extérieur ressemblera à une file d'attente synchrone régulière. Et lorsque nous essayons de télécharger la carte pour tout l'écran (sur lequel, par exemple, 24 tuiles sont placées), c'est qu'il y a un risque de rencontrer un problème.
Et encore une chose. Certains navigateurs ont un timeout: ils arrêteront le chargement après 30 secondes. Et cela signifie qu'avec l'implémentation actuelle, seulement 3-4 tuiles auront le temps de se charger. Eh bien, voyons ce que nous pouvons y faire.
Le moyen le plus évident est probablement d'augmenter simplement le nombre de serveurs sur lesquels notre script s'exécutera. Par exemple, si nous avons 10 serveurs, ils auront le temps de traiter les tuiles pour l'ensemble de l'écran en 30 secondes. (Si vous ne voulez pas payer beaucoup d'argent, vous pouvez l'obtenir en enregistrant plusieurs comptes gratuits sur Heroku)
Deuxièmement, il est toujours possible d'implémenter le multithreading sur le nœud js en utilisant le module worker_threads . Selon mes observations, sur un serveur avec un processeur monocœur sur un compte Heroku gratuit, j'arrive à démarrer trois threads. Trois flux avec un navigateur séparé dans chacun, qui peuvent fonctionner simultanément sans se bloquer. En toute honnêteté, je note qu'en raison de la charge accrue sur le processeur, la vitesse de téléchargement d'une tuile a même légèrement augmenté. Cependant, si vous essayez de télécharger une carte pour tout l'écran, au bout de 30 secondes, plus de la moitié de la carte aura le temps de se charger. Plus de 12 tuiles. Déjà mieux.
Troisièmement. Dans l'implémentation actuelle du script, à chaque demande, nous passons du temps à télécharger le navigateur Chrome, puis à le terminer. Nous allons maintenant créer un navigateur à l'avance et transférer un lien vers celui-ci dans mapshoter.js. Par conséquent, la vitesse ne changera pas pour la première demande. Mais pour toute vitesse de téléchargement ultérieure d'une tuile est réduite à 4 secondes. Et après 30 secondes, toute la carte a le temps de se charger - les 24 tuiles qui sont placées sur mon écran.
Eh bien, si vous implémentez tout cela, le script peut devenir tout à fait viable. Commençons donc. Pour un travail plus simple avec le multithreading, j'utiliserai le module node-worker-threads-pool - une sorte de wrapper sur worker_threads. Installons-le.
$ npm install node-worker-threads-pool --save
Corrigez le fichier router.js. Ajoutez-y la création d'un pool de threads. Les fils seront de 3 pièces. Leur code sera décrit dans le fichier worker.js , nous le verrons plus tard. En attendant, supprimez directement le lancement du module de capture d'écran. Au lieu de cela, nous ajouterons une nouvelle tâche au pool de threads. Ils commenceront à le traiter lorsque l'un des threads sera libéré.
const express = require( 'express' ) const PORT = process.env.PORT || 5000 const app = express() app.listen( PORT, () => { console.log( ' ', PORT ) })
Jetez maintenant un œil au fichier worker.js . Chaque fois qu'une nouvelle tâche arrive, la méthode parentPort.on () sera lancée. Malheureusement, il ne peut pas gérer les fonctions asynchrones / attendent. Nous allons donc utiliser la fonction adaptateur sous la forme de la méthode doMyAsyncCode () .
Dans celui-ci dans un format lisible pratique, nous mettrons la logique du travailleur. Autrement dit, lancez le navigateur (s'il n'est pas déjà en cours d'exécution) et activez la méthode pour prendre une capture d'écran. Au démarrage, nous transmettrons à cette méthode un lien vers le navigateur en cours d'exécution.
const { parentPort, workerData } = require( 'worker_threads' ); const puppeteer = require( 'puppeteer' ) const mapshoter = require( './mapshoter' )
Pour plus de clarté, revenons à la première version de mapshoter.js . Cela ne changera pas grand-chose. Maintenant, dans les paramètres d'entrée, il acceptera un lien vers le navigateur, et lorsque le script se terminera, il ne désactivera pas le navigateur, mais fermera simplement l'onglet créé.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z, browserLink ) {
En principe, c'est tout. Vous pouvez maintenant télécharger le résultat sur le serveur de la manière qui vous convient. Par exemple, via docker. Si vous souhaitez consulter le résultat final, vous pouvez cliquer sur ce lien . Vous pouvez également trouver le code de projet complet sur mon GitHub .
Conclusion
Maintenant, évaluons le résultat. D'une part, même malgré toutes les astuces effectuées, la vitesse de téléchargement est toujours très faible. De plus, à cause des freins, une telle carte est tout simplement désagréable à faire défiler.
D'autre part, ce script fait néanmoins face à des cartes qui auparavant, il était généralement impossible de se connecter au navigateur sur le smartphone. Il est peu probable que cette solution soit appliquée comme méthode principale d'obtention des données cartographiques. Mais ici comme une supplémentaire, à l'aide de laquelle, si nécessaire, il sera possible d'ouvrir une carte exotique - c'est tout à fait possible.
De plus, les avantages de ce script incluent le fait qu'il est facile de travailler avec lui. C'est facile à écrire. Et, plus important encore, il peut être extrêmement facilement refait pour connecter n'importe quelle autre carte en ligne.
Eh bien, dans le prochain article, je traiterai juste de cela. Je vais transformer le script en une sorte d'API pour travailler avec la carte interactive OverpassTurbo.