【Babylon提升】加载瓦片地图 (wms)


前言

需要一定的地图知识,这里记录下加载项目中用到的一类wms瓦片地图服务,其他服务应该可以参考,先看成果图。

一、需求分析

1.1 获取指定范围的地图图片

这里加载的是一种wms服务,后面会再补充一篇制作wms服务(是由原始的shape数据通过ArcMap配色,切图后,再通过geowebcache发布出来的流程)。大致是请求这样的图片,其中核心是BBOX,表示图片的左上角和右下角经纬度。

在这里插入图片描述

1.2 计算每片图片的大小和位置

需要掌握比例尺和三维场景中单位长度的关系。

1.3 优化加载速度

多方面优化

  • 初次加载时,同时请求过多图片,导致浏览器卡顿,影响交互。 – 暂时未实现
  • 图片加载优先级,可以优先中心点图片,往边缘扩散。 – 暂时未实现
  • 本地缓存技术,二次加载时避免重复请求,提升加载速度。

二、核心功能的实现 ( 截取部分代码)

2.1 建立三维坐标和经纬度坐标的对应关系

  1. proportion比较难理解,设定一个固定值即可,后面设置一个plane大小和位置时用到。这里就用层级6的比例尺。结合后面代码,相当于层级6是,plane大小为1。
  2. mapCenter需要传一个经纬度值,把这个经纬度映射到三维坐标的(0,0,0)上。
  3. origin 和 levelArray 是和wms相关的配置。renderLevel是希望取哪个层级的瓦片,这里暂时只能取一个层级的瓦片,不像地图缩放层级能取不同层级图片。
  4. getStep() :后面很多地方需要用到,提取成共用方法。
  5. 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 (使用后二次加载几乎能秒出!)

  1. indexedDb操作代码参考这里。https://blog.csdn.net/vagabond_/article/details/137915571
  2. 逻辑比较简单,主要是需要图片时调用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])
    ]
  }
}

三、成品展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值