本文结合高德API和MapboxGL,仿照手机版高德地图实现用户输入起点和终点位置并模糊搜索选择具体位置,根据选择的起始点位置规划路径,并实现多条路径的切换展示。
regeo
接口设置当前城市;inputtips
实现起始点的模糊查询;v5/direction/driving
接口实现路径规划,根据文档:v5
版本的接口可返回多条路径,v3
貌似只返回一条show_fields: 'polyline,cost'
polyline
用于展示路径cost
用于展示耗时<template>
<div class="container">
<div class="query">
<el-form :model="form" label-width="auto" class="query-form">
<el-form-item label="起点" class="form-start-position">
<el-autocomplete
v-model="form.startPosition"
:fetch-suggestions="querySearchStart"
clearable
placeholder="请输入起点位置"
@select="handleSelectStart"
>
<template #default="{ item }">
<div class="autocomplete-value">{{ item.value }}</div>
<div class="autocomplete-address">{{ item.address }}</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item label="终点" style="margin-bottom: 0" class="form-end-position">
<el-autocomplete
v-model="form.endPosition"
:fetch-suggestions="querySearchEnd"
clearable
placeholder="请输入终点位置"
@select="handleSelectEnd"
>
<template #default="{ item }">
<div class="autocomplete-value">{{ item.value }}</div>
<div class="autocomplete-address">{{ item.address }}</div>
</template>
</el-autocomplete>
</el-form-item>
</el-form>
<div class="query-button">
<el-button :disabled="!form.startPosition || !form.endPosition" class="query-button-inner" @click="queryRoute">
查询
</el-button>
<!-- <el-button class="query-button-inner" @click="queryRoute">查询</el-button> -->
</div>
</div>
<div class="map" id="map">
<div class="routes" v-show="features.length > 0">
<div
v-for="(feature, index) of features"
:key="index"
:class="index === selectedIndex ? 'route active' : 'route'"
@click="updateIndex(index)"
>
<div class="cost">{{ Math.ceil(feature.properties.cost / 60) }}分钟</div>
<div>{{ feature.properties.distance }}公里</div>
<div>{{ index === 0 ? '大众常选' : '方案' + index }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ElMessage } from 'element-plus'
import * as turf from '@turf/turf'
let map = null
const layerId = 'my-route'
function request(url, params) {
const AK = '你申请的AK值'
const BASE_URL = 'https://restapi.amap.com/'
let fullUrl = url.indexOf('json') === -1 ? `${BASE_URL}${url}?key=${AK}` : url
for (const key in params) {
fullUrl += `&${key}=${params[key]}`
}
return new Promise((resolve, reject) => {
fetch(fullUrl)
.then(res => res.json())
.then(res => {
if (res.status === '1') resolve(res)
else {
ElMessage.error('接口请求失败')
reject(res)
}
})
})
}
export default {
data() {
return {
center: [113.94150905808976, 22.523881824251347],
form: {
startPosition: '',
endPosition: '',
startCoord: [],
endCoord: [],
},
cityInfo: {},
features: [],
selectedIndex: 0,
path: null,
}
},
mounted() {
const that = this
function successFunc(position) {
const { longitude, latitude } = position.coords
that.center = [longitude, latitude]
that.initMap()
}
function errFunc() {
that.initMap()
}
if (navigator.geolocation) {
try {
errFunc()
navigator.geolocation.getCurrentPosition(successFunc, errFunc)
} catch (e) {
errFunc()
}
} else {
errFunc()
}
},
methods: {
initMap() {
map = new SFMap.Map({
container: 'map',
center: this.center,
zoom: 17.1,
})
map.on('load', e => {
map.addSource(layerId, {
type: 'geojson',
data: turf.featureCollection([]),
})
map.addSource(layerId + '-line', {
type: 'geojson',
data: turf.featureCollection([]),
})
map.addSource(layerId + '-border', {
type: 'geojson',
data: turf.featureCollection([]),
})
map.addLayer({
id: layerId + '-border',
type: 'line',
source: layerId + '-border',
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#056113',
'line-width': 13,
},
})
map.addLayer({
id: layerId + '-line',
type: 'line',
source: layerId + '-line',
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#80d18c',
'line-width': 10,
},
})
map.addLayer({
id: layerId,
type: 'line',
source: layerId,
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#16ab2d',
'line-width': 10,
},
})
map.on('mousemove', layerId + '-line', e => {
map.getCanvas().style.cursor = 'pointer'
})
map.on('mouseout', layerId + '-line', e => {
map.getCanvas().style.cursor = ''
})
map.on('click', layerId + '-line', e => {
const feature = e.features[0]
this.selectedIndex = feature.properties.index
this.setShowRoute()
})
const arrow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAnElEQVQ4T63TsQ0CMQyF4f/NgMQQ0CBR0FIx190cFIiWhhFoKdgEiRUeSoF0gO8cjkub+Evs2OLPpc9422dgBRwlNZn/BtjeApdOUJsh0QuuwKYWiYAFcAKWHaSR1EbpfAHlkO0ICdMJgV+QXmAAmUu6v9IZA8wkPVKgtg7TF7H25t4UbN+A9ahGmqqVD8AO2GdzUF45+I3ZJJb9JxbwRhEhB66xAAAAAElFTkSuQmCC'
map.loadImage(arrow, function (error, image) {
if (error) throw error
map.addImage(layerId + '-arrow', image)
map.addLayer({
id: layerId + '-line-arrow',
source: layerId + '-line',
type: 'symbol',
layout: {
'symbol-placement': 'line',
'symbol-spacing': 50,
'icon-image': layerId + '-arrow',
'icon-size': 0.6,
'icon-allow-overlap': true,
},
})
map.addLayer({
id: layerId + '-arrow',
source: layerId,
type: 'symbol',
layout: {
'symbol-placement': 'line',
'symbol-spacing': 50,
'icon-image': layerId + '-arrow',
'icon-size': 0.6,
'icon-allow-overlap': true,
},
})
})
request('v3/geocode/regeo', {
location: this.center.join(','),
}).then(res => {
this.cityInfo = res.regeocode.addressComponent
})
})
},
querySearchStart(str, cb) {
if (str) {
request('v3/assistant/inputtips', {
keywords: str,
city: this.cityInfo?.city,
}).then(res => {
cb(
res.tips.map(t => {
t.value = t.name
return t
}),
)
})
} else {
cb([])
}
},
querySearchEnd(str, cb) {
if (str) {
request('v3/assistant/inputtips', {
keywords: str,
city: this.cityInfo?.city,
}).then(res => {
cb(
res.tips.map(t => {
t.value = t.name
return t
}),
)
})
} else {
cb([])
}
},
handleSelectStart(item) {
this.form.startCoord = item.location.split(',').map(Number)
},
handleSelectEnd(item) {
this.form.endCoord = item.location.split(',').map(Number)
},
queryRoute() {
const { startCoord, endCoord } = this.form
// request('./data.json', {
// }).then(res => {
request('v5/direction/driving', {
origin: startCoord.join(','),
destination: endCoord.join(','),
show_fields: 'polyline,cost',
}).then(res => {
const paths = res.route.paths
let features = paths.map((path, index) => {
let coordinates = []
path.steps.forEach(step => {
const polyline = step.polyline.split(';').map(c => c.split(',').map(Number))
coordinates = [...coordinates, ...polyline]
})
return {
type: 'Feature',
properties: {
index,
distance: path.distance,
cost: path?.duration || path?.cost.duration,
},
geometry: {
type: 'LineString',
coordinates: coordinates,
},
}
})
this.features = features
map.getSource(layerId + '-border').setData(turf.featureCollection(features))
this.setShowRoute()
})
},
updateIndex(index) {
if (this.selectedIndex === index) return
this.selectedIndex = index
this.setShowRoute()
},
setShowRoute() {
const feature = this.features[this.selectedIndex]
this.path = feature.properties
map.getSource(layerId).setData(feature)
const features = this.features.filter((f, i) => i !== this.selectedIndex)
map.getSource(layerId + '-line').setData(turf.featureCollection(features))
map.fitBounds(turf.bbox(feature), { padding: { top: 50, bottom: 220, left: 50, right: 50 } })
},
},
}
</script>
<style scoped lang="scss">
.container {
width: 100%;
height: 100vh;
overflow: hidden;
}
.query {
padding: 0.8rem;
overflow: hidden;
background: #ccc;
.query-form {
width: calc(100% - 4rem);
float: left;
}
.query-button {
width: 4rem;
height: 4.7rem;
float: right;
display: flex;
justify-content: flex-end;
align-items: center;
.query-button-inner {
width: 3.8rem;
height: 100%;
}
}
}
.map {
height: calc(100% - 6.4rem);
}
.autocomplete-value,
.autocomplete-address {
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.autocomplete-address {
font-size: 0.8rem;
color: #999;
margin-top: -0.8rem;
border-bottom: 1px solid #efefef;
}
.routes {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 99;
padding: 1rem;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.9);
text-align: left;
display: flex;
flex-direction: row;
font-size: 0.95rem;
.route {
flex-grow: 1;
border-radius: 0.5rem;
line-height: 1.8;
padding: 0.5rem 1rem;
cursor: pointer;
border-right: 1px solid #efefef;
&:last-child {
margin-right: 0;
border-right: none;
}
.cost {
font-size: 1.35rem;
font-weight: bold;
}
&.active {
color: #409eff;
background: #e3effa;
}
}
}
:deep .el-form-item {
margin-bottom: 0.5rem;
}
</style>
<style lang="scss">
.my-custom-popover-class {
background-color: rgba(1, 122, 242, 0.8);
color: #fff;
.driver-popover-arrow-side-top {
border-top-color: rgba(1, 122, 242, 0.8);
}
.driver-popover-arrow-side-bottom {
border-bottom-color: rgba(1, 122, 242, 0.8);
}
.driver-popover-arrow-side-left {
border-left-color: rgba(1, 122, 242, 0.8);
}
.driver-popover-arrow-side-right {
border-right-color: rgba(1, 122, 242, 0.8);
}
.driver-popover-progress-text,
.driver-popover-close-btn {
color: #fff;
}
.driver-popover-prev-btn,
.driver-popover-next-btn {
border: 1px solid #fff;
color: rgba(1, 122, 242, 1);
}
}
</style>