高德室外多点实时轨迹展示(计算角度和平滑移动动画) + 搜索选点 + canvas轨迹展示

需要实现三个功能: 室外多机器人实时轨迹展示(高德) 搜索选点(高德) 室内图轨迹展示(canvas)

高德地图基本概念:

地图 Amap和实例map

AMap 对象提供了加载地图、创建地图、添加覆盖物、进行地图操作,加载插件等等的方法和属性。可以使用AMap创建地图实例。

ts 复制代码
const map = new AMap.Map(mapcontainer, {
  center: [121.412577, 31.218804], // Center coordinates
  zoom: 14, // Initial zoom level
});

地图实例是通过 AMap.Map 创建的一个具体的地图对象。它代表了一个可视化的地图窗口,你可以在上面添加标记、覆盖物、控件等等。地图实例具有与地图相关的属性和方法,例如中心点坐标、缩放级别、平移、添加覆盖物等等。

AMap 是高德地图 JavaScript API 的主要入口,提供了加载地图和各种功能的方法,而地图实例则是你在页面上创建的具体的地图对象,你可以通过它进行地图的操作和交互。

就是点。可以通过调用API进行添加,修改内容,移动,设置点击事件等。这里的逻辑很简单,就是把路径数据的第一个点的位置初始化为一个Marker,后续都是这个Marker的移动,设置点击事件和样式,获取坐标等。

但是动画效果需要自己写,在这个案例中,我设置了自动计算角度也是无效的,同样需要自己计算(这个暂时还没搞清楚为什么)。 路径没用到,就不说了。

室外多机器人实时轨迹展示(高德)

整体分为三个部分:

  1. 初始化地图 (useInitMap)
  2. 获取路径数据(可以从webSocket中获取) (usePathData)
  3. 绘制点 (useMarker)
  4. 让点动起来 计算动画和角度 使之看起来平滑移动 (useMarkAnimations)

index.vue

ts 复制代码
<template>
  <div class="page-tenant-map">
    <RobotInfoCardComponent :robotId="selectRobotId" /> 
    <div id="mapcontainer" style="width: 100%; height: 800px"></div>
  </div>
</template>

<script setup lang="ts">
  import RobotInfoCardComponent from '../RobotInfoCardComponent/index.vue';
  import ReviewRobotDataCardComponent from './components/ReviewRobotDataCardComponent/index.vue';
  import useInitMap from './compositions/useInitMap';
  import { onMounted, ref } from 'vue';
  import AMapLoader from '@amap/amap-jsapi-loader';
  import usePathData from '@/views/tenant/map/components/OutSideMapComponent/compositions/usePathData';
  import useMarker from '@/views/tenant/map/components/OutSideMapComponent/compositions/useMarker';
  import useMarkAnimations from '@/views/tenant/map/components/OutSideMapComponent/compositions/useMarkAnimations';

  const selectRobotId = ref('');
  const AMapKey = '';

  onMounted(async () => {
    // Load AMap API
    const AMap = await AMapLoader.load({
      key: AMapKey,
      version: '2.0', 
    });

    const { map } = useInitMap(AMap, 'mapcontainer');

    const { pathData } = usePathData();
    const { markers } = useMarker(map, AMap, pathData, selectRobotId);

    useMarkAnimations(map, AMap, pathData, markers);
  });
</script>

<style scoped src="./index.less" lang="less" />

useInitMap 初始化地图 加载一些插件

arduino 复制代码
const useInitMap = (AMap, mapcontainer: string) => {
  const map = new AMap.Map(mapcontainer, {
    center: [121.412577, 31.218804], // Center coordinates
    zoom: 14, // Initial zoom level
  });

  AMap.plugin(
    [
      'AMap.ToolBar',
      'AMap.Scale',
      'AMap.Geolocation',
      'AMap.PlaceSearch',
      'AMap.Geocoder',
      'AMap.MoveAnimation',
    ],
    () => {
      // 缩放条
      const toolbar = new AMap.ToolBar();
      // 比例尺
      const scale = new AMap.Scale();

      map.addControl(toolbar);
      map.addControl(scale);
    },
  );

  return { map };
};

export default useInitMap;

usePathData 随便生成的两个路径数据,一个圆的一个方的,测试角度用,注释中是从webSocket连接中获取数据

ini 复制代码
import { useWebSocket } from '@vueuse/core';
import { reactive, watchEffect } from 'vue';

const usePathData = () => {
  // const state = reactive({
  //   server: 'ws://localhost:3300/test',
  //   sendValue: '',
  //   recordList: [] as { id: number; time: number; res: string }[],
  // });
  //
  // const { status, data, send, close, open } = useWebSocket(state.server, {
  //   autoReconnect: false,
  //   heartbeat: true,
  // });
  //
  // // 解码数据 返回
  // watchEffect(() => {
  //   if (data.value) {
  //     try {
  //       const res = JSON.parse(data.value);
  //       state.recordList.push(res);
  //     } catch (error) {
  //       state.recordList.push({
  //         res: data.value,
  //         id: Math.ceil(Math.random() * 1000),
  //         time: new Date().getTime(),
  //       });
  //     }
  //   }
  // });

  // 初始的经纬度
  const center = { lng: 121.422635, lat: 31.216688 };

  // 生成缓慢前进的四方形路径的经纬度数据数组
  function generateSlowSquarePath(center, sideLength, numPoints, distancePerStep) {
    const halfSide = sideLength / 2;
    const path = [];

    for (let i = 0; i < numPoints; i++) {
      const lng = center.lng + (i % 2 === 0 ? halfSide : -halfSide);
      const lat = center.lat + (i < 2 ? halfSide : -halfSide);
      path.push({
        lng,
        lat,
        extData: {
          markerId: 234,
        },
      });
    }

    const slowPath = [];
    for (let i = 0; i < path.length - 1; i++) {
      const start = path[i];
      const end = path[i + 1];
      for (let j = 0; j < distancePerStep; j++) {
        const lng = start.lng + ((end.lng - start.lng) * j) / distancePerStep;
        const lat = start.lat + ((end.lat - start.lat) * j) / distancePerStep;
        slowPath.push({
          lng,
          lat,
          extData: {
            markerId: 567,
          },
        });
      }
    }

    slowPath.push(path[path.length - 1]);

    return slowPath;
  }

  const slowSquarePath = generateSlowSquarePath(center, 0.001, 4, 10);

  // 生成缓慢前进的圆形路径的经纬度数据数组
  function generateSlowCircularPath(center, radius, numPoints, distancePerStep) {
    const path = [];

    for (let i = 0; i < numPoints; i++) {
      const angle = (i / numPoints) * 2 * Math.PI;
      const lng = center.lng + radius * Math.cos(angle);
      const lat = center.lat + radius * Math.sin(angle);
      path.push({
        lng,
        lat,
        extData: {
          markerId: i,
        },
      });
    }

    const slowPath = [];
    for (let i = 0; i < path.length - 1; i++) {
      const start = path[i];
      const end = path[i + 1];
      for (let j = 0; j < distancePerStep; j++) {
        const lng = start.lng + ((end.lng - start.lng) * j) / distancePerStep;
        const lat = start.lat + ((end.lat - start.lat) * j) / distancePerStep;
        slowPath.push({
          lng,
          lat,
          extData: {
            markerId: i,
          },
        });
      }
    }

    slowPath.push(path[path.length - 1]);

    return slowPath;
  }

  const slowCircularPath = generateSlowCircularPath(center, 0.01, 50, 50);

  const pathData = [[...slowSquarePath], [...slowCircularPath]];

  return {
    pathData,
  };
};

export default usePathData;

useMarker 主要是初始化点,设置一些点击事件和样式效果,setFitView是让地图正好包下所有的点。

php 复制代码
import { ref } from 'vue';

const useMarker = (map, AMap, pathData, selectRobotId) => {
  let currentSelectMarker = null;
  const markers = ref([]);
  // Create and add markers to the map based on pathData

  pathData.forEach((path) => {
    const marker = new AMap.Marker({
      title: '配送001\n' + '\n' + '室外,离线\n' + '\n' + '62%,10km/h',
      position: [path[0].lng, path[0].lat],
      map,
      icon: new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u72.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      }),
      autoRotation: true,
      animation: 'AMAP_ANIMATION_DROP',
      extData: {
        a: 1,
        ...path[0].extData,
      },
    });

    marker.on('click', (data) => {
      console.log('click', data, marker.getExtData());
      selectRobotId.value = marker.getExtData()?.markerId;

      const noSelectIcon = new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u72.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      });

      currentSelectMarker && currentSelectMarker.setIcon(noSelectIcon);

      const selectIcon = new AMap.Icon({
        imageSize: new AMap.Size(60, 60),
        image:
          'https://files.axshare.com/gsc/NNGZ7Q/32/bd/f0/32bdf06cae754929be938453c4494f91/images/%E7%A7%9F%E6%88%B7%E5%85%A8%E5%B1%80%E5%9C%B0%E5%9B%BE/u75.svg?pageId=126b2c69-7f1f-4de1-bc41-3f0f0487c1d6',
      });

      marker.setIcon(selectIcon);

      currentSelectMarker = marker;
    });


    markers.value.push(marker);
  });

  map.setFitView(null, true, [220, 220, 220, 220]);

  return {
    markers,
  };
};

export default useMarker;

useMarkAnimations 根据路径 让点平滑移动 (其实有点担心,如果是实时数据获取,网络不稳定的情况下,不处理数据的话,会不会出现很离谱的延迟,放在二期做吧)

ini 复制代码
const useMarkAnimations = (map, AMap, pathData, markers) => {
  const animationInterval = 1000; 
  let currentIndex = 0;

  setInterval(() => {
    markers.value.forEach((marker, markerIndex) => {
      const path = pathData[markerIndex];
      const nextIndex = (currentIndex + 1) % path.length;
      const nextPoint = path[nextIndex];

      const startPosition = marker.getPosition();
      const endPosition = new AMap.LngLat(nextPoint.lng, nextPoint.lat);

      animateMarker(AMap, marker, startPosition, endPosition);
    });

    currentIndex = (currentIndex + 1) % pathData[0].length;
  }, animationInterval);
  
  function animateMarker(AMap, marker, startPosition, endPosition) {
    const startTime = new Date().getTime();
    const startRotation = marker.getAngle(); // 获取起始角度

    const getAngle = (startPoint, endPoint) => {
      if (!(startPoint && endPoint)) {
        return 0;
      }
      let dRotateAngle = Math.atan2(
        Math.abs(startPoint.lng - endPoint.lng),
        Math.abs(startPoint.lat - endPoint.lat),
      );
      if (endPoint.lng >= startPoint.lng) {
        if (endPoint.lat >= startPoint.lat) {
        } else {
          dRotateAngle = Math.PI - dRotateAngle;
        }
      } else {
        if (endPoint.lat >= startPoint.lat) {
          dRotateAngle = 2 * Math.PI - dRotateAngle;
        } else {
          dRotateAngle = Math.PI + dRotateAngle;
        }
      }
      dRotateAngle = (dRotateAngle * 180) / Math.PI;
      return dRotateAngle;
    };

    function step() {
      const currentTime = new Date().getTime();
      const progress = (currentTime - startTime) / 1000;

      if (progress < 1) {
        const lng = startPosition.lng + (endPosition.lng - startPosition.lng) * progress;
        const lat = startPosition.lat + (endPosition.lat - startPosition.lat) * progress;

        const rotation = getAngle(startPosition, endPosition);

        marker.setAngle(rotation);

        marker.setPosition(new AMap.LngLat(lng, lat));

        requestAnimationFrame(step);
      } else {
        marker.setPosition(endPosition);
        marker.setAngle(startRotation); // 完成后还原角度
      }
    }

    step();
  }
};

export default useMarkAnimations;

搜索选点(高德)

index.vue 先放着 稍后拆分

ini 复制代码
<template>
  <div class="map-wrapper">
    <div id="mapcontainer"></div>
    <div class="search-box">
      <a-auto-complete
        v-model:value="keyword"
        style="width: 200px"
        placeholder="输入城市+关键字搜索"
        @select="handleSelect"
        @search="handleSearch"
        :trigger-on-focus="false"
        clearable
        :options="options"
      />
      <a-input
        v-model:value="location.longitude"
        placeholder="点击地图选择经度"
        maxlength="15"
        disabled
        style="width: 150px; margin: 0 5px"
      />
      <a-input
        v-model:value="location.latitude"
        placeholder="点击地图选择纬度"
        maxlength="15"
        disabled
        style="width: 150px"
      />

      <a-button
        type="primary"
        v-if="location.longitude && location.latitude"
        style="width: 150px; margin: 0 5px"
        @click="handleConfirm"
        >选择该位置
      </a-button>
    </div>
  </div>
</template>

<script setup lang="ts">
  import { ref, onMounted, watch } from 'vue';
  import { shallowRef } from 'vue';

  import AMapLoader from '@amap/amap-jsapi-loader';

  window._AMapSecurityConfig = {
    securityJsCode: '',
  };
  const props = defineProps({
    location: {
      type: Object,
      default() {
        return {};
      },
    },
  });
  const emit = defineEmits(['update:modelValue']);
  const map = shallowRef(null);
  const options = ref(null);

  const location = ref(props.location);

  const handleConfirm = () => {
    emit('selectLocation', location.value);
  };

  watch(location, (val) => {
    if (val.longitude && val.latitude) {
      drawMarker();
    }
  });

  const keyword = ref('');
  let placeSearch, AMapObj, marker, geocoder;

  function initMap() {
    AMapLoader.load({
      key: '', // 申请好的Web端Key,首次调用 load 时必填
      version: '2.0',
    }).then((AMap) => {
      AMapObj = AMap;
      map.value = new AMap.Map('mapcontainer');
      // 添加点击事件
      map.value.on('click', onMapClick);
      if (location.value.longitude) {
        drawMarker();
      }
      AMap.plugin(
        [
          'AMap.ToolBar',
          'AMap.Scale',
          'AMap.Geolocation',
          'AMap.PlaceSearch',
          'AMap.Geocoder',
          'AMap.AutoComplete',
        ],
        () => {
          // 缩放条
          const toolbar = new AMap.ToolBar();
          // 比例尺
          const scale = new AMap.Scale();
          // 定位
          const geolocation = new AMap.Geolocation({
            enableHighAccuracy: true, //是否使用高精度定位,默认:true
            timeout: 10000, //超过10秒后停止定位,默认:5s
            position: 'RT', //定位按钮的停靠位置
            buttonOffset: new AMap.Pixel(10, 20), //定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
            zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
          });
          geocoder = new AMap.Geocoder({
            city: '全国',
          });
          map.value.addControl(geolocation);
          map.value.addControl(toolbar);
          map.value.addControl(scale);
          placeSearch = new AMap.PlaceSearch({
            map: map.value,
            city: '',
            pageSize: 10, // 单页显示结果条数
            pageIndex: 1, // 页码
            citylimit: false, // 是否强制限制在设置的城市内搜索
            autoFitView: true,
          });

          placeSearch.on('markerClick', (item) => {
            console.log('markerClick', item.data);

            const { pname, cityname, adname, address, name } = item?.data;
            const { lng, lat } = item.data?.location;
            location.value = {
              longitude: lng,
              latitude: lat,
              address,
              zone: [pname, cityname, adname],
              name,
            };
            map.value?.setZoomAndCenter(16, [lng, lat]);
          });
        },
      );
    });
  }

  onMounted(() => {
    initMap();
  });

  // 搜索地图
  function handleSearch(queryString, cb) {
    placeSearch.search(queryString, (status, result) => {
      if (result && typeof result === 'object' && result.poiList) {
        const list = result.poiList.pois;
        list.forEach((item) => {
          item.value = item.name;
          item.label = item.name;
        });
        cb?.(list);

        options.value = list;
      } else {
        cb?.([]);
      }
    });
  }

  // 点击地图
  function onMapClick(e) {
    const { lng, lat } = e.lnglat;

    // 逆地理编码
    geocoder.getAddress([lng, lat], (status, result) => {
      if (status === 'complete' && result.info === 'OK') {
        const { addressComponent, formattedAddress } = result.regeocode;
        let { city, province, district } = addressComponent;
        if (!city) {
          // 直辖市
          city = province;
        }

        console.log(
          location.value,
          'location',
          lng,
          lat,
          formattedAddress,
          province,
          city,
          district,
        );

        location.value = {
          longitude: lng,
          latitude: lat,
          address: formattedAddress,
          zone: [province, city, district],
        };
      }
    });
  }

  // 点击搜索项
  function handleSelect(label, item) {
    const { pname, cityname, adname, address, name } = item;
    const { lng, lat } = item.location;

    location.value = {
      longitude: lng,
      latitude: lat,
      address,
      zone: [pname, cityname, adname],
      name,
    };
    map.value?.setZoomAndCenter(16, [lng, lat]);
  }

  // 绘制地点marker
  function drawMarker(val) {
    const { longitude, latitude } = location.value || val;
    if (marker) {
      marker.setMap(null);
    }

    marker = new AMapObj.Marker({
      position: new AMapObj.LngLat(longitude, latitude),
      anchor: 'bottom-center',
      clickable: true,
    });

  
    map.value?.add(marker);
    map.value?.setZoomAndCenter(16, [longitude, latitude]);
  }
</script>

<style lang="less" src="./index.less" scoped />

室内图轨迹展示(canvas)

在赶工期ing....

相关推荐
众生回避1 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨2 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
&白帝&1 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂1 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
谢尔登1 小时前
Babel
前端·react.js·node.js
ling1s1 小时前
C#基础(13)结构体
前端·c#
卸任2 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
Estrella162 小时前
经典 web 页面排版:三栏布局
前端·css·面试
lxcw2 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js