uni-app 地图高级功能

核心知识点

1. 地图基础

  • 地图组件:使用 <map> 组件
  • 地图配置:中心坐标、缩放级别、地图类型等
  • 地图初始化:设置初始参数和监听地图加载
  • 地图API:使用 uni.createMapContext() 获取地图上下文

2. 地图标记

  • 标记类型:普通标记、自定义标记、聚合标记
  • 标记属性:位置、图标、标题、内容等
  • 标记事件:点击、拖动、显示、隐藏等
  • 标记动画:添加、删除、移动等动画效果
  • 标记管理:批量添加、删除、更新标记

3. 路线规划

  • 路线类型:驾车、步行、骑行、公交等
  • 路线参数:起点、终点、途经点等
  • 路线计算:调用地图服务计算路线
  • 路线绘制:在地图上绘制路线
  • 路线信息:距离、时间、步骤等

4. 地理编码

  • 正向编码:地址转坐标
  • 反向编码:坐标转地址
  • 地理编码服务:使用第三方地图服务
  • 编码缓存:缓存编码结果,减少请求

5. 地图事件

  • 地图交互事件:点击、拖动、缩放、旋转等
  • 标记事件:点击标记、拖动标记等
  • 覆盖物事件:点击覆盖物、修改覆盖物等
  • 定位事件:位置更新、定位失败等

6. 地图覆盖物

  • 覆盖物类型:多边形、圆形、折线、矩形等
  • 覆盖物属性:位置、样式、层级等
  • 覆盖物操作:添加、删除、更新覆盖物
  • 覆盖物交互:点击、拖动覆盖物等

7. 地图定位

  • 定位API:使用 uni.getLocation() 获取位置
  • 定位精度:设置定位精度和超时时间
  • 定位权限:需要获取位置权限
  • 持续定位:实现位置实时更新

8. 跨平台地图处理

  • 平台差异:不同平台对地图的支持
  • 条件编译:为不同平台提供不同的地图实现
  • 第三方地图:集成高德、百度、腾讯等地图
  • 地图服务:使用地图SDK提供的服务

实用案例

案例 1:导航应用

需求分析

  • 支持起点到终点的路线规划
  • 支持驾车、步行、骑行等多种出行方式
  • 显示路线详情和预计时间
  • 支持实时导航和路线偏离提醒
  • 支持地图缩放和旋转

实现方案

  1. 地图初始化

    <template>
      <view class="map-container">
        <map
          id="navi-map"
          :style="{ width: '100%', height: '100%' }"
          :latitude="centerLat"
          :longitude="centerLng"
          :scale="16"
          :markers="markers"
          :polyline="polyline"
          @load="mapLoad"
          @click="mapClick"
          class="navi-map"
        ></map>
        <view class="controls">
          <button @click="getLocation" class="control-btn">定位</button>
          <button @click="calculateRoute" class="control-btn">规划路线</button>
          <button @click="toggleMode" class="control-btn">{{ travelMode === 'driving' ? '驾车' : travelMode === 'walking' ? '步行' : '骑行' }}</button>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          mapContext: null,
          centerLat: 39.9042,
          centerLng: 116.4074,
          markers: [],
          polyline: [],
          startPoint: { lat: 39.9042, lng: 116.4074 },
          endPoint: { lat: 39.9142, lng: 116.4174 },
          travelMode: 'driving',
          routeInfo: {}
        }
      },
      onLoad() {
        this.initMap()
      },
      methods: {
        initMap() {
          this.mapContext = uni.createMapContext('navi-map', this)
        },
        mapLoad() {
          console.log('地图加载完成')
          this.addMarkers()
        },
        getLocation() {
          uni.getLocation({
            type: 'gcj02',
            altitude: true,
            success: (res) => {
              this.centerLat = res.latitude
              this.centerLng = res.longitude
              this.startPoint = { lat: res.latitude, lng: res.longitude }
              this.mapContext.moveToLocation({
                latitude: res.latitude,
                longitude: res.longitude
              })
              this.addMarkers()
            },
            fail: (err) => {
              console.error('获取位置失败', err)
            }
          })
        },
        addMarkers() {
          this.markers = [
            {
              id: 1,
              latitude: this.startPoint.lat,
              longitude: this.startPoint.lng,
              title: '起点',
              iconPath: '/static/marker-start.png',
              width: 30,
              height: 30
            },
            {
              id: 2,
              latitude: this.endPoint.lat,
              longitude: this.endPoint.lng,
              title: '终点',
              iconPath: '/static/marker-end.png',
              width: 30,
              height: 30
            }
          ]
        },
        calculateRoute() {
          // 这里调用地图服务计算路线
          // 实际项目中需要使用第三方地图API
          this.simulateRoute()
        },
        simulateRoute() {
          // 模拟路线数据
          const routePoints = [
            { latitude: this.startPoint.lat, longitude: this.startPoint.lng },
            { latitude: 39.9082, longitude: 116.4094 },
            { latitude: 39.9102, longitude: 116.4114 },
            { latitude: 39.9122, longitude: 116.4134 },
            { latitude: this.endPoint.lat, longitude: this.endPoint.lng }
          ]
          
          this.polyline = [{
            points: routePoints,
            color: '#007AFF',
            width: 5,
            dottedLine: false
          }]
          
          this.routeInfo = {
            distance: '2.5公里',
            duration: '15分钟',
            steps: [
              '从起点出发,向东行驶',
              '左转进入建国路',
              '直行1.5公里',
              '右转进入朝阳路',
              '到达终点'
            ]
          }
          
          uni.showModal({
            title: '路线规划',
            content: `距离:${this.routeInfo.distance}\n时间:${this.routeInfo.duration}`,
            showCancel: false
          })
        },
        toggleMode() {
          const modes = ['driving', 'walking', 'riding']
          const currentIndex = modes.indexOf(this.travelMode)
          this.travelMode = modes[(currentIndex + 1) % modes.length]
        },
        mapClick(e) {
          console.log('点击地图', e)
        }
      }
    }
    </script>
  2. 实时导航

    • 监听位置更新,实时调整路线
    • 计算与路线的偏差,提供偏离提醒
    • 显示当前位置和目的地的实时距离

案例 2:附近商家应用

需求分析

  • 显示当前位置附近的商家
  • 支持按分类筛选商家
  • 显示商家详情和距离
  • 支持商家导航
  • 支持地图和列表切换查看

实现方案

  1. 附近商家查询

    <template>
      <view class="nearby-container">
        <view class="header">
          <text class="title">附近商家</text>
          <view class="filter">
            <button 
              v-for="(category, index) in categories" 
              :key="index"
              @click="selectCategory(category)"
              :class="{ active: selectedCategory === category }"
              class="filter-btn"
            >
              {{ category }}
            </button>
          </view>
        </view>
        
        <view class="content">
          <map
            id="nearby-map"
            :style="{ width: '100%', height: '500rpx' }"
            :latitude="centerLat"
            :longitude="centerLng"
            :scale="16"
            :markers="markers"
            @markertap="markerTap"
            class="nearby-map"
          ></map>
          
          <view class="list-section">
            <view 
              v-for="(shop, index) in shops" 
              :key="index"
              @click="showShopDetail(shop)"
              class="shop-item"
            >
              <image :src="shop.image" mode="aspectFill" class="shop-image"></image>
              <view class="shop-info">
                <text class="shop-name">{{ shop.name }}</text>
                <text class="shop-address">{{ shop.address }}</text>
                <view class="shop-meta">
                  <text class="shop-distance">{{ shop.distance }}米</text>
                  <text class="shop-rating">{{ shop.rating }}分</text>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          centerLat: 39.9042,
          centerLng: 116.4074,
          categories: ['全部', '餐饮', '购物', '娱乐', '酒店'],
          selectedCategory: '全部',
          shops: [],
          markers: []
        }
      },
      onLoad() {
        this.getLocation()
      },
      methods: {
        getLocation() {
          uni.getLocation({
            type: 'gcj02',
            success: (res) => {
              this.centerLat = res.latitude
              this.centerLng = res.longitude
              this.searchNearbyShops()
            },
            fail: (err) => {
              console.error('获取位置失败', err)
              this.searchNearbyShops()
            }
          })
        },
        searchNearbyShops() {
          // 模拟附近商家数据
          this.shops = [
            {
              id: 1,
              name: '麦当劳',
              address: '北京市朝阳区建国路88号',
              distance: 200,
              rating: 4.5,
              image: 'https://example.com/mcdonalds.jpg',
              latitude: 39.9052,
              longitude: 116.4084,
              category: '餐饮'
            },
            {
              id: 2,
              name: '星巴克',
              address: '北京市朝阳区建国路89号',
              distance: 300,
              rating: 4.7,
              image: 'https://example.com/starbucks.jpg',
              latitude: 39.9062,
              longitude: 116.4094,
              category: '餐饮'
            },
            {
              id: 3,
              name: '国贸商城',
              address: '北京市朝阳区建国门外大街1号',
              distance: 500,
              rating: 4.8,
              image: 'https://example.com/icc.jpg',
              latitude: 39.9072,
              longitude: 116.4104,
              category: '购物'
            },
            {
              id: 4,
              name: '万达影城',
              address: '北京市朝阳区建国路93号',
              distance: 800,
              rating: 4.6,
              image: 'https://example.com/wanda.jpg',
              latitude: 39.9082,
              longitude: 116.4114,
              category: '娱乐'
            }
          ]
          
          this.updateMarkers()
        },
        updateMarkers() {
          this.markers = this.shops.map(shop => ({
            id: shop.id,
            latitude: shop.latitude,
            longitude: shop.longitude,
            title: shop.name,
            snippet: `${shop.distance}米 | ${shop.rating}分`,
            iconPath: '/static/marker-shop.png',
            width: 30,
            height: 30
          }))
        },
        selectCategory(category) {
          this.selectedCategory = category
          // 根据分类筛选商家
          if (category === '全部') {
            this.searchNearbyShops()
          } else {
            const filteredShops = this.shops.filter(shop => shop.category === category)
            this.shops = filteredShops
            this.updateMarkers()
          }
        },
        markerTap(e) {
          const markerId = e.markerId
          const shop = this.shops.find(shop => shop.id === markerId)
          if (shop) {
            this.showShopDetail(shop)
          }
        },
        showShopDetail(shop) {
          uni.showModal({
            title: shop.name,
            content: `地址:${shop.address}\n距离:${shop.distance}米\n评分:${shop.rating}分`,
            confirmText: '导航',
            cancelText: '取消',
            success: (res) => {
              if (res.confirm) {
                this.navigateToShop(shop)
              }
            }
          })
        },
        navigateToShop(shop) {
          // 跳转到导航页面
          uni.navigateTo({
            url: `/pages/navi/navi?startLat=${this.centerLat}&startLng=${this.centerLng}&endLat=${shop.latitude}&endLng=${shop.longitude}&endName=${encodeURIComponent(shop.name)}`
          })
        }
      }
    </script>
  2. 商家详情和导航

    • 点击商家标记或列表项,显示商家详情
    • 提供导航功能,跳转到导航页面
    • 显示商家的详细信息和用户评价

案例 3:地图轨迹应用

需求分析

  • 记录用户的运动轨迹
  • 在地图上显示轨迹路线
  • 计算轨迹的距离和时间
  • 支持轨迹回放和分享

实现方案

  1. 轨迹记录

    <template>
      <view class="track-container">
        <map
          id="track-map"
          :style="{ width: '100%', height: '500rpx' }"
          :latitude="centerLat"
          :longitude="centerLng"
          :scale="16"
          :polyline="polyline"
          :markers="markers"
          class="track-map"
        ></map>
        
        <view class="controls">
          <button 
            @click="toggleRecord" 
            :type="isRecording ? 'warn' : 'primary'"
            class="record-btn"
          >
            {{ isRecording ? '停止记录' : '开始记录' }}
          </button>
          <button @click="saveTrack" type="primary" class="save-btn" v-if="!isRecording && trackPoints.length > 0">
            保存轨迹
          </button>
        </view>
        
        <view class="track-info" v-if="trackInfo">
          <text class="info-item">距离:{{ trackInfo.distance }}米</text>
          <text class="info-item">时间:{{ trackInfo.duration }}分钟</text>
          <text class="info-item">速度:{{ trackInfo.speed }}km/h</text>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          mapContext: null,
          centerLat: 39.9042,
          centerLng: 116.4074,
          isRecording: false,
          trackPoints: [],
          polyline: [],
          markers: [],
          startTime: 0,
          endTime: 0,
          trackInfo: null,
          locationTimer: null
        }
      },
      onLoad() {
        this.initMap()
      },
      onUnload() {
        if (this.locationTimer) {
          clearInterval(this.locationTimer)
        }
      },
      methods: {
        initMap() {
          this.mapContext = uni.createMapContext('track-map', this)
        },
        toggleRecord() {
          if (this.isRecording) {
            this.stopRecord()
          } else {
            this.startRecord()
          }
        },
        startRecord() {
          this.isRecording = true
          this.trackPoints = []
          this.startTime = Date.now()
          this.trackInfo = null
          
          // 开始定位
          this.locationTimer = setInterval(() => {
            this.getLocation()
          }, 5000) // 每5秒获取一次位置
          
          this.getLocation()
        },
        getLocation() {
          uni.getLocation({
            type: 'gcj02',
            altitude: true,
            success: (res) => {
              const point = {
                latitude: res.latitude,
                longitude: res.longitude,
                timestamp: Date.now()
              }
              this.trackPoints.push(point)
              this.centerLat = res.latitude
              this.centerLng = res.longitude
              this.updateMap()
            },
            fail: (err) => {
              console.error('获取位置失败', err)
            }
          })
        },
        updateMap() {
          if (this.trackPoints.length > 0) {
            // 更新轨迹线
            this.polyline = [{
              points: this.trackPoints,
              color: '#FF0000',
              width: 5,
              dottedLine: false
            }]
            
            // 更新标记
            this.markers = [
              {
                id: 1,
                latitude: this.trackPoints[0].latitude,
                longitude: this.trackPoints[0].longitude,
                title: '起点',
                iconPath: '/static/marker-start.png',
                width: 30,
                height: 30
              },
              {
                id: 2,
                latitude: this.trackPoints[this.trackPoints.length - 1].latitude,
                longitude: this.trackPoints[this.trackPoints.length - 1].longitude,
                title: '终点',
                iconPath: '/static/marker-end.png',
                width: 30,
                height: 30
              }
            ]
          }
        },
        stopRecord() {
          this.isRecording = false
          this.endTime = Date.now()
          
          if (this.locationTimer) {
            clearInterval(this.locationTimer)
            this.locationTimer = null
          }
          
          // 计算轨迹信息
          this.calculateTrackInfo()
        },
        calculateTrackInfo() {
          if (this.trackPoints.length < 2) return
          
          // 计算距离(简化计算,实际应使用更精确的算法)
          let distance = 0
          for (let i = 1; i < this.trackPoints.length; i++) {
            const p1 = this.trackPoints[i - 1]
            const p2 = this.trackPoints[i]
            // 使用Haversine公式计算两点之间的距离
            const R = 6371e3 // 地球半径(米)
            const φ1 = p1.latitude * Math.PI / 180
            const φ2 = p2.latitude * Math.PI / 180
            const Δφ = (p2.latitude - p1.latitude) * Math.PI / 180
            const Δλ = (p2.longitude - p1.longitude) * Math.PI / 180
            
            const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
                      Math.cos(φ1) * Math.cos(φ2) *
                      Math.sin(Δλ/2) * Math.sin(Δλ/2)
            const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
            
            distance += R * c
          }
          
          const duration = Math.round((this.endTime - this.startTime) / 60000) // 分钟
          const speed = distance > 0 && duration > 0 ? (distance / 1000 / (duration / 60)).toFixed(1) : 0
          
          this.trackInfo = {
            distance: Math.round(distance),
            duration: duration,
            speed: speed
          }
        },
        saveTrack() {
          if (this.trackPoints.length < 2) {
            uni.showToast({ title: '轨迹太短,无法保存', icon: 'none' })
            return
          }
          
          const track = {
            id: Date.now(),
            points: this.trackPoints,
            distance: this.trackInfo.distance,
            duration: this.trackInfo.duration,
            speed: this.trackInfo.speed,
            startTime: this.startTime,
            endTime: this.endTime
          }
          
          // 保存轨迹到本地存储
          let tracks = uni.getStorageSync('tracks') || []
          tracks.push(track)
          uni.setStorageSync('tracks', tracks)
          
          uni.showToast({ title: '轨迹保存成功', icon: 'success' })
        }
      }
    }
    </script>
  2. 轨迹回放

    • 加载保存的轨迹数据
    • 在地图上动态显示轨迹
    • 控制回放速度和进度

学习目标

  1. 掌握 uni-app 中地图的高级功能和 API
  2. 学会实现地图标记、路线规划、地理编码等功能
  3. 了解地图事件和覆盖物的使用方法
  4. 能够开发复杂的地图应用,如导航、附近商家、轨迹记录等
  5. 掌握跨平台地图处理的技巧
  6. 提升地图应用的用户体验

总结

地图高级功能是 uni-app 应用开发中的重要组成部分,通过合理使用地图标记、路线规划、地理编码等功能,可以开发出功能丰富的地图应用。在实际开发中,开发者应该根据具体场景选择合适的地图功能,结合第三方地图服务,提供最佳的地图体验。

同时,开发者还需要注意跨平台地图处理的差异,使用条件编译或平台特定的 API 来处理不同平台的地图问题,确保应用在所有平台上都能正常运行地图功能。对于需要高精度地图服务的应用,还可以考虑集成第三方地图 SDK,提升地图功能的性能和准确性。

« 上一篇 uni-app 音频处理 下一篇 » uni-app 定位服务