Add Protection Cell Air-Defense Coverage example, analyzer, scenario, and tests - #24
Add Protection Cell Air-Defense Coverage example, analyzer, scenario, and tests#24jakexcosme wants to merge 4 commits into
Conversation
… and tests Co-Authored-By: Jake Cosme <[email protected]>
Original prompt from Jake
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| protected static String sidcForThreat(String type) | ||
| { | ||
| // All modeled threats are hostile air-defense (surface-to-air) systems. | ||
| return "SHGPUCD--------"; | ||
| } |
There was a problem hiding this comment.
📝 Info: Threat type mapping always returns the same SIDC regardless of input
The sidcForThreat method at line 432-436 accepts a type parameter but ignores it entirely, always returning "SHGPUCD--------". The comment explains this is intentional ("All modeled threats are hostile air-defense systems"), and the echelon modifier is used to visually distinguish SAM_LONG/MEDIUM/SHORT. However, if the scenario CSV is extended with non-air-defense threat types (e.g., coastal defense missiles), this method would silently render them with the wrong symbol. Consider whether a switch similar to sidcForAsset would be more future-proof.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Addressed in b3bf60e. The icon stays hostile air-defense (all threats are SAM systems), but addThreatSymbol now sets a MIL-STD-2525 echelon modifier per class via the new echelonForThreat helper: SAM_LONG → battalion, SAM_MEDIUM → battery, SAM_SHORT → platoon. Long/medium/short systems are now visually distinguishable on the map (in addition to the WEZ ring size and the - WEZ N km label).
| @RunWith(JUnit4.class) | ||
| public class ThreatCoverageAnalyzerTest | ||
| { | ||
| private static final double DELTA = 1e-6; |
There was a problem hiding this comment.
📝 Info: Test suite covers boundary, overlap, and count scenarios thoroughly
The test file at test/gov/nasa/worldwindx/examples/symbology/ThreatCoverageAnalyzerTest.java uses a locationAtDistance helper that computes positions via the same spherical model the analyzer uses, ensuring round-trip distance accuracy. Tests cover: just-inside (99 km vs 100 km ring), just-outside (101 km), exact boundary (<=), overlapping threat rings, single-threat exposure, covered assets, report counts, and invalid-argument rejection. The DELTA constant at line 49 is declared but unused — it's harmless but could be cleaned up.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Thanks — no action needed. Boundary (just-inside/just-outside/on-ring), multiple-overlap, single-threat, covered, report counts, and argument-validation cases are all covered.
…lience, threat echelon differentiation; drop author tags Co-Authored-By: Jake Cosme <[email protected]>
| double radiusKm = fields.length > 5 && !fields[5].trim().isEmpty() | ||
| ? Double.parseDouble(fields[5].trim()) : 0.0; | ||
| scenario.threats.add(new Threat(name, type, location, radiusKm * 1000.0)); |
There was a problem hiding this comment.
📝 Info: THREAT entries without a radius silently default to zero
In parseScenarioLine at src/gov/nasa/worldwindx/examples/symbology/AirDefenseCoverage.java:377-378, if a THREAT CSV line has fewer than 6 fields or an empty radius field, the radius defaults to 0.0 km. This creates a degenerate threat with a zero-meter weapon-engagement zone that covers no assets. The current CSV data always provides radii for THREAT entries, but a future scenario author might omit the field expecting a sensible default and get no coverage ring with no warning logged.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 2959453. parseScenarioLine now logs a generic.CannotParse warning when a THREAT row omits or leaves the radius field empty, so a future author gets a visible warning instead of a silent zero-radius (no-coverage) ring.
…nt line endings, drop unused test constant Co-Authored-By: Jake Cosme <[email protected]>
| if (!hasRadius) | ||
| Logging.logger().warning(Logging.getMessage("generic.CannotParse", line)); | ||
|
|
||
| double radiusKm = hasRadius ? Double.parseDouble(fields[5].trim()) : 0.0; | ||
| scenario.threats.add(new Threat(name, type, location, radiusKm * 1000.0)); |
There was a problem hiding this comment.
🟡 Malformed threat entry is warned about but still silently added with a zero-radius default
A threat record missing its radius is logged as unparseable (Logging.logger().warning(...) at src/gov/nasa/worldwindx/examples/symbology/AirDefenseCoverage.java:379) but is still added to the scenario with a zero-meter radius instead of being skipped, so a degenerate zero-radius threat ring is rendered and the threat appears in the coverage report without ever exposing any asset.
Impact: A malformed threat entry produces a misleading warning, a zero-size rendered volume, and an inert entry in the coverage analysis instead of being cleanly skipped like other malformed records.
Inconsistent error-handling pattern compared to other malformed-line cases
Every other malformed-data branch in parseScenarioLine warns and then returns (skips the record):
fields.length < 5atAirDefenseCoverage.java:356-359: warns and returns.- Unknown
kindatAirDefenseCoverage.java:386: warns (implicit return at end of else).
But the missing-radius branch at lines 378-382 warns and then falls through to add the threat with radiusKm = 0.0. This creates a Threat with radiusMeters = 0, which is then passed to addThreatVolume at line 120, creating a CappedCylinder with radius 0 — a degenerate shape. The threat also appears in the CoverageReport but can never expose any asset (since distance <= 0 is only true for co-located points).
The fix should either skip the threat (add a return after the warning, consistent with other branches) or not emit the warning if 0 is an intentional default.
| if (!hasRadius) | |
| Logging.logger().warning(Logging.getMessage("generic.CannotParse", line)); | |
| double radiusKm = hasRadius ? Double.parseDouble(fields[5].trim()) : 0.0; | |
| scenario.threats.add(new Threat(name, type, location, radiusKm * 1000.0)); | |
| if (!hasRadius) | |
| { | |
| Logging.logger().warning(Logging.getMessage("generic.CannotParse", line)); | |
| return; | |
| } | |
| double radiusKm = Double.parseDouble(fields[5].trim()); | |
| scenario.threats.add(new Threat(name, type, location, radiusKm * 1000.0)); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — fixed in 6ca7f72. A THREAT row missing/empty radius now warns and returns (skipped) instead of falling through to a zero-radius Threat, matching the other malformed-record branches. No degenerate zero-radius CappedCylinder or inert coverage-report entry is created.
| ThreatCoverageAnalyzer analyzer = new ThreatCoverageAnalyzer( | ||
| this.getWwd().getModel().getGlobe() != null | ||
| ? this.getWwd().getModel().getGlobe().getRadius() | ||
| : ThreatCoverageAnalyzer.DEFAULT_GLOBE_RADIUS_METERS); |
There was a problem hiding this comment.
📝 Info: Globe.getRadius() resolves through the Extent interface, not Globe directly
The call this.getWwd().getModel().getGlobe().getRadius() at src/gov/nasa/worldwindx/examples/symbology/AirDefenseCoverage.java:136 initially appears problematic because the Globe interface (src/gov/nasa/worldwind/globes/Globe.java) does not declare getRadius(). However, Globe extends WWObject, Extent (line 68 of Globe.java), and Extent declares double getRadius() at src/gov/nasa/worldwind/geom/Extent.java:62. The concrete EllipsoidalGlobe.getRadius() returns the equatorial radius, which matches Earth.WGS84_EQUATORIAL_RADIUS used as the analyzer's default. This is consistent but non-obvious — a reader might expect Globe to declare this method directly.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Correct, and intended — Globe extends Extent, which declares getRadius(); EllipsoidalGlobe.getRadius() returns the equatorial radius, matching Earth.WGS84_EQUATORIAL_RADIUS used as the analyzer's default. No change needed.
| Angle angle = LatLon.greatCircleDistance(a, b); | ||
| return angle.radians * this.globeRadiusMeters; |
There was a problem hiding this comment.
📝 Info: Potential mismatch between analyzer's spherical distance model and rendered cylinder geometry
The ThreatCoverageAnalyzer computes exposure using great-circle distance on a sphere (LatLon.greatCircleDistance × globe radius), while CappedCylinder in addThreatVolume renders a cylinder on the actual ellipsoidal globe surface. For the threat radii in this scenario (120–370 km), the difference between spherical and ellipsoidal distance is small (< 0.3%), so assets near a ring boundary could theoretically show as exposed in the analysis but appear outside the rendered ring, or vice versa. For this example's purposes the discrepancy is negligible, but it's worth documenting if the analyzer is reused for higher-precision applications.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Noted — the spherical model is intentional and already documented in the class javadoc ("on a spherical Earth of the configured radius"), and the analyzer uses the globe's equatorial radius so exposure stays consistent with the rings. As you note the spherical-vs-ellipsoidal delta is <0.3% at these radii; leaving as-is for this example.
| try | ||
| { | ||
| // Load the scenario laydown from the bundled resource. | ||
| Scenario scenario = loadScenario(); | ||
|
|
||
| // Draw threat rings first so symbols render on top of the translucent volumes. | ||
| for (Threat threat : scenario.threats) | ||
| { | ||
| this.addThreatVolume(threat); | ||
| } | ||
|
|
||
| // Draw a MIL-STD-2525 symbol for every entity in the picture. | ||
| for (Asset asset : scenario.assets) | ||
| { | ||
| this.addAssetSymbol(asset); | ||
| } | ||
| for (Threat threat : scenario.threats) | ||
| { | ||
| this.addThreatSymbol(threat); | ||
| } | ||
|
|
||
| // Compute protection coverage and flag exposed assets. | ||
| ThreatCoverageAnalyzer analyzer = new ThreatCoverageAnalyzer( | ||
| this.getWwd().getModel().getGlobe() != null | ||
| ? this.getWwd().getModel().getGlobe().getRadius() | ||
| : ThreatCoverageAnalyzer.DEFAULT_GLOBE_RADIUS_METERS); | ||
| CoverageReport report = analyzer.analyze(scenario.assets, scenario.threats); | ||
|
|
||
| for (AssetExposure exposure : report.getExposures()) | ||
| { | ||
| if (exposure.isExposed()) | ||
| this.addExposureCallout(exposure); | ||
| } | ||
|
|
||
| // Print the coverage summary to the console and show it in the control panel. | ||
| String summary = report.formatSummary(); | ||
| System.out.println(summary); | ||
| this.addCoveragePanel(scenario, report, summary); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| // WorldWind examples must not crash on startup. Log and continue with whatever loaded. | ||
| Logging.logger().log(Level.SEVERE, | ||
| Logging.getMessage("generic.ExceptionAttemptingToReadFile", SCENARIO_RESOURCE), e); | ||
| } |
There was a problem hiding this comment.
📝 Info: File I/O occurs on the EDT during AppFrame construction
The loadScenario() call at line 115 performs file I/O (reading and parsing the CSV) synchronously in the AppFrame constructor, which runs on the Event Dispatch Thread. CONTRIBUTING.md's rule "Within a rendering pass WorldWind does not touch the disk or the network" applies to rendering passes specifically, not constructors, so this is technically compliant. For a small bundled CSV this is fine, but if the scenario file grows or is loaded from a network resource, this would block the UI thread. The existing pattern in other examples (e.g., TacticalSymbols.java) also does setup work in the constructor, so this is consistent with the codebase.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Agreed — this is a constructor, not a rendering pass, so it's compliant with the "no disk/network during a rendering pass" rule, and it matches the setup-in-constructor pattern used by TacticalSymbols/TacticalGraphics. The scenario is a tiny bundled CSV. Leaving as-is.
…te zero-radius threat Co-Authored-By: Jake Cosme <[email protected]>
Description of the Change
Adds a self-contained Protection Cell Air-Defense Coverage capability to the examples, following the pattern of the existing
TacticalSymbols/TacticalGraphicssymbology examples. It renders a common operating picture on the 3D globe and computes air-defense protection coverage — the class of decision aid an Army protection cell uses.New files (no changes to existing files, no public API changes):
src/gov/nasa/worldwindx/examples/symbology/AirDefenseCoverage.java— runnable example (main, launchable like every other example). Loads the scenario, renders each entity as a MIL-STD-2525MilStd2525TacticalSymbolon aRenderableLayer(friendly assets → friendly SIDCs, threats → hostile air-defense SIDCs), draws each threat's weapon-engagement zone as a translucent hostile-red range volume (CappedCylinder), flags exposed assets with a red callout, and prints/shows a coverage summary.src/gov/nasa/worldwindx/examples/symbology/ThreatCoverageAnalyzer.java— rendering-free helper holding the exposure logic (Asset,Threat,AssetExposure,CoverageReport). Exposure =LatLon.greatCircleDistance(asset, threat) * globeRadius <= wezRadius.src/config/AirDefenseCoverageScenario.csv— bundled scenario (14 friendly assets + 4 hostile IADS entries) over a Baltic AO using publicly known place names. Threat rings are open-source coverage estimates from published nominal system ranges; no classified data.test/gov/nasa/worldwindx/examples/symbology/ThreatCoverageAnalyzerTest.java— JUnit4 geometry tests (asset just inside vs. just outside a ring, on-boundary, multiple overlapping threats, single-threat exposure, covered asset, report counts, argument validation).Pseudocode of the core decision:
Verified output on the bundled scenario: 12 of 14 assets exposed, 2 covered (the two northernmost nodes fall outside every ring).
Why Should This Be In Core?
It exercises existing core symbology (
MilStd2525TacticalSymbol), airspace primitives (CappedCylinder), annotations (GlobeAnnotation), and geometry (LatLon.greatCircleDistance) end-to-end as an operator-recognizable decision aid, and gives the examples suite a concrete protection-planning workflow. It is additive and isolated toworldwindx.examples+config.Benefits
Potential Drawbacks
Applicable Issues
None.
Reproduce from text (paste into Ask Devin)
Agent Audit Trail (AGENTS.md §6)
1. Prompt block — see "Reproduce from text" above (verbatim task prompt).
2. Session URL — https://app.devin.ai/sessions/6be415ce16254dec9052c52bcbd7c07c
3. Devin Review verdict — Pass-with-notes. Devin Review flagged 5 items (1 repo-rule warning + 4 info); all addressed in commit
b3bf60e4: (a) log-then-throw on the missing-scenario path; (b) close the underlying stream even if reader construction fails; (c) per-line CSV parsing that logs and skips a malformed record instead of aborting the load; (d) MIL-STD-2525 echelon differentiation so long/medium/short-range SAMs are visually distinct; (e) test-coverage note — no change needed. A second review pass raised 3 further info nits, all resolved in2959453d: warn on a THREAT row with a missing/empty radius, consistent\nline endings in the summary, and removal of an unused test constant. No unresolved findings.4. Plain-English rationale — This PR adds an operator-recognizable protection-cell decision aid: it draws a MIL-STD-2525 common operating picture, shows hostile air-defense weapon-engagement zones as translucent range volumes, and computes which friendly assets sit inside those zones. It is entirely new, isolated code (one example, one analyzer, one data file, one test), so nothing existing changes.
5. Metrics block — Capability-extension PR (not a Java 21 modernization pass), so modernization counters are all 0:
instanceof→ pattern-match conversions: 06. Public API delta — No public API surface changes. All additions live in
gov.nasa.worldwindx.examples.symbologyandsrc/config; no signatures ingov.nasa.worldwind.*were touched.Build & test gates
The default
buildtarget runs the full JUnit suite: 50 test suites passed, 0 failures / 0 errors, including the newThreatCoverageAnalyzerTest(10 tests, all green).Requested by: @jakexcosme
Devin Review