需要实现三个功能: 室外多机器人实时轨迹展示(高德) 搜索选点(高德) 室内图轨迹展示(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的移动,设置点击事件和样式,获取坐标等。
但是动画效果需要自己写,在这个案例中,我设置了自动计算角度也是无效的,同样需要自己计算(这个暂时还没搞清楚为什么)。 路径没用到,就不说了。
室外多机器人实时轨迹展示(高德)
整体分为三个部分:
- 初始化地图 (useInitMap)
- 获取路径数据(可以从webSocket中获取) (usePathData)
- 绘制点 (useMarker)
- 让点动起来 计算动画和角度 使之看起来平滑移动 (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....