美文网首页开源Vue.js专区GIS相关
用 Mapbox 和 Vue.js 制作一个地图可视化船舶定位查

用 Mapbox 和 Vue.js 制作一个地图可视化船舶定位查

作者: IllIIlIlIII | 来源:发表于2019-06-14 19:02 被阅读26次

    主要功能有:

    • 通过搜索框查询船舶关键字
    • 查询到船舶后,自动定位到船舶位置
    • 弹出船舶相关信息
    • DEMO船只搜索代号为 test-123

    • 默认已经使用vue-cli脚手架安装vue.js,并安装了elementUI,Less
    • 本Demo使用了elementUI,为了让示例代码DOM结构清晰,删除部分HTML标签

    1、目录结构
    2、安装Mapbox
    3、Map.vue组件
    4、MapPage.vue页面


    [1]

    1、目录结构

    src\
      |- components\ // 公用组件目录
          |- Map.vue
      |- views\ // 页面组件目录
          // MapPage.vue为显示地图的页面,包含一个Map.vue 组件
          // 其实可以把Map组件的代码也写在MapPage里面,逻辑上还省事情一些,但是代码行数太长了,我不习惯一个组件超过300行代码
          |- MapPage.vue
      |- App.vue
      |- main.js
    
    逻辑结构

    [2]

    2、安装Mapbox

    在项目目录下执行:

    npm install mapbox-gl --save
    

    [3]

    3、Map.vue组件

    • 需要用一个箭头体现出船舶的 位置航向/船头指向
    • 最开始我的解决方法是用一张箭头图片,然后通过 css3 的 transform:rotate(x角度deg) 对图片进行角度转动,但是遇到一个问题,当地图进行旋转和推拉(伪3D效果,官方叫bearing 和 pitch),箭头图片并不会随地图转动。
    • 所以只有将箭头通过添加图层(layer)的方式画在地图就,需要求出箭头各个点的坐标:

    • 当我天真的以为真没提供相关 API,结果还真找到了.....
    • 所以 Map.vue 组件的 addGeoMarker() 方法中加箭头的部分要重新写了,直接用图片,没这么麻烦了(新代码见这里
    • 以前写的老方思路留着把,说不定在其它什么地方能用上

    画个箭头
    • P0为船舶所在位置,作为原点(X0,Y0),α为船舶航向
    • P1(X1,Y1),P2(X2,Y2),P3(X3,Y3),β为135度,γ为225度
    • 以0.05个经纬度作为画箭头的单位长度L,P0P1 = 3 * L,P0P2 = √2 * L
    • X1 = X0 + 3 * L * sinα
    • Y1 = Y0 + 3 * L * cosα
    • X2 = X0 + √2 * L * sin(α + 135)
    • Y1 = Y0 + √2 * L * cos(α + 135)
    • X3 = X0 + √2 * L * sin(α + 225)
    • Y3 = Y0 + √2 * L * cos(α + 225)
    <template>
      <div class="map height-full">
        <!-- Mapbox 绑定的 div -->
        <div ref="Mapbox" style="height:100%;width:100%;"></div>
        <div id="ship-info" :class="{'active': shipInfoBoardDisplay}">
            <!-- 显示船舶详细信息的弹出框,显示隐藏通过判断 shipInfoBoardDisplay -->
        </div>
      </div>
    </template>
    
    <script>
    import mapboxgl from 'mapbox-gl'
    
    export default {
      name: 'Map',
      // 接收父组件传递的船舶信息,父组件从后端 API 取得
      props: ['shipInfo'],
      data () {
        return {
          // 保存 mapboxgl 对象
          mapObject: {},
          shipInfoBoardDisplay: false
        }
      },
      mounted () {
        this.mapObject = this.init()
      },
      methods: {
        // 初始化地图
        init () {
          mapboxgl.accessToken = 'Mapbox官网申请的Token'
          const map = new mapboxgl.Map({
            container: this.$refs.Mapbox,
            style: 'mapbox://styles/mapbox/streets-v11',
            // 设置地图中心
            center: [114.1, 22.2],
            // 设置地图比例
            zoom: 8
          })
    
          // 地图导航
          let nav = new mapboxgl.NavigationControl()
          map.addControl(nav, 'top-left')
    
          // 显示比例尺
          let scale = new mapboxgl.ScaleControl({
            maxWidth: 80,
            unit: 'imperial'
          })
          map.addControl(scale)
          scale.setUnit('metric')
          // 全屏按钮
          map.addControl(new mapboxgl.FullscreenControl())
    
          // 使本地用定位模块
          map.addControl(
            new mapboxgl.GeolocateControl({
              positionOptions: {
                enableHighAccuracy: true
              },
              trackUserLocation: true,
              showUserLocation: true,
              zoom: 14
            })
          )
          return map
        },
    
        // 跳转到坐标
        flyTo () {
          let map = this.mapObject
          map.flyTo({
            center: this.shipInfo.local.value,
            zoom: 7.5,
            speed: 0.5,
            curve: 1
          })
        },
    
        // 添加自定义标记点
        addGeoMarker () {
          let map = this.mapObject
          // 画箭头的单位长度(经纬度偏移)
          let lengthUnit = 0.05
          // 与y轴夹角
          let rotateAngle = this.shipInfo.headDirect.value
          // 箭头的四个点
          let point0 = this.shipInfo.local.value
          let point1 = new Array(2)
          point1[0] = point0[0] + 3 * lengthUnit * Math.sin(rotateAngle * 2 * Math.PI / 360)
          point1[1] = point0[1] + 3 * lengthUnit * Math.cos(rotateAngle * 2 * Math.PI / 360)
          let point2 = new Array(2)
          point2[0] = point0[0] + Math.sqrt(2) * lengthUnit * Math.sin((rotateAngle + 135) * 2 * Math.PI / 360)
          point2[1] = point0[1] + Math.sqrt(2) * lengthUnit * Math.cos((rotateAngle + 135) * 2 * Math.PI / 360)
          let point3 = new Array(2)
          point3[0] = point0[0] + Math.sqrt(2) * lengthUnit * Math.sin((rotateAngle + 225) * 2 * Math.PI / 360)
          point3[1] = point0[1] + Math.sqrt(2) * lengthUnit * Math.cos((rotateAngle + 225) * 2 * Math.PI / 360)
          // 绘制箭头图像区域
          map.addLayer({
            // 将船舶的 callsign 作为 layer 的 ID
            id: this.shipInfo.callsign.value,
            type: 'fill',
            source: {
              type: 'geojson',
              data: {
                type: 'Feature',
                geometry: {
                  type: 'Polygon',
                  coordinates: [[point1, point2, point0, point3]]
                }
              }
            },
            'layout': {},
            'paint': {
              'fill-color': '#409eff',
              'fill-opacity': 1
            }
          })
          // 添加 icon 和 名称 标记
          // 创建 div.marker-wrap, div.marker-title, div.marker-wrap 用作定位, div.marker-title 显示标题
          let elWrap = document.createElement('div')
          let that = this
          elWrap.className = 'marker-wrap'
          elWrap.innerHTML = '<i class="el-icon-ship"></i>'
          elWrap.addEventListener('click', function () {
            that.shipInfoBoardDisplay = !that.shipInfoBoardDisplay
            if (map.getZoom() < 6.5) {
              that.flyTo()
            }
          })
          let elTitle = document.createElement('div')
          elTitle.className = 'marker-title'
          elTitle.innerHTML = '<span>' + that.shipInfo.name.value + '</span>'
          elWrap.appendChild(elTitle)
          // 将 div.marker-wrap 加入到 map
          let markerTagObject = new mapboxgl.Marker(elWrap).setLngLat(this.shipInfo.local.value).addTo(map)
          // 默认添加标记点后显示信息面板
          this.shipInfoBoardDisplay = true
          // 返回 layer 的 id 和自定义的 图层对象
          return { id: this.shipInfo.callsign.value, tagObject: markerTagObject }
        },
        // 移除自定义标记点, 接收对象参数 {id: id, tagObject: markerTagObject}
        removeGeoMarker (markerObject) {
          let map = this.mapObject
          map.removeLayer(markerObject.id)
          map.removeSource(markerObject.id)
          markerObject.tagObject.remove()
          this.shipInfoBoardDisplay = false
        },
        // 关闭信息面板
        shipInfoBoardClose () {
          this.shipInfoBoardDisplay = false
        }
      }
    }
    </script>
    
    <style scoped lang='less'>
    #ship-info {
      display: none;
    }
    #ship-info.active {
      display: block;
    }
    </style>
    
    

    [4]

    4、MapPage.vue页面

    <template>
      <div id="map-page" class="height-full">
         <!-- 搜索框 -->
        <el-row class="search-bar-wrap">
                <el-input v-model="searchForm.shipCode" placeholder="船号, 呼号, MMSI 或 IMO" size="small">
                  <el-button slot="append" @click.prevent="searchSubmit" icon="el-icon-search" type="primary" size="small"></el-button>
                </el-input>
        </el-row>
        <!-- Map组件 -->
        <Map ref="map" :shipInfo="shipInfo" />
      </div>
    </template>
    
    <script>
    import Map from '@/components/Map'
    
    export default {
      name: 'MapPage',
      data () {
        return {
          // 存储返回的标记对象,主要用作判断指定船舶标记是否存在和删除指定船舶标记
          markerObject: {},
          searchForm: {
            shipCode: ''
          },
          // 由于没有从后端拿数据,这里就直接写在逻辑中,传递给Map子组件
          shipInfo: {
            name: {label: '船名', value: '泰坦尼克号'},
            mmsi: {label: 'MMSI', value: 'test-123'},
            callsign: {label: '呼号', value: 'WED2234'}
            // ...
          }
        }
      },
      components: {
        'Map': Map
      },
      methods: {
        searchSubmit () {
          // 由于没有后端,这里直接在逻辑中写死一个船舶的识别码
         if (this.searchForm.shipCode === 'test-123') {
            this.markerAddRemoveToggle()
          } else if (this.searchForm.shipCode === '') {
            this.$message({
              showClose: true,
              message: '请输入船号, 呼号, MMSI 或 IMO'
            })
          } else {
            this.$message({
              showClose: true,
              message: '未找到与 "' + this.searchForm.shipCode + '" 有关的船只信息'
            })
          }
        },
        // 判断 markerObject 是否为空,对 markerlayer 进行增删
        markerAddRemoveToggle () {
          // 将 markerObject 转换成数组,如果数组 length 为 0 则判断 markerObject 是空对象
          let objectArr = Object.keys(this.markerObject)
          if (objectArr.length === 0) {
            // 通过this.$refs.map 触发子组件(<Map ref="map" />)函数
            // 将返回的标记对象赋值给 markerObject
            this.markerObject = this.$refs.map.addGeoMarker()
            this.$refs.map.flyTo()
          } else {
            // 如果 markerObject.id 等于 shipInfo.callsign.value 表示当前已经生成了其 callsign 作为 id 的layer,则不删除直接 flyTo 到其 local
            if (this.markerObject.id === this.shipInfo.callsign.value) {
              this.$refs.map.flyTo()
            } else {
              this.$refs.map.removeGeoMarker(this.markerObject)
              this.markerObject = {}
            }
          }
        }
      }
    }
    </script>
    
    <style lang='less'>
    @import url('https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css');
    /* 
    * 覆盖mapbox的样式,注意 style 没有 scoped
    */
    .mapboxgl-ctrl-top-left,
    .mapboxgl-ctrl-top-right {
      margin-top: 50px;
    }
    </style>
    
    

    1. 目录结构

    2. 安装Mapbox

    3. Map.vue组件

    4. MapPage.vue页面

    相关文章

      网友评论

        本文标题:用 Mapbox 和 Vue.js 制作一个地图可视化船舶定位查

        本文链接:https://www.haomeiwen.com/subject/gpfmfctx.html