From 6add28fdfb94576cbfeb860d3c9842e1c838966d Mon Sep 17 00:00:00 2001 From: MHW Date: Thu, 9 Apr 2026 10:22:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=81=AB=E5=8A=9B=E8=A7=84=E5=88=99=EF=BC=9A?= =?UTF-8?q?=E8=A3=85=E5=A4=87=E5=8C=B9=E9=85=8D=E8=A7=84=E5=88=99=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E7=9B=AE=E6=A0=87=E8=A7=84=E5=88=99=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E9=98=B5=E4=BD=8D=E8=A7=84=E5=88=99=E3=80=81?= =?UTF-8?q?=E8=88=AA=E8=BF=B9=E8=A7=84=E5=88=99=E3=80=90=E5=88=9D=E7=89=88?= =?UTF-8?q?=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/FireRuleExecuteTargetItemVO.java | 2 + .../ultimately/vo/FireRuleOutputVO.java | 51 +-- .../vo/FireRuleRedWeaponEquipmentVO.java | 8 - .../vo/FireRuleRouteTrackPointVO.java | 17 + .../ultimately/vo/FireRuleTrackParamVO.java | 41 ++ .../ultimately/vo/FireRuleTrackRouteVO.java | 28 ++ .../FireRuleRedWeaponOutputFillHelper.java | 397 ++++++++++++++++++ .../com/solution/rule/utils/RuleFunction.java | 20 + .../main/resources/json/new火力规则输出.json | 392 +++++++++++++++++ .../src/main/resources/rules/rule.drl | 39 ++ auto-solution-system/Dockerfile | 4 + 11 files changed, 967 insertions(+), 32 deletions(-) create mode 100644 auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRouteTrackPointVO.java create mode 100644 auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackParamVO.java create mode 100644 auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackRouteVO.java create mode 100644 auto-solution-rule/src/main/resources/json/new火力规则输出.json create mode 100644 auto-solution-system/Dockerfile diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleExecuteTargetItemVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleExecuteTargetItemVO.java index b337bc8..b1bbba1 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleExecuteTargetItemVO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleExecuteTargetItemVO.java @@ -45,6 +45,8 @@ public class FireRuleExecuteTargetItemVO { private String cruiseRouteId; + private String moveRouteId; + private List cruiseRouteOffset; private String fireType; diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleOutputVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleOutputVO.java index 38071b0..7235676 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleOutputVO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleOutputVO.java @@ -1,42 +1,45 @@ package com.solution.rule.domain.ultimately.vo; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; -/** - * 火力规则输出根文档(与 {@code 火力规则输出.json} 字段一一对应)。 - */ @Data @JsonIgnoreProperties(ignoreUnknown = true) public class FireRuleOutputVO { - - /** - * 来源场景文件名 - */ private String sourceFile; - - /** - * 火力规则任务列表(可含防区等输出字段) - */ private List fireRuleInputs; - - /** - * 场景任务节点列表 - */ @JsonProperty("Tasks") private List tasks; + @JsonProperty("TrackParam") + private FireRuleTrackParamVO trackParam; - /** - * 编组列表 - */ - @JsonProperty("Groups") - private List groups; + @JsonIgnore + public List getGroups() { + return trackParam == null ? null : trackParam.getGroups(); + } - /** - * 红方装备列表 - */ - private List redWeapons; + @JsonIgnore + public void setGroups(List groups) { + if (trackParam == null) { + trackParam = new FireRuleTrackParamVO(); + } + trackParam.setGroups(groups); + } + + @JsonIgnore + public List getRedWeapons() { + return trackParam == null ? null : trackParam.getRedWeapons(); + } + + @JsonIgnore + public void setRedWeapons(List redWeapons) { + if (trackParam == null) { + trackParam = new FireRuleTrackParamVO(); + } + trackParam.setRedWeapons(redWeapons); + } } diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java index 77269ac..8b721e1 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java @@ -6,9 +6,6 @@ import lombok.Data; import java.util.Map; -/** - * 火力规则输出中 redWeapons 数组的单项(红方装备/平台) - */ @Data @JsonIgnoreProperties(ignoreUnknown = true) public class FireRuleRedWeaponEquipmentVO { @@ -23,15 +20,10 @@ public class FireRuleRedWeaponEquipmentVO { private String platformType; private Boolean isStrikeTarget; - private Boolean isReconTarget; - private Boolean isInterferenceTarget; - private Boolean isDefendImportantPlace; - //命中率 private Double successTargetRad; - private String groupType; @JsonProperty("EquipmentID") diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRouteTrackPointVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRouteTrackPointVO.java new file mode 100644 index 0000000..71fa4fc --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRouteTrackPointVO.java @@ -0,0 +1,17 @@ +package com.solution.rule.domain.ultimately.vo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FireRuleRouteTrackPointVO { + private String index; + private String longitude; + private String latitude; + private String height; + private String speed; + private String psia; + private Integer time; + private String active; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackParamVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackParamVO.java new file mode 100644 index 0000000..2e99efb --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackParamVO.java @@ -0,0 +1,41 @@ +package com.solution.rule.domain.ultimately.vo; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FireRuleTrackParamVO { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private Map routeMap = new LinkedHashMap<>(); + + @JsonProperty("Groups") + private List groups; + + @JsonProperty("redWeapons") + private List redWeapons; + + @JsonAnySetter + public void putRoute(String key, JsonNode value) { + if ("Groups".equals(key) || "redWeapons".equals(key) || value == null) { + return; + } + routeMap.put(key, MAPPER.convertValue(value, FireRuleTrackRouteVO.class)); + } + + @JsonAnyGetter + public Map getRouteMap() { + return routeMap; + } +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackRouteVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackRouteVO.java new file mode 100644 index 0000000..6ea6d9c --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleTrackRouteVO.java @@ -0,0 +1,28 @@ +package com.solution.rule.domain.ultimately.vo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FireRuleTrackRouteVO { + private String name; + @JsonProperty("StartTime") + private Integer startTime; + @JsonProperty("EndTime") + private Integer endTime; + @JsonProperty("TrackType") + private String trackType; + @JsonProperty("HeightType") + private String heightType; + private String seaType; + @JsonProperty("TrackPoints") + private List trackPoints; + @JsonProperty("Color") + private String color; + @JsonProperty("PointCount") + private Integer pointCount; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java index 7c8776e..18be66b 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java @@ -18,8 +18,12 @@ import com.solution.rule.domain.ultimately.vo.FireRuleRedSubComponentsVO; import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponEquipmentVO; import com.solution.rule.domain.ultimately.vo.FireRuleRedPlatformSlotVO; import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponSlotVO; +import com.solution.rule.domain.ultimately.vo.FireRuleOutputVO; +import com.solution.rule.domain.ultimately.vo.FireRuleRouteTrackPointVO; import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskNodeVO; import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO; +import com.solution.rule.domain.ultimately.vo.FireRuleTrackParamVO; +import com.solution.rule.domain.ultimately.vo.FireRuleTrackRouteVO; import java.util.ArrayList; import java.util.Collections; @@ -170,6 +174,399 @@ public final class FireRuleRedWeaponOutputFillHelper { } } + /** + * 航迹规则:按蓝方 trackPoints 生成 {@link FireRuleTrackParamVO#routeMap} 条目(JSON 顶层 key=航迹 id), + * 并将同一 id 写入对应红方任务 {@code execute[0].targetList[*].moveRouteId}。多蓝方任务循环时 merge,不覆盖已有 id。 + */ + @SuppressWarnings("rawtypes") + public static void fillTrackParamAndBindMoveRoute(FireRuleOutputVO out, FireRuleTaskInputDTO blueTask, Map params) { + if (out == null || blueTask == null || out.getTasks() == null || out.getTasks().isEmpty()) { + return; + } + ensureTrackParam(out); + List tasks = out.getTasks(); + List reds = out.getRedWeapons(); + if (reds == null) { + reds = Collections.emptyList(); + } + List warPoly = toWarZonePolygon(blueTask.getWarZoneLocation()); + Coord blueAnchor = computeBlueAnchor(blueTask); + Coord clampAnchor = resolveClampAnchor(blueAnchor, warPoly); + boolean zoneClamp = Boolean.parseBoolean(String.valueOf(params.getOrDefault("enableTrackWarZoneClamp", true))); + + String dirMode = str(params, "trackPointDirectionMode", "head2next"); + double fallbackBrg = readDouble(params, "trackFallbackBearingDeg", readDouble(params, "fallbackBearingDeg", 0d)); + double mainBearing = computeTrackBearingDeg(blueTask, dirMode, fallbackBrg); + + String algo = str(params, "trackRouteAlgorithm", "followBlue"); + String nameSuffix = str(params, "trackRouteNameSuffix", "航迹"); + String groundType = str(params, "trackGroundTrackType", "routeLineGround"); + String blueIdSeg = sanitizeRouteIdSegment(nz(blueTask.getId())); + + for (int i = 0; i < tasks.size(); i++) { + FireRuleSceneTaskNodeVO node = tasks.get(i); + if (node == null) { + continue; + } + FireRuleRedWeaponEquipmentVO red = i < reds.size() ? reds.get(i) : null; + List nodes = extractSortedBlueTrackNodes(blueTask); + if (nodes.isEmpty()) { + continue; + } + List transformed = applyTrackRouteAlgorithm(nodes, algo, params, mainBearing, i); + int extraMax = readInt(params, "trackExtraNodesMax", 0); + if (extraMax > 0) { + transformed = injectExtraTrackNodes(transformed, extraMax); + } + if (zoneClamp && warPoly.size() >= 3) { + transformed = clampTrackNodesToWarZone(transformed, warPoly, clampAnchor); + } + boolean air = resolveAirTrack(blueTask, red, params); + String routeName = nz(node.getDrawName()) + nameSuffix; + String routeId = buildUniqueRouteId(out.getTrackParam().getRouteMap(), blueIdSeg, sanitizeRouteIdSegment(nz(node.getId())), i); + + FireRuleTrackRouteVO route = buildTrackRouteVo(transformed, routeName, air, groundType); + out.getTrackParam().getRouteMap().put(routeId, route); + bindMoveRouteIdToFirstExecute(node, routeId); + } + } + + private static void ensureTrackParam(FireRuleOutputVO out) { + if (out.getTrackParam() == null) { + out.setTrackParam(new FireRuleTrackParamVO()); + } + } + + private static String buildUniqueRouteId(Map existing, String blueSeg, String nodeSeg, int index) { + String base = "route_" + blueSeg + "_" + nodeSeg + "_" + index; + if (existing == null || !existing.containsKey(base)) { + return base; + } + int k = 1; + while (existing.containsKey(base + "_" + k)) { + k++; + } + return base + "_" + k; + } + + private static void bindMoveRouteIdToFirstExecute(FireRuleSceneTaskNodeVO node, String routeId) { + if (node == null || isBlank(routeId)) { + return; + } + FireRuleSceneTaskPayloadVO payload = node.getTask(); + if (payload == null || payload.getExecute() == null || payload.getExecute().isEmpty()) { + return; + } + FireRuleExecuteBlockVO block = payload.getExecute().get(0); + if (block == null || block.getTargetList() == null) { + return; + } + for (FireRuleExecuteTargetItemVO item : block.getTargetList()) { + if (item != null) { + item.setMoveRouteId(routeId); + } + } + } + + private static FireRuleTrackRouteVO buildTrackRouteVo(List nodes, String name, boolean air, String groundType) { + FireRuleTrackRouteVO vo = new FireRuleTrackRouteVO(); + vo.setName(name); + vo.setStartTime(null); + vo.setEndTime(null); + vo.setTrackType(air ? "routeLineAir" : groundType); + vo.setHeightType("msl"); + vo.setSeaType("seaLevel"); + List pts = new ArrayList<>(nodes.size()); + for (int j = 0; j < nodes.size(); j++) { + TrackNode tn = nodes.get(j); + FireRuleRouteTrackPointVO p = new FireRuleRouteTrackPointVO(); + p.setIndex(String.valueOf(j + 1)); + p.setLongitude(formatCoordNumber(tn.coord.lon)); + p.setLatitude(formatCoordNumber(tn.coord.lat)); + p.setHeight(tn.coord.height != null ? formatCoordNumber(tn.coord.height) : "0"); + p.setSpeed(tn.speed != null ? formatCoordNumber(tn.speed) : null); + pts.add(p); + } + vo.setTrackPoints(pts); + vo.setPointCount(pts.size()); + return vo; + } + + private static String formatCoordNumber(double v) { + return String.valueOf(v); + } + + private static boolean resolveAirTrack(FireRuleTaskInputDTO blue, FireRuleRedWeaponEquipmentVO red, Map params) { + String dt = blue != null ? nz(blue.getDataType()) : ""; + if (csvContainsAny(str(params, "trackAirDataTypeCsv", ""), dt, false)) { + return true; + } + String blob = nz(blue != null ? blue.getDrawName() : ""); + if (red != null) { + blob += " " + nz(red.getName()) + " " + nz(red.getPlatformType()); + } + return csvContainsAny(str(params, "trackAirKeywordsCsv", ""), blob, false); + } + + private static boolean csvContainsAny(String csv, String text, boolean tokenAsEquals) { + if (isBlank(csv) || text == null) { + return false; + } + String t = text.toLowerCase(); + for (String part : csv.split(",")) { + String k = part.trim(); + if (k.isEmpty()) { + continue; + } + if (tokenAsEquals) { + if (t.equalsIgnoreCase(k)) { + return true; + } + } else if (text.contains(k) || t.contains(k.toLowerCase())) { + return true; + } + } + return false; + } + + private static String sanitizeRouteIdSegment(String raw) { + if (raw == null || raw.isEmpty()) { + return "x"; + } + String s = raw.replaceAll("[^a-zA-Z0-9_-]", "_"); + return s.isEmpty() ? "x" : s; + } + + private static class TrackNode { + Coord coord; + Double speed; + TrackNode(Coord coord, Double speed) { + this.coord = coord; + this.speed = speed; + } + } + + private static List extractSortedBlueTrackNodes(FireRuleTaskInputDTO blueTask) { + List list = new ArrayList<>(); + if (blueTask == null || blueTask.getTrackPoints() == null) { + return list; + } + List pts = new ArrayList<>(blueTask.getTrackPoints()); + pts.sort((a, b) -> { + int ia = a != null && a.getIndex() != null ? a.getIndex() : 0; + int ib = b != null && b.getIndex() != null ? b.getIndex() : 0; + return Integer.compare(ia, ib); + }); + for (FireRuleTrackPointDTO p : pts) { + if (p == null || p.getLongitude() == null || p.getLatitude() == null) { + continue; + } + list.add(new TrackNode(new Coord(p.getLongitude(), p.getLatitude(), p.getHeight()), p.getSpeed())); + } + return list; + } + + private static List applyTrackRouteAlgorithm(List nodes, String algo, Map params, double mainBearing, int taskIndex) { + if (nodes == null || nodes.isEmpty()) { + return nodes; + } + String a = algo == null ? "followBlue" : algo.trim(); + if ("shortestPath".equalsIgnoreCase(a)) { + return applyShortestPath(nodes, params); + } + if ("flank".equalsIgnoreCase(a)) { + return applyFlank(nodes, params, taskIndex); + } + if ("jam".equalsIgnoreCase(a)) { + return applyJam(nodes, params, mainBearing); + } + return copyTrackNodes(nodes); + } + + private static List copyTrackNodes(List nodes) { + List out = new ArrayList<>(nodes.size()); + for (TrackNode n : nodes) { + out.add(new TrackNode(new Coord(n.coord.lon, n.coord.lat, n.coord.height), n.speed)); + } + return out; + } + + private static List applyShortestPath(List nodes, Map params) { + int segN = readInt(params, "trackShortPathSegments", 3); + if (segN < 1) { + segN = 1; + } + List out = new ArrayList<>(); + for (int i = 0; i < nodes.size(); i++) { + if (i == 0) { + out.add(copyNode(nodes.get(i))); + continue; + } + Coord a = nodes.get(i - 1).coord; + Coord b = nodes.get(i).coord; + Double spPrev = nodes.get(i - 1).speed; + Double spNext = nodes.get(i).speed; + for (int s = 1; s < segN; s++) { + double t = (double) s / (double) segN; + double lon = a.lon + (b.lon - a.lon) * t; + double lat = a.lat + (b.lat - a.lat) * t; + Double h = null; + if (a.height != null && b.height != null) { + h = a.height + (b.height - a.height) * t; + } else if (b.height != null) { + h = b.height; + } else { + h = a.height; + } + Double spd = null; + if (spPrev != null && spNext != null) { + spd = spPrev + (spNext - spPrev) * t; + } else if (spNext != null) { + spd = spNext; + } else { + spd = spPrev; + } + out.add(new TrackNode(new Coord(lon, lat, h), spd)); + } + out.add(copyNode(nodes.get(i))); + } + return out; + } + + private static TrackNode copyNode(TrackNode n) { + return new TrackNode(new Coord(n.coord.lon, n.coord.lat, n.coord.height), n.speed); + } + + private static List applyFlank(List nodes, Map params, int taskIndex) { + double offsetM = readDouble(params, "trackFlankOffsetMeters", 800d); + String sideMode = str(params, "trackFlankSideMode", "alternate"); + List out = new ArrayList<>(nodes.size()); + for (int i = 0; i < nodes.size(); i++) { + Coord c = nodes.get(i).coord; + double brg; + if (i < nodes.size() - 1) { + Coord nxt = nodes.get(i + 1).coord; + brg = bearingDeg(c.lon, c.lat, nxt.lon, nxt.lat); + } else if (i > 0) { + Coord prev = nodes.get(i - 1).coord; + brg = bearingDeg(prev.lon, prev.lat, c.lon, c.lat); + } else { + brg = 0d; + } + int sign = flankSign(sideMode, i, taskIndex); + Coord moved = moveByMeters(c, sign * offsetM, brg + 90d); + out.add(new TrackNode(moved, nodes.get(i).speed)); + } + return out; + } + + private static int flankSign(String mode, int pointIndex, int taskIndex) { + if ("left".equalsIgnoreCase(mode)) { + return 1; + } + if ("right".equalsIgnoreCase(mode)) { + return -1; + } + return (pointIndex + taskIndex) % 2 == 0 ? 1 : -1; + } + + private static List applyJam(List nodes, Map params, double mainBearing) { + double wobble = readDouble(params, "trackJamWobbleMeters", 400d); + double periods = readDouble(params, "trackJamSegments", 4d); + if (periods < 0.5d) { + periods = 0.5d; + } + int n = nodes.size(); + List out = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + double frac = n <= 1 ? 0d : (double) i / (double) (n - 1); + double lateral = wobble * Math.sin(2d * Math.PI * periods * frac); + Coord c = moveByMeters(nodes.get(i).coord, lateral, mainBearing + 90d); + out.add(new TrackNode(c, nodes.get(i).speed)); + } + return out; + } + + private static List injectExtraTrackNodes(List nodes, int extraMax) { + if (nodes == null || nodes.size() < 2 || extraMax <= 0) { + return nodes; + } + int segs = nodes.size() - 1; + List perSeg = new ArrayList<>(Collections.nCopies(segs, 0)); + int base = extraMax / segs; + int rem = extraMax % segs; + for (int s = 0; s < segs; s++) { + perSeg.set(s, base + (s < rem ? 1 : 0)); + } + List out = new ArrayList<>(); + for (int i = 0; i < nodes.size(); i++) { + out.add(copyNode(nodes.get(i))); + if (i >= nodes.size() - 1) { + break; + } + int ins = perSeg.get(i); + Coord a = nodes.get(i).coord; + Coord b = nodes.get(i + 1).coord; + Double sa = nodes.get(i).speed; + Double sb = nodes.get(i + 1).speed; + for (int k = 1; k <= ins; k++) { + double t = (double) k / (double) (ins + 1); + double lon = a.lon + (b.lon - a.lon) * t; + double lat = a.lat + (b.lat - a.lat) * t; + Double h = null; + if (a.height != null && b.height != null) { + h = a.height + (b.height - a.height) * t; + } else if (b.height != null) { + h = b.height; + } else { + h = a.height; + } + Double spd = null; + if (sa != null && sb != null) { + spd = sa + (sb - sa) * t; + } else if (sb != null) { + spd = sb; + } else { + spd = sa; + } + out.add(new TrackNode(new Coord(lon, lat, h), spd)); + } + } + return out; + } + + private static List clampTrackNodesToWarZone(List nodes, List poly, Coord anchor) { + List out = new ArrayList<>(nodes.size()); + for (TrackNode n : nodes) { + Coord c = n.coord; + if (!isPointInPolygon(c, poly)) { + c = projectInsidePolygon(c, anchor, poly); + } + out.add(new TrackNode(c, n.speed)); + } + return out; + } + + private static Coord resolveClampAnchor(Coord blueAnchor, List poly) { + if (poly.size() >= 3) { + if (blueAnchor != null && isPointInPolygon(blueAnchor, poly)) { + return blueAnchor; + } + return polygonCentroid(poly); + } + return blueAnchor != null ? blueAnchor : new Coord(0d, 0d, null); + } + + private static Coord polygonCentroid(List poly) { + double sx = 0, sy = 0; + for (Coord c : poly) { + sx += c.lon; + sy += c.lat; + } + int n = poly.size(); + return new Coord(sx / n, sy / n, null); + } + /** * 目标分配:为每个红方 Tasks 节点写入 task.execute[0].targetList[*].targetId。\n * targetId 来源:当前蓝方任务 {@link FireRuleTaskInputDTO#getTaskWeapons()} 下每条武器的 equipmentId。\n diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java index 90cb5c8..4074a9f 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java @@ -225,6 +225,26 @@ public final class RuleFunction { FireRuleRedWeaponOutputFillHelper.fillPlatformPositions(out.getRedWeapons(), fact.getTask(), p); } + /** + * 航迹规则:根据蓝方 trackPoints 与作战区生成 TrackParam 航迹,并绑定 execute.targetList.moveRouteId。 + */ + @SuppressWarnings("rawtypes") + public static void trackRoute(DroolsFact fact, Map globalParams) { + if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) { + return; + } + Map p = castParams(globalParams); + boolean enabled = Boolean.parseBoolean(String.valueOf(p.getOrDefault("trackRuleEnabled", true))); + if (!enabled) { + return; + } + FireRuleOutputVO out = fact.getFireRuleOutputVO(); + if (out.getTasks() == null || out.getTasks().isEmpty()) { + return; + } + FireRuleRedWeaponOutputFillHelper.fillTrackParamAndBindMoveRoute(out, fact.getTask(), p); + } + @SuppressWarnings("unchecked") private static Map castParams(Map raw) { return raw == null ? new java.util.HashMap<>() : (Map) raw; diff --git a/auto-solution-rule/src/main/resources/json/new火力规则输出.json b/auto-solution-rule/src/main/resources/json/new火力规则输出.json new file mode 100644 index 0000000..dca6702 --- /dev/null +++ b/auto-solution-rule/src/main/resources/json/new火力规则输出.json @@ -0,0 +1,392 @@ +{ + "sourceFile": "区域防空31111_2026-04-02 15_29_03.json", + "fireRuleInputs": [ + { + "taskWeapons": [ + { + "equipmentId": "40b341f6-d60a-4a29-b84d-2973a3065124", + "name": "导弹", + "supportType": "2001", + "components": [ + { + "deviceId": "81c7e7da-cb7b-4435-bba3-1648e946d2b2", + "deviceName": "导弹机动组件", + "componentParams": [ + { + "uuid": "b47f34c2-3cba-4be3-a1d5-be0a986dff6f", + "attDefaultValue": "", + "attExplain": "", + "number": 1 + } + ] + }, + { + "deviceId": "4cf26cd0-bd5b-4d4c-a2eb-bca6184be5a1", + "deviceName": "导弹平台", + "componentParams": [ + { + "uuid": "32beea38-ffc2-445a-9687-e9bdb087727f", + "attDefaultValue": "", + "attExplain": "", + "number": 1 + } + ] + } + ], + "coordinate": { + "longitude": 124.74107151788, + "latitude": 26.74143394432, + "height": -498.06469972014 + }, + "number": 10, + "targetId": "0c058874-0c12-4902-8fd0-2cde015965e1" + } + ], + "targetId": "0c058874-0c12-4902-8fd0-2cde015965e1", + "warZoneLocation": [ + { + "longitude": 122.18971775079, + "latitude": 28.64177652916, + "height": -99.95248993318 + }, + { + "longitude": 127.22874089381, + "latitude": 28.54075963352, + "height": -1063.0224849918 + }, + { + "longitude": 127.99831970891, + "latitude": 23.81306022325, + "height": -5900.62524601637 + }, + { + "longitude": 120.14866648107, + "latitude": 23.90272134296, + "height": -22.17614107099 + } + ], + "defZoneLocation": [] + } + ], + "TrackParam": { + "routeLine_220e5b3c-270d-4006-87f0-a0ab4b22deda": { + "name": "F-22航线1", + "StartTime": 0, + "EndTime": 1657, + "TrackType": "routeLineAir", + "HeightType": "msl", + "seaType": "seaLevel", + "TrackPoints": [ + { + "index": "1", + "longitude": "124.69258218394617", + "latitude": "27.801527639000142", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "2", + "longitude": "122.9223606765063", + "latitude": "27.585793910609084", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "3", + "longitude": "121.59995205693548", + "latitude": "27.41556157737817", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "4", + "longitude": "121.00161216798338", + "latitude": "27.07354384591234", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "5", + "longitude": "121.0190476615353", + "latitude": "26.599336790536423", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "6", + "longitude": "121.67869100949231", + "latitude": "26.183061747705537", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "7", + "longitude": "123.7900051477299", + "latitude": "25.733670416880216", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + }, + { + "index": "8", + "longitude": "125.47545066085917", + "latitude": "26.136857380879235", + "height": "6000", + "speed": "600", + "psia": "0", + "time": 0, + "active": "null" + } + ], + "Color": "rgb(4,161,246)", + "PointCount": 8 + }, + "Groups": [ + { + "allAngle": 0, + "drawName": "J15编组1", + "editPermission": [], + "groupType": "addGroup", + "id": "5dea9ff7-5e45-4f8e-a67f-3ff4187c39ed", + "idKey": "id", + "isSelected": false, + "isShow": false, + "leader": "7a16c098-ceec-4c4d-8a24-8f44976a90ca", + "name": "addGroup", + "parentId": "56a96b1b-14a8-4daf-a2d0-47c7faa4b831_batFormation", + "permission": [ + "14bc8ff9-3c93-4218-b01a-e144add196f9" + ], + "show": false, + "sort": 1774271497726, + "wingmanData": [ + { + "alt": 40, + "angle": "50", + "distance": 100, + "key": 0, + "name": "9aa9e5aa-9273-4c27-88e1-e582ff561685" + } + ] + } + ], + "redWeapons": [ + { + "SupportType": "car", + "TroopsDetail": {}, + "Platform_type": "HQ-9发射车", + "isStrikeTarget": false, + "isReconTarget": false, + "isInterferenceTarget": false, + "isDefendImportantPlace": false, + "groupType": "equipment", + "EquipmentID": "b3c6de29-2b27-4500-a9ce-95d0bebc5cb9", + "Name": "HQ-9发射车--5", + "OwnerForceSide": "56a96b1b-14a8-4daf-a2d0-47c7faa4b831", + "PlatID": "56a96b1b-14a8-4daf-a2d0-47c7faa4b831_equipmentPlane", + "SubComponents": { + "communication": [ + { + "ObjectHandle": "3ed86636-a33a-41ac-aff2-0627eb1f9b28", + "arithmetic": { + "id": "66cd29fdce08f520f1d9bf0e", + "name": "短波电台" + }, + "device": { + "id": "5e0e5b064f1fae2ee9fa1000", + "name": "3MHZ通信电台", + "refId": "5e0e5b064f1fae2ee9fa1000" + }, + "deviceId": "0b700ca6-290e-422a-b4e3-73b222809938", + "deviceName": "3MHZ通信电台", + "employLabel": false, + "facilityName": "3MHZ通信电台", + "soleId": "3ed86636-a33a-41ac-aff2-0627eb1f9b28", + "twiceModified": {}, + "zLists": [], + "ParentPlat": "04a96dbc-837a-48be-8c04-e4f53586c47e" + } + ], + "motorized_assembly": [ + { + "ObjectHandle": "83ea5d33-90f4-4fd8-9121-eced6c7a44ea", + "arithmetic": {}, + "device": {}, + "deviceId": "55915bc4-c825-43c4-93eb-6a76ab345e42", + "deviceName": "", + "soleId": "83ea5d33-90f4-4fd8-9121-eced6c7a44ea", + "ParentPlat": "04a96dbc-837a-48be-8c04-e4f53586c47e" + } + ], + "platform": [ + { + "ObjectHandle": "04a96dbc-837a-48be-8c04-e4f53586c47e", + "arithmetic": { + "id": "68876d0fd41989f086e905b8", + "name": "通用发射车算法" + }, + "device": { + "id": "68876d2dd41989f086e905b9", + "name": "发射车平台", + "refId": "68876d2dd41989f086e905b9" + }, + "deviceId": "164c75ec-6db5-48f9-8285-a7e726fd2a11", + "deviceName": "发射车平台", + "employLabel": false, + "facilityName": "发射车平台", + "soleId": "87905d6b-857a-4c5a-a922-5e2d191588d4", + "twiceModified": {}, + "zLists": [], + "TrackParamId": "", + "positions": [ + 119.28585462691, + 25.67974332623, + 34.28352933882 + ] + } + ], + "weapon": [ + { + "ObjectHandle": "4e575d4d-2f36-436b-9939-0b96cde96b89", + "arithmetic": { + "id": "669dd6356bc286bd64e5d66c", + "name": "发射架算法" + }, + "codedQueue": "weapon.launcher", + "configuration": { + "classifyName": "导弹平台", + "isMount": 1, + "mountedWeapon": { + "_id": "68f681794ddde62a52c7e569", + "name": "HQ-9" + }, + "number": 4 + }, + "device": { + "id": "67ff279dafa7ea5aaa3a1236", + "name": "通用发射架", + "refId": "b9665788-9437-40fb-8a24-eff3d4d3b529" + }, + "deviceId": "4e910ccf-d937-4a93-9b68-116167eda202", + "deviceName": "通用发射架", + "employLabel": false, + "facilityName": "通用发射架", + "serialNumber": "launcher", + "soleId": "4e575d4d-2f36-436b-9939-0b96cde96b89", + "twiceModified": { + "launcherType": "sam" + }, + "zLists": [], + "ParentPlat": "04a96dbc-837a-48be-8c04-e4f53586c47e" + } + ] + } + } + ] + }, + "Tasks": [ + { + "color": "rgb(220,39,39)", + "dataType": "taskPlane", + "drawName": "HQ-9发射车--3打击任务", + "groupType": "tasks", + "id": "41d3c5ce-3fda-4dee-a38f-fd3ae7e15e69", + "idKey": "id", + "isSelected": false, + "name": "HQ-9发射车--3打击任务", + "parentId": "56a96b1b-14a8-4daf-a2d0-47c7faa4b831_taskPlane", + "permission": [ + "14bc8ff9-3c93-4218-b01a-e144add196f9", + "56a96b1b-14a8-4daf-a2d0-47c7faa4b831" + ], + "show": true, + "side": "红方", + "sort": 1774271151846, + "task": { + "at_time": "180", + "attackId": "267c5d87-bf36-42f2-b0a8-09c608d94b62", + "color": "rgb(220,39,39)", + "departureAirport": "", + "execute": [ + { + "targetList": [ + { + "ID": "fbddc49c-f7b0-4d07-8000-1b5a2633c461", + "arrayPositionId": 4, + "arrayPositionIdBak": "", + "at_time": { + "timeUp": true, + "value": 180 + }, + "attackType": "", + "boost": "", + "bootTime": "", + "companion": "", + "cruiseRouteId": "routeLine_f01be820-33ca-4843-8354-3f93a9986fe3", + "cruiseRouteOffset": [ + { + "UpOrDown": true, + "value": 0 + }, + { + "BeforeOrAfter": true, + "value": 0 + }, + { + "LeftOrRight": true, + "value": 0 + } + ], + "moveRouteId": "routeLine_de092f0b-d8a3-4382-8e46-398efae3649c", + "fireType": "absolute", + "strategy": [], + "targetId": "dd20d7d9-7ce8-4531-aa0c-0b2056e7fbcd", + "times_interval": 1, + "weaponId": "HQ-9", + "weaponRelease": "", + "weaponType": "", + "weaponUseCount": 3 + } + ], + "type": "assault" + } + ], + "landAirport": "", + "missionList": [ + { + "label": "HQ-9(4)", + "launcherType": "sam", + "number": 4, + "value": "HQ-9" + } + ], + "name": "HQ-9发射车--3打击任务", + "side": "红方", + "sideId": "56a96b1b-14a8-4daf-a2d0-47c7faa4b831", + "speed": 600, + "type": "assault", + "weaponId": "" + } + } + ] +} \ No newline at end of file diff --git a/auto-solution-rule/src/main/resources/rules/rule.drl b/auto-solution-rule/src/main/resources/rules/rule.drl index 0ac479b..b059f40 100644 --- a/auto-solution-rule/src/main/resources/rules/rule.drl +++ b/auto-solution-rule/src/main/resources/rules/rule.drl @@ -6,6 +6,7 @@ import java.util.Map; import static com.solution.rule.utils.RuleFunction.equipmentRule; import static com.solution.rule.utils.RuleFunction.target; import static com.solution.rule.utils.RuleFunction.position; +import static com.solution.rule.utils.RuleFunction.trackRoute; global java.util.Map globalParams; @@ -147,6 +148,34 @@ function Map buildParam(){ // minInterPlatformDistanceMeters:平台最小间距(米)下限,用于抑制平台点位过度重叠。 param.put("minInterPlatformDistanceMeters", 80); + // ===================== 航迹规则参数(写入 TrackParam 动态 key + execute.targetList.moveRouteId) ===================== + // trackRuleEnabled:是否启用航迹生成与 moveRouteId 绑定。 + param.put("trackRuleEnabled", true); + // trackRouteAlgorithm:航迹变形算法。followBlue(默认) / shortestPath / flank / jam + param.put("trackRouteAlgorithm", "followBlue"); + // trackRouteNameSuffix:航迹名称 = 红方任务 drawName + 此后缀(默认「航迹」→ 如 xxx打击任务航迹) + param.put("trackRouteNameSuffix", "航迹"); + // trackAirDataTypeCsv:蓝方 dataType 命中任一子串(忽略大小写)则 TrackType=routeLineAir + param.put("trackAirDataTypeCsv", "taskPlane,air,plane,flight"); + // trackAirKeywordsCsv:蓝方 drawName 或红方 name/platformType 命中任一子串则视为飞行航迹 + param.put("trackAirKeywordsCsv", "机,飞,空,J-,F-,无人机,直升机"); + // trackGroundTrackType:非飞行类时 TrackType 取值(可先占位,后续再接地面路网) + param.put("trackGroundTrackType", "routeLineGround"); + // 航迹侧向算法复用上方「阵位规则」中的 trackPointDirectionMode(head2next / tail2prev);缺省回退见 trackFallbackBearingDeg + param.put("trackFallbackBearingDeg", 0); + // enableTrackWarZoneClamp:航迹点是否约束在 warZoneLocation 多边形内 + param.put("enableTrackWarZoneClamp", true); + // trackExtraNodesMax:在蓝方航迹点基础上最多额外插入的点数(0=与蓝方点数持平;>0 时在中段均匀插值) + param.put("trackExtraNodesMax", 0); + // shortestPath:相邻两点间直线插值分段数(>=1),越大折线越平滑(非真实路网最短路径) + param.put("trackShortPathSegments", 3); + // flank:侧向偏移距离(米);trackFlankSideMode:alternate / left / right + param.put("trackFlankOffsetMeters", 800); + param.put("trackFlankSideMode", "alternate"); + // jam:正弦扰动振幅(米)、沿航迹起伏周期数(越大摆动越密) + param.put("trackJamWobbleMeters", 400); + param.put("trackJamSegments", 4); + return param; } @@ -179,3 +208,13 @@ then globalParams.putAll(buildParam()); position($fact, globalParams); end + +rule "航迹匹配" +salience 70 +when + $fact : DroolsFact(task != null) +then + // 显式航迹规则:填充 TrackParam 下各航迹 id,并绑定 execute[0].targetList[*].moveRouteId + globalParams.putAll(buildParam()); + trackRoute($fact, globalParams); +end diff --git a/auto-solution-system/Dockerfile b/auto-solution-system/Dockerfile new file mode 100644 index 0000000..9fd6682 --- /dev/null +++ b/auto-solution-system/Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:latest +LABEL authors="admin" + +ENTRYPOINT ["top", "-b"] \ No newline at end of file