本项目代码使用umi+react+高德地图amap+threejs开发的一款景区管理项目,其中模型使用svg文件并嵌入高德地图中,实现新增景区、绑定店铺、绑定点位设施等功能,使用高德地图的导航功能,可以直接导航到景区内。页面内容包含景区的创建和景区大屏展示,在大屏展示中可以对商铺和点位进行绑定,另页面中集成了coze的ai智能系统,可进行对话,绑定商铺活动或者一些可自动化生产的内容,将在视频演示中详细讲解。
·高德地图AMAP
·THREE.JS
·indexDB
·react + UMI
环境
·nodejs: v18.19.0
·umi: 4.1.8
·react: 18.0.33
·threejs: 0.152.2
·@amap/amap-jsapi-loader: "^1.0.1"
1、创建高德地图实例
import AMapLoader from '@amap/amap-jsapi-loader';
export const CreateAMap = () => {
return new Promise((resolve, reject) => {
// 申请好的Web端开发者Key,首次调用 load 时必填
AMapLoader.load({
"key": "**************",
"version": "2.0",
// 需要使用的的插件列表,如比例尺'AMap.Scale'等,
"plugins": ["AMap.Walking", "AMap.Driving"],
"Loca": {
version: '2.0.0'
},
}).then(async (res) => {
resolve(res)
}).catch((error) => {
reject(error)
})
})
}
useAsyncEffect(async () => {
AMapRef.current = await CreateAMap()
if (containerRef.current) {
createMap()
}
}, [])
在实例创建好以后就该绘制地图了,绘制地图使用实例中的 AMAP.Map方法,接受两个参数,第一个是地图容器,第二个是配置项:new AMap.Map(div: (String | HTMLDivElement), opts: MapOptions)。
const createMap = async () => {
let AMap = AMapRef.current
// 创建地图
var map = new AMap.Map("container", {
resizeEnable: true,
center: [119.986, 30.2235],//地图中心点
zoom: 17.4, //地图显示的缩放级别
viewMode: '3D',//开启3D视图,默认为关闭
buildingAnimation: true,//楼块出现是否带动画
pitch: 45,
rotation: 45,
features: ['bg', 'building'], // 只显示建筑、道路、区域
// showLabel: false, // 隐藏标注信息
mapStyle: "amap://styles/grey",
showIndoorMap: false,
// rotateEnable: false,
// pitchEnable: false,
zIndex: 9
});
mapRef.current = map
var loca = new (window as any).Loca.Container({
map,
zIndex: 9
});
locaRef.current = loca
……
}
首先说明,本人不是UI,只是一个技术死宅,对于美术上的事儿一窍不通,在做的过程中,就有一种感觉,我加入的3d景区,还不如高德原本的模型好看~,勿喷。
3、清除多余楼块
export const cleanBuild = (AMap: any, map: any) => {
// 底图楼块扣除
var building = new AMap.Buildings({
zIndex: 10,
});
building.setStyle({
hideWithoutStyle: false,//是否隐藏设定区域外的楼块
areas: [{
visible: false,//是否可见
rejectTexture: false,//是否屏蔽自定义地图的纹理
color1: '00000000',//楼顶颜色
color2: '00000000',//楼面颜色
path: [ClearBuildPoint]
}]
});
map.add(building);
}
首先在高德地图绘制threejs图层,所需的内容和直接绘制threejs场景是相同的,首先需要有镜头Camera、场景Scene、渲染器Rende、灯光Light,这些内容都可以在THREEJS的官网找到具体的api,这里不赘述。
1、threejs内置到高德地图
项目中选择的threejs版本是0.152.2,我也尝试过160+版本,但是存在很多兼容性的问题,所以退而求其次,选择之前用过的版本,相对稳定点也熟悉点,在高德地图嵌入threejs,就离不开一个API AMap.GLCustomLayer 自由数据图层,所有的3d场景都将在这个layer内绘制。
export const createScene = (AMap: any, map: any, css2dRenderDom?: any) => {
customCoords = map.customCoords;
const center = map.getCenter()
customCoords.setCenter([center.lng, center.lat]);
return new Promise((resolve, reject) => {
var gllayer = new AMap.GLCustomLayer({
zIndex: 110, // 图层的层级
init: async (gl) => {
……
},
render: () => {
……
},
})
map.add(gllayer);
})
}
camera = new PerspectiveCamera(60, window.innerWidth / window.innerHeight, 100, 1 << 30);
renderer = new WebGLRenderer({
context: gl
});
context - 可用于将渲染器附加到已有的渲染环境(RenderingContext)中。默认值是null高德地图jsApi的使用
render回调
// 重新设置图层的渲染中心点,将模型等物体的渲染中心点重置
// 否则和 LOCA 可视化等多个图层能力使用的时候会出现物体位置偏移的问题
customCoords.setCenter([116.271363, 39.992414]);
var { near, far, fov, up, lookAt, position } = customCoords.getCameraParams();
// 2D 地图下使用的正交相机
// var { near, far, top, bottom, left, right, position, rotation } = customCoords.getCameraParams();
// 这里的顺序不能颠倒,否则可能会出现绘制卡顿的效果。
camera.near = near;
camera.far = far;
camera.fov = fov;
camera.position.set(...position);
camera.up.set(...up);
camera.lookAt(...lookAt);
camera.updateProjectionMatrix();
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
// 这里必须执行!!重新设置 three 的 gl 上下文状态。
renderer.resetState();
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader';
const svgLoader = new SVGLoader()
export function loadSVG(url: string) {
return new Promise((res, reg) => {
svgLoader.load(url, (data: any) => {
res(data)
})
})
}
export const getSVG2Model = async (url: string,back=false): Promise<THREE.Group> => {
const svgPaths = await loadSVG(url) as { paths: ShapePath[] }
const floorGroup = new THREE.Group()
for (const path of svgPaths.paths) {
console.dir(path)
const shapes = SVGLoader.createShapes(path);
// 获取路径
for (const shape of shapes) {
const mesh = paths2Mesh(shape, path.userData.node.id)
……
floorGroup.add(mesh)
}
}
return floorGroup
}
// 路径挤压为模型
const paths2Mesh = (shape: THREE.Shape | THREE.Shape[], name: string): THREE.Mesh => {
const extrudeSettings = {
depth: name==='floor'?8:getRandomInt(6,60),
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const mesh = new THREE.Mesh(geometry, unBindStoreMaterial.clone());
if(name === 'floor') {
mesh.receiveShadow = true
} else {
mesh.castShadow = true
}
mesh.name = name
const box3Info = getBox3Info(mesh);
mesh.userData.box3Info = box3Info
return mesh
}
1、点击地图
map.on('click', async (e: any) => {
console.log(e.lnglat.lng, e.lnglat.lat);
const lnglat = [e.lnglat.lng, e.lnglat.lat]
map.render();
})
2、点击模型
……
const mouse = rayState.mouse.clone()
// 高德地图点击事件与3d模型的转换
mouse.x = (e.originEvent.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.originEvent.clientY / window.innerHeight) * 2 + 1;
……
rayState.raycaster.setFromCamera(mouse, camera);
const rallyist: THREE.Intersection<THREE.Object3D<any>>[] = rayState.raycaster.intersectObjects(floorGroupChildrenRef.current);
if (rallyList?.[0] && rallyList[0].object) {
message.success('点击在3d模型上')
const mesh = rallyList[0].object
if (mesh.name !== 'floor') {
// 点击在商铺楼层,用于绑定建筑
} else {
// 点击在地板上,用于绑定设施
}
} else {
message.success('点击在高德地图上')
}
以上,我们便在点击地图的时候区分点击的是高德地图还是3d模型,以便后续的功能开发。
……
if (rallyList?.[0] && rallyList[0].object) {
console.log('模型世界坐标', rallyList[0]?.point);
const viewPoint = new Vector2() // 屏幕坐标
getViewCp(rallyList[0]?.point, viewPoint, camera)
console.log('世界坐标转屏幕坐标', viewPoint);
const lnglat = new Vector2()
coordsToLngLats(viewPoint, AMapRef.current, mapRef.current, lnglat)
const beforelnglat = [e.lnglat.lng, e.lnglat.lat]
console.log('点击获取的经纬度', beforelnglat)
console.log('转化后的经纬度', lnglat);
……
上面的代码用到了两个方法,一个是getViewCp世界坐标转屏幕坐标,另一个是coordsToLngLats屏幕坐标转经纬度,这个是高德地图提供的api。
2、屏幕坐标转经纬度
// 屏幕坐标转经纬度
export const coordsToLngLats = (point: THREE.Vector2, AMap: any, map: any, targetV2?: THREE.Vector2) => {
const { x, y } = point
var pixel = new AMap.Pixel(x, y);
var lnglat = map.containerToLngLat(pixel);
const v2 = new THREE.Vector2()
v2.set(lnglat.lng, lnglat.lat)
if (targetV2?.isVector2) {
targetV2.copy(v2)
}
return v2
}
AMap.Pixel是专门提供给用户获取像素点的。
像素坐标,确定地图上的一个像素点。
3、世界坐标转屏幕坐标
// 世界坐标转屏幕坐标
export const getViewCp = (v3: THREE.Vector3, v2: THREE.Vector2, camera: THREE.Camera) => {
var worldVector = v3.clone();
var standardVector = worldVector.project(camera); //世界坐标转标准设备坐标
var a = window.innerWidth / 2;
var b = window.innerHeight / 2;
var vx = Math.round(standardVector.x * a + a); //标准设备坐标转屏幕坐标
var vy = Math.round(-standardVector.y * b + b); //标准设备坐标转屏幕坐标
const p = new THREE.Vector2(vx, vy)
v2.copy(p)
return p
}
const position = new AMap.LngLat(markData?.lnglat?.[0], markData?.lnglat?.[1]); //Marker 经纬度
const element = document.createElement('div');
const root = ReactDOM.createRoot(element);
root.render(getComponent());
const marker = new AMap.Marker({
position: position,
content: element, //将 html 传给 content
offset: new AMap.Pixel(-iconStype.width / 2, -iconStype.height / 2), //以 icon 的 [center bottom] 为原点
});
map.add(marker);
const getComponent = ()=>{
return <div className="bind-store" style={{ ...iconStype }} onClick={checkMark} >
<div className="img-main"><img src={logo} alt="" /></div>
<p>{name}</p>
</div>
}
这样便将一个用react组件写的标记添加到高德地图中,顺便提一嘴,如果不是在AMAP中添加标记,而是在threejs的css3drender中添加标记 也是同样的道理,需要将react组件转成html标识或者dom节点,所以个人标识不喜欢用虚拟dom的框架开发threejs,太繁琐。
src\utils\indexedDB\index.ts indexDB配置文件 src\request\index.ts 数据请求配置文件 src\request\floor.ts 景区数据操作 src\request\point.ts 设施点位数据操作 src\request\store.ts 建筑数据操作
新增/删除楼层
绑定建筑
// 弹窗确定按钮
const add = () => {
form.validateFields().then((values) => {
if (storeState.isEdit) {
editStore(storeState.storeId || '', values)
} else {
if (props.addId) {
addStore({
...values,
lnglat,
storeId: props.addId
})
}
}
})
}
// 添加接口
const { run: addStore } = useRequest(createStoreInfo, {
manual: true,
onSuccess: (res) => {
if (res.success) {
props?.refreshStore && props?.refreshStore(floorId)
form.resetFields()
message.success(res.msg)
cancel()
}
}
})
绑定点位
AIBOT
效果图
另注:coze的aibot将在8月15日后进行限流访问,可以在代码中替换自己的ai智慧体,高德地图的api也是限流的,超过额度就不可使用了,尽量在项目中使用自己申请的key。
https://www.aspiringcode.com/content?id=17227021262564&uid=b4bfbecae1b542ebb15f421d4852a362
仅代表作者个人观点