Browse Source

4T功能

mh
cuitw 1 month ago
parent
commit
e310220fce
  1. 44
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  2. 2
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
  3. 17
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
  4. 19
      ruoyi-ui/src/api/system/routes.js
  5. 104
      ruoyi-ui/src/views/cesiumMap/index.vue
  6. 36
      ruoyi-ui/src/views/childRoom/index.vue

44
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java

@ -23,6 +23,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.dto.PlatformStyleDTO;
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* 实体部署与航线Controller
@ -40,6 +41,10 @@ public class RoutesController extends BaseController
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
@Qualifier("fourTRedisTemplate")
private RedisTemplate<String, String> fourTRedisTemplate;
/**
* 保存平台样式到 Redis
*/
@ -74,6 +79,45 @@ public class RoutesController extends BaseController
}
/**
* 保存4T数据到 Redis按房间存储
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/save4TData")
public AjaxResult save4TData(@RequestBody java.util.Map<String, Object> params)
{
Object roomId = params.get("roomId");
Object data = params.get("data");
if (roomId == null || data == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + String.valueOf(roomId) + ":4t";
fourTRedisTemplate.opsForValue().set(key, data.toString());
return success();
}
/**
* Redis 获取4T数据
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/get4TData")
public AjaxResult get4TData(Long roomId)
{
if (roomId == null) {
return AjaxResult.error("房间ID不能为空");
}
String key = "room:" + String.valueOf(roomId) + ":4t";
String val = fourTRedisTemplate.opsForValue().get(key);
if (val != null && !val.isEmpty()) {
try {
return success(JSON.parseObject(val));
} catch (Exception e) {
return success(val);
}
}
return success();
}
/**
* 查询实体部署与航线列表
*/
@PreAuthorize("@ss.hasPermi('system:routes:list')")

2
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java

@ -9,6 +9,7 @@ import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
@ -32,6 +33,7 @@ import com.ruoyi.system.domain.SysCache;
public class CacheController
{
@Autowired
@Qualifier("stringRedisTemplate")
private RedisTemplate<String, String> redisTemplate;
private final static List<SysCache> caches = new ArrayList<SysCache>();

17
ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java

@ -63,6 +63,23 @@ public class RedisConfig extends CachingConfigurerSupport
return template;
}
/**
* 纯字符串 RedisTemplate用于存储 4T JSON 字符串避免 FastJson 序列化问题
* 命名为 fourTRedisTemplate 避免与 Spring Boot 自带的 stringRedisTemplate 冲突
*/
@Bean("fourTRedisTemplate")
public RedisTemplate<String, String> fourTRedisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public DefaultRedisScript<Long> limitScript()
{

19
ruoyi-ui/src/api/system/routes.js

@ -60,3 +60,22 @@ export function getPlatformStyle(query) {
params: query
})
}
// 保存4T数据到Redis(禁用防重复提交,因拖拽/调整大小可能快速连续触发保存)
export function save4TData(data) {
return request({
url: '/system/routes/save4TData',
method: 'post',
data,
headers: { repeatSubmit: false }
})
}
// 从Redis获取4T数据
export function get4TData(params) {
return request({
url: '/system/routes/get4TData',
method: 'get',
params
})
}

104
ruoyi-ui/src/views/cesiumMap/index.vue

@ -473,6 +473,8 @@ export default {
waypointDragging: null,
waypointDragPending: null,
WAYPOINT_DRAG_THRESHOLD_PX: 8,
/** 虚拟点(entry/exit)拖拽时的实时预览:{ routeId, dbId, position },拖拽时用 position 作为航点中心重算弧线 */
waypointDragPreview: null,
/** 拖拽航点前相机 enableInputs 状态,松开时恢复 */
waypointDragCameraInputsEnabled: undefined,
lastClickWasDrag: false,
@ -1840,6 +1842,7 @@ export default {
//线
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
this.waypointDragPreview = null;
// 线
const lineId = `route-line-${routeId}`;
const existingLine = this.viewer.entities.getById(lineId);
@ -1909,11 +1912,12 @@ export default {
});
if (!this._routeWaypointIdsByRoute) this._routeWaypointIdsByRoute = {};
this._routeWaypointIdsByRoute[routeId] = waypoints.map((wp) => wp.id);
// i 线
// i 线 45°
const isTurnWaypointWithArc = (i) => {
if (i < 1 || i >= waypoints.length - 1) return false;
const wp = waypoints[i];
if (this.getWaypointRadius(wp) <= 0) return false;
const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length);
if (this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle }) <= 0) return false;
const nextPos = originalPositions[i + 1];
let nextLogical = nextPos;
if (this.isHoldWaypoint(waypoints[i + 1])) {
@ -1924,7 +1928,7 @@ export default {
}
return !!nextLogical;
};
// ++
// + entry/exit 线
waypoints.forEach((wp, index) => {
const pos = originalPositions[index];
if (this.isHoldWaypoint(wp)) {
@ -1956,6 +1960,8 @@ export default {
});
return;
}
// 线 entry/exit dbId
if (isTurnWaypointWithArc(index)) return;
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}`,
name: wp.name || `WP${index + 1}`,
@ -2250,7 +2256,8 @@ export default {
});
lastPos = exit;
} else {
const radius = this.getWaypointRadius(wp);
const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length);
const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle });
let nextLogical = nextPos;
if (nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1])) {
const holdParams = this.parseHoldParams(waypoints[i + 1]);
@ -2259,22 +2266,42 @@ export default {
: this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180);
}
if (i < waypoints.length - 1 && radius > 0 && nextLogical) {
const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius);
const lastPosCloned = Cesium.Cartesian3.clone(lastPos);
const currPosCloned = Cesium.Cartesian3.clone(currPos);
const nextLogicalCloned = Cesium.Cartesian3.clone(nextLogical);
const routeIdCloned = routeId;
const dbIdCloned = wp.id;
const that = this;
const getArcPoints = () => {
const center = (that.waypointDragPreview && that.waypointDragPreview.routeId === routeIdCloned && that.waypointDragPreview.dbId === dbIdCloned)
? that.waypointDragPreview.position : currPosCloned;
return that.computeArcPositions(lastPosCloned, center, nextLogicalCloned, radius);
};
// 线 CallbackProperty
this.viewer.entities.add({
id: `arc-line-${routeId}-${i}`,
polyline: { positions: arcPoints, width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 },
show: false,
polyline: {
positions: new Cesium.CallbackProperty(getArcPoints, false),
width: lineWidth,
material: lineMaterial,
arcType: Cesium.ArcType.NONE,
zIndex: 20
},
properties: { routeId: routeId }
});
// 线dbId wp.id
// CallbackProperty线
const wpName = wp.name || `WP${i + 1}`;
const arcEntry = arcPoints[0];
const arcExit = arcPoints[arcPoints.length - 1];
[arcEntry, arcExit].forEach((pos, idx) => {
[0, 1].forEach((idx) => {
const suffix = idx === 0 ? '_entry' : '_exit';
const getPos = () => {
const pts = getArcPoints();
return idx === 0 ? pts[0] : pts[pts.length - 1];
};
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}${suffix}`,
name: wpName,
position: pos,
position: new Cesium.CallbackProperty(getPos, false),
properties: {
isMissionWaypoint: true,
routeId: routeId,
@ -2298,6 +2325,7 @@ export default {
}
});
});
const arcPoints = getArcPoints();
finalPathPositions.push(...arcPoints);
lastPos = arcPoints[arcPoints.length - 1];
} else {
@ -2326,14 +2354,14 @@ export default {
}
}
},
/** 从各航点/弧线/盘旋实体取当前位置,供主航线折线实时连线(拖拽时动态跟随) */
/** 从各航点/弧线/盘旋实体取当前位置,供主航线折线实时连线(拖拽时动态跟随)。转弯处优先用弧线,不经过航点中心,只展示转弯半径 */
getRouteLinePositionsFromWaypointEntities(routeId) {
const ids = this._routeWaypointIdsByRoute && this._routeWaypointIdsByRoute[routeId];
if (!ids || !ids.length || !this.viewer) return null;
const now = Cesium.JulianDate.now();
const positions = [];
for (let i = 0; i < ids.length; i++) {
// 线线穿线
// 线
const holdEnt = this.viewer.entities.getById(`hold-line-${routeId}-${i}`);
if (holdEnt && holdEnt.polyline && holdEnt.polyline.positions) {
const arr = holdEnt.polyline.positions.getValue(now);
@ -2342,14 +2370,7 @@ export default {
continue;
}
}
const ent = this.viewer.entities.getById(`wp_${routeId}_${ids[i]}`);
if (ent && ent.position) {
const pos = ent.position.getValue(now);
if (pos) {
positions.push(Cesium.Cartesian3.clone(pos));
continue;
}
}
// 线线线线
const arcEnt = this.viewer.entities.getById(`arc-line-${routeId}-${i}`);
if (arcEnt && arcEnt.polyline && arcEnt.polyline.positions) {
const arr = arcEnt.polyline.positions.getValue(now);
@ -2358,10 +2379,23 @@ export default {
continue;
}
}
const ent = this.viewer.entities.getById(`wp_${routeId}_${ids[i]}`);
if (ent && ent.position) {
const pos = ent.position.getValue(now);
if (pos) {
positions.push(Cesium.Cartesian3.clone(pos));
continue;
}
}
}
return positions.length > 0 ? positions : null;
},
/** 渲染/路径用:非首尾航点默认转弯坡度 45°,首尾为 0 */
getEffectiveTurnAngle(wp, index, waypointsLength) {
if (index === 0 || index === waypointsLength - 1) return wp.turnAngle != null ? wp.turnAngle : 0;
return wp.turnAngle != null ? wp.turnAngle : 45;
},
//
getWaypointRadius(wp) {
const speed = wp.speed || 800;
@ -2733,7 +2767,8 @@ export default {
? this.getCircleEntryPoint(originalPositions[i + 1], currPos, holdParams.radius)
: this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180);
}
const radius = this.getWaypointRadius(wp);
const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length);
const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle });
if (i < waypoints.length - 1 && radius > 0 && nextLogical) {
const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius);
arcPoints.forEach(p => path.push(toLngLatAlt(p)));
@ -3187,7 +3222,12 @@ export default {
}
const pos = this.getClickPositionWithHeight(movement.endPosition, this.waypointDragging.originalAlt);
if (pos) {
this.waypointDragging.entity.position = pos;
const entityId = this.waypointDragging.entity.id || '';
if (entityId.endsWith('_entry') || entityId.endsWith('_exit')) {
this.waypointDragPreview = { routeId: this.waypointDragging.routeId, dbId: this.waypointDragging.dbId, position: pos };
} else {
this.waypointDragging.entity.position = pos;
}
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
@ -3196,7 +3236,12 @@ export default {
if (this.waypointDragging) {
const pos = this.getClickPositionWithHeight(movement.endPosition, this.waypointDragging.originalAlt);
if (pos) {
this.waypointDragging.entity.position = pos;
const entityId = this.waypointDragging.entity.id || '';
if (entityId.endsWith('_entry') || entityId.endsWith('_exit')) {
this.waypointDragPreview = { routeId: this.waypointDragging.routeId, dbId: this.waypointDragging.dbId, position: pos };
} else {
this.waypointDragging.entity.position = pos;
}
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
return;
@ -3229,7 +3274,15 @@ export default {
const entity = this.waypointDragging.entity;
const routeId = this.waypointDragging.routeId;
const dbId = this.waypointDragging.dbId;
const pos = entity.position.getValue(Cesium.JulianDate.now());
const entityId = entity.id || '';
let pos;
if (entityId.endsWith('_entry') || entityId.endsWith('_exit')) {
pos = this.waypointDragPreview && this.waypointDragPreview.routeId === routeId && this.waypointDragPreview.dbId === dbId
? this.waypointDragPreview.position : entity.position.getValue(Cesium.JulianDate.now());
this.waypointDragPreview = null;
} else {
pos = entity.position.getValue(Cesium.JulianDate.now());
}
const ll = this.cartesianToLatLngAlt(pos);
if (ll) {
this.$emit('waypoint-position-changed', { dbId, routeId, lat: ll.lat, lng: ll.lng, alt: ll.alt });
@ -3237,6 +3290,7 @@ export default {
this.lastClickWasDrag = true;
this.waypointDragging = null;
}
this.waypointDragPreview = null;
this.waypointDragPending = null;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
} catch (error) {

36
ruoyi-ui/src/views/childRoom/index.vue

@ -367,6 +367,13 @@
@confirm="handleImportConfirm"
/>
<!-- 4T悬浮窗THREAT/TASK/TARGET/TACTIC- 仅点击方案或4T时渲染 -->
<four-t-panel
v-if="show4TPanel && !screenshotMode"
:visible.sync="show4TPanel"
:room-id="currentRoomId"
/>
<el-dialog
title="新建方案"
:visible.sync="showPlanNameDialog"
@ -424,6 +431,7 @@ import LeftMenu from './LeftMenu'
import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import FourTPanel from './FourTPanel'
import { listScenario, addScenario, delScenario } from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle } from "@/api/system/routes";
import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints";
@ -447,7 +455,8 @@ export default {
LeftMenu,
RightPanel,
BottomLeftPanel,
TopHeader
TopHeader,
FourTPanel
},
data() {
return {
@ -515,6 +524,7 @@ export default {
//
defaultMenuItems: [
{ id: 'file', name: '方案', icon: 'plan' },
{ id: '4t', name: '4T', icon: 'T' },
{ id: 'start', name: '冲突', icon: 'chongtu' },
{ id: 'insert', name: '平台', icon: 'el-icon-s-platform' },
{ id: 'pattern', name: '空域', icon: 'ky' },
@ -578,6 +588,8 @@ export default {
isRightPanelHidden: true, //
// K
showKTimePopup: false,
// 4T4T
show4TPanel: false,
// /
showAirport: true,
@ -2108,6 +2120,18 @@ export default {
} catch (e) { /* 解析失败保留默认 */ }
if (Array.isArray(arr) && arr.length > 0) {
const defaultMap = (this.defaultMenuItems || []).reduce((m, it) => { m[it.id] = it; return m }, {})
const savedIds = new Set(arr.map(i => i.id))
// 4T defaultMenuItems
const defaultOrder = (this.defaultMenuItems || []).map(d => d.id)
defaultOrder.forEach(defId => {
if (!savedIds.has(defId) && defaultMap[defId]) {
const insertAfterId = defaultOrder[defaultOrder.indexOf(defId) - 1]
const refIdx = insertAfterId ? arr.findIndex(i => i.id === insertAfterId) : -1
const insertIdx = refIdx >= 0 ? refIdx + 1 : 0
arr.splice(insertIdx, 0, { ...defaultMap[defId] })
savedIds.add(defId)
}
})
this.menuItems = arr.map(item => {
const def = defaultMap[item.id]
if (def) return { ...item, name: def.name, icon: def.icon, action: def.action }
@ -2399,21 +2423,25 @@ export default {
this.handleMenuAction(item.action)
}
//
if (item.id === 'file' || item.id === 'start' || item.id === 'insert') {
// 4T
if (item.id === 'file' || item.id === 'start' || item.id === 'insert' || item.id === '4t') {
this.drawDom = false;
this.airspaceDrawDom = false;
}
//
if (item.id === 'file') {
//
// 4T
this.show4TPanel = true;
if (this.activeRightTab === 'plan' && !this.isRightPanelHidden) {
this.isRightPanelHidden = true;
} else {
this.activeRightTab = 'plan';
this.isRightPanelHidden = false;
}
} else if (item.id === '4t') {
// 4T4T
this.show4TPanel = !this.show4TPanel;
} else if (item.id === 'start') {
//
if (this.activeRightTab === 'conflict' && !this.isRightPanelHidden) {

Loading…
Cancel
Save