本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
背景介绍
最近在做某个海港项目数据可视化大屏,需要接入第三方的水域、船舶AIS、航海规划等专业领域数据,在开发过程中发现供应商的提供的专业领域海图服务较为封闭,不能对外提供地图瓦片服务。换言之如果我不使用供应商提供的自研地图引擎,而使用我们团队比较有开发经验的地图引擎(高德JSAPI、arcGIS地图等)是无法直接在底图上叠加他们的海图的,但我们又需要使用到海图作为AIS数据的参照。
与对方技术团队协商无果后我决定自行处理,使用QGIS将海图数据进行二次处理后自己搭建WMTS服务,最终实现了专业领域地图的叠加调用,顺便也学会了如何制作简单的wmts地图,在这里做一个分享。
概念说明
这里有必要解释下WMTS的概念,以及它和WMS的区别。
WMTS 即 Web Map Tile Service(网络地图瓦片服务),它提供了采用预定义图块方法发布数字地图服务的标准化解决方案。WMTS采用缓存技术在服务端将地图预先切割为固定大小的瓦片并存储,客户端根据需要获取相应瓦片进行拼接显示,可提高地图响应能力。WMTS牺牲了定制的灵活性,来换取增强的伸缩性,这些静态数据的范围框和比例尺会被限定在各个图块内。
与 WMTS相比,WMS(Web Map Service,网络地图服务) 返回的是一张完整的图片,WMS 重在灵活性,可提供定制地图服务,但复用率低,在并发增大时服务端性能会下降;WMTS 则更注重可缓存性和性能优化。
解决思路
我原本的计划是通过供应商提供的海图示例直接获取到内部wmts资源地址,然后使用地图下载工具把所需要的区域按照多个缩放级别缓存下载,然后叠加使用,简单粗暴。然而实践后发现行不通,因为供应商的海图因为某些原因做了整体偏移,即它与公共标准的地图是对不齐的,无法直接使用,如下图所示。
这个时候难点就来了,我可以在现有的地图引擎接入海图图层后将其再做偏移对齐到正确位置,前提是地图引擎有提供这方面的配置功能;或者直接处理已经被偏移的瓦片数据,考虑到大部分地图引擎并没有纠偏功能,只好苦心研究第二种解法,花了不少时间研究QGIS功能最后还是找到能够解决的方法,以下为处理思路。
实现步骤
-
使用QGIS中加载网络地图服务,在QGIS的浏览器面板中右键新建连接,并在弹窗中填入wmts资源地址即可,如下图所示。如果有认证参数比如token或者账号密码之类的可以在配置区域填入,连接成功后就算入库成功。
双击刚接入的wmts项,即可作为图层将其添加到当前编辑的场景,以本文为例我们接入一张自定义的海图。
-
截取范围内的wmts数据。我们先新建一个遮罩图层用于表示需要截取的范围,生成XYZ瓦片独立文件(MBTiles)。 眼尖的盆友可能发现了“生成XYZ瓦片(MBTiles)”这一项和“生成XYZ瓦片(目录)”很相似,那么它跟MBTiles有什么区别呢?事实上两者是同样的数据,只是储存形式上有所区别。MBTiles 是一种独立完整的数据库格式,它将瓦片数据以紧凑和高效的方式存储在单个文件中,便于管理、传输和共享;而后者是一种离散的数据储存形式,就是每个瓦片会单独作为图片存储,这也是我们最终输出的文件格式。
-
生产了XYZ瓦片,我们开始对进行二次加工——地理配准,目的是让瓦片图跟卫星影像底图对齐。其基本原理就是找到瓦片上的一些控制点,让这些控制点与底图位置匹配,然后经由一些算法自动调整瓦片图,简要步骤如下。
(1)在QGIS的顶部菜单打开“图层-配准工具”,此时会有个配准工具面板
(2)在配准工具面板左上角导入刚刚的瓦片文件,并在瓦片地图上选择1个比较明显的控制点,然后点选它在底图上的位置,如此重复多次。如果需要更详细的配准步骤可以看这里
(3)点击工具栏中的“变换设置”,目标CRS选择与底图一致,并设置好输出文件路径,勾选“完成后加载到工程”
(4)点击“开始配准”,QGIS就会自动进入配准模式最后生成一个对齐后的图层。
配准图层后最终效果如下,截取的海图区域已经完美匹配到卫星影像底图。
-
将已经对齐的瓦片导出供使用,我们直接点选图层,并双击工具箱中 “栅格工具 - 转为XYZ瓦片(目录)”,范围就选择当前图层,“最小最大缩放级别”决定了瓦片的采样级别,采样范围越大转换时间越久,最终导出的文件也越多。其他配置项也很好理解,或者选默认就行了。
下图是按指定目录输出的tiles文件目录,其实就是以瓦片zxy值为命名规则的图片文件夹。
-
最后QGIS会自动帮我们生成演示页面,用leaflet做调试wmts的示例,简直不要太贴心。
-
最后把生成的tiles部署为静态文件服务,就可以给其他地图引擎做调用了,下午是高德JSAPI调用叠加的效果。
代码实现
-
leaflet叠加自定义Tiles示例
<!DOCTYPE html> <html> <head> <title>Leaflet Preview</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" crossorigin=""/> <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" crossorigin=""></script> <style type="text/css"> body { margin: 0; padding: 0; } html, body, #map{ width: 100%; height: 100%; } </style> </head> <body> <div id="map"></div> <script> var map = L .map('map', { attributionControl: false } ) .setView([21.404263057999998, 111.10203957350001], 14.0); L.control.attribution( { prefix: false } ).addTo( map ); var tilesource_layer = L.tileLayer('${输出目录}/tiles/{z}/{x}/{y}.png', { minZoom: 12, maxZoom: 16, tms: false, attribution: '由QGIS算法创建: 生成 XYZ 瓦片(目录)' }).addTo(map); </script> </body> </html> -
使用node.js搭建自定义的WMTS瓦片地图服务,这里需要注意一点的就是将返回内容响应头设置为允许任何源访问,以避免因浏览器的跨域安全策略导致瓦片请求被阻止。
//加载必须的模块 var http = require('http') var fs = require('fs') var url = require('url') var path = require('path') //定位静态目录的位置,根据请求找出对应的文件 function staticRoot(staticPath, req, res) { var pathObj = url.parse(req.url, true) if (pathObj.pathname === '/') { pathObj.pathname += 'index.html' } //读取静态目录里面的文件,然后发送出去 var filePath = path.join(staticPath, pathObj.pathname) fs.readFile(filePath, 'binary', function (err, content) { if (err) { res.writeHead(404, 'Not Found') res.end('<h1>404 Not Found</h1>') } else { // 设置 CORS 响应头,允许任何源访问 res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' }); res.write(content, 'binary') res.end() } }) } //创建服务器 var server = http.createServer(function (req, res) { staticRoot(path.join(__dirname, ''), req, res) }) //监听8080端口 server.listen(8080) console.log('http://localhost:8080') -
高德地图调用自定义的WMTS
var map = new AMap.Map("map", { viewMode: "3D", zoom: 13, center: [110.85064, 21.401949], pitch: 52, skyColor: "rgba(140, 176, 222, 1)", }); // 添加卫星地图 const satelliteLayer = new AMap.TileLayer.Satellite(); map.add([satelliteLayer]); // 叠加自定义Tiles图层 const layer = new AMap.TileLayer({ tileUrl: "http://localhost:8080/maomingtest/tiles2/[z]/[x]/[y].png", zooms: [9, 16], tileSize: 256, opacity: 0.8, zIndex: 9, }); map.add(layer);
注意点
高德地图API获取
如果你是使用高德地图作为底图集成的,需要保证JSAPI模块的加载地址协议必须与wmts协议一致,因为高德地图的AMap.TileLayer在访问瓦片时会擅自将配置好的地址协议改为与JSAPI引用时使用的协议一样,也许高德开发团队会认为在https环境里访问http会有比较大的风险。
比如下图,明明配置的是http协议,调试的时候会发现图片明明存在但就是无法访问,后来才发现瓦片的地址变成了https, 这个问题暂时还没有办法通过配置解决。 😂
数据版权
使用QGIS或者其他地图数据下载工具,虽然可以获取到数据,但是数据的版权仍然是在地图服务的供应商那边的。在商用的情况下,如果是有明确版权的地图,仍然需要得到供应商的授权,否则私自离线下载作为软件功能对外售卖是不允许的。