@orbat-mapper/control-measures 0.2.0-alpha.15 → 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-VOKiZO2O.mjs → renderControlMeasure-BVbkVj2S.mjs} +388 -295
- 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;
|
|
@@ -2325,19 +2325,20 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
|
|
|
2325
2325
|
const tip = vecAdd(last.start, vecScale(last.along, last.length));
|
|
2326
2326
|
const length = Math.min(totalLength, totalLength * nonNegative$1(options.arrowheadLengthRatio, DEFAULT_DIRECTION_ARROWHEAD_LENGTH_RATIO));
|
|
2327
2327
|
const innerHalfWidth = length * nonNegative$1(options.arrowheadWidthRatio, DEFAULT_DIRECTION_ARROWHEAD_WIDTH_RATIO) / 2;
|
|
2328
|
-
const
|
|
2328
|
+
const outlineExtension = config.outlineRatio ? length * Math.max(0, config.outlineRatio) : 0;
|
|
2329
|
+
const innerTip = vecSub(tip, vecScale(last.along, outlineExtension));
|
|
2330
|
+
const base = vecSub(innerTip, vecScale(last.along, length));
|
|
2329
2331
|
const innerLeft = vecAdd(base, vecScale(last.perp, innerHalfWidth));
|
|
2330
2332
|
const innerRight = vecSub(base, vecScale(last.perp, innerHalfWidth));
|
|
2331
2333
|
const arrowhead = config.outlineRatio ? (() => {
|
|
2332
|
-
const
|
|
2333
|
-
const outerHalfWidth = innerHalfWidth * (1 + ratio);
|
|
2334
|
+
const outerHalfWidth = innerHalfWidth * (1 + Math.max(0, config.outlineRatio));
|
|
2334
2335
|
const outerLeft = vecAdd(base, vecScale(last.perp, outerHalfWidth));
|
|
2335
2336
|
return [
|
|
2336
2337
|
innerLeft,
|
|
2337
|
-
|
|
2338
|
+
innerTip,
|
|
2338
2339
|
innerRight,
|
|
2339
2340
|
vecSub(base, vecScale(last.perp, outerHalfWidth)),
|
|
2340
|
-
|
|
2341
|
+
tip,
|
|
2341
2342
|
outerLeft,
|
|
2342
2343
|
innerLeft
|
|
2343
2344
|
];
|
|
@@ -2346,9 +2347,14 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
|
|
|
2346
2347
|
tip,
|
|
2347
2348
|
innerRight
|
|
2348
2349
|
];
|
|
2350
|
+
const shaftVerts = outlineExtension > 0 ? verts.slice(0, -1) : verts;
|
|
2351
|
+
if (outlineExtension > 0) {
|
|
2352
|
+
while (shaftVerts.length > 1 && vecDot(vecSub(shaftVerts[shaftVerts.length - 1], innerTip), last.along) > 0) shaftVerts.pop();
|
|
2353
|
+
shaftVerts.push(innerTip);
|
|
2354
|
+
}
|
|
2349
2355
|
const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
|
|
2350
2356
|
const labelGap = directionLabelGap(totalLength, labelPosition, textAmplifiers.T, options);
|
|
2351
|
-
const lines = [...buildGappedLine(
|
|
2357
|
+
const lines = [...buildGappedLine(shaftVerts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
|
|
2352
2358
|
const features = [{
|
|
2353
2359
|
type: "Feature",
|
|
2354
2360
|
properties: { part: config.part },
|
|
@@ -2782,6 +2788,141 @@ const DIRECTION_OF_SUPPORTING_ATTACK = defineControlMeasure({
|
|
|
2782
2788
|
}
|
|
2783
2789
|
});
|
|
2784
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
|
|
2785
2926
|
//#region src/internal/area-echelon.ts
|
|
2786
2927
|
/**
|
|
2787
2928
|
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
@@ -3049,6 +3190,36 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3049
3190
|
totalArc: dist
|
|
3050
3191
|
};
|
|
3051
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
|
+
}
|
|
3052
3223
|
/**
|
|
3053
3224
|
* Pushes an area's L-15 text amplifier labels — centered name/H/W-W1 rows and
|
|
3054
3225
|
* west/east "ENY" edge markers — as `part: "label"` Point features (via
|
|
@@ -3067,25 +3238,27 @@ function ringCrossingsAtY(verts, target) {
|
|
|
3067
3238
|
*
|
|
3068
3239
|
* When `texts.hostile` is set, the ring's west/east crossings of the
|
|
3069
3240
|
* horizontal line through the centroid are found ({@link ringCrossingsAtY});
|
|
3070
|
-
* the minimum/maximum-x crossings
|
|
3071
|
-
*
|
|
3072
|
-
* Each marker
|
|
3073
|
-
*
|
|
3074
|
-
*
|
|
3075
|
-
*
|
|
3076
|
-
*
|
|
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):
|
|
3077
3250
|
* `halfExtent = 0.5 * (textWidth·|d.x| + textHeight·|d.y|) + clearance`, where
|
|
3078
3251
|
* `textHeight = resolveLabelOffsetMeters(options, 1)`,
|
|
3079
3252
|
* `textWidth = text.length * ENY_CHAR_ASPECT_RATIO * textHeight`, and
|
|
3080
3253
|
* `clearance = (ENY_CLEARANCE_RATIO + labelPadding) * textHeight` — mirroring
|
|
3081
3254
|
* {@link placeEchelon}'s glyph-width masking gap (ADR-0023). Fewer than two
|
|
3082
3255
|
* crossings (e.g. a degenerate ring, or a concave shape whose centroid
|
|
3083
|
-
* latitude misses the boundary) skips the
|
|
3084
|
-
*
|
|
3256
|
+
* latitude misses the boundary) skips the markers silently rather than
|
|
3257
|
+
* guessing their default placements.
|
|
3085
3258
|
*
|
|
3086
3259
|
* A ring with fewer than 2 vertices is degenerate — nothing is placed.
|
|
3087
3260
|
*/
|
|
3088
|
-
function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
3261
|
+
function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements) {
|
|
3089
3262
|
if (ringVerts.length < 2) return [];
|
|
3090
3263
|
const sizeProps = labelSizeProps(options);
|
|
3091
3264
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
@@ -3110,15 +3283,26 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
3110
3283
|
const { crossings, totalArc } = ringCrossingsAtY(ringVerts, cy);
|
|
3111
3284
|
if (crossings.length >= 2) {
|
|
3112
3285
|
const byX = [...crossings].sort((a, b) => a.x - b.x);
|
|
3113
|
-
const west = byX[0];
|
|
3114
|
-
const east = byX[byX.length - 1];
|
|
3115
3286
|
const textHeight = resolveLabelOffsetMeters(options, 1);
|
|
3116
3287
|
const textWidth = texts.hostile.length * ENY_CHAR_ASPECT_RATIO * textHeight;
|
|
3117
3288
|
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight;
|
|
3118
|
-
for (const crossing of [west, east]) {
|
|
3119
|
-
|
|
3120
|
-
const
|
|
3121
|
-
|
|
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);
|
|
3122
3306
|
}
|
|
3123
3307
|
}
|
|
3124
3308
|
}
|
|
@@ -3159,11 +3343,11 @@ function boundaryFeature(verts, gaps) {
|
|
|
3159
3343
|
* echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
|
|
3160
3344
|
* common (no `N`) case is unaffected.
|
|
3161
3345
|
*/
|
|
3162
|
-
function labeledAreaFeatures(positions, options, texts) {
|
|
3346
|
+
function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3163
3347
|
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
3164
3348
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
3165
3349
|
const labelFeatures = [];
|
|
3166
|
-
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
|
|
3350
|
+
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
|
|
3167
3351
|
features.push(...labelFeatures);
|
|
3168
3352
|
return {
|
|
3169
3353
|
type: "FeatureCollection",
|
|
@@ -3201,8 +3385,8 @@ const AREA_METADATA = {
|
|
|
3201
3385
|
* at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
|
|
3202
3386
|
* marker, per {@link labeledAreaFeatures}.
|
|
3203
3387
|
*/
|
|
3204
|
-
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
3205
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
3388
|
+
function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3389
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
|
|
3206
3390
|
}
|
|
3207
3391
|
const AREA = defineControlMeasure({
|
|
3208
3392
|
metadata: AREA_METADATA,
|
|
@@ -3448,8 +3632,8 @@ const AREA_OF_OPERATIONS_METADATA = {
|
|
|
3448
3632
|
* `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
|
|
3449
3633
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3450
3634
|
*/
|
|
3451
|
-
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
|
|
3452
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
|
|
3635
|
+
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3636
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
|
|
3453
3637
|
}
|
|
3454
3638
|
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
3455
3639
|
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
@@ -3468,15 +3652,6 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
|
3468
3652
|
}
|
|
3469
3653
|
});
|
|
3470
3654
|
//#endregion
|
|
3471
|
-
//#region src/generators/cm12-command-control-areas/params.ts
|
|
3472
|
-
const SMOOTH_AREA_PARAMS = AREA_SMOOTH_PARAMS;
|
|
3473
|
-
const AREA_DESIGNATION_ONLY_AMPLIFIER = {
|
|
3474
|
-
key: "T",
|
|
3475
|
-
label: "Unique designation",
|
|
3476
|
-
description: "Field T — the area's unique designation.",
|
|
3477
|
-
maxLength: 21
|
|
3478
|
-
};
|
|
3479
|
-
//#endregion
|
|
3480
3655
|
//#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
|
|
3481
3656
|
/**
|
|
3482
3657
|
* Default options for the Named area of interest control measure. The
|
|
@@ -3496,19 +3671,23 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
|
3496
3671
|
value: "120200",
|
|
3497
3672
|
minCoordinates: 3,
|
|
3498
3673
|
geometry: "area",
|
|
3499
|
-
geometryTypes: [
|
|
3674
|
+
geometryTypes: [
|
|
3675
|
+
"Polygon",
|
|
3676
|
+
"MultiLineString",
|
|
3677
|
+
"Point"
|
|
3678
|
+
],
|
|
3500
3679
|
drawRule: "Area1",
|
|
3501
3680
|
capturesLabelSize: true,
|
|
3502
|
-
params:
|
|
3503
|
-
textAmplifiers:
|
|
3681
|
+
params: LABELED_AREA_PARAMS,
|
|
3682
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3504
3683
|
};
|
|
3505
3684
|
/**
|
|
3506
3685
|
* Creates a Named area of interest control measure: a closed area through
|
|
3507
|
-
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T)
|
|
3508
|
-
*
|
|
3686
|
+
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T) plus
|
|
3687
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3509
3688
|
*/
|
|
3510
|
-
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3511
|
-
return labeledAreaFeatures(positions, options,
|
|
3689
|
+
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3690
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
|
|
3512
3691
|
}
|
|
3513
3692
|
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
3514
3693
|
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
@@ -3546,19 +3725,23 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
|
3546
3725
|
value: "120300",
|
|
3547
3726
|
minCoordinates: 3,
|
|
3548
3727
|
geometry: "area",
|
|
3549
|
-
geometryTypes: [
|
|
3728
|
+
geometryTypes: [
|
|
3729
|
+
"Polygon",
|
|
3730
|
+
"MultiLineString",
|
|
3731
|
+
"Point"
|
|
3732
|
+
],
|
|
3550
3733
|
drawRule: "Area1",
|
|
3551
3734
|
capturesLabelSize: true,
|
|
3552
|
-
params:
|
|
3553
|
-
textAmplifiers:
|
|
3735
|
+
params: LABELED_AREA_PARAMS,
|
|
3736
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
3554
3737
|
};
|
|
3555
3738
|
/**
|
|
3556
3739
|
* Creates a Target area of interest control measure: a closed area through
|
|
3557
|
-
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T)
|
|
3558
|
-
*
|
|
3740
|
+
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T) plus
|
|
3741
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
3559
3742
|
*/
|
|
3560
|
-
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
3561
|
-
return labeledAreaFeatures(positions, options,
|
|
3743
|
+
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3744
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
|
|
3562
3745
|
}
|
|
3563
3746
|
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
3564
3747
|
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
@@ -3612,21 +3795,25 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3612
3795
|
],
|
|
3613
3796
|
drawRule: "Area1",
|
|
3614
3797
|
capturesLabelSize: true,
|
|
3615
|
-
params: [
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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
|
+
],
|
|
3624
3811
|
textAmplifiers: [{
|
|
3625
3812
|
key: "H",
|
|
3626
3813
|
label: "Additional information",
|
|
3627
3814
|
description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
|
|
3628
3815
|
maxLength: 20
|
|
3629
|
-
}]
|
|
3816
|
+
}, AREA_HOSTILE_AMPLIFIER]
|
|
3630
3817
|
};
|
|
3631
3818
|
/**
|
|
3632
3819
|
* Creates an Airfield zone control measure: a closed area through
|
|
@@ -3637,12 +3824,14 @@ const AIRFIELD_ZONE_METADATA = {
|
|
|
3637
3824
|
* @param positions - Area anchor points (≥3); the input contract is enforced
|
|
3638
3825
|
* at the render seam (ADR-0014).
|
|
3639
3826
|
* @param options - {@link AirfieldZoneOptions}.
|
|
3640
|
-
* @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.
|
|
3641
3829
|
*/
|
|
3642
|
-
function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
3830
|
+
function createAirfieldZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3643
3831
|
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
3644
3832
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
3645
|
-
const
|
|
3833
|
+
const enyLabels = [];
|
|
3834
|
+
const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
|
|
3646
3835
|
let minX = Infinity;
|
|
3647
3836
|
let maxX = -Infinity;
|
|
3648
3837
|
for (const [x] of verts) {
|
|
@@ -3674,6 +3863,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
|
3674
3863
|
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start", "H");
|
|
3675
3864
|
features.push(...labelFeatures);
|
|
3676
3865
|
}
|
|
3866
|
+
features.push(...enyLabels);
|
|
3677
3867
|
return {
|
|
3678
3868
|
type: "FeatureCollection",
|
|
3679
3869
|
features
|
|
@@ -3717,13 +3907,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
|
3717
3907
|
* before carving the boundary, so either (or both) opens it into a gapped
|
|
3718
3908
|
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
3719
3909
|
*/
|
|
3720
|
-
function echelonLabeledAreaFeatures(positions, options, texts) {
|
|
3910
|
+
function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
3721
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;
|
|
3722
3912
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
3723
3913
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
3724
3914
|
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
3725
3915
|
const labelFeatures = [];
|
|
3726
|
-
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
|
|
3916
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
|
|
3727
3917
|
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
3728
3918
|
if (strokes.length > 0) features.push({
|
|
3729
3919
|
type: "Feature",
|
|
@@ -3792,8 +3982,8 @@ const BASE_CAMP_METADATA = {
|
|
|
3792
3982
|
* amplifier rows, and an optional Field B echelon glyph straddling the
|
|
3793
3983
|
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3794
3984
|
*/
|
|
3795
|
-
function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
|
|
3796
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
|
|
3985
|
+
function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
3986
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
|
|
3797
3987
|
}
|
|
3798
3988
|
const BASE_CAMP = defineControlMeasure({
|
|
3799
3989
|
metadata: BASE_CAMP_METADATA,
|
|
@@ -3845,8 +4035,8 @@ const GUERRILLA_BASE_METADATA = {
|
|
|
3845
4035
|
* `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
|
|
3846
4036
|
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3847
4037
|
*/
|
|
3848
|
-
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
|
|
3849
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
|
|
4038
|
+
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4039
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
|
|
3850
4040
|
}
|
|
3851
4041
|
const GUERRILLA_BASE = defineControlMeasure({
|
|
3852
4042
|
metadata: GUERRILLA_BASE_METADATA,
|
|
@@ -3902,8 +4092,8 @@ const GENERIC_C2_AREA_METADATA = {
|
|
|
3902
4092
|
* through `positions` labeled at its center with the bare `T` (Field T) plus
|
|
3903
4093
|
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
3904
4094
|
*/
|
|
3905
|
-
function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
|
|
3906
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
|
|
4095
|
+
function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4096
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
|
|
3907
4097
|
}
|
|
3908
4098
|
const GENERIC_C2_AREA = defineControlMeasure({
|
|
3909
4099
|
metadata: GENERIC_C2_AREA_METADATA,
|
|
@@ -3952,8 +4142,8 @@ const ASSEMBLY_AREA_METADATA = {
|
|
|
3952
4142
|
* `positions` labeled at its center with "AA "-prefixed `T` (Field T) plus
|
|
3953
4143
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
3954
4144
|
*/
|
|
3955
|
-
function createAssemblyArea(positions, options = {}, textAmplifiers = {}) {
|
|
3956
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"));
|
|
4145
|
+
function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4146
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
|
|
3957
4147
|
}
|
|
3958
4148
|
const ASSEMBLY_AREA = defineControlMeasure({
|
|
3959
4149
|
metadata: ASSEMBLY_AREA_METADATA,
|
|
@@ -4634,7 +4824,7 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4634
4824
|
drawRule: "Area1",
|
|
4635
4825
|
capturesLabelSize: true,
|
|
4636
4826
|
params: ECHELON_AREA_PARAMS,
|
|
4637
|
-
textAmplifiers:
|
|
4827
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
4638
4828
|
};
|
|
4639
4829
|
/**
|
|
4640
4830
|
* Creates a Battle Position control measure: a closed area through `positions`
|
|
@@ -4644,16 +4834,22 @@ const BATTLE_POSITION_METADATA = {
|
|
|
4644
4834
|
* @param positions - Area anchor points (≥3); the input contract is enforced at
|
|
4645
4835
|
* the render seam (ADR-0014).
|
|
4646
4836
|
* @param options - {@link BattlePositionOptions}.
|
|
4647
|
-
* @param textAmplifiers - Field T
|
|
4648
|
-
*
|
|
4649
|
-
*
|
|
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).
|
|
4650
4841
|
*/
|
|
4651
|
-
function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
4842
|
+
function createBattlePosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4652
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;
|
|
4653
4844
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
4654
4845
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$6);
|
|
4655
4846
|
const { strokes, fills, gaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
4656
|
-
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]);
|
|
4657
4853
|
const features = [];
|
|
4658
4854
|
if (boundaryCoords.length > 0) features.push({
|
|
4659
4855
|
type: "Feature",
|
|
@@ -4682,8 +4878,6 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
|
4682
4878
|
coordinates: fills.map((ring) => [ring])
|
|
4683
4879
|
}
|
|
4684
4880
|
});
|
|
4685
|
-
const labelFeatures = [];
|
|
4686
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
4687
4881
|
features.push(...labelFeatures);
|
|
4688
4882
|
return {
|
|
4689
4883
|
type: "FeatureCollection",
|
|
@@ -5047,6 +5241,10 @@ function normalizeSmoothResolution$1(value) {
|
|
|
5047
5241
|
//#endregion
|
|
5048
5242
|
//#region src/generators/cm11-command-control-lines/boundary.ts
|
|
5049
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
|
+
}
|
|
5050
5248
|
/** Default options for the Boundary control measure. */
|
|
5051
5249
|
const DEFAULT_BOUNDARY_OPTIONS = {
|
|
5052
5250
|
echelon: "battalion",
|
|
@@ -5055,6 +5253,7 @@ const DEFAULT_BOUNDARY_OPTIONS = {
|
|
|
5055
5253
|
echelonPadding: .3,
|
|
5056
5254
|
labelRepetitions: 1,
|
|
5057
5255
|
labelSpacing: 1,
|
|
5256
|
+
labelPosition: .5,
|
|
5058
5257
|
labelPadding: .4,
|
|
5059
5258
|
labelOrientation: "along",
|
|
5060
5259
|
smooth: false,
|
|
@@ -5137,6 +5336,16 @@ const BOUNDARY_METADATA = {
|
|
|
5137
5336
|
max: 5,
|
|
5138
5337
|
step: 1
|
|
5139
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
|
+
},
|
|
5140
5349
|
{
|
|
5141
5350
|
key: "labelSpacing",
|
|
5142
5351
|
label: "Label spacing",
|
|
@@ -5145,7 +5354,7 @@ const BOUNDARY_METADATA = {
|
|
|
5145
5354
|
min: 0,
|
|
5146
5355
|
max: 2,
|
|
5147
5356
|
step: .05,
|
|
5148
|
-
visibleWhen: (opts) =>
|
|
5357
|
+
visibleWhen: (opts) => resolveLabelRepetitions(opts.labelRepetitions) > 1
|
|
5149
5358
|
},
|
|
5150
5359
|
{
|
|
5151
5360
|
key: "labelPadding",
|
|
@@ -5208,7 +5417,7 @@ const BOUNDARY_METADATA = {
|
|
|
5208
5417
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
5209
5418
|
*/
|
|
5210
5419
|
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
5211
|
-
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;
|
|
5212
5421
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
5213
5422
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
5214
5423
|
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
@@ -5216,7 +5425,8 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5216
5425
|
let h = echelonSize;
|
|
5217
5426
|
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
5218
5427
|
h = Math.max(EPSILON, h);
|
|
5219
|
-
const reps =
|
|
5428
|
+
const reps = resolveLabelRepetitions(labelRepetitions);
|
|
5429
|
+
const singleGroupPosition = clampLinePosition(labelPosition, DEFAULT_BOUNDARY_OPTIONS.labelPosition);
|
|
5220
5430
|
const spec = resolveEchelon(echelon);
|
|
5221
5431
|
const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
|
|
5222
5432
|
const rawVerts = positions.map((p) => project(p[0], p[1]));
|
|
@@ -5228,7 +5438,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
5228
5438
|
const { segments: segs, totalLength } = polylineSegments(verts);
|
|
5229
5439
|
if (totalLength >= 1e-6) for (let k = 0; k < reps; k++) {
|
|
5230
5440
|
const evenFraction = (k + 1) / (reps + 1);
|
|
5231
|
-
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));
|
|
5232
5442
|
const target = fraction * totalLength;
|
|
5233
5443
|
const frame = pointAlongPolyline(segs, totalLength, fraction);
|
|
5234
5444
|
const center = frame.point;
|
|
@@ -7663,8 +7873,8 @@ const DROP_ZONE_METADATA = {
|
|
|
7663
7873
|
* labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
|
|
7664
7874
|
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7665
7875
|
*/
|
|
7666
|
-
function createDropZone(positions, options = {}, textAmplifiers = {}) {
|
|
7667
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
|
|
7876
|
+
function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7877
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
|
|
7668
7878
|
}
|
|
7669
7879
|
const DROP_ZONE = defineControlMeasure({
|
|
7670
7880
|
metadata: DROP_ZONE_METADATA,
|
|
@@ -7778,11 +7988,11 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7778
7988
|
* Creates an Encirclement tactical graphic: a complete smooth boundary ring
|
|
7779
7989
|
* with outward arrowhead barbs resting on it. The boundary is an unfilled
|
|
7780
7990
|
* Polygon; the barbs are open "V" strokes (a MultiLineString) whose base is the
|
|
7781
|
-
* boundary arc itself. When a Field N (ENY) marker is set, it
|
|
7782
|
-
* boundary
|
|
7783
|
-
* glyph, ADR-0023) — the boundary becomes a
|
|
7784
|
-
* a closed Polygon, and any barb whose
|
|
7785
|
-
* 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.
|
|
7786
7996
|
*
|
|
7787
7997
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
7788
7998
|
* at the render seam (ADR-0014).
|
|
@@ -7790,12 +8000,12 @@ function buildBarbs(perimeter, spacing, barbLength, gaps) {
|
|
|
7790
8000
|
* @param textAmplifiers - Field N only — rendered as "ENY" markers at the
|
|
7791
8001
|
* ring's west and east edges, per the doctrinal hostile example.
|
|
7792
8002
|
*/
|
|
7793
|
-
function createEncirclement(positions, options = {}, textAmplifiers = {}) {
|
|
8003
|
+
function createEncirclement(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
7794
8004
|
const { barbSpacingRatio = DEFAULT_BARB_SPACING_RATIO, barbLengthRatio = DEFAULT_BARB_LENGTH_RATIO, smooth = DEFAULT_ENCIRCLEMENT_OPTIONS.smooth, smoothResolution = DEFAULT_ENCIRCLEMENT_OPTIONS.smoothResolution } = options;
|
|
7795
8005
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
|
|
7796
8006
|
const perimeter = buildAreaPerimeter(verts);
|
|
7797
8007
|
const labelFeatures = [];
|
|
7798
|
-
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options);
|
|
8008
|
+
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
|
|
7799
8009
|
const radius = meanRadius(verts);
|
|
7800
8010
|
const barbLength = radius * Math.max(0, barbLengthRatio);
|
|
7801
8011
|
const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
|
|
@@ -7870,8 +8080,8 @@ const EXTRACTION_ZONE_METADATA = {
|
|
|
7870
8080
|
* `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
|
|
7871
8081
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
7872
8082
|
*/
|
|
7873
|
-
function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
|
|
7874
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
|
|
8083
|
+
function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
8084
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
|
|
7875
8085
|
}
|
|
7876
8086
|
const EXTRACTION_ZONE = defineControlMeasure({
|
|
7877
8087
|
metadata: EXTRACTION_ZONE_METADATA,
|
|
@@ -8771,7 +8981,7 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8771
8981
|
drawRule: "Area1",
|
|
8772
8982
|
capturesLabelSize: true,
|
|
8773
8983
|
params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
|
|
8774
|
-
textAmplifiers:
|
|
8984
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
8775
8985
|
};
|
|
8776
8986
|
/**
|
|
8777
8987
|
* Creates a Fortified Area tactical graphic.
|
|
@@ -8785,8 +8995,8 @@ const FORTIFIED_AREA_METADATA = {
|
|
|
8785
8995
|
* @param options - Configuration options for the graphic
|
|
8786
8996
|
* @param textAmplifiers - Field T (bare, no prefix — "TANGO", not "FA TANGO")
|
|
8787
8997
|
* plus Field H/W/W1, centered on the pre-teeth boundary ring (see
|
|
8788
|
-
* `boundaryInput` below)
|
|
8789
|
-
*
|
|
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).
|
|
8790
9000
|
* @returns A GeoJSON FeatureCollection containing the castellated Polygon
|
|
8791
9001
|
* plus any label Points
|
|
8792
9002
|
*/
|
|
@@ -8816,7 +9026,8 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
8816
9026
|
name: textAmplifiers.T,
|
|
8817
9027
|
additionalInformation: textAmplifiers.H,
|
|
8818
9028
|
dtgStart: textAmplifiers.W,
|
|
8819
|
-
dtgEnd: textAmplifiers.W1
|
|
9029
|
+
dtgEnd: textAmplifiers.W1,
|
|
9030
|
+
hostile: textAmplifiers.N
|
|
8820
9031
|
}, options);
|
|
8821
9032
|
features.push(...labelFeatures);
|
|
8822
9033
|
return {
|
|
@@ -9237,8 +9448,8 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
|
|
|
9237
9448
|
* (Field T) plus `H`/`W`/`W1`/`N` amplifier rows, per
|
|
9238
9449
|
* {@link labeledAreaFeatures}.
|
|
9239
9450
|
*/
|
|
9240
|
-
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9241
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""));
|
|
9451
|
+
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9452
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
|
|
9242
9453
|
}
|
|
9243
9454
|
const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
9244
9455
|
metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
|
|
@@ -9291,8 +9502,8 @@ const LANDING_ZONE_METADATA = {
|
|
|
9291
9502
|
* labeled at its center with "LZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9292
9503
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9293
9504
|
*/
|
|
9294
|
-
function createLandingZone(positions, options = {}, textAmplifiers = {}) {
|
|
9295
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"));
|
|
9505
|
+
function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9506
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
|
|
9296
9507
|
}
|
|
9297
9508
|
const LANDING_ZONE = defineControlMeasure({
|
|
9298
9509
|
metadata: LANDING_ZONE_METADATA,
|
|
@@ -9811,8 +10022,8 @@ const PICKUP_ZONE_METADATA = {
|
|
|
9811
10022
|
* labeled at its center with "PZ "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
9812
10023
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
9813
10024
|
*/
|
|
9814
|
-
function createPickupZone(positions, options = {}, textAmplifiers = {}) {
|
|
9815
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"));
|
|
10025
|
+
function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10026
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
|
|
9816
10027
|
}
|
|
9817
10028
|
const PICKUP_ZONE = defineControlMeasure({
|
|
9818
10029
|
metadata: PICKUP_ZONE_METADATA,
|
|
@@ -9846,19 +10057,23 @@ const ASSAULT_POSITION_METADATA = {
|
|
|
9846
10057
|
value: "151500",
|
|
9847
10058
|
minCoordinates: 3,
|
|
9848
10059
|
geometry: "area",
|
|
9849
|
-
geometryTypes: [
|
|
10060
|
+
geometryTypes: [
|
|
10061
|
+
"Polygon",
|
|
10062
|
+
"MultiLineString",
|
|
10063
|
+
"Point"
|
|
10064
|
+
],
|
|
9850
10065
|
drawRule: "Area1",
|
|
9851
10066
|
capturesLabelSize: true,
|
|
9852
|
-
params:
|
|
9853
|
-
textAmplifiers:
|
|
10067
|
+
params: LABELED_AREA_PARAMS,
|
|
10068
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9854
10069
|
};
|
|
9855
10070
|
/**
|
|
9856
10071
|
* Creates an Assault position control measure: a closed area through
|
|
9857
|
-
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T)
|
|
9858
|
-
*
|
|
10072
|
+
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) plus
|
|
10073
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9859
10074
|
*/
|
|
9860
|
-
function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9861
|
-
return labeledAreaFeatures(positions, options,
|
|
10075
|
+
function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10076
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
|
|
9862
10077
|
}
|
|
9863
10078
|
const ASSAULT_POSITION = defineControlMeasure({
|
|
9864
10079
|
metadata: ASSAULT_POSITION_METADATA,
|
|
@@ -9892,19 +10107,23 @@ const ATTACK_POSITION_METADATA = {
|
|
|
9892
10107
|
value: "151600",
|
|
9893
10108
|
minCoordinates: 3,
|
|
9894
10109
|
geometry: "area",
|
|
9895
|
-
geometryTypes: [
|
|
10110
|
+
geometryTypes: [
|
|
10111
|
+
"Polygon",
|
|
10112
|
+
"MultiLineString",
|
|
10113
|
+
"Point"
|
|
10114
|
+
],
|
|
9896
10115
|
drawRule: "Area1",
|
|
9897
10116
|
capturesLabelSize: true,
|
|
9898
|
-
params:
|
|
9899
|
-
textAmplifiers:
|
|
10117
|
+
params: LABELED_AREA_PARAMS,
|
|
10118
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9900
10119
|
};
|
|
9901
10120
|
/**
|
|
9902
10121
|
* Creates an Attack position control measure: a closed area through
|
|
9903
|
-
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T)
|
|
9904
|
-
*
|
|
10122
|
+
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T) plus
|
|
10123
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9905
10124
|
*/
|
|
9906
|
-
function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
|
|
9907
|
-
return labeledAreaFeatures(positions, options,
|
|
10125
|
+
function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10126
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
|
|
9908
10127
|
}
|
|
9909
10128
|
const ATTACK_POSITION = defineControlMeasure({
|
|
9910
10129
|
metadata: ATTACK_POSITION_METADATA,
|
|
@@ -9938,19 +10157,23 @@ const OBJECTIVE_AREA_METADATA = {
|
|
|
9938
10157
|
value: "151700",
|
|
9939
10158
|
minCoordinates: 3,
|
|
9940
10159
|
geometry: "area",
|
|
9941
|
-
geometryTypes: [
|
|
10160
|
+
geometryTypes: [
|
|
10161
|
+
"Polygon",
|
|
10162
|
+
"MultiLineString",
|
|
10163
|
+
"Point"
|
|
10164
|
+
],
|
|
9942
10165
|
drawRule: "Area1",
|
|
9943
10166
|
capturesLabelSize: true,
|
|
9944
|
-
params:
|
|
9945
|
-
textAmplifiers:
|
|
10167
|
+
params: LABELED_AREA_PARAMS,
|
|
10168
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
9946
10169
|
};
|
|
9947
10170
|
/**
|
|
9948
10171
|
* Creates an Objective area control measure: a closed area through
|
|
9949
|
-
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T)
|
|
9950
|
-
*
|
|
10172
|
+
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) plus
|
|
10173
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9951
10174
|
*/
|
|
9952
|
-
function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
|
|
9953
|
-
return labeledAreaFeatures(positions, options,
|
|
10175
|
+
function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10176
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
|
|
9954
10177
|
}
|
|
9955
10178
|
const OBJECTIVE_AREA = defineControlMeasure({
|
|
9956
10179
|
metadata: OBJECTIVE_AREA_METADATA,
|
|
@@ -10073,7 +10296,7 @@ const STRONG_POINT_METADATA = {
|
|
|
10073
10296
|
drawRule: "Area1",
|
|
10074
10297
|
capturesLabelSize: true,
|
|
10075
10298
|
params: ECHELON_AREA_PARAMS,
|
|
10076
|
-
textAmplifiers:
|
|
10299
|
+
textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
|
|
10077
10300
|
};
|
|
10078
10301
|
/**
|
|
10079
10302
|
* Outward perimeter tics: a stroke every `h` meters along the ring, each `h`
|
|
@@ -10108,17 +10331,24 @@ function buildStrongPointTics(perimeter, gaps, h) {
|
|
|
10108
10331
|
* @param positions - Area anchor points (>=3); the input contract is enforced
|
|
10109
10332
|
* at the render seam (ADR-0014).
|
|
10110
10333
|
* @param options - {@link StrongPointOptions}.
|
|
10111
|
-
* @param textAmplifiers - Field T
|
|
10112
|
-
*
|
|
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.
|
|
10113
10337
|
*/
|
|
10114
|
-
function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
10338
|
+
function createStrongPoint(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10115
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;
|
|
10116
10340
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
10117
10341
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION);
|
|
10118
10342
|
const perimeter = buildAreaPerimeter(verts);
|
|
10119
10343
|
const { strokes, fills, gaps } = placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
10120
|
-
const
|
|
10121
|
-
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);
|
|
10122
10352
|
const features = [];
|
|
10123
10353
|
if (boundaryCoords.length > 0) features.push({
|
|
10124
10354
|
type: "Feature",
|
|
@@ -10155,8 +10385,6 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}) {
|
|
|
10155
10385
|
coordinates: fills.map((ring) => [ring])
|
|
10156
10386
|
}
|
|
10157
10387
|
});
|
|
10158
|
-
const labelFeatures = [];
|
|
10159
|
-
pushAreaLabels(labelFeatures, verts, { name: textAmplifiers.T }, options);
|
|
10160
10388
|
features.push(...labelFeatures);
|
|
10161
10389
|
return {
|
|
10162
10390
|
type: "FeatureCollection",
|
|
@@ -10216,8 +10444,8 @@ const SUBMARINE_ACTION_AREA_METADATA = {
|
|
|
10216
10444
|
* `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
|
|
10217
10445
|
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10218
10446
|
*/
|
|
10219
|
-
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10220
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
|
|
10447
|
+
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10448
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
|
|
10221
10449
|
}
|
|
10222
10450
|
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
10223
10451
|
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
@@ -10271,8 +10499,8 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
|
10271
10499
|
* through `positions` labeled at its center with "SGSA -"-prefixed `T`
|
|
10272
10500
|
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
10273
10501
|
*/
|
|
10274
|
-
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
10275
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
|
|
10502
|
+
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10503
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
|
|
10276
10504
|
}
|
|
10277
10505
|
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
10278
10506
|
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
@@ -10854,141 +11082,6 @@ function applyLayer(target, layer) {
|
|
|
10854
11082
|
}
|
|
10855
11083
|
}
|
|
10856
11084
|
//#endregion
|
|
10857
|
-
//#region src/text-amplifiers.ts
|
|
10858
|
-
/**
|
|
10859
|
-
* The vocabulary of text amplifier fields: doctrinal code, milsymbol-style
|
|
10860
|
-
* friendly alias, and a human-readable label. The single source of truth for
|
|
10861
|
-
* both the runtime catalog and the derived alias→code lookup used by
|
|
10862
|
-
* {@link normalizeTextAmplifiers}.
|
|
10863
|
-
*/
|
|
10864
|
-
const TEXT_AMPLIFIER_FIELDS = {
|
|
10865
|
-
C: {
|
|
10866
|
-
alias: "quantity",
|
|
10867
|
-
label: "Quantity"
|
|
10868
|
-
},
|
|
10869
|
-
H: {
|
|
10870
|
-
alias: "additionalInformation",
|
|
10871
|
-
label: "Additional information"
|
|
10872
|
-
},
|
|
10873
|
-
N: {
|
|
10874
|
-
alias: "hostile",
|
|
10875
|
-
label: "Hostile (ENY) marker"
|
|
10876
|
-
},
|
|
10877
|
-
T: {
|
|
10878
|
-
alias: "uniqueDesignation",
|
|
10879
|
-
label: "Unique designation"
|
|
10880
|
-
},
|
|
10881
|
-
V: {
|
|
10882
|
-
alias: "type",
|
|
10883
|
-
label: "Equipment/type"
|
|
10884
|
-
},
|
|
10885
|
-
W: {
|
|
10886
|
-
alias: "dtg",
|
|
10887
|
-
label: "Date-time group / O/O"
|
|
10888
|
-
},
|
|
10889
|
-
X: {
|
|
10890
|
-
alias: "altitudeDepth",
|
|
10891
|
-
label: "Altitude/depth"
|
|
10892
|
-
},
|
|
10893
|
-
Y: {
|
|
10894
|
-
alias: "location",
|
|
10895
|
-
label: "Location"
|
|
10896
|
-
},
|
|
10897
|
-
AM: {
|
|
10898
|
-
alias: "distance",
|
|
10899
|
-
label: "Distance/range/radius"
|
|
10900
|
-
},
|
|
10901
|
-
AN: {
|
|
10902
|
-
alias: "azimuth",
|
|
10903
|
-
label: "Azimuth"
|
|
10904
|
-
},
|
|
10905
|
-
AP: {
|
|
10906
|
-
alias: "targetNumber",
|
|
10907
|
-
label: "Target number"
|
|
10908
|
-
},
|
|
10909
|
-
AS: {
|
|
10910
|
-
alias: "country",
|
|
10911
|
-
label: "Country code"
|
|
10912
|
-
}
|
|
10913
|
-
};
|
|
10914
|
-
/** Derived alias→code lookup, so the vocabulary table stays the single source of truth. */
|
|
10915
|
-
const CODE_BY_ALIAS = Object.fromEntries(Object.entries(TEXT_AMPLIFIER_FIELDS).map(([code, { alias }]) => [alias, code]));
|
|
10916
|
-
const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code) => [
|
|
10917
|
-
code,
|
|
10918
|
-
`${code}1`,
|
|
10919
|
-
`${code}2`
|
|
10920
|
-
]));
|
|
10921
|
-
/**
|
|
10922
|
-
* Resolves a raw placement entry to its object form: a bare `Position` is
|
|
10923
|
-
* legacy shorthand for `{ position }`; an object form passes through only the
|
|
10924
|
-
* fields that are actually valid (`position` must be a finite `[lon, lat]`,
|
|
10925
|
-
* `rotation` a finite number); `undefined` resolves to `{}`.
|
|
10926
|
-
*/
|
|
10927
|
-
function resolveAmplifierPlacement(value) {
|
|
10928
|
-
if (value === void 0) return {};
|
|
10929
|
-
if (Array.isArray(value)) return isValidPosition(value) ? { position: value } : {};
|
|
10930
|
-
const result = {};
|
|
10931
|
-
if (value.position !== void 0 && isValidPosition(value.position)) result.position = value.position;
|
|
10932
|
-
if (typeof value.rotation === "number" && Number.isFinite(value.rotation)) result.rotation = value.rotation;
|
|
10933
|
-
return result;
|
|
10934
|
-
}
|
|
10935
|
-
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
10936
|
-
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
10937
|
-
function splitSuffix(key) {
|
|
10938
|
-
const last = key.charAt(key.length - 1);
|
|
10939
|
-
if (last === "1" || last === "2") return {
|
|
10940
|
-
base: key.slice(0, -1),
|
|
10941
|
-
suffix: last
|
|
10942
|
-
};
|
|
10943
|
-
return {
|
|
10944
|
-
base: key,
|
|
10945
|
-
suffix: ""
|
|
10946
|
-
};
|
|
10947
|
-
}
|
|
10948
|
-
/**
|
|
10949
|
-
* Resolves any accepted key (canonical or friendly alias, with an optional
|
|
10950
|
-
* `1`/`2` suffix) to its canonical key, or `undefined` if unknown — e.g.
|
|
10951
|
-
* `"T"` → `"T"`, `"uniqueDesignation1"` → `"T1"`, `"bogus"` → `undefined`.
|
|
10952
|
-
*/
|
|
10953
|
-
function canonicalTextAmplifierKey(key) {
|
|
10954
|
-
if (CANONICAL_KEYS.has(key)) return key;
|
|
10955
|
-
const { base, suffix } = splitSuffix(key);
|
|
10956
|
-
const code = CODE_BY_ALIAS[base];
|
|
10957
|
-
return code ? `${code}${suffix}` : void 0;
|
|
10958
|
-
}
|
|
10959
|
-
/**
|
|
10960
|
-
* Resolves a `TextAmplifiers` record (canonical keys, friendly aliases, or a
|
|
10961
|
-
* mix) to `CanonicalTextAmplifiers` keyed purely by doctrinal field code.
|
|
10962
|
-
*
|
|
10963
|
-
* - Friendly aliases (with an optional `1`/`2` suffix) map to their canonical
|
|
10964
|
-
* code (e.g. `country1` → `AS1`).
|
|
10965
|
-
* - Values are trimmed; entries that are empty/whitespace-only after
|
|
10966
|
-
* trimming, or not strings, are dropped.
|
|
10967
|
-
* - When both a canonical key and its friendly alias are present for the same
|
|
10968
|
-
* field + suffix, the **canonical key wins** regardless of insertion order.
|
|
10969
|
-
* This holds even for an empty/whitespace canonical value: it suppresses
|
|
10970
|
-
* (clears) the alias entry rather than letting the alias survive.
|
|
10971
|
-
* - Unknown keys are ignored.
|
|
10972
|
-
*
|
|
10973
|
-
* `undefined` or an empty input returns a shared frozen empty object.
|
|
10974
|
-
*/
|
|
10975
|
-
function normalizeTextAmplifiers(input) {
|
|
10976
|
-
if (!input) return EMPTY_TEXT_AMPLIFIERS;
|
|
10977
|
-
const canonicalPresent = /* @__PURE__ */ new Set();
|
|
10978
|
-
for (const [key, rawValue] of Object.entries(input)) if (typeof rawValue === "string" && CANONICAL_KEYS.has(key)) canonicalPresent.add(key);
|
|
10979
|
-
const result = {};
|
|
10980
|
-
for (const [key, rawValue] of Object.entries(input)) {
|
|
10981
|
-
if (typeof rawValue !== "string") continue;
|
|
10982
|
-
const value = rawValue.trim();
|
|
10983
|
-
if (value.length === 0) continue;
|
|
10984
|
-
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
10985
|
-
if (!canonicalKey) continue;
|
|
10986
|
-
if (!CANONICAL_KEYS.has(key) && canonicalPresent.has(canonicalKey)) continue;
|
|
10987
|
-
result[canonicalKey] = value;
|
|
10988
|
-
}
|
|
10989
|
-
return Object.keys(result).length > 0 ? Object.freeze(result) : EMPTY_TEXT_AMPLIFIERS;
|
|
10990
|
-
}
|
|
10991
|
-
//#endregion
|
|
10992
11085
|
//#region src/renderControlMeasure.ts
|
|
10993
11086
|
/**
|
|
10994
11087
|
* Pure render: `ControlMeasure` → `ControlMeasureRender` (a
|
|
@@ -11024,7 +11117,7 @@ function dispatchControlMeasure(cm, opts) {
|
|
|
11024
11117
|
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
11025
11118
|
const generator = definition.generator;
|
|
11026
11119
|
const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
|
|
11027
|
-
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers);
|
|
11120
|
+
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
|
|
11028
11121
|
}
|
|
11029
11122
|
/**
|
|
11030
11123
|
* The control-point *input contract*: at least `metadata.minCoordinates` points,
|
|
@@ -11041,15 +11134,15 @@ function validateInputContract(points, metadata, mode) {
|
|
|
11041
11134
|
}
|
|
11042
11135
|
/**
|
|
11043
11136
|
* The `amplifierPlacements` key that repositions this feature, if any: the
|
|
11044
|
-
* canonical amplifier field when the
|
|
11045
|
-
*
|
|
11137
|
+
* explicit `labelPlacementKey`, the canonical amplifier field when the
|
|
11138
|
+
* generator declared one, or a synthesized `label:<part>:<ordinal>` key for any
|
|
11046
11139
|
* other point label — unless the generator opted out via
|
|
11047
11140
|
* `labelPlacement: false`.
|
|
11048
11141
|
*/
|
|
11049
11142
|
function resolveLabelPlacementKey(sourceProps, amplifierField, text, geometryType, part, generatedLabelOrdinal) {
|
|
11050
11143
|
if (sourceProps.labelPlacement === false) return void 0;
|
|
11051
|
-
if (amplifierField) return amplifierField;
|
|
11052
11144
|
if (typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) return sourceProps.labelPlacementKey;
|
|
11145
|
+
if (amplifierField) return amplifierField;
|
|
11053
11146
|
if (text !== void 0 && geometryType === "Point") return `label:${part}:${generatedLabelOrdinal}`;
|
|
11054
11147
|
}
|
|
11055
11148
|
function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyle, graphicsStyle, amplifierPlacements) {
|
|
@@ -11135,4 +11228,4 @@ function assertNever(value) {
|
|
|
11135
11228
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
11136
11229
|
}
|
|
11137
11230
|
//#endregion
|
|
11138
|
-
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 };
|