Skip to content

Commit fe5a9d7

Browse files
Copilotkjellmf
andauthored
Add programmatic creation support for MSDL Environment elements (#104)
* Initial plan * Initial analysis of Environment element support Co-authored-by: kjellmf <[email protected]> * Add Environment.create() and areaOfInterest setter with comprehensive tests Co-authored-by: kjellmf <[email protected]> * Add changeset for Environment creation support Co-authored-by: kjellmf <[email protected]> * Format changeset file so that CI passes * Refactor Environment.create() to use createEmptyXMLElementFromTagName for XML element creation * Add RectangleArea.fromModel method with validation and tests --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: kjellmf <[email protected]> Co-authored-by: kmf <[email protected]>
1 parent 3512b7f commit fe5a9d7

File tree

4 files changed

+201
-5
lines changed

4 files changed

+201
-5
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@orbat-mapper/msdllib": minor
3+
---
4+
5+
Add programmatic creation support for Environment elements. This includes:
6+
7+
- `Environment.create()` static factory method for creating new Environment elements
8+
- `areaOfInterest` getter and setter for programmatic manipulation of AreaOfInterest
9+
- `RectangleArea.create()` static factory method for creating RectangleArea elements with coordinate cloning

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/environment.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getValueOrUndefined,
44
removeUndefinedValues,
55
setOrCreateTagValue,
6+
createEmptyXMLElementFromTagName,
67
} from "./domutils.js";
78
import { MsdlCoordinates } from "./geo.js";
89
import type { BBox, Feature, Polygon } from "geojson";
@@ -29,16 +30,16 @@ export interface RectangleAreaType {
2930
export class Environment implements EnvironmentType {
3031
static readonly TAG_NAME = "Environment";
3132
element: Element;
32-
areaOfInterest?: RectangleArea;
33+
#areaOfInterest?: RectangleArea;
3334
#scenarioTime?: string;
3435
constructor(element: Element) {
3536
this.element = element;
3637
this.#scenarioTime = getValueOrUndefined(element, "ScenarioTime");
3738
const areaOfInterestElement = getTagElement(element, "AreaOfInterest");
3839
if (areaOfInterestElement) {
39-
this.areaOfInterest = new RectangleArea(areaOfInterestElement);
40+
this.#areaOfInterest = new RectangleArea(areaOfInterestElement);
4041
} else {
41-
this.areaOfInterest = undefined;
42+
this.#areaOfInterest = undefined;
4243
}
4344
}
4445

@@ -53,12 +54,37 @@ export class Environment implements EnvironmentType {
5354
setOrCreateTagValue(this.element, "ScenarioTime", value);
5455
}
5556

57+
get areaOfInterest(): RectangleArea | undefined {
58+
if (this.#areaOfInterest) return this.#areaOfInterest;
59+
const areaOfInterestElement = getTagElement(this.element, "AreaOfInterest");
60+
if (areaOfInterestElement) {
61+
this.#areaOfInterest = new RectangleArea(areaOfInterestElement);
62+
}
63+
return this.#areaOfInterest;
64+
}
65+
66+
set areaOfInterest(value: RectangleArea | undefined) {
67+
this.#areaOfInterest = value;
68+
const existingElement = getTagElement(this.element, "AreaOfInterest");
69+
if (existingElement) {
70+
this.element.removeChild(existingElement);
71+
}
72+
if (value) {
73+
this.element.appendChild(value.element);
74+
}
75+
}
76+
5677
toObject(): EnvironmentType {
5778
return removeUndefinedValues({
5879
scenarioTime: this.scenarioTime,
5980
areaOfInterest: this.areaOfInterest?.toObject(),
6081
}) as EnvironmentType;
6182
}
83+
84+
static create(): Environment {
85+
const element = createEmptyXMLElementFromTagName(Environment.TAG_NAME);
86+
return new Environment(element);
87+
}
6288
}
6389

6490
export class RectangleArea {
@@ -124,4 +150,39 @@ export class RectangleArea {
124150
bboxPolygon(bbox, { properties: { name: this.name } }),
125151
);
126152
}
153+
154+
static create(
155+
upperRight: MsdlCoordinates,
156+
lowerLeft: MsdlCoordinates,
157+
): RectangleArea {
158+
const element = createEmptyXMLElementFromTagName("AreaOfInterest");
159+
160+
// Clone coordinates with proper tag names
161+
const upperRightClone = MsdlCoordinates.create(
162+
upperRight.coordinateChoice,
163+
"UpperRight",
164+
);
165+
upperRightClone.location = upperRight.location;
166+
167+
const lowerLeftClone = MsdlCoordinates.create(
168+
lowerLeft.coordinateChoice,
169+
"LowerLeft",
170+
);
171+
lowerLeftClone.location = lowerLeft.location;
172+
173+
element.appendChild(upperRightClone.element);
174+
element.appendChild(lowerLeftClone.element);
175+
return new RectangleArea(element);
176+
}
177+
178+
static fromModel(model: RectangleAreaType): RectangleArea {
179+
if (!model.upperRight || !model.lowerLeft) {
180+
throw new Error("Both upperRight and lowerLeft coordinates are required");
181+
}
182+
const area = RectangleArea.create(model.upperRight, model.lowerLeft);
183+
if (model.name) {
184+
area.name = model.name;
185+
}
186+
return area;
187+
}
127188
}

src/test/environment.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,129 @@ describe("MilitaryScenario Environment", () => {
154154
expect(scenario.environment?.scenarioTime).toBe("2025-07-09T11:11:00Z");
155155
});
156156
});
157+
158+
describe("Environment creation", () => {
159+
it("should create an empty Environment using create()", () => {
160+
const environment = Environment.create();
161+
expect(environment).toBeInstanceOf(Environment);
162+
expect(environment.element).toBeInstanceOf(Element);
163+
expect(environment.scenarioTime).toBeUndefined();
164+
expect(environment.areaOfInterest).toBeUndefined();
165+
});
166+
167+
it("should allow setting scenarioTime programmatically", () => {
168+
const environment = Environment.create();
169+
environment.scenarioTime = "2025-12-31T23:59:59Z";
170+
expect(environment.scenarioTime).toBe("2025-12-31T23:59:59Z");
171+
expect(environment.element.outerHTML).toContain(
172+
"<ScenarioTime>2025-12-31T23:59:59Z</ScenarioTime>",
173+
);
174+
});
175+
176+
it("should allow setting areaOfInterest programmatically", () => {
177+
const environment = Environment.create();
178+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
179+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
180+
const rectangleArea = RectangleArea.create(upperRight, lowerLeft);
181+
rectangleArea.name = "Test Area";
182+
183+
environment.areaOfInterest = rectangleArea;
184+
185+
expect(environment.areaOfInterest).toBeInstanceOf(RectangleArea);
186+
expect(environment.areaOfInterest?.name).toBe("Test Area");
187+
expect(environment.element.outerHTML).toContain("<AreaOfInterest>");
188+
});
189+
190+
it("should allow removing areaOfInterest", () => {
191+
const environment = new Environment(
192+
parseFromString(ENVIRONMENT_SAMPLE_MGRS),
193+
);
194+
expect(environment.areaOfInterest).toBeInstanceOf(RectangleArea);
195+
196+
environment.areaOfInterest = undefined;
197+
expect(environment.areaOfInterest).toBeUndefined();
198+
expect(environment.element.outerHTML).not.toContain("<AreaOfInterest>");
199+
});
200+
});
201+
202+
describe("RectangleArea creation", () => {
203+
it("should create a RectangleArea using create()", () => {
204+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
205+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
206+
const rectangleArea = RectangleArea.create(upperRight, lowerLeft);
207+
208+
expect(rectangleArea).toBeInstanceOf(RectangleArea);
209+
expect(rectangleArea.upperRight).toBeInstanceOf(MsdlCoordinates);
210+
expect(rectangleArea.lowerLeft).toBeInstanceOf(MsdlCoordinates);
211+
});
212+
213+
it("should allow setting name on created RectangleArea", () => {
214+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
215+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
216+
const rectangleArea = RectangleArea.create(upperRight, lowerLeft);
217+
218+
rectangleArea.name = "My Area";
219+
expect(rectangleArea.name).toBe("My Area");
220+
expect(rectangleArea.element.outerHTML).toContain("<Name>My Area</Name>");
221+
});
222+
223+
it("should compute correct bounding box for created RectangleArea", () => {
224+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
225+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
226+
const rectangleArea = RectangleArea.create(upperRight, lowerLeft);
227+
228+
const bbox = rectangleArea.toBoundingBox();
229+
expect(bbox).toBeDefined();
230+
expect(bbox).toHaveLength(4);
231+
expect(bbox![0]).toBeCloseTo(5, 5);
232+
expect(bbox![1]).toBeCloseTo(15, 5);
233+
expect(bbox![2]).toBeCloseTo(10, 5);
234+
expect(bbox![3]).toBeCloseTo(20, 5);
235+
});
236+
});
237+
238+
describe("RectangleArea.fromModel", () => {
239+
it("should create a RectangleArea from a valid model object", () => {
240+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
241+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
242+
const model = {
243+
name: "Test AOI",
244+
upperRight,
245+
lowerLeft,
246+
};
247+
const area = RectangleArea.fromModel(model);
248+
expect(area).toBeInstanceOf(RectangleArea);
249+
expect(area.name).toBe("Test AOI");
250+
expect(area.upperRight.location).toEqual([10, 20, 0]);
251+
expect(area.lowerLeft.location).toEqual([5, 15, 0]);
252+
});
253+
254+
it("should throw if upperRight is missing", () => {
255+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
256+
const model = {
257+
name: "Test AOI",
258+
lowerLeft,
259+
};
260+
expect(() => RectangleArea.fromModel(model as any)).toThrow();
261+
});
262+
263+
it("should throw if lowerLeft is missing", () => {
264+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
265+
const model = {
266+
name: "Test AOI",
267+
upperRight,
268+
};
269+
expect(() => RectangleArea.fromModel(model as any)).toThrow();
270+
});
271+
272+
it("should set name to undefined if not provided", () => {
273+
const upperRight = MsdlCoordinates.createGDCLocation([10, 20, 0]);
274+
const lowerLeft = MsdlCoordinates.createGDCLocation([5, 15, 0]);
275+
const model = {
276+
upperRight,
277+
lowerLeft,
278+
};
279+
const area = RectangleArea.fromModel(model);
280+
expect(area.name).toBeUndefined();
281+
});
282+
});

0 commit comments

Comments
 (0)