用 Mapbox 和 Vue.js 制作一個(gè)地圖可視化船舶定位查詢系統(tǒng)

主要功能有:

  • 通過(guò)搜索框查詢船舶關(guān)鍵字
  • 查詢到船舶后东羹,自動(dòng)定位到船舶位置
  • 彈出船舶相關(guān)信息
  • DEMO:船只搜索代號(hào)為 9445320(已經(jīng)接了真實(shí)全球數(shù)據(jù))

  • 默認(rèn)已經(jīng)使用vue-cli腳手架安裝vue.js鞠呈,并安裝了elementUI,Less
  • 本Demo使用了elementUI,為了讓示例代碼DOM結(jié)構(gòu)清晰尘颓,刪除部分HTML標(biāo)簽

1、目錄結(jié)構(gòu)
2、安裝Mapbox
3饿自、Map.vue組件
4、MapPage.vue頁(yè)面


[1]

1龄坪、目錄結(jié)構(gòu)

src\
  |- components\ // 公用組件目錄
      |- Map.vue
  |- views\ // 頁(yè)面組件目錄
      // MapPage.vue為顯示地圖的頁(yè)面昭雌,包含一個(gè)Map.vue 組件
      // 其實(shí)可以把Map組件的代碼也寫(xiě)在MapPage里面,邏輯上還省事情一些健田,但是代碼行數(shù)太長(zhǎng)了烛卧,我不習(xí)慣一個(gè)組件超過(guò)300行代碼
      |- MapPage.vue
  |- App.vue
  |- main.js
邏輯結(jié)構(gòu)

[2]

2、安裝Mapbox

在項(xiàng)目目錄下執(zhí)行:

npm install mapbox-gl --save

[3]

3抄课、Map.vue組件

  • 需要用一個(gè)箭頭體現(xiàn)出船舶的 位置航向/船頭指向
  • 最開(kāi)始我的解決方法是用一張箭頭圖片唱星,然后通過(guò) css3 的 transform:rotate(x角度deg) 對(duì)圖片進(jìn)行角度轉(zhuǎn)動(dòng)雳旅,但是遇到一個(gè)問(wèn)題跟磨,當(dāng)?shù)貓D進(jìn)行旋轉(zhuǎn)和推拉(偽3D效果,官方叫bearing 和 pitch)攒盈,箭頭圖片并不會(huì)隨地圖轉(zhuǎn)動(dòng)抵拘。
  • 所以只有將箭頭通過(guò)添加圖層(layer)的方式畫(huà)在地圖就,需要求出箭頭各個(gè)點(diǎn)的坐標(biāo):

  • 當(dāng)我天真的以為真沒(méi)提供相關(guān) API型豁,結(jié)果還真找到了.....
  • 所以 Map.vue 組件的 addGeoMarker() 方法中加箭頭的部分要重新寫(xiě)了僵蛛,直接用圖片尚蝌,沒(méi)這么麻煩了(新代碼見(jiàn)這里
  • 以前寫(xiě)的老方思路留著把,說(shuō)不定在其它什么地方能用上

畫(huà)個(gè)箭頭
  • P0為船舶所在位置充尉,作為原點(diǎn)(X0,Y0)飘言,α為船舶航向
  • P1(X1,Y1),P2(X2,Y2)驼侠,P3(X3,Y3)姿鸿,β為135°,γ為225°
  • 以0.05個(gè)經(jīng)緯度作為畫(huà)箭頭的單位長(zhǎng)度 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}">
        <!-- 顯示船舶詳細(xì)信息的彈出框,顯示隱藏通過(guò)判斷 shipInfoBoardDisplay -->
    </div>
  </div>
</template>

<script>
import mapboxgl from 'mapbox-gl'

export default {
  name: 'Map',
  // 接收父組件傳遞的船舶信息笋熬,父組件從后端 API 取得
  props: ['shipInfo'],
  data () {
    return {
      // 保存 mapboxgl 對(duì)象
      mapObject: {},
      shipInfoBoardDisplay: false
    }
  },
  mounted () {
    this.mapObject = this.init()
  },
  methods: {
    // 初始化地圖
    init () {
      mapboxgl.accessToken = 'Mapbox官網(wǎng)申請(qǐng)的Token'
      const map = new mapboxgl.Map({
        container: this.$refs.Mapbox,
        style: 'mapbox://styles/mapbox/streets-v11',
        // 設(shè)置地圖中心
        center: [114.1, 22.2],
        // 設(shè)置地圖比例
        zoom: 8
      })

      // 地圖導(dǎo)航
      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
    },

    // 跳轉(zhuǎn)到坐標(biāo)
    flyTo () {
      let map = this.mapObject
      map.flyTo({
        center: this.shipInfo.local.value,
        zoom: 7.5,
        speed: 0.5,
        curve: 1
      })
    },

    // 添加自定義標(biāo)記點(diǎn)
    addGeoMarker () {
      let map = this.mapObject
      // 畫(huà)箭頭的單位長(zhǎng)度(經(jīng)緯度偏移)
      let lengthUnit = 0.05
      // 與y軸夾角
      let rotateAngle = this.shipInfo.headDirect.value
      // 箭頭的四個(gè)點(diǎn)
      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)
      // 繪制箭頭圖像區(qū)域
      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 和 名稱 標(biāo)記
      // 創(chuàng)建 div.marker-wrap, div.marker-title, div.marker-wrap 用作定位, div.marker-title 顯示標(biāo)題
      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)
      // 默認(rèn)添加標(biāo)記點(diǎn)后顯示信息面板
      this.shipInfoBoardDisplay = true
      // 返回 layer 的 id 和自定義的 圖層對(duì)象
      return { id: this.shipInfo.callsign.value, tagObject: markerTagObject }
    },
    // 移除自定義標(biāo)記點(diǎn), 接收對(duì)象參數(shù) {id: id, tagObject: markerTagObject}
    removeGeoMarker (markerObject) {
      let map = this.mapObject
      map.removeLayer(markerObject.id)
      map.removeSource(markerObject.id)
      markerObject.tagObject.remove()
      this.shipInfoBoardDisplay = false
    },
    // 關(guān)閉信息面板
    shipInfoBoardClose () {
      this.shipInfoBoardDisplay = false
    }
  }
}
</script>

<style scoped lang='less'>
#ship-info {
  display: none;
}
#ship-info.active {
  display: block;
}
</style>


[4]

4热某、MapPage.vue頁(yè)面

<template>
  <div id="map-page" class="height-full">
     <!-- 搜索框 -->
    <el-row class="search-bar-wrap">
            <el-input v-model="searchForm.shipCode" placeholder="船號(hào), 呼號(hào), 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 {
      // 存儲(chǔ)返回的標(biāo)記對(duì)象,主要用作判斷指定船舶標(biāo)記是否存在和刪除指定船舶標(biāo)記
      markerObject: {},
      searchForm: {
        shipCode: ''
      },
      // 由于沒(méi)有從后端拿數(shù)據(jù)胳螟,這里就直接寫(xiě)在邏輯中昔馋,傳遞給Map子組件
      shipInfo: {
        name: {label: '船名', value: '泰坦尼克號(hào)'},
        mmsi: {label: 'MMSI', value: 'test-123'},
        callsign: {label: '呼號(hào)', value: 'WED2234'}
        // ...
      }
    }
  },
  components: {
    'Map': Map
  },
  methods: {
    searchSubmit () {
      // 由于沒(méi)有后端,這里直接在邏輯中寫(xiě)死一個(gè)船舶的識(shí)別碼
     if (this.searchForm.shipCode === 'test-123') {
        this.markerAddRemoveToggle()
      } else if (this.searchForm.shipCode === '') {
        this.$message({
          showClose: true,
          message: '請(qǐng)輸入船號(hào), 呼號(hào), MMSI 或 IMO'
        })
      } else {
        this.$message({
          showClose: true,
          message: '未找到與 "' + this.searchForm.shipCode + '" 有關(guān)的船只信息'
        })
      }
    },
    // 判斷 markerObject 是否為空糖耸,對(duì) markerlayer 進(jìn)行增刪
    markerAddRemoveToggle () {
      // 將 markerObject 轉(zhuǎn)換成數(shù)組绒极,如果數(shù)組 length 為 0 則判斷 markerObject 是空對(duì)象
      let objectArr = Object.keys(this.markerObject)
      if (objectArr.length === 0) {
        // 通過(guò)this.$refs.map 觸發(fā)子組件(<Map ref="map" />)函數(shù)
        // 將返回的標(biāo)記對(duì)象賦值給 markerObject
        this.markerObject = this.$refs.map.addGeoMarker()
        this.$refs.map.flyTo()
      } else {
        // 如果 markerObject.id 等于 shipInfo.callsign.value 表示當(dāng)前已經(jīng)生成了其 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 沒(méi)有 scoped
*/
.mapboxgl-ctrl-top-left,
.mapboxgl-ctrl-top-right {
  margin-top: 50px;
}
</style>


  1. 目錄結(jié)構(gòu) ?

  2. 安裝Mapbox ?

  3. Map.vue組件 ?

  4. MapPage.vue頁(yè)面 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載垄提,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末周拐,一起剝皮案震驚了整個(gè)濱河市铡俐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妥粟,老刑警劉巖审丘,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異勾给,居然都是意外死亡滩报,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門播急,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)脓钾,“玉大人,你說(shuō)我怎么就攤上這事桩警】裳担” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)握截。 經(jīng)常有香客問(wèn)我飞崖,道長(zhǎng),這世上最難降的妖魔是什么谨胞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任固歪,我火速辦了婚禮,結(jié)果婚禮上胯努,老公的妹妹穿的比我還像新娘昼牛。我一直安慰自己,他們只是感情好康聂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布贰健。 她就那樣靜靜地躺著,像睡著了一般恬汁。 火紅的嫁衣襯著肌膚如雪伶椿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天氓侧,我揣著相機(jī)與錄音脊另,去河邊找鬼。 笑死约巷,一個(gè)胖子當(dāng)著我的面吹牛偎痛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播独郎,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼踩麦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了氓癌?” 一聲冷哼從身側(cè)響起谓谦,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贪婉,沒(méi)想到半個(gè)月后反粥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疲迂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年才顿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尤蒿。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡郑气,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出优质,到底是詐尸還是另有隱情竣贪,我是刑警寧澤军洼,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布巩螃,位于F島的核電站演怎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏避乏。R本人自食惡果不足惜爷耀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拍皮。 院中可真熱鬧歹叮,春花似錦、人聲如沸铆帽。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)爹橱。三九已至萨螺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愧驱,已是汗流浹背慰技。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留组砚,地道東北人吻商。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像糟红,于是被迫代替她去往敵國(guó)和親艾帐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容