一、概要
在 OpenLayers 中,TileLayer 用于显示地图瓦片,以不同缩放级别和地理区域切分管理地图各部分的图片。本文对使用固定的 URL 模板和自定义瓦片网格两种方式对Openlayers 瓦片请求行为进行探究,并列举自定义瓦片网格的使用场景。
二、 瓦片请求探究
- 使用固定 URL 模板加载谷歌影像:
const map = new Map({
target,
layers: [
new TileLayer({
source: new XYZ({
url: 'https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous',
wrapX: true
})
})
],
view: new View({
projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
zoom: 0, // 打开页面时默认地图缩放级别
center: fromLonLat([121.5, 25]), // 转到墨卡托坐标系
})
})
加载结果:
url参数中, 地址字符串内{x}、{y} 和 {z} 是占位符
当加载特定切片时,会根据当前视图的 x、y 和 z 值替换占位符,生成对应切片的 URL
- x={x}: 占位符 {x} 会被切片的水平索引替换。
- y={y}: 占位符 {y} 会被切片的垂直索引替换。
- z={z}: 占位符 {z} 会被缩放级别替换。
除使用固定的 URL 模板外,地图数据源 (new XYZ) 还接受自定义瓦片网格的回调函数( tileUrlFunction),tileUrlFunction 接受一个三维坐标参数,可动态生成返回每个切片的 URL,用于处理特殊的瓦片请求或自定义加载逻辑。
- 使用自定义瓦片网格加载谷歌影像:
const map = new Map({
target,
layers: [
new TileLayer({
source: new XYZ({
tileUrlFunction,
//url: 'https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous',
wrapX: true
})
})
],
view: new View({
projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
zoom: 0, // 打开页面时默认地图缩放级别
center: fromLonLat([121.5, 25]), // 转到墨卡托坐标系
})
})
/**
* @description 自定义瓦片网格回调函数,作为 XYZ数据源 tileUrlFunction 的参数
* @param {number[]} zxy 瓦片坐标 [z, x, y]
* @returns {string} 生成的瓦片 URL
*/
function tileUrlFunction(zxy: number[]): string {
const [z, x, y] = zxy
return `https://www.google.com/maps/vt?lyrs=y&gl=cn&x=${x}&y=${y}&z=${z}`
}
加载结果:
(和使用固定的 URL 模板加载的执行结果是一样的)
参数解析:
tileUrlFunction 的参数 zxy ,是瓦片网格坐标系的三维坐标数组
其中,z 表示缩放级别, x 和 y 表示切片的行列索引。
经测试,具体关系如下:
- 在特定的 z 级别下,x 和 y 确定具体的切片位置。x 从左到右递增,y 从上到下递增。
- x 和 y 的取值范围是从 0 到 2^z - 1。
根据其特性,就可以花式加载瓦片了。
三、自定义瓦片网格的使用场景
- 根据比例尺缩放加载不同底图(当然也可以通过监听地图缩放事件实现):
const map = new Map({
target,
layers: [
new TileLayer({
source: new XYZ({
tileUrlFunction,
//url: 'https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous',
wrapX: true
})
})
],
view: new View({
projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
zoom: 0, // 打开页面时默认地图缩放级别
center: fromLonLat([121.5, 25]), // 转到墨卡托坐标系
})
})
/**
* @description 根据瓦片坐标生成瓦片的 URL
* @param {number[]} zxy 瓦片坐标 [z, x, y]
* @returns {string} 生成的瓦片 URL
*/
function tileUrlFunction(zxy: number[]): string {
const [z, x, y] = zxy;
if (z > 4) {
// 缩放级别大于4
return `http://t0.tianditu.gov.cn/DataServer?T=vec_w&x=${x}&y=${y}&l=${z}&tk=99a8ea4a53c8553f6f3c565f7ffc15ec`;
} else {
// 缩放级别小于等于4
return `https://www.google.com/maps/vt?lyrs=y&gl=cn&x=${x}&y=${y}&z=${z}`;
}
}
效果:
- 根据圈定范围加载不同底图:
(小范围内加载的精细影像,全球范围加载低层级影像,当然也可以通过图层叠加方式实现)
<template>
<!--地图-->
<div ref="mapContainer" class="mapContainer" id="mapContainer"></div>
</template>
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue'
import { View, Map } from "ol"
import { fromLonLat } from 'ol/proj'
import TileLayer from 'ol/layer/Tile'
import { XYZ } from 'ol/source'
import { intersects } from 'ol/extent'
// 地图容器
const mapContainer = shallowRef<HTMLDivElement>()
// 地图对象
const map = shallowRef<Map>()
// 范围
const chinaExtent = [74.69154385120196, -0.000001, 137.8205912282765, 55.456454556548]
/**
* @description 创建地图实例
* @param {Document | DocumentId} target 地图容器
* @returns 地图对象
*/
const createMap = function (target: HTMLElement | string,): Map {
// 创建地图
const map = new Map({
target,
layers: [
new TileLayer({
source: new XYZ({
tileUrlFunction,
//url: 'https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous',
wrapX: true
})
})
],
view: new View({
projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
zoom: 0, // 打开页面时默认地图缩放级别
center: fromLonLat([121.5, 25]), // 转到墨卡托坐标系
})
})
return map
}
/**
* @description 确认瓦片是否与指定边界范围相交
* @param {number[]} zxy 瓦片坐标 [z, x, y]
* @returns {boolean} 相交判定
*/
function isIntersect(zxy: number[]): boolean {
// 获取瓦片的地理坐标范围
const extent = getExtent(zxy);
return intersects(extent, chinaExtent);
}
/**
* @description 将瓦片坐标转换为地理坐标范围
* @param {number[]} zxy 瓦片坐标 [z, x, y]
* @returns {number[]} 瓦片的地理坐标范围 [minX, minY, maxX, maxY]
*/
function getExtent(zxy: number[]): number[] {
const [z, x, y] = zxy;
// 计算当前缩放级别的比例因子 2^z
const scale = Math.pow(2, z);
// 计算瓦片在经度方向的范围
const minX = (x / scale) * 360 - 180;
const maxX = ((x + 1) / scale) * 360 - 180;
// 计算瓦片在纬度方向的范围
const minY = 90 - (y / scale) * 180;
const maxY = 90 - ((y + 1) / scale) * 180;
// 返回地理坐标范围
return [minX, minY, maxX, maxY];
}
/**
* @description 根据瓦片坐标生成瓦片的 URL
* @param {number[]} zxy - 瓦片坐标 [z, x, y]
* @returns {string} - 生成的瓦片 URL
*/
function tileUrlFunction(zxy: number[]): string {
const [z, x, y] = zxy;
// 判断瓦片是否与边界范围相交(这里也可以同时加入缩放比例尺判定)
if (isIntersect(zxy)) {
// 瓦片与边界相交,使用天地图 URL
return `http://t0.tianditu.gov.cn/DataServer?T=vec_w&x=${x}&y=${y}&l=${z}&tk=99a8ea4a53c8553f6f3c565f7ffc15ec`;
} else {
// 瓦片不与边界相交,使用 Google URL
return `https://www.google.com/maps/vt?lyrs=y&gl=cn&x=${x}&y=${y}&z=${z}`;
}
}
onMounted(async () => {
map.value = createMap(mapContainer.value!);
})
</script>
<style lang="scss">
#mapContainer {
position: absolute;
top: 0;
z-index: 0;
width: 100%;
height: 100%;
}
</style>
效果:
- 适应自定义瓦片切片规则
一般瓦片的切片文件夹组织结构是类似这样的↓
这种情况使用固定 URL 模板加载是没有问题的:
‘https://www.google.com/maps/vt?lyrs=y&gl=cn&x={x}&y={y}&z={z}’
但如果瓦片切片是使用16进制命名组织的:
此时无法直接使用URL模板加载,需换算zxy至16进制并补0,需要使用自定义瓦片网格方法:
/**
* @description 将数字转换为 8 位 16 进制字符串
* @param {number} num 要转换的数字
* @returns {string} 8 位 16 进制字符串
*/
function toHex8(num: number): string {
return num.toString(16).padStart(8, '0'); // 转换为 16 进制并补齐到 8 位
}
/**
* @description 根据瓦片坐标生成自定义的瓦片 URL
* @param {number[]} zxy 瓦片坐标 [z, x, y]
* @returns {string} 生成的瓦片 URL
*/
function tileUrlFunction(zxy: number[]): string {
const [z, x, y] = zxy;
// 将瓦片坐标转换为 8 位 16 进制路径
const hexX = toHex8(x);
const hexY = toHex8(y);
// 构建瓦片 URL
return `https://tileserver.com/${z}/${hexX}/${hexY}.png`;
}
也可以花里胡哨: