主要功能有:
通过搜索框查询船舶关键字
查询到船舶后, 自动定位到船舶位置
弹出船舶相关信息
DEMO: 船只搜索代号为 9445320(已经接了真实全球数据)
默认已经使用 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>
目录结构
安装 Mapbox
Map.vue 组件
MapPage.vue 页面
来源: http://www.jianshu.com/p/ff5bfcf4008b