加载瓦片地图(wms)
前言
需要一定的地图知识,这里记录下加载项目中用到的一类wms瓦片地图服务,其他服务应该可以参考,先看成果图。
一、需求分析
1.1 获取指定范围的地图图片
这里加载的是一种wms服务,后面会再补充一篇制作wms服务(是由原始的shape数据通过ArcMap配色,切图后,再通过geowebcache发布出来的流程)。大致是请求这样的图片,其中核心是BBOX,表示图片的左上角和右下角经纬度。
1.2 计算每片图片的大小和位置
需要掌握比例尺和三维场景中单位长度的关系。
1.3 优化加载速度
多方面优化
- 初次加载时,同时请求过多图片,导致浏览器卡顿,影响交互。 – 暂时未实现
- 图片加载优先级,可以优先中心点图片,往边缘扩散。 – 暂时未实现
- 本地缓存技术,二次加载时避免重复请求,提升加载速度。
二、核心功能的实现 ( 截取部分代码)
2.1 建立三维坐标和经纬度坐标的对应关系
- proportion比较难理解,设定一个固定值即可,后面设置一个plane大小和位置时用到。这里就用层级6的比例尺。结合后面代码,相当于层级6是,plane大小为1。
- mapCenter需要传一个经纬度值,把这个经纬度映射到三维坐标的(0,0,0)上。
- origin 和 levelArray 是和wms相关的配置。renderLevel是希望取哪个层级的瓦片,这里暂时只能取一个层级的瓦片,不像地图缩放层级能取不同层级图片。
- getStep() :后面很多地方需要用到,提取成共用方法。
- getVector3ByXy 和 getXyByVector3 暂时未用到,但要在地图上继续其他业务时会用到。
// 地图管理器
class MapManger {
// 渲染中心点
mapCenter:number[]
// 渲染层级
renderLevel: number
// 比例尺 不同层级,比例尺不同
levelArray: number[]
origin: number[]
proportion: number // 1个图片plane相对大小 1 = this.proportion * 256
constructor(mapCenter:number[], renderLevel: number){
this.mapCenter = mapCenter
this.renderLevel = renderLevel
this.proportion = 0.00015228550437313792
this.origin = [-400, 400]
// 不同层级下的比例尺, 自己做的切图这个可以直接获取,开源的地图需要网上找资料。
this.levelArray = [
0.0095178440233211203,
0.0047589220116605602,
0.0023794610058302801,
0.00118973050291514,
0.00059486525145757002,
0.00029743262572878501,
0.00015228550437313792, // 6
7.6142752186568962e-005,
3.8071376093284481e-005,
1.903568804664224e-005,
9.5178440233211202e-006
]
}
// 不同层级下的单块图片的大小。 这里默认用的是256大小的图片
getStep () :number {
const l = this.levelArray[this.renderLevel]
const size = 256
return l * size
}
// 根据经纬度获取position位置
getVector3ByXy (point: number[]) {
const step = this.getStep()
const size = this.proportion / this.levelArray[this.renderLevel]
const [x, y] = point
const [x0, y0] = this.mapCenter
return new BABYLON.Vector3((x - x0) / step * size , 0, (y - y0) / step * size)
}
// 根据position位置获取经纬度
getXyByVector3 (vector3:BABYLON.Vector3) {
const step = this.getStep()
const size = this.proportion / this.levelArray[this.renderLevel]
const {x , y ,z } = vector3
const [x0, y0] = this.mapCenter
return [x0 + x * step / size, y0 + z * step / size]
}
}
2.2 图片本地缓存 indexedDB (使用后二次加载几乎能秒出!)
- indexedDb操作代码参考这里。https://blog.csdn.net/vagabond_/article/details/137915571
- 逻辑比较简单,主要是需要图片时调用getImageData方法,内部逻辑判断是下载还是直接从缓存获取,存储的格式是Blob类型。
// 图片池
class PhotoPool {
// 存储数据库名称
dbName: string = 'imageCacheDB'
// 存储空间名称
storeName: string = 'images'
constructor () {
// 1、打开或创建 IndexedDB 数据库
const dbPromise = indexedDB.open(this.dbName, 1);
// 定义对象存储空间, 只有在创建时会调用
dbPromise.onupgradeneeded = (event:any) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' });
}
};
}
/**
* 获取图片
* @param id 存储时的key,这里用瓦片的横纵序号
* @param url 瓦片地址
* @param callback 回调函数
*/
getImageData (id: string, url: string, callback: Function) {
// 3、 读取
const dbPromise :IDBOpenDBRequest = indexedDB.open(this.dbName, 1);
dbPromise.onsuccess = (event:any) => {
const db = event.target.result;
const tx = db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = (event:any) => {
const blob = event.target.result;
if (blob) {
callback(blob.imageData)
} else {
this.downloadImage(id, url, callback)
}
};
};
}
/**
* 下载并存储图片
* @param id 存储时的key,这里用瓦片的横纵序号
* @param url 瓦片地址
* @param callback 回调函数
*/
downloadImage (id: string, url: string, callback: Function) {
// const req = axios.get( url, { responseType: 'arraybuffer' });
// 发送GET请求获取图片
axios.get(url, { responseType: 'blob' })
.then(response => {
// 创建一个Blob对象
const blob = new Blob([response.data], { type: 'image/jpeg' });
console.log(blob)
callback(blob)
// 2、 新增 or 修改
const dbPromise = indexedDB.open('imageCacheDB');
dbPromise.onsuccess = (event:any) => {
const db = event.target.result;
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
store.put({ id: id, imageData: blob }); // 将 Blob 对象存储到 IndexedDB
};
})
.catch(error => {
console.error('请求图片失败:', error);
});
}
}
2.3 加载瓦片
步骤
- 从 createTiled 方法开始看
- 通过 getBbox() 方法获取指定位置的瓦片 BBOX
- 根据渲染大小 number * number ,计算出整体大小的左上角经纬度
- 根据 number * number 和 每块图片的大小step,循环加载每一块图片plane
难点:
- getBbox():这个方法非常关键,主要根据 origin 原点位置 和 上面getStep() 来计算当前经纬度所在的瓦片范围。理论文章参考:https://www.cnblogs.com/zhangbig/p/17439290.html
- planeMaterial.diffuseTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; 这两行代码能消除两张图片之间的缝隙。
// 地图管理器
class MapManger {
...
// wsm图层名称
LAYERS: string
// 本地缓存图片控制器
pool: PhotoPool
constructor(mapCenter:number[], renderLevel: number){
...
this.LAYERS = 'xsCockpitBlackMap'
this.pool = new PhotoPool()
}
...
// 获取bbox 指定中心点对应的bbox
getBbox (point: number[]) : number[]{
const [ x, y ] = point
// 原点
const step = this.getStep()
const xNum = Math.ceil((x - this.origin[0]) / step) - 1
const yNum = Math.ceil((y - this.origin[1]) / step) - 1
const bbox = []
bbox.push(step * xNum + this.origin[0])
bbox.push(step * yNum + this.origin[1])
bbox.push(bbox[0] + step)
bbox.push(bbox[1] + step)
return bbox
}
/**
* 创建一片 以 center 为中心的 number * number 瓦片地图
* @param scene Babylons场景
* @param center 加载瓦片的中心点, 可以和场景中心点不同。
*/
createTiled (scene : BABYLON.Scene, center: number[]) {
const node = new BABYLON.TransformNode('xyzNode')
// 渲染图片数量。 number * number
const number = 18
const step = this.getStep()
// 1、 获取 center 的bbox
const bbox: number[] = this.getBbox(center)
// 2、 获取左上角顶点位置
const letfPoint = [ bbox[0] - Math.floor(number / 2) * step, bbox[1] - Math.floor(number / 2) * step]
for (let x = 0; x < number; x++) {
for (let y = 0; y < number; y++) {
const indexBBox = [
letfPoint[0] + x * step,
letfPoint[1] + y * step,
letfPoint[0] + x * step + step,
letfPoint[1] + y * step + step
]
const name = this.getTileNumber(indexBBox[0], indexBBox[1]).join('-')
const size = this.proportion / this.levelArray[this.renderLevel]
const plane = BABYLON.MeshBuilder.CreatePlane(name, {width: size, height: size}, scene);
plane.parent = node
plane.position.x = (this.mapCenter[0] - (indexBBox[0] + 1 / 2 * step)) / step * size
plane.position.z = (this.mapCenter[1] - (indexBBox[1] + 1 / 2 * step)) / step * size
plane.rotation.x = Math.PI / 2
plane.rotation.y = Math.PI
const planeMaterial = new BABYLON.StandardMaterial("planeMaterial");
this.pool.getImageData(name, this.getTileUrl(indexBBox), (blob:Blob) => {
const texture = new BABYLON.Texture(URL.createObjectURL(blob)) // 替换为你的纹理图片路径
planeMaterial.diffuseTexture = texture
planeMaterial.diffuseTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE;
planeMaterial.diffuseTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;
})
planeMaterial.backFaceCulling = false; // 如果你想要平面的背面也能看到纹理
planeMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
planeMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
plane.material = planeMaterial;
}
}
}
// 获取wms瓦片地址
getTileUrl (bbox: number[]) {
// 和 wsm 有关, 需要传递的参数主要是LAYERS 和BBOX
const urlBase = `/geowebcache/service/wms?LAYERS=${this.LAYERS}&FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4326&WIDTH=256&HEIGHT=256`
return `${urlBase}&BBOX=${bbox.toString()}`
}
// 获取序号
getTileNumber (x:number , y:number) {
return [
Math.round((x - this.origin[0]) / 256 / this.levelArray[this.renderLevel]),
Math.round((y - this.origin[1]) / 256 / this.levelArray[this.renderLevel])
]
}
}