Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/woka/cesium-map-object into ctw

# Conflicts:
#	ruoyi-ui/src/views/cesiumMap/index.vue
ctw
cuitw 2 weeks ago
parent
commit
1a19a74a3b
  1. 13
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomPlatformIconController.java
  2. 85
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  3. 165
      ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlatformStyleDTO.java
  4. 9
      ruoyi-ui/src/api/system/routes.js
  5. 18
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  6. 1992
      ruoyi-ui/src/views/cesiumMap/index.vue
  7. 103
      ruoyi-ui/src/views/childRoom/index.vue

13
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomPlatformIconController.java

@ -2,6 +2,7 @@ package com.ruoyi.web.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
@ -21,6 +22,9 @@ public class RoomPlatformIconController extends BaseController {
@Autowired
private IRoomPlatformIconService roomPlatformIconService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 按房间ID查询该房间下所有地图平台图标不分页
*/
@ -53,12 +57,19 @@ public class RoomPlatformIconController extends BaseController {
}
/**
* 删除
* 删除同步删除该实例在 Redis 中的平台样式避免残留威力区/探测区
*/
@PreAuthorize("@ss.hasPermi('system:roomPlatformIcon:remove')")
@Log(title = "房间地图平台图标", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public AjaxResult remove(@PathVariable Long id) {
RoomPlatformIcon icon = roomPlatformIconService.selectById(id);
if (icon != null && icon.getRoomId() != null) {
String key = "room:" + icon.getRoomId() + ":platformIcons:platforms";
redisTemplate.opsForHash().delete(key, String.valueOf(id));
String oldKey = "room:" + icon.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(id));
}
return toAjax(roomPlatformIconService.deleteById(id));
}
}

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

@ -58,34 +58,97 @@ public class RoutesController extends BaseController
/**
* 保存平台样式到 Redis
* 独立平台routeId=0若传 platformIconInstanceId 则按实例存储每个拖上去的图标一套样式否则按 platformId 存储兼容旧逻辑
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/savePlatformStyle")
public AjaxResult savePlatformStyle(@RequestBody PlatformStyleDTO dto)
{
if (dto.getRoomId() == null || dto.getRouteId() == null || dto.getPlatformId() == null) {
if (dto.getRoomId() == null || dto.getRouteId() == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + dto.getRoomId() + ":route:" + dto.getRouteId() + ":platforms";
redisTemplate.opsForHash().put(key, String.valueOf(dto.getPlatformId()), JSON.toJSONString(dto));
Long routeId = dto.getRouteId();
boolean isPlatformIcon = routeId != null && routeId == 0L;
if (isPlatformIcon) {
if (dto.getPlatformIconInstanceId() == null && dto.getPlatformId() == null) {
return AjaxResult.error("独立平台样式需传 platformIconInstanceId 或 platformId");
}
} else {
if (dto.getPlatformId() == null) {
return AjaxResult.error("参数不完整");
}
}
String key = isPlatformIcon
? ("room:" + dto.getRoomId() + ":platformIcons:platforms")
: ("room:" + dto.getRoomId() + ":route:" + routeId + ":platforms");
String hashField = isPlatformIcon && dto.getPlatformIconInstanceId() != null
? String.valueOf(dto.getPlatformIconInstanceId())
: String.valueOf(dto.getPlatformId());
redisTemplate.opsForHash().put(key, hashField, JSON.toJSONString(dto));
if (isPlatformIcon && dto.getPlatformIconInstanceId() != null) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, hashField);
} else if (isPlatformIcon) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(dto.getPlatformId()));
}
return success();
}
/**
* Redis 获取平台样式
* 独立平台routeId=0优先用 platformIconInstanceId 取该实例样式未传时用 platformId兼容旧数据
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/getPlatformStyle")
public AjaxResult getPlatformStyle(PlatformStyleDTO dto)
{
if (dto.getRoomId() == null || dto.getRouteId() == null || dto.getPlatformId() == null) {
if (dto.getRoomId() == null || dto.getRouteId() == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + dto.getRoomId() + ":route:" + dto.getRouteId() + ":platforms";
Object val = redisTemplate.opsForHash().get(key, String.valueOf(dto.getPlatformId()));
if (val != null) {
return success(JSON.parseObject(val.toString(), PlatformStyleDTO.class));
Long routeId = dto.getRouteId();
boolean isPlatformIcon = routeId != null && routeId == 0L;
if (isPlatformIcon) {
if (dto.getPlatformIconInstanceId() == null && dto.getPlatformId() == null) {
return AjaxResult.error("独立平台样式需传 platformIconInstanceId 或 platformId");
}
} else {
if (dto.getPlatformId() == null) {
return AjaxResult.error("参数不完整");
}
}
String key = isPlatformIcon
? ("room:" + dto.getRoomId() + ":platformIcons:platforms")
: ("room:" + dto.getRoomId() + ":route:" + routeId + ":platforms");
String hashField = isPlatformIcon && dto.getPlatformIconInstanceId() != null
? String.valueOf(dto.getPlatformIconInstanceId())
: String.valueOf(dto.getPlatformId());
Object val = redisTemplate.opsForHash().get(key, hashField);
if (val == null && isPlatformIcon) {
String oldKey = "room:" + dto.getRoomId() + ":route:0:platforms";
val = redisTemplate.opsForHash().get(oldKey, hashField);
}
if (val != null) return success(JSON.parseObject(val.toString(), PlatformStyleDTO.class));
return success();
}
/**
* 删除独立平台图标样式按实例删除避免删除图标后仍残留威力区/探测区
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@DeleteMapping("/platformIconStyle")
public AjaxResult deletePlatformIconStyle(@RequestParam Long roomId, @RequestParam Long platformIconInstanceId)
{
if (roomId == null || platformIconInstanceId == null) {
return AjaxResult.error("roomId 与 platformIconInstanceId 不能为空");
}
String key = "room:" + roomId + ":platformIcons:platforms";
redisTemplate.opsForHash().delete(key, String.valueOf(platformIconInstanceId));
String oldKey = "room:" + roomId + ":route:0:platforms";
redisTemplate.opsForHash().delete(oldKey, String.valueOf(platformIconInstanceId));
return success();
}
@ -440,10 +503,16 @@ public class RoutesController extends BaseController
int rows = routesService.deleteRoutesByIds(ids);
if (rows > 0) {
for (Long routeId : ids) {
// 清除该航线在所有房间下的导弹参数
Set<String> keys = redisTemplate.keys("missile:params:*:" + routeId + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
// 同步删除该航线在所有房间下的平台样式(探测区/威力区等),避免库表已删但 Redis 仍残留
Set<String> styleKeys = redisTemplate.keys("room:*:route:" + routeId + ":platforms");
if (styleKeys != null && !styleKeys.isEmpty()) {
redisTemplate.delete(styleKeys);
}
}
}
return toAjax(rows);

165
ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlatformStyleDTO.java

@ -1,6 +1,7 @@
package com.ruoyi.system.domain.dto;
import java.io.Serializable;
import java.util.List;
/**
* 平台样式 DTO
@ -11,7 +12,10 @@ public class PlatformStyleDTO implements Serializable {
private String roomId;
private Long routeId;
private Long platformId;
/** 独立平台图标实例 ID(拖到地图上的每条记录的 id,非平台类型 id)。routeId=0 时用此字段作 Redis hash field,实现每个实例独立样式 */
private Long platformIconInstanceId;
/** 平台名称 */
private String platformName;
@ -27,6 +31,12 @@ public class PlatformStyleDTO implements Serializable {
/** 平台颜色 */
private String platformColor;
/** 多探测区配置(支持叠加与独立显隐) */
private List<DetectionZoneDTO> detectionZones;
/** 多威力区配置(支持叠加与独立显隐) */
private List<PowerZoneDTO> powerZones;
/** 探测区半径(千米),整圆 */
private Double detectionZoneRadius;
/** 探测区填充颜色 */
@ -47,6 +57,151 @@ public class PlatformStyleDTO implements Serializable {
/** 威力区是否在地图上显示 */
private Boolean powerZoneVisible;
public static class DetectionZoneDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** zone 唯一 id(前端生成,后端原样保存) */
private String zoneId;
/** 探测区半径(千米) */
private Double radiusKm;
/** 探测区填充颜色(css rgba 或 hex) */
private String color;
/** 探测区透明度 0-1 */
private Double opacity;
/** 是否在地图上显示 */
private Boolean visible;
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public Double getRadiusKm() {
return radiusKm;
}
public void setRadiusKm(Double radiusKm) {
this.radiusKm = radiusKm;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Double getOpacity() {
return opacity;
}
public void setOpacity(Double opacity) {
this.opacity = opacity;
}
public Boolean getVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
}
public static class PowerZoneDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** zone 唯一 id(前端生成,后端原样保存) */
private String zoneId;
/** 威力区半径(千米) */
private Double radiusKm;
/** 威力区扇形夹角(度) */
private Double angleDeg;
/** 威力区填充颜色(css rgba 或 hex) */
private String color;
/** 威力区透明度 0-1 */
private Double opacity;
/** 是否在地图上显示 */
private Boolean visible;
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public Double getRadiusKm() {
return radiusKm;
}
public void setRadiusKm(Double radiusKm) {
this.radiusKm = radiusKm;
}
public Double getAngleDeg() {
return angleDeg;
}
public void setAngleDeg(Double angleDeg) {
this.angleDeg = angleDeg;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Double getOpacity() {
return opacity;
}
public void setOpacity(Double opacity) {
this.opacity = opacity;
}
public Boolean getVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
}
public List<DetectionZoneDTO> getDetectionZones() {
return detectionZones;
}
public void setDetectionZones(List<DetectionZoneDTO> detectionZones) {
this.detectionZones = detectionZones;
}
public List<PowerZoneDTO> getPowerZones() {
return powerZones;
}
public void setPowerZones(List<PowerZoneDTO> powerZones) {
this.powerZones = powerZones;
}
public String getRoomId() {
return roomId;
}
@ -71,6 +226,14 @@ public class PlatformStyleDTO implements Serializable {
this.platformId = platformId;
}
public Long getPlatformIconInstanceId() {
return platformIconInstanceId;
}
public void setPlatformIconInstanceId(Long platformIconInstanceId) {
this.platformIconInstanceId = platformIconInstanceId;
}
public String getPlatformName() {
return platformName;
}

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

@ -65,6 +65,15 @@ export function getPlatformStyle(query) {
})
}
// 删除独立平台图标样式(按实例删除,避免删除图标后残留威力区/探测区)
export function deletePlatformIconStyle(roomId, platformIconInstanceId) {
return request({
url: '/system/routes/platformIconStyle',
method: 'delete',
params: { roomId, platformIconInstanceId }
})
}
// 保存4T数据到Redis(禁用防重复提交,因拖拽/调整大小可能快速连续触发保存)
export function save4TData(data) {
return request({

18
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -504,6 +504,24 @@
</div>
</div>
<!-- 探测区单个区 -->
<div class="menu-section" v-if="entityData && entityData.type === 'detectionZone'">
<div class="menu-title">探测区</div>
<div class="menu-item menu-item-sub" @click="handleToggleDetectionZone">
<span class="menu-icon">{{ detectionZoneVisible ? '👁' : '👁‍🗨' }}</span>
<span>{{ detectionZoneVisible ? '隐藏该探测区' : '显示该探测区' }}</span>
</div>
</div>
<!-- 威力区单个区 -->
<div class="menu-section" v-if="entityData && entityData.type === 'powerZone'">
<div class="menu-title">威力区</div>
<div class="menu-item menu-item-sub" @click="handleTogglePowerZone">
<span class="menu-icon">{{ powerZoneVisible ? '👁' : '👁‍🗨' }}</span>
<span>{{ powerZoneVisible ? '隐藏该威力区' : '显示该威力区' }}</span>
</div>
</div>
<!-- 白板平台仅显示伸缩框用于旋转 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && entityData.isWhiteboard">
<div class="menu-title">白板平台</div>

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

File diff suppressed because it is too large

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

@ -2114,12 +2114,19 @@ export default {
const styleRes = await getPlatformStyle({ roomId: rId, routeId, platformId: route.platformId });
if (styleRes.data && this.$refs.cesiumMap) {
this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
if (styleRes.data.detectionZoneVisible !== false && styleRes.data.detectionZoneRadius != null && Number(styleRes.data.detectionZoneRadius) > 0) {
this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, styleRes.data.detectionZoneRadius, styleRes.data.detectionZoneColor || 'rgba(0, 150, 255, 0.35)', styleRes.data.detectionZoneOpacity);
}
if (styleRes.data.powerZoneVisible !== false && styleRes.data.powerZoneRadius != null && Number(styleRes.data.powerZoneRadius) > 0) {
this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, styleRes.data.powerZoneRadius, styleRes.data.powerZoneAngle ?? 120, styleRes.data.powerZoneColor || 'rgba(255, 0, 0, 0.3)', styleRes.data.powerZoneOpacity);
}
const normalized = this.$refs.cesiumMap.normalizeZonesFromStyle(styleRes.data);
(normalized.detectionZones || []).forEach(dz => {
if (!dz) return;
if (dz.radiusKm == null || Number(dz.radiusKm) <= 0) return;
const opacity = dz.visible === false ? 0 : dz.opacity;
this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, dz.zoneId, dz.radiusKm, dz.color, opacity);
});
(normalized.powerZones || []).forEach(pz => {
if (!pz) return;
if (pz.radiusKm == null || Number(pz.radiusKm) <= 0) return;
const opacity = pz.visible === false ? 0 : pz.opacity;
this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, pz.zoneId, pz.radiusKm, pz.angleDeg ?? 120, pz.color, opacity);
});
}
} catch (_) {}
}
@ -2470,6 +2477,26 @@ export default {
});
this.routes = allRoutes;
// / routeId
// activeRouteIds routes 线
const existingIdSet = new Set(allRoutes.map(r => String(r.id)));
const missingRouteIds = (this.activeRouteIds || []).filter(id => !existingIdSet.has(String(id)));
if (missingRouteIds.length > 0 && this.$refs.cesiumMap) {
missingRouteIds.forEach((routeId) => {
// removeRouteById 线//
this.$refs.cesiumMap.removeRouteById(routeId);
// /removeRouteById
this.$refs.cesiumMap.removeDetectionZoneByRouteId(routeId);
this.$refs.cesiumMap.removePowerZoneByRouteId(routeId);
});
}
this.activeRouteIds = (this.activeRouteIds || []).filter(id => existingIdSet.has(String(id)));
// 线/
if (this.selectedRouteId != null && !existingIdSet.has(String(this.selectedRouteId))) {
this.selectedRouteId = null;
this.selectedRouteDetails = null;
}
// 线 roomId
if (this.activeRouteIds.length > 0 && this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
@ -4010,7 +4037,7 @@ export default {
map.setPlatformIconServerId(entityData.id, res.data.id, this.currentRoomId)
entityData.serverId = res.data.id
entityData.roomId = this.currentRoomId
this.$message.success('平台图标已保存到当前房间')
//
this.wsConnection?.sendSyncPlatformIcons?.()
}
} catch (e) {
@ -5542,6 +5569,62 @@ export default {
if (cache) {
routeIdToTimeline[routeId] = cache;
routeIdsWithTimeline.push(routeId);
//
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = cache;
const routeName = route.name || `航线${route.id}`;
(earlyArrivalLegs || []).forEach(leg => {
const earlyMin = leg.earlyMinutes != null ? Math.round(leg.earlyMinutes * 10) / 10 : 0;
const earlyStr = earlyMin >= 0.1 ? `${earlyMin} 分钟` : `${Math.round(earlyMin * 60)}`;
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)';
const kTimeStr = this.minutesToStartTime(leg.scheduled);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'early_arrival',
title: '提前到达',
routeName,
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
time: this.minutesToStartTime(leg.actualArrival),
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`,
severity: 'high'
});
});
(lateArrivalLegs || []).forEach(leg => {
const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : '';
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚';
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'late_arrival',
title: '无法按时到达',
routeName,
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`,
severity: 'high'
});
});
(holdDelayConflicts || []).forEach(conf => {
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'hold_delay',
title: '盘旋时间不足',
routeName,
routeIds: [routeId],
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: `实际切出将延迟 ${conf.delaySeconds} 秒。① 延长该盘旋点相对K时 ② 定时盘旋调转弯半径,非定时调上一航点速度或本点相对K时 ③ 微调上下游航点相对K时`,
severity: 'high',
holdCenter: conf.holdCenter,
positionLng: conf.holdCenter && conf.holdCenter.lng,
positionLat: conf.holdCenter && conf.holdCenter.lat,
positionAlt: conf.holdCenter && conf.holdCenter.alt
});
});
} else {
let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
@ -5554,7 +5637,11 @@ export default {
routeIdToTimeline[routeId] = {
segments: timeline.segments,
path: pathData && pathData.path ? pathData.path : null,
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null,
//
earlyArrivalLegs: timeline.earlyArrivalLegs || [],
lateArrivalLegs: timeline.lateArrivalLegs || [],
holdDelayConflicts: timeline.holdDelayConflicts || []
};
this._setConflictTimelineCache(routeId, route.waypoints, minMinutes, maxMinutes, routeIdToTimeline[routeId]);
routeIdsWithTimeline.push(routeId);

Loading…
Cancel
Save