我们正在编写一个服务器应用程序,它将基于在线矢量地图生成PNG栅格图块。 与Puppeteer一起使用网络抓取功能来获取地图数据。
内容:
1- 简介。 标准栅格图
2-继续。 为矢量地图编写一个简单的光栅化器
3- 特例。 我们连接了OverpassTurbo卡
延续性
因此,我们来到了最有趣的话题。 想象一下,我们发现了一个带有地图的站点,我们确实要将其添加到导航器中。 我们按照上一部分的说明进行所有操作。 我们打开了对该网站内容的查看,没有图片! 绝对是 好吧,几个图标就可以了。 和其他一些带有坐标列表的文本文件。
恭喜,我们找到了矢量图。 粗略地说,它是由浏览器实时呈现的。 因此,她根本不需要任何准备好的瓷砖。 一方面,到目前为止,没有太多的矢量地图。 但是这项技术非常有前途,随着时间的流逝,它们的数量可能会增加很多倍。 好吧,我们知道了。 但是,我们现在该怎么办?
首先,您可以尝试下载非常老版本的浏览器。 一种不支持渲染地图所需的功能。 可能会向您显示该网站的其他版本。 带有栅格图。 好吧,您已经知道该怎么做。
但是,如果这种方法不起作用,但是您仍然真的想获得此卡,而且不是在智能手机的浏览器中(即在您的导航器中),那么有一种方法。
主要思想
我们将从想要获得可以在任何导航器中打开的地图的事实出发。 然后,我们需要一个适配器-一种中介,它将为我们生成PNG格式的图块。
原来你需要 发明自行车 开发另一个用于可视化矢量数据的引擎。 好吧,或者您可以编写一个脚本,该脚本将转到该站点,从而允许他自己绘制自己的矢量地图。 然后他将等待下载,截屏,裁剪并返回给用户。 也许我会选择第二个选项。
要获取屏幕截图,我将使用“遥控浏览器”-无头Chrome。 您可以使用node js库Puppeteer对其进行控制。 您可以从本文了解使用此库的基础知识。
世界您好! 或创建和自定义项目
如果尚未安装Node.js,请转至此页面或此页面,选择操作系统,然后根据说明执行安装。
为项目创建一个新文件夹,然后在终端中打开它。
$ cd /Mapshoter_habr
我们开始创建新项目的经理
$ npm init
在这里,您可以指定项目的名称( 包名称 ),用于输入应用程序的文件的名称( 入口点 )和作者的名称( author )。 对于其他所有请求,我们都同意默认参数:不输入任何内容,只需按Enter即可 。 最后,按y ,然后按Enter 。
接下来,安装必要的工作框架。 Express用于创建服务器,Puppeteer用于使用浏览器。
$ npm install express $ npm i puppeteer
结果,项目配置文件package.json出现在项目文件夹中。 就我而言:
{ "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" } }
我将开始行添加到脚本部分,以更方便地启动我们的应用程序。
"scripts": { "start": "node router.js", "test": "echo \"Error: no test specified\" && exit 1" },
现在创建具有基本功能实现的两个文件。 第一个文件是应用程序的入口点。 就我而言,是router.js 。 他将创建服务器并进行路由。
现在创建第二个文件。 他将控制浏览器并拍摄屏幕截图。 我把它叫做mapshoter.js 。
const puppeteer = require( 'puppeteer' ) async function makeTile( x, y, z ) {
运行我们的脚本并检查其性能。 为此,请在控制台中输入:
$ npm start
出现一条消息,说“服务器在端口5000上创建”。 现在,在您的计算机上打开浏览器,然后转到我们服务器的本地地址。 您可以输入任何数字来代替x,y,z坐标。 我输入了1、2、3。
http://localhost:5000/1/2/3
如果一切操作正确,将显示Google网站的屏幕截图。

在控制台中按Ctrl + C停止脚本。
恭喜,我们的申请基础已经准备就绪! 我们创建了一个服务器,该服务器接受html请求,截屏并向我们返回图像。 现在是时候进行细节的实现了。
计算坐标
这个想法是浏览器将打开一个带有地图的站点,然后在搜索栏中输入我们需要的地点的坐标。 单击“查找”按钮后,该位置将恰好在屏幕中央。 因此,很容易切出我们需要的区域。
但首先,您需要根据其序列号计算图块中心的坐标。 我将根据查找左上角的公式进行此操作。 我把它放在getCoordinates()函数中。
由于对于某些站点,除了图块的中心之外,您还需要指定其边界,因此我也会寻找它们的边界。 好吧,让我们以geoTools.js的名称为这些计算创建一个单独的模块。 这是他的代码:
现在,我们准备开始实现用于浏览器的脚本。 让我们看一下如何做到这一点的几种方案。
方案1-API搜索
让我们从最简单的情况开始,您只需在地图页面的URL中输入坐标即可。 例如,像这样:
https://nakarte.me/#m=5/50.28144/89.30666&l=O/Wp
让我们看一下脚本。 只需替换,删除mapshoter.js文件的全部内容,然后粘贴下面的代码即可。
在此版本中,启动浏览器时,我们指定了其他参数,使它可以启动并在Linux服务器(例如Heroku)上运行。 同样,现在我们将减小窗口的大小,以使屏幕上尽可能少的地图图块适合您。 因此,我们提高了页面加载速度。
接下来,我们计算所需图块中心的坐标。 我们将它们粘贴到URL中并单击。 该图块恰好出现在屏幕中央。 切一块256x256像素。 这将是我们需要的瓷砖。 它仅保留将其返回给用户。
在继续进行代码之前,我注意到为清楚起见,已从脚本中删除了所有错误处理。
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
现在运行我们的脚本,并查看此部分的地图。
http://localhost:5000/24/10/5
如果一切都正确完成,则服务器应返回此类磁贴:

为确保裁剪时不会混淆任何东西,请将我们的图块与OpenStreetMaps.org中的图块进行比较

方案2-使用网站界面进行搜索
但是,并非总是可以通过浏览器行来控制卡。 好吧,在这种情况下,我们的脚本将表现得像一个真实的用户。 他将在搜索框中打印坐标,然后单击“搜索”按钮。 之后,他将删除找到的点的标记,该标记通常出现在屏幕中央。 然后,他将单击按钮来增大或减小比例,直到达到所需的比例为止。 然后将截取屏幕截图并将其返回给用户。
我注意到通常在搜索后会设置相同的比例。 例如15号。 在我们的示例中,这并不总是发生。 因此,我们将从页面上html元素的参数中识别缩放级别。
同样在此示例中,我们将使用XPath选择器查找接口元素。 但是您如何识别它们?
为此,请在浏览器中打开所需的页面,然后打开开发人员工具栏(对于Chrome浏览器,请单击Ctll + Alt + I )。 按下按钮选择项目。 我们单击您感兴趣的元素(我单击了搜索字段)。

项目列表滚动到您单击的项目,并以蓝色突出显示。 单击名称左侧的三个点的按钮。
从弹出菜单中,选择“复制”。 接下来,如果您需要常规选择器,请点击复制选择器 。 但是对于同一示例,我们将使用“ 复制XPath”项。

现在,用此代码替换mapshoter.js文件的内容。 在其中,我已经收集了所有必要接口元素的选择器。
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
运行我们的脚本并点击链接。 如果一切都正确完成,那么脚本将返回给我们类似此图块的信息。
http://localhost:5000/1237/640/11

最佳化
原则上,上述两种方法足以连接到具有矢量图的许多站点。 但是,如果您突然需要访问某些新地图,则只需稍微修改mapshoter.js文件中的脚本即可。 也就是说,此方法使添加新卡非常容易。 这是因为它的优势。
但是也有缺点。 最主要的是工作速度。 只是比较一下。 平均而言,下载一个常规的光栅图块大约需要0.5秒。 目前从我们的脚本接收一个图块大约需要8秒钟。
但这还不是全部! 我们使用单线程节点js,我们的长请求最终将阻塞主线程,从外部看,它看起来像是常规同步队列。 而且,当我们尝试下载整个屏幕的地图时(例如,在该地图上放置了24个图块),即存在遇到问题的风险。
还有一件事。 一些导航器超时:它们将在30秒后停止加载。 这意味着在当前实施中,只有3-4个图块将有时间加载。 好吧,让我们看看我们能做些什么。
可能最明显的方法是简单地增加运行脚本的服务器数量。 例如,如果我们有10台服务器,那么他们将有时间在30秒内处理整个屏幕的图块。 (如果您不想支付很多钱,可以通过在Heroku上注册多个免费帐户来获得)
其次,仍然可以使用worker_threads模块在节点js上实现多线程。 根据我的观察,在具有免费Heroku帐户的单核处理器的服务器上,我设法启动了三个线程。 三个流,每个流中都有一个单独的浏览器,它们可以同时工作而不会互相阻塞。 公平地说,我注意到由于处理器负载的增加,一个图块的下载速度甚至略有提高。 但是,如果您尝试下载整个屏幕的地图,则30秒后,一半以上的地图将有时间加载。 超过12个磁贴。 已经更好了。
第三。 在脚本的当前实现中,对于每个请求,我们都花时间下载Chrome浏览器,然后完成它。 现在,我们将预先创建一个浏览器,并将在mapshoter.js中转移到它的链接。 结果,对于第一个请求,速度将不会改变。 但是对于所有后续的下载,一个图块的下载速度降低到4秒。 30秒后,整个地图都有时间加载-所有24个图块都放置在我的屏幕上。
好吧,如果您实现了所有这些功能,那么脚本将变得非常可行。 因此,让我们开始吧。 对于使用多线程的更简单的工作,我将使用node-worker-threads-pool模块-一种worker_threads的包装器。 让我们安装它。
$ npm install node-worker-threads-pool --save
更正router.js文件。 向其添加线程池的创建。 线程将是3件。 他们的代码将在worker.js文件中描述,我们将在以后进行介绍。 同时,直接删除屏幕截图模块的启动。 相反,我们将向线程池添加一个新任务。 当任何线程释放时,他们将开始处理它。
const express = require( 'express' ) const PORT = process.env.PORT || 5000 const app = express() app.listen( PORT, () => { console.log( ' ', PORT ) })
现在看一下worker.js文件。 每当新任务到达时,就会启动parentPort.on()方法。 不幸的是,它无法处理异步/等待功能。 因此,我们将以doMyAsyncCode()方法的形式使用适配器功能。
以一种方便易读的格式将工作人员的逻辑放入其中。 也就是说,启动浏览器(如果尚未运行)并激活用于截屏的方法。 在启动时,我们将传递一个指向正在运行的浏览器的链接到此方法中。
const { parentPort, workerData } = require( 'worker_threads' ); const puppeteer = require( 'puppeteer' ) const mapshoter = require( './mapshoter' )
为了清楚起见,让我们返回mapshoter.js的第一个版本。 它不会有太大变化。 现在,在输入参数中,它将接受到浏览器的链接,并且在脚本结束时,它不会关闭浏览器,而只是关闭创建的选项卡。
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z, browserLink ) {
原则上,仅此而已。 现在,您可以使用任何方便的方式将结果上传到服务器。 例如,通过docker。 如果要查看完成的结果,可以单击此链接 。 您还可以在我的GitHub上找到完整的项目代码。
结论
现在让我们评估结果。 一方面,尽管已完成所有技巧,但下载速度仍然非常低。 而且,由于制动,这种卡根本不容易滚动。
另一方面,此脚本仍然可以处理以前无法连接到智能手机上的导航器的卡片。 这种解决方案不可能用作获取制图数据的主要方法。 但是这里是另外一个,如果有必要的话,有可能在其中帮助下打开一些奇特的卡片-这是完全可能的。
另外,此脚本的优点包括易于使用的事实。 这很容易写。 而且,最重要的是,可以非常轻松地重做连接任何其他在线卡的操作。
好吧, 在下一篇文章中,我将讨论这一点。 我将脚本转换为一种与OverpassTurbo交互式地图配合使用的API。