import { deepMerge, wgs84ToGcj02Format } from "../util"; import SijiEntity from "../base/siji_entity"; import CesiumEntity from "../base/cesium_entity"; export default (hnMap: any) => { const defaultOption = { id: "", position: [], speed: 10, replaySpeed: 20, clockLoop: false, image: { src: "", width: 10, height: 10, }, camera: { type: "", pitch: -30, radius: 500, }, polyline: { color: "#0000ff", width: 2, }, path: { color: "#ff0000", width: 2, }, label: { text: "", // 文本内容 font_size: 16, color: "#ffffff", outline: false, outlineColor: "#000000", outlineWidth: 1, outlineOpacity: 1, // 描边透明度 horizontalOrigin: "left", // 水平对齐方式 verticalOrigin: "bottom", // 垂直对齐方式 pixelOffset: [0, 0], }, }; class mars3d_class { type: any = "route"; id: any = null; option: any = JSON.parse(JSON.stringify(defaultOption)); config: any = null; graphic: any = null; constructor(option: any) { this.id = option.id; deepMerge(this.option, option); this.config = this.formatConfig(this.option); this.graphic = new mars3d.graphic.FixedRoute(this.config); } formatConfig(option: any) { return { id: option.id, speed: option.speed, positions: option.position, // leadTime: option.leadTime, clockLoop: option.clockLoop, // 是否循环播放 camera: { type: option.camera.type, // 轨迹类型 pitch: option.camera.pitch, radius: option.camera.radius, }, billboard: { image: option.image.src, width: option.image.width, height: option.image.height, }, // Marker polyline: { color: option.polyline.color, width: option.polyline.width, // showAll: true, }, path: { color: option.path.color, width: option.path.width, }, // 轨迹 label: { text: option.label.text, // 文本内容 font_size: option.label.textSize, color: option.label.color, outline: option.label.outline, outlineColor: option.label.outLineColor, outlineWidth: option.label.outlineWidth, outlineOpacity: option.label.outlineOpacity, // 描边透明度 horizontalOrigin: option.label.horizontalOrigin, // 水平对齐方式 verticalOrigin: option.label.verticalOrigin, // 垂直对齐方式 pixelOffset: option.label.pixelOffset, }, // model: { // url: option.models.url, // 模型地址 // scale: option.models.scale, // 模型缩放比例 // minimumPixelSize: option.models.minimumPixelSize, // 模型最小尺寸 // clampToGround: option.models.clampToGround, // 模型贴地 // }, attr: option.data, }; } set(option: any) { deepMerge(this.option, option); this.config = this.formatConfig(this.option); this.graphic.setOptions(this.config); } start() { this.graphic.start(); } pause() { this.graphic.pause(); } proceed() { this.graphic.proceed(); } stop() { this.graphic.stop(); } destroy() { this.graphic.destroy(); } flyTo() { this.graphic.flyToPoint(); } } class gaode_class { id: any = null; option: any = JSON.parse(JSON.stringify(defaultOption)); config_graphic: any = null; config_path: any = null; graphic: any = null; pathLayer: any = null; path: any = null; constructor(option: any) { this.id = option.id; deepMerge(this.option, option); this.config_graphic = this.formatConfigGraphic(this.option); this.config_path = this.formatConfigPath(this.option); this.graphic = new AMap.Marker(this.config_graphic); this.addPath(); this.graphic.on("moving", (e: any) => { hnMap.map.map.setCenter(e.target.getPosition(), true); }); hnMap.map.map.setFitView(); } formatConfigGraphic(option: any) { const amapPosition = wgs84ToGcj02Format(option.position); return { position: amapPosition[0], speed: option.speed, icon: new AMap.Icon({ image: option.image.src, imageSize: new AMap.Size(option.image.width, option.image.height), size: new AMap.Size(option.image.width, option.image.height), }), extData: { id: option.id, }, }; } formatConfigPath(option: any) { const amapPosition = wgs84ToGcj02Format(option.position); return { id: option.id + "_path", position: amapPosition, width: option.polyline.width, color: option.polyline.color, data: { id: option.id + "_path" }, }; } set(option: any) { deepMerge(this.option, option); this.config_graphic = this.formatConfigGraphic(this.option); this.config_path = this.formatConfigPath(this.option); this.graphic.setOptions(this.config_graphic); this.stop(); } addPath() { this.pathLayer = hnMap.map.addLayer( new hnMap.Layer({ id: this.id + "_path_layer" }) ); if (this.pathLayer) { this.path = new hnMap.Line(this.config_path); this.pathLayer.addEntity(this.path); } } start() { this.config_path.position.reduce((prev: any, next: any) => { let t1 = turf.point(prev); let t2 = turf.point(next); let distance = turf.distance(t1, t2, { units: "meters" }); this.graphic.moveAlong(this.path.graphic._opts.path, { duration: Number(distance / this.config_graphic.speed).toFixed(0), autoRotation: true, circlable: true, }); return next; }); } pause() { this.graphic.pauseMove(); } proceed() { this.graphic.resumeMove(); } stop() { this.graphic.stopMove(); } destroy() { this.stop(); this.graphic.remove(); this.pathLayer.destroy(); } flyTo() { if (this.graphic.getBounds) { const bounds = this.graphic.getBounds(); hnMap.map.map.setBounds(bounds); } } } class siji_class extends SijiEntity { type: any = "route"; id: any = null; option: any = JSON.parse(JSON.stringify(defaultOption)); config_routeline: any = null; config_routeplay: any = null; imgMarker: any = null; chunkData: any = null; // timer: any = null; constructor(option: any) { super(hnMap); this.id = option.id; deepMerge(this.option, option); this.config_routeline = this.formatConfig(this.option); this.config_routeplay = this.formatConfigPlay(this.option); } formatConfig(option: any) { return { id: "route-line_" + option.id, type: "line", source: { type: "geojson", data: { geometry: { type: "LineString", coordinates: option.position, }, properties: {}, type: "Feature", }, }, layout: { "line-join": "round", "line-cap": "round", }, paint: { "line-color": option.polyline.color, "line-width": Number(option.polyline.width), }, }; } formatConfigPlay(option: any) { return { id: "route-played_" + option.id, type: "line", source: { type: "geojson", data: { type: "FeatureCollection", features: [], }, }, layout: { "line-join": "round", "line-cap": "round", }, paint: { "line-color": option.path.color, "line-width": Number(option.path.width), }, }; } createCar() { if (this.imgMarker) { this.imgMarker.remove(); } // 画marker点 var el = document.createElement("img"); el.src = this.option.image.src; el.style.width = this.option.image.width + "px"; el.style.height = this.option.image.height + "px"; this.imgMarker = new SGMap.Marker(el) .setLngLat(this.config_routeline.source.data.geometry.coordinates[0]) .addTo(hnMap.map.map); } joinLinePoint() { const speed = 60; // 播放速度,这里选择20倍播放速度 const replaySpeed = 20; // 按照实际车辆跑的速度,每秒跑 60 / 3600 km // 车辆位置按每秒刷新20帧算,1/20秒移动的距离为 60 / 3600 / 20 km const step = 60 / 3600 / 20; // 倍数播放时,每帧移动的步长为 step * replaySpeed const scaleSpeed = step * replaySpeed; // 线路根据步长scaleSpeed间隔进行插值,插值线总长/步长(distance/scaleSpeed)个点,得到chunkData // 注意:线路长度一定,scaleSpeed越小,插值数量越多。线路长度很长的情况下,请调整replaySpeed,避免插值点数过多产生卡顿 this.chunkData = turf.lineChunk( this.config_routeline.source.data, scaleSpeed, { units: "kilometers", } ); return this.chunkData; } start() { const features = this.chunkData.features; let max = features.length; let order = 0; let timer = setInterval(() => { order++; if (order >= max) { clearInterval(timer); } else { const beforePoint = features[order - 1].geometry.coordinates[1]; const nowPoint = features[order].geometry.coordinates[1]; // 设置小车的位置 this.imgMarker.setLngLat(nowPoint); // 设置小车的方向 var bearing = turf.bearing( turf.point(beforePoint), turf.point(nowPoint) ); bearing && this.imgMarker.setRotation(bearing - 90); // 更新已走过的线路图层数据 this.readerReplayedRouterLayer(order, this.option); } }, 1000 / 20); // 间隔1000 / 20执行一次,每秒执行20次,可以和上面计算线路的时间间隔配合 } readerReplayedRouterLayer(order: any, option: any) { hnMap.map.map.getSource("route-played_" + option.id).setData({ type: "FeatureCollection", features: this.chunkData.features.slice(0, order + 1), }); } set(option: any) { deepMerge(this.option, option); if (this.imgMarker) { this.imgMarker.remove(); } this.createCar(); this.config_routeline = this.formatConfig(this.option); let mySource_routeline = hnMap.map.map.getSource( this.config_routeline.id ); mySource_routeline.setData({ geometry: { type: "LineString", coordinates: option.position, }, properties: option.data, type: "Feature", }); for (let key in this.config_routeline) { if (this.config_routeline.hasOwnProperty(key)) { if (key == "paint") { for (let key2 in this.config_routeline[key]) { if (this.config_routeline[key].hasOwnProperty(key2)) { // 遍历 paint 属性 hnMap.map.map.setPaintProperty( this.config_routeline.id, key2, this.config_routeline[key][key2] ); } } } } } this.config_routeplay = this.formatConfigPlay(this.option); let mySource_routeplay = hnMap.map.map.getSource( this.config_routeplay.id ); mySource_routeplay.setData({ geometry: { type: "LineString", coordinates: option.position, }, properties: option.data, type: "Feature", }); for (let key in this.config_routeplay) { if (this.config_routeplay.hasOwnProperty(key)) { if (key == "paint") { for (let key2 in this.config_routeplay[key]) { if (this.config_routeplay[key].hasOwnProperty(key2)) { // 遍历 paint 属性 hnMap.map.map.setPaintProperty( this.config_routeplay.id, key2, this.config_routeplay[key][key2] ); } } } } } this.joinLinePoint(); this.start(); } } // 新增 Cesium 漫游类 class cesium_class extends CesiumEntity { type: any = "route"; id: any = null; option: any = JSON.parse(JSON.stringify(defaultOption)); config_polyline: any = null; config_path: any = null; config_billboard: any = null; config_label: any = null; routeEntity: any = null; // 移动实体 pathEntity: any = null; // 已走过路径实体 polylineEntity: any = null; // 整个路径实体 clock: any = null; // Cesium时钟 positionProperty: any = null; // 位置属性 startTime: any = null; // 开始时间 stopTime: any = null; // 结束时间 isPlaying: boolean = false; // 播放状态 currentTime: any = null; // 当前时间 graphic: any = null; // 👇 新增:让 flyTo() 能识别这是“线”类型 polyline: any = null; // 符合 cesium_entity.ts 的判断条件 constructor(option: any) { super(hnMap); this.id = option.id; deepMerge(this.option, option); // this.graphic = this; const [lng, lat, alt] = this.option.position[0]; const positions = this.convertPositions(this.option.position); if (positions.length < 2) { console.error("Route needs at least 2 positions"); return; } // // 先初始化一个安全的 positionProperty(即使后面会被替换) this.positionProperty = new Cesium.ConstantPositionProperty( Cesium.Cartesian3.fromDegrees(lng, lat, alt) ); // // 1. 创建整个路径线(polyline) // this.config_polyline = this.formatConfig_polyline(this.option); // this.polylineEntity = new Cesium.Entity(this.config_polyline); // 👇 关键:将 polyline 数据挂到 this 上,供 flyTo 使用 this.polyline = { positions: positions, // 注意:这里是普通数组,不是 Property! }; this.createRoute(); } // 将经纬度数组转换为 Cesium Cartesian3 数组 convertPositions(positions: any[]) { if (!positions || positions.length === 0) { return []; } return positions.map((pos) => { if (pos.length === 2) { return Cesium.Cartesian3.fromDegrees(pos[0], pos[1], 0); } else if (pos.length === 3) { return Cesium.Cartesian3.fromDegrees(pos[0], pos[1], pos[2]); } return Cesium.Cartesian3.fromDegrees(0, 0, 0); }); } formatConfig_polyline(option: any) { const positions = this.convertPositions(this.option.position); return { id: `${option.id}_polyline`, polyline: { positions: positions, width: Number(option.polyline.width), material: Cesium.Color.fromCssColorString(option.polyline.color), clampToGround: true, }, properties: option.data || {}, // customData: this.option.data, }; } formatConfig_path(option: any) { const positions = this.convertPositions(this.option.position); // 2. 创建已走过路径线 - 使用有效的位置数组 // 注意:不能传入空数组!至少要有一个有效位置 const initialPathPositions = positions.length > 0 ? [positions[0]] : [ Cesium.Cartesian3.fromDegrees(117.220356, 31.833959, 43.67), Cesium.Cartesian3.fromDegrees(117.220361, 31.835111, 44.36), ]; return { id: `${option.id}_path`, polyline: { positions: initialPathPositions, // 静态位置, // 初始为空 width: Number(option.path.width), material: Cesium.Color.fromCssColorString(option.path.color), clampToGround: true, }, }; } formatConfig_billboard(option: any) { return { id: option.id, position: this.positionProperty, orientation: new Cesium.VelocityOrientationProperty( this.positionProperty ), billboard: { image: option.image.src, width: Number(option.image.width) || 32, height: Number(option.image.height) || 32, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, }, label: option.label.text ? { text: option.label.text, font: `${option.label.textSize || 16}px sans-serif`, fillColor: Cesium.Color.fromCssColorString( option.label.color || "#9e6969ff" ), outline: option.label.outline, outlineColor: option.label.outline ? Cesium.Color.fromCssColorString( option.label.outlineColor ).withAlpha(Number(option.label.outlineOpacity) || 1) : undefined, outlineWidth: option.label.outline ? Number(option.label.outlineWidth) : 0, style: Cesium.LabelStyle.FILL_AND_OUTLINE, verticalOrigin: Cesium.VerticalOrigin[ option.label.verticalOrigin?.toUpperCase() || "CENTER" ], horizontalOrigin: Cesium.HorizontalOrigin[ option.label.horizontalOrigin?.toUpperCase() || "CENTER" ], pixelOffset: new Cesium.Cartesian2( option.label.pixelOffset?.[0] || 0, option.label.pixelOffset?.[1] || 0 ), } : undefined, properties: option.data || {}, }; } // 创建漫游 createRoute() { const positions = this.convertPositions(this.option.position); console.log("positions:", positions); // 1. 创建整个路径线(polyline) this.config_polyline = this.formatConfig_polyline(this.option); this.polylineEntity = new Cesium.Entity(this.config_polyline); hnMap.map.map.entities.add(this.polylineEntity); // 2. 创建已走过路径线(初始为空) this.config_path = this.formatConfig_path(this.option); this.pathEntity = new Cesium.Entity(this.config_path); hnMap.map.map.entities.add(this.pathEntity); // 3. 计算路径长度和速度 const totalDistance = this.calculatePathDistance(positions); console.log("总距离:", totalDistance); const totalTime = totalDistance / this.option.speed; // 以米/秒计算 // 4. 创建采样位置属性 const startTime = Cesium.JulianDate.now(); this.startTime = startTime; const stopTime = Cesium.JulianDate.addSeconds( startTime, totalTime, new Cesium.JulianDate() ); this.stopTime = stopTime; // 创建采样位置 this.positionProperty = new Cesium.SampledPositionProperty(); // 创建一个空的位置属性 // 均匀采样,每秒10个点 const sampleCount = Math.ceil(totalTime) * 10; for (let i = 0; i <= sampleCount; i++) { const fraction = i / sampleCount; // 时间间隔 const time = Cesium.JulianDate.addSeconds( this.startTime, totalTime * fraction, new Cesium.JulianDate() ); const position = this.getPositionAtFraction(positions, fraction); this.positionProperty.addSample(time, position); } // 5. 创建移动实体 this.createMovingEntity(positions[0]); // 6. 配置时钟 if (this.option.clockLoop) { hnMap.map.map.clock.multiplier = this.option.replaySpeed || 20; hnMap.map.map.clock.shouldAnimate = false; } // 7. 监听时间变化更新已走过路径 this.setupPathUpdate(); } // 计算路径总距离 calculatePathDistance(positions: any[]) { let totalDistance = 0; for (let i = 1; i < positions.length; i++) { totalDistance += Cesium.Cartesian3.distance( positions[i - 1], positions[i] ); } return totalDistance; } // 根据比例获取路径上的位置 getPositionAtFraction(positions: any[], fraction: number) { if (fraction <= 0) return positions[0]; if (fraction >= 1) return positions[positions.length - 1]; // 计算每个段落的长度 const segmentLengths = []; let totalLength = 0; for (let i = 1; i < positions.length; i++) { const segmentLength = Cesium.Cartesian3.distance( positions[i - 1], positions[i] ); segmentLengths.push(segmentLength); totalLength += segmentLength; } // 找到目标段落 const targetDistance = fraction * totalLength; let accumulatedDistance = 0; for (let i = 0; i < segmentLengths.length; i++) { if (targetDistance <= accumulatedDistance + segmentLengths[i]) { const segmentFraction = (targetDistance - accumulatedDistance) / segmentLengths[i]; return Cesium.Cartesian3.lerp( positions[i], positions[i + 1], segmentFraction, new Cesium.Cartesian3() ); } accumulatedDistance += segmentLengths[i]; } return positions[positions.length - 1]; } // 创建移动实体 createMovingEntity(startPosition: any) { this.config_billboard = this.formatConfig_billboard(this.option); this.routeEntity = new Cesium.Entity(this.config_billboard); hnMap.map.map.entities.add(this.routeEntity); // 设置相机跟随 if (this.option.camera.type) { this.setupCamera(); } } // 设置相机 setupCamera() { const cameraType = this.option.camera.type; const pitch = Cesium.Math.toRadians(this.option.camera.pitch || -30); const radius = this.option.camera.radius || 500; if (cameraType === "follow") { // 跟随模式 hnMap.map.map.trackedEntity = this.routeEntity; } else if (cameraType === "orbit") { // 轨道模式 const scene = hnMap.map.map.scene; scene.screenSpaceCameraController.enableTilt = false; // 监听位置变化更新相机 hnMap.map.map.clock.onTick.addEventListener(() => { if (this.routeEntity && this.isPlaying) { const position = this.routeEntity.position.getValue( hnMap.map.map.clock.currentTime ); if (position) { const destination = Cesium.Cartesian3.add( position, new Cesium.Cartesian3( radius * Math.cos(pitch), radius * Math.sin(pitch), 0 ), new Cesium.Cartesian3() ); hnMap.map.map.camera.setView({ destination: destination, orientation: { heading: 0, pitch: pitch, roll: 0, }, }); } } }); } } // 设置路径更新 setupPathUpdate() { hnMap.map.map.clock.onTick.addEventListener(() => { if (this.isPlaying && this.routeEntity) { this.updatePath(); } }); } // 更新已走过路径 updatePath() { if (!this.routeEntity || !this.positionProperty) return; const currentTime = hnMap.map.map.clock.currentTime; const start = Cesium.JulianDate.toDate(this.startTime).getTime(); const now = Cesium.JulianDate.toDate(currentTime).getTime(); const total = Cesium.JulianDate.toDate(this.stopTime).getTime() - start; const elapsed = Math.min(now - start, total); const fraction = elapsed / total; // 获取当前和之前的位置 const positions = this.convertPositions(this.option.position); const pathPositions = []; // 添加已走过的位置 const sampleCount = Math.ceil(fraction * 100); // 最多100个点 for (let i = 0; i <= sampleCount; i++) { const sampleFraction = (i / sampleCount) * fraction; if (sampleFraction > 0) { const position = this.getPositionAtFraction( positions, sampleFraction ); pathPositions.push(position); } } // 添加当前位置 const currentPosition = this.routeEntity.position.getValue(currentTime); if (currentPosition) { pathPositions.push(currentPosition); } // 更新路径实体 if (this.pathEntity && pathPositions.length > 1) { this.pathEntity.polyline.positions = pathPositions; } } // 设置属性 set(option: any) { deepMerge(this.option, option); // 停止当前播放 if (this.isPlaying) { this.stop(); } // 移除现有实体 if (this.routeEntity) { hnMap.map.map.entities.remove(this.routeEntity); } if (this.pathEntity) { hnMap.map.map.entities.remove(this.pathEntity); } if (this.polylineEntity) { hnMap.map.map.entities.remove(this.polylineEntity); } // 重新创建 this.createRoute(); // 如果之前是播放状态,重新开始 if (this.isPlaying) { this.start(); } } // 开始漫游 // start() { // if (!this.routeEntity) return; // this.isPlaying = true; // // 设置时钟 // if (this.option.clockLoop) { // hnMap.map.map.clock.shouldAnimate = true; // hnMap.map.map.clock.currentTime = this.startTime; // } else { // // 一次性播放 // hnMap.map.map.clock.shouldAnimate = true; // hnMap.map.map.clock.currentTime = this.startTime; // // 监听播放结束 // const listener = hnMap.map.map.clock.onTick.addEventListener(() => { // const currentTime = hnMap.map.map.clock.currentTime; // if (Cesium.JulianDate.compare(currentTime, this.stopTime) >= 0) { // this.stop(); // hnMap.map.map.clock.onTick.removeEventListener(listener); // } // }); // } // } start() { // if (!this.routeEntity) return; // this.isPlaying = true; // // // ✅ 启用 Cesium 时间动画 // hnMap.map.map.clock.shouldAnimate = true; // hnMap.map.map.clock.currentTime = this.startTime; // // // 如果需要循环播放 // if (this.option.clockLoop) { // hnMap.map.map.clock.multiplier = this.option.replaySpeed || 20; // } } // 暂停漫游 pause() { this.isPlaying = false; hnMap.map.map.clock.shouldAnimate = false; } // 继续漫游 proceed() { this.isPlaying = true; hnMap.map.map.clock.shouldAnimate = true; } // 停止漫游 stop() { this.isPlaying = false; hnMap.map.map.clock.shouldAnimate = false; // 重置到开始位置 hnMap.map.map.clock.currentTime = this.startTime; // 清空已走过路径 if (this.pathEntity) { this.pathEntity.polyline.positions = []; } } // 销毁漫游 destroy() { this.stop(); if (this.routeEntity) { hnMap.map.map.entities.remove(this.routeEntity); } if (this.pathEntity) { hnMap.map.map.entities.remove(this.pathEntity); } if (this.polylineEntity) { hnMap.map.map.entities.remove(this.polylineEntity); } this.routeEntity = null; this.pathEntity = null; this.polylineEntity = null; this.positionProperty = null; this.isPlaying = false; } // route.ts - cesium_class flyTo(option: any = {}) { if (this.polyline?.positions?.length > 0) { const bs = Cesium.BoundingSphere.fromPoints(this.polyline.positions); // 获取包围球 this.hnMap.map.map.camera.flyToBoundingSphere(bs, { duration: option.duration || 2.0, offset: new Cesium.HeadingPitchRange( 0, -Cesium.Math.PI_OVER_FOUR, bs.radius * 2 ), }); } } } const fn: any = { mars3d: mars3d_class, gaode: gaode_class, siji: siji_class, cesium: cesium_class, }; return fn[hnMap.mapType]; };