maptalks 开发手册-进阶篇

前言

在上一篇中,对maptalks的基础功能,及地图如何绘制已经了解,对于有探索能力 的小伙伴可能已经完成了更加高级的功能,但在这里,作为手册性质还是会慢慢记录下开发中的细节。

客户需要的效果千姿百态,但也不可逃离的是功能性的变化。

下面的例子基于上一遍的例子进行

项目代码地址(demo)

mark

实际应用中的创建与消除

在实际应用中,mark标记,是随着用户选择的类型进行显示,那么这涉及到了mark的消除与创建。

  1. 我们先定义一个页面,如下,在地图上方留一个工具栏
<template>
  <div class="container">
    <div class="map-toolbar">
      <el-checkbox-group v-model="checkList" @change="handleCheckChange">
        <el-checkbox :label="item.label" v-bind:key="item.value" :value="item.value" v-for="item in chooseData"></el-checkbox>
      </el-checkbox-group>
    </div>
    <div
      id="map"
      class="map-container"
    />
  </div>
</template>

<style scoped lang="scss">
  html, body {
      
      
    margin: 0px;
    height: 100%;
    width: 100%;
  }
.container {
      
      
  width: 100%;
  height: 100%;
  position: absolute
}
.map-toolbar {
      
      
  position: relative;
  z-index: 1000;
  width: 500px;
  height: 50px;
  float: right;
  top:10px;
  right: 300px;
  background-color: #fff;
  border-radius:5px;
  padding: 10px;

}
  .map-container {
      
      
    width: 100%;
    height: 100%
  }
</style>

image-20211031113311804

  1. 定义方法:

    // 定义两个常量(仅测试)   
    const schoolCoordinate = [
            {
          
          
                adcode: 510104,
                name: "小学校",
                center:  [118.13734735854666, 24.498801064931087],
                parent: {
          
          
                    adcode: 510100
                },
            },  {
          
          
                adcode: 510104,
                name: "大学校",
                center:  [118.11804966596969, 24.47633923130772],
                parent: {
          
          
                    adcode: 510100
                },
            }
        ]
    
        const hospitalCoordinate = [
            {
          
          
                adcode: 510104,
                name: "小医院",
                center:  [118.18303419164465, 24.51143878000599],
                parent: {
          
          
                    adcode: 510100
                },
            },{
          
          
                adcode: 510104,
                name: "大医院",
                center:  [118.19412188942488, 24.53586932160701],
                parent: {
          
          
                    adcode: 510100
                },
            }
    
        ]
    // data 里定义的变量:
    // 类型复选框
    checkList: [],
        chooseData: [{
          
          
            label: '学校',
            value: 'school'
        }, {
          
          
            label: '医院',
            value: 'hospital'
        }],
            // 保持选择的图层的名称
            checkLayers: []
    
    
    // methods定义的方法
    drawMark(centerPointList, layer) {
          
          
         if (!centerPointList) {
          
          
             console.log('无区域中心点数据')
             return
         }
         const info = {
          
           content: '', width: 150, minHeight: 100 }
         const result = []
         // 这里 d 的数据格式是数组,如:[-0.113049, 51.498568]
         centerPointList.forEach(d => {
          
          
             if (!d.info) {
          
          
                 d.info = info
             }
             // 设有高度、高亮的mark
             const mark = new maptalks.Marker(d.center, {
          
          
                 // 设置了这个属性,会替换默认的图标
                 // symbol: {
          
          
                 // markerFile: 'foo.png',
                 // textName: d.name
                 // },
                 properties: {
          
          
                     // 高度设置
                     altitude: 50
                 }
             }).addTo(layer)
             mark.setInfoWindow({
          
          
                 title: d.name,
                 content: '<div>' + d.adcode + '</div>',
                 // autoPan: true,
                 width: d.info.width,
                 minHeight: d.info.minHeight,
             })
    
             mark.setZIndex(1000)
             result.push(mark)
         })
         return result
     },
    
     // 复选框选中
         handleCheckChange(data) {
          
          
             console.log(data)
             const _t = this
             // 清楚图层
             this.checkLayers.forEach(l => {
          
          
                 l.clear()
             })
             const schoolMarkLayer = this.mapEngine.getLayer('schoolMark')
             const hospitalMarkLayer = this.mapEngine.getLayer('hospitalMark')
             data.forEach(d => {
          
          
                 if (d === '学校') {
          
          
                     _t.drawMark(schoolCoordinate, schoolMarkLayer)
                     _t.checkLayers.push('schoolMark')
                 }
                 if (d === '医院') {
          
          
                     _t.drawMark(hospitalCoordinate, hospitalMarkLayer)
                     _t.checkLayers.push('hospitalMark')
                 }
             })
         },
    

效果不知道怎么传视频。代码已竟可能全了,没有全贴是不想占太多空间;

这里说一下,它的一些方法

  1. layer.clear() ,他会清除图层上的所以东西;
  2. mark.remove() , 移除mark可以用这个,不过这个需要mark对象调用,如果前端要实现上述功能,那么就要保存mark列表,这个很不明智,遇到撒点多的时候,这个前端可能承受不了;所以这里缓存了每个类型的图层,使用图层进行操作;

自定义图标

这里就以vue的logo作为替换图标进行示例

顶部require引入静态资源
const logo = require('../../assets/logo.png')


// 创建时,使用symbol替换默认的样式
const mark = new maptalks.Marker(d.center, {
    
    
                        symbol: {
    
    
                            // markerType: 'square',
                            markerFile: logo,
                            markerWidth: 40,
                            markerHeight: 40,
                            markerDx: 0,
                            markerDy: 0,
                            markerOpacity: 1
                        },
                        properties: {
    
    
                            // 高度设置
                            altitude: 0
                        }
                    }).addTo(layer)

这里markerFile才是替换图标的,markerType,marker类型,它表示的是什么样的图标,当你有markerFile属性时,它是覆盖不了的,没有markerFile时,可以以markerType进行显示,它有几种类型:ellipse cross x diamond bar square triangle pin pie,效果可以自己设置一下看看。

效果如下:

image-20211106190724575

增加动画效果

增加了自己的mark后,可能会要求视觉上的一些效果,mark也提供了animate的方法设置自己的动画,那么我们就设置一个mark出现时的动画,vue logo的横向展开,

在上面代码的基础上增加下面代码,然后将new maptalks.Marker里默认设置的symbol.markerHeight设置为0, symbol.markerWidth设置为10,表示初始高度0, 初始宽度10,,变换到高度40, 宽度40。

mark.animate({
    
    
    symbol: {
    
    
        markerWidth: 40,
        markerHeight: 40
    },
    properties: {
    
    
        altitude: 800
    }
}, {
    
    
    duration: 150,
}, function (frame) {
    
    
    if (frame.state.playState === 'finished') {
    
    
        console.log('animation finished');
    }
});

当我们点击学校或医院选项时,这些撒点就自动展开,也可以增加监听,在每个动画阶段都可以做一些处理。

完整的代码:

   drawMark(centerPointList, layer) {
    
    
                if (!centerPointList) {
    
    
                    console.log('无区域中心点数据')
                    return
                }
                const info = {
    
     content: '', width: 150, minHeight: 100 }
                const result = []
                // 这里 d 的数据格式是数组,如:[-0.113049, 51.498568]
                centerPointList.forEach(d => {
    
    
                    if (!d.info) {
    
    
                        d.info = info
                    }
                    // 设有高度、高亮的mark
                    const mark = new maptalks.Marker(d.center, {
    
    
                        symbol: {
    
    
                            markerType: 'square',
                            markerFile: logo,
                            markerWidth: 10,
                            markerHeight: 0,
                            markerDx: 0,
                            markerDy: 0,
                            markerOpacity: 1
                        },
                        properties: {
    
    
                            // 高度设置
                            altitude: 0
                        }
                    }).addTo(layer)
                    mark.setInfoWindow({
    
    
                        title: d.name,
                        content: '<div>' + d.adcode + '</div>',
                        // autoPan: true,
                        width: d.info.width,
                        minHeight: d.info.minHeight,
                    })

                    mark.animate({
    
    
                        symbol: {
    
    
                             markerWidth: 40,
            				 markerHeight: 40
                        },
                        properties: {
    
    
                            altitude: 800
                        }
                    }, {
    
    
                        duration: 150,
                    }, function (frame) {
    
    
                        if (frame.state.playState === 'finished') {
    
    
                            console.log('animation finished');
                        }
                    });

                    mark.setZIndex(1000)
                    result.push(mark)
                })
                return result
            },

可以看到,在一开始创建mark对象时,设置的宽度width只有10,这就是动画的开始宽度,之后在animate方法里设置的属性就是需要变化的属性,也是最终的属性,通过duration控制动画的执行的时间.

工具

这里基本都是symbol,那么这里还是要再提一次,就是symbol,涉及这个属性的,我们都可以去查它的文档,因为它是一个系统,都是统一的完整的;

下面的相关属性注释第一个比较全,后面的差不多,所以不会都标注

测距工具

API: Class: DistanceTool (maptalks.org)

距离测算,复制官方案例,进行稍微修改,增加结束事件,

    /**
       * 测距工具
       * @returns {*}
       */
    addDistanceTool () {
    
    
      return new maptalks.DistanceTool({
    
    
        symbol: {
    
    
          lineColor: '#34495e',
          lineWidth: 2
        },
        // 请看 symbol属性说明文档:https://github.com/maptalks/maptalks.js/wiki/Symbol-Reference
        // 绘制的线的样式
        vertexSymbol: {
    
    
          // 绘制的marker类型,也就是简单形状,支持:ellipse cross x diamond bar square triangle pin pie
          markerType: 'square',
          // 绘制的marker的填充色
          markerFill: '#1bbc9b',
          // 绘制的线的颜色
          markerLineColor: '#000',
          // 绘制的线的宽度
          markerLineWidth: 1,
          // 绘制的marker大小
          markerWidth: 10,
          markerHeight: 10
        },
		// 文本标签属性
        labelOptions: {
    
    
          textSymbol: {
    
    
            // 文本呈现的样式,默认是: monospace, 当你设置 textFont时会覆盖这个属性
            textFaceName: 'monospace',
            // 文本填充色(字体颜色)
            textFill: '#fff',
            // 行距
            textLineSpacing: 1,
            // 对齐方式
            textHorizontalAlignment: 'right',
            // 文本标签与marker的距离,也就是与打点的位置的距离
            textDx: 20,
            // 标签的线的颜色
            markerLineColor: '#b4b3b3',
            // 标签的填充色
            markerFill: '#000'
          },
          boxStyle: {
    
    
            // 标签的padding, 第一个值是左右的padding,第二个是上下的padding
            padding: [6, 5],
            symbol: {
    
    
              // 绘制的marker类型,也就是简单形状,支持:ellipse cross x diamond bar square triangle pin pie
              markerType: 'square',
              markerFill: '#000',
              markerFillOpacity: 0.9,
              markerLineColor: '#b4b3b3'
            }
          }
        },
        // 清楚按钮的symbol
        clearButtonSymbol: [{
    
    
          markerType: 'square',
          markerFill: '#000',
          markerLineColor: '#b4b3b3',
          markerLineWidth: 2,
          markerWidth: 15,
          markerHeight: 15,
          markerDx: 20
        }, {
    
    
          markerType: 'x',
          markerWidth: 10,
          markerHeight: 10,
          markerLineColor: '#fff',
          markerDx: 20
        }],
        language: 'zh-CN',
        once: true
      }).addTo(this.mapEngine)
        .on('drawend', p => {
    
    
          console.log('地图坐标:' + p.coordinate)
          console.log('界面坐标:' + p.containerPoint)
          console.log('测量距离:' + p.drawTool.getLastMeasure() + '米')
        })
    },

增加工具按钮:

这里增加了【测距】按钮,在点击时激活测绘过年过节,再次点击时,关闭测绘

    /**
       * 创建工具栏
       */
    addToolbar () {
    
    
      const _t = this
      const map = this.mapEngine
      new maptalks.control.Toolbar({
    
    
        items: [
          {
    
    
            item: '测距',
            click: () => {
    
    
              if (_t.distanceTool.isEnabled()) {
    
    
                _t.distanceTool.disable()
              } else {
    
    
                _t.distanceTool.enable()
              }
            }
          },
          {
    
    
            item: '放大',
            click: () => {
    
    
              map.setZoom(_t.zoom += 1)
            }
          },
          {
    
    
            item: '缩小',
            click: () => {
    
    
              map.setZoom(_t.zoom -= 1)
            }
          },
          {
    
    
            item: '旋转',
            click: () => {
    
    
              map.setBearing(_t.bearing -= 50)
            }
          },
          {
    
    
            item: '重置',
            click: () => {
    
    
              _t.mapDataReset(map)
            }
          },
          {
    
    
            item: '锁定',
            click: (t) => {
    
    
              if (t.target.item === '锁定') {
    
    
                map.setOptions({
    
    
                  // 可拖动
                  draggable: false,
                  // 平移
                  dragPan: false,
                  // 旋转
                  dragRotate: false,
                  // 间距
                  dragPitch: false,
                  // 滚动缩放
                  scrollWheelZoom: false,
                  // 点击 缩放
                  touchZoom: false,
                  // 双击缩放
                  doubleClickZoom: false
                })
                t.target.item = '取消锁定'
              } else {
    
    
                map.setOptions({
    
    
                  // 可拖动
                  draggable: true,
                  // 平移
                  dragPan: true,
                  // 旋转
                  dragRotate: true,
                  // 间距
                  dragPitch: true,
                  // 滚动缩放
                  scrollWheelZoom: true,
                  // 点击 缩放
                  touchZoom: true,
                  // 双击缩放
                  doubleClickZoom: true
                })
                t.target.item = '锁定'
              }
            }
          }
        ]
      }).addTo(map)
    },

效果:

image-20220108163237187

上面这个功能是,在点击【测距】后开启测距模式,但也有的情况下是,点击一次按钮,测一次,这个在这里也是可以实现的,maptalks提供了这样的api,只需要在options里增加“onece”属性,并为true就行了。

    /**
     * 测距工具
     * @returns {*}
     */
    addDistanceTool () {
    
    
      return new maptalks.DistanceTool({
    
    
   // ... 省略
        language: 'zh-CN',
          // 只测绘一次
        once: true
      }).addTo(this.mapEngine)
        .on('drawend', p => {
    
    
          console.log('地图坐标:' + p.coordinate)
          console.log('界面坐标:' + p.containerPoint)
          console.log('测量距离:' + p.drawTool.getLastMeasure() + '米')
        })
    }

如上配置的话,在addToolbar这个方法里,我们也不需要修改,因为,这个onece的属性,在一次测绘后,也是禁用的,所以,我们addToolbar里的方法还是可以延用。

绘制工具

同样,可以直接把官方demo拿过来改改。


/**
       * 绘制工具
       */
    addDrawTool (layer) {
    
    
      const drawTool = new maptalks.DrawTool({
    
    
        mode: 'Point'
      }).addTo(this.mapEngine)
      // 默认是禁用的,当点击按钮后才能使用
        .disable()

      drawTool.on('drawend', function (param) {
    
    
        layer.addGeometry(param.geometry)
      })

      this.drawTool = drawTool
      return drawTool
    }

   /**
     *增加绘制工具栏
     */
    addDrawToolbar (layer) {
    
    
      const _t = this

      const items = [{
    
    code: 'Point', name: '点'},
        {
    
    code: 'LineString', name: '线'},
        {
    
    code: 'Polygon', name: '几何面'},
        {
    
    code: 'Circle', name: '圆'},
        {
    
    code: 'Ellipse', name: '椭圆'},
        {
    
    code: 'Rectangle', name: '矩形'},
        {
    
    code: 'FreeHandLineString', name: '自由绘制'},
        {
    
    code: 'FreeHandPolygon', name: '任意几何面'}]
        .map(function (value) {
    
    
          return {
    
    
            item: value.name,
            click: function () {
    
    
              _t.drawTool.setMode(value.code).enable()
            }
          }
        })
      new maptalks.control.Toolbar({
    
    
        position: {
    
    
          top: 100,
          right: 50
        },
        items: [
          {
    
    
            item: '绘制工具',
            children: items
          },
          {
    
    
            item: '禁用',
            click: function () {
    
    
              _t.drawTool.disable()
            }
          },
          {
    
    
            item: '清除',
            click: function () {
    
    
              layer.clear()
            }
          }
        ]
      }).addTo(this.mapEngine)
    },

效果:

image-20220108210804183

测面工具

另一个工具也是一个比较有面子的工具:

  /**
     * 测面工具
     */
    addAreaTool () {
    
    
      const areaTool = new maptalks.AreaTool({
    
    
        once: true,
        // 请看 symbol属性说明文档:https://github.com/maptalks/maptalks.js/wiki/Symbol-Reference
        symbol: {
    
    
          lineColor: '#1bbc9b',
          lineWidth: 2,
          polygonFill: '#fff',
          polygonOpacity: 0.3
        },
        vertexSymbol: {
    
    
          // 绘制的marker类型,也就是简单形状,支持:ellipse cross x diamond bar square triangle pin pie
          markerType: 'ellipse',
          // 绘制的marker的填充色
          markerFill: '#34495e',
          // 绘制的线的颜色
          markerLineColor: '#1bbc9b',
          // 绘制的线的宽度
          markerLineWidth: 3,
          // 绘制的marker大小
          markerWidth: 10,
          markerHeight: 10
        },
        // 文本标签属性
        labelOptions: {
    
    
          textSymbol: {
    
    
            // 文本呈现的样式,默认是: monospace, 当你设置 textFont时会覆盖这个属性
            textFaceName: 'monospace',
            // 文本填充色(字体颜色)
            textFill: '#fff',
            // 行距
            textLineSpacing: 1,
            // 对齐方式
            textHorizontalAlignment: 'right',
            // 文本标签与marker的距离,也就是与打点的位置的距离
            textDx: 15
          },
          boxStyle: {
    
    
            // 标签的padding, 第一个值是左右的padding,第二个是上下的padding
            padding: [6, 2],
            symbol: {
    
    
              markerType: 'square',
              markerFill: '#000',
              markerFillOpacity: 0.9,
              markerLineColor: '#b4b3b3'
            }
          }
        },
        clearButtonSymbol: [{
    
    
          markerType: 'square',
          markerFill: '#000',
          markerLineColor: '#b4b3b3',
          markerLineWidth: 2,
          markerWidth: 15,
          markerHeight: 15,
          markerDx: 22
        }, {
    
    
          markerType: 'x',
          markerWidth: 10,
          markerHeight: 10,
          markerLineColor: '#fff',
          markerDx: 22
        }],
        language: 'zh-CN'
      }).addTo(this.mapEngine)
      // 默认关闭
      areaTool.disable()
      this.areaTool = areaTool
      return areaTool
    }

绘制工具栏里,添加这个菜单;

 {
    
    
            item: '测面工具',
            click: function () {
    
    
              if (_t.areaTool.isEnabled()) {
    
    
                _t.areaTool.disable()
              } else {
    
    
                _t.areaTool.enable()
              }
            }
          },

在绘制工具栏里也添加一个菜单,然后就是下面的效果:

image-20220108212851554

地图动画

如果,你的页面一打开,镜头由上到下,慢慢的,360度旋转后定位到指定为,然后图标跳出来,这样的一个效果,一定是能够俘获大部分的心的。

在maptalks里也提供了api,我们也只是调用一下:

  /**
     * 地图动画
     */
    mapAnimate () {
    
    
      const _t = this
      _t.mapEngine.animateTo({
    
    
        center: [118.13245430046891, 24.495713873147764],
        zoom: 13,
        pitch: 0,
        bearing: 20
      }, {
    
    
        duration: 5000
      })
      setTimeout(function () {
    
    
        _t.mapEngine.animateTo({
    
    
          center: [118.087828, 24.462059],
          zoom: 14,
          pitch: 65,
          bearing: 360
        }, {
    
    
          duration: 7000
        })
      }, 5000)
    }

这个我们软件,截不了 动图,这个可也代码跑一下就行;

而一般会带上marker,marker也是动画的,两个合起来一看就是比较完美的。

所以,我这里把上面做的学校和医院的功能,拿下来,组合一下:

 /**
     * 地图动画
     */
    mapAnimate () {
    
    
      const _t = this
      _t.mapEngine.animateTo({
    
    
        center: [118.13245430046891, 24.495713873147764],
        zoom: 13,
        pitch: 0,
        bearing: 20
      }, {
    
    
        duration: 5000
      })
      setTimeout(function () {
    
    
        _t.mapEngine.animateTo({
    
    
          center: [118.087828, 24.462059],
          zoom: 14,
          pitch: 65,
          bearing: 360
        }, {
    
    
          duration: 7000
        })
      }, 5000)
        // 在地图动画块结束时,撒点动画加入,并设置默认值
      setTimeout(function () {
    
    
        _t.checkList = []
        _t.checkList.push('学校')
        _t.handleCheckChange(_t.checkList)
      }, 9000)
    }

我再增加了一个播放按钮,可以重复播放,恩。没有软件,没有gif.

聚合

聚合这个功能也不得不说,所有的地图设计到缩放,都会有这个功能。

这个单靠maptalks是有点难的,但是用maptalks的好处就是,它有很多插件,所有这个聚合功能我们也用插件来实现:

文档: GitHub - maptalks/maptalks.markercluster: A layer of maptalks to cluster markers.

引入插件:

npm install maptalks.markercluster

使用如下;

import * as maptalks from 'maptalks'
import {
    
    ClusterLayer} from 'maptalks.markercluster'

我们将之前写的marker页面修改下:

这次我们创建的图层是markercluster,会有些不一样,我们只将图层创建方式变更一下,相应注释我也添加了,再看代码会比较清晰些。

这里的symbol,我没有写全,一是直接复制官网的,二是懒了,它和其他的symbol是一样的,都可以复制的。

// 先创建图层
 // 创建学校和医院的mark图层
      // new maptalks.VectorLayer('schoolMark').addTo(_t.mapEngine)
      // new maptalks.VectorLayer('hospitalMark').addTo(_t.mapEngine)
      const symbol = {
    
    
        // 聚合最大半径
        maxClusterRadius: 160,
        // 聚合的最大缩放比例,也就是当缩放到某一个层级时,进行聚合
        maxClusterZoom: 19,
        // 是否开启动画,默认true(开启)
        animation: true,
        // 动画时长
        animationDuration: 30,
        // textSumProperty: '',
        // 这里的属性,可以看官网,都是统一的
        // symbol: {},
        textSymbol: {
    
    
          // 文本呈现的样式,默认是: monospace, 当你设置 textFont时会覆盖这个属性
          textFaceName: 'monospace',
          // 字体大小
          textSize: 16
        }
      }
      new ClusterLayer('schoolMark', symbol).addTo(this.mapEngine)
      new ClusterLayer('hospitalMark', symbol).addTo(this.mapEngine)

addMarker方法修改:

 drawMark (centerPointList, layer) {
    
    
      if (!centerPointList) {
    
    
        console.log('无区域中心点数据')
        return
      }
      const info = {
    
    content: '', width: 150, minHeight: 100}
      const result = []
      // 这里 d 的数据格式是数组,如:[-0.113049, 51.498568]
      centerPointList.forEach(d => {
    
    
        if (!d.info) {
    
    
          d.info = info
        }
        // 设有高度、高亮的mark
        const mark = new maptalks.Marker(d.center, {
    
    
          symbol: {
    
    
            markerType: 'square',
            markerFile: logo,
            markerWidth: 10,
            markerHeight: 0,
            markerDx: 0,
            markerDy: 0,
            markerOpacity: 1
          },
          properties: {
    
    
            // 高度设置
            altitude: 0
          }
        })
        mark.setInfoWindow({
    
    
          title: d.name,
          content: '<div>' + d.adcode + '</div>',
          // autoPan: true,
          width: d.info.width,
          minHeight: d.info.minHeight
        })

        mark.animate({
    
    
          symbol: {
    
    
            markerWidth: 40,
            markerHeight: 40
          },
          properties: {
    
    
            altitude: 800
          }
        }, {
    
    
          duration: 150
        }, function (frame) {
    
    
          if (frame.state.playState === 'finished') {
    
    
            console.log('animation finished')
          }
        })

        mark.setZIndex(1000)
        result.push(mark)
      })
      layer.addMarker(result)
      return result
    },

效果如下:

image-20220110225340346

底图风格

常规的地图设计不好,会使看的人很疲惫,所以可以换一个风格。

这个比较简单,就是一个属性: cssFilter

  /**
       * 初始化地图
       */
    initMap () {
    
    
      const _t = this
      return new maptalks.Map('map', {
    
    
        // 默认中心点点位
        center: _t.center,
        // 缩放层级
        zoom: _t.zoom,
        // 倾斜度
        pitch: _t.pitch,
        // 最小缩放层级
        minZoom: 1,
        // 最大缩放层级
        maxZoom: 18,
        baseLayer: new maptalks.TileLayer('base', {
    
    
          // 电子地图图层
          urlTemplate: _t.urlTemplate,
          subdomains: _t.subdomains,
          attribution: _t.attribution,
          cssFilter: 'sepia(100%) invert(90%)'
        })
      })
    },

它提供了两种:

image-20220110230801619

效果:

image-20220110230946871

热力图

热力图,它是集成了heatmap.js

增加热力图层插件

npm install maptalks.heatmap

添加方式也是它的一个方式,它的数据是一个坐标数组,自己变量添加就行,这里就把做

// 获取热力点
const h1 = []
for (let i = 0; i < simingAreaData.length; i++) {
    
    
    const s = simingAreaData[i]
    s.pointList.forEach(ss => {
    
    
        ss.forEach(sss => {
    
    
            sss.push(i + '100')
            h1.push(sss)
        })
    })
}
// 创建图层
new HeatLayer('heat', h1, {
    
    
    // 热力比例(我的理解是,在0-1之间,也就是0-100%,一个热力点,的热力程度是多少
    heatValueScale: 0.7,
    // 旋转时强制刷新
    forceRenderOnRotating: true,
    // 移动时强制刷新
    forceRenderOnMoving: true,
    // 模糊因子, 值越大,越平滑,默认值是15
    // blur: 20,
    // 每个数据点的半径(默认值25,也建议25,我建议不能小于20)
    // radius: 25,
    // 最小不透明读,越小越透明
    // minOpacity: 0.8,
    // 热力梯度,是热力点外围的颜色值,从外围到里,值是递增的,最大值就是中心的位置
    // gradient: {
    
    
    //   0.4: 'blue',
    //   0.6: 'cyan',
    //   0.7: 'lime',
    //   0.8: 'yellow',
    //   1.0: 'red'
    // }
}).addTo(this.mapEngine)

首先要了解一下热力的呈现方式

  1. 以点为中心向外渐变;
  2. 多个点可以叠加,或多个点在聚集在一起,呈现出面;
  3. 每个点的显示是一样的,都有一个热力梯度,就是从外到内的一个颜色变化(从浅到什);

所以它和上面的几何不一样,几何需要3个点以上,而热力图最少只需要一个:

上面代码也对热力属性进行了标注:

  • heatValueScale: 这个值的作用不是很理解,
  • forceRenderOnRotating:旋转时强制刷新
  • forceRenderOnMoving:移动时强制刷新
  • blur:模糊因子, 值越大,越平滑,默认值是15
  • radius:每个数据点的半径(默认值25,也建议25,我建议不能小于20)
  • minOpacity:最小不透明读,越小越透明
  • gradient:热力梯度,是热力点外围的颜色值,从外围到里,值是递增的,最大值就是中心的位置

上述值都有默认配置,可以直接使用,也可以自定义;

热力图的交互

回应一位朋友提出的问题,因字数限制,就加在文章中

热力图,本身是对点绘制渲染的,而且并没有开放出可交互的接口(我暂时还没找到),所以只能找替代方法
这里提供两种方法:

      // 方法一
      // canvas绘制point,和热力图重合,并添加监听事件,也可以绘制mark,不过它们之间的性能需要分析下
      // 这个方法可以单独对点做弹窗
      // 对应的point文档:https://maptalks.org/maptalks.js/api/1.x/MultiPoint.html#setSymbol
      const h1 = []
      for (let i = 0; i < simingAreaData.simingAreaData.length; i++) {
    
    
        simingAreaData.simingAreaData[i].pointList[0].forEach(d => {
    
    
          h1.push(d)
        })
      }
      var mPoint = new maptalks.MultiPoint(h1, {
    
    
        visible: true,
        // 可交互
        interactive: true,
        cursor: 'pointer',
        // 可拖拽
        draggable: false,
        zIndex: 10,
        symbol: {
    
    
          // 不透明度,这里不设置0是因为,值为0交互会失效
          markerOpacity: 0.1,
          markerFillOpacity: 0.1,
          markerLineOpacity: 0,
          // 填充色,使用热力图相近的颜色
          markerFill: 'red',
          markerType: 'ellipse',
          // 这里保持和热力图(对应heatMap的radiu,而且要大一点,这里我是比热力图的大10)
          markerWidth: 35,
          markerHeight: 35
        }
      })
      mPoint.on('mousemove', function (e) {
    
    
        e.target.openInfoWindow(e.coordinate)
      }).on('mouseout', function (e) {
    
    
        e.target.closeInfoWindow()
      }).setInfoWindow({
    
    
        content: 'hello world',
        title: 'message',
        animationDuration: 0,
        autoOpenOn: false
      })
      new maptalks.VectorLayer('pointLayer', mPoint).addTo(this.mapEngine)


// 方法二
      // 用其他可交互图形替代,使用几何图形,这个要求是对真个热力图都是一样的弹窗,将整个热力图作为一个整体
      // 这里我用的几何图形,有太多限制,不太符合要求,但可以作为一个思路参考,
      const geometry = maptalks.GeoJSON.toGeometry(simingAreaData.geoData)
      if (geometry) {
    
    
        geometry.forEach(g => {
    
    
          g.setSymbol({
    
    
            // 线色
            lineColor: '#f298bc',
            // 线宽
            // 这里保持和热力图(对应heatMap的radiu,而且要大一点)
            lineWidth: 30,
            // 不透明度
            polygonOpacity: 0,
            // 圆角设置
            lineJoin: 'round'
          })
          g.on('mousemove', function (e) {
    
    
            e.target.openInfoWindow(e.coordinate)
          }).on('mouseout', function (e) {
    
    
            e.target.closeInfoWindow()
          }).setInfoWindow({
    
    
            content: 'hello world',
            title: 'message',
            animationDuration: 0,
            autoOpenOn: false
          })
        })
      }
      const options = {
    
    
        enableAltitude: true,
        zIndex: 10
      }
      new maptalks.VectorLayer('lineLayer', options).addGeometry(geometry).addTo(this.mapEngine)

第一种方法效果:
在这里插入图片描述

第二种方法效果演示:
在这里插入图片描述

3D - three.js

除了使用上面的echarts,还有专门做三维的前端开发框架three.js,这个还是比较出门的,这个就比echarts好了很多,而且它集成到地图后,有做绑定,所以,基本上是和地图的操作同步,而且感觉简单许多;

maptalks集成three后 的文档:maptalks.three/API.ZH-CN.md at master · maptalks/maptalks.three · GitHub

npm install maptalks.three

// 这里安装128的版本就行,其他版本会出现报错

npm install [email protected]

创建板块

maptalks集成了three.js,有些转为maptalks定制的用法,但基本上还是和three的一样,只是看maptalks.three 的文档,是不会有什么进步的,有需要制作特别厉害的效果,还是看three的官方文档。

 drawArea() {
    
    
     const _t = this
	 const layer = new ThreeLayer('three')
     // 图层三维设置
     layer.setOptions({
    
    
         // 地图移动、缩放、旋转时强制渲染
         forceRenderOnMoving: true,
         forceRenderOnZooming: true,
         forceRenderOnRotating: true,
         zIndex: 10
     })
	
     layer.prepareToDraw = function(gl, scene) {
    
    
         _t.create3DPlan2(scene, layer)
     }

     _t.mapEngine.addLayer(layer)

 },
     
  /**
             * 创建3D板块
             * @param scene
             * @param layer
             * @returns {ExtrudePolygons}
             */
     create3DPlan(scene, layer) {
    
    
         // 创建直线光
         const light =  new THREE.DirectionalLight(0xffffff)
         light.position.set(0, -10, 10).normalize()
         scene.add(light)
         // 创建环境光
         scene.add(new THREE.AmbientLight(0xffffff, 0.2))

         // 创建材质
         var material = new THREE.MeshLambertMaterial({
    
     color: cc.defaultPolygonFillColor, opacity: 0.7 })

         const list = []
         const polygons = maptalks.GeoJSON.toGeometry(geoJson)
         polygons.forEach(p => {
    
    
             // 该方法是旧版本方法,官方推荐使用toExtrudePolygon
             var mesh = layer.toExtrudeMesh(p, 400, material)

             if (Array.isArray(mesh)) {
    
    
                 scene.add.apply(scene, mesh)
             } else {
    
    
                 scene.add(mesh.object3d)
             }
             // infowindow test
             mesh.setInfoWindow({
    
    
                 content: '<br style="color:#f00">中心点:' + p.properties.center + ' </br>行政区划:' + p.properties.adcode + ' </br>父级行政区划:' + p.properties.parent.adcode + '</div>',
                 title: 'message',
                 animationDuration: 0,
                 autoOpenOn: false
             })
             list.push(mesh)
         })
         // 批量添加到图层
         layer.addMesh(list)
     },
         
    create3DPlan2(scene, layer) {
    
    
        // 创建直线光
        const light =  new THREE.DirectionalLight(0xffffff)
        light.position.set(0, -10, 10).normalize()
        scene.add(light)
        // 创建环境光
        scene.add(new THREE.AmbientLight(0xffffff, 0.2))

        // 创建材质
        var material = new THREE.MeshLambertMaterial({
    
     color: cc.defaultPolygonFillColor, opacity: 0.7 })

        const options = {
    
    
            altitude: 1,
            height: 400,
            bottomHeight: 0,
            topColor: null,
            bottomColor: cc.defaultPolygonFillColor,
            interactive: true
        }

        const list = []
        const polygons = maptalks.GeoJSON.toGeometry(geoJson)
        polygons.forEach(p => {
    
    
            // 这里源码中,可以找到toExtrudePolygons,但是创建的得到的MultiExtrudePolygons无法正常添加到scence中,所以这里还是用它推荐的方法
            var mesh = layer.toExtrudePolygon(p, options, material)
            if (Array.isArray(mesh)) {
    
    
                scene.add.apply(scene, mesh)
            } else {
    
    
                scene.add(mesh)
            }

            mesh.setInfoWindow({
    
    
                title: p.properties.name,
                content: '<br style="color:#f00">中心点:' + p.properties.center + ' </br>行政区划:' + p.properties.adcode + ' </br>父级行政区划:' + p.properties.parent.adcode + '</div>',
                animationDuration: 0,
                autoOpenOn: false
            })
            list.push(mesh)
        })

        // 批量添加到图层
        layer.addMesh(list)
    }

上面写了两种挤压出物的方式:toExtrudeMesh,toExtrudePolygon,其实还有一种,在源码里toExtrudePolygons这个是可以传入一个数组,但是实际使用时,它得到的是一个对象,并且,执行 scene.add(mesh)还会报错,导致得到的结果是一个面,而不是一个三维物体。

效果如下:

image-20211023225451890

现在已经生成了地图块,而且还有点难看,和画区域面一样,给他分块,然后加入鼠标事件。

分块

这个就是在生成的物体上增加分割线,可以使用之前的方法,再增加一个图层,生成一个面,面的透明度设为0,线框,宽度1 ,再加个猛男粉,OK

 drawLine() {
    
    
     const geometry = maptalks.GeoJSON.toGeometry(geoJson)
     if (geometry) {
    
    
         geometry.forEach(g => {
    
    
             g.setSymbol({
    
    
                 // 线色
                 lineColor: '#f298bc',
                 // 线宽
                 lineWidth: 1,
                 // 不透明度
                 polygonOpacity: 0
             })
         })
     }
     new maptalks.VectorLayer("line").addGeometry(geometry).addTo(this.mapEngine)
 }

来看看效果

image-20211024134514871

这是贴在地图上的,有点不好看,我们再设个高程

drawLine() {
    
    
    const geometry = maptalks.GeoJSON.toGeometry(geoJson)
    if (geometry) {
    
    
        geometry.forEach(g => {
    
    
            g.setSymbol({
    
    
                // 线色
                lineColor: '#f298bc',
                // 线宽
                lineWidth: 1,
                // 不透明度
                polygonOpacity: 0
            })
            // 添加高程
            g.setProperties({
    
    
                // 高度设置(对应挤压的高度height)
                altitude: 400
            })
        })
    }
    const options = {
    
    
        enableAltitude: true,
        zIndex: 9
    }
    new maptalks.VectorLayer("line", options).addGeometry(geometry).addTo(this.mapEngine)
}

效果:

image-20211024135517544

事件

three创建的对象文档:maptalks.three/API.ZH-CN.md at master · maptalks/maptalks.three · GitHub

可以看出适配还是挺不错的。

设置方式和区域面的一样,不过存在一个问题,先看代码,也是在之前的方法中新增了on监听

create3DPlan2(scene, layer) {
    
    
                // 创建直线光
                const light =  new THREE.DirectionalLight(0xffffff)
                light.position.set(0, -10, 10).normalize()
                scene.add(light)
                // 创建环境光
                scene.add(new THREE.AmbientLight(0xffffff, 0.2))

                // 创建材质
                var material = new THREE.MeshLambertMaterial({
    
     color: cc.defaultPolygonFillColor, opacity: 0.7 })

                const options = {
    
    
                    altitude: 1,
                    height: 400,
                    bottomHeight: 0,
                    topColor: null,
                    bottomColor: cc.defaultPolygonFillColor,
                    interactive: true
                }

// 定义高亮材质
                const highlightmaterial = new THREE.MeshBasicMaterial({
    
     color: '#ff87a4', transparent: true, opacity: 0.8 })

                const list = []
                const polygons = maptalks.GeoJSON.toGeometry(geoJson)
                polygons.forEach(p => {
    
    
                    // 这里源码中,可以找到toExtrudePolygons,但是创建的得到的MultiExtrudePolygons无法正常添加到scence中,所以这里还是用它推荐的方法
                    var mesh = layer.toExtrudePolygon(p, options, material)
                    if (Array.isArray(mesh)) {
    
    
                        scene.add.apply(scene, mesh)
                    } else {
    
    
                        scene.add(mesh)
                    }

                    mesh.setInfoWindow({
    
    
                        title: p.properties.name,
                        content: '<br style="color:#f00">中心点:' + p.properties.center + ' </br>行政区划:' + p.properties.adcode + ' </br>父级行政区划:' + p.properties.parent.adcode + '</div>',
                        animationDuration: 0,
                        autoOpenOn: false
                    })
                    
                    // 添加鼠标事件
                    mesh.on('mouseover', function(e) {
    
    
                        e.target.setSymbol(highlightmaterial)
                        layer.redraw()
                      }).on('mouseout', function(e) {
    
    
                        e.target.setSymbol(material)
                        layer.redraw()
                    })
                    list.push(mesh)
                })
                // 批量添加到图层
                layer.addMesh(list)
            },

可以看到,我在事件中都加了layer.redraw(),为什么呢?

官方文档及示例中并没有提及要这么做,我一直以为它和矢量图形一样,会自动渲染,但并没有,而且,总是莫名其妙的就渲染了,后来发现,每当我移动地图、或旋转时,它的图形才会变化,这让我想起了,矢量图层有一个设置在移动、旋转时强制渲染,和现在的情况非常相似,然后再次看它的文档,并没有提及渲染的方法,但让人高兴的是,three图层是继承于CanvasLayer,拥有它的所有方法,到这,问题就解决了,只要调用redraw()重新绘制three图层就可以了。

有一点不同的是在maptalks.three里,setSymbol参数是传的材质对象

来看效果图:

image-20211024141815638

不借助其他插件绘制3D

在不使用three.js 和 echarts的情况下,也可以通过它自身的面去搭建一个3D结构,用过3维模型的可能比较清晰一点,在三维软件中,面可以被挤压,这样就可以得到一个多面体,这里的three就是这个道理,最传统的就是在三维空间里,将面组合,在maptalks里也可以实现。

将之前的代码稍微修改下:

   /**
             * 再图层上画面
             */
            drawArea() {
    
    
                const options = this.getOptions()

                const lowSymbol = {
    
    }
                const topSymbol = {
    
    }
                Object.assign(lowSymbol, options.symbol)
                Object.assign(topSymbol, options.symbol)

                const lowOptions = {
    
    
                    zIndex: 9,
                    symbol: lowSymbol,
                    properties: {
    
    
                        altitude: 0
                    }
                }

                const topOptions = {
    
    
                    zIndex: 11,
                    symbol: topSymbol,
                    properties: {
    
    
                        altitude: 400
                    }
                }

                const _t = this

                const layer = new maptalks.VectorLayer('vector')
                // 创建底面
                _t.drawAreaPlan(_t.geoJson, layer, lowOptions)
                // 高面
                const centerCoordinate = _t.drawAreaPlan(_t.geoJson, layer, topOptions, true)

                const lineOptions = {
    
    
                    zIndex: 10,
                    symbol: options.symbol,
                    properties: {
    
    
                        altitude: 400
                    }
                }
                const lineCoordinate = []
                if (geoJson.type === 'FeatureCollection') {
    
    
                    geoJson.features.forEach(c => {
    
    
                        lineCoordinate.push(c.geometry.coordinates[0])
                    })
                }
                lineCoordinate.forEach(l => {
    
    
                  const line = new maptalks.LineString(l[0], lineOptions).addTo(layer)
                })

                _t.drawMark(centerCoordinate, layer)
                layer.setOptions({
    
    
                    // 启用高度绘制
                    enableAltitude: true,
                    altitudeProperty: 'altitude',
                    // 绘制高度线
                    drawAltitude: {
    
    
                        // lineWidth: 1,
                        // lineColor: '#000',
                        // 高度面的填充色
                        polygonFill: options.symbol.polygonFill,
                        // 高度面的透明度
                        polygonOpacity: options.symbol.polygonOpacity
                    }
                })
                layer.addTo(_t.mapEngine)
            },

            /**
             * 根据geojson画区域面
             * @param geoJson geoJson数据
             * @param layer 需要话的图层
             */
            drawAreaPlan(geoJson, layer, options, isActions) {
    
    
                const geometry = maptalks.GeoJSON.toGeometry(geoJson)

                const centerCoordinates = []

                if (geometry) {
    
    
                    geometry.forEach(g => {
    
    
                        g.setSymbol(options.symbol)
                        const gProperties = g.properties
                        // 避免信息覆盖
                        gProperties.altitude = options.properties.altitude
                        g.setProperties(gProperties)
                        // 设置图形层级
                        g.setZIndex(options.zIndex)
                        // 设置信息框
                        g.setInfoWindow({
    
    
                            title: g.properties.name,
                            content: '<br style="color:#f00">中心点:' + g.properties.center + ' </br>行政区划:' + g.properties.adcode + ' </br>父级行政区划:' + g.properties.parent.adcode + '</div>'
                        })
                        g.setMenu({
    
    
                            width: 160,
                            custom: false,
                            items: [
                                {
    
     item: '菜单一', click: function() {
    
     alert('Query Clicked!'); return false } },
                                '-',
                                {
    
     item: '菜单二', click: function() {
    
     alert('Edit Clicked!') } },
                                {
    
     item: '菜单三', click: function() {
    
     alert('About Clicked!') } }
                            ]
                        })
                        // 鼠标交互事件监听
                        g.on('mouseenter', function(e) {
    
    
                            e.target.updateSymbol({
    
    
                                polygonFill: '#ffaeb0'
                            })
                        }).on('mouseout', function(e) {
    
    
                            e.target.updateSymbol({
    
    
                                polygonFill: 'rgb(135,196,240)'
                            })
                        })

                        if (isActions) {
    
    
                            g.on('click', function(e) {
    
    
                                e.target.openInfoWindow(e.coordinate)
                            });
                        }

                        // 获取中心坐标
                        centerCoordinates.push(g.properties)
                    })
                }
                layer.addGeometry(geometry)

               return centerCoordinates
            },
// mark
            drawMark(centerPointList, layer) {
    
    
                if (!centerPointList) {
    
    
                    console.log('无区域中心点数据')
                    return
                }
                const info = {
    
     content: '', width: 150, minHeight: 100 }
                const result = []
                // 这里 d 的数据格式是数组,如:[-0.113049, 51.498568]
                centerPointList.forEach(d => {
    
    
                    if (!d.info) {
    
    
                        d.info = info
                    }
                    // 设有高度、高亮的mark
                    const mark = new maptalks.Marker(d.center, {
    
    
                        // 设置了这个属性,会替换默认的图标
                        // symbol: {
    
    
                        // markerFile: 'foo.png',
                        // textName: d.name
                        // },
                        properties: {
    
    
                            // 高度设置
                            altitude: 400
                        }
                    }).addTo(layer)
                    mark.setInfoWindow({
    
    
                        title: d.name,
                        content: '<div>' + d.adcode + '</div>',
                        // autoPan: true,
                        width: d.info.width,
                        minHeight: d.info.minHeight,
                        // 'custom': false,
                        // 点击打开和关闭
                        // autoOpenOn: 'click',
                        // autoCloseOn: 'click'
                    })

                    // 没有高度的mark
                    // new maptalks.Marker(d).updateSymbol({
    
    
                    //   markerOpacity: 0.5,
                    //   markerFill: '#bbb'
                    // }).addTo(layer)

                    mark.setZIndex(1000)
                    result.push(mark)
                })
                return result
            },

            getOptions() {
    
    
                return {
    
    
                    zIndex: 10,
                    symbol: {
    
    
                        // 线色
                        lineColor: '#ff87a4',
                        // 线宽
                        lineWidth: 1,
                        // 填充色
                        polygonFill: 'rgb(135,196,240)',
                        // 不透明度
                        polygonOpacity: 0.9,
                    },
                    properties: {
    
    
                        // 高度设置(不生效,需要设置图层的高度polygonOpacity属性)
                        altitude: 400
                    }
                }
            }

效果如下:

这里我只是提供思路,直白的说这种方法的实现效果没有three的好

image-20211030152616191.png

猜你喜欢

转载自blog.csdn.net/qq_28911061/article/details/122515860