Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >仿高德地图实现输入起点和终点规划路径并可切换

仿高德地图实现输入起点和终点规划路径并可切换

作者头像
牛老师讲GIS
发布于 2025-05-31 07:29:45
发布于 2025-05-31 07:29:45
14600
代码可运行
举报
运行总次数:0
代码可运行

概述

本文结合高德API和MapboxGL,仿照手机版高德地图实现用户输入起点和终点位置并模糊搜索选择具体位置,根据选择的起始点位置规划路径,并实现多条路径的切换展示。

实现效果

动画.gif
动画.gif

代码实现

1. 实现思路
  1. 页面初始化的时候获取用户当前的位置,并设置为地图的初始化位置;
  2. 调用regeo接口设置当前城市;
  3. 通过inputtips实现起始点的模糊查询;
  4. 调用v5/direction/driving接口实现路径规划,根据文档:
  • v5版本的接口可返回多条路径,v3貌似只返回一条
  • 需传入参数show_fields: 'polyline,cost'
  • polyline用于展示路径
  • cost用于展示耗时
  1. 通过三个图层实现规划路径的展示
  • 最底层的是路径描边,颜色设置最深
  • 中间层是未选择的路径
  • 最上面的是选择的路径
2.实现代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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>
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验