@orbat-mapper/control-measures 0.2.0-alpha.10 → 0.2.0-alpha.12
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-D3uCYKe3.d.mts → index-CsJcNxFQ.d.mts} +335 -69
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/preview/index.d.mts +65 -10
- package/dist/preview/index.mjs +129 -18
- package/dist/{renderControlMeasure-BblcXXLp.mjs → renderControlMeasure-DHQO01PA.mjs} +1531 -615
- package/media/airborne-attack.svg +1 -1
- package/media/ambush.svg +1 -1
- package/media/antitank-ditch-completed.svg +1 -1
- package/media/antitank-ditch-under-construction.svg +1 -1
- package/media/antitank-wall.svg +1 -1
- package/media/area-of-operations.svg +1 -0
- package/media/assembly-area.svg +1 -0
- package/media/attack-by-fire.svg +1 -1
- package/media/attack-helicopter.svg +1 -1
- package/media/battle-handover-line.svg +1 -0
- package/media/battle-position.svg +1 -1
- package/media/block-arrow.svg +1 -1
- package/media/block-mission-task.svg +1 -1
- package/media/block.svg +1 -1
- package/media/boundary.svg +1 -1
- package/media/breach.svg +1 -1
- package/media/bypass.svg +1 -1
- package/media/canalize.svg +1 -1
- package/media/circle.svg +1 -1
- package/media/classic-arrow.svg +1 -1
- package/media/clear.svg +1 -1
- package/media/delay.svg +1 -1
- package/media/disrupt.svg +1 -1
- package/media/encirclement.svg +1 -1
- package/media/engineer-work-line.svg +1 -0
- package/media/final-protective-fire-left.svg +1 -1
- package/media/final-protective-fire-right.svg +1 -1
- package/media/fix.svg +1 -1
- package/media/flot.svg +1 -1
- package/media/fortified-area.svg +1 -1
- package/media/fortified-line.svg +1 -1
- package/media/generic-c2-line.svg +1 -0
- package/media/handover-line.svg +1 -0
- package/media/isolate.svg +1 -1
- package/media/joint-tactical-action-area.svg +1 -0
- package/media/landing-zone.svg +1 -0
- package/media/light-line.svg +1 -0
- package/media/line.svg +1 -1
- package/media/main-attack.svg +1 -1
- package/media/obstacle-bypass-difficult.svg +1 -1
- package/media/obstacle-bypass-easy.svg +1 -1
- package/media/obstacle-bypass-impossible.svg +1 -1
- package/media/phase-line.svg +1 -0
- package/media/pickup-zone.svg +1 -0
- package/media/polygon.svg +1 -1
- package/media/principal-direction-of-fire.svg +1 -1
- package/media/rectangle.svg +1 -1
- package/media/search-area.svg +1 -1
- package/media/strong-point.svg +1 -1
- package/media/support-by-fire.svg +1 -1
- package/media/supporting-attack.svg +1 -1
- package/media/turn.svg +1 -1
- package/package.json +1 -1
|
@@ -963,7 +963,231 @@ const rectangleDrawRule = {
|
|
|
963
963
|
}
|
|
964
964
|
};
|
|
965
965
|
//#endregion
|
|
966
|
+
//#region src/internal/echelon.ts
|
|
967
|
+
/**
|
|
968
|
+
* APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
|
|
969
|
+
* stamp a unit-size amplifier onto their geometry (Boundary, Battle Position).
|
|
970
|
+
*
|
|
971
|
+
* A compact port of the APP-6 echelon designs, modeled on milsymbol but
|
|
972
|
+
* expressed as parametric geometry so it bakes into GeoJSON and strokes
|
|
973
|
+
* monocolor (ADR-0023). Every glyph is built from three primitives — vertical
|
|
974
|
+
* bars, crosses, and dots — plus the team/crew ring and command `++`.
|
|
975
|
+
*/
|
|
976
|
+
const ECHELONS = {
|
|
977
|
+
team: {
|
|
978
|
+
kind: "ring",
|
|
979
|
+
count: 1,
|
|
980
|
+
label: "Team/Crew"
|
|
981
|
+
},
|
|
982
|
+
crew: {
|
|
983
|
+
kind: "ring",
|
|
984
|
+
count: 1
|
|
985
|
+
},
|
|
986
|
+
squad: {
|
|
987
|
+
kind: "dots",
|
|
988
|
+
count: 1,
|
|
989
|
+
label: "Squad"
|
|
990
|
+
},
|
|
991
|
+
section: {
|
|
992
|
+
kind: "dots",
|
|
993
|
+
count: 2,
|
|
994
|
+
label: "Section"
|
|
995
|
+
},
|
|
996
|
+
platoon: {
|
|
997
|
+
kind: "dots",
|
|
998
|
+
count: 3,
|
|
999
|
+
label: "Platoon"
|
|
1000
|
+
},
|
|
1001
|
+
company: {
|
|
1002
|
+
kind: "bars",
|
|
1003
|
+
count: 1,
|
|
1004
|
+
label: "Company"
|
|
1005
|
+
},
|
|
1006
|
+
battalion: {
|
|
1007
|
+
kind: "bars",
|
|
1008
|
+
count: 2,
|
|
1009
|
+
label: "Battalion"
|
|
1010
|
+
},
|
|
1011
|
+
regiment: {
|
|
1012
|
+
kind: "bars",
|
|
1013
|
+
count: 3,
|
|
1014
|
+
label: "Regiment"
|
|
1015
|
+
},
|
|
1016
|
+
brigade: {
|
|
1017
|
+
kind: "crosses",
|
|
1018
|
+
count: 1,
|
|
1019
|
+
label: "Brigade"
|
|
1020
|
+
},
|
|
1021
|
+
division: {
|
|
1022
|
+
kind: "crosses",
|
|
1023
|
+
count: 2,
|
|
1024
|
+
label: "Division"
|
|
1025
|
+
},
|
|
1026
|
+
corps: {
|
|
1027
|
+
kind: "crosses",
|
|
1028
|
+
count: 3,
|
|
1029
|
+
label: "Corps"
|
|
1030
|
+
},
|
|
1031
|
+
army: {
|
|
1032
|
+
kind: "crosses",
|
|
1033
|
+
count: 4,
|
|
1034
|
+
label: "Army"
|
|
1035
|
+
},
|
|
1036
|
+
"army-group": {
|
|
1037
|
+
kind: "crosses",
|
|
1038
|
+
count: 5,
|
|
1039
|
+
label: "Army Group"
|
|
1040
|
+
},
|
|
1041
|
+
region: {
|
|
1042
|
+
kind: "crosses",
|
|
1043
|
+
count: 6,
|
|
1044
|
+
label: "Region"
|
|
1045
|
+
},
|
|
1046
|
+
command: {
|
|
1047
|
+
kind: "command",
|
|
1048
|
+
count: 2,
|
|
1049
|
+
label: "Command"
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
/** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
|
|
1053
|
+
const ECHELON_ALIASES = {
|
|
1054
|
+
ø: "team",
|
|
1055
|
+
"•": "squad",
|
|
1056
|
+
"••": "section",
|
|
1057
|
+
"•••": "platoon",
|
|
1058
|
+
i: "company",
|
|
1059
|
+
ii: "battalion",
|
|
1060
|
+
iii: "regiment",
|
|
1061
|
+
x: "brigade",
|
|
1062
|
+
xx: "division",
|
|
1063
|
+
xxx: "corps",
|
|
1064
|
+
xxxx: "army",
|
|
1065
|
+
xxxxx: "army-group",
|
|
1066
|
+
xxxxxx: "region",
|
|
1067
|
+
"++": "command"
|
|
1068
|
+
};
|
|
1069
|
+
/** Enum options for the params panel, derived from {@link ECHELONS}. */
|
|
1070
|
+
const ECHELON_PARAM_OPTIONS = [{
|
|
1071
|
+
label: "None",
|
|
1072
|
+
value: "none"
|
|
1073
|
+
}, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
|
|
1074
|
+
label: spec.label,
|
|
1075
|
+
value
|
|
1076
|
+
}))];
|
|
1077
|
+
function resolveEchelon(echelon) {
|
|
1078
|
+
const key = echelon.trim().toLowerCase();
|
|
1079
|
+
if (key.length === 0) return void 0;
|
|
1080
|
+
const alias = ECHELON_ALIASES[key];
|
|
1081
|
+
return ECHELONS[key] ?? (alias ? ECHELONS[alias] : void 0);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Builds the echelon glyph centered at `center`, with its baseline along
|
|
1085
|
+
* `along` and `perp` (both unit vectors), `h` meters tall. Returns the glyph's
|
|
1086
|
+
* `strokes` (open polylines), `fills` (closed rings drawn as solid shapes —
|
|
1087
|
+
* the squad/section/platoon dots), and its intrinsic width along the line; the
|
|
1088
|
+
* caller adds the configurable side padding to derive the line gap.
|
|
1089
|
+
*/
|
|
1090
|
+
function buildEchelonGlyph(center, along, perp, h, spec) {
|
|
1091
|
+
const P = (a, p) => {
|
|
1092
|
+
const q = vecAdd(vecAdd(center, vecScale(along, a)), vecScale(perp, p));
|
|
1093
|
+
return unproject(q[0], q[1]);
|
|
1094
|
+
};
|
|
1095
|
+
const ring = (a, r) => {
|
|
1096
|
+
const pts = [];
|
|
1097
|
+
const steps = 16;
|
|
1098
|
+
for (let i = 0; i <= steps; i++) {
|
|
1099
|
+
const t = 2 * Math.PI * i / steps;
|
|
1100
|
+
pts.push(P(a + r * Math.cos(t), r * Math.sin(t)));
|
|
1101
|
+
}
|
|
1102
|
+
return pts;
|
|
1103
|
+
};
|
|
1104
|
+
const strokes = [];
|
|
1105
|
+
const fills = [];
|
|
1106
|
+
const half = h / 2;
|
|
1107
|
+
switch (spec.kind) {
|
|
1108
|
+
case "bars": {
|
|
1109
|
+
const spacing = h * .8;
|
|
1110
|
+
const total = (spec.count - 1) * spacing;
|
|
1111
|
+
for (let j = 0; j < spec.count; j++) {
|
|
1112
|
+
const a = -total / 2 + j * spacing;
|
|
1113
|
+
strokes.push([P(a, -half), P(a, half)]);
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
strokes,
|
|
1117
|
+
fills,
|
|
1118
|
+
width: total
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
case "crosses": {
|
|
1122
|
+
const w = h;
|
|
1123
|
+
const spacing = h * 1.4;
|
|
1124
|
+
const total = (spec.count - 1) * spacing;
|
|
1125
|
+
for (let j = 0; j < spec.count; j++) {
|
|
1126
|
+
const a0 = -total / 2 + j * spacing;
|
|
1127
|
+
strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
|
|
1128
|
+
strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
strokes,
|
|
1132
|
+
fills,
|
|
1133
|
+
width: total + w
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
case "dots": {
|
|
1137
|
+
const r = h * .3;
|
|
1138
|
+
const spacing = h * 1.2;
|
|
1139
|
+
const total = (spec.count - 1) * spacing;
|
|
1140
|
+
for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
|
|
1141
|
+
return {
|
|
1142
|
+
strokes,
|
|
1143
|
+
fills,
|
|
1144
|
+
width: total + 2 * r
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
case "ring": {
|
|
1148
|
+
const r = h * .6;
|
|
1149
|
+
strokes.push(ring(0, r));
|
|
1150
|
+
strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
|
|
1151
|
+
return {
|
|
1152
|
+
strokes,
|
|
1153
|
+
fills,
|
|
1154
|
+
width: h * 1.6
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
case "command": {
|
|
1158
|
+
const arm = h * .5;
|
|
1159
|
+
const spacing = h * 1.4;
|
|
1160
|
+
const total = (spec.count - 1) * spacing;
|
|
1161
|
+
for (let j = 0; j < spec.count; j++) {
|
|
1162
|
+
const a0 = -total / 2 + j * spacing;
|
|
1163
|
+
strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
|
|
1164
|
+
strokes.push([P(a0, -half), P(a0, half)]);
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
strokes,
|
|
1168
|
+
fills,
|
|
1169
|
+
width: total + 2 * arm
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
//#endregion
|
|
966
1175
|
//#region src/generators/cm15-maneuver-areas/params.ts
|
|
1176
|
+
const AREA_SMOOTH_PARAMS = [{
|
|
1177
|
+
key: "smooth",
|
|
1178
|
+
label: "Smooth",
|
|
1179
|
+
description: "Round the area's corners by curving the boundary through the control points.",
|
|
1180
|
+
type: "boolean"
|
|
1181
|
+
}, {
|
|
1182
|
+
key: "smoothResolution",
|
|
1183
|
+
label: "Smooth resolution",
|
|
1184
|
+
description: "Number of samples per segment when smooth mode is enabled.",
|
|
1185
|
+
type: "number",
|
|
1186
|
+
min: 2,
|
|
1187
|
+
max: 64,
|
|
1188
|
+
step: 1,
|
|
1189
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1190
|
+
}];
|
|
967
1191
|
const ATTACK_SHAFT_PARAMS = [
|
|
968
1192
|
{
|
|
969
1193
|
key: "shaftWidthRatio",
|
|
@@ -1075,25 +1299,7 @@ const ENCIRCLEMENT_PARAMS = [
|
|
|
1075
1299
|
step: .01
|
|
1076
1300
|
}
|
|
1077
1301
|
];
|
|
1078
|
-
const FORTIFIED_AREA_PARAMS = [
|
|
1079
|
-
{
|
|
1080
|
-
key: "smooth",
|
|
1081
|
-
label: "Smooth",
|
|
1082
|
-
description: "Round the area's corners by curving the boundary through the control points.",
|
|
1083
|
-
type: "boolean"
|
|
1084
|
-
},
|
|
1085
|
-
{
|
|
1086
|
-
key: "smoothResolution",
|
|
1087
|
-
label: "Smooth resolution",
|
|
1088
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
1089
|
-
type: "number",
|
|
1090
|
-
min: 2,
|
|
1091
|
-
max: 64,
|
|
1092
|
-
step: 1,
|
|
1093
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1094
|
-
},
|
|
1095
|
-
...FORTIFIED_AREA_SIZE_PARAMS
|
|
1096
|
-
];
|
|
1302
|
+
const FORTIFIED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, ...FORTIFIED_AREA_SIZE_PARAMS];
|
|
1097
1303
|
const AREA_LABEL_PADDING_PARAM = {
|
|
1098
1304
|
key: "labelPadding",
|
|
1099
1305
|
label: "Label padding",
|
|
@@ -1103,22 +1309,53 @@ const AREA_LABEL_PADDING_PARAM = {
|
|
|
1103
1309
|
max: 2,
|
|
1104
1310
|
step: .05
|
|
1105
1311
|
};
|
|
1106
|
-
const LABELED_AREA_PARAMS = [
|
|
1312
|
+
const LABELED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM];
|
|
1313
|
+
const ECHELON_AREA_PARAMS = [
|
|
1314
|
+
...AREA_SMOOTH_PARAMS,
|
|
1107
1315
|
{
|
|
1108
|
-
key: "
|
|
1109
|
-
label: "
|
|
1110
|
-
description: "
|
|
1111
|
-
type: "
|
|
1316
|
+
key: "echelon",
|
|
1317
|
+
label: "Echelon",
|
|
1318
|
+
description: "Field B unit-size amplifier, drawn on the boundary.",
|
|
1319
|
+
type: "enum",
|
|
1320
|
+
options: ECHELON_PARAM_OPTIONS
|
|
1112
1321
|
},
|
|
1113
1322
|
{
|
|
1114
|
-
key: "
|
|
1115
|
-
label: "
|
|
1116
|
-
description: "
|
|
1323
|
+
key: "echelonSizePixels",
|
|
1324
|
+
label: "Echelon size",
|
|
1325
|
+
description: "Echelon glyph height in screen pixels.",
|
|
1117
1326
|
type: "number",
|
|
1118
|
-
min:
|
|
1119
|
-
max:
|
|
1327
|
+
min: 8,
|
|
1328
|
+
max: 48,
|
|
1120
1329
|
step: 1,
|
|
1121
|
-
|
|
1330
|
+
unit: "px"
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
key: "echelonSize",
|
|
1334
|
+
label: "Echelon size",
|
|
1335
|
+
description: "Echelon glyph height in meters.",
|
|
1336
|
+
type: "number",
|
|
1337
|
+
min: 50,
|
|
1338
|
+
max: 2e4,
|
|
1339
|
+
step: 50,
|
|
1340
|
+
unit: "m"
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
key: "echelonPadding",
|
|
1344
|
+
label: "Echelon padding",
|
|
1345
|
+
description: "Gap on each side of the echelon, as a ratio of its height.",
|
|
1346
|
+
type: "number",
|
|
1347
|
+
min: 0,
|
|
1348
|
+
max: 2,
|
|
1349
|
+
step: .05
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
key: "echelonPosition",
|
|
1353
|
+
label: "Echelon position",
|
|
1354
|
+
description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
|
|
1355
|
+
type: "number",
|
|
1356
|
+
min: -.5,
|
|
1357
|
+
max: .5,
|
|
1358
|
+
step: .01
|
|
1122
1359
|
},
|
|
1123
1360
|
AREA_LABEL_PADDING_PARAM
|
|
1124
1361
|
];
|
|
@@ -1165,6 +1402,16 @@ const AREA_TEXT_AMPLIFIERS = [
|
|
|
1165
1402
|
/** {@link AREA_TEXT_AMPLIFIERS} without the N (hostile) field, for measures
|
|
1166
1403
|
* whose template carries no ENY marker (Fortified Area). */
|
|
1167
1404
|
const AREA_TEXT_AMPLIFIERS_NO_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "N");
|
|
1405
|
+
/** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
|
|
1406
|
+
* for measures whose template omits that row but keeps the W/W1 window and
|
|
1407
|
+
* the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
|
|
1408
|
+
* Area). */
|
|
1409
|
+
const AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "H");
|
|
1410
|
+
/** {@link AREA_TEXT_AMPLIFIERS} reduced to just the designation (Field T) and
|
|
1411
|
+
* hostile marker (Field N), for measures whose template carries no
|
|
1412
|
+
* additional-information or date-time-group rows (Area, Drop Zone,
|
|
1413
|
+
* Extraction Zone). */
|
|
1414
|
+
const AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key === "T" || d.key === "N");
|
|
1168
1415
|
//#endregion
|
|
1169
1416
|
//#region src/generators/cm15-maneuver-areas/airborneAttack.ts
|
|
1170
1417
|
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
@@ -1415,215 +1662,6 @@ const AMBUSH = defineControlMeasure({
|
|
|
1415
1662
|
rule: ambushDrawRule
|
|
1416
1663
|
});
|
|
1417
1664
|
//#endregion
|
|
1418
|
-
//#region src/internal/echelon.ts
|
|
1419
|
-
/**
|
|
1420
|
-
* APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
|
|
1421
|
-
* stamp a unit-size amplifier onto their geometry (Boundary, Battle Position).
|
|
1422
|
-
*
|
|
1423
|
-
* A compact port of the APP-6 echelon designs, modeled on milsymbol but
|
|
1424
|
-
* expressed as parametric geometry so it bakes into GeoJSON and strokes
|
|
1425
|
-
* monocolor (ADR-0023). Every glyph is built from three primitives — vertical
|
|
1426
|
-
* bars, crosses, and dots — plus the team/crew ring and command `++`.
|
|
1427
|
-
*/
|
|
1428
|
-
const ECHELONS = {
|
|
1429
|
-
team: {
|
|
1430
|
-
kind: "ring",
|
|
1431
|
-
count: 1,
|
|
1432
|
-
label: "Team/Crew"
|
|
1433
|
-
},
|
|
1434
|
-
crew: {
|
|
1435
|
-
kind: "ring",
|
|
1436
|
-
count: 1
|
|
1437
|
-
},
|
|
1438
|
-
squad: {
|
|
1439
|
-
kind: "dots",
|
|
1440
|
-
count: 1,
|
|
1441
|
-
label: "Squad"
|
|
1442
|
-
},
|
|
1443
|
-
section: {
|
|
1444
|
-
kind: "dots",
|
|
1445
|
-
count: 2,
|
|
1446
|
-
label: "Section"
|
|
1447
|
-
},
|
|
1448
|
-
platoon: {
|
|
1449
|
-
kind: "dots",
|
|
1450
|
-
count: 3,
|
|
1451
|
-
label: "Platoon"
|
|
1452
|
-
},
|
|
1453
|
-
company: {
|
|
1454
|
-
kind: "bars",
|
|
1455
|
-
count: 1,
|
|
1456
|
-
label: "Company"
|
|
1457
|
-
},
|
|
1458
|
-
battalion: {
|
|
1459
|
-
kind: "bars",
|
|
1460
|
-
count: 2,
|
|
1461
|
-
label: "Battalion"
|
|
1462
|
-
},
|
|
1463
|
-
regiment: {
|
|
1464
|
-
kind: "bars",
|
|
1465
|
-
count: 3,
|
|
1466
|
-
label: "Regiment"
|
|
1467
|
-
},
|
|
1468
|
-
brigade: {
|
|
1469
|
-
kind: "crosses",
|
|
1470
|
-
count: 1,
|
|
1471
|
-
label: "Brigade"
|
|
1472
|
-
},
|
|
1473
|
-
division: {
|
|
1474
|
-
kind: "crosses",
|
|
1475
|
-
count: 2,
|
|
1476
|
-
label: "Division"
|
|
1477
|
-
},
|
|
1478
|
-
corps: {
|
|
1479
|
-
kind: "crosses",
|
|
1480
|
-
count: 3,
|
|
1481
|
-
label: "Corps"
|
|
1482
|
-
},
|
|
1483
|
-
army: {
|
|
1484
|
-
kind: "crosses",
|
|
1485
|
-
count: 4,
|
|
1486
|
-
label: "Army"
|
|
1487
|
-
},
|
|
1488
|
-
"army-group": {
|
|
1489
|
-
kind: "crosses",
|
|
1490
|
-
count: 5,
|
|
1491
|
-
label: "Army Group"
|
|
1492
|
-
},
|
|
1493
|
-
region: {
|
|
1494
|
-
kind: "crosses",
|
|
1495
|
-
count: 6,
|
|
1496
|
-
label: "Region"
|
|
1497
|
-
},
|
|
1498
|
-
command: {
|
|
1499
|
-
kind: "command",
|
|
1500
|
-
count: 2,
|
|
1501
|
-
label: "Command"
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
1504
|
-
/** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
|
|
1505
|
-
const ECHELON_ALIASES = {
|
|
1506
|
-
ø: "team",
|
|
1507
|
-
"•": "squad",
|
|
1508
|
-
"••": "section",
|
|
1509
|
-
"•••": "platoon",
|
|
1510
|
-
i: "company",
|
|
1511
|
-
ii: "battalion",
|
|
1512
|
-
iii: "regiment",
|
|
1513
|
-
x: "brigade",
|
|
1514
|
-
xx: "division",
|
|
1515
|
-
xxx: "corps",
|
|
1516
|
-
xxxx: "army",
|
|
1517
|
-
xxxxx: "army-group",
|
|
1518
|
-
xxxxxx: "region",
|
|
1519
|
-
"++": "command"
|
|
1520
|
-
};
|
|
1521
|
-
/** Enum options for the params panel, derived from {@link ECHELONS}. */
|
|
1522
|
-
const ECHELON_PARAM_OPTIONS = [{
|
|
1523
|
-
label: "None",
|
|
1524
|
-
value: "none"
|
|
1525
|
-
}, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
|
|
1526
|
-
label: spec.label,
|
|
1527
|
-
value
|
|
1528
|
-
}))];
|
|
1529
|
-
function resolveEchelon(echelon) {
|
|
1530
|
-
const key = echelon.trim().toLowerCase();
|
|
1531
|
-
if (key.length === 0) return void 0;
|
|
1532
|
-
const alias = ECHELON_ALIASES[key];
|
|
1533
|
-
return ECHELONS[key] ?? (alias ? ECHELONS[alias] : void 0);
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* Builds the echelon glyph centered at `center`, with its baseline along
|
|
1537
|
-
* `along` and `perp` (both unit vectors), `h` meters tall. Returns the glyph's
|
|
1538
|
-
* `strokes` (open polylines), `fills` (closed rings drawn as solid shapes —
|
|
1539
|
-
* the squad/section/platoon dots), and its intrinsic width along the line; the
|
|
1540
|
-
* caller adds the configurable side padding to derive the line gap.
|
|
1541
|
-
*/
|
|
1542
|
-
function buildEchelonGlyph(center, along, perp, h, spec) {
|
|
1543
|
-
const P = (a, p) => {
|
|
1544
|
-
const q = vecAdd(vecAdd(center, vecScale(along, a)), vecScale(perp, p));
|
|
1545
|
-
return unproject(q[0], q[1]);
|
|
1546
|
-
};
|
|
1547
|
-
const ring = (a, r) => {
|
|
1548
|
-
const pts = [];
|
|
1549
|
-
const steps = 16;
|
|
1550
|
-
for (let i = 0; i <= steps; i++) {
|
|
1551
|
-
const t = 2 * Math.PI * i / steps;
|
|
1552
|
-
pts.push(P(a + r * Math.cos(t), r * Math.sin(t)));
|
|
1553
|
-
}
|
|
1554
|
-
return pts;
|
|
1555
|
-
};
|
|
1556
|
-
const strokes = [];
|
|
1557
|
-
const fills = [];
|
|
1558
|
-
const half = h / 2;
|
|
1559
|
-
switch (spec.kind) {
|
|
1560
|
-
case "bars": {
|
|
1561
|
-
const spacing = h * .8;
|
|
1562
|
-
const total = (spec.count - 1) * spacing;
|
|
1563
|
-
for (let j = 0; j < spec.count; j++) {
|
|
1564
|
-
const a = -total / 2 + j * spacing;
|
|
1565
|
-
strokes.push([P(a, -half), P(a, half)]);
|
|
1566
|
-
}
|
|
1567
|
-
return {
|
|
1568
|
-
strokes,
|
|
1569
|
-
fills,
|
|
1570
|
-
width: total
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
case "crosses": {
|
|
1574
|
-
const w = h;
|
|
1575
|
-
const spacing = h * 1.4;
|
|
1576
|
-
const total = (spec.count - 1) * spacing;
|
|
1577
|
-
for (let j = 0; j < spec.count; j++) {
|
|
1578
|
-
const a0 = -total / 2 + j * spacing;
|
|
1579
|
-
strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
|
|
1580
|
-
strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
|
|
1581
|
-
}
|
|
1582
|
-
return {
|
|
1583
|
-
strokes,
|
|
1584
|
-
fills,
|
|
1585
|
-
width: total + w
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
case "dots": {
|
|
1589
|
-
const r = h * .3;
|
|
1590
|
-
const spacing = h * 1.2;
|
|
1591
|
-
const total = (spec.count - 1) * spacing;
|
|
1592
|
-
for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
|
|
1593
|
-
return {
|
|
1594
|
-
strokes,
|
|
1595
|
-
fills,
|
|
1596
|
-
width: total + 2 * r
|
|
1597
|
-
};
|
|
1598
|
-
}
|
|
1599
|
-
case "ring": {
|
|
1600
|
-
const r = h * .6;
|
|
1601
|
-
strokes.push(ring(0, r));
|
|
1602
|
-
strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
|
|
1603
|
-
return {
|
|
1604
|
-
strokes,
|
|
1605
|
-
fills,
|
|
1606
|
-
width: h * 1.6
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
case "command": {
|
|
1610
|
-
const arm = h * .5;
|
|
1611
|
-
const spacing = h * 1.4;
|
|
1612
|
-
const total = (spec.count - 1) * spacing;
|
|
1613
|
-
for (let j = 0; j < spec.count; j++) {
|
|
1614
|
-
const a0 = -total / 2 + j * spacing;
|
|
1615
|
-
strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
|
|
1616
|
-
strokes.push([P(a0, -half), P(a0, half)]);
|
|
1617
|
-
}
|
|
1618
|
-
return {
|
|
1619
|
-
strokes,
|
|
1620
|
-
fills,
|
|
1621
|
-
width: total + 2 * arm
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
//#endregion
|
|
1627
1665
|
//#region src/internal/gapped-line.ts
|
|
1628
1666
|
/**
|
|
1629
1667
|
* Pushes the arc-length gap `[g0, g1]` onto `gaps`, normalizing it onto
|
|
@@ -1752,13 +1790,81 @@ function arrowAxis(coordinates) {
|
|
|
1752
1790
|
};
|
|
1753
1791
|
}
|
|
1754
1792
|
/**
|
|
1755
|
-
* Clamps and rounds a smooth-resolution option (samples per segment) onto
|
|
1756
|
-
* `[MIN_SMOOTH_RESOLUTION, MAX_SMOOTH_RESOLUTION]`, falling back to `fallback`
|
|
1757
|
-
* for non-finite input. The default varies per measure, so it's passed in.
|
|
1793
|
+
* Clamps and rounds a smooth-resolution option (samples per segment) onto
|
|
1794
|
+
* `[MIN_SMOOTH_RESOLUTION, MAX_SMOOTH_RESOLUTION]`, falling back to `fallback`
|
|
1795
|
+
* for non-finite input. The default varies per measure, so it's passed in.
|
|
1796
|
+
*/
|
|
1797
|
+
function normalizeSmoothResolution$2(value, fallback) {
|
|
1798
|
+
if (!Number.isFinite(value)) return fallback;
|
|
1799
|
+
return Math.min(64, Math.max(2, Math.round(value)));
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Shared param descriptors for {@link SmoothLineOptions}, modeled on
|
|
1803
|
+
* `SMOOTH_PATH_PARAMS` (cm99-generic-graphics/params.ts). Measures that add
|
|
1804
|
+
* their own params should spread these at the top of the array.
|
|
1805
|
+
*/
|
|
1806
|
+
const SMOOTH_LINE_PARAMS = [{
|
|
1807
|
+
key: "smooth",
|
|
1808
|
+
label: "Smooth",
|
|
1809
|
+
description: "Round the line's corners by curving the path through the control points.",
|
|
1810
|
+
type: "boolean"
|
|
1811
|
+
}, {
|
|
1812
|
+
key: "smoothResolution",
|
|
1813
|
+
label: "Smooth resolution",
|
|
1814
|
+
description: "Number of samples per segment when smooth mode is enabled.",
|
|
1815
|
+
type: "number",
|
|
1816
|
+
min: 2,
|
|
1817
|
+
max: 64,
|
|
1818
|
+
step: 1,
|
|
1819
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1820
|
+
}];
|
|
1821
|
+
/**
|
|
1822
|
+
* Applies {@link SmoothLineOptions} to projected vertices `verts`: runs
|
|
1823
|
+
* {@link catmullRom} at the resolved (clamped) sample resolution when `smooth`
|
|
1824
|
+
* is set, else returns `verts` unchanged.
|
|
1825
|
+
*/
|
|
1826
|
+
function smoothLineVerts(verts, options, defaultResolution) {
|
|
1827
|
+
return options.smooth ? catmullRom(verts, normalizeSmoothResolution$2(options.smoothResolution ?? defaultResolution, defaultResolution)) : verts;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Resamples a path into equal arc-length segments.
|
|
1831
|
+
*
|
|
1832
|
+
* This is useful for a repeated motif (such as a fortified tooth or FLOT
|
|
1833
|
+
* scallop) after smoothing. Catmull-Rom samples are evenly spaced in spline
|
|
1834
|
+
* parameter, not distance, so using them directly as motif boundaries causes
|
|
1835
|
+
* the motifs to bunch up where the curve's samples are closer together.
|
|
1758
1836
|
*/
|
|
1759
|
-
function
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1837
|
+
function evenlySpacePath(points, targetSegmentLength) {
|
|
1838
|
+
if (points.length < 2) return points;
|
|
1839
|
+
const segmentLengths = [];
|
|
1840
|
+
let totalLength = 0;
|
|
1841
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1842
|
+
const length = vecMag(vecSub(points[i + 1], points[i]));
|
|
1843
|
+
segmentLengths.push(length);
|
|
1844
|
+
totalLength += length;
|
|
1845
|
+
}
|
|
1846
|
+
if (totalLength < 1e-6) return [points[0]];
|
|
1847
|
+
const segmentCount = Math.max(1, Math.round(totalLength / targetSegmentLength));
|
|
1848
|
+
const segmentLength = totalLength / segmentCount;
|
|
1849
|
+
const out = [points[0]];
|
|
1850
|
+
let sourceIndex = 0;
|
|
1851
|
+
let sourceStart = points[0];
|
|
1852
|
+
let consumed = 0;
|
|
1853
|
+
for (let segment = 1; segment < segmentCount; segment++) {
|
|
1854
|
+
const target = segment * segmentLength;
|
|
1855
|
+
while (sourceIndex < segmentLengths.length - 1 && consumed + segmentLengths[sourceIndex] < target) {
|
|
1856
|
+
consumed += segmentLengths[sourceIndex];
|
|
1857
|
+
sourceIndex++;
|
|
1858
|
+
sourceStart = points[sourceIndex];
|
|
1859
|
+
}
|
|
1860
|
+
const length = segmentLengths[sourceIndex];
|
|
1861
|
+
if (length < 1e-6) continue;
|
|
1862
|
+
const t = (target - consumed) / length;
|
|
1863
|
+
const sourceEnd = points[sourceIndex + 1];
|
|
1864
|
+
out.push([sourceStart[0] + (sourceEnd[0] - sourceStart[0]) * t, sourceStart[1] + (sourceEnd[1] - sourceStart[1]) * t]);
|
|
1865
|
+
}
|
|
1866
|
+
out.push(points[points.length - 1]);
|
|
1867
|
+
return out;
|
|
1762
1868
|
}
|
|
1763
1869
|
/** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
|
|
1764
1870
|
function catmullRomAt(p0, p1, p2, p3, t) {
|
|
@@ -1934,6 +2040,31 @@ function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, ec
|
|
|
1934
2040
|
gaps
|
|
1935
2041
|
};
|
|
1936
2042
|
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Factory for the `freezeOrientation` hook (ADR-0023) shared by every
|
|
2045
|
+
* echelon-carrying closed-area measure (Battle Position, Strong Point, Base
|
|
2046
|
+
* Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
|
|
2047
|
+
* fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
|
|
2048
|
+
* transform box's rotate grip) does not re-derive a different "bottom edge"
|
|
2049
|
+
* against the rotated geometry. `defaults` supplies the measure's own
|
|
2050
|
+
* `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
|
|
2051
|
+
* `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
|
|
2052
|
+
* vs. Base Camp/Guerrilla Base's `echelon: "none"`).
|
|
2053
|
+
*
|
|
2054
|
+
* The returned hook returns `undefined` — nothing to freeze — when:
|
|
2055
|
+
* - `echelonAnchor` is already set (previously frozen or user-authored), or
|
|
2056
|
+
* - the echelon resolves to none (nothing visible to pin), or
|
|
2057
|
+
* - the ring is degenerate (no segments, or a near-zero perimeter).
|
|
2058
|
+
*/
|
|
2059
|
+
function makeFreezeEchelonAnchor(defaults) {
|
|
2060
|
+
return function freezeEchelonAnchor(controlPoints, options) {
|
|
2061
|
+
if (options.echelonAnchor !== void 0) return void 0;
|
|
2062
|
+
if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
|
|
2063
|
+
const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
|
|
2064
|
+
if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
|
|
2065
|
+
return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
1937
2068
|
//#endregion
|
|
1938
2069
|
//#region src/internal/angle-utils.ts
|
|
1939
2070
|
/**
|
|
@@ -1976,13 +2107,13 @@ function labelRotationAlong(dir) {
|
|
|
1976
2107
|
*/
|
|
1977
2108
|
function pushUnitLabels(out, frame, units) {
|
|
1978
2109
|
const { center, perp, offset, rotation, labelSize } = frame;
|
|
1979
|
-
for (const { side, designator, country, textAnchor } of units) {
|
|
2110
|
+
for (const { side, designator, country, textAnchor, amplifierField } of units) {
|
|
1980
2111
|
let text;
|
|
1981
2112
|
if (designator.length > 0 && country.length > 0) text = `${designator} (${country})`;
|
|
1982
2113
|
else if (designator.length > 0) text = designator;
|
|
1983
2114
|
else if (country.length > 0) text = `(${country})`;
|
|
1984
2115
|
else continue;
|
|
1985
|
-
pushLabel(out, vecAdd(center, vecScale(perp, side * offset)), text, rotation, labelSize, textAnchor);
|
|
2116
|
+
pushLabel(out, vecAdd(center, vecScale(perp, side * offset)), text, rotation, labelSize, textAnchor, amplifierField);
|
|
1986
2117
|
}
|
|
1987
2118
|
}
|
|
1988
2119
|
/**
|
|
@@ -2095,19 +2226,20 @@ function endTangents(verts) {
|
|
|
2095
2226
|
};
|
|
2096
2227
|
}
|
|
2097
2228
|
/**
|
|
2098
|
-
* Resolves {@link LabelSizeOptions} into the
|
|
2099
|
-
*
|
|
2229
|
+
* Resolves {@link LabelSizeOptions} into the single text-size property the
|
|
2230
|
+
* label feature carries (ADR-0028): `textSizePixels` when screen-anchored
|
|
2231
|
+
* (`labelSizePixels` present — pixels win over a stray `labelSize`),
|
|
2232
|
+
* `textSizeMeters` when ground-anchored (`labelSize` alone), or neither, in
|
|
2233
|
+
* which case adapters render their engine default, screen-fixed.
|
|
2100
2234
|
*/
|
|
2101
2235
|
function labelSizeProps(options) {
|
|
2102
|
-
const {
|
|
2103
|
-
|
|
2104
|
-
if (
|
|
2105
|
-
|
|
2106
|
-
if (labelSizeResolution !== void 0) props.textSizeResolution = labelSizeResolution;
|
|
2107
|
-
return props;
|
|
2236
|
+
const { labelSize, labelSizePixels } = options;
|
|
2237
|
+
if (labelSizePixels !== void 0) return { textSizePixels: labelSizePixels };
|
|
2238
|
+
if (labelSize !== void 0) return { textSizeMeters: labelSize };
|
|
2239
|
+
return {};
|
|
2108
2240
|
}
|
|
2109
2241
|
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2110
|
-
function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
2242
|
+
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField) {
|
|
2111
2243
|
out.push({
|
|
2112
2244
|
type: "Feature",
|
|
2113
2245
|
properties: {
|
|
@@ -2115,6 +2247,7 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
|
2115
2247
|
text,
|
|
2116
2248
|
rotation,
|
|
2117
2249
|
...textAnchor ? { textAnchor } : {},
|
|
2250
|
+
...amplifierField ? { amplifierField } : {},
|
|
2118
2251
|
...sizeProps
|
|
2119
2252
|
},
|
|
2120
2253
|
geometry: {
|
|
@@ -2123,20 +2256,21 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
|
2123
2256
|
}
|
|
2124
2257
|
});
|
|
2125
2258
|
}
|
|
2126
|
-
/** Ratio of resolved label height (`labelSizePixels ×
|
|
2259
|
+
/** Ratio of resolved label height (`labelSize`, or `labelSizePixels × metersPerPixel`) used as end-label clearance. */
|
|
2127
2260
|
const END_LABEL_CLEARANCE_RATIO = .3;
|
|
2128
2261
|
/** Ratio of resolved label height used to inset "above" end labels from the tip. */
|
|
2129
2262
|
const ABOVE_LABEL_INSET_RATIO = .3;
|
|
2130
2263
|
/** Ratio of resolved label height used as the "above" end labels' perpendicular offset. */
|
|
2131
2264
|
const ABOVE_LABEL_OFFSET_RATIO = .6;
|
|
2132
2265
|
function textHeight(options) {
|
|
2133
|
-
const { labelSizePixels,
|
|
2134
|
-
if (labelSizePixels !== void 0
|
|
2266
|
+
const { labelSize, labelSizePixels, metersPerPixel } = options;
|
|
2267
|
+
if (labelSizePixels !== void 0) return metersPerPixel !== void 0 && metersPerPixel > 0 ? labelSizePixels * metersPerPixel : void 0;
|
|
2268
|
+
return labelSize;
|
|
2135
2269
|
}
|
|
2136
2270
|
/**
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
* size
|
|
2271
|
+
* Label offset in meters: `ratio` × the label's resolved height (`labelSize`
|
|
2272
|
+
* meters, or `labelSizePixels × metersPerPixel` when screen-anchored), or
|
|
2273
|
+
* `fallbackMeters` when no size resolves (e.g. no map resolution yet). The fallback is small
|
|
2140
2274
|
* relative to typical control measure spans — the same order of magnitude as
|
|
2141
2275
|
* Boundary's own smallest label offsets — so a fixed label still reads as "just
|
|
2142
2276
|
* clear of the line" rather than crowding it or floating away. Callers supply
|
|
@@ -2230,16 +2364,20 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
|
|
|
2230
2364
|
* as a complete FeatureCollection.
|
|
2231
2365
|
*/
|
|
2232
2366
|
function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
2233
|
-
const { part, placement = "beyond", phaseLineName, includePrefix = true, ...sizeOptions } = options;
|
|
2367
|
+
const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
2368
|
+
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
2369
|
+
smooth,
|
|
2370
|
+
smoothResolution
|
|
2371
|
+
}, 12);
|
|
2372
|
+
const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
|
|
2234
2373
|
const features = [{
|
|
2235
2374
|
type: "Feature",
|
|
2236
2375
|
properties: { part },
|
|
2237
2376
|
geometry: {
|
|
2238
2377
|
type: "LineString",
|
|
2239
|
-
coordinates:
|
|
2378
|
+
coordinates: lineCoordinates
|
|
2240
2379
|
}
|
|
2241
2380
|
}];
|
|
2242
|
-
const verts = positions.map((p) => project(p[0], p[1]));
|
|
2243
2381
|
const labelFeatures = [];
|
|
2244
2382
|
if (placement === "above") pushEndLabelsAbove(labelFeatures, verts, textForEnd, sizeOptions);
|
|
2245
2383
|
else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
|
|
@@ -2254,7 +2392,9 @@ function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
|
2254
2392
|
//#endregion
|
|
2255
2393
|
//#region src/internal/area-labels.ts
|
|
2256
2394
|
/** Stored label rotation for horizontal (screen-upright) area text — adapters
|
|
2257
|
-
* render feature rotation as `π − rotation`, so `π` renders as `0`.
|
|
2395
|
+
* render feature rotation as `π − rotation`, so `π` renders as `0`. Exported
|
|
2396
|
+
* for reuse by measures that place a horizontal label outside the area's
|
|
2397
|
+
* boundary (e.g. Airfield Zone's Field H). */
|
|
2258
2398
|
const HORIZONTAL_LABEL_ROTATION = Math.PI;
|
|
2259
2399
|
/** Row pitch, as a ratio of the resolved label height, between stacked
|
|
2260
2400
|
* name/H/W-W1 rows (see {@link pushAreaLabels}). */
|
|
@@ -2268,7 +2408,7 @@ const ENY_CLEARANCE_RATIO = .3;
|
|
|
2268
2408
|
* for sizing the boundary knockout gap — no live text-measurement is
|
|
2269
2409
|
* available at generator time. */
|
|
2270
2410
|
const ENY_CHAR_ASPECT_RATIO = .6;
|
|
2271
|
-
const DEFAULT_SMOOTH_RESOLUTION$
|
|
2411
|
+
const DEFAULT_SMOOTH_RESOLUTION$9 = 16;
|
|
2272
2412
|
/**
|
|
2273
2413
|
* Composes an area's main label text from a `prefix` (e.g. `"AO"`) and a
|
|
2274
2414
|
* `designation` (e.g. `"GREEN"`), mirroring Phase Line's "PL" precedent: a
|
|
@@ -2305,7 +2445,8 @@ function composeAreaLabelTexts(textAmplifiers, prefix = "", separator = " ") {
|
|
|
2305
2445
|
* divide by zero). For a **concave** ring the area centroid can fall outside
|
|
2306
2446
|
* the shape; that limitation is accepted here, matching the doctrinal
|
|
2307
2447
|
* template's plain "center of the area" placement rather than a
|
|
2308
|
-
* point-in-polygon-safe visual center.
|
|
2448
|
+
* point-in-polygon-safe visual center. Exported for reuse by measures that
|
|
2449
|
+
* center a non-label glyph on the area (e.g. Airfield Zone's runway glyph).
|
|
2309
2450
|
*/
|
|
2310
2451
|
function ringCentroid(verts) {
|
|
2311
2452
|
const n = verts.length - 1;
|
|
@@ -2331,12 +2472,6 @@ function ringCentroid(verts) {
|
|
|
2331
2472
|
const factor = 1 / (3 * area2);
|
|
2332
2473
|
return [cx * factor, cy * factor];
|
|
2333
2474
|
}
|
|
2334
|
-
/** Joins the W/W1 date-time-group pair as `"W - W1"`; a single supplied value
|
|
2335
|
-
* passes through unchanged; neither set yields `""`. */
|
|
2336
|
-
function joinDtg(start, end) {
|
|
2337
|
-
if (start && end) return `${start} - ${end}`;
|
|
2338
|
-
return start ?? end ?? "";
|
|
2339
|
-
}
|
|
2340
2475
|
/**
|
|
2341
2476
|
* Points where the closed ring `verts` crosses the horizontal line
|
|
2342
2477
|
* `y = target`, one per crossing edge, plus the ring's total arc length —
|
|
@@ -2380,7 +2515,7 @@ function ringCrossingsAtY(verts, target) {
|
|
|
2380
2515
|
* {@link buildGappedLine} uses) an "ENY" marker knocks out of the boundary —
|
|
2381
2516
|
* empty unless `texts.hostile` was placed.
|
|
2382
2517
|
*
|
|
2383
|
-
* Rows are `[name, additionalInformation,
|
|
2518
|
+
* Rows are `[name (T), additionalInformation (H), dtgStart (W), dtgEnd (W1)]`,
|
|
2384
2519
|
* filtered to non-empty text, stacked around the ring's {@link ringCentroid}
|
|
2385
2520
|
* at `pitch = resolveLabelOffsetMeters(options, 1.2 + labelPadding)` (the 50m
|
|
2386
2521
|
* no-size fallback applies until a live label size is captured — see
|
|
@@ -2414,14 +2549,18 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
2414
2549
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
2415
2550
|
const [cx, cy] = ringCentroid(ringVerts);
|
|
2416
2551
|
const rows = [
|
|
2417
|
-
texts.name,
|
|
2418
|
-
texts.additionalInformation,
|
|
2419
|
-
|
|
2420
|
-
|
|
2552
|
+
[texts.name, "T"],
|
|
2553
|
+
[texts.additionalInformation, "H"],
|
|
2554
|
+
[texts.dtgStart, "W"],
|
|
2555
|
+
[texts.dtgEnd, "W1"]
|
|
2556
|
+
].flatMap(([text, field]) => text ? [{
|
|
2557
|
+
text,
|
|
2558
|
+
field
|
|
2559
|
+
}] : []);
|
|
2421
2560
|
if (rows.length > 0) {
|
|
2422
2561
|
const pitch = resolveLabelOffsetMeters(options, ROW_PITCH_RATIO + padding);
|
|
2423
|
-
rows.forEach((text, i) => {
|
|
2424
|
-
pushLabel(out, [cx, cy + ((rows.length - 1) / 2 - i) * pitch], text, HORIZONTAL_LABEL_ROTATION, sizeProps);
|
|
2562
|
+
rows.forEach(({ text, field }, i) => {
|
|
2563
|
+
pushLabel(out, [cx, cy + ((rows.length - 1) / 2 - i) * pitch], text, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, field);
|
|
2425
2564
|
});
|
|
2426
2565
|
}
|
|
2427
2566
|
const gaps = [];
|
|
@@ -2435,7 +2574,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
2435
2574
|
const textWidth = texts.hostile.length * ENY_CHAR_ASPECT_RATIO * textHeight;
|
|
2436
2575
|
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight;
|
|
2437
2576
|
for (const crossing of [west, east]) {
|
|
2438
|
-
pushLabel(out, [crossing.x, cy], texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps);
|
|
2577
|
+
pushLabel(out, [crossing.x, cy], texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, "N");
|
|
2439
2578
|
const halfExtent = .5 * (textWidth * Math.abs(crossing.along[0]) + textHeight * Math.abs(crossing.along[1])) + clearance;
|
|
2440
2579
|
pushWrappedGap(gaps, crossing.arc - halfExtent, crossing.arc + halfExtent, totalArc);
|
|
2441
2580
|
}
|
|
@@ -2479,8 +2618,8 @@ function boundaryFeature(verts, gaps) {
|
|
|
2479
2618
|
* common (no `N`) case is unaffected.
|
|
2480
2619
|
*/
|
|
2481
2620
|
function labeledAreaFeatures(positions, options, texts) {
|
|
2482
|
-
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$
|
|
2483
|
-
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$
|
|
2621
|
+
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
2622
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
2484
2623
|
const labelFeatures = [];
|
|
2485
2624
|
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
|
|
2486
2625
|
features.push(...labelFeatures);
|
|
@@ -2490,44 +2629,492 @@ function labeledAreaFeatures(positions, options, texts) {
|
|
|
2490
2629
|
};
|
|
2491
2630
|
}
|
|
2492
2631
|
//#endregion
|
|
2493
|
-
//#region src/generators/
|
|
2494
|
-
/** Default options for the Area
|
|
2495
|
-
const
|
|
2496
|
-
smooth: false,
|
|
2497
|
-
smoothResolution: 16
|
|
2632
|
+
//#region src/generators/cm15-maneuver-areas/area.ts
|
|
2633
|
+
/** Default options for the Area control measure. */
|
|
2634
|
+
const DEFAULT_AREA_OPTIONS = {
|
|
2635
|
+
smooth: false,
|
|
2636
|
+
smoothResolution: 16
|
|
2637
|
+
};
|
|
2638
|
+
const AREA_METADATA = {
|
|
2639
|
+
id: "area",
|
|
2640
|
+
name: "Area",
|
|
2641
|
+
description: "A generic maneuver area enclosed by a boundary.",
|
|
2642
|
+
entity: "Maneuver Areas",
|
|
2643
|
+
entityType: "Area",
|
|
2644
|
+
value: "150100",
|
|
2645
|
+
minCoordinates: 3,
|
|
2646
|
+
geometry: "area",
|
|
2647
|
+
geometryTypes: [
|
|
2648
|
+
"Polygon",
|
|
2649
|
+
"MultiLineString",
|
|
2650
|
+
"Point"
|
|
2651
|
+
],
|
|
2652
|
+
drawRule: "Area1",
|
|
2653
|
+
capturesLabelSize: true,
|
|
2654
|
+
params: LABELED_AREA_PARAMS,
|
|
2655
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
2656
|
+
};
|
|
2657
|
+
/**
|
|
2658
|
+
* Creates an Area control measure: a closed area through `positions` labeled
|
|
2659
|
+
* at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
|
|
2660
|
+
* marker, per {@link labeledAreaFeatures}.
|
|
2661
|
+
*/
|
|
2662
|
+
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
2663
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
2664
|
+
}
|
|
2665
|
+
const AREA = defineControlMeasure({
|
|
2666
|
+
metadata: AREA_METADATA,
|
|
2667
|
+
generator: createArea,
|
|
2668
|
+
defaultOptions: DEFAULT_AREA_OPTIONS,
|
|
2669
|
+
rule: area1DrawRule,
|
|
2670
|
+
previewSample: {
|
|
2671
|
+
controlPoints: [
|
|
2672
|
+
[-1, -.5],
|
|
2673
|
+
[1, -.5],
|
|
2674
|
+
[1, .5],
|
|
2675
|
+
[-1, .5]
|
|
2676
|
+
],
|
|
2677
|
+
textAmplifiers: {},
|
|
2678
|
+
options: { labelSize: 96 }
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
//#endregion
|
|
2682
|
+
//#region src/generators/cm12-command-control-areas/area-of-operations.ts
|
|
2683
|
+
/** Default options for the Area of operations control measure. */
|
|
2684
|
+
const DEFAULT_AREA_OF_OPERATIONS_OPTIONS = {
|
|
2685
|
+
smooth: false,
|
|
2686
|
+
smoothResolution: 16
|
|
2687
|
+
};
|
|
2688
|
+
const AREA_OF_OPERATIONS_METADATA = {
|
|
2689
|
+
id: "area-of-operations",
|
|
2690
|
+
name: "Area of operations",
|
|
2691
|
+
description: "An operational area defined by a commander for land and naval forces, within which the assigned force conducts military operations.",
|
|
2692
|
+
entity: "Command and Control Areas",
|
|
2693
|
+
entityType: "Area of Operations",
|
|
2694
|
+
value: "120100",
|
|
2695
|
+
minCoordinates: 3,
|
|
2696
|
+
geometry: "area",
|
|
2697
|
+
geometryTypes: [
|
|
2698
|
+
"Polygon",
|
|
2699
|
+
"MultiLineString",
|
|
2700
|
+
"Point"
|
|
2701
|
+
],
|
|
2702
|
+
drawRule: "Area1",
|
|
2703
|
+
capturesLabelSize: true,
|
|
2704
|
+
params: LABELED_AREA_PARAMS,
|
|
2705
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2706
|
+
};
|
|
2707
|
+
/**
|
|
2708
|
+
* Creates an Area of operations control measure: a closed area through
|
|
2709
|
+
* `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
|
|
2710
|
+
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
2711
|
+
*/
|
|
2712
|
+
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
|
|
2713
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
|
|
2714
|
+
}
|
|
2715
|
+
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
2716
|
+
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
2717
|
+
generator: createAreaOfOperations,
|
|
2718
|
+
defaultOptions: DEFAULT_AREA_OF_OPERATIONS_OPTIONS,
|
|
2719
|
+
rule: area1DrawRule,
|
|
2720
|
+
previewSample: {
|
|
2721
|
+
controlPoints: [
|
|
2722
|
+
[-1, -.5],
|
|
2723
|
+
[1, -.5],
|
|
2724
|
+
[1, .5],
|
|
2725
|
+
[-1, .5]
|
|
2726
|
+
],
|
|
2727
|
+
textAmplifiers: { T: "GREEN" },
|
|
2728
|
+
options: { labelSize: 96 }
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/generators/cm12-command-control-areas/params.ts
|
|
2733
|
+
const SMOOTH_AREA_PARAMS = AREA_SMOOTH_PARAMS;
|
|
2734
|
+
const AREA_DESIGNATION_ONLY_AMPLIFIER = {
|
|
2735
|
+
key: "T",
|
|
2736
|
+
label: "Unique designation",
|
|
2737
|
+
description: "Field T — the area's unique designation.",
|
|
2738
|
+
maxLength: 21
|
|
2739
|
+
};
|
|
2740
|
+
//#endregion
|
|
2741
|
+
//#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
|
|
2742
|
+
/**
|
|
2743
|
+
* Default options for the Named area of interest control measure. The
|
|
2744
|
+
* standard's example renders a straight-sided polygon, so `smooth` defaults
|
|
2745
|
+
* off (unlike the rounded L-15 templates) while staying available.
|
|
2746
|
+
*/
|
|
2747
|
+
const DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS = {
|
|
2748
|
+
smooth: false,
|
|
2749
|
+
smoothResolution: 16
|
|
2750
|
+
};
|
|
2751
|
+
const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
2752
|
+
id: "named-area-of-interest",
|
|
2753
|
+
name: "Named area of interest",
|
|
2754
|
+
description: "A geographical area where information is gathered to satisfy specific intelligence requirements.",
|
|
2755
|
+
entity: "Command and Control Areas",
|
|
2756
|
+
entityType: "Named Area of Interest",
|
|
2757
|
+
value: "120200",
|
|
2758
|
+
minCoordinates: 3,
|
|
2759
|
+
geometry: "area",
|
|
2760
|
+
geometryTypes: ["Polygon", "Point"],
|
|
2761
|
+
drawRule: "Area1",
|
|
2762
|
+
capturesLabelSize: true,
|
|
2763
|
+
params: SMOOTH_AREA_PARAMS,
|
|
2764
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
2765
|
+
};
|
|
2766
|
+
/**
|
|
2767
|
+
* Creates a Named area of interest control measure: a closed area through
|
|
2768
|
+
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T) —
|
|
2769
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
2770
|
+
*/
|
|
2771
|
+
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
2772
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("NAI", textAmplifiers.T) });
|
|
2773
|
+
}
|
|
2774
|
+
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
2775
|
+
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
2776
|
+
generator: createNamedAreaOfInterest,
|
|
2777
|
+
defaultOptions: DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS,
|
|
2778
|
+
rule: area1DrawRule,
|
|
2779
|
+
previewSample: {
|
|
2780
|
+
controlPoints: [
|
|
2781
|
+
[-1, -.5],
|
|
2782
|
+
[1, -.5],
|
|
2783
|
+
[1, .5],
|
|
2784
|
+
[-1, .5]
|
|
2785
|
+
],
|
|
2786
|
+
textAmplifiers: { T: "1" },
|
|
2787
|
+
options: { labelSize: 96 }
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region src/generators/cm12-command-control-areas/target-area-of-interest.ts
|
|
2792
|
+
/**
|
|
2793
|
+
* Default options for the Target area of interest control measure. The
|
|
2794
|
+
* standard's example renders a straight-sided polygon, so `smooth` defaults
|
|
2795
|
+
* off (unlike the rounded L-15 templates) while staying available.
|
|
2796
|
+
*/
|
|
2797
|
+
const DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS = {
|
|
2798
|
+
smooth: false,
|
|
2799
|
+
smoothResolution: 16
|
|
2800
|
+
};
|
|
2801
|
+
const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
2802
|
+
id: "target-area-of-interest",
|
|
2803
|
+
name: "Target area of interest",
|
|
2804
|
+
description: "The geographical area where high-value targets can be acquired and engaged by friendly forces.",
|
|
2805
|
+
entity: "Command and Control Areas",
|
|
2806
|
+
entityType: "Target Area of Interest",
|
|
2807
|
+
value: "120300",
|
|
2808
|
+
minCoordinates: 3,
|
|
2809
|
+
geometry: "area",
|
|
2810
|
+
geometryTypes: ["Polygon", "Point"],
|
|
2811
|
+
drawRule: "Area1",
|
|
2812
|
+
capturesLabelSize: true,
|
|
2813
|
+
params: SMOOTH_AREA_PARAMS,
|
|
2814
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
2815
|
+
};
|
|
2816
|
+
/**
|
|
2817
|
+
* Creates a Target area of interest control measure: a closed area through
|
|
2818
|
+
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T) —
|
|
2819
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
2820
|
+
*/
|
|
2821
|
+
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
2822
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("TAI", textAmplifiers.T) });
|
|
2823
|
+
}
|
|
2824
|
+
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
2825
|
+
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
2826
|
+
generator: createTargetAreaOfInterest,
|
|
2827
|
+
defaultOptions: DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS,
|
|
2828
|
+
rule: area1DrawRule,
|
|
2829
|
+
previewSample: {
|
|
2830
|
+
controlPoints: [
|
|
2831
|
+
[-1, -.5],
|
|
2832
|
+
[1, -.5],
|
|
2833
|
+
[1, .5],
|
|
2834
|
+
[-1, .5]
|
|
2835
|
+
],
|
|
2836
|
+
textAmplifiers: { T: "YUKON" },
|
|
2837
|
+
options: { labelSize: 96 }
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
//#endregion
|
|
2841
|
+
//#region src/generators/cm12-command-control-areas/airfield-zone.ts
|
|
2842
|
+
const DEFAULT_SMOOTH_RESOLUTION$8 = 16;
|
|
2843
|
+
/** Long runway line's tilt off the horizontal, in radians (+15°). */
|
|
2844
|
+
const LONG_LINE_ANGLE = 15 * Math.PI / 180;
|
|
2845
|
+
/** Short runway line's tilt off the horizontal, in radians (+50°). */
|
|
2846
|
+
const SHORT_LINE_ANGLE = 50 * Math.PI / 180;
|
|
2847
|
+
/** Short line's half-length as a ratio of the long line's half-length. */
|
|
2848
|
+
const SHORT_LINE_RATIO = .55;
|
|
2849
|
+
/** Field H's offset east of the area's max-x edge, as a ratio of the resolved label offset. */
|
|
2850
|
+
const H_LABEL_OFFSET_RATIO = .75;
|
|
2851
|
+
/**
|
|
2852
|
+
* Default options for the Airfield zone control measure. The standard's
|
|
2853
|
+
* example is a rounded blob, so `smooth` defaults on.
|
|
2854
|
+
*/
|
|
2855
|
+
const DEFAULT_AIRFIELD_ZONE_OPTIONS = {
|
|
2856
|
+
smooth: true,
|
|
2857
|
+
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$8,
|
|
2858
|
+
glyphSizeRatio: .55
|
|
2859
|
+
};
|
|
2860
|
+
const AIRFIELD_ZONE_METADATA = {
|
|
2861
|
+
id: "airfield-zone",
|
|
2862
|
+
name: "Airfield zone",
|
|
2863
|
+
description: "A zone enclosing an airfield, marked with a runway glyph; Field H carries the airfield type, runway length and other pertinent information.",
|
|
2864
|
+
entity: "Command and Control Areas",
|
|
2865
|
+
entityType: "Airfield Zone",
|
|
2866
|
+
value: "120400",
|
|
2867
|
+
minCoordinates: 3,
|
|
2868
|
+
geometry: "area",
|
|
2869
|
+
geometryTypes: [
|
|
2870
|
+
"Polygon",
|
|
2871
|
+
"MultiLineString",
|
|
2872
|
+
"Point"
|
|
2873
|
+
],
|
|
2874
|
+
drawRule: "Area1",
|
|
2875
|
+
capturesLabelSize: true,
|
|
2876
|
+
params: [...SMOOTH_AREA_PARAMS, {
|
|
2877
|
+
key: "glyphSizeRatio",
|
|
2878
|
+
label: "Runway size",
|
|
2879
|
+
description: "Size of the runway glyph as a ratio of the area's width.",
|
|
2880
|
+
type: "number",
|
|
2881
|
+
min: .2,
|
|
2882
|
+
max: .9,
|
|
2883
|
+
step: .05
|
|
2884
|
+
}],
|
|
2885
|
+
textAmplifiers: [{
|
|
2886
|
+
key: "H",
|
|
2887
|
+
label: "Additional information",
|
|
2888
|
+
description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
|
|
2889
|
+
maxLength: 20
|
|
2890
|
+
}]
|
|
2891
|
+
};
|
|
2892
|
+
/**
|
|
2893
|
+
* Creates an Airfield zone control measure: a closed area through
|
|
2894
|
+
* `positions` with a runway glyph (two crossed lines forming a shallow
|
|
2895
|
+
* asymmetric X) centered on the ring, and Field H placed outside the area to
|
|
2896
|
+
* the east when set.
|
|
2897
|
+
*
|
|
2898
|
+
* @param positions - Area anchor points (≥3); the input contract is enforced
|
|
2899
|
+
* at the render seam (ADR-0014).
|
|
2900
|
+
* @param options - {@link AirfieldZoneOptions}.
|
|
2901
|
+
* @param textAmplifiers - Field H only.
|
|
2902
|
+
*/
|
|
2903
|
+
function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
2904
|
+
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
2905
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
2906
|
+
const features = [boundaryFeature(verts, [])];
|
|
2907
|
+
let minX = Infinity;
|
|
2908
|
+
let maxX = -Infinity;
|
|
2909
|
+
for (const [x] of verts) {
|
|
2910
|
+
if (x < minX) minX = x;
|
|
2911
|
+
if (x > maxX) maxX = x;
|
|
2912
|
+
}
|
|
2913
|
+
const bboxWidth = maxX - minX;
|
|
2914
|
+
const [cx, cy] = ringCentroid(verts);
|
|
2915
|
+
if (bboxWidth >= 1e-6) {
|
|
2916
|
+
const longHalf = .5 * glyphSizeRatio * bboxWidth;
|
|
2917
|
+
const shortHalf = longHalf * SHORT_LINE_RATIO;
|
|
2918
|
+
const center = [cx, cy];
|
|
2919
|
+
const longDir = [Math.cos(LONG_LINE_ANGLE), Math.sin(LONG_LINE_ANGLE)];
|
|
2920
|
+
const shortDir = [Math.cos(SHORT_LINE_ANGLE), Math.sin(SHORT_LINE_ANGLE)];
|
|
2921
|
+
const along = (dir, half) => [[center[0] - dir[0] * half, center[1] - dir[1] * half], [center[0] + dir[0] * half, center[1] + dir[1] * half]];
|
|
2922
|
+
const [longA, longB] = along(longDir, longHalf);
|
|
2923
|
+
const [shortA, shortB] = along(shortDir, shortHalf);
|
|
2924
|
+
features.push({
|
|
2925
|
+
type: "Feature",
|
|
2926
|
+
properties: {},
|
|
2927
|
+
geometry: {
|
|
2928
|
+
type: "MultiLineString",
|
|
2929
|
+
coordinates: [[unproject(longA[0], longA[1]), unproject(longB[0], longB[1])], [unproject(shortA[0], shortA[1]), unproject(shortB[0], shortB[1])]]
|
|
2930
|
+
}
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
if (textAmplifiers.H) {
|
|
2934
|
+
const labelFeatures = [];
|
|
2935
|
+
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start", "H");
|
|
2936
|
+
features.push(...labelFeatures);
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
type: "FeatureCollection",
|
|
2940
|
+
features
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
const AIRFIELD_ZONE = defineControlMeasure({
|
|
2944
|
+
metadata: AIRFIELD_ZONE_METADATA,
|
|
2945
|
+
generator: createAirfieldZone,
|
|
2946
|
+
defaultOptions: DEFAULT_AIRFIELD_ZONE_OPTIONS,
|
|
2947
|
+
rule: area1DrawRule,
|
|
2948
|
+
previewSample: {
|
|
2949
|
+
controlPoints: [
|
|
2950
|
+
[-1, -.5],
|
|
2951
|
+
[1, -.5],
|
|
2952
|
+
[1, .5],
|
|
2953
|
+
[-1, .5]
|
|
2954
|
+
],
|
|
2955
|
+
textAmplifiers: { H: "570M" },
|
|
2956
|
+
options: { labelSize: 96 }
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
//#endregion
|
|
2960
|
+
//#region src/generators/cm12-command-control-areas/echelon-labeled-area.ts
|
|
2961
|
+
const DEFAULT_SMOOTH_RESOLUTION$7 = 16;
|
|
2962
|
+
/** Default options shared by Base Camp and Guerrilla Base — echelon off, boundary rounded. */
|
|
2963
|
+
const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
2964
|
+
echelon: "none",
|
|
2965
|
+
echelonSize: 750,
|
|
2966
|
+
echelonSizePixels: 16,
|
|
2967
|
+
echelonPadding: .3,
|
|
2968
|
+
echelonPosition: 0,
|
|
2969
|
+
smooth: true,
|
|
2970
|
+
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$7
|
|
2971
|
+
};
|
|
2972
|
+
/**
|
|
2973
|
+
* Builds the shared Base Camp / Guerrilla Base geometry: a closed area
|
|
2974
|
+
* through `positions` carrying the full L-15 text amplifier template
|
|
2975
|
+
* ({@link pushAreaLabels}) plus an optional Field B echelon glyph straddling
|
|
2976
|
+
* the boundary ({@link placeEchelon}, mirroring Battle Position). The
|
|
2977
|
+
* echelon's masking gap and any Field N "ENY" masking gap are concatenated
|
|
2978
|
+
* before carving the boundary, so either (or both) opens it into a gapped
|
|
2979
|
+
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
2980
|
+
*/
|
|
2981
|
+
function echelonLabeledAreaFeatures(positions, options, texts) {
|
|
2982
|
+
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;
|
|
2983
|
+
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
2984
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
2985
|
+
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
2986
|
+
const labelFeatures = [];
|
|
2987
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
|
|
2988
|
+
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
2989
|
+
if (strokes.length > 0) features.push({
|
|
2990
|
+
type: "Feature",
|
|
2991
|
+
properties: { part: "echelon" },
|
|
2992
|
+
geometry: {
|
|
2993
|
+
type: "MultiLineString",
|
|
2994
|
+
coordinates: strokes
|
|
2995
|
+
}
|
|
2996
|
+
});
|
|
2997
|
+
if (fills.length > 0) features.push({
|
|
2998
|
+
type: "Feature",
|
|
2999
|
+
properties: {
|
|
3000
|
+
part: "echelon",
|
|
3001
|
+
fill: true
|
|
3002
|
+
},
|
|
3003
|
+
geometry: {
|
|
3004
|
+
type: "MultiPolygon",
|
|
3005
|
+
coordinates: fills.map((ring) => [ring])
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
features.push(...labelFeatures);
|
|
3009
|
+
return {
|
|
3010
|
+
type: "FeatureCollection",
|
|
3011
|
+
features
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* `freezeOrientation` hook factory instance shared by Base Camp and
|
|
3016
|
+
* Guerrilla Base — see {@link makeFreezeEchelonAnchor} (ADR-0023) for the
|
|
3017
|
+
* mechanism; both measures default to `echelon: "none"`, so unlike Battle
|
|
3018
|
+
* Position's own instance (battlePosition.ts) freezing is a no-op until a
|
|
3019
|
+
* user explicitly picks an echelon.
|
|
3020
|
+
*/
|
|
3021
|
+
const freezeEchelonLabeledAreaAnchor = makeFreezeEchelonAnchor({
|
|
3022
|
+
echelon: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon,
|
|
3023
|
+
smooth: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth,
|
|
3024
|
+
smoothResolution: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution
|
|
3025
|
+
});
|
|
3026
|
+
//#endregion
|
|
3027
|
+
//#region src/generators/cm12-command-control-areas/base-camp.ts
|
|
3028
|
+
/** Default options for the Base camp control measure. */
|
|
3029
|
+
const DEFAULT_BASE_CAMP_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
3030
|
+
const BASE_CAMP_METADATA = {
|
|
3031
|
+
id: "base-camp",
|
|
3032
|
+
name: "Base camp",
|
|
3033
|
+
description: "An evolving military facility that supports military operations of a deployed unit and provides the necessary support and services for sustained operations.",
|
|
3034
|
+
entity: "Command and Control Areas",
|
|
3035
|
+
entityType: "Base Camp",
|
|
3036
|
+
value: "120500",
|
|
3037
|
+
minCoordinates: 3,
|
|
3038
|
+
geometry: "area",
|
|
3039
|
+
geometryTypes: [
|
|
3040
|
+
"Polygon",
|
|
3041
|
+
"MultiLineString",
|
|
3042
|
+
"MultiPolygon",
|
|
3043
|
+
"Point"
|
|
3044
|
+
],
|
|
3045
|
+
drawRule: "Area1",
|
|
3046
|
+
capturesLabelSize: true,
|
|
3047
|
+
params: ECHELON_AREA_PARAMS,
|
|
3048
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2498
3049
|
};
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3050
|
+
/**
|
|
3051
|
+
* Creates a Base camp control measure: a closed area through `positions`
|
|
3052
|
+
* labeled at its center with "BC "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
3053
|
+
* amplifier rows, and an optional Field B echelon glyph straddling the
|
|
3054
|
+
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3055
|
+
*/
|
|
3056
|
+
function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
|
|
3057
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
|
|
3058
|
+
}
|
|
3059
|
+
const BASE_CAMP = defineControlMeasure({
|
|
3060
|
+
metadata: BASE_CAMP_METADATA,
|
|
3061
|
+
generator: createBaseCamp,
|
|
3062
|
+
defaultOptions: DEFAULT_BASE_CAMP_OPTIONS,
|
|
3063
|
+
rule: area1DrawRule,
|
|
3064
|
+
freezeOrientation: freezeEchelonLabeledAreaAnchor,
|
|
3065
|
+
previewSample: {
|
|
3066
|
+
controlPoints: [
|
|
3067
|
+
[-1, -.5],
|
|
3068
|
+
[1, -.5],
|
|
3069
|
+
[1, .5],
|
|
3070
|
+
[-1, .5]
|
|
3071
|
+
],
|
|
3072
|
+
textAmplifiers: { T: "LEATHERNECK" },
|
|
3073
|
+
options: {
|
|
3074
|
+
smooth: true,
|
|
3075
|
+
labelSize: 96
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
//#endregion
|
|
3080
|
+
//#region src/generators/cm12-command-control-areas/guerrilla-base.ts
|
|
3081
|
+
/** Default options for the Guerrilla base control measure. */
|
|
3082
|
+
const DEFAULT_GUERRILLA_BASE_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
3083
|
+
const GUERRILLA_BASE_METADATA = {
|
|
3084
|
+
id: "guerrilla-base",
|
|
3085
|
+
name: "Guerrilla base",
|
|
3086
|
+
description: "A temporary site where guerrilla installations, headquarters, and units are located.",
|
|
2503
3087
|
entity: "Command and Control Areas",
|
|
2504
|
-
entityType: "
|
|
2505
|
-
value: "
|
|
3088
|
+
entityType: "Guerrilla Base",
|
|
3089
|
+
value: "120600",
|
|
2506
3090
|
minCoordinates: 3,
|
|
2507
3091
|
geometry: "area",
|
|
2508
3092
|
geometryTypes: [
|
|
2509
3093
|
"Polygon",
|
|
2510
3094
|
"MultiLineString",
|
|
3095
|
+
"MultiPolygon",
|
|
2511
3096
|
"Point"
|
|
2512
3097
|
],
|
|
2513
3098
|
drawRule: "Area1",
|
|
2514
3099
|
capturesLabelSize: true,
|
|
2515
|
-
params:
|
|
3100
|
+
params: ECHELON_AREA_PARAMS,
|
|
2516
3101
|
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2517
3102
|
};
|
|
2518
3103
|
/**
|
|
2519
|
-
* Creates
|
|
2520
|
-
* `positions` labeled at its center with "
|
|
2521
|
-
* `H`/`W`/`W1`/`N` amplifier rows,
|
|
3104
|
+
* Creates a Guerrilla base control measure: a closed area through
|
|
3105
|
+
* `positions` labeled at its center with "GB "-prefixed `T` (Field T) plus
|
|
3106
|
+
* `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
|
|
3107
|
+
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
2522
3108
|
*/
|
|
2523
|
-
function
|
|
2524
|
-
return
|
|
3109
|
+
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
|
|
3110
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
|
|
2525
3111
|
}
|
|
2526
|
-
const
|
|
2527
|
-
metadata:
|
|
2528
|
-
generator:
|
|
2529
|
-
defaultOptions:
|
|
3112
|
+
const GUERRILLA_BASE = defineControlMeasure({
|
|
3113
|
+
metadata: GUERRILLA_BASE_METADATA,
|
|
3114
|
+
generator: createGuerrillaBase,
|
|
3115
|
+
defaultOptions: DEFAULT_GUERRILLA_BASE_OPTIONS,
|
|
2530
3116
|
rule: area1DrawRule,
|
|
3117
|
+
freezeOrientation: freezeEchelonLabeledAreaAnchor,
|
|
2531
3118
|
previewSample: {
|
|
2532
3119
|
controlPoints: [
|
|
2533
3120
|
[-1, -.5],
|
|
@@ -2535,14 +3122,67 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
|
2535
3122
|
[1, .5],
|
|
2536
3123
|
[-1, .5]
|
|
2537
3124
|
],
|
|
2538
|
-
textAmplifiers: { T: "
|
|
3125
|
+
textAmplifiers: { T: "GUEVARA" },
|
|
2539
3126
|
options: {
|
|
2540
|
-
|
|
2541
|
-
|
|
3127
|
+
smooth: true,
|
|
3128
|
+
labelSize: 96
|
|
2542
3129
|
}
|
|
2543
3130
|
}
|
|
2544
3131
|
});
|
|
2545
3132
|
//#endregion
|
|
3133
|
+
//#region src/generators/cm12-command-control-areas/generic-c2-area.ts
|
|
3134
|
+
/**
|
|
3135
|
+
* Default options for the Generic command and control area control measure.
|
|
3136
|
+
* The standard's example is a rounded blob, so `smooth` defaults on.
|
|
3137
|
+
*/
|
|
3138
|
+
const DEFAULT_GENERIC_C2_AREA_OPTIONS = {
|
|
3139
|
+
smooth: true,
|
|
3140
|
+
smoothResolution: 16
|
|
3141
|
+
};
|
|
3142
|
+
const GENERIC_C2_AREA_METADATA = {
|
|
3143
|
+
id: "generic-c2-area",
|
|
3144
|
+
name: "Generic command and control area",
|
|
3145
|
+
description: "A general-purpose command and control area carrying a designation, additional information and an effective date-time-group window.",
|
|
3146
|
+
entity: "Command and Control Areas",
|
|
3147
|
+
entityType: "Generic",
|
|
3148
|
+
value: "120700",
|
|
3149
|
+
minCoordinates: 3,
|
|
3150
|
+
geometry: "area",
|
|
3151
|
+
geometryTypes: [
|
|
3152
|
+
"Polygon",
|
|
3153
|
+
"MultiLineString",
|
|
3154
|
+
"Point"
|
|
3155
|
+
],
|
|
3156
|
+
drawRule: "Area1",
|
|
3157
|
+
capturesLabelSize: true,
|
|
3158
|
+
params: LABELED_AREA_PARAMS,
|
|
3159
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
3160
|
+
};
|
|
3161
|
+
/**
|
|
3162
|
+
* Creates a Generic command and control area control measure: a closed area
|
|
3163
|
+
* through `positions` labeled at its center with the bare `T` (Field T) plus
|
|
3164
|
+
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
3165
|
+
*/
|
|
3166
|
+
function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
|
|
3167
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
|
|
3168
|
+
}
|
|
3169
|
+
const GENERIC_C2_AREA = defineControlMeasure({
|
|
3170
|
+
metadata: GENERIC_C2_AREA_METADATA,
|
|
3171
|
+
generator: createGenericC2Area,
|
|
3172
|
+
defaultOptions: DEFAULT_GENERIC_C2_AREA_OPTIONS,
|
|
3173
|
+
rule: area1DrawRule,
|
|
3174
|
+
previewSample: {
|
|
3175
|
+
controlPoints: [
|
|
3176
|
+
[-1, -.5],
|
|
3177
|
+
[1, -.5],
|
|
3178
|
+
[1, .5],
|
|
3179
|
+
[-1, .5]
|
|
3180
|
+
],
|
|
3181
|
+
textAmplifiers: { T: "RIOT FOODS" },
|
|
3182
|
+
options: { labelSize: 96 }
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
//#endregion
|
|
2546
3186
|
//#region src/generators/cm15-maneuver-areas/assemblyArea.ts
|
|
2547
3187
|
/** Default options for the Assembly area control measure. */
|
|
2548
3188
|
const DEFAULT_ASSEMBLY_AREA_OPTIONS = {
|
|
@@ -2589,31 +3229,13 @@ const ASSEMBLY_AREA = defineControlMeasure({
|
|
|
2589
3229
|
[-1, .5]
|
|
2590
3230
|
],
|
|
2591
3231
|
textAmplifiers: { T: "BLUE" },
|
|
2592
|
-
options: {
|
|
2593
|
-
labelSizePixels: 12,
|
|
2594
|
-
labelSizeResolution: 8
|
|
2595
|
-
}
|
|
3232
|
+
options: { labelSize: 96 }
|
|
2596
3233
|
}
|
|
2597
3234
|
});
|
|
2598
3235
|
//#endregion
|
|
2599
3236
|
//#region src/generators/cm29-protection-lines/params.ts
|
|
2600
3237
|
const FORTIFIED_PARAMS = [
|
|
2601
|
-
|
|
2602
|
-
key: "smooth",
|
|
2603
|
-
label: "Smooth",
|
|
2604
|
-
description: "Round the line's corners by curving the path through the control points.",
|
|
2605
|
-
type: "boolean"
|
|
2606
|
-
},
|
|
2607
|
-
{
|
|
2608
|
-
key: "smoothResolution",
|
|
2609
|
-
label: "Smooth resolution",
|
|
2610
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
2611
|
-
type: "number",
|
|
2612
|
-
min: 2,
|
|
2613
|
-
max: 64,
|
|
2614
|
-
step: 1,
|
|
2615
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
2616
|
-
},
|
|
3238
|
+
...SMOOTH_LINE_PARAMS,
|
|
2617
3239
|
{
|
|
2618
3240
|
key: "toothSizePixels",
|
|
2619
3241
|
label: "Tooth size",
|
|
@@ -2636,6 +3258,7 @@ const FORTIFIED_PARAMS = [
|
|
|
2636
3258
|
}
|
|
2637
3259
|
];
|
|
2638
3260
|
const ANTITANK_DITCH_PARAMS = [
|
|
3261
|
+
...SMOOTH_LINE_PARAMS,
|
|
2639
3262
|
{
|
|
2640
3263
|
key: "toothHeightPixels",
|
|
2641
3264
|
label: "Tooth height",
|
|
@@ -2685,7 +3308,9 @@ const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
|
|
|
2685
3308
|
const DEFAULT_ANTITANK_DITCH_OPTIONS = {
|
|
2686
3309
|
toothHeight: DEFAULT_TOOTH_HEIGHT,
|
|
2687
3310
|
toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
|
|
2688
|
-
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1
|
|
3311
|
+
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1,
|
|
3312
|
+
smooth: false,
|
|
3313
|
+
smoothResolution: 16
|
|
2689
3314
|
};
|
|
2690
3315
|
const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
|
|
2691
3316
|
id: "antitank-ditch-under-construction",
|
|
@@ -2746,13 +3371,25 @@ function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidth
|
|
|
2746
3371
|
}
|
|
2747
3372
|
return triangles;
|
|
2748
3373
|
}
|
|
2749
|
-
|
|
3374
|
+
/**
|
|
3375
|
+
* Projects `positions` and, when `options.smooth` is set, curves the result
|
|
3376
|
+
* through the control points (centripetal Catmull-Rom) before the teeth are
|
|
3377
|
+
* traced — matching Fortified Line's base-path smoothing. Returns the input
|
|
3378
|
+
* unchanged (no project/unproject round trip) when smoothing is off.
|
|
3379
|
+
*/
|
|
3380
|
+
function smoothedDitchBasePath(positions, options) {
|
|
3381
|
+
const projectedPoints = positions.map((position) => project(position[0], position[1]));
|
|
3382
|
+
const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
|
|
2750
3383
|
const toothHeight = calculateAntitankDitchHeight(options);
|
|
2751
|
-
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1;
|
|
2752
|
-
return
|
|
3384
|
+
const toothWidthRatio = Math.max(EPSILON, options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
3385
|
+
return options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, toothWidthRatio * toothHeight) : smoothedPath;
|
|
3386
|
+
}
|
|
3387
|
+
function buildAntitankDitchTriangles(basePath, options) {
|
|
3388
|
+
return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
2753
3389
|
}
|
|
2754
3390
|
function createAntitankDitchUnderConstruction(positions, options = {}) {
|
|
2755
|
-
const
|
|
3391
|
+
const basePath = smoothedDitchBasePath(positions, options);
|
|
3392
|
+
const triangles = buildAntitankDitchTriangles(basePath, options);
|
|
2756
3393
|
return {
|
|
2757
3394
|
type: "FeatureCollection",
|
|
2758
3395
|
features: [{
|
|
@@ -2760,7 +3397,7 @@ function createAntitankDitchUnderConstruction(positions, options = {}) {
|
|
|
2760
3397
|
properties: { part: "ditch" },
|
|
2761
3398
|
geometry: {
|
|
2762
3399
|
type: "MultiLineString",
|
|
2763
|
-
coordinates: [positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
|
|
3400
|
+
coordinates: [options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
|
|
2764
3401
|
}
|
|
2765
3402
|
}]
|
|
2766
3403
|
};
|
|
@@ -2776,7 +3413,7 @@ function createAntitankDitchCompleted(positions, options = {}) {
|
|
|
2776
3413
|
},
|
|
2777
3414
|
geometry: {
|
|
2778
3415
|
type: "MultiPolygon",
|
|
2779
|
-
coordinates: buildAntitankDitchTriangles(positions, options).map((triangle) => {
|
|
3416
|
+
coordinates: buildAntitankDitchTriangles(smoothedDitchBasePath(positions, options), options).map((triangle) => {
|
|
2780
3417
|
return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
|
|
2781
3418
|
})
|
|
2782
3419
|
}
|
|
@@ -2853,6 +3490,8 @@ function createAntitankWall(positions, options = {}) {
|
|
|
2853
3490
|
const toothHeight = calculateAntitankDitchHeight(options);
|
|
2854
3491
|
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
|
|
2855
3492
|
const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
|
|
3493
|
+
const projectedPoints = positions.map((position) => project(position[0], position[1]));
|
|
3494
|
+
const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
|
|
2856
3495
|
return {
|
|
2857
3496
|
type: "FeatureCollection",
|
|
2858
3497
|
features: [{
|
|
@@ -2860,7 +3499,7 @@ function createAntitankWall(positions, options = {}) {
|
|
|
2860
3499
|
properties: { part: "wall" },
|
|
2861
3500
|
geometry: {
|
|
2862
3501
|
type: "MultiLineString",
|
|
2863
|
-
coordinates: [generateAntitankWallPoints(
|
|
3502
|
+
coordinates: [generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
|
|
2864
3503
|
}
|
|
2865
3504
|
}]
|
|
2866
3505
|
};
|
|
@@ -3255,70 +3894,7 @@ const BATTLE_POSITION_METADATA = {
|
|
|
3255
3894
|
],
|
|
3256
3895
|
drawRule: "Area1",
|
|
3257
3896
|
capturesLabelSize: true,
|
|
3258
|
-
params:
|
|
3259
|
-
{
|
|
3260
|
-
key: "smooth",
|
|
3261
|
-
label: "Smooth",
|
|
3262
|
-
description: "Round the area's corners by curving the boundary through the control points.",
|
|
3263
|
-
type: "boolean"
|
|
3264
|
-
},
|
|
3265
|
-
{
|
|
3266
|
-
key: "smoothResolution",
|
|
3267
|
-
label: "Smooth resolution",
|
|
3268
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
3269
|
-
type: "number",
|
|
3270
|
-
min: 2,
|
|
3271
|
-
max: 64,
|
|
3272
|
-
step: 1,
|
|
3273
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
3274
|
-
},
|
|
3275
|
-
{
|
|
3276
|
-
key: "echelon",
|
|
3277
|
-
label: "Echelon",
|
|
3278
|
-
description: "Field B unit-size amplifier, drawn on the boundary.",
|
|
3279
|
-
type: "enum",
|
|
3280
|
-
options: ECHELON_PARAM_OPTIONS
|
|
3281
|
-
},
|
|
3282
|
-
{
|
|
3283
|
-
key: "echelonSizePixels",
|
|
3284
|
-
label: "Echelon size",
|
|
3285
|
-
description: "Echelon glyph height in screen pixels.",
|
|
3286
|
-
type: "number",
|
|
3287
|
-
min: 8,
|
|
3288
|
-
max: 48,
|
|
3289
|
-
step: 1,
|
|
3290
|
-
unit: "px"
|
|
3291
|
-
},
|
|
3292
|
-
{
|
|
3293
|
-
key: "echelonSize",
|
|
3294
|
-
label: "Echelon size",
|
|
3295
|
-
description: "Echelon glyph height in meters.",
|
|
3296
|
-
type: "number",
|
|
3297
|
-
min: 50,
|
|
3298
|
-
max: 2e4,
|
|
3299
|
-
step: 50,
|
|
3300
|
-
unit: "m"
|
|
3301
|
-
},
|
|
3302
|
-
{
|
|
3303
|
-
key: "echelonPadding",
|
|
3304
|
-
label: "Echelon padding",
|
|
3305
|
-
description: "Gap on each side of the echelon, as a ratio of its height.",
|
|
3306
|
-
type: "number",
|
|
3307
|
-
min: 0,
|
|
3308
|
-
max: 2,
|
|
3309
|
-
step: .05
|
|
3310
|
-
},
|
|
3311
|
-
{
|
|
3312
|
-
key: "echelonPosition",
|
|
3313
|
-
label: "Echelon position",
|
|
3314
|
-
description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
|
|
3315
|
-
type: "number",
|
|
3316
|
-
min: -.5,
|
|
3317
|
-
max: .5,
|
|
3318
|
-
step: .01
|
|
3319
|
-
},
|
|
3320
|
-
AREA_LABEL_PADDING_PARAM
|
|
3321
|
-
],
|
|
3897
|
+
params: ECHELON_AREA_PARAMS,
|
|
3322
3898
|
textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
|
|
3323
3899
|
};
|
|
3324
3900
|
/**
|
|
@@ -3380,18 +3956,17 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
|
3380
3956
|
* Point: pins the echelon anchor to its current bottom-edge fraction so a
|
|
3381
3957
|
* subsequent rigid rotation of `controlPoints` (e.g. the transform box's
|
|
3382
3958
|
* rotate grip) does not re-derive a different "bottom edge" against the
|
|
3383
|
-
* rotated geometry.
|
|
3384
|
-
*
|
|
3385
|
-
*
|
|
3386
|
-
*
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
}
|
|
3959
|
+
* rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
|
|
3960
|
+
* factory every echelon-carrying area measure (Battle Position, Strong
|
|
3961
|
+
* Point, Base Camp, Guerrilla Base) builds its own instance from — this
|
|
3962
|
+
* export just binds Battle Position's own defaults, keeping strongPoint.ts's
|
|
3963
|
+
* existing import (and any other importer) unchanged.
|
|
3964
|
+
*/
|
|
3965
|
+
const freezeEchelonAnchor = makeFreezeEchelonAnchor({
|
|
3966
|
+
echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
|
|
3967
|
+
smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
|
|
3968
|
+
smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
|
|
3969
|
+
});
|
|
3395
3970
|
const BATTLE_POSITION = defineControlMeasure({
|
|
3396
3971
|
metadata: BATTLE_POSITION_METADATA,
|
|
3397
3972
|
generator: createBattlePosition,
|
|
@@ -3409,8 +3984,7 @@ const BATTLE_POSITION = defineControlMeasure({
|
|
|
3409
3984
|
options: {
|
|
3410
3985
|
echelonSize: 200,
|
|
3411
3986
|
smooth: true,
|
|
3412
|
-
|
|
3413
|
-
labelSizeResolution: 8
|
|
3987
|
+
labelSize: 96
|
|
3414
3988
|
}
|
|
3415
3989
|
}
|
|
3416
3990
|
});
|
|
@@ -3762,7 +4336,6 @@ const BOUNDARY_METADATA = {
|
|
|
3762
4336
|
"Point"
|
|
3763
4337
|
],
|
|
3764
4338
|
drawRule: "Line1",
|
|
3765
|
-
capturesLabelSize: true,
|
|
3766
4339
|
params: [
|
|
3767
4340
|
{
|
|
3768
4341
|
key: "smooth",
|
|
@@ -3896,7 +4469,7 @@ const BOUNDARY_METADATA = {
|
|
|
3896
4469
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
3897
4470
|
*/
|
|
3898
4471
|
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
3899
|
-
const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel,
|
|
4472
|
+
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;
|
|
3900
4473
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
3901
4474
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
3902
4475
|
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
@@ -3906,14 +4479,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
3906
4479
|
h = Math.max(EPSILON, h);
|
|
3907
4480
|
const reps = Math.max(1, Math.round(labelRepetitions));
|
|
3908
4481
|
const spec = resolveEchelon(echelon);
|
|
3909
|
-
const
|
|
3910
|
-
const refResolution = screenAnchored ? metersPerPixel : labelSizeResolution;
|
|
3911
|
-
const labelSize = {};
|
|
3912
|
-
if (refResolution !== void 0 && refResolution > 0) labelSize.textSizePixels = Math.round(h / refResolution);
|
|
3913
|
-
if (!screenAnchored) {
|
|
3914
|
-
if (labelSizeResolution !== void 0) labelSize.textSizeResolution = labelSizeResolution;
|
|
3915
|
-
if (labelSizeZoom !== void 0) labelSize.textSizeZoom = labelSizeZoom;
|
|
3916
|
-
}
|
|
4482
|
+
const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
|
|
3917
4483
|
const rawVerts = positions.map((p) => project(p[0], p[1]));
|
|
3918
4484
|
const verts = smooth ? catmullRom(rawVerts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$4)) : rawVerts;
|
|
3919
4485
|
const glyphStrokes = [];
|
|
@@ -4018,13 +4584,24 @@ const BOUNDARY = defineControlMeasure({
|
|
|
4018
4584
|
rule: line1DrawRule,
|
|
4019
4585
|
previewSample: {
|
|
4020
4586
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4021
|
-
options: {
|
|
4587
|
+
options: {
|
|
4588
|
+
echelonSize: 375,
|
|
4589
|
+
labelPadding: .5
|
|
4590
|
+
},
|
|
4591
|
+
textAmplifiers: {
|
|
4592
|
+
T: "2INF",
|
|
4593
|
+
T1: "3INF"
|
|
4594
|
+
}
|
|
4022
4595
|
}
|
|
4023
4596
|
});
|
|
4024
4597
|
//#endregion
|
|
4025
4598
|
//#region src/generators/cm11-command-control-lines/light-line.ts
|
|
4026
4599
|
/** Default options for the Light line control measure. */
|
|
4027
|
-
const DEFAULT_LIGHT_LINE_OPTIONS = {
|
|
4600
|
+
const DEFAULT_LIGHT_LINE_OPTIONS = {
|
|
4601
|
+
includePrefix: true,
|
|
4602
|
+
smooth: false,
|
|
4603
|
+
smoothResolution: 12
|
|
4604
|
+
};
|
|
4028
4605
|
const LIGHT_LINE_METADATA = {
|
|
4029
4606
|
id: "light-line",
|
|
4030
4607
|
name: "Light line",
|
|
@@ -4038,13 +4615,15 @@ const LIGHT_LINE_METADATA = {
|
|
|
4038
4615
|
drawRule: "Line1",
|
|
4039
4616
|
capturesLabelSize: true,
|
|
4040
4617
|
params: [
|
|
4618
|
+
...SMOOTH_LINE_PARAMS,
|
|
4041
4619
|
{
|
|
4042
4620
|
key: "phaseLineName",
|
|
4043
4621
|
label: "Phase line name",
|
|
4044
4622
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4045
4623
|
type: "text",
|
|
4046
4624
|
placeholder: "DAVID",
|
|
4047
|
-
maxLength: 21
|
|
4625
|
+
maxLength: 21,
|
|
4626
|
+
field: "T"
|
|
4048
4627
|
},
|
|
4049
4628
|
{
|
|
4050
4629
|
key: "includePrefix",
|
|
@@ -4082,16 +4661,17 @@ const LIGHT_LINE = defineControlMeasure({
|
|
|
4082
4661
|
rule: line1DrawRule,
|
|
4083
4662
|
previewSample: {
|
|
4084
4663
|
controlPoints: [[-1, 0], [1, 0]],
|
|
4085
|
-
options: {
|
|
4086
|
-
labelSizePixels: 12,
|
|
4087
|
-
labelSizeResolution: 8
|
|
4088
|
-
}
|
|
4664
|
+
options: { labelSize: 96 }
|
|
4089
4665
|
}
|
|
4090
4666
|
});
|
|
4091
4667
|
//#endregion
|
|
4092
4668
|
//#region src/generators/cm11-command-control-lines/engineer-work-line.ts
|
|
4093
4669
|
/** Default options for the Engineer work line control measure. */
|
|
4094
|
-
const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = {
|
|
4670
|
+
const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = {
|
|
4671
|
+
includePrefix: true,
|
|
4672
|
+
smooth: false,
|
|
4673
|
+
smoothResolution: 12
|
|
4674
|
+
};
|
|
4095
4675
|
const ENGINEER_WORK_LINE_METADATA = {
|
|
4096
4676
|
id: "engineer-work-line",
|
|
4097
4677
|
name: "Engineer work line",
|
|
@@ -4105,13 +4685,15 @@ const ENGINEER_WORK_LINE_METADATA = {
|
|
|
4105
4685
|
drawRule: "Line1",
|
|
4106
4686
|
capturesLabelSize: true,
|
|
4107
4687
|
params: [
|
|
4688
|
+
...SMOOTH_LINE_PARAMS,
|
|
4108
4689
|
{
|
|
4109
4690
|
key: "phaseLineName",
|
|
4110
4691
|
label: "Phase line name",
|
|
4111
4692
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4112
4693
|
type: "text",
|
|
4113
4694
|
placeholder: "DAVID",
|
|
4114
|
-
maxLength: 21
|
|
4695
|
+
maxLength: 21,
|
|
4696
|
+
field: "T"
|
|
4115
4697
|
},
|
|
4116
4698
|
{
|
|
4117
4699
|
key: "includePrefix",
|
|
@@ -4173,17 +4755,19 @@ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
|
|
|
4173
4755
|
placement: "above",
|
|
4174
4756
|
...options
|
|
4175
4757
|
}, "EWL").features];
|
|
4176
|
-
const { segments, totalLength } = polylineSegments(positions.map((p) => project(p[0], p[1])));
|
|
4758
|
+
const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
|
|
4177
4759
|
const frame = pointAlongPolyline(segments, totalLength, .5);
|
|
4178
4760
|
if (frame) {
|
|
4179
4761
|
const units = [{
|
|
4180
4762
|
side: 1,
|
|
4181
4763
|
designator: textAmplifiers.T ?? "",
|
|
4182
|
-
country: textAmplifiers.AS ?? ""
|
|
4764
|
+
country: textAmplifiers.AS ?? "",
|
|
4765
|
+
amplifierField: "T"
|
|
4183
4766
|
}, {
|
|
4184
4767
|
side: -1,
|
|
4185
4768
|
designator: textAmplifiers.T1 ?? "",
|
|
4186
|
-
country: textAmplifiers.AS1 ?? ""
|
|
4769
|
+
country: textAmplifiers.AS1 ?? "",
|
|
4770
|
+
amplifierField: "T1"
|
|
4187
4771
|
}];
|
|
4188
4772
|
const labelFeatures = [];
|
|
4189
4773
|
pushUnitLabels(labelFeatures, {
|
|
@@ -4208,21 +4792,20 @@ const ENGINEER_WORK_LINE = defineControlMeasure({
|
|
|
4208
4792
|
previewSample: {
|
|
4209
4793
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4210
4794
|
textAmplifiers: {
|
|
4211
|
-
T: "
|
|
4212
|
-
|
|
4213
|
-
T1: "127EN BN",
|
|
4214
|
-
AS1: "USA"
|
|
4795
|
+
T: "2BN",
|
|
4796
|
+
T1: "3BN"
|
|
4215
4797
|
},
|
|
4216
|
-
options: {
|
|
4217
|
-
labelSizePixels: 12,
|
|
4218
|
-
labelSizeResolution: 20
|
|
4219
|
-
}
|
|
4798
|
+
options: { labelSize: 240 }
|
|
4220
4799
|
}
|
|
4221
4800
|
});
|
|
4222
4801
|
//#endregion
|
|
4223
4802
|
//#region src/generators/cm11-command-control-lines/generic-c2-line.ts
|
|
4224
4803
|
/** Default options for the Generic command and control line. */
|
|
4225
|
-
const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
|
|
4804
|
+
const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
|
|
4805
|
+
includePrefix: true,
|
|
4806
|
+
smooth: false,
|
|
4807
|
+
smoothResolution: 12
|
|
4808
|
+
};
|
|
4226
4809
|
const GENERIC_C2_LINE_METADATA = {
|
|
4227
4810
|
id: "generic-c2-line",
|
|
4228
4811
|
name: "Generic command and control line",
|
|
@@ -4236,13 +4819,15 @@ const GENERIC_C2_LINE_METADATA = {
|
|
|
4236
4819
|
drawRule: "Line1",
|
|
4237
4820
|
capturesLabelSize: true,
|
|
4238
4821
|
params: [
|
|
4822
|
+
...SMOOTH_LINE_PARAMS,
|
|
4239
4823
|
{
|
|
4240
4824
|
key: "phaseLineName",
|
|
4241
4825
|
label: "Phase line name",
|
|
4242
4826
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4243
4827
|
type: "text",
|
|
4244
4828
|
placeholder: "DAVID",
|
|
4245
|
-
maxLength: 21
|
|
4829
|
+
maxLength: 21,
|
|
4830
|
+
field: "T"
|
|
4246
4831
|
},
|
|
4247
4832
|
{
|
|
4248
4833
|
key: "includePrefix",
|
|
@@ -4298,20 +4883,23 @@ const GENERIC_C2_LINE_METADATA = {
|
|
|
4298
4883
|
* `base + perp·offset` and the other at `base − perp·offset`.
|
|
4299
4884
|
*/
|
|
4300
4885
|
function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
|
|
4301
|
-
const { phaseLineName, includePrefix = true, ...sizeOptions } = options;
|
|
4886
|
+
const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
4887
|
+
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
4888
|
+
smooth,
|
|
4889
|
+
smoothResolution
|
|
4890
|
+
}, 12);
|
|
4302
4891
|
const features = [{
|
|
4303
4892
|
type: "Feature",
|
|
4304
4893
|
properties: { part: "generic-c2-line" },
|
|
4305
4894
|
geometry: {
|
|
4306
4895
|
type: "LineString",
|
|
4307
|
-
coordinates: positions
|
|
4896
|
+
coordinates: smooth ? verts.map((v) => unproject(v[0], v[1])) : positions
|
|
4308
4897
|
}
|
|
4309
4898
|
}];
|
|
4310
4899
|
const above = [textAmplifiers.H, textAmplifiers.T].filter((v) => Boolean(v)).join(" ");
|
|
4311
4900
|
const below = [textAmplifiers.W, textAmplifiers.W1].filter((v) => Boolean(v)).join(" - ");
|
|
4312
4901
|
const trimmedPhaseLineName = phaseLineName?.trim();
|
|
4313
4902
|
if (above.length > 0 || below.length > 0 || trimmedPhaseLineName) {
|
|
4314
|
-
const verts = positions.map((p) => project(p[0], p[1]));
|
|
4315
4903
|
const labelFeatures = [];
|
|
4316
4904
|
if (above.length > 0 || below.length > 0) {
|
|
4317
4905
|
const frames = aboveEndFrames(verts, sizeOptions);
|
|
@@ -4340,15 +4928,12 @@ const GENERIC_C2_LINE = defineControlMeasure({
|
|
|
4340
4928
|
previewSample: {
|
|
4341
4929
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4342
4930
|
textAmplifiers: {
|
|
4343
|
-
H: "
|
|
4344
|
-
T: "
|
|
4345
|
-
W: "
|
|
4346
|
-
W1: "
|
|
4931
|
+
H: "<H>",
|
|
4932
|
+
T: "<T>",
|
|
4933
|
+
W: "<W>",
|
|
4934
|
+
W1: "<W1>"
|
|
4347
4935
|
},
|
|
4348
|
-
options: {
|
|
4349
|
-
labelSizePixels: 12,
|
|
4350
|
-
labelSizeResolution: 20
|
|
4351
|
-
}
|
|
4936
|
+
options: { labelSize: 240 }
|
|
4352
4937
|
}
|
|
4353
4938
|
});
|
|
4354
4939
|
//#endregion
|
|
@@ -4371,21 +4956,21 @@ function createArrowHead$1(tip, dir, length, width) {
|
|
|
4371
4956
|
];
|
|
4372
4957
|
}
|
|
4373
4958
|
/**
|
|
4374
|
-
* Builds the glyph label as a Point feature, forwarding
|
|
4375
|
-
*
|
|
4376
|
-
*
|
|
4377
|
-
*
|
|
4959
|
+
* Builds the glyph label as a Point feature, forwarding the resolved size
|
|
4960
|
+
* anchor (`textSizeMeters`/`textSizePixels`, ADR-0028) to adapters for
|
|
4961
|
+
* styling. `point` is in projected meters; the rotation must already be in
|
|
4962
|
+
* the generator's feature-rotation convention. Shared by every mission-task
|
|
4963
|
+
* measure that centers a glyph on its symbol.
|
|
4378
4964
|
*/
|
|
4379
4965
|
function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
4380
4966
|
return {
|
|
4381
4967
|
type: "Feature",
|
|
4382
4968
|
properties: {
|
|
4383
4969
|
part: "label",
|
|
4970
|
+
labelPlacement: false,
|
|
4384
4971
|
text,
|
|
4385
4972
|
rotation,
|
|
4386
|
-
...labelSize
|
|
4387
|
-
...labelSize.zoom !== void 0 ? { textSizeZoom: labelSize.zoom } : {},
|
|
4388
|
-
...labelSize.resolution !== void 0 ? { textSizeResolution: labelSize.resolution } : {}
|
|
4973
|
+
...labelSizeProps(labelSize)
|
|
4389
4974
|
},
|
|
4390
4975
|
geometry: {
|
|
4391
4976
|
type: "Point",
|
|
@@ -4550,9 +5135,8 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
|
|
|
4550
5135
|
]
|
|
4551
5136
|
}
|
|
4552
5137
|
}, createLabelFeature(labelPoint, "B", labelRotation, {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
resolution: options.labelSizeResolution
|
|
5138
|
+
labelSize: options.labelSize,
|
|
5139
|
+
labelSizePixels: options.labelSizePixels
|
|
4556
5140
|
})]
|
|
4557
5141
|
};
|
|
4558
5142
|
}
|
|
@@ -4635,9 +5219,8 @@ function createBreachSymbol(coordinates, options = {}) {
|
|
|
4635
5219
|
labelText: "B",
|
|
4636
5220
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
|
|
4637
5221
|
labelSize: {
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
resolution: options.labelSizeResolution
|
|
5222
|
+
labelSize: options.labelSize,
|
|
5223
|
+
labelSizePixels: options.labelSizePixels
|
|
4641
5224
|
},
|
|
4642
5225
|
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
4643
5226
|
const halfTick = frontLength * tickLengthRatio / 2;
|
|
@@ -4719,9 +5302,8 @@ function createBypassSymbol(coordinates, options = {}) {
|
|
|
4719
5302
|
labelText: "B",
|
|
4720
5303
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
|
|
4721
5304
|
labelSize: {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
resolution: options.labelSizeResolution
|
|
5305
|
+
labelSize: options.labelSize,
|
|
5306
|
+
labelSizePixels: options.labelSizePixels
|
|
4725
5307
|
},
|
|
4726
5308
|
buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
|
|
4727
5309
|
const arrowheadLength = frontLength * arrowheadLengthRatio;
|
|
@@ -4808,9 +5390,8 @@ function createCanalizeSymbol(coordinates, options = {}) {
|
|
|
4808
5390
|
labelText: "C",
|
|
4809
5391
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
|
|
4810
5392
|
labelSize: {
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
resolution: options.labelSizeResolution
|
|
5393
|
+
labelSize: options.labelSize,
|
|
5394
|
+
labelSizePixels: options.labelSizePixels
|
|
4814
5395
|
},
|
|
4815
5396
|
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
4816
5397
|
const halfTick = frontLength * tickLengthRatio / 2;
|
|
@@ -5539,9 +6120,8 @@ function createClearSymbol(coordinates, options = {}) {
|
|
|
5539
6120
|
return {
|
|
5540
6121
|
type: "FeatureCollection",
|
|
5541
6122
|
features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
resolution: options.labelSizeResolution
|
|
6123
|
+
labelSize: options.labelSize,
|
|
6124
|
+
labelSizePixels: options.labelSizePixels
|
|
5545
6125
|
})]
|
|
5546
6126
|
};
|
|
5547
6127
|
}
|
|
@@ -5649,9 +6229,8 @@ function createDelaySymbol(coordinates, options = {}) {
|
|
|
5649
6229
|
return {
|
|
5650
6230
|
type: "FeatureCollection",
|
|
5651
6231
|
features: [lineFeature, createLabelFeature(labelPoint, "D", normalizeRadians(Math.PI + lineRotation), {
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
resolution: options.labelSizeResolution
|
|
6232
|
+
labelSize: options.labelSize,
|
|
6233
|
+
labelSizePixels: options.labelSizePixels
|
|
5655
6234
|
})]
|
|
5656
6235
|
};
|
|
5657
6236
|
}
|
|
@@ -5883,6 +6462,56 @@ const DISRUPT = defineControlMeasure({
|
|
|
5883
6462
|
previewSample: { controlPoints: [[0, .4], [0, -.4]] }
|
|
5884
6463
|
});
|
|
5885
6464
|
//#endregion
|
|
6465
|
+
//#region src/generators/cm15-maneuver-areas/dropZone.ts
|
|
6466
|
+
/** Default options for the Drop zone control measure. */
|
|
6467
|
+
const DEFAULT_DROP_ZONE_OPTIONS = {
|
|
6468
|
+
smooth: false,
|
|
6469
|
+
smoothResolution: 16
|
|
6470
|
+
};
|
|
6471
|
+
const DROP_ZONE_METADATA = {
|
|
6472
|
+
id: "drop-zone",
|
|
6473
|
+
name: "Drop zone",
|
|
6474
|
+
description: "A specified area upon which airborne troops, equipment, or supplies are airdropped.",
|
|
6475
|
+
entity: "Maneuver Areas",
|
|
6476
|
+
entityType: "Drop Zone",
|
|
6477
|
+
value: "150600",
|
|
6478
|
+
minCoordinates: 3,
|
|
6479
|
+
geometry: "area",
|
|
6480
|
+
geometryTypes: [
|
|
6481
|
+
"Polygon",
|
|
6482
|
+
"MultiLineString",
|
|
6483
|
+
"Point"
|
|
6484
|
+
],
|
|
6485
|
+
drawRule: "Area1",
|
|
6486
|
+
capturesLabelSize: true,
|
|
6487
|
+
params: LABELED_AREA_PARAMS,
|
|
6488
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
6489
|
+
};
|
|
6490
|
+
/**
|
|
6491
|
+
* Creates a Drop zone control measure: a closed area through `positions`
|
|
6492
|
+
* labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
|
|
6493
|
+
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
6494
|
+
*/
|
|
6495
|
+
function createDropZone(positions, options = {}, textAmplifiers = {}) {
|
|
6496
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
|
|
6497
|
+
}
|
|
6498
|
+
const DROP_ZONE = defineControlMeasure({
|
|
6499
|
+
metadata: DROP_ZONE_METADATA,
|
|
6500
|
+
generator: createDropZone,
|
|
6501
|
+
defaultOptions: DEFAULT_DROP_ZONE_OPTIONS,
|
|
6502
|
+
rule: area1DrawRule,
|
|
6503
|
+
previewSample: {
|
|
6504
|
+
controlPoints: [
|
|
6505
|
+
[-1, -.5],
|
|
6506
|
+
[1, -.5],
|
|
6507
|
+
[1, .5],
|
|
6508
|
+
[-1, .5]
|
|
6509
|
+
],
|
|
6510
|
+
textAmplifiers: { T: "HAWK" },
|
|
6511
|
+
options: { labelSize: 96 }
|
|
6512
|
+
}
|
|
6513
|
+
});
|
|
6514
|
+
//#endregion
|
|
5886
6515
|
//#region src/generators/cm15-maneuver-areas/encirclement.ts
|
|
5887
6516
|
const DEFAULT_BARB_SPACING_RATIO = .4;
|
|
5888
6517
|
const DEFAULT_BARB_LENGTH_RATIO = .16;
|
|
@@ -6030,17 +6659,66 @@ const ENCIRCLEMENT = defineControlMeasure({
|
|
|
6030
6659
|
[-.4, .85],
|
|
6031
6660
|
[-1, .5]
|
|
6032
6661
|
],
|
|
6033
|
-
textAmplifiers: {
|
|
6662
|
+
textAmplifiers: {},
|
|
6034
6663
|
options: {
|
|
6035
6664
|
barbSpacingRatio: .4,
|
|
6036
6665
|
barbLengthRatio: .18,
|
|
6037
6666
|
smooth: true,
|
|
6038
|
-
|
|
6039
|
-
labelSizeResolution: 8
|
|
6667
|
+
labelSize: 96
|
|
6040
6668
|
}
|
|
6041
6669
|
}
|
|
6042
6670
|
});
|
|
6043
6671
|
//#endregion
|
|
6672
|
+
//#region src/generators/cm15-maneuver-areas/extractionZone.ts
|
|
6673
|
+
/** Default options for the Extraction zone control measure. */
|
|
6674
|
+
const DEFAULT_EXTRACTION_ZONE_OPTIONS = {
|
|
6675
|
+
smooth: false,
|
|
6676
|
+
smoothResolution: 16
|
|
6677
|
+
};
|
|
6678
|
+
const EXTRACTION_ZONE_METADATA = {
|
|
6679
|
+
id: "extraction-zone",
|
|
6680
|
+
name: "Extraction zone",
|
|
6681
|
+
description: "A specified drop zone used for the delivery of supplies and/or equipment by means of an extraction technique from an aircraft flying very close to the ground.",
|
|
6682
|
+
entity: "Maneuver Areas",
|
|
6683
|
+
entityType: "Extraction Zone",
|
|
6684
|
+
value: "150700",
|
|
6685
|
+
minCoordinates: 3,
|
|
6686
|
+
geometry: "area",
|
|
6687
|
+
geometryTypes: [
|
|
6688
|
+
"Polygon",
|
|
6689
|
+
"MultiLineString",
|
|
6690
|
+
"Point"
|
|
6691
|
+
],
|
|
6692
|
+
drawRule: "Area1",
|
|
6693
|
+
capturesLabelSize: true,
|
|
6694
|
+
params: LABELED_AREA_PARAMS,
|
|
6695
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
6696
|
+
};
|
|
6697
|
+
/**
|
|
6698
|
+
* Creates an Extraction zone control measure: a closed area through
|
|
6699
|
+
* `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
|
|
6700
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
6701
|
+
*/
|
|
6702
|
+
function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
|
|
6703
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
|
|
6704
|
+
}
|
|
6705
|
+
const EXTRACTION_ZONE = defineControlMeasure({
|
|
6706
|
+
metadata: EXTRACTION_ZONE_METADATA,
|
|
6707
|
+
generator: createExtractionZone,
|
|
6708
|
+
defaultOptions: DEFAULT_EXTRACTION_ZONE_OPTIONS,
|
|
6709
|
+
rule: area1DrawRule,
|
|
6710
|
+
previewSample: {
|
|
6711
|
+
controlPoints: [
|
|
6712
|
+
[-1, -.5],
|
|
6713
|
+
[1, -.5],
|
|
6714
|
+
[1, .5],
|
|
6715
|
+
[-1, .5]
|
|
6716
|
+
],
|
|
6717
|
+
textAmplifiers: { T: "ROCK" },
|
|
6718
|
+
options: { labelSize: 96 }
|
|
6719
|
+
}
|
|
6720
|
+
});
|
|
6721
|
+
//#endregion
|
|
6044
6722
|
//#region src/internal/field-of-fire.ts
|
|
6045
6723
|
/**
|
|
6046
6724
|
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
@@ -6411,7 +7089,9 @@ const DEFAULT_ARC_SEGMENTS = 16;
|
|
|
6411
7089
|
const DEFAULT_FLOT_OPTIONS = {
|
|
6412
7090
|
radius: DEFAULT_RADIUS,
|
|
6413
7091
|
radiusPixels: DEFAULT_RADIUS_PIXELS,
|
|
6414
|
-
arcSegments: DEFAULT_ARC_SEGMENTS
|
|
7092
|
+
arcSegments: DEFAULT_ARC_SEGMENTS,
|
|
7093
|
+
smooth: false,
|
|
7094
|
+
smoothResolution: 16
|
|
6415
7095
|
};
|
|
6416
7096
|
const FLOT_METADATA = {
|
|
6417
7097
|
id: "flot",
|
|
@@ -6425,6 +7105,7 @@ const FLOT_METADATA = {
|
|
|
6425
7105
|
geometryTypes: ["LineString"],
|
|
6426
7106
|
drawRule: "Line1",
|
|
6427
7107
|
params: [
|
|
7108
|
+
...SMOOTH_LINE_PARAMS,
|
|
6428
7109
|
{
|
|
6429
7110
|
key: "radiusPixels",
|
|
6430
7111
|
label: "Radius",
|
|
@@ -6539,21 +7220,31 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
|
|
|
6539
7220
|
* ```
|
|
6540
7221
|
*/
|
|
6541
7222
|
function createFLOT(positions, options = {}) {
|
|
6542
|
-
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS } = options;
|
|
7223
|
+
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
|
|
6543
7224
|
let effectiveRadius = radius;
|
|
6544
7225
|
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
6545
7226
|
const safeRadius = Math.max(EPSILON, effectiveRadius);
|
|
6546
7227
|
const safeArcSegments = Math.max(3, Math.round(arcSegments));
|
|
6547
7228
|
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
7229
|
+
const smoothedPath = smoothLineVerts(projectedPoints, {
|
|
7230
|
+
smooth,
|
|
7231
|
+
smoothResolution
|
|
7232
|
+
}, 16);
|
|
7233
|
+
const basePath = smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, 2 * safeRadius) : smoothedPath;
|
|
6548
7234
|
const allPoints = [];
|
|
6549
|
-
for (let i = 0; i <
|
|
6550
|
-
const p1 =
|
|
6551
|
-
const p2 =
|
|
7235
|
+
for (let i = 0; i < basePath.length - 1; i++) {
|
|
7236
|
+
const p1 = basePath[i];
|
|
7237
|
+
const p2 = basePath[i + 1];
|
|
6552
7238
|
const segmentPoints = processSegment(p1, p2, safeRadius, safeArcSegments, i === 0);
|
|
6553
7239
|
allPoints.push(...segmentPoints);
|
|
6554
7240
|
}
|
|
6555
|
-
if (allPoints.length > 0 &&
|
|
6556
|
-
const
|
|
7241
|
+
if (allPoints.length > 0 && basePath.length > 1) {
|
|
7242
|
+
const firstOriginal = basePath[0];
|
|
7243
|
+
const firstGenerated = allPoints[0];
|
|
7244
|
+
if (vecMag(vecSub(firstOriginal, firstGenerated)) > 1e-6) allPoints.unshift(firstOriginal);
|
|
7245
|
+
}
|
|
7246
|
+
if (allPoints.length > 0 && basePath.length > 1) {
|
|
7247
|
+
const lastOriginal = basePath[basePath.length - 1];
|
|
6557
7248
|
const lastGenerated = allPoints[allPoints.length - 1];
|
|
6558
7249
|
if (vecMag(vecSub(lastOriginal, lastGenerated)) > 1e-6) allPoints.push(lastOriginal);
|
|
6559
7250
|
}
|
|
@@ -6579,7 +7270,11 @@ const FLOT = defineControlMeasure({
|
|
|
6579
7270
|
//#endregion
|
|
6580
7271
|
//#region src/generators/cm14-maneuver-lines/phase-line.ts
|
|
6581
7272
|
/** Default options for the Phase line control measure. */
|
|
6582
|
-
const DEFAULT_PHASE_LINE_OPTIONS = {
|
|
7273
|
+
const DEFAULT_PHASE_LINE_OPTIONS = {
|
|
7274
|
+
includePrefix: true,
|
|
7275
|
+
smooth: false,
|
|
7276
|
+
smoothResolution: 12
|
|
7277
|
+
};
|
|
6583
7278
|
const PHASE_LINE_METADATA = {
|
|
6584
7279
|
id: "phase-line",
|
|
6585
7280
|
name: "Phase line",
|
|
@@ -6592,20 +7287,24 @@ const PHASE_LINE_METADATA = {
|
|
|
6592
7287
|
geometryTypes: ["LineString", "Point"],
|
|
6593
7288
|
drawRule: "Line1",
|
|
6594
7289
|
capturesLabelSize: true,
|
|
6595
|
-
params: [
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
7290
|
+
params: [
|
|
7291
|
+
...SMOOTH_LINE_PARAMS,
|
|
7292
|
+
{
|
|
7293
|
+
key: "includePrefix",
|
|
7294
|
+
label: "Show PL prefix",
|
|
7295
|
+
description: "Prefix the end labels with \"PL\" (or show a bare \"PL\" with no designation).",
|
|
7296
|
+
type: "boolean"
|
|
7297
|
+
},
|
|
7298
|
+
{
|
|
7299
|
+
key: "labelPadding",
|
|
7300
|
+
label: "Label padding",
|
|
7301
|
+
description: "Extra clearance between the line and its labels, as a ratio of the label height",
|
|
7302
|
+
type: "number",
|
|
7303
|
+
min: 0,
|
|
7304
|
+
max: 2,
|
|
7305
|
+
step: .05
|
|
7306
|
+
}
|
|
7307
|
+
],
|
|
6609
7308
|
textAmplifiers: [{
|
|
6610
7309
|
key: "T",
|
|
6611
7310
|
label: "Unique designation",
|
|
@@ -6635,16 +7334,17 @@ const PHASE_LINE = defineControlMeasure({
|
|
|
6635
7334
|
previewSample: {
|
|
6636
7335
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6637
7336
|
textAmplifiers: { T: "ECHO" },
|
|
6638
|
-
options: {
|
|
6639
|
-
labelSizePixels: 12,
|
|
6640
|
-
labelSizeResolution: 8
|
|
6641
|
-
}
|
|
7337
|
+
options: { labelSize: 96 }
|
|
6642
7338
|
}
|
|
6643
7339
|
});
|
|
6644
7340
|
//#endregion
|
|
6645
7341
|
//#region src/generators/cm14-maneuver-lines/handover-line.ts
|
|
6646
7342
|
/** Default options for the Handover line control measure. */
|
|
6647
|
-
const DEFAULT_HANDOVER_LINE_OPTIONS = {
|
|
7343
|
+
const DEFAULT_HANDOVER_LINE_OPTIONS = {
|
|
7344
|
+
includePrefix: true,
|
|
7345
|
+
smooth: false,
|
|
7346
|
+
smoothResolution: 12
|
|
7347
|
+
};
|
|
6648
7348
|
const HANDOVER_LINE_METADATA = {
|
|
6649
7349
|
id: "handover-line",
|
|
6650
7350
|
name: "Handover line",
|
|
@@ -6658,13 +7358,15 @@ const HANDOVER_LINE_METADATA = {
|
|
|
6658
7358
|
drawRule: "Line1",
|
|
6659
7359
|
capturesLabelSize: true,
|
|
6660
7360
|
params: [
|
|
7361
|
+
...SMOOTH_LINE_PARAMS,
|
|
6661
7362
|
{
|
|
6662
7363
|
key: "phaseLineName",
|
|
6663
7364
|
label: "Phase line name",
|
|
6664
7365
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
6665
7366
|
type: "text",
|
|
6666
7367
|
placeholder: "DAVID",
|
|
6667
|
-
maxLength: 21
|
|
7368
|
+
maxLength: 21,
|
|
7369
|
+
field: "T"
|
|
6668
7370
|
},
|
|
6669
7371
|
{
|
|
6670
7372
|
key: "includePrefix",
|
|
@@ -6702,16 +7404,17 @@ const HANDOVER_LINE = defineControlMeasure({
|
|
|
6702
7404
|
rule: line1DrawRule,
|
|
6703
7405
|
previewSample: {
|
|
6704
7406
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6705
|
-
options: {
|
|
6706
|
-
labelSizePixels: 12,
|
|
6707
|
-
labelSizeResolution: 8
|
|
6708
|
-
}
|
|
7407
|
+
options: { labelSize: 96 }
|
|
6709
7408
|
}
|
|
6710
7409
|
});
|
|
6711
7410
|
//#endregion
|
|
6712
7411
|
//#region src/generators/cm14-maneuver-lines/battle-handover-line.ts
|
|
6713
7412
|
/** Default options for the Battle handover line control measure. */
|
|
6714
|
-
const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = {
|
|
7413
|
+
const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = {
|
|
7414
|
+
includePrefix: true,
|
|
7415
|
+
smooth: false,
|
|
7416
|
+
smoothResolution: 12
|
|
7417
|
+
};
|
|
6715
7418
|
const BATTLE_HANDOVER_LINE_METADATA = {
|
|
6716
7419
|
id: "battle-handover-line",
|
|
6717
7420
|
name: "Battle handover line",
|
|
@@ -6725,13 +7428,15 @@ const BATTLE_HANDOVER_LINE_METADATA = {
|
|
|
6725
7428
|
drawRule: "Line1",
|
|
6726
7429
|
capturesLabelSize: true,
|
|
6727
7430
|
params: [
|
|
7431
|
+
...SMOOTH_LINE_PARAMS,
|
|
6728
7432
|
{
|
|
6729
7433
|
key: "phaseLineName",
|
|
6730
7434
|
label: "Phase line name",
|
|
6731
7435
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
6732
7436
|
type: "text",
|
|
6733
7437
|
placeholder: "DAVID",
|
|
6734
|
-
maxLength: 21
|
|
7438
|
+
maxLength: 21,
|
|
7439
|
+
field: "T"
|
|
6735
7440
|
},
|
|
6736
7441
|
{
|
|
6737
7442
|
key: "includePrefix",
|
|
@@ -6769,10 +7474,7 @@ const BATTLE_HANDOVER_LINE = defineControlMeasure({
|
|
|
6769
7474
|
rule: line1DrawRule,
|
|
6770
7475
|
previewSample: {
|
|
6771
7476
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6772
|
-
options: {
|
|
6773
|
-
labelSizePixels: 12,
|
|
6774
|
-
labelSizeResolution: 8
|
|
6775
|
-
}
|
|
7477
|
+
options: { labelSize: 96 }
|
|
6776
7478
|
}
|
|
6777
7479
|
});
|
|
6778
7480
|
//#endregion
|
|
@@ -6919,7 +7621,7 @@ function createFortifiedLine(positions, options = {}) {
|
|
|
6919
7621
|
properties: {},
|
|
6920
7622
|
geometry: {
|
|
6921
7623
|
type: "LineString",
|
|
6922
|
-
coordinates: generateFortifiedPoints(smooth ? catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)) : projectedPoints, safeSize).map((p) => unproject(p[0], p[1]))
|
|
7624
|
+
coordinates: generateFortifiedPoints(smooth ? evenlySpacePath(catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)), 2 * safeSize) : projectedPoints, safeSize).map((p) => unproject(p[0], p[1]))
|
|
6923
7625
|
}
|
|
6924
7626
|
}]
|
|
6925
7627
|
};
|
|
@@ -6971,7 +7673,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
6971
7673
|
const safeSize = calculateEffectiveSize(options);
|
|
6972
7674
|
const projected = positions.map((p) => project(p[0], p[1]));
|
|
6973
7675
|
const ringPts = projected.length > 1 && vecMag(vecSub(projected[0], projected[projected.length - 1])) < 1e-6 ? projected.slice(0, -1) : projected;
|
|
6974
|
-
const boundaryInput = smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution)) : [...ringPts, ringPts[0]];
|
|
7676
|
+
const boundaryInput = smooth ? evenlySpacePath(closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution)), 2 * safeSize) : [...ringPts, ringPts[0]];
|
|
6975
7677
|
const boundaryPoints = generateFortifiedPoints(boundaryInput, safeSize);
|
|
6976
7678
|
if (boundaryPoints.length > 0) {
|
|
6977
7679
|
const firstPt = boundaryPoints[0];
|
|
@@ -7013,10 +7715,7 @@ const FORTIFIED_AREA = defineControlMeasure({
|
|
|
7013
7715
|
[-1, .5]
|
|
7014
7716
|
],
|
|
7015
7717
|
textAmplifiers: { T: "TANGO" },
|
|
7016
|
-
options: {
|
|
7017
|
-
labelSizePixels: 12,
|
|
7018
|
-
labelSizeResolution: 8
|
|
7019
|
-
}
|
|
7718
|
+
options: { labelSize: 96 }
|
|
7020
7719
|
}
|
|
7021
7720
|
});
|
|
7022
7721
|
//#endregion
|
|
@@ -7199,14 +7898,10 @@ const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
|
7199
7898
|
],
|
|
7200
7899
|
textAmplifiers: {
|
|
7201
7900
|
T: "02",
|
|
7202
|
-
N: "ENY",
|
|
7203
7901
|
W: "051030Z",
|
|
7204
7902
|
W1: "051600Z"
|
|
7205
7903
|
},
|
|
7206
|
-
options: {
|
|
7207
|
-
labelSizePixels: 12,
|
|
7208
|
-
labelSizeResolution: 8
|
|
7209
|
-
}
|
|
7904
|
+
options: { labelSize: 96 }
|
|
7210
7905
|
}
|
|
7211
7906
|
});
|
|
7212
7907
|
//#endregion
|
|
@@ -7256,10 +7951,7 @@ const LANDING_ZONE = defineControlMeasure({
|
|
|
7256
7951
|
[-1, .5]
|
|
7257
7952
|
],
|
|
7258
7953
|
textAmplifiers: { T: "SILVER" },
|
|
7259
|
-
options: {
|
|
7260
|
-
labelSizePixels: 12,
|
|
7261
|
-
labelSizeResolution: 8
|
|
7262
|
-
}
|
|
7954
|
+
options: { labelSize: 96 }
|
|
7263
7955
|
}
|
|
7264
7956
|
});
|
|
7265
7957
|
//#endregion
|
|
@@ -7779,10 +8471,145 @@ const PICKUP_ZONE = defineControlMeasure({
|
|
|
7779
8471
|
[-1, .5]
|
|
7780
8472
|
],
|
|
7781
8473
|
textAmplifiers: { T: "WOLF" },
|
|
7782
|
-
options: {
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
8474
|
+
options: { labelSize: 96 }
|
|
8475
|
+
}
|
|
8476
|
+
});
|
|
8477
|
+
//#endregion
|
|
8478
|
+
//#region src/generators/cm15-maneuver-areas/assaultPosition.ts
|
|
8479
|
+
/** Default options for the Assault position control measure. */
|
|
8480
|
+
const DEFAULT_ASSAULT_POSITION_OPTIONS = {
|
|
8481
|
+
smooth: false,
|
|
8482
|
+
smoothResolution: 16
|
|
8483
|
+
};
|
|
8484
|
+
const ASSAULT_POSITION_METADATA = {
|
|
8485
|
+
id: "assault-position",
|
|
8486
|
+
name: "Assault position",
|
|
8487
|
+
description: "That position between the line of departure and the objective in an attack from which forces assault the objective. Ideally, it is the last covered and concealed position before reaching the objective.",
|
|
8488
|
+
entity: "Maneuver Areas",
|
|
8489
|
+
entityType: "Assault Position",
|
|
8490
|
+
value: "151500",
|
|
8491
|
+
minCoordinates: 3,
|
|
8492
|
+
geometry: "area",
|
|
8493
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8494
|
+
drawRule: "Area1",
|
|
8495
|
+
capturesLabelSize: true,
|
|
8496
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8497
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8498
|
+
};
|
|
8499
|
+
/**
|
|
8500
|
+
* Creates an Assault position control measure: a closed area through
|
|
8501
|
+
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) —
|
|
8502
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8503
|
+
*/
|
|
8504
|
+
function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
|
|
8505
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("ASLT", textAmplifiers.T) });
|
|
8506
|
+
}
|
|
8507
|
+
const ASSAULT_POSITION = defineControlMeasure({
|
|
8508
|
+
metadata: ASSAULT_POSITION_METADATA,
|
|
8509
|
+
generator: createAssaultPosition,
|
|
8510
|
+
defaultOptions: DEFAULT_ASSAULT_POSITION_OPTIONS,
|
|
8511
|
+
rule: area1DrawRule,
|
|
8512
|
+
previewSample: {
|
|
8513
|
+
controlPoints: [
|
|
8514
|
+
[-1, -.5],
|
|
8515
|
+
[1, -.5],
|
|
8516
|
+
[1, .5],
|
|
8517
|
+
[-1, .5]
|
|
8518
|
+
],
|
|
8519
|
+
textAmplifiers: { T: "DANUBE" },
|
|
8520
|
+
options: { labelSize: 96 }
|
|
8521
|
+
}
|
|
8522
|
+
});
|
|
8523
|
+
//#endregion
|
|
8524
|
+
//#region src/generators/cm15-maneuver-areas/attackPosition.ts
|
|
8525
|
+
/** Default options for the Attack position control measure. */
|
|
8526
|
+
const DEFAULT_ATTACK_POSITION_OPTIONS = {
|
|
8527
|
+
smooth: false,
|
|
8528
|
+
smoothResolution: 16
|
|
8529
|
+
};
|
|
8530
|
+
const ATTACK_POSITION_METADATA = {
|
|
8531
|
+
id: "attack-position",
|
|
8532
|
+
name: "Attack position",
|
|
8533
|
+
description: "The last position occupied by the assault echelon before crossing the start line/line of departure.",
|
|
8534
|
+
entity: "Maneuver Areas",
|
|
8535
|
+
entityType: "Attack Position",
|
|
8536
|
+
value: "151600",
|
|
8537
|
+
minCoordinates: 3,
|
|
8538
|
+
geometry: "area",
|
|
8539
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8540
|
+
drawRule: "Area1",
|
|
8541
|
+
capturesLabelSize: true,
|
|
8542
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8543
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8544
|
+
};
|
|
8545
|
+
/**
|
|
8546
|
+
* Creates an Attack position control measure: a closed area through
|
|
8547
|
+
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T) —
|
|
8548
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8549
|
+
*/
|
|
8550
|
+
function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
|
|
8551
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("ATK", textAmplifiers.T) });
|
|
8552
|
+
}
|
|
8553
|
+
const ATTACK_POSITION = defineControlMeasure({
|
|
8554
|
+
metadata: ATTACK_POSITION_METADATA,
|
|
8555
|
+
generator: createAttackPosition,
|
|
8556
|
+
defaultOptions: DEFAULT_ATTACK_POSITION_OPTIONS,
|
|
8557
|
+
rule: area1DrawRule,
|
|
8558
|
+
previewSample: {
|
|
8559
|
+
controlPoints: [
|
|
8560
|
+
[-1, -.5],
|
|
8561
|
+
[1, -.5],
|
|
8562
|
+
[1, .5],
|
|
8563
|
+
[-1, .5]
|
|
8564
|
+
],
|
|
8565
|
+
textAmplifiers: { T: "NILE" },
|
|
8566
|
+
options: { labelSize: 96 }
|
|
8567
|
+
}
|
|
8568
|
+
});
|
|
8569
|
+
//#endregion
|
|
8570
|
+
//#region src/generators/cm15-maneuver-areas/objectiveArea.ts
|
|
8571
|
+
/** Default options for the Objective area control measure. */
|
|
8572
|
+
const DEFAULT_OBJECTIVE_AREA_OPTIONS = {
|
|
8573
|
+
smooth: false,
|
|
8574
|
+
smoothResolution: 16
|
|
8575
|
+
};
|
|
8576
|
+
const OBJECTIVE_AREA_METADATA = {
|
|
8577
|
+
id: "objective-area",
|
|
8578
|
+
name: "Objective area",
|
|
8579
|
+
description: "A defined geographical area within which is located an objective to be captured or reached by the military forces. This area is defined by competent authority for purposes of command and control.",
|
|
8580
|
+
entity: "Maneuver Areas",
|
|
8581
|
+
entityType: "Objective Area",
|
|
8582
|
+
value: "151700",
|
|
8583
|
+
minCoordinates: 3,
|
|
8584
|
+
geometry: "area",
|
|
8585
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8586
|
+
drawRule: "Area1",
|
|
8587
|
+
capturesLabelSize: true,
|
|
8588
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8589
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8590
|
+
};
|
|
8591
|
+
/**
|
|
8592
|
+
* Creates an Objective area control measure: a closed area through
|
|
8593
|
+
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) —
|
|
8594
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8595
|
+
*/
|
|
8596
|
+
function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
|
|
8597
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("OBJ", textAmplifiers.T) });
|
|
8598
|
+
}
|
|
8599
|
+
const OBJECTIVE_AREA = defineControlMeasure({
|
|
8600
|
+
metadata: OBJECTIVE_AREA_METADATA,
|
|
8601
|
+
generator: createObjectiveArea,
|
|
8602
|
+
defaultOptions: DEFAULT_OBJECTIVE_AREA_OPTIONS,
|
|
8603
|
+
rule: area1DrawRule,
|
|
8604
|
+
previewSample: {
|
|
8605
|
+
controlPoints: [
|
|
8606
|
+
[-1, -.5],
|
|
8607
|
+
[1, -.5],
|
|
8608
|
+
[1, .5],
|
|
8609
|
+
[-1, .5]
|
|
8610
|
+
],
|
|
8611
|
+
textAmplifiers: { T: "FIVE" },
|
|
8612
|
+
options: { labelSize: 96 }
|
|
7786
8613
|
}
|
|
7787
8614
|
});
|
|
7788
8615
|
//#endregion
|
|
@@ -8056,70 +8883,7 @@ const STRONG_POINT_METADATA = {
|
|
|
8056
8883
|
],
|
|
8057
8884
|
drawRule: "Area1",
|
|
8058
8885
|
capturesLabelSize: true,
|
|
8059
|
-
params:
|
|
8060
|
-
{
|
|
8061
|
-
key: "smooth",
|
|
8062
|
-
label: "Smooth",
|
|
8063
|
-
description: "Round the area's corners by curving the boundary through the control points.",
|
|
8064
|
-
type: "boolean"
|
|
8065
|
-
},
|
|
8066
|
-
{
|
|
8067
|
-
key: "smoothResolution",
|
|
8068
|
-
label: "Smooth resolution",
|
|
8069
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
8070
|
-
type: "number",
|
|
8071
|
-
min: 2,
|
|
8072
|
-
max: 64,
|
|
8073
|
-
step: 1,
|
|
8074
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
8075
|
-
},
|
|
8076
|
-
{
|
|
8077
|
-
key: "echelon",
|
|
8078
|
-
label: "Echelon",
|
|
8079
|
-
description: "Field B unit-size amplifier, drawn on the boundary.",
|
|
8080
|
-
type: "enum",
|
|
8081
|
-
options: ECHELON_PARAM_OPTIONS
|
|
8082
|
-
},
|
|
8083
|
-
{
|
|
8084
|
-
key: "echelonSizePixels",
|
|
8085
|
-
label: "Echelon size",
|
|
8086
|
-
description: "Echelon glyph height in screen pixels.",
|
|
8087
|
-
type: "number",
|
|
8088
|
-
min: 8,
|
|
8089
|
-
max: 48,
|
|
8090
|
-
step: 1,
|
|
8091
|
-
unit: "px"
|
|
8092
|
-
},
|
|
8093
|
-
{
|
|
8094
|
-
key: "echelonSize",
|
|
8095
|
-
label: "Echelon size",
|
|
8096
|
-
description: "Echelon glyph height in meters.",
|
|
8097
|
-
type: "number",
|
|
8098
|
-
min: 50,
|
|
8099
|
-
max: 2e4,
|
|
8100
|
-
step: 50,
|
|
8101
|
-
unit: "m"
|
|
8102
|
-
},
|
|
8103
|
-
{
|
|
8104
|
-
key: "echelonPadding",
|
|
8105
|
-
label: "Echelon padding",
|
|
8106
|
-
description: "Gap on each side of the echelon, as a ratio of its height.",
|
|
8107
|
-
type: "number",
|
|
8108
|
-
min: 0,
|
|
8109
|
-
max: 2,
|
|
8110
|
-
step: .05
|
|
8111
|
-
},
|
|
8112
|
-
{
|
|
8113
|
-
key: "echelonPosition",
|
|
8114
|
-
label: "Echelon position",
|
|
8115
|
-
description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
|
|
8116
|
-
type: "number",
|
|
8117
|
-
min: -.5,
|
|
8118
|
-
max: .5,
|
|
8119
|
-
step: .01
|
|
8120
|
-
},
|
|
8121
|
-
AREA_LABEL_PADDING_PARAM
|
|
8122
|
-
],
|
|
8886
|
+
params: ECHELON_AREA_PARAMS,
|
|
8123
8887
|
textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
|
|
8124
8888
|
};
|
|
8125
8889
|
/**
|
|
@@ -8227,12 +8991,121 @@ const STRONG_POINT = defineControlMeasure({
|
|
|
8227
8991
|
options: {
|
|
8228
8992
|
echelonSize: 200,
|
|
8229
8993
|
smooth: true,
|
|
8230
|
-
|
|
8231
|
-
labelSizeResolution: 8
|
|
8994
|
+
labelSize: 96
|
|
8232
8995
|
}
|
|
8233
8996
|
}
|
|
8234
8997
|
});
|
|
8235
8998
|
//#endregion
|
|
8999
|
+
//#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
|
|
9000
|
+
/** Default options for the Submarine action area control measure. */
|
|
9001
|
+
const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
|
|
9002
|
+
smooth: false,
|
|
9003
|
+
smoothResolution: 16
|
|
9004
|
+
};
|
|
9005
|
+
const SUBMARINE_ACTION_AREA_METADATA = {
|
|
9006
|
+
id: "submarine-action-area",
|
|
9007
|
+
name: "Submarine action area",
|
|
9008
|
+
description: "An area, including the water space and airspace above it, reserved for submarine operations.",
|
|
9009
|
+
entity: "Maneuver Areas",
|
|
9010
|
+
entityType: "Action Area",
|
|
9011
|
+
entitySubtype: "Submarine Action Area",
|
|
9012
|
+
value: "150502",
|
|
9013
|
+
minCoordinates: 3,
|
|
9014
|
+
geometry: "area",
|
|
9015
|
+
geometryTypes: [
|
|
9016
|
+
"Polygon",
|
|
9017
|
+
"MultiLineString",
|
|
9018
|
+
"Point"
|
|
9019
|
+
],
|
|
9020
|
+
drawRule: "Area1",
|
|
9021
|
+
capturesLabelSize: true,
|
|
9022
|
+
params: LABELED_AREA_PARAMS,
|
|
9023
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
|
|
9024
|
+
};
|
|
9025
|
+
/**
|
|
9026
|
+
* Creates a Submarine action area control measure: a closed area through
|
|
9027
|
+
* `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
|
|
9028
|
+
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
9029
|
+
*/
|
|
9030
|
+
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9031
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
|
|
9032
|
+
}
|
|
9033
|
+
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
9034
|
+
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
9035
|
+
generator: createSubmarineActionArea,
|
|
9036
|
+
defaultOptions: DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS,
|
|
9037
|
+
rule: area1DrawRule,
|
|
9038
|
+
previewSample: {
|
|
9039
|
+
controlPoints: [
|
|
9040
|
+
[-1, -.5],
|
|
9041
|
+
[1, -.5],
|
|
9042
|
+
[1, .5],
|
|
9043
|
+
[-1, .5]
|
|
9044
|
+
],
|
|
9045
|
+
textAmplifiers: {
|
|
9046
|
+
T: "02",
|
|
9047
|
+
W: "051030Z",
|
|
9048
|
+
W1: "051600Z"
|
|
9049
|
+
},
|
|
9050
|
+
options: { labelSize: 96 }
|
|
9051
|
+
}
|
|
9052
|
+
});
|
|
9053
|
+
//#endregion
|
|
9054
|
+
//#region src/generators/cm15-maneuver-areas/submarineGeneratedActionArea.ts
|
|
9055
|
+
/** Default options for the Submarine-generated action area control measure. */
|
|
9056
|
+
const DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS = {
|
|
9057
|
+
smooth: false,
|
|
9058
|
+
smoothResolution: 16
|
|
9059
|
+
};
|
|
9060
|
+
const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
9061
|
+
id: "submarine-generated-action-area",
|
|
9062
|
+
name: "Submarine-generated action area",
|
|
9063
|
+
description: "An action area generated around a submarine's reported position, reserved for its operations.",
|
|
9064
|
+
entity: "Maneuver Areas",
|
|
9065
|
+
entityType: "Action Area",
|
|
9066
|
+
entitySubtype: "Submarine-Generated Action Area",
|
|
9067
|
+
value: "150503",
|
|
9068
|
+
minCoordinates: 3,
|
|
9069
|
+
geometry: "area",
|
|
9070
|
+
geometryTypes: [
|
|
9071
|
+
"Polygon",
|
|
9072
|
+
"MultiLineString",
|
|
9073
|
+
"Point"
|
|
9074
|
+
],
|
|
9075
|
+
drawRule: "Area1",
|
|
9076
|
+
capturesLabelSize: true,
|
|
9077
|
+
params: LABELED_AREA_PARAMS,
|
|
9078
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
|
|
9079
|
+
};
|
|
9080
|
+
/**
|
|
9081
|
+
* Creates a Submarine-generated action area control measure: a closed area
|
|
9082
|
+
* through `positions` labeled at its center with "SGSA -"-prefixed `T`
|
|
9083
|
+
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
9084
|
+
*/
|
|
9085
|
+
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9086
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
|
|
9087
|
+
}
|
|
9088
|
+
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
9089
|
+
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
9090
|
+
generator: createSubmarineGeneratedActionArea,
|
|
9091
|
+
defaultOptions: DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS,
|
|
9092
|
+
rule: area1DrawRule,
|
|
9093
|
+
previewSample: {
|
|
9094
|
+
controlPoints: [
|
|
9095
|
+
[-1, -.5],
|
|
9096
|
+
[1, -.5],
|
|
9097
|
+
[1, .5],
|
|
9098
|
+
[-1, .5]
|
|
9099
|
+
],
|
|
9100
|
+
textAmplifiers: {
|
|
9101
|
+
T: "02",
|
|
9102
|
+
W: "051030Z",
|
|
9103
|
+
W1: "051600Z"
|
|
9104
|
+
},
|
|
9105
|
+
options: { labelSize: 96 }
|
|
9106
|
+
}
|
|
9107
|
+
});
|
|
9108
|
+
//#endregion
|
|
8236
9109
|
//#region src/generators/cm15-maneuver-areas/supportByFire.ts
|
|
8237
9110
|
const DEFAULT_SUPPORT_BY_FIRE_OPTIONS = {
|
|
8238
9111
|
arrowheadWidthRatio: .3,
|
|
@@ -8571,11 +9444,25 @@ const DEFINITIONS = {
|
|
|
8571
9444
|
"final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
|
|
8572
9445
|
"search-area": SEARCH_AREA,
|
|
8573
9446
|
encirclement: ENCIRCLEMENT,
|
|
9447
|
+
area: AREA,
|
|
8574
9448
|
"assembly-area": ASSEMBLY_AREA,
|
|
8575
9449
|
"joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
|
|
9450
|
+
"submarine-action-area": SUBMARINE_ACTION_AREA,
|
|
9451
|
+
"submarine-generated-action-area": SUBMARINE_GENERATED_ACTION_AREA,
|
|
9452
|
+
"drop-zone": DROP_ZONE,
|
|
9453
|
+
"extraction-zone": EXTRACTION_ZONE,
|
|
8576
9454
|
"landing-zone": LANDING_ZONE,
|
|
8577
9455
|
"pickup-zone": PICKUP_ZONE,
|
|
9456
|
+
"assault-position": ASSAULT_POSITION,
|
|
9457
|
+
"attack-position": ATTACK_POSITION,
|
|
9458
|
+
"objective-area": OBJECTIVE_AREA,
|
|
8578
9459
|
"area-of-operations": AREA_OF_OPERATIONS,
|
|
9460
|
+
"named-area-of-interest": NAMED_AREA_OF_INTEREST,
|
|
9461
|
+
"target-area-of-interest": TARGET_AREA_OF_INTEREST,
|
|
9462
|
+
"airfield-zone": AIRFIELD_ZONE,
|
|
9463
|
+
"base-camp": BASE_CAMP,
|
|
9464
|
+
"guerrilla-base": GUERRILLA_BASE,
|
|
9465
|
+
"generic-c2-area": GENERIC_C2_AREA,
|
|
8579
9466
|
"main-attack": MAIN_ATTACK,
|
|
8580
9467
|
"supporting-attack": SUPPORTING_ATTACK,
|
|
8581
9468
|
"classic-arrow": CLASSIC_ARROW,
|
|
@@ -8854,7 +9741,16 @@ function normalizeTextAmplifiers(input) {
|
|
|
8854
9741
|
* `properties.style` when non-empty.
|
|
8855
9742
|
*/
|
|
8856
9743
|
function renderControlMeasure(cm, opts) {
|
|
8857
|
-
const
|
|
9744
|
+
const raw = dispatchControlMeasure(cm, opts);
|
|
9745
|
+
const generatedLabelOrdinals = /* @__PURE__ */ new Map();
|
|
9746
|
+
const features = raw.features.map((feature, index) => {
|
|
9747
|
+
const sourceProps = feature.properties ?? {};
|
|
9748
|
+
const part = typeof sourceProps.part === "string" && sourceProps.part.length > 0 ? sourceProps.part : "main";
|
|
9749
|
+
const isGeneratedLabel = sourceProps.labelPlacement !== false && !(typeof sourceProps.amplifierField === "string" && canonicalTextAmplifierKey(sourceProps.amplifierField)) && !(typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) && typeof sourceProps.text === "string" && feature.geometry.type === "Point";
|
|
9750
|
+
const generatedLabelOrdinal = isGeneratedLabel ? generatedLabelOrdinals.get(part) ?? 0 : index;
|
|
9751
|
+
if (isGeneratedLabel) generatedLabelOrdinals.set(part, generatedLabelOrdinal + 1);
|
|
9752
|
+
return normalizeFeature(cm.id, feature, index, generatedLabelOrdinal, cm.style, opts?.graphicsStyle, cm.amplifierPlacements);
|
|
9753
|
+
});
|
|
8858
9754
|
const bbox = computeBBox(features);
|
|
8859
9755
|
return {
|
|
8860
9756
|
type: "FeatureCollection",
|
|
@@ -8886,7 +9782,20 @@ function validateInputContract(points, metadata, mode) {
|
|
|
8886
9782
|
if (points.some((point) => !isValidPosition(point))) return reportValidationIssue(mode, `"${metadata.id}" control points must contain finite [lon, lat] values.`);
|
|
8887
9783
|
return true;
|
|
8888
9784
|
}
|
|
8889
|
-
|
|
9785
|
+
/**
|
|
9786
|
+
* The `amplifierPlacements` key that repositions this feature, if any: the
|
|
9787
|
+
* canonical amplifier field when the generator declared one, an explicit
|
|
9788
|
+
* `labelPlacementKey`, or a synthesized `label:<part>:<ordinal>` key for any
|
|
9789
|
+
* other point label — unless the generator opted out via
|
|
9790
|
+
* `labelPlacement: false`.
|
|
9791
|
+
*/
|
|
9792
|
+
function resolveLabelPlacementKey(sourceProps, amplifierField, text, geometryType, part, generatedLabelOrdinal) {
|
|
9793
|
+
if (sourceProps.labelPlacement === false) return void 0;
|
|
9794
|
+
if (amplifierField) return amplifierField;
|
|
9795
|
+
if (typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) return sourceProps.labelPlacementKey;
|
|
9796
|
+
if (text !== void 0 && geometryType === "Point") return `label:${part}:${generatedLabelOrdinal}`;
|
|
9797
|
+
}
|
|
9798
|
+
function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyle, graphicsStyle, amplifierPlacements) {
|
|
8890
9799
|
const sourceProps = feature.properties ?? {};
|
|
8891
9800
|
const part = typeof sourceProps.part === "string" && sourceProps.part.length > 0 ? sourceProps.part : "main";
|
|
8892
9801
|
const generatorStyle = typeof sourceProps.style === "object" && sourceProps.style !== null ? sourceProps.style : void 0;
|
|
@@ -8895,9 +9804,15 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
8895
9804
|
const text = typeof sourceProps.text === "string" ? sourceProps.text : void 0;
|
|
8896
9805
|
const rotation = typeof sourceProps.rotation === "number" ? sourceProps.rotation : void 0;
|
|
8897
9806
|
const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
|
|
8898
|
-
const
|
|
8899
|
-
const textSizeResolution = typeof sourceProps.textSizeResolution === "number" ? sourceProps.textSizeResolution : void 0;
|
|
9807
|
+
const textSizeMeters = typeof sourceProps.textSizeMeters === "number" ? sourceProps.textSizeMeters : void 0;
|
|
8900
9808
|
const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
|
|
9809
|
+
const amplifierField = typeof sourceProps.amplifierField === "string" ? canonicalTextAmplifierKey(sourceProps.amplifierField) : void 0;
|
|
9810
|
+
const labelPlacementKey = resolveLabelPlacementKey(sourceProps, amplifierField, text, feature.geometry.type, part, generatedLabelOrdinal);
|
|
9811
|
+
const placement = labelPlacementKey ? amplifierPlacements?.[labelPlacementKey] : void 0;
|
|
9812
|
+
const geometry = placement && isValidPosition(placement) && feature.geometry.type === "Point" ? {
|
|
9813
|
+
...feature.geometry,
|
|
9814
|
+
coordinates: [...placement]
|
|
9815
|
+
} : feature.geometry;
|
|
8901
9816
|
return {
|
|
8902
9817
|
type: "Feature",
|
|
8903
9818
|
id: `${id}:${part}:${index}`,
|
|
@@ -8908,11 +9823,12 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
8908
9823
|
...text !== void 0 ? { text } : {},
|
|
8909
9824
|
...rotation !== void 0 ? { rotation } : {},
|
|
8910
9825
|
...textAnchor !== void 0 ? { textAnchor } : {},
|
|
9826
|
+
...amplifierField !== void 0 ? { amplifierField } : {},
|
|
9827
|
+
...labelPlacementKey !== void 0 ? { labelPlacementKey } : {},
|
|
8911
9828
|
...textSizePixels !== void 0 ? { textSizePixels } : {},
|
|
8912
|
-
...
|
|
8913
|
-
...textSizeResolution !== void 0 ? { textSizeResolution } : {}
|
|
9829
|
+
...textSizeMeters !== void 0 ? { textSizeMeters } : {}
|
|
8914
9830
|
},
|
|
8915
|
-
geometry
|
|
9831
|
+
geometry
|
|
8916
9832
|
};
|
|
8917
9833
|
}
|
|
8918
9834
|
function computeBBox(features) {
|