@orbat-mapper/control-measures 0.2.0-alpha.16 → 0.2.0-alpha.17
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-9CY6UM92.d.mts} +65 -49
- 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-BVbkVj2S.mjs} +376 -289
- package/package.json +1 -1
|
@@ -1411,6 +1411,13 @@ const AREA_DESIGNATION_AMPLIFIER = {
|
|
|
1411
1411
|
description: "Field T — the unit's designation, centered in the area.",
|
|
1412
1412
|
maxLength: 21
|
|
1413
1413
|
};
|
|
1414
|
+
const AREA_HOSTILE_AMPLIFIER = {
|
|
1415
|
+
key: "N",
|
|
1416
|
+
label: "Hostile (ENY) marker",
|
|
1417
|
+
description: "Field N — literal \"ENY\" marker at the area's west and east edges.",
|
|
1418
|
+
placeholder: "ENY",
|
|
1419
|
+
maxLength: 3
|
|
1420
|
+
};
|
|
1414
1421
|
const AREA_TEXT_AMPLIFIERS = [
|
|
1415
1422
|
{
|
|
1416
1423
|
key: "T",
|
|
@@ -1437,17 +1444,9 @@ const AREA_TEXT_AMPLIFIERS = [
|
|
|
1437
1444
|
description: "Field W1 — date-time group the area ceases to be effective.",
|
|
1438
1445
|
maxLength: 16
|
|
1439
1446
|
},
|
|
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
|
-
}
|
|
1447
|
+
AREA_HOSTILE_AMPLIFIER
|
|
1447
1448
|
];
|
|
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");
|
|
1449
|
+
const AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS = [AREA_DESIGNATION_AMPLIFIER, AREA_HOSTILE_AMPLIFIER];
|
|
1451
1450
|
/** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
|
|
1452
1451
|
* for measures whose template omits that row but keeps the W/W1 window and
|
|
1453
1452
|
* the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
|
|
@@ -2040,6 +2039,10 @@ function polylineSegments(verts) {
|
|
|
2040
2039
|
totalLength
|
|
2041
2040
|
};
|
|
2042
2041
|
}
|
|
2042
|
+
/** Resolves an along-line position onto the valid start-to-end range. */
|
|
2043
|
+
function clampLinePosition(value, fallback) {
|
|
2044
|
+
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
2045
|
+
}
|
|
2043
2046
|
/**
|
|
2044
2047
|
* Resolves the point at `fraction` (clamped to `[0, 1]`) of `totalLength`
|
|
2045
2048
|
* along `segments`, walking cumulative arc length exactly like Boundary's
|
|
@@ -2135,7 +2138,7 @@ function labelSizeProps(options) {
|
|
|
2135
2138
|
return {};
|
|
2136
2139
|
}
|
|
2137
2140
|
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2138
|
-
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField) {
|
|
2141
|
+
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField, labelPlacementKey) {
|
|
2139
2142
|
out.push({
|
|
2140
2143
|
type: "Feature",
|
|
2141
2144
|
properties: {
|
|
@@ -2144,6 +2147,7 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierF
|
|
|
2144
2147
|
rotation,
|
|
2145
2148
|
...textAnchor ? { textAnchor } : {},
|
|
2146
2149
|
...amplifierField ? { amplifierField } : {},
|
|
2150
|
+
...labelPlacementKey ? { labelPlacementKey } : {},
|
|
2147
2151
|
...sizeProps
|
|
2148
2152
|
},
|
|
2149
2153
|
geometry: {
|
|
@@ -2297,10 +2301,6 @@ const LABEL_GAP_CLEARANCE_RATIO = .3;
|
|
|
2297
2301
|
function nonNegative$1(value, fallback) {
|
|
2298
2302
|
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
2299
2303
|
}
|
|
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
2304
|
/** Arc-length knockout centered on an along-route text label. */
|
|
2305
2305
|
function directionLabelGap(totalLength, fraction, text, options) {
|
|
2306
2306
|
if (!text) return void 0;
|
|
@@ -2788,6 +2788,141 @@ const DIRECTION_OF_SUPPORTING_ATTACK = defineControlMeasure({
|
|
|
2788
2788
|
}
|
|
2789
2789
|
});
|
|
2790
2790
|
//#endregion
|
|
2791
|
+
//#region src/text-amplifiers.ts
|
|
2792
|
+
/**
|
|
2793
|
+
* The vocabulary of text amplifier fields: doctrinal code, milsymbol-style
|
|
2794
|
+
* friendly alias, and a human-readable label. The single source of truth for
|
|
2795
|
+
* both the runtime catalog and the derived alias→code lookup used by
|
|
2796
|
+
* {@link normalizeTextAmplifiers}.
|
|
2797
|
+
*/
|
|
2798
|
+
const TEXT_AMPLIFIER_FIELDS = {
|
|
2799
|
+
C: {
|
|
2800
|
+
alias: "quantity",
|
|
2801
|
+
label: "Quantity"
|
|
2802
|
+
},
|
|
2803
|
+
H: {
|
|
2804
|
+
alias: "additionalInformation",
|
|
2805
|
+
label: "Additional information"
|
|
2806
|
+
},
|
|
2807
|
+
N: {
|
|
2808
|
+
alias: "hostile",
|
|
2809
|
+
label: "Hostile (ENY) marker"
|
|
2810
|
+
},
|
|
2811
|
+
T: {
|
|
2812
|
+
alias: "uniqueDesignation",
|
|
2813
|
+
label: "Unique designation"
|
|
2814
|
+
},
|
|
2815
|
+
V: {
|
|
2816
|
+
alias: "type",
|
|
2817
|
+
label: "Equipment/type"
|
|
2818
|
+
},
|
|
2819
|
+
W: {
|
|
2820
|
+
alias: "dtg",
|
|
2821
|
+
label: "Date-time group / O/O"
|
|
2822
|
+
},
|
|
2823
|
+
X: {
|
|
2824
|
+
alias: "altitudeDepth",
|
|
2825
|
+
label: "Altitude/depth"
|
|
2826
|
+
},
|
|
2827
|
+
Y: {
|
|
2828
|
+
alias: "location",
|
|
2829
|
+
label: "Location"
|
|
2830
|
+
},
|
|
2831
|
+
AM: {
|
|
2832
|
+
alias: "distance",
|
|
2833
|
+
label: "Distance/range/radius"
|
|
2834
|
+
},
|
|
2835
|
+
AN: {
|
|
2836
|
+
alias: "azimuth",
|
|
2837
|
+
label: "Azimuth"
|
|
2838
|
+
},
|
|
2839
|
+
AP: {
|
|
2840
|
+
alias: "targetNumber",
|
|
2841
|
+
label: "Target number"
|
|
2842
|
+
},
|
|
2843
|
+
AS: {
|
|
2844
|
+
alias: "country",
|
|
2845
|
+
label: "Country code"
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
/** Derived alias→code lookup, so the vocabulary table stays the single source of truth. */
|
|
2849
|
+
const CODE_BY_ALIAS = Object.fromEntries(Object.entries(TEXT_AMPLIFIER_FIELDS).map(([code, { alias }]) => [alias, code]));
|
|
2850
|
+
const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code) => [
|
|
2851
|
+
code,
|
|
2852
|
+
`${code}1`,
|
|
2853
|
+
`${code}2`
|
|
2854
|
+
]));
|
|
2855
|
+
/**
|
|
2856
|
+
* Resolves a raw placement entry to its object form: a bare `Position` is
|
|
2857
|
+
* legacy shorthand for `{ position }`; an object form passes through only the
|
|
2858
|
+
* fields that are actually valid (`position` must be a finite `[lon, lat]`,
|
|
2859
|
+
* `rotation` a finite number); `undefined` resolves to `{}`.
|
|
2860
|
+
*/
|
|
2861
|
+
function resolveAmplifierPlacement(value) {
|
|
2862
|
+
if (value === void 0) return {};
|
|
2863
|
+
if (Array.isArray(value)) return isValidPosition(value) ? { position: value } : {};
|
|
2864
|
+
const result = {};
|
|
2865
|
+
if (value.position !== void 0 && isValidPosition(value.position)) result.position = value.position;
|
|
2866
|
+
if (typeof value.rotation === "number" && Number.isFinite(value.rotation)) result.rotation = value.rotation;
|
|
2867
|
+
return result;
|
|
2868
|
+
}
|
|
2869
|
+
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
2870
|
+
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
2871
|
+
function splitSuffix(key) {
|
|
2872
|
+
const last = key.charAt(key.length - 1);
|
|
2873
|
+
if (last === "1" || last === "2") return {
|
|
2874
|
+
base: key.slice(0, -1),
|
|
2875
|
+
suffix: last
|
|
2876
|
+
};
|
|
2877
|
+
return {
|
|
2878
|
+
base: key,
|
|
2879
|
+
suffix: ""
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Resolves any accepted key (canonical or friendly alias, with an optional
|
|
2884
|
+
* `1`/`2` suffix) to its canonical key, or `undefined` if unknown — e.g.
|
|
2885
|
+
* `"T"` → `"T"`, `"uniqueDesignation1"` → `"T1"`, `"bogus"` → `undefined`.
|
|
2886
|
+
*/
|
|
2887
|
+
function canonicalTextAmplifierKey(key) {
|
|
2888
|
+
if (CANONICAL_KEYS.has(key)) return key;
|
|
2889
|
+
const { base, suffix } = splitSuffix(key);
|
|
2890
|
+
const code = CODE_BY_ALIAS[base];
|
|
2891
|
+
return code ? `${code}${suffix}` : void 0;
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Resolves a `TextAmplifiers` record (canonical keys, friendly aliases, or a
|
|
2895
|
+
* mix) to `CanonicalTextAmplifiers` keyed purely by doctrinal field code.
|
|
2896
|
+
*
|
|
2897
|
+
* - Friendly aliases (with an optional `1`/`2` suffix) map to their canonical
|
|
2898
|
+
* code (e.g. `country1` → `AS1`).
|
|
2899
|
+
* - Values are trimmed; entries that are empty/whitespace-only after
|
|
2900
|
+
* trimming, or not strings, are dropped.
|
|
2901
|
+
* - When both a canonical key and its friendly alias are present for the same
|
|
2902
|
+
* field + suffix, the **canonical key wins** regardless of insertion order.
|
|
2903
|
+
* This holds even for an empty/whitespace canonical value: it suppresses
|
|
2904
|
+
* (clears) the alias entry rather than letting the alias survive.
|
|
2905
|
+
* - Unknown keys are ignored.
|
|
2906
|
+
*
|
|
2907
|
+
* `undefined` or an empty input returns a shared frozen empty object.
|
|
2908
|
+
*/
|
|
2909
|
+
function normalizeTextAmplifiers(input) {
|
|
2910
|
+
if (!input) return EMPTY_TEXT_AMPLIFIERS;
|
|
2911
|
+
const canonicalPresent = /* @__PURE__ */ new Set();
|
|
2912
|
+
for (const [key, rawValue] of Object.entries(input)) if (typeof rawValue === "string" && CANONICAL_KEYS.has(key)) canonicalPresent.add(key);
|
|
2913
|
+
const result = {};
|
|
2914
|
+
for (const [key, rawValue] of Object.entries(input)) {
|
|
2915
|
+
if (typeof rawValue !== "string") continue;
|
|
2916
|
+
const value = rawValue.trim();
|
|
2917
|
+
if (value.length === 0) continue;
|
|
2918
|
+
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
2919
|
+
if (!canonicalKey) continue;
|
|
2920
|
+
if (!CANONICAL_KEYS.has(key) && canonicalPresent.has(canonicalKey)) continue;
|
|
2921
|
+
result[canonicalKey] = value;
|
|
2922
|
+
}
|
|
2923
|
+
return Object.keys(result).length > 0 ? Object.freeze(result) : EMPTY_TEXT_AMPLIFIERS;
|
|
2924
|
+
}
|
|
2925
|
+
//#endregion
|
|
2791
2926
|
//#region src/internal/area-echelon.ts
|
|
2792
2927
|
/**
|
|
2793
2928
|
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
@@ -3055,6 +3190,36 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3055
3190
|
totalArc: dist
|
|
3056
3191
|
};
|
|
3057
3192
|
}
|
|
3193
|
+
/** Nearest contact point on a closed ring for a dragged marker anchor. */
|
|
3194
|
+
function nearestPointOnRing(verts, target) {
|
|
3195
|
+
let nearest;
|
|
3196
|
+
let totalArc = 0;
|
|
3197
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
3198
|
+
const a = verts[i];
|
|
3199
|
+
const b = verts[i + 1];
|
|
3200
|
+
const seg = vecSub(b, a);
|
|
3201
|
+
const length = vecMag(seg);
|
|
3202
|
+
if (length < 1e-6) continue;
|
|
3203
|
+
const along = vecScale(seg, 1 / length);
|
|
3204
|
+
const t = Math.max(0, Math.min(length, vecDot(vecSub(target, a), along)));
|
|
3205
|
+
const distance = vecMag(vecSub(target, vecAdd(a, vecScale(along, t))));
|
|
3206
|
+
if (!nearest || distance < nearest.distance) nearest = {
|
|
3207
|
+
along,
|
|
3208
|
+
arc: totalArc + t,
|
|
3209
|
+
distance
|
|
3210
|
+
};
|
|
3211
|
+
totalArc += length;
|
|
3212
|
+
}
|
|
3213
|
+
return nearest ? {
|
|
3214
|
+
...nearest,
|
|
3215
|
+
totalArc
|
|
3216
|
+
} : void 0;
|
|
3217
|
+
}
|
|
3218
|
+
function labelHalfExtent(direction, textWidth, textHeight, rotation) {
|
|
3219
|
+
const widthAxis = [Math.cos(rotation), Math.sin(rotation)];
|
|
3220
|
+
const heightAxis = [-widthAxis[1], widthAxis[0]];
|
|
3221
|
+
return .5 * (textWidth * Math.abs(vecDot(direction, widthAxis)) + textHeight * Math.abs(vecDot(direction, heightAxis)));
|
|
3222
|
+
}
|
|
3058
3223
|
/**
|
|
3059
3224
|
* Pushes an area's L-15 text amplifier labels — centered name/H/W-W1 rows and
|
|
3060
3225
|
* west/east "ENY" edge markers — as `part: "label"` Point features (via
|
|
@@ -3073,25 +3238,27 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3073
3238
|
*
|
|
3074
3239
|
* When `texts.hostile` is set, the ring's west/east crossings of the
|
|
3075
3240
|
* horizontal line through the centroid are found ({@link ringCrossingsAtY});
|
|
3076
|
-
* the minimum/maximum-x crossings
|
|
3077
|
-
*
|
|
3078
|
-
* Each marker
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3082
|
-
*
|
|
3241
|
+
* the minimum/maximum-x crossings are the default anchors for the two "ENY"
|
|
3242
|
+
* labels. A persisted `label:N:west` / `label:N:east` placement replaces that
|
|
3243
|
+
* anchor. Each marker carves a masking {@link ArcGap} at the nearest point on
|
|
3244
|
+
* the boundary only while its estimated text box touches the boundary; moving
|
|
3245
|
+
* it away restores the line. The gap follows the label around any edge and is
|
|
3246
|
+
* sized as the support function of its possibly rotated text box along the
|
|
3247
|
+
* contacted edge's own direction `d` (a boundary running vertically there
|
|
3248
|
+
* needs roughly the label's height; one running horizontally needs roughly
|
|
3249
|
+
* its width):
|
|
3083
3250
|
* `halfExtent = 0.5 * (textWidth·|d.x| + textHeight·|d.y|) + clearance`, where
|
|
3084
3251
|
* `textHeight = resolveLabelOffsetMeters(options, 1)`,
|
|
3085
3252
|
* `textWidth = text.length * ENY_CHAR_ASPECT_RATIO * textHeight`, and
|
|
3086
3253
|
* `clearance = (ENY_CLEARANCE_RATIO + labelPadding) * textHeight` — mirroring
|
|
3087
3254
|
* {@link placeEchelon}'s glyph-width masking gap (ADR-0023). Fewer than two
|
|
3088
3255
|
* crossings (e.g. a degenerate ring, or a concave shape whose centroid
|
|
3089
|
-
* latitude misses the boundary) skips the
|
|
3090
|
-
*
|
|
3256
|
+
* latitude misses the boundary) skips the markers silently rather than
|
|
3257
|
+
* guessing their default placements.
|
|
3091
3258
|
*
|
|
3092
3259
|
* A ring with fewer than 2 vertices is degenerate — nothing is placed.
|
|
3093
3260
|
*/
|
|
3094
|
-
function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
3261
|
+
function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements) {
|
|
3095
3262
|
if (ringVerts.length < 2) return [];
|
|
3096
3263
|
const sizeProps = labelSizeProps(options);
|
|
3097
3264
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
@@ -3116,15 +3283,26 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
3116
3283
|
const { crossings, totalArc } = ringCrossingsAtY(ringVerts, cy);
|
|
3117
3284
|
if (crossings.length >= 2) {
|
|
3118
3285
|
const byX = [...crossings].sort((a, b) => a.x - b.x);
|
|
3119
|
-
const west = byX[0];
|
|
3120
|
-
const east = byX[byX.length - 1];
|
|
3121
3286
|
const textHeight = resolveLabelOffsetMeters(options, 1);
|
|
3122
3287
|
const textWidth = texts.hostile.length * ENY_CHAR_ASPECT_RATIO * textHeight;
|
|
3123
3288
|
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight;
|
|
3124
|
-
for (const crossing of [west, east]) {
|
|
3125
|
-
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3289
|
+
for (const [side, crossing] of [["west", byX[0]], ["east", byX[byX.length - 1]]]) {
|
|
3290
|
+
const labelPlacementKey = `label:N:${side}`;
|
|
3291
|
+
const placement = resolveAmplifierPlacement(amplifierPlacements?.[labelPlacementKey]);
|
|
3292
|
+
const rotation = placement.rotation ?? HORIZONTAL_LABEL_ROTATION;
|
|
3293
|
+
const labelPoint = placement.position ? project(placement.position[0], placement.position[1]) : [crossing.x, cy];
|
|
3294
|
+
pushLabel(out, labelPoint, texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, "N", labelPlacementKey);
|
|
3295
|
+
const contact = placement.position ? nearestPointOnRing(ringVerts, labelPoint) : {
|
|
3296
|
+
along: crossing.along,
|
|
3297
|
+
arc: crossing.arc,
|
|
3298
|
+
distance: 0,
|
|
3299
|
+
totalArc
|
|
3300
|
+
};
|
|
3301
|
+
if (!contact) continue;
|
|
3302
|
+
const normal = [-contact.along[1], contact.along[0]];
|
|
3303
|
+
if (contact.distance > labelHalfExtent(normal, textWidth, textHeight, rotation)) continue;
|
|
3304
|
+
const tangentExtent = labelHalfExtent(contact.along, textWidth, textHeight, rotation) + clearance;
|
|
3305
|
+
pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
|
|
3128
3306
|
}
|
|
3129
3307
|
}
|
|
3130
3308
|
}
|
|
@@ -3165,11 +3343,11 @@ function boundaryFeature(verts, gaps) {
|
|
|
3165
3343
|
* echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
|
|
3166
3344
|
* common (no `N`) case is unaffected.
|
|
3167
3345
|
*/
|
|
3168
|
-
function labeledAreaFeatures(positions, options, texts) {
|
|
3346
|
+
function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3169
3347
|
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
3170
3348
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
3171
3349
|
const labelFeatures = [];
|
|
3172
|
-
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
|
|
3350
|
+
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
|
|
3173
3351
|
features.push(...labelFeatures);
|
|
3174
3352
|
return {
|
|
3175
3353
|
type: "FeatureCollection",
|
|
@@ -3207,8 +3385,8 @@ const AREA_METADATA = {
|
|
|
3207
3385
|
* at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
|
|
3208
3386
|
* marker, per {@link labeledAreaFeatures}.
|
|
3209
3387
|
*/
|
|
3210
|
-
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
3211
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
3388
|
+
function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3389
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
|
|
3212
3390
|
}
|
|
3213
3391
|
const AREA = defineControlMeasure({
|
|
3214
3392
|
metadata: AREA_METADATA,
|
|
@@ -3454,8 +3632,8 @@ const AREA_OF_OPERATIONS_METADATA = {
|
|
|
3454
3632
|
* `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
|
|
3455
3633
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3456
3634
|
*/
|
|
3457
|
-
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
|
|
3458
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
|
|
3635
|
+
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3636
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
|
|
3459
3637
|
}
|
|
3460
3638
|
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
3461
3639
|
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
@@ -3474,15 +3652,6 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
|
3474
3652
|
}
|
|
3475
3653
|
});
|
|
3476
3654
|
//#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
3655
|
//#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
|
|
3487
3656
|
/**
|
|
3488
3657
|
* Default options for the Named area of interest control measure. The
|
|
@@ -3502,19 +3671,23 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
|
3502
3671
|
value: "120200",
|
|
3503
3672
|
minCoordinates: 3,
|
|
3504
3673
|
geometry: "area",
|
|
3505
|
-
geometryTypes: [
|
|
3674
|
+
geometryTypes: [
|
|
3675
|
+
"Polygon",
|
|
3676
|
+
"MultiLineString",
|
|
3677
|
+
"Point"
|
|
3678
|
+
],
|
|
3506
3679
|
drawRule: "Area1",
|
|
3507
3680
|
capturesLabelSize: true,
|
|
3508
|
-
params:
|
|
3509
|
-
textAmplifiers:
|
|
3681
|
+
params: LABELED_AREA_PARAMS,
|
|
3682
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3510
3683
|
};
|
|
3511
3684
|
/**
|
|
3512
3685
|
* 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
|
-
*
|
|
3686
|
+
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T) plus
|
|
3687
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3515
3688
|
*/
|
|
3516
|
-
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3517
|
-
return labeledAreaFeatures(positions, options,
|
|
3689
|
+
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3690
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
|
|
3518
3691
|
}
|
|
3519
3692
|
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
3520
3693
|
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
@@ -3552,19 +3725,23 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
|
3552
3725
|
value: "120300",
|
|
3553
3726
|
minCoordinates: 3,
|
|
3554
3727
|
geometry: "area",
|
|
3555
|
-
geometryTypes: [
|
|
3728
|
+
geometryTypes: [
|
|
3729
|
+
"Polygon",
|
|
3730
|
+
"MultiLineString",
|
|
3731
|
+
"Point"
|
|
3732
|
+
],
|
|
3556
3733
|
drawRule: "Area1",
|
|
3557
3734
|
capturesLabelSize: true,
|
|
3558
|
-
params:
|
|
3559
|
-
textAmplifiers:
|
|
3735
|
+
params: LABELED_AREA_PARAMS,
|
|
3736
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3560
3737
|
};
|
|
3561
3738
|
/**
|
|
3562
3739
|
* 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
|
-
*
|
|
3740
|
+
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T) plus
|
|
3741
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3565
3742
|
*/
|
|
3566
|
-
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3567
|
-
return labeledAreaFeatures(positions, options,
|
|
3743
|
+
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3744
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
|
|
3568
3745
|
}
|
|
3569
3746
|
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
3570
3747
|
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
@@ -3618,21 +3795,25 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3618
3795
|
],
|
|
3619
3796
|
drawRule: "Area1",
|
|
3620
3797
|
capturesLabelSize: true,
|
|
3621
|
-
params: [
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3798
|
+
params: [
|
|
3799
|
+
...AREA_SMOOTH_PARAMS,
|
|
3800
|
+
{
|
|
3801
|
+
key: "glyphSizeRatio",
|
|
3802
|
+
label: "Runway size",
|
|
3803
|
+
description: "Size of the runway glyph as a ratio of the area's width.",
|
|
3804
|
+
type: "number",
|
|
3805
|
+
min: .2,
|
|
3806
|
+
max: .9,
|
|
3807
|
+
step: .05
|
|
3808
|
+
},
|
|
3809
|
+
AREA_LABEL_PADDING_PARAM
|
|
3810
|
+
],
|
|
3630
3811
|
textAmplifiers: [{
|
|
3631
3812
|
key: "H",
|
|
3632
3813
|
label: "Additional information",
|
|
3633
3814
|
description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
|
|
3634
3815
|
maxLength: 20
|
|
3635
|
-
}]
|
|
3816
|
+
}, AREA_HOSTILE_AMPLIFIER]
|
|
3636
3817
|
};
|
|
3637
3818
|
/**
|
|
3638
3819
|
* Creates an Airfield zone control measure: a closed area through
|
|
@@ -3643,12 +3824,14 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3643
3824
|
* @param positions - Area anchor points (≥3); the input contract is enforced
|
|
3644
3825
|
* at the render seam (ADR-0014).
|
|
3645
3826
|
* @param options - {@link AirfieldZoneOptions}.
|
|
3646
|
-
* @param textAmplifiers - Field H
|
|
3827
|
+
* @param textAmplifiers - Field H (placed east of the area) plus an optional
|
|
3828
|
+
* Field N "ENY" hostile marker on the west/east edges.
|
|
3647
3829
|
*/
|
|
3648
|
-
function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
3830
|
+
function createAirfieldZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3649
3831
|
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
3650
3832
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
3651
|
-
const
|
|
3833
|
+
const enyLabels = [];
|
|
3834
|
+
const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
|
|
3652
3835
|
let minX = Infinity;
|
|
3653
3836
|
let maxX = -Infinity;
|
|
3654
3837
|
for (const [x] of verts) {
|
|
@@ -3680,6 +3863,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
|
3680
3863
|
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start", "H");
|
|
3681
3864
|
features.push(...labelFeatures);
|
|
3682
3865
|
}
|
|
3866
|
+
features.push(...enyLabels);
|
|
3683
3867
|
return {
|
|
3684
3868
|
type: "FeatureCollection",
|
|
3685
3869
|
features
|
|
@@ -3723,13 +3907,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
|
3723
3907
|
* before carving the boundary, so either (or both) opens it into a gapped
|
|
3724
3908
|
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
3725
3909
|
*/
|
|
3726
|
-
function echelonLabeledAreaFeatures(positions, options, texts) {
|
|
3910
|
+
function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3727
3911
|
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
3912
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
3729
3913
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
3730
3914
|
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
3731
3915
|
const labelFeatures = [];
|
|
3732
|
-
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
|
|
3916
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
|
|
3733
3917
|
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
3734
3918
|
if (strokes.length > 0) features.push({
|
|
3735
3919
|
type: "Feature",
|
|
@@ -3798,8 +3982,8 @@ const BASE_CAMP_METADATA = {
|
|
|
3798
3982
|
* amplifier rows, and an optional Field B echelon glyph straddling the
|
|
3799
3983
|
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3800
3984
|
*/
|
|
3801
|
-
function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
|
|
3802
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
|
|
3985
|
+
function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3986
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
|
|
3803
3987
|
}
|
|
3804
3988
|
const BASE_CAMP = defineControlMeasure({
|
|
3805
3989
|
metadata: BASE_CAMP_METADATA,
|
|
@@ -3851,8 +4035,8 @@ const GUERRILLA_BASE_METADATA = {
|
|
|
3851
4035
|
* `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
|
|
3852
4036
|
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3853
4037
|
*/
|
|
3854
|
-
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
|
|
3855
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
|
|
4038
|
+
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4039
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
|
|
3856
4040
|
}
|
|
3857
4041
|
const GUERRILLA_BASE = defineControlMeasure({
|
|
3858
4042
|
metadata: GUERRILLA_BASE_METADATA,
|
|
@@ -3908,8 +4092,8 @@ const GENERIC_C2_AREA_METADATA = {
|
|
|
3908
4092
|
* through `positions` labeled at its center with the bare `T` (Field T) plus
|
|
3909
4093
|
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
3910
4094
|
*/
|
|
3911
|
-
function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
|
|
3912
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
|
|
4095
|
+
function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4096
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
|
|
3913
4097
|
}
|
|
3914
4098
|
const GENERIC_C2_AREA = defineControlMeasure({
|
|
3915
4099
|
metadata: GENERIC_C2_AREA_METADATA,
|
|
@@ -3958,8 +4142,8 @@ const ASSEMBLY_AREA_METADATA = {
|
|
|
3958
4142
|
* `positions` labeled at its center with "AA "-prefixed `T` (Field T) plus
|
|
3959
4143
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3960
4144
|
*/
|
|
3961
|
-
function createAssemblyArea(positions, options = {}, textAmplifiers = {}) {
|
|
3962
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"));
|
|
4145
|
+
function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4146
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
|
|
3963
4147
|
}
|
|
3964
4148
|
const ASSEMBLY_AREA = defineControlMeasure({
|
|
3965
4149
|
metadata: ASSEMBLY_AREA_METADATA,
|
|
@@ -4640,7 +4824,7 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4640
4824
|
drawRule: "Area1",
|
|
4641
4825
|
capturesLabelSize: true,
|
|
4642
4826
|
params: ECHELON_AREA_PARAMS,
|
|
4643
|
-
textAmplifiers:
|
|
4827
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
4644
4828
|
};
|
|
4645
4829
|
/**
|
|
4646
4830
|
* Creates a Battle Position control measure: a closed area through `positions`
|
|
@@ -4650,16 +4834,22 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4650
4834
|
* @param positions - Area anchor points (≥3); the input contract is enforced at
|
|
4651
4835
|
* the render seam (ADR-0014).
|
|
4652
4836
|
* @param options - {@link BattlePositionOptions}.
|
|
4653
|
-
* @param textAmplifiers - Field T
|
|
4654
|
-
*
|
|
4655
|
-
*
|
|
4837
|
+
* @param textAmplifiers - Field T (designation, centered in the area) plus an
|
|
4838
|
+
* optional Field N "ENY" hostile marker at the west/east edges (no H/W rows —
|
|
4839
|
+
* this measure's template pairs the designation with the Field B glyph, not
|
|
4840
|
+
* the plain L-15 boundary template).
|
|
4656
4841
|
*/
|
|
4657
|
-
function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
4842
|
+
function createBattlePosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4658
4843
|
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
4844
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
4660
4845
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$6);
|
|
4661
4846
|
const { strokes, fills, gaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
4662
|
-
const
|
|
4847
|
+
const labelFeatures = [];
|
|
4848
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
4849
|
+
name: textAmplifiers.T,
|
|
4850
|
+
hostile: textAmplifiers.N
|
|
4851
|
+
}, options, context.amplifierPlacements);
|
|
4852
|
+
const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
|
|
4663
4853
|
const features = [];
|
|
4664
4854
|
if (boundaryCoords.length > 0) features.push({
|
|
4665
4855
|
type: "Feature",
|
|
@@ -4688,8 +4878,6 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
|
4688
4878
|
coordinates: fills.map((ring) => [ring])
|
|
4689
4879
|
}
|
|
4690
4880
|
});
|
|
4691
|
-
const labelFeatures = [];
|
|
4692
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
4693
4881
|
features.push(...labelFeatures);
|
|
4694
4882
|
return {
|
|
4695
4883
|
type: "FeatureCollection",
|
|
@@ -5053,6 +5241,10 @@ function normalizeSmoothResolution$1(value) {
|
|
|
5053
5241
|
//#endregion
|
|
5054
5242
|
//#region src/generators/cm11-command-control-lines/boundary.ts
|
|
5055
5243
|
const DEFAULT_SMOOTH_RESOLUTION$4 = 12;
|
|
5244
|
+
function resolveLabelRepetitions(value) {
|
|
5245
|
+
const numeric = Number(value ?? 1);
|
|
5246
|
+
return Number.isFinite(numeric) ? Math.max(1, Math.round(numeric)) : 1;
|
|
5247
|
+
}
|
|
5056
5248
|
/** Default options for the Boundary control measure. */
|
|
5057
5249
|
const DEFAULT_BOUNDARY_OPTIONS = {
|
|
5058
5250
|
echelon: "battalion",
|
|
@@ -5061,6 +5253,7 @@ const DEFAULT_BOUNDARY_OPTIONS = {
|
|
|
5061
5253
|
echelonPadding: .3,
|
|
5062
5254
|
labelRepetitions: 1,
|
|
5063
5255
|
labelSpacing: 1,
|
|
5256
|
+
labelPosition: .5,
|
|
5064
5257
|
labelPadding: .4,
|
|
5065
5258
|
labelOrientation: "along",
|
|
5066
5259
|
smooth: false,
|
|
@@ -5143,6 +5336,16 @@ const BOUNDARY_METADATA = {
|
|
|
5143
5336
|
max: 5,
|
|
5144
5337
|
step: 1
|
|
5145
5338
|
},
|
|
5339
|
+
{
|
|
5340
|
+
key: "labelPosition",
|
|
5341
|
+
label: "Label position",
|
|
5342
|
+
description: "Position of the label group along the boundary, from the first point (0) to the last (1)",
|
|
5343
|
+
type: "number",
|
|
5344
|
+
min: 0,
|
|
5345
|
+
max: 1,
|
|
5346
|
+
step: .01,
|
|
5347
|
+
visibleWhen: (opts) => resolveLabelRepetitions(opts.labelRepetitions) === 1
|
|
5348
|
+
},
|
|
5146
5349
|
{
|
|
5147
5350
|
key: "labelSpacing",
|
|
5148
5351
|
label: "Label spacing",
|
|
@@ -5151,7 +5354,7 @@ const BOUNDARY_METADATA = {
|
|
|
5151
5354
|
min: 0,
|
|
5152
5355
|
max: 2,
|
|
5153
5356
|
step: .05,
|
|
5154
|
-
visibleWhen: (opts) =>
|
|
5357
|
+
visibleWhen: (opts) => resolveLabelRepetitions(opts.labelRepetitions) > 1
|
|
5155
5358
|
},
|
|
5156
5359
|
{
|
|
5157
5360
|
key: "labelPadding",
|
|
@@ -5214,7 +5417,7 @@ const BOUNDARY_METADATA = {
|
|
|
5214
5417
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
5215
5418
|
*/
|
|
5216
5419
|
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;
|
|
5420
|
+
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
5421
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
5219
5422
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
5220
5423
|
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
@@ -5222,7 +5425,8 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5222
5425
|
let h = echelonSize;
|
|
5223
5426
|
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
5224
5427
|
h = Math.max(EPSILON, h);
|
|
5225
|
-
const reps =
|
|
5428
|
+
const reps = resolveLabelRepetitions(labelRepetitions);
|
|
5429
|
+
const singleGroupPosition = clampLinePosition(labelPosition, DEFAULT_BOUNDARY_OPTIONS.labelPosition);
|
|
5226
5430
|
const spec = resolveEchelon(echelon);
|
|
5227
5431
|
const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
|
|
5228
5432
|
const rawVerts = positions.map((p) => project(p[0], p[1]));
|
|
@@ -5234,7 +5438,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5234
5438
|
const { segments: segs, totalLength } = polylineSegments(verts);
|
|
5235
5439
|
if (totalLength >= 1e-6) for (let k = 0; k < reps; k++) {
|
|
5236
5440
|
const evenFraction = (k + 1) / (reps + 1);
|
|
5237
|
-
const fraction = Math.min(1, Math.max(0, .5 + (evenFraction - .5) * labelSpacing));
|
|
5441
|
+
const fraction = Math.min(1, Math.max(0, reps === 1 ? singleGroupPosition : .5 + (evenFraction - .5) * labelSpacing));
|
|
5238
5442
|
const target = fraction * totalLength;
|
|
5239
5443
|
const frame = pointAlongPolyline(segs, totalLength, fraction);
|
|
5240
5444
|
const center = frame.point;
|
|
@@ -7669,8 +7873,8 @@ const DROP_ZONE_METADATA = {
|
|
|
7669
7873
|
* labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
|
|
7670
7874
|
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7671
7875
|
*/
|
|
7672
|
-
function createDropZone(positions, options = {}, textAmplifiers = {}) {
|
|
7673
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
|
|
7876
|
+
function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7877
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
|
|
7674
7878
|
}
|
|
7675
7879
|
const DROP_ZONE = defineControlMeasure({
|
|
7676
7880
|
metadata: DROP_ZONE_METADATA,
|
|
@@ -7784,11 +7988,11 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7784
7988
|
* Creates an Encirclement tactical graphic: a complete smooth boundary ring
|
|
7785
7989
|
* with outward arrowhead barbs resting on it. The boundary is an unfilled
|
|
7786
7990
|
* 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.
|
|
7991
|
+
* boundary arc itself. When a Field N (ENY) marker is set, it masks the
|
|
7992
|
+
* boundary with a knockout gap wherever its text box touches the ring
|
|
7993
|
+
* (mirroring Field B's echelon glyph, ADR-0023) — the boundary becomes a
|
|
7994
|
+
* gapped MultiLineString instead of a closed Polygon, and any barb whose
|
|
7995
|
+
* center falls inside the gap is suppressed; the no-`N` case is unaffected.
|
|
7792
7996
|
*
|
|
7793
7997
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
7794
7998
|
* at the render seam (ADR-0014).
|
|
@@ -7796,12 +8000,12 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7796
8000
|
* @param textAmplifiers - Field N only — rendered as "ENY" markers at the
|
|
7797
8001
|
* ring's west and east edges, per the doctrinal hostile example.
|
|
7798
8002
|
*/
|
|
7799
|
-
function createEncirclement(positions, options = {}, textAmplifiers = {}) {
|
|
8003
|
+
function createEncirclement(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7800
8004
|
const { barbSpacingRatio = DEFAULT_BARB_SPACING_RATIO, barbLengthRatio = DEFAULT_BARB_LENGTH_RATIO, smooth = DEFAULT_ENCIRCLEMENT_OPTIONS.smooth, smoothResolution = DEFAULT_ENCIRCLEMENT_OPTIONS.smoothResolution } = options;
|
|
7801
8005
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
|
|
7802
8006
|
const perimeter = buildAreaPerimeter(verts);
|
|
7803
8007
|
const labelFeatures = [];
|
|
7804
|
-
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options);
|
|
8008
|
+
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
|
|
7805
8009
|
const radius = meanRadius(verts);
|
|
7806
8010
|
const barbLength = radius * Math.max(0, barbLengthRatio);
|
|
7807
8011
|
const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
|
|
@@ -7876,8 +8080,8 @@ const EXTRACTION_ZONE_METADATA = {
|
|
|
7876
8080
|
* `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
|
|
7877
8081
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7878
8082
|
*/
|
|
7879
|
-
function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
|
|
7880
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
|
|
8083
|
+
function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
8084
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
|
|
7881
8085
|
}
|
|
7882
8086
|
const EXTRACTION_ZONE = defineControlMeasure({
|
|
7883
8087
|
metadata: EXTRACTION_ZONE_METADATA,
|
|
@@ -8777,7 +8981,7 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8777
8981
|
drawRule: "Area1",
|
|
8778
8982
|
capturesLabelSize: true,
|
|
8779
8983
|
params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
|
|
8780
|
-
textAmplifiers:
|
|
8984
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
8781
8985
|
};
|
|
8782
8986
|
/**
|
|
8783
8987
|
* Creates a Fortified Area tactical graphic.
|
|
@@ -8791,8 +8995,8 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8791
8995
|
* @param options - Configuration options for the graphic
|
|
8792
8996
|
* @param textAmplifiers - Field T (bare, no prefix — "TANGO", not "FA TANGO")
|
|
8793
8997
|
* plus Field H/W/W1, centered on the pre-teeth boundary ring (see
|
|
8794
|
-
* `boundaryInput` below)
|
|
8795
|
-
*
|
|
8998
|
+
* `boundaryInput` below), and an optional Field N "ENY" hostile marker on
|
|
8999
|
+
* that ring's west/east edges (placed but not boundary-masked — see below).
|
|
8796
9000
|
* @returns A GeoJSON FeatureCollection containing the castellated Polygon
|
|
8797
9001
|
* plus any label Points
|
|
8798
9002
|
*/
|
|
@@ -8822,7 +9026,8 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
8822
9026
|
name: textAmplifiers.T,
|
|
8823
9027
|
additionalInformation: textAmplifiers.H,
|
|
8824
9028
|
dtgStart: textAmplifiers.W,
|
|
8825
|
-
dtgEnd: textAmplifiers.W1
|
|
9029
|
+
dtgEnd: textAmplifiers.W1,
|
|
9030
|
+
hostile: textAmplifiers.N
|
|
8826
9031
|
}, options);
|
|
8827
9032
|
features.push(...labelFeatures);
|
|
8828
9033
|
return {
|
|
@@ -9243,8 +9448,8 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
|
|
|
9243
9448
|
* (Field T) plus `H`/`W`/`W1`/`N` amplifier rows, per
|
|
9244
9449
|
* {@link labeledAreaFeatures}.
|
|
9245
9450
|
*/
|
|
9246
|
-
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9247
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""));
|
|
9451
|
+
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9452
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
|
|
9248
9453
|
}
|
|
9249
9454
|
const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
9250
9455
|
metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
|
|
@@ -9297,8 +9502,8 @@ const LANDING_ZONE_METADATA = {
|
|
|
9297
9502
|
* labeled at its center with "LZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9298
9503
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9299
9504
|
*/
|
|
9300
|
-
function createLandingZone(positions, options = {}, textAmplifiers = {}) {
|
|
9301
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"));
|
|
9505
|
+
function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9506
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
|
|
9302
9507
|
}
|
|
9303
9508
|
const LANDING_ZONE = defineControlMeasure({
|
|
9304
9509
|
metadata: LANDING_ZONE_METADATA,
|
|
@@ -9817,8 +10022,8 @@ const PICKUP_ZONE_METADATA = {
|
|
|
9817
10022
|
* labeled at its center with "PZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9818
10023
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9819
10024
|
*/
|
|
9820
|
-
function createPickupZone(positions, options = {}, textAmplifiers = {}) {
|
|
9821
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"));
|
|
10025
|
+
function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10026
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
|
|
9822
10027
|
}
|
|
9823
10028
|
const PICKUP_ZONE = defineControlMeasure({
|
|
9824
10029
|
metadata: PICKUP_ZONE_METADATA,
|
|
@@ -9852,19 +10057,23 @@ const ASSAULT_POSITION_METADATA = {
|
|
|
9852
10057
|
value: "151500",
|
|
9853
10058
|
minCoordinates: 3,
|
|
9854
10059
|
geometry: "area",
|
|
9855
|
-
geometryTypes: [
|
|
10060
|
+
geometryTypes: [
|
|
10061
|
+
"Polygon",
|
|
10062
|
+
"MultiLineString",
|
|
10063
|
+
"Point"
|
|
10064
|
+
],
|
|
9856
10065
|
drawRule: "Area1",
|
|
9857
10066
|
capturesLabelSize: true,
|
|
9858
|
-
params:
|
|
9859
|
-
textAmplifiers:
|
|
10067
|
+
params: LABELED_AREA_PARAMS,
|
|
10068
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9860
10069
|
};
|
|
9861
10070
|
/**
|
|
9862
10071
|
* Creates an Assault position control measure: a closed area through
|
|
9863
|
-
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T)
|
|
9864
|
-
*
|
|
10072
|
+
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) plus
|
|
10073
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9865
10074
|
*/
|
|
9866
|
-
function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9867
|
-
return labeledAreaFeatures(positions, options,
|
|
10075
|
+
function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10076
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
|
|
9868
10077
|
}
|
|
9869
10078
|
const ASSAULT_POSITION = defineControlMeasure({
|
|
9870
10079
|
metadata: ASSAULT_POSITION_METADATA,
|
|
@@ -9898,19 +10107,23 @@ const ATTACK_POSITION_METADATA = {
|
|
|
9898
10107
|
value: "151600",
|
|
9899
10108
|
minCoordinates: 3,
|
|
9900
10109
|
geometry: "area",
|
|
9901
|
-
geometryTypes: [
|
|
10110
|
+
geometryTypes: [
|
|
10111
|
+
"Polygon",
|
|
10112
|
+
"MultiLineString",
|
|
10113
|
+
"Point"
|
|
10114
|
+
],
|
|
9902
10115
|
drawRule: "Area1",
|
|
9903
10116
|
capturesLabelSize: true,
|
|
9904
|
-
params:
|
|
9905
|
-
textAmplifiers:
|
|
10117
|
+
params: LABELED_AREA_PARAMS,
|
|
10118
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9906
10119
|
};
|
|
9907
10120
|
/**
|
|
9908
10121
|
* Creates an Attack position control measure: a closed area through
|
|
9909
|
-
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T)
|
|
9910
|
-
*
|
|
10122
|
+
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T) plus
|
|
10123
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9911
10124
|
*/
|
|
9912
|
-
function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9913
|
-
return labeledAreaFeatures(positions, options,
|
|
10125
|
+
function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10126
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
|
|
9914
10127
|
}
|
|
9915
10128
|
const ATTACK_POSITION = defineControlMeasure({
|
|
9916
10129
|
metadata: ATTACK_POSITION_METADATA,
|
|
@@ -9944,19 +10157,23 @@ const OBJECTIVE_AREA_METADATA = {
|
|
|
9944
10157
|
value: "151700",
|
|
9945
10158
|
minCoordinates: 3,
|
|
9946
10159
|
geometry: "area",
|
|
9947
|
-
geometryTypes: [
|
|
10160
|
+
geometryTypes: [
|
|
10161
|
+
"Polygon",
|
|
10162
|
+
"MultiLineString",
|
|
10163
|
+
"Point"
|
|
10164
|
+
],
|
|
9948
10165
|
drawRule: "Area1",
|
|
9949
10166
|
capturesLabelSize: true,
|
|
9950
|
-
params:
|
|
9951
|
-
textAmplifiers:
|
|
10167
|
+
params: LABELED_AREA_PARAMS,
|
|
10168
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9952
10169
|
};
|
|
9953
10170
|
/**
|
|
9954
10171
|
* Creates an Objective area control measure: a closed area through
|
|
9955
|
-
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T)
|
|
9956
|
-
*
|
|
10172
|
+
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) plus
|
|
10173
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9957
10174
|
*/
|
|
9958
|
-
function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
|
|
9959
|
-
return labeledAreaFeatures(positions, options,
|
|
10175
|
+
function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10176
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
|
|
9960
10177
|
}
|
|
9961
10178
|
const OBJECTIVE_AREA = defineControlMeasure({
|
|
9962
10179
|
metadata: OBJECTIVE_AREA_METADATA,
|
|
@@ -10079,7 +10296,7 @@ const STRONG_POINT_METADATA = {
|
|
|
10079
10296
|
drawRule: "Area1",
|
|
10080
10297
|
capturesLabelSize: true,
|
|
10081
10298
|
params: ECHELON_AREA_PARAMS,
|
|
10082
|
-
textAmplifiers:
|
|
10299
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
10083
10300
|
};
|
|
10084
10301
|
/**
|
|
10085
10302
|
* Outward perimeter tics: a stroke every `h` meters along the ring, each `h`
|
|
@@ -10114,17 +10331,24 @@ function buildStrongPointTics(perimeter, gaps, h) {
|
|
|
10114
10331
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
10115
10332
|
* at the render seam (ADR-0014).
|
|
10116
10333
|
* @param options - {@link StrongPointOptions}.
|
|
10117
|
-
* @param textAmplifiers - Field T
|
|
10118
|
-
*
|
|
10334
|
+
* @param textAmplifiers - Field T (designation, centered in the area) plus an
|
|
10335
|
+
* optional Field N "ENY" hostile marker at the west/east edges, same as
|
|
10336
|
+
* Battle Position.
|
|
10119
10337
|
*/
|
|
10120
|
-
function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
10338
|
+
function createStrongPoint(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10121
10339
|
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
10340
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
10123
10341
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION);
|
|
10124
10342
|
const perimeter = buildAreaPerimeter(verts);
|
|
10125
10343
|
const { strokes, fills, gaps } = placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
10126
|
-
const
|
|
10127
|
-
const
|
|
10344
|
+
const labelFeatures = [];
|
|
10345
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
10346
|
+
name: textAmplifiers.T,
|
|
10347
|
+
hostile: textAmplifiers.N
|
|
10348
|
+
}, options, context.amplifierPlacements);
|
|
10349
|
+
const allGaps = [...gaps, ...enyGaps];
|
|
10350
|
+
const boundaryCoords = buildGappedLine(verts, allGaps);
|
|
10351
|
+
const tics = buildStrongPointTics(perimeter, allGaps, h);
|
|
10128
10352
|
const features = [];
|
|
10129
10353
|
if (boundaryCoords.length > 0) features.push({
|
|
10130
10354
|
type: "Feature",
|
|
@@ -10161,8 +10385,6 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
|
10161
10385
|
coordinates: fills.map((ring) => [ring])
|
|
10162
10386
|
}
|
|
10163
10387
|
});
|
|
10164
|
-
const labelFeatures = [];
|
|
10165
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
10166
10388
|
features.push(...labelFeatures);
|
|
10167
10389
|
return {
|
|
10168
10390
|
type: "FeatureCollection",
|
|
@@ -10222,8 +10444,8 @@ const SUBMARINE_ACTION_AREA_METADATA = {
|
|
|
10222
10444
|
* `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
|
|
10223
10445
|
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10224
10446
|
*/
|
|
10225
|
-
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10226
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
|
|
10447
|
+
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10448
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
|
|
10227
10449
|
}
|
|
10228
10450
|
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
10229
10451
|
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
@@ -10277,8 +10499,8 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
|
10277
10499
|
* through `positions` labeled at its center with "SGSA -"-prefixed `T`
|
|
10278
10500
|
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10279
10501
|
*/
|
|
10280
|
-
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10281
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
|
|
10502
|
+
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10503
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
|
|
10282
10504
|
}
|
|
10283
10505
|
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
10284
10506
|
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
@@ -10860,141 +11082,6 @@ function applyLayer(target, layer) {
|
|
|
10860
11082
|
}
|
|
10861
11083
|
}
|
|
10862
11084
|
//#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
11085
|
//#region src/renderControlMeasure.ts
|
|
10999
11086
|
/**
|
|
11000
11087
|
* Pure render: `ControlMeasure` → `ControlMeasureRender` (a
|
|
@@ -11030,7 +11117,7 @@ function dispatchControlMeasure(cm, opts) {
|
|
|
11030
11117
|
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
11031
11118
|
const generator = definition.generator;
|
|
11032
11119
|
const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
|
|
11033
|
-
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers);
|
|
11120
|
+
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
|
|
11034
11121
|
}
|
|
11035
11122
|
/**
|
|
11036
11123
|
* The control-point *input contract*: at least `metadata.minCoordinates` points,
|
|
@@ -11047,15 +11134,15 @@ function validateInputContract(points, metadata, mode) {
|
|
|
11047
11134
|
}
|
|
11048
11135
|
/**
|
|
11049
11136
|
* The `amplifierPlacements` key that repositions this feature, if any: the
|
|
11050
|
-
* canonical amplifier field when the
|
|
11051
|
-
*
|
|
11137
|
+
* explicit `labelPlacementKey`, the canonical amplifier field when the
|
|
11138
|
+
* generator declared one, or a synthesized `label:<part>:<ordinal>` key for any
|
|
11052
11139
|
* other point label — unless the generator opted out via
|
|
11053
11140
|
* `labelPlacement: false`.
|
|
11054
11141
|
*/
|
|
11055
11142
|
function resolveLabelPlacementKey(sourceProps, amplifierField, text, geometryType, part, generatedLabelOrdinal) {
|
|
11056
11143
|
if (sourceProps.labelPlacement === false) return void 0;
|
|
11057
|
-
if (amplifierField) return amplifierField;
|
|
11058
11144
|
if (typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) return sourceProps.labelPlacementKey;
|
|
11145
|
+
if (amplifierField) return amplifierField;
|
|
11059
11146
|
if (text !== void 0 && geometryType === "Point") return `label:${part}:${generatedLabelOrdinal}`;
|
|
11060
11147
|
}
|
|
11061
11148
|
function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyle, graphicsStyle, amplifierPlacements) {
|
|
@@ -11141,4 +11228,4 @@ function assertNever(value) {
|
|
|
11141
11228
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
11142
11229
|
}
|
|
11143
11230
|
//#endregion
|
|
11144
|
-
export {
|
|
11231
|
+
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 };
|