@orbat-mapper/control-measures 0.2.0-alpha.16 → 0.2.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-BCn2uBC0.d.mts → index-CFTEcmQi.d.mts} +72 -53
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/preview/index.d.mts +1 -1
- package/dist/preview/index.mjs +1 -1
- package/dist/{renderControlMeasure-aijO8FNF.mjs → renderControlMeasure-C1Ur1rH0.mjs} +408 -311
- package/package.json +1 -1
|
@@ -273,6 +273,7 @@ function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, smooth, smoo
|
|
|
273
273
|
];
|
|
274
274
|
const fullSpine = [...remainingSpine, shaftEndCenter];
|
|
275
275
|
return {
|
|
276
|
+
shaftCenterline: fullSpine,
|
|
276
277
|
shaftLeft: offsetPolyline(fullSpine, shaftHalfWidth, smooth, smoothResolution),
|
|
277
278
|
shaftRight: offsetPolyline(fullSpine, -shaftHalfWidth, smooth, smoothResolution),
|
|
278
279
|
headRing,
|
|
@@ -1411,6 +1412,13 @@ const AREA_DESIGNATION_AMPLIFIER = {
|
|
|
1411
1412
|
description: "Field T — the unit's designation, centered in the area.",
|
|
1412
1413
|
maxLength: 21
|
|
1413
1414
|
};
|
|
1415
|
+
const AREA_HOSTILE_AMPLIFIER = {
|
|
1416
|
+
key: "N",
|
|
1417
|
+
label: "Hostile (ENY) marker",
|
|
1418
|
+
description: "Field N — literal \"ENY\" marker at the area's west and east edges.",
|
|
1419
|
+
placeholder: "ENY",
|
|
1420
|
+
maxLength: 3
|
|
1421
|
+
};
|
|
1414
1422
|
const AREA_TEXT_AMPLIFIERS = [
|
|
1415
1423
|
{
|
|
1416
1424
|
key: "T",
|
|
@@ -1437,17 +1445,9 @@ const AREA_TEXT_AMPLIFIERS = [
|
|
|
1437
1445
|
description: "Field W1 — date-time group the area ceases to be effective.",
|
|
1438
1446
|
maxLength: 16
|
|
1439
1447
|
},
|
|
1440
|
-
|
|
1441
|
-
key: "N",
|
|
1442
|
-
label: "Hostile (ENY) marker",
|
|
1443
|
-
description: "Field N — literal \"ENY\" marker at the area's west and east edges.",
|
|
1444
|
-
placeholder: "ENY",
|
|
1445
|
-
maxLength: 3
|
|
1446
|
-
}
|
|
1448
|
+
AREA_HOSTILE_AMPLIFIER
|
|
1447
1449
|
];
|
|
1448
|
-
|
|
1449
|
-
* whose template carries no ENY marker (Fortified Area). */
|
|
1450
|
-
const AREA_TEXT_AMPLIFIERS_NO_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "N");
|
|
1450
|
+
const AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS = [AREA_DESIGNATION_AMPLIFIER, AREA_HOSTILE_AMPLIFIER];
|
|
1451
1451
|
/** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
|
|
1452
1452
|
* for measures whose template omits that row but keeps the W/W1 window and
|
|
1453
1453
|
* the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
|
|
@@ -2040,6 +2040,10 @@ function polylineSegments(verts) {
|
|
|
2040
2040
|
totalLength
|
|
2041
2041
|
};
|
|
2042
2042
|
}
|
|
2043
|
+
/** Resolves an along-line position onto the valid start-to-end range. */
|
|
2044
|
+
function clampLinePosition(value, fallback) {
|
|
2045
|
+
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
2046
|
+
}
|
|
2043
2047
|
/**
|
|
2044
2048
|
* Resolves the point at `fraction` (clamped to `[0, 1]`) of `totalLength`
|
|
2045
2049
|
* along `segments`, walking cumulative arc length exactly like Boundary's
|
|
@@ -2135,7 +2139,7 @@ function labelSizeProps(options) {
|
|
|
2135
2139
|
return {};
|
|
2136
2140
|
}
|
|
2137
2141
|
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2138
|
-
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField) {
|
|
2142
|
+
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField, labelPlacementKey) {
|
|
2139
2143
|
out.push({
|
|
2140
2144
|
type: "Feature",
|
|
2141
2145
|
properties: {
|
|
@@ -2144,6 +2148,7 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierF
|
|
|
2144
2148
|
rotation,
|
|
2145
2149
|
...textAnchor ? { textAnchor } : {},
|
|
2146
2150
|
...amplifierField ? { amplifierField } : {},
|
|
2151
|
+
...labelPlacementKey ? { labelPlacementKey } : {},
|
|
2147
2152
|
...sizeProps
|
|
2148
2153
|
},
|
|
2149
2154
|
geometry: {
|
|
@@ -2297,10 +2302,6 @@ const LABEL_GAP_CLEARANCE_RATIO = .3;
|
|
|
2297
2302
|
function nonNegative$1(value, fallback) {
|
|
2298
2303
|
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
2299
2304
|
}
|
|
2300
|
-
/** Resolves an along-line position option onto the valid tail-to-tip range. */
|
|
2301
|
-
function clampLinePosition(value, fallback) {
|
|
2302
|
-
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
2303
|
-
}
|
|
2304
2305
|
/** Arc-length knockout centered on an along-route text label. */
|
|
2305
2306
|
function directionLabelGap(totalLength, fraction, text, options) {
|
|
2306
2307
|
if (!text) return void 0;
|
|
@@ -2788,6 +2789,141 @@ const DIRECTION_OF_SUPPORTING_ATTACK = defineControlMeasure({
|
|
|
2788
2789
|
}
|
|
2789
2790
|
});
|
|
2790
2791
|
//#endregion
|
|
2792
|
+
//#region src/text-amplifiers.ts
|
|
2793
|
+
/**
|
|
2794
|
+
* The vocabulary of text amplifier fields: doctrinal code, milsymbol-style
|
|
2795
|
+
* friendly alias, and a human-readable label. The single source of truth for
|
|
2796
|
+
* both the runtime catalog and the derived alias→code lookup used by
|
|
2797
|
+
* {@link normalizeTextAmplifiers}.
|
|
2798
|
+
*/
|
|
2799
|
+
const TEXT_AMPLIFIER_FIELDS = {
|
|
2800
|
+
C: {
|
|
2801
|
+
alias: "quantity",
|
|
2802
|
+
label: "Quantity"
|
|
2803
|
+
},
|
|
2804
|
+
H: {
|
|
2805
|
+
alias: "additionalInformation",
|
|
2806
|
+
label: "Additional information"
|
|
2807
|
+
},
|
|
2808
|
+
N: {
|
|
2809
|
+
alias: "hostile",
|
|
2810
|
+
label: "Hostile (ENY) marker"
|
|
2811
|
+
},
|
|
2812
|
+
T: {
|
|
2813
|
+
alias: "uniqueDesignation",
|
|
2814
|
+
label: "Unique designation"
|
|
2815
|
+
},
|
|
2816
|
+
V: {
|
|
2817
|
+
alias: "type",
|
|
2818
|
+
label: "Equipment/type"
|
|
2819
|
+
},
|
|
2820
|
+
W: {
|
|
2821
|
+
alias: "dtg",
|
|
2822
|
+
label: "Date-time group / O/O"
|
|
2823
|
+
},
|
|
2824
|
+
X: {
|
|
2825
|
+
alias: "altitudeDepth",
|
|
2826
|
+
label: "Altitude/depth"
|
|
2827
|
+
},
|
|
2828
|
+
Y: {
|
|
2829
|
+
alias: "location",
|
|
2830
|
+
label: "Location"
|
|
2831
|
+
},
|
|
2832
|
+
AM: {
|
|
2833
|
+
alias: "distance",
|
|
2834
|
+
label: "Distance/range/radius"
|
|
2835
|
+
},
|
|
2836
|
+
AN: {
|
|
2837
|
+
alias: "azimuth",
|
|
2838
|
+
label: "Azimuth"
|
|
2839
|
+
},
|
|
2840
|
+
AP: {
|
|
2841
|
+
alias: "targetNumber",
|
|
2842
|
+
label: "Target number"
|
|
2843
|
+
},
|
|
2844
|
+
AS: {
|
|
2845
|
+
alias: "country",
|
|
2846
|
+
label: "Country code"
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
/** Derived alias→code lookup, so the vocabulary table stays the single source of truth. */
|
|
2850
|
+
const CODE_BY_ALIAS = Object.fromEntries(Object.entries(TEXT_AMPLIFIER_FIELDS).map(([code, { alias }]) => [alias, code]));
|
|
2851
|
+
const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code) => [
|
|
2852
|
+
code,
|
|
2853
|
+
`${code}1`,
|
|
2854
|
+
`${code}2`
|
|
2855
|
+
]));
|
|
2856
|
+
/**
|
|
2857
|
+
* Resolves a raw placement entry to its object form: a bare `Position` is
|
|
2858
|
+
* legacy shorthand for `{ position }`; an object form passes through only the
|
|
2859
|
+
* fields that are actually valid (`position` must be a finite `[lon, lat]`,
|
|
2860
|
+
* `rotation` a finite number); `undefined` resolves to `{}`.
|
|
2861
|
+
*/
|
|
2862
|
+
function resolveAmplifierPlacement(value) {
|
|
2863
|
+
if (value === void 0) return {};
|
|
2864
|
+
if (Array.isArray(value)) return isValidPosition(value) ? { position: value } : {};
|
|
2865
|
+
const result = {};
|
|
2866
|
+
if (value.position !== void 0 && isValidPosition(value.position)) result.position = value.position;
|
|
2867
|
+
if (typeof value.rotation === "number" && Number.isFinite(value.rotation)) result.rotation = value.rotation;
|
|
2868
|
+
return result;
|
|
2869
|
+
}
|
|
2870
|
+
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
2871
|
+
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
2872
|
+
function splitSuffix(key) {
|
|
2873
|
+
const last = key.charAt(key.length - 1);
|
|
2874
|
+
if (last === "1" || last === "2") return {
|
|
2875
|
+
base: key.slice(0, -1),
|
|
2876
|
+
suffix: last
|
|
2877
|
+
};
|
|
2878
|
+
return {
|
|
2879
|
+
base: key,
|
|
2880
|
+
suffix: ""
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Resolves any accepted key (canonical or friendly alias, with an optional
|
|
2885
|
+
* `1`/`2` suffix) to its canonical key, or `undefined` if unknown — e.g.
|
|
2886
|
+
* `"T"` → `"T"`, `"uniqueDesignation1"` → `"T1"`, `"bogus"` → `undefined`.
|
|
2887
|
+
*/
|
|
2888
|
+
function canonicalTextAmplifierKey(key) {
|
|
2889
|
+
if (CANONICAL_KEYS.has(key)) return key;
|
|
2890
|
+
const { base, suffix } = splitSuffix(key);
|
|
2891
|
+
const code = CODE_BY_ALIAS[base];
|
|
2892
|
+
return code ? `${code}${suffix}` : void 0;
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Resolves a `TextAmplifiers` record (canonical keys, friendly aliases, or a
|
|
2896
|
+
* mix) to `CanonicalTextAmplifiers` keyed purely by doctrinal field code.
|
|
2897
|
+
*
|
|
2898
|
+
* - Friendly aliases (with an optional `1`/`2` suffix) map to their canonical
|
|
2899
|
+
* code (e.g. `country1` → `AS1`).
|
|
2900
|
+
* - Values are trimmed; entries that are empty/whitespace-only after
|
|
2901
|
+
* trimming, or not strings, are dropped.
|
|
2902
|
+
* - When both a canonical key and its friendly alias are present for the same
|
|
2903
|
+
* field + suffix, the **canonical key wins** regardless of insertion order.
|
|
2904
|
+
* This holds even for an empty/whitespace canonical value: it suppresses
|
|
2905
|
+
* (clears) the alias entry rather than letting the alias survive.
|
|
2906
|
+
* - Unknown keys are ignored.
|
|
2907
|
+
*
|
|
2908
|
+
* `undefined` or an empty input returns a shared frozen empty object.
|
|
2909
|
+
*/
|
|
2910
|
+
function normalizeTextAmplifiers(input) {
|
|
2911
|
+
if (!input) return EMPTY_TEXT_AMPLIFIERS;
|
|
2912
|
+
const canonicalPresent = /* @__PURE__ */ new Set();
|
|
2913
|
+
for (const [key, rawValue] of Object.entries(input)) if (typeof rawValue === "string" && CANONICAL_KEYS.has(key)) canonicalPresent.add(key);
|
|
2914
|
+
const result = {};
|
|
2915
|
+
for (const [key, rawValue] of Object.entries(input)) {
|
|
2916
|
+
if (typeof rawValue !== "string") continue;
|
|
2917
|
+
const value = rawValue.trim();
|
|
2918
|
+
if (value.length === 0) continue;
|
|
2919
|
+
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
2920
|
+
if (!canonicalKey) continue;
|
|
2921
|
+
if (!CANONICAL_KEYS.has(key) && canonicalPresent.has(canonicalKey)) continue;
|
|
2922
|
+
result[canonicalKey] = value;
|
|
2923
|
+
}
|
|
2924
|
+
return Object.keys(result).length > 0 ? Object.freeze(result) : EMPTY_TEXT_AMPLIFIERS;
|
|
2925
|
+
}
|
|
2926
|
+
//#endregion
|
|
2791
2927
|
//#region src/internal/area-echelon.ts
|
|
2792
2928
|
/**
|
|
2793
2929
|
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
@@ -3055,6 +3191,36 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3055
3191
|
totalArc: dist
|
|
3056
3192
|
};
|
|
3057
3193
|
}
|
|
3194
|
+
/** Nearest contact point on a closed ring for a dragged marker anchor. */
|
|
3195
|
+
function nearestPointOnRing(verts, target) {
|
|
3196
|
+
let nearest;
|
|
3197
|
+
let totalArc = 0;
|
|
3198
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
3199
|
+
const a = verts[i];
|
|
3200
|
+
const b = verts[i + 1];
|
|
3201
|
+
const seg = vecSub(b, a);
|
|
3202
|
+
const length = vecMag(seg);
|
|
3203
|
+
if (length < 1e-6) continue;
|
|
3204
|
+
const along = vecScale(seg, 1 / length);
|
|
3205
|
+
const t = Math.max(0, Math.min(length, vecDot(vecSub(target, a), along)));
|
|
3206
|
+
const distance = vecMag(vecSub(target, vecAdd(a, vecScale(along, t))));
|
|
3207
|
+
if (!nearest || distance < nearest.distance) nearest = {
|
|
3208
|
+
along,
|
|
3209
|
+
arc: totalArc + t,
|
|
3210
|
+
distance
|
|
3211
|
+
};
|
|
3212
|
+
totalArc += length;
|
|
3213
|
+
}
|
|
3214
|
+
return nearest ? {
|
|
3215
|
+
...nearest,
|
|
3216
|
+
totalArc
|
|
3217
|
+
} : void 0;
|
|
3218
|
+
}
|
|
3219
|
+
function labelHalfExtent(direction, textWidth, textHeight, rotation) {
|
|
3220
|
+
const widthAxis = [Math.cos(rotation), Math.sin(rotation)];
|
|
3221
|
+
const heightAxis = [-widthAxis[1], widthAxis[0]];
|
|
3222
|
+
return .5 * (textWidth * Math.abs(vecDot(direction, widthAxis)) + textHeight * Math.abs(vecDot(direction, heightAxis)));
|
|
3223
|
+
}
|
|
3058
3224
|
/**
|
|
3059
3225
|
* Pushes an area's L-15 text amplifier labels — centered name/H/W-W1 rows and
|
|
3060
3226
|
* west/east "ENY" edge markers — as `part: "label"` Point features (via
|
|
@@ -3073,28 +3239,36 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3073
3239
|
*
|
|
3074
3240
|
* When `texts.hostile` is set, the ring's west/east crossings of the
|
|
3075
3241
|
* horizontal line through the centroid are found ({@link ringCrossingsAtY});
|
|
3076
|
-
* the minimum/maximum-x crossings
|
|
3077
|
-
*
|
|
3078
|
-
* Each marker
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3082
|
-
*
|
|
3242
|
+
* the minimum/maximum-x crossings are the default anchors for the two "ENY"
|
|
3243
|
+
* labels. A persisted `label:N:west` / `label:N:east` placement replaces that
|
|
3244
|
+
* anchor. Each marker carves a masking {@link ArcGap} at the nearest point on
|
|
3245
|
+
* the boundary only while its estimated text box touches the boundary; moving
|
|
3246
|
+
* it away restores the line. The gap follows the label around any edge and is
|
|
3247
|
+
* sized as the support function of its possibly rotated text box along the
|
|
3248
|
+
* contacted edge's own direction `d` (a boundary running vertically there
|
|
3249
|
+
* needs roughly the label's height; one running horizontally needs roughly
|
|
3250
|
+
* its width):
|
|
3083
3251
|
* `halfExtent = 0.5 * (textWidth·|d.x| + textHeight·|d.y|) + clearance`, where
|
|
3084
3252
|
* `textHeight = resolveLabelOffsetMeters(options, 1)`,
|
|
3085
3253
|
* `textWidth = text.length * ENY_CHAR_ASPECT_RATIO * textHeight`, and
|
|
3086
3254
|
* `clearance = (ENY_CLEARANCE_RATIO + labelPadding) * textHeight` — mirroring
|
|
3087
3255
|
* {@link placeEchelon}'s glyph-width masking gap (ADR-0023). Fewer than two
|
|
3088
3256
|
* crossings (e.g. a degenerate ring, or a concave shape whose centroid
|
|
3089
|
-
* latitude misses the boundary) skips the
|
|
3090
|
-
*
|
|
3257
|
+
* latitude misses the boundary) skips the markers silently rather than
|
|
3258
|
+
* guessing their default placements.
|
|
3091
3259
|
*
|
|
3092
3260
|
* A ring with fewer than 2 vertices is degenerate — nothing is placed.
|
|
3261
|
+
*
|
|
3262
|
+
* `maskBoundary` may supply a different rendered boundary to mask while
|
|
3263
|
+
* keeping label placement anchored to `ringVerts`. This is used by Fortified
|
|
3264
|
+
* Area, whose labels are centered against its smooth pre-teeth ring while the
|
|
3265
|
+
* visible boundary follows a longer castellated trace.
|
|
3093
3266
|
*/
|
|
3094
|
-
function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
3267
|
+
function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary) {
|
|
3095
3268
|
if (ringVerts.length < 2) return [];
|
|
3096
3269
|
const sizeProps = labelSizeProps(options);
|
|
3097
3270
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
3271
|
+
const maskRingVerts = maskBoundary?.ringVerts ?? ringVerts;
|
|
3098
3272
|
const [cx, cy] = ringCentroid(ringVerts);
|
|
3099
3273
|
const rows = [
|
|
3100
3274
|
[texts.name, "T"],
|
|
@@ -3116,15 +3290,27 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
3116
3290
|
const { crossings, totalArc } = ringCrossingsAtY(ringVerts, cy);
|
|
3117
3291
|
if (crossings.length >= 2) {
|
|
3118
3292
|
const byX = [...crossings].sort((a, b) => a.x - b.x);
|
|
3119
|
-
const west = byX[0];
|
|
3120
|
-
const east = byX[byX.length - 1];
|
|
3121
3293
|
const textHeight = resolveLabelOffsetMeters(options, 1);
|
|
3122
3294
|
const textWidth = texts.hostile.length * ENY_CHAR_ASPECT_RATIO * textHeight;
|
|
3123
|
-
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight;
|
|
3124
|
-
for (const crossing of [west, east]) {
|
|
3125
|
-
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3295
|
+
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight + Math.max(0, maskBoundary?.extraClearanceMeters ?? 0);
|
|
3296
|
+
for (const [side, crossing] of [["west", byX[0]], ["east", byX[byX.length - 1]]]) {
|
|
3297
|
+
const labelPlacementKey = `label:N:${side}`;
|
|
3298
|
+
const placement = resolveAmplifierPlacement(amplifierPlacements?.[labelPlacementKey]);
|
|
3299
|
+
const rotation = placement.rotation ?? HORIZONTAL_LABEL_ROTATION;
|
|
3300
|
+
const labelPoint = placement.position ? project(placement.position[0], placement.position[1]) : [crossing.x, cy];
|
|
3301
|
+
pushLabel(out, labelPoint, texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, "N", labelPlacementKey);
|
|
3302
|
+
const usesDifferentMaskRing = maskRingVerts !== ringVerts;
|
|
3303
|
+
const contact = placement.position || usesDifferentMaskRing ? nearestPointOnRing(maskRingVerts, labelPoint) : {
|
|
3304
|
+
along: crossing.along,
|
|
3305
|
+
arc: crossing.arc,
|
|
3306
|
+
distance: 0,
|
|
3307
|
+
totalArc
|
|
3308
|
+
};
|
|
3309
|
+
if (!contact) continue;
|
|
3310
|
+
const normal = [-contact.along[1], contact.along[0]];
|
|
3311
|
+
if (placement.position && contact.distance > labelHalfExtent(normal, textWidth, textHeight, rotation)) continue;
|
|
3312
|
+
const tangentExtent = labelHalfExtent(contact.along, textWidth, textHeight, rotation) + clearance;
|
|
3313
|
+
pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
|
|
3128
3314
|
}
|
|
3129
3315
|
}
|
|
3130
3316
|
}
|
|
@@ -3165,11 +3351,11 @@ function boundaryFeature(verts, gaps) {
|
|
|
3165
3351
|
* echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
|
|
3166
3352
|
* common (no `N`) case is unaffected.
|
|
3167
3353
|
*/
|
|
3168
|
-
function labeledAreaFeatures(positions, options, texts) {
|
|
3354
|
+
function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3169
3355
|
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
3170
3356
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
3171
3357
|
const labelFeatures = [];
|
|
3172
|
-
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
|
|
3358
|
+
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
|
|
3173
3359
|
features.push(...labelFeatures);
|
|
3174
3360
|
return {
|
|
3175
3361
|
type: "FeatureCollection",
|
|
@@ -3207,8 +3393,8 @@ const AREA_METADATA = {
|
|
|
3207
3393
|
* at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
|
|
3208
3394
|
* marker, per {@link labeledAreaFeatures}.
|
|
3209
3395
|
*/
|
|
3210
|
-
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
3211
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
3396
|
+
function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3397
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
|
|
3212
3398
|
}
|
|
3213
3399
|
const AREA = defineControlMeasure({
|
|
3214
3400
|
metadata: AREA_METADATA,
|
|
@@ -3454,8 +3640,8 @@ const AREA_OF_OPERATIONS_METADATA = {
|
|
|
3454
3640
|
* `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
|
|
3455
3641
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3456
3642
|
*/
|
|
3457
|
-
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
|
|
3458
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
|
|
3643
|
+
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3644
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
|
|
3459
3645
|
}
|
|
3460
3646
|
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
3461
3647
|
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
@@ -3474,15 +3660,6 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
|
3474
3660
|
}
|
|
3475
3661
|
});
|
|
3476
3662
|
//#endregion
|
|
3477
|
-
//#region src/generators/cm12-command-control-areas/params.ts
|
|
3478
|
-
const SMOOTH_AREA_PARAMS = AREA_SMOOTH_PARAMS;
|
|
3479
|
-
const AREA_DESIGNATION_ONLY_AMPLIFIER = {
|
|
3480
|
-
key: "T",
|
|
3481
|
-
label: "Unique designation",
|
|
3482
|
-
description: "Field T — the area's unique designation.",
|
|
3483
|
-
maxLength: 21
|
|
3484
|
-
};
|
|
3485
|
-
//#endregion
|
|
3486
3663
|
//#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
|
|
3487
3664
|
/**
|
|
3488
3665
|
* Default options for the Named area of interest control measure. The
|
|
@@ -3502,19 +3679,23 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
|
3502
3679
|
value: "120200",
|
|
3503
3680
|
minCoordinates: 3,
|
|
3504
3681
|
geometry: "area",
|
|
3505
|
-
geometryTypes: [
|
|
3682
|
+
geometryTypes: [
|
|
3683
|
+
"Polygon",
|
|
3684
|
+
"MultiLineString",
|
|
3685
|
+
"Point"
|
|
3686
|
+
],
|
|
3506
3687
|
drawRule: "Area1",
|
|
3507
3688
|
capturesLabelSize: true,
|
|
3508
|
-
params:
|
|
3509
|
-
textAmplifiers:
|
|
3689
|
+
params: LABELED_AREA_PARAMS,
|
|
3690
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3510
3691
|
};
|
|
3511
3692
|
/**
|
|
3512
3693
|
* Creates a Named area of interest control measure: a closed area through
|
|
3513
|
-
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T)
|
|
3514
|
-
*
|
|
3694
|
+
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T) plus
|
|
3695
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3515
3696
|
*/
|
|
3516
|
-
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3517
|
-
return labeledAreaFeatures(positions, options,
|
|
3697
|
+
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3698
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
|
|
3518
3699
|
}
|
|
3519
3700
|
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
3520
3701
|
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
@@ -3552,19 +3733,23 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
|
3552
3733
|
value: "120300",
|
|
3553
3734
|
minCoordinates: 3,
|
|
3554
3735
|
geometry: "area",
|
|
3555
|
-
geometryTypes: [
|
|
3736
|
+
geometryTypes: [
|
|
3737
|
+
"Polygon",
|
|
3738
|
+
"MultiLineString",
|
|
3739
|
+
"Point"
|
|
3740
|
+
],
|
|
3556
3741
|
drawRule: "Area1",
|
|
3557
3742
|
capturesLabelSize: true,
|
|
3558
|
-
params:
|
|
3559
|
-
textAmplifiers:
|
|
3743
|
+
params: LABELED_AREA_PARAMS,
|
|
3744
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3560
3745
|
};
|
|
3561
3746
|
/**
|
|
3562
3747
|
* Creates a Target area of interest control measure: a closed area through
|
|
3563
|
-
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T)
|
|
3564
|
-
*
|
|
3748
|
+
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T) plus
|
|
3749
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3565
3750
|
*/
|
|
3566
|
-
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3567
|
-
return labeledAreaFeatures(positions, options,
|
|
3751
|
+
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3752
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
|
|
3568
3753
|
}
|
|
3569
3754
|
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
3570
3755
|
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
@@ -3618,21 +3803,25 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3618
3803
|
],
|
|
3619
3804
|
drawRule: "Area1",
|
|
3620
3805
|
capturesLabelSize: true,
|
|
3621
|
-
params: [
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3806
|
+
params: [
|
|
3807
|
+
...AREA_SMOOTH_PARAMS,
|
|
3808
|
+
{
|
|
3809
|
+
key: "glyphSizeRatio",
|
|
3810
|
+
label: "Runway size",
|
|
3811
|
+
description: "Size of the runway glyph as a ratio of the area's width.",
|
|
3812
|
+
type: "number",
|
|
3813
|
+
min: .2,
|
|
3814
|
+
max: .9,
|
|
3815
|
+
step: .05
|
|
3816
|
+
},
|
|
3817
|
+
AREA_LABEL_PADDING_PARAM
|
|
3818
|
+
],
|
|
3630
3819
|
textAmplifiers: [{
|
|
3631
3820
|
key: "H",
|
|
3632
3821
|
label: "Additional information",
|
|
3633
3822
|
description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
|
|
3634
3823
|
maxLength: 20
|
|
3635
|
-
}]
|
|
3824
|
+
}, AREA_HOSTILE_AMPLIFIER]
|
|
3636
3825
|
};
|
|
3637
3826
|
/**
|
|
3638
3827
|
* Creates an Airfield zone control measure: a closed area through
|
|
@@ -3643,12 +3832,14 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3643
3832
|
* @param positions - Area anchor points (≥3); the input contract is enforced
|
|
3644
3833
|
* at the render seam (ADR-0014).
|
|
3645
3834
|
* @param options - {@link AirfieldZoneOptions}.
|
|
3646
|
-
* @param textAmplifiers - Field H
|
|
3835
|
+
* @param textAmplifiers - Field H (placed east of the area) plus an optional
|
|
3836
|
+
* Field N "ENY" hostile marker on the west/east edges.
|
|
3647
3837
|
*/
|
|
3648
|
-
function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
3838
|
+
function createAirfieldZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3649
3839
|
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
3650
3840
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
3651
|
-
const
|
|
3841
|
+
const enyLabels = [];
|
|
3842
|
+
const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
|
|
3652
3843
|
let minX = Infinity;
|
|
3653
3844
|
let maxX = -Infinity;
|
|
3654
3845
|
for (const [x] of verts) {
|
|
@@ -3680,6 +3871,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
|
3680
3871
|
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start", "H");
|
|
3681
3872
|
features.push(...labelFeatures);
|
|
3682
3873
|
}
|
|
3874
|
+
features.push(...enyLabels);
|
|
3683
3875
|
return {
|
|
3684
3876
|
type: "FeatureCollection",
|
|
3685
3877
|
features
|
|
@@ -3723,13 +3915,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
|
3723
3915
|
* before carving the boundary, so either (or both) opens it into a gapped
|
|
3724
3916
|
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
3725
3917
|
*/
|
|
3726
|
-
function echelonLabeledAreaFeatures(positions, options, texts) {
|
|
3918
|
+
function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3727
3919
|
const { echelon = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon, echelonSize = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPadding, echelonPosition = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth, smoothResolution = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution, ...labelOptions } = options;
|
|
3728
3920
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
3729
3921
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
3730
3922
|
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
3731
3923
|
const labelFeatures = [];
|
|
3732
|
-
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
|
|
3924
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
|
|
3733
3925
|
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
3734
3926
|
if (strokes.length > 0) features.push({
|
|
3735
3927
|
type: "Feature",
|
|
@@ -3798,8 +3990,8 @@ const BASE_CAMP_METADATA = {
|
|
|
3798
3990
|
* amplifier rows, and an optional Field B echelon glyph straddling the
|
|
3799
3991
|
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3800
3992
|
*/
|
|
3801
|
-
function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
|
|
3802
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
|
|
3993
|
+
function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3994
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
|
|
3803
3995
|
}
|
|
3804
3996
|
const BASE_CAMP = defineControlMeasure({
|
|
3805
3997
|
metadata: BASE_CAMP_METADATA,
|
|
@@ -3851,8 +4043,8 @@ const GUERRILLA_BASE_METADATA = {
|
|
|
3851
4043
|
* `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
|
|
3852
4044
|
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3853
4045
|
*/
|
|
3854
|
-
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
|
|
3855
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
|
|
4046
|
+
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4047
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
|
|
3856
4048
|
}
|
|
3857
4049
|
const GUERRILLA_BASE = defineControlMeasure({
|
|
3858
4050
|
metadata: GUERRILLA_BASE_METADATA,
|
|
@@ -3908,8 +4100,8 @@ const GENERIC_C2_AREA_METADATA = {
|
|
|
3908
4100
|
* through `positions` labeled at its center with the bare `T` (Field T) plus
|
|
3909
4101
|
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
3910
4102
|
*/
|
|
3911
|
-
function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
|
|
3912
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
|
|
4103
|
+
function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4104
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
|
|
3913
4105
|
}
|
|
3914
4106
|
const GENERIC_C2_AREA = defineControlMeasure({
|
|
3915
4107
|
metadata: GENERIC_C2_AREA_METADATA,
|
|
@@ -3958,8 +4150,8 @@ const ASSEMBLY_AREA_METADATA = {
|
|
|
3958
4150
|
* `positions` labeled at its center with "AA "-prefixed `T` (Field T) plus
|
|
3959
4151
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3960
4152
|
*/
|
|
3961
|
-
function createAssemblyArea(positions, options = {}, textAmplifiers = {}) {
|
|
3962
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"));
|
|
4153
|
+
function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4154
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
|
|
3963
4155
|
}
|
|
3964
4156
|
const ASSEMBLY_AREA = defineControlMeasure({
|
|
3965
4157
|
metadata: ASSEMBLY_AREA_METADATA,
|
|
@@ -4640,7 +4832,7 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4640
4832
|
drawRule: "Area1",
|
|
4641
4833
|
capturesLabelSize: true,
|
|
4642
4834
|
params: ECHELON_AREA_PARAMS,
|
|
4643
|
-
textAmplifiers:
|
|
4835
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
4644
4836
|
};
|
|
4645
4837
|
/**
|
|
4646
4838
|
* Creates a Battle Position control measure: a closed area through `positions`
|
|
@@ -4650,16 +4842,22 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4650
4842
|
* @param positions - Area anchor points (≥3); the input contract is enforced at
|
|
4651
4843
|
* the render seam (ADR-0014).
|
|
4652
4844
|
* @param options - {@link BattlePositionOptions}.
|
|
4653
|
-
* @param textAmplifiers - Field T
|
|
4654
|
-
*
|
|
4655
|
-
*
|
|
4845
|
+
* @param textAmplifiers - Field T (designation, centered in the area) plus an
|
|
4846
|
+
* optional Field N "ENY" hostile marker at the west/east edges (no H/W rows —
|
|
4847
|
+
* this measure's template pairs the designation with the Field B glyph, not
|
|
4848
|
+
* the plain L-15 boundary template).
|
|
4656
4849
|
*/
|
|
4657
|
-
function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
4850
|
+
function createBattlePosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4658
4851
|
const { echelon = DEFAULT_BATTLE_POSITION_OPTIONS.echelon, echelonSize = DEFAULT_BATTLE_POSITION_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BATTLE_POSITION_OPTIONS.echelonPadding, echelonPosition = DEFAULT_BATTLE_POSITION_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_BATTLE_POSITION_OPTIONS.smooth, smoothResolution = DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution } = options;
|
|
4659
4852
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
4660
4853
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$6);
|
|
4661
4854
|
const { strokes, fills, gaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
4662
|
-
const
|
|
4855
|
+
const labelFeatures = [];
|
|
4856
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
4857
|
+
name: textAmplifiers.T,
|
|
4858
|
+
hostile: textAmplifiers.N
|
|
4859
|
+
}, options, context.amplifierPlacements);
|
|
4860
|
+
const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
|
|
4663
4861
|
const features = [];
|
|
4664
4862
|
if (boundaryCoords.length > 0) features.push({
|
|
4665
4863
|
type: "Feature",
|
|
@@ -4688,8 +4886,6 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
|
4688
4886
|
coordinates: fills.map((ring) => [ring])
|
|
4689
4887
|
}
|
|
4690
4888
|
});
|
|
4691
|
-
const labelFeatures = [];
|
|
4692
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
4693
4889
|
features.push(...labelFeatures);
|
|
4694
4890
|
return {
|
|
4695
4891
|
type: "FeatureCollection",
|
|
@@ -5053,6 +5249,10 @@ function normalizeSmoothResolution$1(value) {
|
|
|
5053
5249
|
//#endregion
|
|
5054
5250
|
//#region src/generators/cm11-command-control-lines/boundary.ts
|
|
5055
5251
|
const DEFAULT_SMOOTH_RESOLUTION$4 = 12;
|
|
5252
|
+
function resolveLabelRepetitions(value) {
|
|
5253
|
+
const numeric = Number(value ?? 1);
|
|
5254
|
+
return Number.isFinite(numeric) ? Math.max(1, Math.round(numeric)) : 1;
|
|
5255
|
+
}
|
|
5056
5256
|
/** Default options for the Boundary control measure. */
|
|
5057
5257
|
const DEFAULT_BOUNDARY_OPTIONS = {
|
|
5058
5258
|
echelon: "battalion",
|
|
@@ -5061,6 +5261,7 @@ const DEFAULT_BOUNDARY_OPTIONS = {
|
|
|
5061
5261
|
echelonPadding: .3,
|
|
5062
5262
|
labelRepetitions: 1,
|
|
5063
5263
|
labelSpacing: 1,
|
|
5264
|
+
labelPosition: .5,
|
|
5064
5265
|
labelPadding: .4,
|
|
5065
5266
|
labelOrientation: "along",
|
|
5066
5267
|
smooth: false,
|
|
@@ -5143,6 +5344,16 @@ const BOUNDARY_METADATA = {
|
|
|
5143
5344
|
max: 5,
|
|
5144
5345
|
step: 1
|
|
5145
5346
|
},
|
|
5347
|
+
{
|
|
5348
|
+
key: "labelPosition",
|
|
5349
|
+
label: "Label position",
|
|
5350
|
+
description: "Position of the label group along the boundary, from the first point (0) to the last (1)",
|
|
5351
|
+
type: "number",
|
|
5352
|
+
min: 0,
|
|
5353
|
+
max: 1,
|
|
5354
|
+
step: .01,
|
|
5355
|
+
visibleWhen: (opts) => resolveLabelRepetitions(opts.labelRepetitions) === 1
|
|
5356
|
+
},
|
|
5146
5357
|
{
|
|
5147
5358
|
key: "labelSpacing",
|
|
5148
5359
|
label: "Label spacing",
|
|
@@ -5151,7 +5362,7 @@ const BOUNDARY_METADATA = {
|
|
|
5151
5362
|
min: 0,
|
|
5152
5363
|
max: 2,
|
|
5153
5364
|
step: .05,
|
|
5154
|
-
visibleWhen: (opts) =>
|
|
5365
|
+
visibleWhen: (opts) => resolveLabelRepetitions(opts.labelRepetitions) > 1
|
|
5155
5366
|
},
|
|
5156
5367
|
{
|
|
5157
5368
|
key: "labelPadding",
|
|
@@ -5214,7 +5425,7 @@ const BOUNDARY_METADATA = {
|
|
|
5214
5425
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
5215
5426
|
*/
|
|
5216
5427
|
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
5217
|
-
const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
|
|
5428
|
+
const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPosition = DEFAULT_BOUNDARY_OPTIONS.labelPosition, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
|
|
5218
5429
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
5219
5430
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
5220
5431
|
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
@@ -5222,7 +5433,8 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5222
5433
|
let h = echelonSize;
|
|
5223
5434
|
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
5224
5435
|
h = Math.max(EPSILON, h);
|
|
5225
|
-
const reps =
|
|
5436
|
+
const reps = resolveLabelRepetitions(labelRepetitions);
|
|
5437
|
+
const singleGroupPosition = clampLinePosition(labelPosition, DEFAULT_BOUNDARY_OPTIONS.labelPosition);
|
|
5226
5438
|
const spec = resolveEchelon(echelon);
|
|
5227
5439
|
const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
|
|
5228
5440
|
const rawVerts = positions.map((p) => project(p[0], p[1]));
|
|
@@ -5234,7 +5446,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5234
5446
|
const { segments: segs, totalLength } = polylineSegments(verts);
|
|
5235
5447
|
if (totalLength >= 1e-6) for (let k = 0; k < reps; k++) {
|
|
5236
5448
|
const evenFraction = (k + 1) / (reps + 1);
|
|
5237
|
-
const fraction = Math.min(1, Math.max(0, .5 + (evenFraction - .5) * labelSpacing));
|
|
5449
|
+
const fraction = Math.min(1, Math.max(0, reps === 1 ? singleGroupPosition : .5 + (evenFraction - .5) * labelSpacing));
|
|
5238
5450
|
const target = fraction * totalLength;
|
|
5239
5451
|
const frame = pointAlongPolyline(segs, totalLength, fraction);
|
|
5240
5452
|
const center = frame.point;
|
|
@@ -7669,8 +7881,8 @@ const DROP_ZONE_METADATA = {
|
|
|
7669
7881
|
* labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
|
|
7670
7882
|
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7671
7883
|
*/
|
|
7672
|
-
function createDropZone(positions, options = {}, textAmplifiers = {}) {
|
|
7673
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
|
|
7884
|
+
function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7885
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
|
|
7674
7886
|
}
|
|
7675
7887
|
const DROP_ZONE = defineControlMeasure({
|
|
7676
7888
|
metadata: DROP_ZONE_METADATA,
|
|
@@ -7784,11 +7996,11 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7784
7996
|
* Creates an Encirclement tactical graphic: a complete smooth boundary ring
|
|
7785
7997
|
* with outward arrowhead barbs resting on it. The boundary is an unfilled
|
|
7786
7998
|
* Polygon; the barbs are open "V" strokes (a MultiLineString) whose base is the
|
|
7787
|
-
* boundary arc itself. When a Field N (ENY) marker is set, it
|
|
7788
|
-
* boundary
|
|
7789
|
-
* glyph, ADR-0023) — the boundary becomes a
|
|
7790
|
-
* a closed Polygon, and any barb whose
|
|
7791
|
-
* suppressed; the no-`N` case is unaffected.
|
|
7999
|
+
* boundary arc itself. When a Field N (ENY) marker is set, it masks the
|
|
8000
|
+
* boundary with a knockout gap wherever its text box touches the ring
|
|
8001
|
+
* (mirroring Field B's echelon glyph, ADR-0023) — the boundary becomes a
|
|
8002
|
+
* gapped MultiLineString instead of a closed Polygon, and any barb whose
|
|
8003
|
+
* center falls inside the gap is suppressed; the no-`N` case is unaffected.
|
|
7792
8004
|
*
|
|
7793
8005
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
7794
8006
|
* at the render seam (ADR-0014).
|
|
@@ -7796,12 +8008,12 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7796
8008
|
* @param textAmplifiers - Field N only — rendered as "ENY" markers at the
|
|
7797
8009
|
* ring's west and east edges, per the doctrinal hostile example.
|
|
7798
8010
|
*/
|
|
7799
|
-
function createEncirclement(positions, options = {}, textAmplifiers = {}) {
|
|
8011
|
+
function createEncirclement(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7800
8012
|
const { barbSpacingRatio = DEFAULT_BARB_SPACING_RATIO, barbLengthRatio = DEFAULT_BARB_LENGTH_RATIO, smooth = DEFAULT_ENCIRCLEMENT_OPTIONS.smooth, smoothResolution = DEFAULT_ENCIRCLEMENT_OPTIONS.smoothResolution } = options;
|
|
7801
8013
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
|
|
7802
8014
|
const perimeter = buildAreaPerimeter(verts);
|
|
7803
8015
|
const labelFeatures = [];
|
|
7804
|
-
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options);
|
|
8016
|
+
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
|
|
7805
8017
|
const radius = meanRadius(verts);
|
|
7806
8018
|
const barbLength = radius * Math.max(0, barbLengthRatio);
|
|
7807
8019
|
const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
|
|
@@ -7876,8 +8088,8 @@ const EXTRACTION_ZONE_METADATA = {
|
|
|
7876
8088
|
* `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
|
|
7877
8089
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7878
8090
|
*/
|
|
7879
|
-
function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
|
|
7880
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
|
|
8091
|
+
function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
8092
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
|
|
7881
8093
|
}
|
|
7882
8094
|
const EXTRACTION_ZONE = defineControlMeasure({
|
|
7883
8095
|
metadata: EXTRACTION_ZONE_METADATA,
|
|
@@ -8773,11 +8985,15 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8773
8985
|
value: "151000",
|
|
8774
8986
|
minCoordinates: 3,
|
|
8775
8987
|
geometry: "area",
|
|
8776
|
-
geometryTypes: [
|
|
8988
|
+
geometryTypes: [
|
|
8989
|
+
"Polygon",
|
|
8990
|
+
"MultiLineString",
|
|
8991
|
+
"Point"
|
|
8992
|
+
],
|
|
8777
8993
|
drawRule: "Area1",
|
|
8778
8994
|
capturesLabelSize: true,
|
|
8779
8995
|
params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
|
|
8780
|
-
textAmplifiers:
|
|
8996
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
8781
8997
|
};
|
|
8782
8998
|
/**
|
|
8783
8999
|
* Creates a Fortified Area tactical graphic.
|
|
@@ -8791,12 +9007,15 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8791
9007
|
* @param options - Configuration options for the graphic
|
|
8792
9008
|
* @param textAmplifiers - Field T (bare, no prefix — "TANGO", not "FA TANGO")
|
|
8793
9009
|
* plus Field H/W/W1, centered on the pre-teeth boundary ring (see
|
|
8794
|
-
* `boundaryInput` below)
|
|
8795
|
-
*
|
|
8796
|
-
* @
|
|
8797
|
-
*
|
|
8798
|
-
|
|
8799
|
-
|
|
9010
|
+
* `boundaryInput` below), and an optional Field N "ENY" hostile marker on
|
|
9011
|
+
* that ring's west/east edges, masking the castellated boundary beneath it.
|
|
9012
|
+
* @param context - Persisted amplifier placements used to move or restore each
|
|
9013
|
+
* ENY cutout with its independently dragged label.
|
|
9014
|
+
* @returns A GeoJSON FeatureCollection containing the castellated boundary
|
|
9015
|
+
* (a Polygon normally, or a gapped MultiLineString with Field N) plus any
|
|
9016
|
+
* label Points
|
|
9017
|
+
*/
|
|
9018
|
+
function createFortifiedArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
8800
9019
|
const { smooth = DEFAULT_FORTIFIED_AREA_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution } = options;
|
|
8801
9020
|
const safeSize = calculateEffectiveSize(options);
|
|
8802
9021
|
const projected = positions.map((p) => project(p[0], p[1]));
|
|
@@ -8809,21 +9028,20 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
8809
9028
|
if (vecMag(vecSub(firstPt, lastPt)) > 1e-6) boundaryPoints.push(firstPt);
|
|
8810
9029
|
else boundaryPoints[boundaryPoints.length - 1] = firstPt;
|
|
8811
9030
|
}
|
|
9031
|
+
const labelFeatures = [];
|
|
8812
9032
|
const features = [{
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
9033
|
+
...boundaryFeature(boundaryPoints, pushAreaLabels(labelFeatures, boundaryInput, {
|
|
9034
|
+
name: textAmplifiers.T,
|
|
9035
|
+
additionalInformation: textAmplifiers.H,
|
|
9036
|
+
dtgStart: textAmplifiers.W,
|
|
9037
|
+
dtgEnd: textAmplifiers.W1,
|
|
9038
|
+
hostile: textAmplifiers.N
|
|
9039
|
+
}, options, context.amplifierPlacements, {
|
|
9040
|
+
ringVerts: boundaryPoints,
|
|
9041
|
+
extraClearanceMeters: 3 * safeSize
|
|
9042
|
+
})),
|
|
9043
|
+
properties: {}
|
|
8819
9044
|
}];
|
|
8820
|
-
const labelFeatures = [];
|
|
8821
|
-
pushAreaLabels(labelFeatures, boundaryInput, {
|
|
8822
|
-
name: textAmplifiers.T,
|
|
8823
|
-
additionalInformation: textAmplifiers.H,
|
|
8824
|
-
dtgStart: textAmplifiers.W,
|
|
8825
|
-
dtgEnd: textAmplifiers.W1
|
|
8826
|
-
}, options);
|
|
8827
9045
|
features.push(...labelFeatures);
|
|
8828
9046
|
return {
|
|
8829
9047
|
type: "FeatureCollection",
|
|
@@ -8857,9 +9075,6 @@ const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
|
|
|
8857
9075
|
dtgPosition: .62,
|
|
8858
9076
|
labelPadding: 0
|
|
8859
9077
|
};
|
|
8860
|
-
function midpoint(a, b) {
|
|
8861
|
-
return vecScale(vecAdd(a, b), .5);
|
|
8862
|
-
}
|
|
8863
9078
|
function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
|
|
8864
9079
|
const resolved = {
|
|
8865
9080
|
...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
|
|
@@ -8877,7 +9092,7 @@ function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
|
|
|
8877
9092
|
geometry.headRing[2],
|
|
8878
9093
|
...[...geometry.shaftRight].reverse()
|
|
8879
9094
|
];
|
|
8880
|
-
const centerline = geometry.
|
|
9095
|
+
const centerline = geometry.shaftCenterline;
|
|
8881
9096
|
const { segments, totalLength } = polylineSegments(centerline);
|
|
8882
9097
|
if (segments.length === 0) return {
|
|
8883
9098
|
type: "FeatureCollection",
|
|
@@ -9243,8 +9458,8 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
|
|
|
9243
9458
|
* (Field T) plus `H`/`W`/`W1`/`N` amplifier rows, per
|
|
9244
9459
|
* {@link labeledAreaFeatures}.
|
|
9245
9460
|
*/
|
|
9246
|
-
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9247
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""));
|
|
9461
|
+
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9462
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
|
|
9248
9463
|
}
|
|
9249
9464
|
const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
9250
9465
|
metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
|
|
@@ -9297,8 +9512,8 @@ const LANDING_ZONE_METADATA = {
|
|
|
9297
9512
|
* labeled at its center with "LZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9298
9513
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9299
9514
|
*/
|
|
9300
|
-
function createLandingZone(positions, options = {}, textAmplifiers = {}) {
|
|
9301
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"));
|
|
9515
|
+
function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9516
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
|
|
9302
9517
|
}
|
|
9303
9518
|
const LANDING_ZONE = defineControlMeasure({
|
|
9304
9519
|
metadata: LANDING_ZONE_METADATA,
|
|
@@ -9817,8 +10032,8 @@ const PICKUP_ZONE_METADATA = {
|
|
|
9817
10032
|
* labeled at its center with "PZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9818
10033
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9819
10034
|
*/
|
|
9820
|
-
function createPickupZone(positions, options = {}, textAmplifiers = {}) {
|
|
9821
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"));
|
|
10035
|
+
function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10036
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
|
|
9822
10037
|
}
|
|
9823
10038
|
const PICKUP_ZONE = defineControlMeasure({
|
|
9824
10039
|
metadata: PICKUP_ZONE_METADATA,
|
|
@@ -9852,19 +10067,23 @@ const ASSAULT_POSITION_METADATA = {
|
|
|
9852
10067
|
value: "151500",
|
|
9853
10068
|
minCoordinates: 3,
|
|
9854
10069
|
geometry: "area",
|
|
9855
|
-
geometryTypes: [
|
|
10070
|
+
geometryTypes: [
|
|
10071
|
+
"Polygon",
|
|
10072
|
+
"MultiLineString",
|
|
10073
|
+
"Point"
|
|
10074
|
+
],
|
|
9856
10075
|
drawRule: "Area1",
|
|
9857
10076
|
capturesLabelSize: true,
|
|
9858
|
-
params:
|
|
9859
|
-
textAmplifiers:
|
|
10077
|
+
params: LABELED_AREA_PARAMS,
|
|
10078
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9860
10079
|
};
|
|
9861
10080
|
/**
|
|
9862
10081
|
* Creates an Assault position control measure: a closed area through
|
|
9863
|
-
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T)
|
|
9864
|
-
*
|
|
10082
|
+
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) plus
|
|
10083
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9865
10084
|
*/
|
|
9866
|
-
function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9867
|
-
return labeledAreaFeatures(positions, options,
|
|
10085
|
+
function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10086
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
|
|
9868
10087
|
}
|
|
9869
10088
|
const ASSAULT_POSITION = defineControlMeasure({
|
|
9870
10089
|
metadata: ASSAULT_POSITION_METADATA,
|
|
@@ -9898,19 +10117,23 @@ const ATTACK_POSITION_METADATA = {
|
|
|
9898
10117
|
value: "151600",
|
|
9899
10118
|
minCoordinates: 3,
|
|
9900
10119
|
geometry: "area",
|
|
9901
|
-
geometryTypes: [
|
|
10120
|
+
geometryTypes: [
|
|
10121
|
+
"Polygon",
|
|
10122
|
+
"MultiLineString",
|
|
10123
|
+
"Point"
|
|
10124
|
+
],
|
|
9902
10125
|
drawRule: "Area1",
|
|
9903
10126
|
capturesLabelSize: true,
|
|
9904
|
-
params:
|
|
9905
|
-
textAmplifiers:
|
|
10127
|
+
params: LABELED_AREA_PARAMS,
|
|
10128
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9906
10129
|
};
|
|
9907
10130
|
/**
|
|
9908
10131
|
* Creates an Attack position control measure: a closed area through
|
|
9909
|
-
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T)
|
|
9910
|
-
*
|
|
10132
|
+
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T) plus
|
|
10133
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9911
10134
|
*/
|
|
9912
|
-
function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9913
|
-
return labeledAreaFeatures(positions, options,
|
|
10135
|
+
function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10136
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
|
|
9914
10137
|
}
|
|
9915
10138
|
const ATTACK_POSITION = defineControlMeasure({
|
|
9916
10139
|
metadata: ATTACK_POSITION_METADATA,
|
|
@@ -9944,19 +10167,23 @@ const OBJECTIVE_AREA_METADATA = {
|
|
|
9944
10167
|
value: "151700",
|
|
9945
10168
|
minCoordinates: 3,
|
|
9946
10169
|
geometry: "area",
|
|
9947
|
-
geometryTypes: [
|
|
10170
|
+
geometryTypes: [
|
|
10171
|
+
"Polygon",
|
|
10172
|
+
"MultiLineString",
|
|
10173
|
+
"Point"
|
|
10174
|
+
],
|
|
9948
10175
|
drawRule: "Area1",
|
|
9949
10176
|
capturesLabelSize: true,
|
|
9950
|
-
params:
|
|
9951
|
-
textAmplifiers:
|
|
10177
|
+
params: LABELED_AREA_PARAMS,
|
|
10178
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9952
10179
|
};
|
|
9953
10180
|
/**
|
|
9954
10181
|
* Creates an Objective area control measure: a closed area through
|
|
9955
|
-
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T)
|
|
9956
|
-
*
|
|
10182
|
+
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) plus
|
|
10183
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9957
10184
|
*/
|
|
9958
|
-
function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
|
|
9959
|
-
return labeledAreaFeatures(positions, options,
|
|
10185
|
+
function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10186
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
|
|
9960
10187
|
}
|
|
9961
10188
|
const OBJECTIVE_AREA = defineControlMeasure({
|
|
9962
10189
|
metadata: OBJECTIVE_AREA_METADATA,
|
|
@@ -10079,7 +10306,7 @@ const STRONG_POINT_METADATA = {
|
|
|
10079
10306
|
drawRule: "Area1",
|
|
10080
10307
|
capturesLabelSize: true,
|
|
10081
10308
|
params: ECHELON_AREA_PARAMS,
|
|
10082
|
-
textAmplifiers:
|
|
10309
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
10083
10310
|
};
|
|
10084
10311
|
/**
|
|
10085
10312
|
* Outward perimeter tics: a stroke every `h` meters along the ring, each `h`
|
|
@@ -10114,17 +10341,24 @@ function buildStrongPointTics(perimeter, gaps, h) {
|
|
|
10114
10341
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
10115
10342
|
* at the render seam (ADR-0014).
|
|
10116
10343
|
* @param options - {@link StrongPointOptions}.
|
|
10117
|
-
* @param textAmplifiers - Field T
|
|
10118
|
-
*
|
|
10344
|
+
* @param textAmplifiers - Field T (designation, centered in the area) plus an
|
|
10345
|
+
* optional Field N "ENY" hostile marker at the west/east edges, same as
|
|
10346
|
+
* Battle Position.
|
|
10119
10347
|
*/
|
|
10120
|
-
function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
10348
|
+
function createStrongPoint(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10121
10349
|
const { echelon = DEFAULT_STRONG_POINT_OPTIONS.echelon, echelonSize = DEFAULT_STRONG_POINT_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_STRONG_POINT_OPTIONS.echelonPadding, echelonPosition = DEFAULT_STRONG_POINT_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_STRONG_POINT_OPTIONS.smooth, smoothResolution = DEFAULT_STRONG_POINT_OPTIONS.smoothResolution } = options;
|
|
10122
10350
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
10123
10351
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION);
|
|
10124
10352
|
const perimeter = buildAreaPerimeter(verts);
|
|
10125
10353
|
const { strokes, fills, gaps } = placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
10126
|
-
const
|
|
10127
|
-
const
|
|
10354
|
+
const labelFeatures = [];
|
|
10355
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
10356
|
+
name: textAmplifiers.T,
|
|
10357
|
+
hostile: textAmplifiers.N
|
|
10358
|
+
}, options, context.amplifierPlacements);
|
|
10359
|
+
const allGaps = [...gaps, ...enyGaps];
|
|
10360
|
+
const boundaryCoords = buildGappedLine(verts, allGaps);
|
|
10361
|
+
const tics = buildStrongPointTics(perimeter, allGaps, h);
|
|
10128
10362
|
const features = [];
|
|
10129
10363
|
if (boundaryCoords.length > 0) features.push({
|
|
10130
10364
|
type: "Feature",
|
|
@@ -10161,8 +10395,6 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
|
10161
10395
|
coordinates: fills.map((ring) => [ring])
|
|
10162
10396
|
}
|
|
10163
10397
|
});
|
|
10164
|
-
const labelFeatures = [];
|
|
10165
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
10166
10398
|
features.push(...labelFeatures);
|
|
10167
10399
|
return {
|
|
10168
10400
|
type: "FeatureCollection",
|
|
@@ -10222,8 +10454,8 @@ const SUBMARINE_ACTION_AREA_METADATA = {
|
|
|
10222
10454
|
* `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
|
|
10223
10455
|
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10224
10456
|
*/
|
|
10225
|
-
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10226
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
|
|
10457
|
+
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10458
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
|
|
10227
10459
|
}
|
|
10228
10460
|
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
10229
10461
|
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
@@ -10277,8 +10509,8 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
|
10277
10509
|
* through `positions` labeled at its center with "SGSA -"-prefixed `T`
|
|
10278
10510
|
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10279
10511
|
*/
|
|
10280
|
-
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10281
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
|
|
10512
|
+
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10513
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
|
|
10282
10514
|
}
|
|
10283
10515
|
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
10284
10516
|
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
@@ -10860,141 +11092,6 @@ function applyLayer(target, layer) {
|
|
|
10860
11092
|
}
|
|
10861
11093
|
}
|
|
10862
11094
|
//#endregion
|
|
10863
|
-
//#region src/text-amplifiers.ts
|
|
10864
|
-
/**
|
|
10865
|
-
* The vocabulary of text amplifier fields: doctrinal code, milsymbol-style
|
|
10866
|
-
* friendly alias, and a human-readable label. The single source of truth for
|
|
10867
|
-
* both the runtime catalog and the derived alias→code lookup used by
|
|
10868
|
-
* {@link normalizeTextAmplifiers}.
|
|
10869
|
-
*/
|
|
10870
|
-
const TEXT_AMPLIFIER_FIELDS = {
|
|
10871
|
-
C: {
|
|
10872
|
-
alias: "quantity",
|
|
10873
|
-
label: "Quantity"
|
|
10874
|
-
},
|
|
10875
|
-
H: {
|
|
10876
|
-
alias: "additionalInformation",
|
|
10877
|
-
label: "Additional information"
|
|
10878
|
-
},
|
|
10879
|
-
N: {
|
|
10880
|
-
alias: "hostile",
|
|
10881
|
-
label: "Hostile (ENY) marker"
|
|
10882
|
-
},
|
|
10883
|
-
T: {
|
|
10884
|
-
alias: "uniqueDesignation",
|
|
10885
|
-
label: "Unique designation"
|
|
10886
|
-
},
|
|
10887
|
-
V: {
|
|
10888
|
-
alias: "type",
|
|
10889
|
-
label: "Equipment/type"
|
|
10890
|
-
},
|
|
10891
|
-
W: {
|
|
10892
|
-
alias: "dtg",
|
|
10893
|
-
label: "Date-time group / O/O"
|
|
10894
|
-
},
|
|
10895
|
-
X: {
|
|
10896
|
-
alias: "altitudeDepth",
|
|
10897
|
-
label: "Altitude/depth"
|
|
10898
|
-
},
|
|
10899
|
-
Y: {
|
|
10900
|
-
alias: "location",
|
|
10901
|
-
label: "Location"
|
|
10902
|
-
},
|
|
10903
|
-
AM: {
|
|
10904
|
-
alias: "distance",
|
|
10905
|
-
label: "Distance/range/radius"
|
|
10906
|
-
},
|
|
10907
|
-
AN: {
|
|
10908
|
-
alias: "azimuth",
|
|
10909
|
-
label: "Azimuth"
|
|
10910
|
-
},
|
|
10911
|
-
AP: {
|
|
10912
|
-
alias: "targetNumber",
|
|
10913
|
-
label: "Target number"
|
|
10914
|
-
},
|
|
10915
|
-
AS: {
|
|
10916
|
-
alias: "country",
|
|
10917
|
-
label: "Country code"
|
|
10918
|
-
}
|
|
10919
|
-
};
|
|
10920
|
-
/** Derived alias→code lookup, so the vocabulary table stays the single source of truth. */
|
|
10921
|
-
const CODE_BY_ALIAS = Object.fromEntries(Object.entries(TEXT_AMPLIFIER_FIELDS).map(([code, { alias }]) => [alias, code]));
|
|
10922
|
-
const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code) => [
|
|
10923
|
-
code,
|
|
10924
|
-
`${code}1`,
|
|
10925
|
-
`${code}2`
|
|
10926
|
-
]));
|
|
10927
|
-
/**
|
|
10928
|
-
* Resolves a raw placement entry to its object form: a bare `Position` is
|
|
10929
|
-
* legacy shorthand for `{ position }`; an object form passes through only the
|
|
10930
|
-
* fields that are actually valid (`position` must be a finite `[lon, lat]`,
|
|
10931
|
-
* `rotation` a finite number); `undefined` resolves to `{}`.
|
|
10932
|
-
*/
|
|
10933
|
-
function resolveAmplifierPlacement(value) {
|
|
10934
|
-
if (value === void 0) return {};
|
|
10935
|
-
if (Array.isArray(value)) return isValidPosition(value) ? { position: value } : {};
|
|
10936
|
-
const result = {};
|
|
10937
|
-
if (value.position !== void 0 && isValidPosition(value.position)) result.position = value.position;
|
|
10938
|
-
if (typeof value.rotation === "number" && Number.isFinite(value.rotation)) result.rotation = value.rotation;
|
|
10939
|
-
return result;
|
|
10940
|
-
}
|
|
10941
|
-
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
10942
|
-
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
10943
|
-
function splitSuffix(key) {
|
|
10944
|
-
const last = key.charAt(key.length - 1);
|
|
10945
|
-
if (last === "1" || last === "2") return {
|
|
10946
|
-
base: key.slice(0, -1),
|
|
10947
|
-
suffix: last
|
|
10948
|
-
};
|
|
10949
|
-
return {
|
|
10950
|
-
base: key,
|
|
10951
|
-
suffix: ""
|
|
10952
|
-
};
|
|
10953
|
-
}
|
|
10954
|
-
/**
|
|
10955
|
-
* Resolves any accepted key (canonical or friendly alias, with an optional
|
|
10956
|
-
* `1`/`2` suffix) to its canonical key, or `undefined` if unknown — e.g.
|
|
10957
|
-
* `"T"` → `"T"`, `"uniqueDesignation1"` → `"T1"`, `"bogus"` → `undefined`.
|
|
10958
|
-
*/
|
|
10959
|
-
function canonicalTextAmplifierKey(key) {
|
|
10960
|
-
if (CANONICAL_KEYS.has(key)) return key;
|
|
10961
|
-
const { base, suffix } = splitSuffix(key);
|
|
10962
|
-
const code = CODE_BY_ALIAS[base];
|
|
10963
|
-
return code ? `${code}${suffix}` : void 0;
|
|
10964
|
-
}
|
|
10965
|
-
/**
|
|
10966
|
-
* Resolves a `TextAmplifiers` record (canonical keys, friendly aliases, or a
|
|
10967
|
-
* mix) to `CanonicalTextAmplifiers` keyed purely by doctrinal field code.
|
|
10968
|
-
*
|
|
10969
|
-
* - Friendly aliases (with an optional `1`/`2` suffix) map to their canonical
|
|
10970
|
-
* code (e.g. `country1` → `AS1`).
|
|
10971
|
-
* - Values are trimmed; entries that are empty/whitespace-only after
|
|
10972
|
-
* trimming, or not strings, are dropped.
|
|
10973
|
-
* - When both a canonical key and its friendly alias are present for the same
|
|
10974
|
-
* field + suffix, the **canonical key wins** regardless of insertion order.
|
|
10975
|
-
* This holds even for an empty/whitespace canonical value: it suppresses
|
|
10976
|
-
* (clears) the alias entry rather than letting the alias survive.
|
|
10977
|
-
* - Unknown keys are ignored.
|
|
10978
|
-
*
|
|
10979
|
-
* `undefined` or an empty input returns a shared frozen empty object.
|
|
10980
|
-
*/
|
|
10981
|
-
function normalizeTextAmplifiers(input) {
|
|
10982
|
-
if (!input) return EMPTY_TEXT_AMPLIFIERS;
|
|
10983
|
-
const canonicalPresent = /* @__PURE__ */ new Set();
|
|
10984
|
-
for (const [key, rawValue] of Object.entries(input)) if (typeof rawValue === "string" && CANONICAL_KEYS.has(key)) canonicalPresent.add(key);
|
|
10985
|
-
const result = {};
|
|
10986
|
-
for (const [key, rawValue] of Object.entries(input)) {
|
|
10987
|
-
if (typeof rawValue !== "string") continue;
|
|
10988
|
-
const value = rawValue.trim();
|
|
10989
|
-
if (value.length === 0) continue;
|
|
10990
|
-
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
10991
|
-
if (!canonicalKey) continue;
|
|
10992
|
-
if (!CANONICAL_KEYS.has(key) && canonicalPresent.has(canonicalKey)) continue;
|
|
10993
|
-
result[canonicalKey] = value;
|
|
10994
|
-
}
|
|
10995
|
-
return Object.keys(result).length > 0 ? Object.freeze(result) : EMPTY_TEXT_AMPLIFIERS;
|
|
10996
|
-
}
|
|
10997
|
-
//#endregion
|
|
10998
11095
|
//#region src/renderControlMeasure.ts
|
|
10999
11096
|
/**
|
|
11000
11097
|
* Pure render: `ControlMeasure` → `ControlMeasureRender` (a
|
|
@@ -11030,7 +11127,7 @@ function dispatchControlMeasure(cm, opts) {
|
|
|
11030
11127
|
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
11031
11128
|
const generator = definition.generator;
|
|
11032
11129
|
const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
|
|
11033
|
-
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers);
|
|
11130
|
+
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
|
|
11034
11131
|
}
|
|
11035
11132
|
/**
|
|
11036
11133
|
* The control-point *input contract*: at least `metadata.minCoordinates` points,
|
|
@@ -11047,15 +11144,15 @@ function validateInputContract(points, metadata, mode) {
|
|
|
11047
11144
|
}
|
|
11048
11145
|
/**
|
|
11049
11146
|
* The `amplifierPlacements` key that repositions this feature, if any: the
|
|
11050
|
-
* canonical amplifier field when the
|
|
11051
|
-
*
|
|
11147
|
+
* explicit `labelPlacementKey`, the canonical amplifier field when the
|
|
11148
|
+
* generator declared one, or a synthesized `label:<part>:<ordinal>` key for any
|
|
11052
11149
|
* other point label — unless the generator opted out via
|
|
11053
11150
|
* `labelPlacement: false`.
|
|
11054
11151
|
*/
|
|
11055
11152
|
function resolveLabelPlacementKey(sourceProps, amplifierField, text, geometryType, part, generatedLabelOrdinal) {
|
|
11056
11153
|
if (sourceProps.labelPlacement === false) return void 0;
|
|
11057
|
-
if (amplifierField) return amplifierField;
|
|
11058
11154
|
if (typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) return sourceProps.labelPlacementKey;
|
|
11155
|
+
if (amplifierField) return amplifierField;
|
|
11059
11156
|
if (text !== void 0 && geometryType === "Point") return `label:${part}:${generatedLabelOrdinal}`;
|
|
11060
11157
|
}
|
|
11061
11158
|
function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyle, graphicsStyle, amplifierPlacements) {
|
|
@@ -11141,4 +11238,4 @@ function assertNever(value) {
|
|
|
11141
11238
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
11142
11239
|
}
|
|
11143
11240
|
//#endregion
|
|
11144
|
-
export {
|
|
11241
|
+
export { DEFAULT_BLOCK_ARROW_OPTIONS as $, DEFAULT_FLOT_OPTIONS as A, disruptDrawRule as At, DEFAULT_GENERIC_RECTANGLE_OPTIONS as B, project as Bt, DEFAULT_SCREEN_OPTIONS as C, line23DrawRule as Ct, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as D, attackByFireDrawRule as Dt, DEFAULT_FORTIFIED_LINE_OPTIONS as E, ambushDrawRule as Et, DEFAULT_GUARD_OPTIONS as F, pointOnMidpointPerpendicularAxis as Ft, DEFAULT_CANALIZE_OPTIONS as G, DEFAULT_GENERIC_LINE_OPTIONS as H, EPSILON as Ht, DEFAULT_DELAY_OPTIONS as I, snapToMidpointPerpendicular as It, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as J, DEFAULT_BYPASS_OPTIONS as K, DEFAULT_COVER_OPTIONS as L, createBaselineFrame as Lt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as M, computeDefaultMidpointPerpendicularPoint as Mt, DEFAULT_ENCIRCLEMENT_OPTIONS as N, createMidpointPerpendicularDrawRule as Nt, DEFAULT_HANDOVER_LINE_OPTIONS as O, point12DrawRule as Ot, DEFAULT_DISRUPT_OPTIONS as P, getMidpointPerpendicularSignedDistance as Pt, DEFAULT_BOUNDARY_OPTIONS as Q, DEFAULT_TACTICAL_ARROW_OPTIONS as R, calculateMetrics as Rt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as S, line24DrawRule as St, DEFAULT_FORTIFIED_AREA_OPTIONS as T, line1DrawRule as Tt, DEFAULT_GENERIC_CIRCLE_OPTIONS as U, getMetersPerPixel as Ut, DEFAULT_GENERIC_POLYGON_OPTIONS as V, unproject as Vt, DEFAULT_CLASSIC_ARROW_OPTIONS as W, roundToFixed as Wt, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as X, DEFAULT_GENERIC_C2_LINE_OPTIONS as Y, DEFAULT_LIGHT_LINE_OPTIONS as Z, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as _, DEFAULT_AIRBORNE_ATTACK_OPTIONS as _t, DEFINITIONS as a, DEFAULT_ANTITANK_DITCH_OPTIONS as at, DEFAULT_MAIN_ATTACK_OPTIONS as b, supportByFireDrawRule as bt, getDefaultOptions as c, DEFAULT_AREA_DEFENSE_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, normalizeTextAmplifiers as dt, DEFAULT_BLOCK_OPTIONS as et, DEFAULT_SUPPORTING_ATTACK_OPTIONS as f, resolveAmplifierPlacement as ft, DEFAULT_PICKUP_ZONE_OPTIONS as g, DEFAULT_AMBUSH_OPTIONS as gt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as h, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_ANTITANK_WALL_OPTIONS as it, DEFAULT_FIX_OPTIONS as j, blockDrawRule as jt, DEFAULT_PHASE_LINE_OPTIONS as k, centerRadiusDrawRule as kt, listControlMeasureMetadata as l, TEXT_AMPLIFIER_FIELDS as lt, DEFAULT_STRONG_POINT_OPTIONS as m, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as mt, resolveStyleHints as n, DEFAULT_ATTACK_HELICOPTER_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_ASSEMBLY_AREA_OPTIONS as ot, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as p, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as pt, DEFAULT_BREACH_OPTIONS as q, CONTROL_MEASURE_IDS as r, DEFAULT_ATTACK_BY_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as st, renderControlMeasure as t, DEFAULT_BATTLE_POSITION_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, canonicalTextAmplifierKey as ut, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as v, rectangleDrawRule as vt, DEFAULT_FRONTAL_ATTACK_OPTIONS as w, turnDrawRule as wt, DEFAULT_LANDING_ZONE_OPTIONS as x, line26DrawRule as xt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as y, axis1DrawRule as yt, DEFAULT_CLEAR_OPTIONS as z, computeInitialWidthPoint as zt };
|