1. 什么是XYZ瓦片

XYZ瓦片是一种在线地图数据格式,常见的地图底图如Google、OpenStreetMap 等互联网的瓦片地图服务,都是XYZ瓦片,严格来说是ZXY规范的地图瓦片

ZXY规范的地图瓦片规则如下:将地图全幅显示时的图片从左上角开始,往下和往右进行切割,切割的大小默认为 256*256 像素,左上角的格网行号为 0,列号为 0,往下和往右依次递增,如下图所示:

从整体来说,XYZ瓦片数据结构是一种影像金字塔,如下图所示:

对于用户端的软件来说,所谓浏览XYZ格式的地图,就是根据当前的缩放等级和屏幕显示的地理范围,去服务端加载对应的XYZ瓦片(通常是PNG图片)

2. XYZ瓦片与经纬度的计算以及原理

首先给出经纬度与XYZ行列号之间的计算公式:

现在解释一下原理

下面是一张OpenStreetMap在zoom等级为2时的瓦片示意图

z 是当前的瓦片等级,就是缩放等级,由上面的图可以看出:z 等级时,共有\(2^z\)个瓦片,x范围为0-\(2^z-1\),y范围也是0-\(2^z-1\)

首先 x 的计算很简单:

  • 目的:将经度从-180度到180度,映射到0到\(2^z\)之间的整数列号上
  • 过程:先将经度加180度,使其从0到360度,然后除以360(归一化)再乘以\(2^z\)得到行号,最后向下取整数部分,得到最终的行号

y 的计算就复杂多了:

  • 目的:将纬度从-90度到90度,映射到0到\(2^z\)之间的整数行号上

  • 存在的问题:纬度分布不均匀,XYZ瓦片试图将地图展开为一个正方形(参考上图,本质上就是Web墨卡托投影),然而纬度是中间(赤道)长两极短,如果只是像 x 一样简单的映射,会导致两极的紧凑,赤道附近稀疏

  • 解决方案:将纬度通过一种映射,使其能均匀一点,然后就采用了下面的函数

    \[y=\frac{\left(1-\ln(\tan(x)+1/\cos(x))/\pi\right)}{2}
    \]

    这个函数图像如下图所示:

  • 过程:在采取上面的这个纬度的映射函数以后(归一化),再乘以\(2^z\),最后向下取整数部分,得到最终的列号

3. 在浏览器端实现XYZ瓦片的加载示例

3.1 计算公式实现

根据上面的公式,很容易就把根据经纬度算行列号的函数写出来

function lon2tile(lon, zoom) {
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)))
} function lat2tile(lat, zoom) {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)))
}

事实上,这个网站已经给出了这个公式的各种编程语言的实现:Slippy map tilenames - OpenStreetMap Wiki

3.2 核心代码

根据经纬度计算XYZ瓦片的URL,并加载到浏览器上,核心代码如下

function lon2tile(lon, zoom) {
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
} function lat2tile(lat, zoom) {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
} const loadMapByBounds = (minLon, minLat, maxLon, maxLat, zoom) => {
const minTileX = lon2tile(minLon, zoom);
const minTileY = lat2tile(maxLat, zoom); // Y轴是反的,自上而下
const maxTileX = lon2tile(maxLon, zoom);
const maxTileY = lat2tile(minLat, zoom);
for (let x = minTileX; x <= maxTileX; x++) {
for (let y = minTileY; y <= maxTileY; y++) {
loadTile(x, y, zoom); // 加载瓦片
}
}
}

3.3 完整实现

为了简单,这里使用img标签来加载瓦片图,并根据瓦片编号排列,设置对应的偏移值

为了能拖动以浏览全图实现简单的交互,这里还设置了根据鼠标按压后拖动的偏移值来添加对应的偏移值

实现效果如下:

完整代码如下:

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
img {
width: 256px;
height: 256px;
} html,
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100%;
width: 100%;
} #map {
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
border: 1px solid #000;
}
</style>
</head> <body>
<div id="map"></div>
<script>
function lon2tile(lon, zoom) {
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
} function lat2tile(lat, zoom) {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
} // 根据鼠标滚轮缩放地图
let zoom = 2; document.body.onwheel = (e) => { if (e.deltaY > 0) {
zoom--;
} else {
zoom++;
}
if (zoom < 0) {
zoom = 0;
return;
} document.querySelector('#map').innerHTML = "";
// EPSG:3857(Web墨卡托投影) 对应的 WGS84范围:-180.0 ,-85.06,180.0, 85.06,不在这个经纬度范围内,地图会显示异常(没有这个瓦片)
const x1 = lon2tile(-179, zoom);
const y2 = lat2tile(-80, zoom);
const x2 = lon2tile(179, zoom);
const y1 = lat2tile(80, zoom);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2; for (let y = y1; y <= y2; y++) {
for (let x = x1; x <= x2; x++) {
const img = document.createElement("img");
img.src = `https://a.tile.openstreetmap.org/${zoom}/${x}/${y}.png`;
img.alt = `${zoom}-${x}-${y}`;
img.style.position = "absolute";
img.draggable = false;
// img.style.left = `${(x - x1) * 256}px`;
// img.style.top = `${(y - y1) * 256}px`;
img.style.left = `${(x - centerX) * 256 + 256}px`;
img.style.top = `${(y - centerY) * 256 + 256}px`;
document.querySelector('#map').appendChild(img);
}
} } const event = new Event("wheel")
document.body.dispatchEvent(event); document.body.onmousedown = (e) => {
document.body.style.cursor = "grabbing";
document.querySelector('#map').onmousemove = (e) => {
// 移动地图
const x = e.movementX;
const y = e.movementY;
const map = document.querySelector('#map');
map.childNodes.forEach((img) => {
img.style.left = `${parseInt(img.style.left) + x}px`;
img.style.top = `${parseInt(img.style.top) + y}px`;
});
}
} document.body.onmouseup = (e) => {
document.body.style.cursor = "default";
document.querySelector('#map').onmousemove = null;
} </script>
</body> </html>

4. 参考资料

[1] Slippy map tilenames - OpenStreetMap Wiki

[2] ZXY标准瓦片 (supermap.com.cn)

[3] 瓦片底图:在线地图的下载和使用 | Mars3D开发教程

GIS中XYZ瓦片的加载流程解析与实现的更多相关文章

  1. Android 8.1 SystemUI虚拟导航键加载流程解析

    需求 基于MTK 8.1平台定制导航栏部分,在左边增加音量减,右边增加音量加 思路 需求开始做之前,一定要研读SystemUI Navigation模块的代码流程!!!不要直接去网上copy别人改的需 ...

  2. angular源码分析:angular的整个加载流程

    在前面,我们讲了angular的目录结构.JQLite以及依赖注入的实现,在这一期中我们将重点分析angular的整个框架的加载流程. 一.从源代码的编译顺序开始 下面是我们在目录结构哪一期理出的an ...

  3. 在Unity3D的网络游戏中实现资源动态加载

    用Unity3D制作基于web的网络游戏,不可避免的会用到一个技术-资源动态加载.比如想加载一个大场景的资源,不应该在游戏的开始让用户长时间等待全部资源的加载完毕.应该优先加载用户附近的场景资源,在游 ...

  4. android源码解析(十七)-->Activity布局加载流程

    版权声明:本文为博主原创文章,未经博主允许不得转载. 好吧,终于要开始讲讲Activity的布局加载流程了,大家都知道在Android体系中Activity扮演了一个界面展示的角色,这也是它与andr ...

  5. HTML页面加载和解析流程详细介绍

    浏览器加载和渲染html的顺序 1. IE下载的顺序是从上到下,渲染的顺序也是从上到下,下载和渲染是同时进行的. 2. 在渲染到页面的某一部分时,其上面的所有部分都已经下载完成(并不是说所有相关联的元 ...

  6. html页面加载和解析流程

    HTML页面加载和解析流程 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件: 浏览器开始载入html代码,发现<head>标签内有一 ...

  7. MapXtreme在asp.net中的使用之加载地图(转)

    MapXtreme在asp.net中的使用之加载地图(转) Posted on 2010-05-04 19:44 Happy Coding 阅读(669) 评论(0) 编辑 收藏 1.地图保存在本地的 ...

  8. Android5.1图库Gallery2代码分析数据加载流程

    图片数据加载流程. Gallery---->GalleryActivity------>AlbumSetPage------->AlbumPage--------->Photo ...

  9. Cocos Creator 资源加载流程剖析【二】——Download部分

    Download流程的处理由Downloader这个pipe负责(downloader.js),Downloader提供了各种资源的"下载"方式--即如何获取文件内容,有从网络获取 ...

  10. Cocos Creator 资源加载流程剖析【一】——cc.loader与加载管线

    这系列文章会对Cocos Creator的资源加载和管理进行深入的剖析.主要包含以下内容: cc.loader与加载管线 Download部分 Load部分 额外流程(MD5 Pipe) 从编辑器到运 ...

随机推荐

  1. Liunx-LVM创建与扩容

    LVM是 Logical Volume Manager(逻辑卷管理)的简写,它是Linux环境下对磁盘分区进行管理的一种机制,它由Heinz Mauelshagen在Linux 2.4内核上实现,最新 ...

  2. LOTO示波器的变长存储深度和分段存储

    LOTO示波器的变长存储深度和分段存储 经常有客户咨询和不理解LOTO示波器的存储深度为什么是变长的,也表示对LOTO示波器的分段存储功能不理解,本文对LOTO示波器的存储机制做一次完整的梳理,帮助我 ...

  3. #dp#CodeChef Little Elephant and Mouses

    LEMOUSE 分析 由于被单只老鼠吓到只能算一次,所以前两次走的位置也可能会被老鼠吓到. 设 \(dp[n][m][o][p]\) 表示走到 \((n,m)\) 上一步走的是 \(o\) 这种方式, ...

  4. #树状数组,并查集#CF920F SUM and REPLACE

    题目 分析 由于\(a_i=1或2\)时\(d(a_i)=a_i\),且其余情况修改后答案只会越来越小, 考虑用树状数组维护区间和,用并查集跳过\(a_i=1或2\)的情况 代码 #include & ...

  5. HttpOnly的笔记

    HttpOnly是什么 简单的说: HttpOnly是Cookie的属性: Web服务器通过为Cookie设置这个属性,告诉浏览器应当避免页面端的脚本访问对应Cookie的值,保证被Cookie承载的 ...

  6. JDK9的新特性:String压缩和字符编码

    目录 简介 底层实现 总结 简介 String的底层存储是什么?相信大部分人都会说是数组.如果要是再问一句,那么是以什么数组来存储呢?相信不同的人有不同的答案. 在JDK9之前,String的底层存储 ...

  7. 部署solr服务

    前言:请各大网友尊重本人原创知识分享,谨记本人博客:南国以南i 一.Sorl单机部署 准备:solr5.5.tomcat8.5.jdk1.8 1.解压 solr-5.5.0.zip压缩包 2.复制./ ...

  8. Avalonia中的自绘控件

    在构建用户界面时,控件扮演着至关重要的角色.它们不仅负责展示内容,还处理用户的交互.然而,有时标准的控件库可能无法满足我们的需求,这时自绘控件就显得尤为重要.在Avalonia UI框架中,自绘控件允 ...

  9. MOGDB/openGauss索引推荐及虚拟索引

    MOGDB/openGauss 索引推荐及虚拟索引 索引推荐 在 ORACLE 的优化中,可能大家有接触过 SQL Tuning Advisor(SQL 调优顾问,STA),类似的 MOGDB/ope ...

  10. Leetcode-最小覆盖子串

    题目描述 给你一个字符串 s .一个字符串 t .返回 s 中涵盖 t 所有字符的最小子串.如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" . 注意:如果 s 中 ...