使用ArcGIS for JS实现展示海量点标记

4,775 阅读5分钟

介绍

Arcgis作为全球领先的GIS平台软件服务供应商早已声名在外,然而由于盘踞在国内互联网的两大地图服务的影响力和先天语言优势,我一直没有与Arcgis for Js有过多的接触。直到有一天需求场景所迫,某个项目需要更加精细的底图和专用的投影坐标系,让我不得不用耐着性子看了一遍Arcgis for JS文字量庞大的API Reference和Sample Code,并照着Sample和文档将其运用到项目中,项目做完了回过头发现真香。

Arcgis的产品功能、性能,自4.1X版本之后与旧版本相比有了质的飞跃,不过令人赞叹的地方还是其详尽的文档,以及灵活的接口设计思路。

优势

与国内的互联网地图服务相比,Arcgis可能至少有下面几个优势:

  1. 支持自定义部署的地图切片服务
  2. 支持各种官方的或自定义的地理坐标系、投影坐标系
  3. 支持将three.js作为渲染器,进行3D图层的开发
  4. 处理海量数据性能优越
  5. 文档齐全,接口异常灵活

简单的开发示例

Honeycam 2021-06-24 22-05-06.gif

arcgis功能太多,本次就以实现展示海量点标记为例,分步骤讲解实现方式:

  1. 安装依赖包
  2. 初始化地图
  3. 初始化图层
  4. 图层数据筛选
  5. 鼠标交互

安装依赖包

以vue-cli工程为例,安装指令如下,建议使用4.18以上的版本

npm i @arcgis/core -S

安装完之后需要将一部分arcgis静态目录拷贝到发布包中,找到./node_modules/@arcgis/core/assets 并将其拷贝到public/assets。

也可以通过指令在构建时执行相同的动作

"copy": "cp -R ./node_modules/@arcgis/core/assets ./dist/assets",

在入口html文件中引入主题css(可自由切换),以保证arcgis自带组件能正常显示。

<link href="./assets/esri/themes/light/main.css" rel="stylesheet"/>

初始化地图

创建一个名为FeatureLayer.vue的页面放置所有的代码,最简单的创建地图代码如下。

地图需要basemap作为底图,我们可以使用arcgis提供的 各种需要Token或不需要Token的底图,或自己创建的地图服务作为底图。在底图的基础上,还可以通过layers属性在地图上叠加更多的图层,比如路网、水网、电网等等。

除了声明map,还需要实例化一个视图(MapView为2D视图,SceneView为3D视图),这里以2D视图为例,声明视图的容器和各种初始值。

<template>  
    <div class="wrap">    
        <div id="viewDiv" class="container"></div>
    </div>
</template>
<script>
   import Map from '@arcgis/core/Map';
   import MapView from '@arcgis/core/views/MapView';
   export defaultname: 'FeatureLayer', 
        data () {

        },
        mounted () {
            this.initMap()
        },
        methods: {
            initMap(){
                const map = new Map({  
                    basemap: "hybrid" //卫星图  
                })
                //创建2D视图
                const view = new MapView({  
                    container: "viewDiv"map: map,  
                    zoom: 11center: [113.54735115356235, 22.78385103258788], 
               })
               this.map = map
               this.view = view
            }
        }
   }
</script>

初始化图层

Honeycam 2021-06-24 22-08-35.gif

数据量级庞大的图层一般会使用FeatureLayer作为渲染图层,主要有三个步骤,实例化图层、把图层交给map,往图层里塞数据。

实例化图层并交给map

const layer = new FeatureLayer({
   //可以通过map.findLayerById(id)找到图层
  id: 'deviceLayer',
  title: "设备图层",
  //从attributes中获取的内部fields
  fields: [
    {
      name: "ObjectID",
      type: "oid"
    },
    {
      name: "id",
      type: "string"
    },
    {
      name: "deviceUid",
      type: "string"
    },
    {
      name: "deviceStatus",
      type: "string"
    }
  ],
  //让所有fields对外可访问
  outFields: ["*"],
  //点标记的唯一标识
  objectIdField: "ObjectID",
  geometryType: "point",
  //初始数据
  source: [],
  renderer: {
    type: "unique-value",
    //提取deviceStatus作为渲染图标类型的标识
    valueExpression: `return $feature.deviceStatus`,
    //所有图标类型声明
    uniqueValueInfos,
    //根据缩放程度调整尺寸
    visualVariables: [
      {
        type: "size",
        valueExpression: "$view.scale",
        stops: [
          { value: 500000, size: 1 },
          { value: 250000, size: 5 },
          { value: 125000, size: 15 },
          { value: 32000, size: 20 },
        ]
      }
    ]

  },
  //点击点标记后弹窗内容模板
  popupTemplate: {
    title: "{deviceUid}",
    content: [{
      type: "fields",
      fieldInfos: [{
        label: '设备Uid',
        fieldName: 'deviceUid'
      },{
        label: '设备id',
        fieldName: 'id'
      },{
        label: '设备状态',
        fieldName: 'deviceStatus'
      }]
    }]
  }
})
//把图层交给map
this.map.add(layer)

这段初始化代码中非常重要的两个属性是renderer.uniqueValueInfos和renderer.valueExpression,前者声明了所有的图标类型,后者则声明了以那个字段或者组合字段作为图标类型的唯一标识。

在例子中 renderer.valueExpression = return $feature.deviceStatus , 表示取原数据中的deviceStatus属性值(ONLINE,OFFINE, UNACTIVATED)作数据最终显示为哪个图标的依据。

renderer.uniqueValueInfos的声明如下,我们声明了3种图标

uniqueValueInfos = [{
    value: 'ONLINE',
    symbol: {
      type: "picture-marker",
      url: `images/svg/camera_m_1.svg`
    }
},{
    value: 'OFFINE',
    symbol: {
      type: "picture-marker",
      url: `images/svg/camera_m_2.svg`
    }
},{
    value: 'UNACTIVATED',
    symbol: {
      type: "picture-marker",
      url: `images/svg/camera_m_3.svg`
    }
}]

往图层里塞数据

有两种方法: (1)在示例化图层期间填充source属性 (2)通过layer.applyEdits({addFeatures: source}) 往图层里动态加数据

以下为往图层里动态加数据

layer.applyEdits({addFeatures: source}).then(results => {
      //添加成功后执行,打印成功添加的数据量
     console.log(`addFeatures:${results.addFeatureResults.length}`)
})

source 的形式为graphic组成的数组,每个graphic声明了点标注的形态(可能是点、线、面、体、自定义图形等等)和自带属性。

值得注意的一点,如果在当前视图中使用了特定的参考坐标系,则一定要在geometry中通过spatialReference声明坐标系,否则最终点标记很可能在图层中无法显示出来。

 //原始数据
 const data = [{
    location: {latitude: 33, longitude: 112}, //坐标
    deviceStatus: 'ONLINE'//自定义属性
    deviceUid: '123'
 }]
 
//创建graphic组成的数组
const graphics = []
data.forEach(item => {
      const {longitude, latitude} = item.location
      let graphic = new Graphic({
        //每个点的地理属性
        geometry: {
          type: "point",
          latitude,
          longitude,
          // spatialReference: { wkid: WKID },
        },
        //每个点的属性
        attributes: item
      })
      graphics.push(graphic)
})

图层数据筛选

Honeycam 2021-06-24 22-07-17.gif

如图所示,在前端对图层做筛选,仅展示符合条件的点标记,需要调用到图层的filter属性,我们直接将筛选条件赋值给filter(arcgis的各种类,其属性都是可直接读写的),并重新渲染图层即可。

// 调用筛选方法
filterDeviceLayer({
    mode: 'deviceStatus',
    values: ['ONLINE', 'OFFLINE']
})


filterDeviceLayer ({mode, values = []} = {}) {

    const deviceLayer = this.map.findLayerById('deviceLayer')

    this.view.whenLayerView(deviceLayer).then(function (layerView) {

      let queryStr = ''
      if (values.length > 0) {
        queryStr +=  values.map(o => {
          return `${mode}='${o}'`
        }).join(' OR ')
      }
      console.log('queryStr: ', queryStr)  
      //queryStr: deviceStatus = 'ONLINE' OR  deviceStatus = 'OFFLINE'
      layerView.filter = {where: queryStr}

      //重新渲染
      deviceLayer.refresh()
    })

}

鼠标交互

图层的鼠标交互目前只提两种,鼠标悬浮和点击。

鼠标悬浮

监听鼠标移动并检测碰撞对象,如果对象为deviceLayer图层的点标注,则Tip显示点标注的Uid,否则移除Tip。

this.view.on("pointer-move", (event) => {

      const {x, y} = event

      _view.hitTest(event).then((res) => {

        const results = res.results

        if (results.length && results[0].graphic) {

          const {sourceLayer, attributes} = results[0].graphic
          if (!sourceLayer || !attributes) {
            return
          }
          const {deviceUid} = attributes

          if (sourceLayer && ['deviceLayer'].includes(sourceLayer.id)) {
            //tip内容
            this.currTip = `Uid: ${deviceUid}`
            //定位tip
            this.currTipStyle = {top: y + 15 + 'px', left: x + 15 + 'px'}
          } else {
            this.currTip = ''
          }

        } else {
          //tip内容为空时不显示
          this.currTip = ''
        }

      })
})

鼠标点击

类似鼠标悬浮的处理方式,需要重点说明是,在图层初始化时往往会通过声明popupTemplate,来配置默认的弹窗行为和模板。如果你不想用默认弹窗,则不声明popupTemplate,并在 t.view.hitTest的回调函数中编写自己的弹窗逻辑。

你可以从 results[0].graphic.attributes拿到点标注的自定义属性,前提是这些属性在图层初始化时用fields声明过。

const t = this
this.view.on('click', (event) => {

  const {x, y} = event.mapPoint
  console.log({x, y})

   // 这里可以关闭图层初始化时声明的自带弹窗
  // t.view.popup.autoOpenEnabled = false

  t.view.hitTest(event).then((res) => {
    //类似悬浮的逻辑
    console.log(res.results)
  })
})

到此就完成了本次代码示例的所有步骤。

结尾

篇幅有限,仅仅讲了Arcgis for Js 的一个常用功能, 后面有需要再分享其他的,以上内容已整理成工程放到github/study-arcgis

相关链接

developers.arcgis.com/javascript/…