使用arcgis开发自定义图层-遮罩层

968 阅读3分钟

需求说明

Honeycam 2022-07-28 16-55-11.gif

如图所示,中间区域为第三方调用的区域卫星地图,仅显示指定区域,区域外其余图片信息为空;设计人员为了提升视觉效果,增加了区域边界和区域外的一张暗色底图,需要在地图调整缩放级别时,暗色底图也能同步缩放,且保持相对定位。

找了一下arcgis的官方示例,有个自定义遮罩层的功能比较接近最终效果,实际上是使用了BaseLayerView2D可以创造扩展子类的特性实现了自定义图层,学会了这个方法后其实我们还可以创造更多可观的自定义图层。

developers.arcgis.com/javascript/…

实现思路

自定义图层还是切片地图的模式,即又多块指定大小的正方形切片拼接而成,每个切片的内容可以用cavas绘制,实现步骤如下:

  1. 加载底图文件,完成后开始绘制图层切片;

image.png

  1. 假设当前缩放程度为level,  每个切片在当前level下会有固定的所在行row和所在列col属性(row,col可为负数),作为其唯一标识;

  2. 确定当前level下,底图左上角所在的行列,并以此为原点绘制所有切片,比如切片大小为512,原点位于第0行第0列,则切片col0_row0选取底图从(0,0)到(512,512)的裁剪图作为画布内容; image.png

  3. 计算行政区域边界线与当前切片的交集A,绘制线段A; image.png

  4. 找到边界线和当前切片的面交集M,并挖空M,让底下的卫星图显示出来。 image.png

  5. 调整缩放程度level时,重新执行步骤2

  6. 调整边界线数据时,重新执行步骤2

相关代码

主要框架

内部方法执行顺序为constructor -> attach -> render 

// 声明自定义图层视图
const CustomLayerView2D = BaseLayerView2D.createSubclass({
     //构造器
     constructor: function(){	
    },
   // 图层添加到map实例后执行
   attach: async function () {  
       // 加载背景图片
       // 监听各种属性变化后重绘
       this.watchHandles.add([])
   },
   // 图层被销毁时执行
   detach: () => {
    if (this && this.watchHandles) {
      //清除监听
      this.watchHandles.removeAll()
    }
  },
   // 渲染器
   render(){
      // 更新每个切片,在视野中的切片则重绘,不在视野中的销毁
      this.manageTileImages()
      // 如果整个地图旋转角度不为0的话,将所有切片旋转
      ...
   },
   // 图层与鼠标行为的碰撞检测
   hitTest(mapPoint, { x, y }){
   	 return promiseUtils.resolve({
     	 //自定义碰撞事件
     })
   }
})

// 声明自定义图层类
const MaskLayerClass =  Layer.createSubclass({	
  properties: {
    color: {},	//边界线颜色
    geometry: {}, // 非遮罩区域多边形 Polygon
    id: '', // 图层id
    title: '', // 图层标题
    visible: false, // 图层可见
    spatialReference: { wkid: 4326 }, // 坐标系WGS84
    background: '', // 底图路径
    tileConf: {
    	 9: {  //地图缩放级别level
       minCol: -1,   //原点所在列
       minRow: -3,   //原点所在行
       offsetX: -740, //底图x偏移值
       offsetY: -200, //底图y偏移值
       tileSize: 1500,  //每个切片包含底图的尺寸
       border: 6 //边界线宽度
       },
       ...
    } // 切片配置
  },
  constructor: function (conf) {
    this.color = [0, 0, 0, 1]
    // 切片信息 
    // 坐标系spatialReference一定要声明!!!
    this.tileInfo = TileInfo.create({
      size: SIZE,
      spatialReference: conf.spatialReference
    })   
  }, 
  // 创建图层视图
  createLayerView: function (view) {
    if (view.type === '2d') {
      return new CustomLayerView2D({
        view,
        layer: this
      })
    }
  }
})

// 实例化图层
const layer = new MaskLayerClass({
    ...
})

绘制单个切片内容

// 绘制单个切片
drawGeometryfunction (ctx, bounds, tileIndex, tileInfo) {
  ctx.globalCompositeOperation = 'source-over'
  const width = ctx.canvas.width
  const height = ctx.canvas.height

  // No geometry; entire map is unmasked.
  if (!this.projectedGeometry) {
    ctx.clearRect(00, width, height)
    return
  }

  const { col, row, level } = tileInfo
  const tileMap = this.layer.tileConf

  // 切片贴图
  if (tileMap[level]) {
    const { minCol, minRow, offsetX, offsetY, tileSize } = tileMap[level]
    // 偏移法绘制瓦片
    const startX = (col - minCol) * tileSize + offsetX
    const startY = (row - minRow) * tileSize + offsetY
    ctx.drawImage(
      this.backgroundImg,
      startX, // 开始裁剪x坐标
      startY, // 开始裁剪y坐标
      tileSize, tileSize, // 裁剪尺寸
      00// 图像开始位置
      SIZESIZE // 最终图像尺寸
    )
  }

绘制单个切片边界

// 获整个边界数据
const rings =
  this.projectedGeometry.type === 'extent'
    ? Polygon.fromExtent(this.projectedGeometry).rings
    : this.projectedGeometry.rings ||
    this.projectedGeometry.paths

// 边界坐标转为切片内坐标
const transformed = rings.map((ring) => {
  return ring.map((coords) => {
    return [
      Math.round(
        (width * (coords[0] - bounds[0])) /
        (bounds[2] - bounds[0])
      ),
      Math.round(
        height *
        (1 - (coords[1] - bounds[1]) / (bounds[3] - bounds[1]))
      )
    ]
  })
})

ctx.lineJoin = 'round'
const [red, green, blue, alpha] = this.layer.color

// 绘制边缘
ctx.strokeStyle = `rgba(${red},${green},${blue},${alpha})`
ctx.lineWidth = tileMap[level] ? tileMap[level].border20

for (let i = 0; i < transformed.length; ++i) {
  let ring = transformed[i]

  ctx.beginPath()
  ctx.moveTo(ring[0][0], ring[0][1])

  for (let j = 1; j < ring.length; ++j) {
    ctx.lineTo(ring[j][0], ring[j][1])
  }

  this.projectedGeometry.type !== 'polyline' && ctx.closePath()
  ctx.stroke()
}

镂空单个切片的边界内区域

// 只有源图像外的目标图像部分会被显示,源图像是透明的
ctx.globalCompositeOperation = 'destination-out'
ctx.fillStyle = 'rgba(0, 0, 0, 1)'

for (let i = 0; i < transformed.length; ++i) {
  const ring = transformed[i]

  ctx.beginPath()
  ctx.moveTo(ring[0][0], ring[0][1])

  for (let j = 1; j < ring.length; ++j) {
    ctx.lineTo(ring[j][0], ring[j][1])
  }

  ctx.closePath()
  ctx.fill()
}

扩展思考

2D模式下的arcgis自定义图层都可以在 BaseLayerView2D.createSubclass 的基础上使用Canvas或webGL 技术,查看更多官方效果可以在以下链接搜索 “BaseLayerView2D”

developers.arcgis.com/javascript/…

3D模式可以使用 externalRenderers 实现自定义图层

developers.arcgis.com/javascript/…