MCP+JS实现动态路线规划+
- Github仓库:github.com/TianweiGan/…
项目简介
本项目基于 MCP + React + @baidumap/mapv-three + Three.js + Cesium 实现了广州到从化的路线规划并进行3D可视化展示。项目支持Bing卫星地图 + Cesium 真实地形底图,使用3D人物模型沿路线动态移动,并使相机跟随进行视角跟随。
效果展示
相关技术
- JavaScript
- React
- @baidumap/mapv-three(百度地图JS API Three)
- Cesium(提供地形服务)
- 百度地图 MCP Server
目录结构
lychee/
├── data/
│ └── lychee.geojson # 路线地理数据
├── public/
│ └── models/
│ └── running_man.glb # 3D人物模型
├── src/
│ ├── Demo.jsx # 主要展示组件
│ └── index.js # 入口文件
├── webpack.config.js # 构建配置
├── package.json # 项目依赖
└── README.md # 项目说明
快速开始
依赖安装
# 百度地图JS API Three 和 React相关
npm install --save @baidumap/mapv-three three react react-dom
# webpack相关
npm install --save-dev webpack webpack-cli copy-webpack-plugin html-webpack-plugin @babel/core @babel/preset-env @babel/preset-react babel-loader
# Cesium 相关
npm install cesium
构建与运行
npx webpack
npx serve dist
关键技术细节
webpack配置``
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'node_modules/@baidumap/mapv-three/dist/assets'),
to: 'mapvthree/assets',
},
{
from: path.resolve(__dirname, 'data'),
to: 'data',
},
],
})]
入口文件src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import Demo from './Demo';
const root = createRoot(document.getElementById('container'));
root.render(<Demo />);
MCP路径规划
现在CUrsor或其他平台使用百度地图MCP进行路线规划。
要求:路线详细+真实(路线在道路上)+将路线写入data/lychee.geojson文件中
配置AK:百度MCP API文档
数据文件data/lychee.geojson
类似这样的结构,coordinates为路线
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[113.27345, 23.13538],
......
[113.59358, 23.55379]
]
},
"properties": {
......
}
}
]
}
核心组件src/Demo.jsx
AK配置
需要使用百度地图JS API Three以及Cesium的AK,注意百度地图使用浏览器端AK AK获取:百度地图开放平台、Cesium官网
引擎与底图
初始化底图引擎,使用卫星+真实地形底图(BingImageryTileProvider + CesiumTerrainTileProvider)
const engine = new mapvthree.Engine(ref.current, {
map: {
provider: null,
center: [113.27345, 23.13538],
heading: 0,
pitch: 60,
range: 2000,
projection: 'EPSG:4326',
},
rendering: {
enableAnimationLoop: true,
},
});
let terrain = new mapvthree.CesiumTerrainTileProvider();
let imagery = new mapvthree.BingImageryTileProvider();
let vector = new mapvthree.BaiduVectorTileProvider();
engine.add(new mapvthree.MapView({
terrainProvider: terrain,
imageryProvider: imagery,
vectorProvider: null,
}));
console.log('terrainProvider:', terrain);
console.log('imageryProvider:', imagery);
console.log('vectorProvider:', vector);
飞线设置
对地图飞线进行设置
const line = engine.add(new mapvthree.FatLine({
lineWidth: 10,
keepSize: true,
color: '#87CEFA',
}));
const flyline = engine.add(new mapvthree.FatLine({
color: '#ff0000',
lineWidth: 10,
keepSize: true,
lineCap: 'round',
enableAnimation: true,
enableAnimationChaos: true,
animationTailType: 1,
animationTailRatio: 0.2,
animationIdle: 1000,
animationSpeed: 1000,
emissive: new THREE.Color(0xcf9c00),
}));
3D人物模型加载
加载动态3D人物模型,需要提前将模型放入public/models/running_man.glb
const loader = new GLTFLoader();
console.log('开始加载模型...');
loader.load(runningManModel, (gltf) => {
console.log('模型加载成功:', gltf);
model = gltf.scene;
model.scale.set(modelScale, modelScale, modelScale);
// 创建动画混合器
mixer = new THREE.AnimationMixer(model);
console.log('动画数量:', gltf.animations.length);
// 播放所有动画
if (gltf.animations.length > 0) {
const runAction = mixer.clipAction(gltf.animations[0]);
runAction.play();
}
engine.add(model);
},
// 加载进度回调
(progress) => {
console.log('加载进度:', (progress.loaded / progress.total * 100) + '%');
},
// 错误回调
(error) => {
console.error('模型加载错误:', error);
});
动画设置
创建动画,包括人物移动平滑、相机跟随、相机平滑等操作
function animate() {
const currentTime = performance.now();
const deltaTime = Math.min((currentTime - lastTime) / 1000, MAX_DELTA_TIME);
lastTime = currentTime;
if (coords && coords.length > 0 && model) {
if (mixer) {
mixer.update(deltaTime);
}
let currentPosOutput = [];
engine.map.projectArrayCoordinate(coords[currentPointIndex], currentPosOutput);
const nextIndex = (currentPointIndex + 1) % coords.length;
let nextPosOutput = [];
engine.map.projectArrayCoordinate(coords[nextIndex], nextPosOutput);
const distance = Math.sqrt(
Math.pow(nextPosOutput[0] - currentPosOutput[0], 2) +
Math.pow(nextPosOutput[1] - currentPosOutput[1], 2)
);
const progressDelta = Math.min((MOVEMENT_SPEED * deltaTime) / distance, 0.1);
progress += progressDelta;
const targetX = currentPosOutput[0] + (nextPosOutput[0] - currentPosOutput[0]) * progress;
const targetY = currentPosOutput[1] + (nextPosOutput[1] - currentPosOutput[1]) * progress;
// 使用真实高程数据进行插值
const currentHeight = coords[currentPointIndex][2];
const nextHeight = coords[nextIndex][2];
const targetZ = currentHeight + (nextHeight - currentHeight) * progress;
const direction = new THREE.Vector3(
nextPosOutput[0] - currentPosOutput[0],
nextPosOutput[1] - currentPosOutput[1],
0
).normalize();
const targetAngle = Math.atan2(direction.y, direction.x) + Math.PI / 2;
if (lastModelPos.x === 0 && lastModelPos.y === 0 && lastModelPos.z === 0) {
lastModelPos.set(targetX, targetY, targetZ);
lastModelAngle = targetAngle;
} else {
lastModelPos.x += (targetX - lastModelPos.x) * 0.1;
lastModelPos.y += (targetY - lastModelPos.y) * 0.1;
lastModelPos.z = targetZ;
let angleDiff = targetAngle - lastModelAngle;
if (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
if (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
lastModelAngle += angleDiff * 0.1;
}
if (model) {
model.position.copy(lastModelPos);
model.rotation.set(0, 0, 0);
model.rotateZ(lastModelAngle);
model.rotateX(Math.PI / 2);
}
const targetCameraPos = new THREE.Vector3(
lastModelPos.x - direction.x * CAMERA_DISTANCE + direction.y * CAMERA_OFFSET,
lastModelPos.y - direction.y * CAMERA_DISTANCE - direction.x * CAMERA_OFFSET,
lastModelPos.z + CAMERA_HEIGHT
);
if (!currentCameraPos) {
currentCameraPos = targetCameraPos.clone();
}
currentCameraPos.lerp(targetCameraPos, CAMERA_SMOOTH);
engine.camera.position.copy(currentCameraPos);
engine.camera.lookAt(lastModelPos.x, lastModelPos.y, lastModelPos.z + 5);
engine.camera.up.set(0, 0, 1);
if (progress >= 1) {
progress = 0;
currentPointIndex = nextIndex;
}
}
animationFrameId = requestAnimationFrame(animate);
}
数据加载
加载data/lychee.geojson中的数据,绘制飞线、调用动画
async function loadData() {
try {
// 读取geojson文件
const response = await fetch('data/lychee.geojson');
const geojsonData = await response.json();
// 设定高程
const terrainProvider = await Cesium.createWorldTerrainAsync();
// 将坐标转换为Cartographic对象列表
const positions = geojsonData.features[0].geometry.coordinates.map(coord =>
Cesium.Cartographic.fromDegrees(coord[0], coord[1])
);
try {
const updatedPositions = await Cesium.sampleTerrainMostDetailed(terrainProvider, positions, true);
// 更新geojson中的坐标,添加高程信息
geojsonData.features[0].geometry.coordinates = geojsonData.features[0].geometry.coordinates.map((coord, index) => {
return [coord[0]- map_bias_x, coord[1]- map_bias_y, updatedPositions[index].height + 25];
});
console.log(geojsonData.features[0].geometry.coordinates);
}
catch (error) {
console.error('Error sampling terrain:', error);
}
// 使用修改后的geojson数据创建数据源
const dataSource = mapvthree.GeoJSONDataSource.fromGeoJSON(geojsonData);
flyline.dataSource = dataSource;
line.dataSource = dataSource;
// 保存坐标数据用于动画
coords = geojsonData.features[0].geometry.coordinates;
// 延时2秒后开始动画,等待底图渲染
console.log('等待2秒让底图渲染...');
setTimeout(() => {
console.log('开始动画');
animate();
}, 3000);
} catch (error) {
console.error('Error loading data:', error);
}
}
loadData();
释放资源
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
if (mixer) {
mixer.stopAllAction();
}
engine.dispose();
};
一些参数设置
const MOVEMENT_SPEED = 300; // 移动速度
const CAMERA_DISTANCE = 500; // 后方距离
const CAMERA_HEIGHT = 300; // 相机高度
const CAMERA_OFFSET = 0; // 相机水平偏移
const CAMERA_SMOOTH = 0.05; // 相机平滑系数
const MAX_DELTA_TIME = 0.05; // 最大时间步长
const modelScale = 20; // 模型缩放比例
const map_bias_x = 0.0118; // 经度偏移
const map_bias_y = 0.0028; // 纬度偏移