@orbat-mapper/control-measures 0.2.0-alpha.10 → 0.2.0-alpha.11
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-C6nqkw1A.d.mts} +316 -68
- package/dist/index.d.mts +1 -1
- 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-NKEYq3Fg.mjs} +1430 -595
- 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 = {
|
|
@@ -1405,224 +1652,15 @@ function createAmbushSymbol(coordinates, options = {}) {
|
|
|
1405
1652
|
type: "MultiLineString",
|
|
1406
1653
|
coordinates: lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
1407
1654
|
}
|
|
1408
|
-
}]
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
const AMBUSH = defineControlMeasure({
|
|
1412
|
-
metadata: AMBUSH_METADATA,
|
|
1413
|
-
generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
|
|
1414
|
-
defaultOptions: DEFAULT_AMBUSH_OPTIONS,
|
|
1415
|
-
rule: ambushDrawRule
|
|
1416
|
-
});
|
|
1417
|
-
//#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
|
-
}
|
|
1655
|
+
}]
|
|
1656
|
+
};
|
|
1625
1657
|
}
|
|
1658
|
+
const AMBUSH = defineControlMeasure({
|
|
1659
|
+
metadata: AMBUSH_METADATA,
|
|
1660
|
+
generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
|
|
1661
|
+
defaultOptions: DEFAULT_AMBUSH_OPTIONS,
|
|
1662
|
+
rule: ambushDrawRule
|
|
1663
|
+
});
|
|
1626
1664
|
//#endregion
|
|
1627
1665
|
//#region src/internal/gapped-line.ts
|
|
1628
1666
|
/**
|
|
@@ -1760,6 +1798,34 @@ function normalizeSmoothResolution$2(value, fallback) {
|
|
|
1760
1798
|
if (!Number.isFinite(value)) return fallback;
|
|
1761
1799
|
return Math.min(64, Math.max(2, Math.round(value)));
|
|
1762
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
|
+
}
|
|
1763
1829
|
/** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
|
|
1764
1830
|
function catmullRomAt(p0, p1, p2, p3, t) {
|
|
1765
1831
|
const t2 = t * t;
|
|
@@ -1934,6 +2000,31 @@ function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, ec
|
|
|
1934
2000
|
gaps
|
|
1935
2001
|
};
|
|
1936
2002
|
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Factory for the `freezeOrientation` hook (ADR-0023) shared by every
|
|
2005
|
+
* echelon-carrying closed-area measure (Battle Position, Strong Point, Base
|
|
2006
|
+
* Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
|
|
2007
|
+
* fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
|
|
2008
|
+
* transform box's rotate grip) does not re-derive a different "bottom edge"
|
|
2009
|
+
* against the rotated geometry. `defaults` supplies the measure's own
|
|
2010
|
+
* `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
|
|
2011
|
+
* `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
|
|
2012
|
+
* vs. Base Camp/Guerrilla Base's `echelon: "none"`).
|
|
2013
|
+
*
|
|
2014
|
+
* The returned hook returns `undefined` — nothing to freeze — when:
|
|
2015
|
+
* - `echelonAnchor` is already set (previously frozen or user-authored), or
|
|
2016
|
+
* - the echelon resolves to none (nothing visible to pin), or
|
|
2017
|
+
* - the ring is degenerate (no segments, or a near-zero perimeter).
|
|
2018
|
+
*/
|
|
2019
|
+
function makeFreezeEchelonAnchor(defaults) {
|
|
2020
|
+
return function freezeEchelonAnchor(controlPoints, options) {
|
|
2021
|
+
if (options.echelonAnchor !== void 0) return void 0;
|
|
2022
|
+
if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
|
|
2023
|
+
const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
|
|
2024
|
+
if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
|
|
2025
|
+
return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
1937
2028
|
//#endregion
|
|
1938
2029
|
//#region src/internal/angle-utils.ts
|
|
1939
2030
|
/**
|
|
@@ -2095,16 +2186,17 @@ function endTangents(verts) {
|
|
|
2095
2186
|
};
|
|
2096
2187
|
}
|
|
2097
2188
|
/**
|
|
2098
|
-
* Resolves {@link LabelSizeOptions} into the
|
|
2099
|
-
*
|
|
2189
|
+
* Resolves {@link LabelSizeOptions} into the single text-size property the
|
|
2190
|
+
* label feature carries (ADR-0028): `textSizePixels` when screen-anchored
|
|
2191
|
+
* (`labelSizePixels` present — pixels win over a stray `labelSize`),
|
|
2192
|
+
* `textSizeMeters` when ground-anchored (`labelSize` alone), or neither, in
|
|
2193
|
+
* which case adapters render their engine default, screen-fixed.
|
|
2100
2194
|
*/
|
|
2101
2195
|
function labelSizeProps(options) {
|
|
2102
|
-
const {
|
|
2103
|
-
|
|
2104
|
-
if (
|
|
2105
|
-
|
|
2106
|
-
if (labelSizeResolution !== void 0) props.textSizeResolution = labelSizeResolution;
|
|
2107
|
-
return props;
|
|
2196
|
+
const { labelSize, labelSizePixels } = options;
|
|
2197
|
+
if (labelSizePixels !== void 0) return { textSizePixels: labelSizePixels };
|
|
2198
|
+
if (labelSize !== void 0) return { textSizeMeters: labelSize };
|
|
2199
|
+
return {};
|
|
2108
2200
|
}
|
|
2109
2201
|
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2110
2202
|
function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
@@ -2123,20 +2215,21 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
|
2123
2215
|
}
|
|
2124
2216
|
});
|
|
2125
2217
|
}
|
|
2126
|
-
/** Ratio of resolved label height (`labelSizePixels ×
|
|
2218
|
+
/** Ratio of resolved label height (`labelSize`, or `labelSizePixels × metersPerPixel`) used as end-label clearance. */
|
|
2127
2219
|
const END_LABEL_CLEARANCE_RATIO = .3;
|
|
2128
2220
|
/** Ratio of resolved label height used to inset "above" end labels from the tip. */
|
|
2129
2221
|
const ABOVE_LABEL_INSET_RATIO = .3;
|
|
2130
2222
|
/** Ratio of resolved label height used as the "above" end labels' perpendicular offset. */
|
|
2131
2223
|
const ABOVE_LABEL_OFFSET_RATIO = .6;
|
|
2132
2224
|
function textHeight(options) {
|
|
2133
|
-
const { labelSizePixels,
|
|
2134
|
-
if (labelSizePixels !== void 0
|
|
2225
|
+
const { labelSize, labelSizePixels, metersPerPixel } = options;
|
|
2226
|
+
if (labelSizePixels !== void 0) return metersPerPixel !== void 0 && metersPerPixel > 0 ? labelSizePixels * metersPerPixel : void 0;
|
|
2227
|
+
return labelSize;
|
|
2135
2228
|
}
|
|
2136
2229
|
/**
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
* size
|
|
2230
|
+
* Label offset in meters: `ratio` × the label's resolved height (`labelSize`
|
|
2231
|
+
* meters, or `labelSizePixels × metersPerPixel` when screen-anchored), or
|
|
2232
|
+
* `fallbackMeters` when no size resolves (e.g. no map resolution yet). The fallback is small
|
|
2140
2233
|
* relative to typical control measure spans — the same order of magnitude as
|
|
2141
2234
|
* Boundary's own smallest label offsets — so a fixed label still reads as "just
|
|
2142
2235
|
* clear of the line" rather than crowding it or floating away. Callers supply
|
|
@@ -2230,16 +2323,20 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
|
|
|
2230
2323
|
* as a complete FeatureCollection.
|
|
2231
2324
|
*/
|
|
2232
2325
|
function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
2233
|
-
const { part, placement = "beyond", phaseLineName, includePrefix = true, ...sizeOptions } = options;
|
|
2326
|
+
const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
2327
|
+
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
2328
|
+
smooth,
|
|
2329
|
+
smoothResolution
|
|
2330
|
+
}, 12);
|
|
2331
|
+
const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
|
|
2234
2332
|
const features = [{
|
|
2235
2333
|
type: "Feature",
|
|
2236
2334
|
properties: { part },
|
|
2237
2335
|
geometry: {
|
|
2238
2336
|
type: "LineString",
|
|
2239
|
-
coordinates:
|
|
2337
|
+
coordinates: lineCoordinates
|
|
2240
2338
|
}
|
|
2241
2339
|
}];
|
|
2242
|
-
const verts = positions.map((p) => project(p[0], p[1]));
|
|
2243
2340
|
const labelFeatures = [];
|
|
2244
2341
|
if (placement === "above") pushEndLabelsAbove(labelFeatures, verts, textForEnd, sizeOptions);
|
|
2245
2342
|
else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
|
|
@@ -2254,7 +2351,9 @@ function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
|
2254
2351
|
//#endregion
|
|
2255
2352
|
//#region src/internal/area-labels.ts
|
|
2256
2353
|
/** Stored label rotation for horizontal (screen-upright) area text — adapters
|
|
2257
|
-
* render feature rotation as `π − rotation`, so `π` renders as `0`.
|
|
2354
|
+
* render feature rotation as `π − rotation`, so `π` renders as `0`. Exported
|
|
2355
|
+
* for reuse by measures that place a horizontal label outside the area's
|
|
2356
|
+
* boundary (e.g. Airfield Zone's Field H). */
|
|
2258
2357
|
const HORIZONTAL_LABEL_ROTATION = Math.PI;
|
|
2259
2358
|
/** Row pitch, as a ratio of the resolved label height, between stacked
|
|
2260
2359
|
* name/H/W-W1 rows (see {@link pushAreaLabels}). */
|
|
@@ -2268,7 +2367,7 @@ const ENY_CLEARANCE_RATIO = .3;
|
|
|
2268
2367
|
* for sizing the boundary knockout gap — no live text-measurement is
|
|
2269
2368
|
* available at generator time. */
|
|
2270
2369
|
const ENY_CHAR_ASPECT_RATIO = .6;
|
|
2271
|
-
const DEFAULT_SMOOTH_RESOLUTION$
|
|
2370
|
+
const DEFAULT_SMOOTH_RESOLUTION$9 = 16;
|
|
2272
2371
|
/**
|
|
2273
2372
|
* Composes an area's main label text from a `prefix` (e.g. `"AO"`) and a
|
|
2274
2373
|
* `designation` (e.g. `"GREEN"`), mirroring Phase Line's "PL" precedent: a
|
|
@@ -2305,7 +2404,8 @@ function composeAreaLabelTexts(textAmplifiers, prefix = "", separator = " ") {
|
|
|
2305
2404
|
* divide by zero). For a **concave** ring the area centroid can fall outside
|
|
2306
2405
|
* the shape; that limitation is accepted here, matching the doctrinal
|
|
2307
2406
|
* template's plain "center of the area" placement rather than a
|
|
2308
|
-
* point-in-polygon-safe visual center.
|
|
2407
|
+
* point-in-polygon-safe visual center. Exported for reuse by measures that
|
|
2408
|
+
* center a non-label glyph on the area (e.g. Airfield Zone's runway glyph).
|
|
2309
2409
|
*/
|
|
2310
2410
|
function ringCentroid(verts) {
|
|
2311
2411
|
const n = verts.length - 1;
|
|
@@ -2479,8 +2579,8 @@ function boundaryFeature(verts, gaps) {
|
|
|
2479
2579
|
* common (no `N`) case is unaffected.
|
|
2480
2580
|
*/
|
|
2481
2581
|
function labeledAreaFeatures(positions, options, texts) {
|
|
2482
|
-
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$
|
|
2483
|
-
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$
|
|
2582
|
+
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
2583
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
2484
2584
|
const labelFeatures = [];
|
|
2485
2585
|
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
|
|
2486
2586
|
features.push(...labelFeatures);
|
|
@@ -2490,44 +2590,492 @@ function labeledAreaFeatures(positions, options, texts) {
|
|
|
2490
2590
|
};
|
|
2491
2591
|
}
|
|
2492
2592
|
//#endregion
|
|
2493
|
-
//#region src/generators/
|
|
2494
|
-
/** Default options for the Area
|
|
2495
|
-
const
|
|
2496
|
-
smooth: false,
|
|
2497
|
-
smoothResolution: 16
|
|
2593
|
+
//#region src/generators/cm15-maneuver-areas/area.ts
|
|
2594
|
+
/** Default options for the Area control measure. */
|
|
2595
|
+
const DEFAULT_AREA_OPTIONS = {
|
|
2596
|
+
smooth: false,
|
|
2597
|
+
smoothResolution: 16
|
|
2598
|
+
};
|
|
2599
|
+
const AREA_METADATA = {
|
|
2600
|
+
id: "area",
|
|
2601
|
+
name: "Area",
|
|
2602
|
+
description: "A generic maneuver area enclosed by a boundary.",
|
|
2603
|
+
entity: "Maneuver Areas",
|
|
2604
|
+
entityType: "Area",
|
|
2605
|
+
value: "150100",
|
|
2606
|
+
minCoordinates: 3,
|
|
2607
|
+
geometry: "area",
|
|
2608
|
+
geometryTypes: [
|
|
2609
|
+
"Polygon",
|
|
2610
|
+
"MultiLineString",
|
|
2611
|
+
"Point"
|
|
2612
|
+
],
|
|
2613
|
+
drawRule: "Area1",
|
|
2614
|
+
capturesLabelSize: true,
|
|
2615
|
+
params: LABELED_AREA_PARAMS,
|
|
2616
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
2617
|
+
};
|
|
2618
|
+
/**
|
|
2619
|
+
* Creates an Area control measure: a closed area through `positions` labeled
|
|
2620
|
+
* at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
|
|
2621
|
+
* marker, per {@link labeledAreaFeatures}.
|
|
2622
|
+
*/
|
|
2623
|
+
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
2624
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
2625
|
+
}
|
|
2626
|
+
const AREA = defineControlMeasure({
|
|
2627
|
+
metadata: AREA_METADATA,
|
|
2628
|
+
generator: createArea,
|
|
2629
|
+
defaultOptions: DEFAULT_AREA_OPTIONS,
|
|
2630
|
+
rule: area1DrawRule,
|
|
2631
|
+
previewSample: {
|
|
2632
|
+
controlPoints: [
|
|
2633
|
+
[-1, -.5],
|
|
2634
|
+
[1, -.5],
|
|
2635
|
+
[1, .5],
|
|
2636
|
+
[-1, .5]
|
|
2637
|
+
],
|
|
2638
|
+
textAmplifiers: {},
|
|
2639
|
+
options: { labelSize: 96 }
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
//#endregion
|
|
2643
|
+
//#region src/generators/cm12-command-control-areas/area-of-operations.ts
|
|
2644
|
+
/** Default options for the Area of operations control measure. */
|
|
2645
|
+
const DEFAULT_AREA_OF_OPERATIONS_OPTIONS = {
|
|
2646
|
+
smooth: false,
|
|
2647
|
+
smoothResolution: 16
|
|
2648
|
+
};
|
|
2649
|
+
const AREA_OF_OPERATIONS_METADATA = {
|
|
2650
|
+
id: "area-of-operations",
|
|
2651
|
+
name: "Area of operations",
|
|
2652
|
+
description: "An operational area defined by a commander for land and naval forces, within which the assigned force conducts military operations.",
|
|
2653
|
+
entity: "Command and Control Areas",
|
|
2654
|
+
entityType: "Area of Operations",
|
|
2655
|
+
value: "120100",
|
|
2656
|
+
minCoordinates: 3,
|
|
2657
|
+
geometry: "area",
|
|
2658
|
+
geometryTypes: [
|
|
2659
|
+
"Polygon",
|
|
2660
|
+
"MultiLineString",
|
|
2661
|
+
"Point"
|
|
2662
|
+
],
|
|
2663
|
+
drawRule: "Area1",
|
|
2664
|
+
capturesLabelSize: true,
|
|
2665
|
+
params: LABELED_AREA_PARAMS,
|
|
2666
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2667
|
+
};
|
|
2668
|
+
/**
|
|
2669
|
+
* Creates an Area of operations control measure: a closed area through
|
|
2670
|
+
* `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
|
|
2671
|
+
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
2672
|
+
*/
|
|
2673
|
+
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
|
|
2674
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
|
|
2675
|
+
}
|
|
2676
|
+
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
2677
|
+
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
2678
|
+
generator: createAreaOfOperations,
|
|
2679
|
+
defaultOptions: DEFAULT_AREA_OF_OPERATIONS_OPTIONS,
|
|
2680
|
+
rule: area1DrawRule,
|
|
2681
|
+
previewSample: {
|
|
2682
|
+
controlPoints: [
|
|
2683
|
+
[-1, -.5],
|
|
2684
|
+
[1, -.5],
|
|
2685
|
+
[1, .5],
|
|
2686
|
+
[-1, .5]
|
|
2687
|
+
],
|
|
2688
|
+
textAmplifiers: { T: "GREEN" },
|
|
2689
|
+
options: { labelSize: 96 }
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
//#endregion
|
|
2693
|
+
//#region src/generators/cm12-command-control-areas/params.ts
|
|
2694
|
+
const SMOOTH_AREA_PARAMS = AREA_SMOOTH_PARAMS;
|
|
2695
|
+
const AREA_DESIGNATION_ONLY_AMPLIFIER = {
|
|
2696
|
+
key: "T",
|
|
2697
|
+
label: "Unique designation",
|
|
2698
|
+
description: "Field T — the area's unique designation.",
|
|
2699
|
+
maxLength: 21
|
|
2700
|
+
};
|
|
2701
|
+
//#endregion
|
|
2702
|
+
//#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
|
|
2703
|
+
/**
|
|
2704
|
+
* Default options for the Named area of interest control measure. The
|
|
2705
|
+
* standard's example renders a straight-sided polygon, so `smooth` defaults
|
|
2706
|
+
* off (unlike the rounded L-15 templates) while staying available.
|
|
2707
|
+
*/
|
|
2708
|
+
const DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS = {
|
|
2709
|
+
smooth: false,
|
|
2710
|
+
smoothResolution: 16
|
|
2711
|
+
};
|
|
2712
|
+
const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
2713
|
+
id: "named-area-of-interest",
|
|
2714
|
+
name: "Named area of interest",
|
|
2715
|
+
description: "A geographical area where information is gathered to satisfy specific intelligence requirements.",
|
|
2716
|
+
entity: "Command and Control Areas",
|
|
2717
|
+
entityType: "Named Area of Interest",
|
|
2718
|
+
value: "120200",
|
|
2719
|
+
minCoordinates: 3,
|
|
2720
|
+
geometry: "area",
|
|
2721
|
+
geometryTypes: ["Polygon", "Point"],
|
|
2722
|
+
drawRule: "Area1",
|
|
2723
|
+
capturesLabelSize: true,
|
|
2724
|
+
params: SMOOTH_AREA_PARAMS,
|
|
2725
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
2726
|
+
};
|
|
2727
|
+
/**
|
|
2728
|
+
* Creates a Named area of interest control measure: a closed area through
|
|
2729
|
+
* `positions` labeled at its center with "NAI "-prefixed `T` (Field T) —
|
|
2730
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
2731
|
+
*/
|
|
2732
|
+
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
2733
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("NAI", textAmplifiers.T) });
|
|
2734
|
+
}
|
|
2735
|
+
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
2736
|
+
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
2737
|
+
generator: createNamedAreaOfInterest,
|
|
2738
|
+
defaultOptions: DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS,
|
|
2739
|
+
rule: area1DrawRule,
|
|
2740
|
+
previewSample: {
|
|
2741
|
+
controlPoints: [
|
|
2742
|
+
[-1, -.5],
|
|
2743
|
+
[1, -.5],
|
|
2744
|
+
[1, .5],
|
|
2745
|
+
[-1, .5]
|
|
2746
|
+
],
|
|
2747
|
+
textAmplifiers: { T: "1" },
|
|
2748
|
+
options: { labelSize: 96 }
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
//#endregion
|
|
2752
|
+
//#region src/generators/cm12-command-control-areas/target-area-of-interest.ts
|
|
2753
|
+
/**
|
|
2754
|
+
* Default options for the Target area of interest control measure. The
|
|
2755
|
+
* standard's example renders a straight-sided polygon, so `smooth` defaults
|
|
2756
|
+
* off (unlike the rounded L-15 templates) while staying available.
|
|
2757
|
+
*/
|
|
2758
|
+
const DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS = {
|
|
2759
|
+
smooth: false,
|
|
2760
|
+
smoothResolution: 16
|
|
2761
|
+
};
|
|
2762
|
+
const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
2763
|
+
id: "target-area-of-interest",
|
|
2764
|
+
name: "Target area of interest",
|
|
2765
|
+
description: "The geographical area where high-value targets can be acquired and engaged by friendly forces.",
|
|
2766
|
+
entity: "Command and Control Areas",
|
|
2767
|
+
entityType: "Target Area of Interest",
|
|
2768
|
+
value: "120300",
|
|
2769
|
+
minCoordinates: 3,
|
|
2770
|
+
geometry: "area",
|
|
2771
|
+
geometryTypes: ["Polygon", "Point"],
|
|
2772
|
+
drawRule: "Area1",
|
|
2773
|
+
capturesLabelSize: true,
|
|
2774
|
+
params: SMOOTH_AREA_PARAMS,
|
|
2775
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
2776
|
+
};
|
|
2777
|
+
/**
|
|
2778
|
+
* Creates a Target area of interest control measure: a closed area through
|
|
2779
|
+
* `positions` labeled at its center with "TAI "-prefixed `T` (Field T) —
|
|
2780
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
2781
|
+
*/
|
|
2782
|
+
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
|
|
2783
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("TAI", textAmplifiers.T) });
|
|
2784
|
+
}
|
|
2785
|
+
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
2786
|
+
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
2787
|
+
generator: createTargetAreaOfInterest,
|
|
2788
|
+
defaultOptions: DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS,
|
|
2789
|
+
rule: area1DrawRule,
|
|
2790
|
+
previewSample: {
|
|
2791
|
+
controlPoints: [
|
|
2792
|
+
[-1, -.5],
|
|
2793
|
+
[1, -.5],
|
|
2794
|
+
[1, .5],
|
|
2795
|
+
[-1, .5]
|
|
2796
|
+
],
|
|
2797
|
+
textAmplifiers: { T: "YUKON" },
|
|
2798
|
+
options: { labelSize: 96 }
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
//#endregion
|
|
2802
|
+
//#region src/generators/cm12-command-control-areas/airfield-zone.ts
|
|
2803
|
+
const DEFAULT_SMOOTH_RESOLUTION$8 = 16;
|
|
2804
|
+
/** Long runway line's tilt off the horizontal, in radians (+15°). */
|
|
2805
|
+
const LONG_LINE_ANGLE = 15 * Math.PI / 180;
|
|
2806
|
+
/** Short runway line's tilt off the horizontal, in radians (+50°). */
|
|
2807
|
+
const SHORT_LINE_ANGLE = 50 * Math.PI / 180;
|
|
2808
|
+
/** Short line's half-length as a ratio of the long line's half-length. */
|
|
2809
|
+
const SHORT_LINE_RATIO = .55;
|
|
2810
|
+
/** Field H's offset east of the area's max-x edge, as a ratio of the resolved label offset. */
|
|
2811
|
+
const H_LABEL_OFFSET_RATIO = .75;
|
|
2812
|
+
/**
|
|
2813
|
+
* Default options for the Airfield zone control measure. The standard's
|
|
2814
|
+
* example is a rounded blob, so `smooth` defaults on.
|
|
2815
|
+
*/
|
|
2816
|
+
const DEFAULT_AIRFIELD_ZONE_OPTIONS = {
|
|
2817
|
+
smooth: true,
|
|
2818
|
+
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$8,
|
|
2819
|
+
glyphSizeRatio: .55
|
|
2820
|
+
};
|
|
2821
|
+
const AIRFIELD_ZONE_METADATA = {
|
|
2822
|
+
id: "airfield-zone",
|
|
2823
|
+
name: "Airfield zone",
|
|
2824
|
+
description: "A zone enclosing an airfield, marked with a runway glyph; Field H carries the airfield type, runway length and other pertinent information.",
|
|
2825
|
+
entity: "Command and Control Areas",
|
|
2826
|
+
entityType: "Airfield Zone",
|
|
2827
|
+
value: "120400",
|
|
2828
|
+
minCoordinates: 3,
|
|
2829
|
+
geometry: "area",
|
|
2830
|
+
geometryTypes: [
|
|
2831
|
+
"Polygon",
|
|
2832
|
+
"MultiLineString",
|
|
2833
|
+
"Point"
|
|
2834
|
+
],
|
|
2835
|
+
drawRule: "Area1",
|
|
2836
|
+
capturesLabelSize: true,
|
|
2837
|
+
params: [...SMOOTH_AREA_PARAMS, {
|
|
2838
|
+
key: "glyphSizeRatio",
|
|
2839
|
+
label: "Runway size",
|
|
2840
|
+
description: "Size of the runway glyph as a ratio of the area's width.",
|
|
2841
|
+
type: "number",
|
|
2842
|
+
min: .2,
|
|
2843
|
+
max: .9,
|
|
2844
|
+
step: .05
|
|
2845
|
+
}],
|
|
2846
|
+
textAmplifiers: [{
|
|
2847
|
+
key: "H",
|
|
2848
|
+
label: "Additional information",
|
|
2849
|
+
description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
|
|
2850
|
+
maxLength: 20
|
|
2851
|
+
}]
|
|
2852
|
+
};
|
|
2853
|
+
/**
|
|
2854
|
+
* Creates an Airfield zone control measure: a closed area through
|
|
2855
|
+
* `positions` with a runway glyph (two crossed lines forming a shallow
|
|
2856
|
+
* asymmetric X) centered on the ring, and Field H placed outside the area to
|
|
2857
|
+
* the east when set.
|
|
2858
|
+
*
|
|
2859
|
+
* @param positions - Area anchor points (≥3); the input contract is enforced
|
|
2860
|
+
* at the render seam (ADR-0014).
|
|
2861
|
+
* @param options - {@link AirfieldZoneOptions}.
|
|
2862
|
+
* @param textAmplifiers - Field H only.
|
|
2863
|
+
*/
|
|
2864
|
+
function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
2865
|
+
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
2866
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
2867
|
+
const features = [boundaryFeature(verts, [])];
|
|
2868
|
+
let minX = Infinity;
|
|
2869
|
+
let maxX = -Infinity;
|
|
2870
|
+
for (const [x] of verts) {
|
|
2871
|
+
if (x < minX) minX = x;
|
|
2872
|
+
if (x > maxX) maxX = x;
|
|
2873
|
+
}
|
|
2874
|
+
const bboxWidth = maxX - minX;
|
|
2875
|
+
const [cx, cy] = ringCentroid(verts);
|
|
2876
|
+
if (bboxWidth >= 1e-6) {
|
|
2877
|
+
const longHalf = .5 * glyphSizeRatio * bboxWidth;
|
|
2878
|
+
const shortHalf = longHalf * SHORT_LINE_RATIO;
|
|
2879
|
+
const center = [cx, cy];
|
|
2880
|
+
const longDir = [Math.cos(LONG_LINE_ANGLE), Math.sin(LONG_LINE_ANGLE)];
|
|
2881
|
+
const shortDir = [Math.cos(SHORT_LINE_ANGLE), Math.sin(SHORT_LINE_ANGLE)];
|
|
2882
|
+
const along = (dir, half) => [[center[0] - dir[0] * half, center[1] - dir[1] * half], [center[0] + dir[0] * half, center[1] + dir[1] * half]];
|
|
2883
|
+
const [longA, longB] = along(longDir, longHalf);
|
|
2884
|
+
const [shortA, shortB] = along(shortDir, shortHalf);
|
|
2885
|
+
features.push({
|
|
2886
|
+
type: "Feature",
|
|
2887
|
+
properties: {},
|
|
2888
|
+
geometry: {
|
|
2889
|
+
type: "MultiLineString",
|
|
2890
|
+
coordinates: [[unproject(longA[0], longA[1]), unproject(longB[0], longB[1])], [unproject(shortA[0], shortA[1]), unproject(shortB[0], shortB[1])]]
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
if (textAmplifiers.H) {
|
|
2895
|
+
const labelFeatures = [];
|
|
2896
|
+
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start");
|
|
2897
|
+
features.push(...labelFeatures);
|
|
2898
|
+
}
|
|
2899
|
+
return {
|
|
2900
|
+
type: "FeatureCollection",
|
|
2901
|
+
features
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
const AIRFIELD_ZONE = defineControlMeasure({
|
|
2905
|
+
metadata: AIRFIELD_ZONE_METADATA,
|
|
2906
|
+
generator: createAirfieldZone,
|
|
2907
|
+
defaultOptions: DEFAULT_AIRFIELD_ZONE_OPTIONS,
|
|
2908
|
+
rule: area1DrawRule,
|
|
2909
|
+
previewSample: {
|
|
2910
|
+
controlPoints: [
|
|
2911
|
+
[-1, -.5],
|
|
2912
|
+
[1, -.5],
|
|
2913
|
+
[1, .5],
|
|
2914
|
+
[-1, .5]
|
|
2915
|
+
],
|
|
2916
|
+
textAmplifiers: { H: "570M" },
|
|
2917
|
+
options: { labelSize: 96 }
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
//#endregion
|
|
2921
|
+
//#region src/generators/cm12-command-control-areas/echelon-labeled-area.ts
|
|
2922
|
+
const DEFAULT_SMOOTH_RESOLUTION$7 = 16;
|
|
2923
|
+
/** Default options shared by Base Camp and Guerrilla Base — echelon off, boundary rounded. */
|
|
2924
|
+
const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
2925
|
+
echelon: "none",
|
|
2926
|
+
echelonSize: 750,
|
|
2927
|
+
echelonSizePixels: 16,
|
|
2928
|
+
echelonPadding: .3,
|
|
2929
|
+
echelonPosition: 0,
|
|
2930
|
+
smooth: true,
|
|
2931
|
+
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$7
|
|
2932
|
+
};
|
|
2933
|
+
/**
|
|
2934
|
+
* Builds the shared Base Camp / Guerrilla Base geometry: a closed area
|
|
2935
|
+
* through `positions` carrying the full L-15 text amplifier template
|
|
2936
|
+
* ({@link pushAreaLabels}) plus an optional Field B echelon glyph straddling
|
|
2937
|
+
* the boundary ({@link placeEchelon}, mirroring Battle Position). The
|
|
2938
|
+
* echelon's masking gap and any Field N "ENY" masking gap are concatenated
|
|
2939
|
+
* before carving the boundary, so either (or both) opens it into a gapped
|
|
2940
|
+
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
2941
|
+
*/
|
|
2942
|
+
function echelonLabeledAreaFeatures(positions, options, texts) {
|
|
2943
|
+
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;
|
|
2944
|
+
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
2945
|
+
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
2946
|
+
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
2947
|
+
const labelFeatures = [];
|
|
2948
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
|
|
2949
|
+
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
2950
|
+
if (strokes.length > 0) features.push({
|
|
2951
|
+
type: "Feature",
|
|
2952
|
+
properties: { part: "echelon" },
|
|
2953
|
+
geometry: {
|
|
2954
|
+
type: "MultiLineString",
|
|
2955
|
+
coordinates: strokes
|
|
2956
|
+
}
|
|
2957
|
+
});
|
|
2958
|
+
if (fills.length > 0) features.push({
|
|
2959
|
+
type: "Feature",
|
|
2960
|
+
properties: {
|
|
2961
|
+
part: "echelon",
|
|
2962
|
+
fill: true
|
|
2963
|
+
},
|
|
2964
|
+
geometry: {
|
|
2965
|
+
type: "MultiPolygon",
|
|
2966
|
+
coordinates: fills.map((ring) => [ring])
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
features.push(...labelFeatures);
|
|
2970
|
+
return {
|
|
2971
|
+
type: "FeatureCollection",
|
|
2972
|
+
features
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* `freezeOrientation` hook factory instance shared by Base Camp and
|
|
2977
|
+
* Guerrilla Base — see {@link makeFreezeEchelonAnchor} (ADR-0023) for the
|
|
2978
|
+
* mechanism; both measures default to `echelon: "none"`, so unlike Battle
|
|
2979
|
+
* Position's own instance (battlePosition.ts) freezing is a no-op until a
|
|
2980
|
+
* user explicitly picks an echelon.
|
|
2981
|
+
*/
|
|
2982
|
+
const freezeEchelonLabeledAreaAnchor = makeFreezeEchelonAnchor({
|
|
2983
|
+
echelon: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon,
|
|
2984
|
+
smooth: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth,
|
|
2985
|
+
smoothResolution: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution
|
|
2986
|
+
});
|
|
2987
|
+
//#endregion
|
|
2988
|
+
//#region src/generators/cm12-command-control-areas/base-camp.ts
|
|
2989
|
+
/** Default options for the Base camp control measure. */
|
|
2990
|
+
const DEFAULT_BASE_CAMP_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
2991
|
+
const BASE_CAMP_METADATA = {
|
|
2992
|
+
id: "base-camp",
|
|
2993
|
+
name: "Base camp",
|
|
2994
|
+
description: "An evolving military facility that supports military operations of a deployed unit and provides the necessary support and services for sustained operations.",
|
|
2995
|
+
entity: "Command and Control Areas",
|
|
2996
|
+
entityType: "Base Camp",
|
|
2997
|
+
value: "120500",
|
|
2998
|
+
minCoordinates: 3,
|
|
2999
|
+
geometry: "area",
|
|
3000
|
+
geometryTypes: [
|
|
3001
|
+
"Polygon",
|
|
3002
|
+
"MultiLineString",
|
|
3003
|
+
"MultiPolygon",
|
|
3004
|
+
"Point"
|
|
3005
|
+
],
|
|
3006
|
+
drawRule: "Area1",
|
|
3007
|
+
capturesLabelSize: true,
|
|
3008
|
+
params: ECHELON_AREA_PARAMS,
|
|
3009
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2498
3010
|
};
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3011
|
+
/**
|
|
3012
|
+
* Creates a Base camp control measure: a closed area through `positions`
|
|
3013
|
+
* labeled at its center with "BC "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
|
|
3014
|
+
* amplifier rows, and an optional Field B echelon glyph straddling the
|
|
3015
|
+
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
3016
|
+
*/
|
|
3017
|
+
function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
|
|
3018
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
|
|
3019
|
+
}
|
|
3020
|
+
const BASE_CAMP = defineControlMeasure({
|
|
3021
|
+
metadata: BASE_CAMP_METADATA,
|
|
3022
|
+
generator: createBaseCamp,
|
|
3023
|
+
defaultOptions: DEFAULT_BASE_CAMP_OPTIONS,
|
|
3024
|
+
rule: area1DrawRule,
|
|
3025
|
+
freezeOrientation: freezeEchelonLabeledAreaAnchor,
|
|
3026
|
+
previewSample: {
|
|
3027
|
+
controlPoints: [
|
|
3028
|
+
[-1, -.5],
|
|
3029
|
+
[1, -.5],
|
|
3030
|
+
[1, .5],
|
|
3031
|
+
[-1, .5]
|
|
3032
|
+
],
|
|
3033
|
+
textAmplifiers: { T: "LEATHERNECK" },
|
|
3034
|
+
options: {
|
|
3035
|
+
smooth: true,
|
|
3036
|
+
labelSize: 96
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
//#endregion
|
|
3041
|
+
//#region src/generators/cm12-command-control-areas/guerrilla-base.ts
|
|
3042
|
+
/** Default options for the Guerrilla base control measure. */
|
|
3043
|
+
const DEFAULT_GUERRILLA_BASE_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
3044
|
+
const GUERRILLA_BASE_METADATA = {
|
|
3045
|
+
id: "guerrilla-base",
|
|
3046
|
+
name: "Guerrilla base",
|
|
3047
|
+
description: "A temporary site where guerrilla installations, headquarters, and units are located.",
|
|
2503
3048
|
entity: "Command and Control Areas",
|
|
2504
|
-
entityType: "
|
|
2505
|
-
value: "
|
|
3049
|
+
entityType: "Guerrilla Base",
|
|
3050
|
+
value: "120600",
|
|
2506
3051
|
minCoordinates: 3,
|
|
2507
3052
|
geometry: "area",
|
|
2508
3053
|
geometryTypes: [
|
|
2509
3054
|
"Polygon",
|
|
2510
3055
|
"MultiLineString",
|
|
3056
|
+
"MultiPolygon",
|
|
2511
3057
|
"Point"
|
|
2512
3058
|
],
|
|
2513
3059
|
drawRule: "Area1",
|
|
2514
3060
|
capturesLabelSize: true,
|
|
2515
|
-
params:
|
|
3061
|
+
params: ECHELON_AREA_PARAMS,
|
|
2516
3062
|
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
2517
3063
|
};
|
|
2518
3064
|
/**
|
|
2519
|
-
* Creates
|
|
2520
|
-
* `positions` labeled at its center with "
|
|
2521
|
-
* `H`/`W`/`W1`/`N` amplifier rows,
|
|
3065
|
+
* Creates a Guerrilla base control measure: a closed area through
|
|
3066
|
+
* `positions` labeled at its center with "GB "-prefixed `T` (Field T) plus
|
|
3067
|
+
* `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
|
|
3068
|
+
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
2522
3069
|
*/
|
|
2523
|
-
function
|
|
2524
|
-
return
|
|
3070
|
+
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
|
|
3071
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
|
|
2525
3072
|
}
|
|
2526
|
-
const
|
|
2527
|
-
metadata:
|
|
2528
|
-
generator:
|
|
2529
|
-
defaultOptions:
|
|
3073
|
+
const GUERRILLA_BASE = defineControlMeasure({
|
|
3074
|
+
metadata: GUERRILLA_BASE_METADATA,
|
|
3075
|
+
generator: createGuerrillaBase,
|
|
3076
|
+
defaultOptions: DEFAULT_GUERRILLA_BASE_OPTIONS,
|
|
2530
3077
|
rule: area1DrawRule,
|
|
3078
|
+
freezeOrientation: freezeEchelonLabeledAreaAnchor,
|
|
2531
3079
|
previewSample: {
|
|
2532
3080
|
controlPoints: [
|
|
2533
3081
|
[-1, -.5],
|
|
@@ -2535,14 +3083,67 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
|
2535
3083
|
[1, .5],
|
|
2536
3084
|
[-1, .5]
|
|
2537
3085
|
],
|
|
2538
|
-
textAmplifiers: { T: "
|
|
3086
|
+
textAmplifiers: { T: "GUEVARA" },
|
|
2539
3087
|
options: {
|
|
2540
|
-
|
|
2541
|
-
|
|
3088
|
+
smooth: true,
|
|
3089
|
+
labelSize: 96
|
|
2542
3090
|
}
|
|
2543
3091
|
}
|
|
2544
3092
|
});
|
|
2545
3093
|
//#endregion
|
|
3094
|
+
//#region src/generators/cm12-command-control-areas/generic-c2-area.ts
|
|
3095
|
+
/**
|
|
3096
|
+
* Default options for the Generic command and control area control measure.
|
|
3097
|
+
* The standard's example is a rounded blob, so `smooth` defaults on.
|
|
3098
|
+
*/
|
|
3099
|
+
const DEFAULT_GENERIC_C2_AREA_OPTIONS = {
|
|
3100
|
+
smooth: true,
|
|
3101
|
+
smoothResolution: 16
|
|
3102
|
+
};
|
|
3103
|
+
const GENERIC_C2_AREA_METADATA = {
|
|
3104
|
+
id: "generic-c2-area",
|
|
3105
|
+
name: "Generic command and control area",
|
|
3106
|
+
description: "A general-purpose command and control area carrying a designation, additional information and an effective date-time-group window.",
|
|
3107
|
+
entity: "Command and Control Areas",
|
|
3108
|
+
entityType: "Generic",
|
|
3109
|
+
value: "120700",
|
|
3110
|
+
minCoordinates: 3,
|
|
3111
|
+
geometry: "area",
|
|
3112
|
+
geometryTypes: [
|
|
3113
|
+
"Polygon",
|
|
3114
|
+
"MultiLineString",
|
|
3115
|
+
"Point"
|
|
3116
|
+
],
|
|
3117
|
+
drawRule: "Area1",
|
|
3118
|
+
capturesLabelSize: true,
|
|
3119
|
+
params: LABELED_AREA_PARAMS,
|
|
3120
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS
|
|
3121
|
+
};
|
|
3122
|
+
/**
|
|
3123
|
+
* Creates a Generic command and control area control measure: a closed area
|
|
3124
|
+
* through `positions` labeled at its center with the bare `T` (Field T) plus
|
|
3125
|
+
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
3126
|
+
*/
|
|
3127
|
+
function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
|
|
3128
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
|
|
3129
|
+
}
|
|
3130
|
+
const GENERIC_C2_AREA = defineControlMeasure({
|
|
3131
|
+
metadata: GENERIC_C2_AREA_METADATA,
|
|
3132
|
+
generator: createGenericC2Area,
|
|
3133
|
+
defaultOptions: DEFAULT_GENERIC_C2_AREA_OPTIONS,
|
|
3134
|
+
rule: area1DrawRule,
|
|
3135
|
+
previewSample: {
|
|
3136
|
+
controlPoints: [
|
|
3137
|
+
[-1, -.5],
|
|
3138
|
+
[1, -.5],
|
|
3139
|
+
[1, .5],
|
|
3140
|
+
[-1, .5]
|
|
3141
|
+
],
|
|
3142
|
+
textAmplifiers: { T: "RIOT FOODS" },
|
|
3143
|
+
options: { labelSize: 96 }
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
//#endregion
|
|
2546
3147
|
//#region src/generators/cm15-maneuver-areas/assemblyArea.ts
|
|
2547
3148
|
/** Default options for the Assembly area control measure. */
|
|
2548
3149
|
const DEFAULT_ASSEMBLY_AREA_OPTIONS = {
|
|
@@ -2589,31 +3190,13 @@ const ASSEMBLY_AREA = defineControlMeasure({
|
|
|
2589
3190
|
[-1, .5]
|
|
2590
3191
|
],
|
|
2591
3192
|
textAmplifiers: { T: "BLUE" },
|
|
2592
|
-
options: {
|
|
2593
|
-
labelSizePixels: 12,
|
|
2594
|
-
labelSizeResolution: 8
|
|
2595
|
-
}
|
|
3193
|
+
options: { labelSize: 96 }
|
|
2596
3194
|
}
|
|
2597
3195
|
});
|
|
2598
3196
|
//#endregion
|
|
2599
3197
|
//#region src/generators/cm29-protection-lines/params.ts
|
|
2600
3198
|
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
|
-
},
|
|
3199
|
+
...SMOOTH_LINE_PARAMS,
|
|
2617
3200
|
{
|
|
2618
3201
|
key: "toothSizePixels",
|
|
2619
3202
|
label: "Tooth size",
|
|
@@ -2636,6 +3219,7 @@ const FORTIFIED_PARAMS = [
|
|
|
2636
3219
|
}
|
|
2637
3220
|
];
|
|
2638
3221
|
const ANTITANK_DITCH_PARAMS = [
|
|
3222
|
+
...SMOOTH_LINE_PARAMS,
|
|
2639
3223
|
{
|
|
2640
3224
|
key: "toothHeightPixels",
|
|
2641
3225
|
label: "Tooth height",
|
|
@@ -2685,7 +3269,9 @@ const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
|
|
|
2685
3269
|
const DEFAULT_ANTITANK_DITCH_OPTIONS = {
|
|
2686
3270
|
toothHeight: DEFAULT_TOOTH_HEIGHT,
|
|
2687
3271
|
toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
|
|
2688
|
-
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1
|
|
3272
|
+
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1,
|
|
3273
|
+
smooth: false,
|
|
3274
|
+
smoothResolution: 16
|
|
2689
3275
|
};
|
|
2690
3276
|
const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
|
|
2691
3277
|
id: "antitank-ditch-under-construction",
|
|
@@ -2746,13 +3332,21 @@ function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidth
|
|
|
2746
3332
|
}
|
|
2747
3333
|
return triangles;
|
|
2748
3334
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
3335
|
+
/**
|
|
3336
|
+
* Projects `positions` and, when `options.smooth` is set, curves the result
|
|
3337
|
+
* through the control points (centripetal Catmull-Rom) before the teeth are
|
|
3338
|
+
* traced — matching Fortified Line's base-path smoothing. Returns the input
|
|
3339
|
+
* unchanged (no project/unproject round trip) when smoothing is off.
|
|
3340
|
+
*/
|
|
3341
|
+
function smoothedDitchBasePath(positions, options) {
|
|
3342
|
+
return smoothLineVerts(positions.map((position) => project(position[0], position[1])), options, 16);
|
|
3343
|
+
}
|
|
3344
|
+
function buildAntitankDitchTriangles(basePath, options) {
|
|
3345
|
+
return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
2753
3346
|
}
|
|
2754
3347
|
function createAntitankDitchUnderConstruction(positions, options = {}) {
|
|
2755
|
-
const
|
|
3348
|
+
const basePath = smoothedDitchBasePath(positions, options);
|
|
3349
|
+
const triangles = buildAntitankDitchTriangles(basePath, options);
|
|
2756
3350
|
return {
|
|
2757
3351
|
type: "FeatureCollection",
|
|
2758
3352
|
features: [{
|
|
@@ -2760,7 +3354,7 @@ function createAntitankDitchUnderConstruction(positions, options = {}) {
|
|
|
2760
3354
|
properties: { part: "ditch" },
|
|
2761
3355
|
geometry: {
|
|
2762
3356
|
type: "MultiLineString",
|
|
2763
|
-
coordinates: [positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
|
|
3357
|
+
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
3358
|
}
|
|
2765
3359
|
}]
|
|
2766
3360
|
};
|
|
@@ -2776,7 +3370,7 @@ function createAntitankDitchCompleted(positions, options = {}) {
|
|
|
2776
3370
|
},
|
|
2777
3371
|
geometry: {
|
|
2778
3372
|
type: "MultiPolygon",
|
|
2779
|
-
coordinates: buildAntitankDitchTriangles(positions, options).map((triangle) => {
|
|
3373
|
+
coordinates: buildAntitankDitchTriangles(smoothedDitchBasePath(positions, options), options).map((triangle) => {
|
|
2780
3374
|
return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
|
|
2781
3375
|
})
|
|
2782
3376
|
}
|
|
@@ -2860,7 +3454,7 @@ function createAntitankWall(positions, options = {}) {
|
|
|
2860
3454
|
properties: { part: "wall" },
|
|
2861
3455
|
geometry: {
|
|
2862
3456
|
type: "MultiLineString",
|
|
2863
|
-
coordinates: [generateAntitankWallPoints(positions.map((position) => project(position[0], position[1])), toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
|
|
3457
|
+
coordinates: [generateAntitankWallPoints(smoothLineVerts(positions.map((position) => project(position[0], position[1])), options, 16), toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
|
|
2864
3458
|
}
|
|
2865
3459
|
}]
|
|
2866
3460
|
};
|
|
@@ -3255,70 +3849,7 @@ const BATTLE_POSITION_METADATA = {
|
|
|
3255
3849
|
],
|
|
3256
3850
|
drawRule: "Area1",
|
|
3257
3851
|
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
|
-
],
|
|
3852
|
+
params: ECHELON_AREA_PARAMS,
|
|
3322
3853
|
textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
|
|
3323
3854
|
};
|
|
3324
3855
|
/**
|
|
@@ -3380,18 +3911,17 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
|
|
|
3380
3911
|
* Point: pins the echelon anchor to its current bottom-edge fraction so a
|
|
3381
3912
|
* subsequent rigid rotation of `controlPoints` (e.g. the transform box's
|
|
3382
3913
|
* 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
|
-
}
|
|
3914
|
+
* rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
|
|
3915
|
+
* factory every echelon-carrying area measure (Battle Position, Strong
|
|
3916
|
+
* Point, Base Camp, Guerrilla Base) builds its own instance from — this
|
|
3917
|
+
* export just binds Battle Position's own defaults, keeping strongPoint.ts's
|
|
3918
|
+
* existing import (and any other importer) unchanged.
|
|
3919
|
+
*/
|
|
3920
|
+
const freezeEchelonAnchor = makeFreezeEchelonAnchor({
|
|
3921
|
+
echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
|
|
3922
|
+
smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
|
|
3923
|
+
smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
|
|
3924
|
+
});
|
|
3395
3925
|
const BATTLE_POSITION = defineControlMeasure({
|
|
3396
3926
|
metadata: BATTLE_POSITION_METADATA,
|
|
3397
3927
|
generator: createBattlePosition,
|
|
@@ -3409,8 +3939,7 @@ const BATTLE_POSITION = defineControlMeasure({
|
|
|
3409
3939
|
options: {
|
|
3410
3940
|
echelonSize: 200,
|
|
3411
3941
|
smooth: true,
|
|
3412
|
-
|
|
3413
|
-
labelSizeResolution: 8
|
|
3942
|
+
labelSize: 96
|
|
3414
3943
|
}
|
|
3415
3944
|
}
|
|
3416
3945
|
});
|
|
@@ -3762,7 +4291,6 @@ const BOUNDARY_METADATA = {
|
|
|
3762
4291
|
"Point"
|
|
3763
4292
|
],
|
|
3764
4293
|
drawRule: "Line1",
|
|
3765
|
-
capturesLabelSize: true,
|
|
3766
4294
|
params: [
|
|
3767
4295
|
{
|
|
3768
4296
|
key: "smooth",
|
|
@@ -3896,7 +4424,7 @@ const BOUNDARY_METADATA = {
|
|
|
3896
4424
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
3897
4425
|
*/
|
|
3898
4426
|
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
3899
|
-
const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel,
|
|
4427
|
+
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
4428
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
3901
4429
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
3902
4430
|
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
@@ -3906,14 +4434,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
3906
4434
|
h = Math.max(EPSILON, h);
|
|
3907
4435
|
const reps = Math.max(1, Math.round(labelRepetitions));
|
|
3908
4436
|
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
|
-
}
|
|
4437
|
+
const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
|
|
3917
4438
|
const rawVerts = positions.map((p) => project(p[0], p[1]));
|
|
3918
4439
|
const verts = smooth ? catmullRom(rawVerts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$4)) : rawVerts;
|
|
3919
4440
|
const glyphStrokes = [];
|
|
@@ -4018,13 +4539,24 @@ const BOUNDARY = defineControlMeasure({
|
|
|
4018
4539
|
rule: line1DrawRule,
|
|
4019
4540
|
previewSample: {
|
|
4020
4541
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4021
|
-
options: {
|
|
4542
|
+
options: {
|
|
4543
|
+
echelonSize: 375,
|
|
4544
|
+
labelPadding: .5
|
|
4545
|
+
},
|
|
4546
|
+
textAmplifiers: {
|
|
4547
|
+
T: "2INF",
|
|
4548
|
+
T1: "3INF"
|
|
4549
|
+
}
|
|
4022
4550
|
}
|
|
4023
4551
|
});
|
|
4024
4552
|
//#endregion
|
|
4025
4553
|
//#region src/generators/cm11-command-control-lines/light-line.ts
|
|
4026
4554
|
/** Default options for the Light line control measure. */
|
|
4027
|
-
const DEFAULT_LIGHT_LINE_OPTIONS = {
|
|
4555
|
+
const DEFAULT_LIGHT_LINE_OPTIONS = {
|
|
4556
|
+
includePrefix: true,
|
|
4557
|
+
smooth: false,
|
|
4558
|
+
smoothResolution: 12
|
|
4559
|
+
};
|
|
4028
4560
|
const LIGHT_LINE_METADATA = {
|
|
4029
4561
|
id: "light-line",
|
|
4030
4562
|
name: "Light line",
|
|
@@ -4038,13 +4570,15 @@ const LIGHT_LINE_METADATA = {
|
|
|
4038
4570
|
drawRule: "Line1",
|
|
4039
4571
|
capturesLabelSize: true,
|
|
4040
4572
|
params: [
|
|
4573
|
+
...SMOOTH_LINE_PARAMS,
|
|
4041
4574
|
{
|
|
4042
4575
|
key: "phaseLineName",
|
|
4043
4576
|
label: "Phase line name",
|
|
4044
4577
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4045
4578
|
type: "text",
|
|
4046
4579
|
placeholder: "DAVID",
|
|
4047
|
-
maxLength: 21
|
|
4580
|
+
maxLength: 21,
|
|
4581
|
+
field: "T"
|
|
4048
4582
|
},
|
|
4049
4583
|
{
|
|
4050
4584
|
key: "includePrefix",
|
|
@@ -4082,16 +4616,17 @@ const LIGHT_LINE = defineControlMeasure({
|
|
|
4082
4616
|
rule: line1DrawRule,
|
|
4083
4617
|
previewSample: {
|
|
4084
4618
|
controlPoints: [[-1, 0], [1, 0]],
|
|
4085
|
-
options: {
|
|
4086
|
-
labelSizePixels: 12,
|
|
4087
|
-
labelSizeResolution: 8
|
|
4088
|
-
}
|
|
4619
|
+
options: { labelSize: 96 }
|
|
4089
4620
|
}
|
|
4090
4621
|
});
|
|
4091
4622
|
//#endregion
|
|
4092
4623
|
//#region src/generators/cm11-command-control-lines/engineer-work-line.ts
|
|
4093
4624
|
/** Default options for the Engineer work line control measure. */
|
|
4094
|
-
const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = {
|
|
4625
|
+
const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = {
|
|
4626
|
+
includePrefix: true,
|
|
4627
|
+
smooth: false,
|
|
4628
|
+
smoothResolution: 12
|
|
4629
|
+
};
|
|
4095
4630
|
const ENGINEER_WORK_LINE_METADATA = {
|
|
4096
4631
|
id: "engineer-work-line",
|
|
4097
4632
|
name: "Engineer work line",
|
|
@@ -4105,13 +4640,15 @@ const ENGINEER_WORK_LINE_METADATA = {
|
|
|
4105
4640
|
drawRule: "Line1",
|
|
4106
4641
|
capturesLabelSize: true,
|
|
4107
4642
|
params: [
|
|
4643
|
+
...SMOOTH_LINE_PARAMS,
|
|
4108
4644
|
{
|
|
4109
4645
|
key: "phaseLineName",
|
|
4110
4646
|
label: "Phase line name",
|
|
4111
4647
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4112
4648
|
type: "text",
|
|
4113
4649
|
placeholder: "DAVID",
|
|
4114
|
-
maxLength: 21
|
|
4650
|
+
maxLength: 21,
|
|
4651
|
+
field: "T"
|
|
4115
4652
|
},
|
|
4116
4653
|
{
|
|
4117
4654
|
key: "includePrefix",
|
|
@@ -4173,7 +4710,7 @@ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
|
|
|
4173
4710
|
placement: "above",
|
|
4174
4711
|
...options
|
|
4175
4712
|
}, "EWL").features];
|
|
4176
|
-
const { segments, totalLength } = polylineSegments(positions.map((p) => project(p[0], p[1])));
|
|
4713
|
+
const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
|
|
4177
4714
|
const frame = pointAlongPolyline(segments, totalLength, .5);
|
|
4178
4715
|
if (frame) {
|
|
4179
4716
|
const units = [{
|
|
@@ -4208,21 +4745,20 @@ const ENGINEER_WORK_LINE = defineControlMeasure({
|
|
|
4208
4745
|
previewSample: {
|
|
4209
4746
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4210
4747
|
textAmplifiers: {
|
|
4211
|
-
T: "
|
|
4212
|
-
|
|
4213
|
-
T1: "127EN BN",
|
|
4214
|
-
AS1: "USA"
|
|
4748
|
+
T: "2BN",
|
|
4749
|
+
T1: "3BN"
|
|
4215
4750
|
},
|
|
4216
|
-
options: {
|
|
4217
|
-
labelSizePixels: 12,
|
|
4218
|
-
labelSizeResolution: 20
|
|
4219
|
-
}
|
|
4751
|
+
options: { labelSize: 240 }
|
|
4220
4752
|
}
|
|
4221
4753
|
});
|
|
4222
4754
|
//#endregion
|
|
4223
4755
|
//#region src/generators/cm11-command-control-lines/generic-c2-line.ts
|
|
4224
4756
|
/** Default options for the Generic command and control line. */
|
|
4225
|
-
const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
|
|
4757
|
+
const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
|
|
4758
|
+
includePrefix: true,
|
|
4759
|
+
smooth: false,
|
|
4760
|
+
smoothResolution: 12
|
|
4761
|
+
};
|
|
4226
4762
|
const GENERIC_C2_LINE_METADATA = {
|
|
4227
4763
|
id: "generic-c2-line",
|
|
4228
4764
|
name: "Generic command and control line",
|
|
@@ -4236,13 +4772,15 @@ const GENERIC_C2_LINE_METADATA = {
|
|
|
4236
4772
|
drawRule: "Line1",
|
|
4237
4773
|
capturesLabelSize: true,
|
|
4238
4774
|
params: [
|
|
4775
|
+
...SMOOTH_LINE_PARAMS,
|
|
4239
4776
|
{
|
|
4240
4777
|
key: "phaseLineName",
|
|
4241
4778
|
label: "Phase line name",
|
|
4242
4779
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
4243
4780
|
type: "text",
|
|
4244
4781
|
placeholder: "DAVID",
|
|
4245
|
-
maxLength: 21
|
|
4782
|
+
maxLength: 21,
|
|
4783
|
+
field: "T"
|
|
4246
4784
|
},
|
|
4247
4785
|
{
|
|
4248
4786
|
key: "includePrefix",
|
|
@@ -4298,20 +4836,23 @@ const GENERIC_C2_LINE_METADATA = {
|
|
|
4298
4836
|
* `base + perp·offset` and the other at `base − perp·offset`.
|
|
4299
4837
|
*/
|
|
4300
4838
|
function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
|
|
4301
|
-
const { phaseLineName, includePrefix = true, ...sizeOptions } = options;
|
|
4839
|
+
const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
4840
|
+
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
4841
|
+
smooth,
|
|
4842
|
+
smoothResolution
|
|
4843
|
+
}, 12);
|
|
4302
4844
|
const features = [{
|
|
4303
4845
|
type: "Feature",
|
|
4304
4846
|
properties: { part: "generic-c2-line" },
|
|
4305
4847
|
geometry: {
|
|
4306
4848
|
type: "LineString",
|
|
4307
|
-
coordinates: positions
|
|
4849
|
+
coordinates: smooth ? verts.map((v) => unproject(v[0], v[1])) : positions
|
|
4308
4850
|
}
|
|
4309
4851
|
}];
|
|
4310
4852
|
const above = [textAmplifiers.H, textAmplifiers.T].filter((v) => Boolean(v)).join(" ");
|
|
4311
4853
|
const below = [textAmplifiers.W, textAmplifiers.W1].filter((v) => Boolean(v)).join(" - ");
|
|
4312
4854
|
const trimmedPhaseLineName = phaseLineName?.trim();
|
|
4313
4855
|
if (above.length > 0 || below.length > 0 || trimmedPhaseLineName) {
|
|
4314
|
-
const verts = positions.map((p) => project(p[0], p[1]));
|
|
4315
4856
|
const labelFeatures = [];
|
|
4316
4857
|
if (above.length > 0 || below.length > 0) {
|
|
4317
4858
|
const frames = aboveEndFrames(verts, sizeOptions);
|
|
@@ -4340,15 +4881,12 @@ const GENERIC_C2_LINE = defineControlMeasure({
|
|
|
4340
4881
|
previewSample: {
|
|
4341
4882
|
controlPoints: [[-2.5, 0], [2.5, 0]],
|
|
4342
4883
|
textAmplifiers: {
|
|
4343
|
-
H: "
|
|
4344
|
-
T: "
|
|
4345
|
-
W: "
|
|
4346
|
-
W1: "
|
|
4884
|
+
H: "<H>",
|
|
4885
|
+
T: "<T>",
|
|
4886
|
+
W: "<W>",
|
|
4887
|
+
W1: "<W1>"
|
|
4347
4888
|
},
|
|
4348
|
-
options: {
|
|
4349
|
-
labelSizePixels: 12,
|
|
4350
|
-
labelSizeResolution: 20
|
|
4351
|
-
}
|
|
4889
|
+
options: { labelSize: 240 }
|
|
4352
4890
|
}
|
|
4353
4891
|
});
|
|
4354
4892
|
//#endregion
|
|
@@ -4371,10 +4909,11 @@ function createArrowHead$1(tip, dir, length, width) {
|
|
|
4371
4909
|
];
|
|
4372
4910
|
}
|
|
4373
4911
|
/**
|
|
4374
|
-
* Builds the glyph label as a Point feature, forwarding
|
|
4375
|
-
*
|
|
4376
|
-
*
|
|
4377
|
-
*
|
|
4912
|
+
* Builds the glyph label as a Point feature, forwarding the resolved size
|
|
4913
|
+
* anchor (`textSizeMeters`/`textSizePixels`, ADR-0028) to adapters for
|
|
4914
|
+
* styling. `point` is in projected meters; the rotation must already be in
|
|
4915
|
+
* the generator's feature-rotation convention. Shared by every mission-task
|
|
4916
|
+
* measure that centers a glyph on its symbol.
|
|
4378
4917
|
*/
|
|
4379
4918
|
function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
4380
4919
|
return {
|
|
@@ -4383,9 +4922,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
|
4383
4922
|
part: "label",
|
|
4384
4923
|
text,
|
|
4385
4924
|
rotation,
|
|
4386
|
-
...labelSize
|
|
4387
|
-
...labelSize.zoom !== void 0 ? { textSizeZoom: labelSize.zoom } : {},
|
|
4388
|
-
...labelSize.resolution !== void 0 ? { textSizeResolution: labelSize.resolution } : {}
|
|
4925
|
+
...labelSizeProps(labelSize)
|
|
4389
4926
|
},
|
|
4390
4927
|
geometry: {
|
|
4391
4928
|
type: "Point",
|
|
@@ -4550,9 +5087,8 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
|
|
|
4550
5087
|
]
|
|
4551
5088
|
}
|
|
4552
5089
|
}, createLabelFeature(labelPoint, "B", labelRotation, {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
resolution: options.labelSizeResolution
|
|
5090
|
+
labelSize: options.labelSize,
|
|
5091
|
+
labelSizePixels: options.labelSizePixels
|
|
4556
5092
|
})]
|
|
4557
5093
|
};
|
|
4558
5094
|
}
|
|
@@ -4635,9 +5171,8 @@ function createBreachSymbol(coordinates, options = {}) {
|
|
|
4635
5171
|
labelText: "B",
|
|
4636
5172
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
|
|
4637
5173
|
labelSize: {
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
resolution: options.labelSizeResolution
|
|
5174
|
+
labelSize: options.labelSize,
|
|
5175
|
+
labelSizePixels: options.labelSizePixels
|
|
4641
5176
|
},
|
|
4642
5177
|
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
4643
5178
|
const halfTick = frontLength * tickLengthRatio / 2;
|
|
@@ -4719,9 +5254,8 @@ function createBypassSymbol(coordinates, options = {}) {
|
|
|
4719
5254
|
labelText: "B",
|
|
4720
5255
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
|
|
4721
5256
|
labelSize: {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
resolution: options.labelSizeResolution
|
|
5257
|
+
labelSize: options.labelSize,
|
|
5258
|
+
labelSizePixels: options.labelSizePixels
|
|
4725
5259
|
},
|
|
4726
5260
|
buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
|
|
4727
5261
|
const arrowheadLength = frontLength * arrowheadLengthRatio;
|
|
@@ -4808,9 +5342,8 @@ function createCanalizeSymbol(coordinates, options = {}) {
|
|
|
4808
5342
|
labelText: "C",
|
|
4809
5343
|
labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
|
|
4810
5344
|
labelSize: {
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
resolution: options.labelSizeResolution
|
|
5345
|
+
labelSize: options.labelSize,
|
|
5346
|
+
labelSizePixels: options.labelSizePixels
|
|
4814
5347
|
},
|
|
4815
5348
|
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
4816
5349
|
const halfTick = frontLength * tickLengthRatio / 2;
|
|
@@ -5539,9 +6072,8 @@ function createClearSymbol(coordinates, options = {}) {
|
|
|
5539
6072
|
return {
|
|
5540
6073
|
type: "FeatureCollection",
|
|
5541
6074
|
features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
resolution: options.labelSizeResolution
|
|
6075
|
+
labelSize: options.labelSize,
|
|
6076
|
+
labelSizePixels: options.labelSizePixels
|
|
5545
6077
|
})]
|
|
5546
6078
|
};
|
|
5547
6079
|
}
|
|
@@ -5649,9 +6181,8 @@ function createDelaySymbol(coordinates, options = {}) {
|
|
|
5649
6181
|
return {
|
|
5650
6182
|
type: "FeatureCollection",
|
|
5651
6183
|
features: [lineFeature, createLabelFeature(labelPoint, "D", normalizeRadians(Math.PI + lineRotation), {
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
resolution: options.labelSizeResolution
|
|
6184
|
+
labelSize: options.labelSize,
|
|
6185
|
+
labelSizePixels: options.labelSizePixels
|
|
5655
6186
|
})]
|
|
5656
6187
|
};
|
|
5657
6188
|
}
|
|
@@ -5883,6 +6414,56 @@ const DISRUPT = defineControlMeasure({
|
|
|
5883
6414
|
previewSample: { controlPoints: [[0, .4], [0, -.4]] }
|
|
5884
6415
|
});
|
|
5885
6416
|
//#endregion
|
|
6417
|
+
//#region src/generators/cm15-maneuver-areas/dropZone.ts
|
|
6418
|
+
/** Default options for the Drop zone control measure. */
|
|
6419
|
+
const DEFAULT_DROP_ZONE_OPTIONS = {
|
|
6420
|
+
smooth: false,
|
|
6421
|
+
smoothResolution: 16
|
|
6422
|
+
};
|
|
6423
|
+
const DROP_ZONE_METADATA = {
|
|
6424
|
+
id: "drop-zone",
|
|
6425
|
+
name: "Drop zone",
|
|
6426
|
+
description: "A specified area upon which airborne troops, equipment, or supplies are airdropped.",
|
|
6427
|
+
entity: "Maneuver Areas",
|
|
6428
|
+
entityType: "Drop Zone",
|
|
6429
|
+
value: "150600",
|
|
6430
|
+
minCoordinates: 3,
|
|
6431
|
+
geometry: "area",
|
|
6432
|
+
geometryTypes: [
|
|
6433
|
+
"Polygon",
|
|
6434
|
+
"MultiLineString",
|
|
6435
|
+
"Point"
|
|
6436
|
+
],
|
|
6437
|
+
drawRule: "Area1",
|
|
6438
|
+
capturesLabelSize: true,
|
|
6439
|
+
params: LABELED_AREA_PARAMS,
|
|
6440
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
6441
|
+
};
|
|
6442
|
+
/**
|
|
6443
|
+
* Creates a Drop zone control measure: a closed area through `positions`
|
|
6444
|
+
* labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
|
|
6445
|
+
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
6446
|
+
*/
|
|
6447
|
+
function createDropZone(positions, options = {}, textAmplifiers = {}) {
|
|
6448
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
|
|
6449
|
+
}
|
|
6450
|
+
const DROP_ZONE = defineControlMeasure({
|
|
6451
|
+
metadata: DROP_ZONE_METADATA,
|
|
6452
|
+
generator: createDropZone,
|
|
6453
|
+
defaultOptions: DEFAULT_DROP_ZONE_OPTIONS,
|
|
6454
|
+
rule: area1DrawRule,
|
|
6455
|
+
previewSample: {
|
|
6456
|
+
controlPoints: [
|
|
6457
|
+
[-1, -.5],
|
|
6458
|
+
[1, -.5],
|
|
6459
|
+
[1, .5],
|
|
6460
|
+
[-1, .5]
|
|
6461
|
+
],
|
|
6462
|
+
textAmplifiers: { T: "HAWK" },
|
|
6463
|
+
options: { labelSize: 96 }
|
|
6464
|
+
}
|
|
6465
|
+
});
|
|
6466
|
+
//#endregion
|
|
5886
6467
|
//#region src/generators/cm15-maneuver-areas/encirclement.ts
|
|
5887
6468
|
const DEFAULT_BARB_SPACING_RATIO = .4;
|
|
5888
6469
|
const DEFAULT_BARB_LENGTH_RATIO = .16;
|
|
@@ -6030,17 +6611,66 @@ const ENCIRCLEMENT = defineControlMeasure({
|
|
|
6030
6611
|
[-.4, .85],
|
|
6031
6612
|
[-1, .5]
|
|
6032
6613
|
],
|
|
6033
|
-
textAmplifiers: {
|
|
6614
|
+
textAmplifiers: {},
|
|
6034
6615
|
options: {
|
|
6035
6616
|
barbSpacingRatio: .4,
|
|
6036
6617
|
barbLengthRatio: .18,
|
|
6037
6618
|
smooth: true,
|
|
6038
|
-
|
|
6039
|
-
labelSizeResolution: 8
|
|
6619
|
+
labelSize: 96
|
|
6040
6620
|
}
|
|
6041
6621
|
}
|
|
6042
6622
|
});
|
|
6043
6623
|
//#endregion
|
|
6624
|
+
//#region src/generators/cm15-maneuver-areas/extractionZone.ts
|
|
6625
|
+
/** Default options for the Extraction zone control measure. */
|
|
6626
|
+
const DEFAULT_EXTRACTION_ZONE_OPTIONS = {
|
|
6627
|
+
smooth: false,
|
|
6628
|
+
smoothResolution: 16
|
|
6629
|
+
};
|
|
6630
|
+
const EXTRACTION_ZONE_METADATA = {
|
|
6631
|
+
id: "extraction-zone",
|
|
6632
|
+
name: "Extraction zone",
|
|
6633
|
+
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.",
|
|
6634
|
+
entity: "Maneuver Areas",
|
|
6635
|
+
entityType: "Extraction Zone",
|
|
6636
|
+
value: "150700",
|
|
6637
|
+
minCoordinates: 3,
|
|
6638
|
+
geometry: "area",
|
|
6639
|
+
geometryTypes: [
|
|
6640
|
+
"Polygon",
|
|
6641
|
+
"MultiLineString",
|
|
6642
|
+
"Point"
|
|
6643
|
+
],
|
|
6644
|
+
drawRule: "Area1",
|
|
6645
|
+
capturesLabelSize: true,
|
|
6646
|
+
params: LABELED_AREA_PARAMS,
|
|
6647
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
|
|
6648
|
+
};
|
|
6649
|
+
/**
|
|
6650
|
+
* Creates an Extraction zone control measure: a closed area through
|
|
6651
|
+
* `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
|
|
6652
|
+
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
6653
|
+
*/
|
|
6654
|
+
function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
|
|
6655
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
|
|
6656
|
+
}
|
|
6657
|
+
const EXTRACTION_ZONE = defineControlMeasure({
|
|
6658
|
+
metadata: EXTRACTION_ZONE_METADATA,
|
|
6659
|
+
generator: createExtractionZone,
|
|
6660
|
+
defaultOptions: DEFAULT_EXTRACTION_ZONE_OPTIONS,
|
|
6661
|
+
rule: area1DrawRule,
|
|
6662
|
+
previewSample: {
|
|
6663
|
+
controlPoints: [
|
|
6664
|
+
[-1, -.5],
|
|
6665
|
+
[1, -.5],
|
|
6666
|
+
[1, .5],
|
|
6667
|
+
[-1, .5]
|
|
6668
|
+
],
|
|
6669
|
+
textAmplifiers: { T: "ROCK" },
|
|
6670
|
+
options: { labelSize: 96 }
|
|
6671
|
+
}
|
|
6672
|
+
});
|
|
6673
|
+
//#endregion
|
|
6044
6674
|
//#region src/internal/field-of-fire.ts
|
|
6045
6675
|
/**
|
|
6046
6676
|
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
@@ -6411,7 +7041,9 @@ const DEFAULT_ARC_SEGMENTS = 16;
|
|
|
6411
7041
|
const DEFAULT_FLOT_OPTIONS = {
|
|
6412
7042
|
radius: DEFAULT_RADIUS,
|
|
6413
7043
|
radiusPixels: DEFAULT_RADIUS_PIXELS,
|
|
6414
|
-
arcSegments: DEFAULT_ARC_SEGMENTS
|
|
7044
|
+
arcSegments: DEFAULT_ARC_SEGMENTS,
|
|
7045
|
+
smooth: false,
|
|
7046
|
+
smoothResolution: 16
|
|
6415
7047
|
};
|
|
6416
7048
|
const FLOT_METADATA = {
|
|
6417
7049
|
id: "flot",
|
|
@@ -6425,6 +7057,7 @@ const FLOT_METADATA = {
|
|
|
6425
7057
|
geometryTypes: ["LineString"],
|
|
6426
7058
|
drawRule: "Line1",
|
|
6427
7059
|
params: [
|
|
7060
|
+
...SMOOTH_LINE_PARAMS,
|
|
6428
7061
|
{
|
|
6429
7062
|
key: "radiusPixels",
|
|
6430
7063
|
label: "Radius",
|
|
@@ -6539,21 +7172,29 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
|
|
|
6539
7172
|
* ```
|
|
6540
7173
|
*/
|
|
6541
7174
|
function createFLOT(positions, options = {}) {
|
|
6542
|
-
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS } = options;
|
|
7175
|
+
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
|
|
6543
7176
|
let effectiveRadius = radius;
|
|
6544
7177
|
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
6545
7178
|
const safeRadius = Math.max(EPSILON, effectiveRadius);
|
|
6546
7179
|
const safeArcSegments = Math.max(3, Math.round(arcSegments));
|
|
6547
|
-
const
|
|
7180
|
+
const basePath = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
7181
|
+
smooth,
|
|
7182
|
+
smoothResolution
|
|
7183
|
+
}, 16);
|
|
6548
7184
|
const allPoints = [];
|
|
6549
|
-
for (let i = 0; i <
|
|
6550
|
-
const p1 =
|
|
6551
|
-
const p2 =
|
|
7185
|
+
for (let i = 0; i < basePath.length - 1; i++) {
|
|
7186
|
+
const p1 = basePath[i];
|
|
7187
|
+
const p2 = basePath[i + 1];
|
|
6552
7188
|
const segmentPoints = processSegment(p1, p2, safeRadius, safeArcSegments, i === 0);
|
|
6553
7189
|
allPoints.push(...segmentPoints);
|
|
6554
7190
|
}
|
|
6555
|
-
if (allPoints.length > 0 &&
|
|
6556
|
-
const
|
|
7191
|
+
if (allPoints.length > 0 && basePath.length > 1) {
|
|
7192
|
+
const firstOriginal = basePath[0];
|
|
7193
|
+
const firstGenerated = allPoints[0];
|
|
7194
|
+
if (vecMag(vecSub(firstOriginal, firstGenerated)) > 1e-6) allPoints.unshift(firstOriginal);
|
|
7195
|
+
}
|
|
7196
|
+
if (allPoints.length > 0 && basePath.length > 1) {
|
|
7197
|
+
const lastOriginal = basePath[basePath.length - 1];
|
|
6557
7198
|
const lastGenerated = allPoints[allPoints.length - 1];
|
|
6558
7199
|
if (vecMag(vecSub(lastOriginal, lastGenerated)) > 1e-6) allPoints.push(lastOriginal);
|
|
6559
7200
|
}
|
|
@@ -6579,7 +7220,11 @@ const FLOT = defineControlMeasure({
|
|
|
6579
7220
|
//#endregion
|
|
6580
7221
|
//#region src/generators/cm14-maneuver-lines/phase-line.ts
|
|
6581
7222
|
/** Default options for the Phase line control measure. */
|
|
6582
|
-
const DEFAULT_PHASE_LINE_OPTIONS = {
|
|
7223
|
+
const DEFAULT_PHASE_LINE_OPTIONS = {
|
|
7224
|
+
includePrefix: true,
|
|
7225
|
+
smooth: false,
|
|
7226
|
+
smoothResolution: 12
|
|
7227
|
+
};
|
|
6583
7228
|
const PHASE_LINE_METADATA = {
|
|
6584
7229
|
id: "phase-line",
|
|
6585
7230
|
name: "Phase line",
|
|
@@ -6592,20 +7237,24 @@ const PHASE_LINE_METADATA = {
|
|
|
6592
7237
|
geometryTypes: ["LineString", "Point"],
|
|
6593
7238
|
drawRule: "Line1",
|
|
6594
7239
|
capturesLabelSize: true,
|
|
6595
|
-
params: [
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
7240
|
+
params: [
|
|
7241
|
+
...SMOOTH_LINE_PARAMS,
|
|
7242
|
+
{
|
|
7243
|
+
key: "includePrefix",
|
|
7244
|
+
label: "Show PL prefix",
|
|
7245
|
+
description: "Prefix the end labels with \"PL\" (or show a bare \"PL\" with no designation).",
|
|
7246
|
+
type: "boolean"
|
|
7247
|
+
},
|
|
7248
|
+
{
|
|
7249
|
+
key: "labelPadding",
|
|
7250
|
+
label: "Label padding",
|
|
7251
|
+
description: "Extra clearance between the line and its labels, as a ratio of the label height",
|
|
7252
|
+
type: "number",
|
|
7253
|
+
min: 0,
|
|
7254
|
+
max: 2,
|
|
7255
|
+
step: .05
|
|
7256
|
+
}
|
|
7257
|
+
],
|
|
6609
7258
|
textAmplifiers: [{
|
|
6610
7259
|
key: "T",
|
|
6611
7260
|
label: "Unique designation",
|
|
@@ -6635,16 +7284,17 @@ const PHASE_LINE = defineControlMeasure({
|
|
|
6635
7284
|
previewSample: {
|
|
6636
7285
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6637
7286
|
textAmplifiers: { T: "ECHO" },
|
|
6638
|
-
options: {
|
|
6639
|
-
labelSizePixels: 12,
|
|
6640
|
-
labelSizeResolution: 8
|
|
6641
|
-
}
|
|
7287
|
+
options: { labelSize: 96 }
|
|
6642
7288
|
}
|
|
6643
7289
|
});
|
|
6644
7290
|
//#endregion
|
|
6645
7291
|
//#region src/generators/cm14-maneuver-lines/handover-line.ts
|
|
6646
7292
|
/** Default options for the Handover line control measure. */
|
|
6647
|
-
const DEFAULT_HANDOVER_LINE_OPTIONS = {
|
|
7293
|
+
const DEFAULT_HANDOVER_LINE_OPTIONS = {
|
|
7294
|
+
includePrefix: true,
|
|
7295
|
+
smooth: false,
|
|
7296
|
+
smoothResolution: 12
|
|
7297
|
+
};
|
|
6648
7298
|
const HANDOVER_LINE_METADATA = {
|
|
6649
7299
|
id: "handover-line",
|
|
6650
7300
|
name: "Handover line",
|
|
@@ -6658,13 +7308,15 @@ const HANDOVER_LINE_METADATA = {
|
|
|
6658
7308
|
drawRule: "Line1",
|
|
6659
7309
|
capturesLabelSize: true,
|
|
6660
7310
|
params: [
|
|
7311
|
+
...SMOOTH_LINE_PARAMS,
|
|
6661
7312
|
{
|
|
6662
7313
|
key: "phaseLineName",
|
|
6663
7314
|
label: "Phase line name",
|
|
6664
7315
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
6665
7316
|
type: "text",
|
|
6666
7317
|
placeholder: "DAVID",
|
|
6667
|
-
maxLength: 21
|
|
7318
|
+
maxLength: 21,
|
|
7319
|
+
field: "T"
|
|
6668
7320
|
},
|
|
6669
7321
|
{
|
|
6670
7322
|
key: "includePrefix",
|
|
@@ -6702,16 +7354,17 @@ const HANDOVER_LINE = defineControlMeasure({
|
|
|
6702
7354
|
rule: line1DrawRule,
|
|
6703
7355
|
previewSample: {
|
|
6704
7356
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6705
|
-
options: {
|
|
6706
|
-
labelSizePixels: 12,
|
|
6707
|
-
labelSizeResolution: 8
|
|
6708
|
-
}
|
|
7357
|
+
options: { labelSize: 96 }
|
|
6709
7358
|
}
|
|
6710
7359
|
});
|
|
6711
7360
|
//#endregion
|
|
6712
7361
|
//#region src/generators/cm14-maneuver-lines/battle-handover-line.ts
|
|
6713
7362
|
/** Default options for the Battle handover line control measure. */
|
|
6714
|
-
const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = {
|
|
7363
|
+
const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = {
|
|
7364
|
+
includePrefix: true,
|
|
7365
|
+
smooth: false,
|
|
7366
|
+
smoothResolution: 12
|
|
7367
|
+
};
|
|
6715
7368
|
const BATTLE_HANDOVER_LINE_METADATA = {
|
|
6716
7369
|
id: "battle-handover-line",
|
|
6717
7370
|
name: "Battle handover line",
|
|
@@ -6725,13 +7378,15 @@ const BATTLE_HANDOVER_LINE_METADATA = {
|
|
|
6725
7378
|
drawRule: "Line1",
|
|
6726
7379
|
capturesLabelSize: true,
|
|
6727
7380
|
params: [
|
|
7381
|
+
...SMOOTH_LINE_PARAMS,
|
|
6728
7382
|
{
|
|
6729
7383
|
key: "phaseLineName",
|
|
6730
7384
|
label: "Phase line name",
|
|
6731
7385
|
description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
|
|
6732
7386
|
type: "text",
|
|
6733
7387
|
placeholder: "DAVID",
|
|
6734
|
-
maxLength: 21
|
|
7388
|
+
maxLength: 21,
|
|
7389
|
+
field: "T"
|
|
6735
7390
|
},
|
|
6736
7391
|
{
|
|
6737
7392
|
key: "includePrefix",
|
|
@@ -6769,10 +7424,7 @@ const BATTLE_HANDOVER_LINE = defineControlMeasure({
|
|
|
6769
7424
|
rule: line1DrawRule,
|
|
6770
7425
|
previewSample: {
|
|
6771
7426
|
controlPoints: [[-1, 0], [1, 0]],
|
|
6772
|
-
options: {
|
|
6773
|
-
labelSizePixels: 12,
|
|
6774
|
-
labelSizeResolution: 8
|
|
6775
|
-
}
|
|
7427
|
+
options: { labelSize: 96 }
|
|
6776
7428
|
}
|
|
6777
7429
|
});
|
|
6778
7430
|
//#endregion
|
|
@@ -7013,10 +7665,7 @@ const FORTIFIED_AREA = defineControlMeasure({
|
|
|
7013
7665
|
[-1, .5]
|
|
7014
7666
|
],
|
|
7015
7667
|
textAmplifiers: { T: "TANGO" },
|
|
7016
|
-
options: {
|
|
7017
|
-
labelSizePixels: 12,
|
|
7018
|
-
labelSizeResolution: 8
|
|
7019
|
-
}
|
|
7668
|
+
options: { labelSize: 96 }
|
|
7020
7669
|
}
|
|
7021
7670
|
});
|
|
7022
7671
|
//#endregion
|
|
@@ -7199,14 +7848,10 @@ const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
|
7199
7848
|
],
|
|
7200
7849
|
textAmplifiers: {
|
|
7201
7850
|
T: "02",
|
|
7202
|
-
N: "ENY",
|
|
7203
7851
|
W: "051030Z",
|
|
7204
7852
|
W1: "051600Z"
|
|
7205
7853
|
},
|
|
7206
|
-
options: {
|
|
7207
|
-
labelSizePixels: 12,
|
|
7208
|
-
labelSizeResolution: 8
|
|
7209
|
-
}
|
|
7854
|
+
options: { labelSize: 96 }
|
|
7210
7855
|
}
|
|
7211
7856
|
});
|
|
7212
7857
|
//#endregion
|
|
@@ -7256,10 +7901,7 @@ const LANDING_ZONE = defineControlMeasure({
|
|
|
7256
7901
|
[-1, .5]
|
|
7257
7902
|
],
|
|
7258
7903
|
textAmplifiers: { T: "SILVER" },
|
|
7259
|
-
options: {
|
|
7260
|
-
labelSizePixels: 12,
|
|
7261
|
-
labelSizeResolution: 8
|
|
7262
|
-
}
|
|
7904
|
+
options: { labelSize: 96 }
|
|
7263
7905
|
}
|
|
7264
7906
|
});
|
|
7265
7907
|
//#endregion
|
|
@@ -7779,10 +8421,145 @@ const PICKUP_ZONE = defineControlMeasure({
|
|
|
7779
8421
|
[-1, .5]
|
|
7780
8422
|
],
|
|
7781
8423
|
textAmplifiers: { T: "WOLF" },
|
|
7782
|
-
options: {
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
8424
|
+
options: { labelSize: 96 }
|
|
8425
|
+
}
|
|
8426
|
+
});
|
|
8427
|
+
//#endregion
|
|
8428
|
+
//#region src/generators/cm15-maneuver-areas/assaultPosition.ts
|
|
8429
|
+
/** Default options for the Assault position control measure. */
|
|
8430
|
+
const DEFAULT_ASSAULT_POSITION_OPTIONS = {
|
|
8431
|
+
smooth: false,
|
|
8432
|
+
smoothResolution: 16
|
|
8433
|
+
};
|
|
8434
|
+
const ASSAULT_POSITION_METADATA = {
|
|
8435
|
+
id: "assault-position",
|
|
8436
|
+
name: "Assault position",
|
|
8437
|
+
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.",
|
|
8438
|
+
entity: "Maneuver Areas",
|
|
8439
|
+
entityType: "Assault Position",
|
|
8440
|
+
value: "151500",
|
|
8441
|
+
minCoordinates: 3,
|
|
8442
|
+
geometry: "area",
|
|
8443
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8444
|
+
drawRule: "Area1",
|
|
8445
|
+
capturesLabelSize: true,
|
|
8446
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8447
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8448
|
+
};
|
|
8449
|
+
/**
|
|
8450
|
+
* Creates an Assault position control measure: a closed area through
|
|
8451
|
+
* `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) —
|
|
8452
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8453
|
+
*/
|
|
8454
|
+
function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
|
|
8455
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("ASLT", textAmplifiers.T) });
|
|
8456
|
+
}
|
|
8457
|
+
const ASSAULT_POSITION = defineControlMeasure({
|
|
8458
|
+
metadata: ASSAULT_POSITION_METADATA,
|
|
8459
|
+
generator: createAssaultPosition,
|
|
8460
|
+
defaultOptions: DEFAULT_ASSAULT_POSITION_OPTIONS,
|
|
8461
|
+
rule: area1DrawRule,
|
|
8462
|
+
previewSample: {
|
|
8463
|
+
controlPoints: [
|
|
8464
|
+
[-1, -.5],
|
|
8465
|
+
[1, -.5],
|
|
8466
|
+
[1, .5],
|
|
8467
|
+
[-1, .5]
|
|
8468
|
+
],
|
|
8469
|
+
textAmplifiers: { T: "DANUBE" },
|
|
8470
|
+
options: { labelSize: 96 }
|
|
8471
|
+
}
|
|
8472
|
+
});
|
|
8473
|
+
//#endregion
|
|
8474
|
+
//#region src/generators/cm15-maneuver-areas/attackPosition.ts
|
|
8475
|
+
/** Default options for the Attack position control measure. */
|
|
8476
|
+
const DEFAULT_ATTACK_POSITION_OPTIONS = {
|
|
8477
|
+
smooth: false,
|
|
8478
|
+
smoothResolution: 16
|
|
8479
|
+
};
|
|
8480
|
+
const ATTACK_POSITION_METADATA = {
|
|
8481
|
+
id: "attack-position",
|
|
8482
|
+
name: "Attack position",
|
|
8483
|
+
description: "The last position occupied by the assault echelon before crossing the start line/line of departure.",
|
|
8484
|
+
entity: "Maneuver Areas",
|
|
8485
|
+
entityType: "Attack Position",
|
|
8486
|
+
value: "151600",
|
|
8487
|
+
minCoordinates: 3,
|
|
8488
|
+
geometry: "area",
|
|
8489
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8490
|
+
drawRule: "Area1",
|
|
8491
|
+
capturesLabelSize: true,
|
|
8492
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8493
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8494
|
+
};
|
|
8495
|
+
/**
|
|
8496
|
+
* Creates an Attack position control measure: a closed area through
|
|
8497
|
+
* `positions` labeled at its center with "ATK "-prefixed `T` (Field T) —
|
|
8498
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8499
|
+
*/
|
|
8500
|
+
function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
|
|
8501
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("ATK", textAmplifiers.T) });
|
|
8502
|
+
}
|
|
8503
|
+
const ATTACK_POSITION = defineControlMeasure({
|
|
8504
|
+
metadata: ATTACK_POSITION_METADATA,
|
|
8505
|
+
generator: createAttackPosition,
|
|
8506
|
+
defaultOptions: DEFAULT_ATTACK_POSITION_OPTIONS,
|
|
8507
|
+
rule: area1DrawRule,
|
|
8508
|
+
previewSample: {
|
|
8509
|
+
controlPoints: [
|
|
8510
|
+
[-1, -.5],
|
|
8511
|
+
[1, -.5],
|
|
8512
|
+
[1, .5],
|
|
8513
|
+
[-1, .5]
|
|
8514
|
+
],
|
|
8515
|
+
textAmplifiers: { T: "NILE" },
|
|
8516
|
+
options: { labelSize: 96 }
|
|
8517
|
+
}
|
|
8518
|
+
});
|
|
8519
|
+
//#endregion
|
|
8520
|
+
//#region src/generators/cm15-maneuver-areas/objectiveArea.ts
|
|
8521
|
+
/** Default options for the Objective area control measure. */
|
|
8522
|
+
const DEFAULT_OBJECTIVE_AREA_OPTIONS = {
|
|
8523
|
+
smooth: false,
|
|
8524
|
+
smoothResolution: 16
|
|
8525
|
+
};
|
|
8526
|
+
const OBJECTIVE_AREA_METADATA = {
|
|
8527
|
+
id: "objective-area",
|
|
8528
|
+
name: "Objective area",
|
|
8529
|
+
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.",
|
|
8530
|
+
entity: "Maneuver Areas",
|
|
8531
|
+
entityType: "Objective Area",
|
|
8532
|
+
value: "151700",
|
|
8533
|
+
minCoordinates: 3,
|
|
8534
|
+
geometry: "area",
|
|
8535
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8536
|
+
drawRule: "Area1",
|
|
8537
|
+
capturesLabelSize: true,
|
|
8538
|
+
params: SMOOTH_AREA_PARAMS,
|
|
8539
|
+
textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
|
|
8540
|
+
};
|
|
8541
|
+
/**
|
|
8542
|
+
* Creates an Objective area control measure: a closed area through
|
|
8543
|
+
* `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) —
|
|
8544
|
+
* the sole label row, per {@link labeledAreaFeatures}.
|
|
8545
|
+
*/
|
|
8546
|
+
function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
|
|
8547
|
+
return labeledAreaFeatures(positions, options, { name: composeAreaName("OBJ", textAmplifiers.T) });
|
|
8548
|
+
}
|
|
8549
|
+
const OBJECTIVE_AREA = defineControlMeasure({
|
|
8550
|
+
metadata: OBJECTIVE_AREA_METADATA,
|
|
8551
|
+
generator: createObjectiveArea,
|
|
8552
|
+
defaultOptions: DEFAULT_OBJECTIVE_AREA_OPTIONS,
|
|
8553
|
+
rule: area1DrawRule,
|
|
8554
|
+
previewSample: {
|
|
8555
|
+
controlPoints: [
|
|
8556
|
+
[-1, -.5],
|
|
8557
|
+
[1, -.5],
|
|
8558
|
+
[1, .5],
|
|
8559
|
+
[-1, .5]
|
|
8560
|
+
],
|
|
8561
|
+
textAmplifiers: { T: "FIVE" },
|
|
8562
|
+
options: { labelSize: 96 }
|
|
7786
8563
|
}
|
|
7787
8564
|
});
|
|
7788
8565
|
//#endregion
|
|
@@ -8056,70 +8833,7 @@ const STRONG_POINT_METADATA = {
|
|
|
8056
8833
|
],
|
|
8057
8834
|
drawRule: "Area1",
|
|
8058
8835
|
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
|
-
],
|
|
8836
|
+
params: ECHELON_AREA_PARAMS,
|
|
8123
8837
|
textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
|
|
8124
8838
|
};
|
|
8125
8839
|
/**
|
|
@@ -8227,12 +8941,121 @@ const STRONG_POINT = defineControlMeasure({
|
|
|
8227
8941
|
options: {
|
|
8228
8942
|
echelonSize: 200,
|
|
8229
8943
|
smooth: true,
|
|
8230
|
-
|
|
8231
|
-
labelSizeResolution: 8
|
|
8944
|
+
labelSize: 96
|
|
8232
8945
|
}
|
|
8233
8946
|
}
|
|
8234
8947
|
});
|
|
8235
8948
|
//#endregion
|
|
8949
|
+
//#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
|
|
8950
|
+
/** Default options for the Submarine action area control measure. */
|
|
8951
|
+
const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
|
|
8952
|
+
smooth: false,
|
|
8953
|
+
smoothResolution: 16
|
|
8954
|
+
};
|
|
8955
|
+
const SUBMARINE_ACTION_AREA_METADATA = {
|
|
8956
|
+
id: "submarine-action-area",
|
|
8957
|
+
name: "Submarine action area",
|
|
8958
|
+
description: "An area, including the water space and airspace above it, reserved for submarine operations.",
|
|
8959
|
+
entity: "Maneuver Areas",
|
|
8960
|
+
entityType: "Action Area",
|
|
8961
|
+
entitySubtype: "Submarine Action Area",
|
|
8962
|
+
value: "150502",
|
|
8963
|
+
minCoordinates: 3,
|
|
8964
|
+
geometry: "area",
|
|
8965
|
+
geometryTypes: [
|
|
8966
|
+
"Polygon",
|
|
8967
|
+
"MultiLineString",
|
|
8968
|
+
"Point"
|
|
8969
|
+
],
|
|
8970
|
+
drawRule: "Area1",
|
|
8971
|
+
capturesLabelSize: true,
|
|
8972
|
+
params: LABELED_AREA_PARAMS,
|
|
8973
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
|
|
8974
|
+
};
|
|
8975
|
+
/**
|
|
8976
|
+
* Creates a Submarine action area control measure: a closed area through
|
|
8977
|
+
* `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
|
|
8978
|
+
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
8979
|
+
*/
|
|
8980
|
+
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
8981
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
|
|
8982
|
+
}
|
|
8983
|
+
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
8984
|
+
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
8985
|
+
generator: createSubmarineActionArea,
|
|
8986
|
+
defaultOptions: DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS,
|
|
8987
|
+
rule: area1DrawRule,
|
|
8988
|
+
previewSample: {
|
|
8989
|
+
controlPoints: [
|
|
8990
|
+
[-1, -.5],
|
|
8991
|
+
[1, -.5],
|
|
8992
|
+
[1, .5],
|
|
8993
|
+
[-1, .5]
|
|
8994
|
+
],
|
|
8995
|
+
textAmplifiers: {
|
|
8996
|
+
T: "02",
|
|
8997
|
+
W: "051030Z",
|
|
8998
|
+
W1: "051600Z"
|
|
8999
|
+
},
|
|
9000
|
+
options: { labelSize: 96 }
|
|
9001
|
+
}
|
|
9002
|
+
});
|
|
9003
|
+
//#endregion
|
|
9004
|
+
//#region src/generators/cm15-maneuver-areas/submarineGeneratedActionArea.ts
|
|
9005
|
+
/** Default options for the Submarine-generated action area control measure. */
|
|
9006
|
+
const DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS = {
|
|
9007
|
+
smooth: false,
|
|
9008
|
+
smoothResolution: 16
|
|
9009
|
+
};
|
|
9010
|
+
const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
9011
|
+
id: "submarine-generated-action-area",
|
|
9012
|
+
name: "Submarine-generated action area",
|
|
9013
|
+
description: "An action area generated around a submarine's reported position, reserved for its operations.",
|
|
9014
|
+
entity: "Maneuver Areas",
|
|
9015
|
+
entityType: "Action Area",
|
|
9016
|
+
entitySubtype: "Submarine-Generated Action Area",
|
|
9017
|
+
value: "150503",
|
|
9018
|
+
minCoordinates: 3,
|
|
9019
|
+
geometry: "area",
|
|
9020
|
+
geometryTypes: [
|
|
9021
|
+
"Polygon",
|
|
9022
|
+
"MultiLineString",
|
|
9023
|
+
"Point"
|
|
9024
|
+
],
|
|
9025
|
+
drawRule: "Area1",
|
|
9026
|
+
capturesLabelSize: true,
|
|
9027
|
+
params: LABELED_AREA_PARAMS,
|
|
9028
|
+
textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
|
|
9029
|
+
};
|
|
9030
|
+
/**
|
|
9031
|
+
* Creates a Submarine-generated action area control measure: a closed area
|
|
9032
|
+
* through `positions` labeled at its center with "SGSA -"-prefixed `T`
|
|
9033
|
+
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
9034
|
+
*/
|
|
9035
|
+
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
|
|
9036
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
|
|
9037
|
+
}
|
|
9038
|
+
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
9039
|
+
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
9040
|
+
generator: createSubmarineGeneratedActionArea,
|
|
9041
|
+
defaultOptions: DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS,
|
|
9042
|
+
rule: area1DrawRule,
|
|
9043
|
+
previewSample: {
|
|
9044
|
+
controlPoints: [
|
|
9045
|
+
[-1, -.5],
|
|
9046
|
+
[1, -.5],
|
|
9047
|
+
[1, .5],
|
|
9048
|
+
[-1, .5]
|
|
9049
|
+
],
|
|
9050
|
+
textAmplifiers: {
|
|
9051
|
+
T: "02",
|
|
9052
|
+
W: "051030Z",
|
|
9053
|
+
W1: "051600Z"
|
|
9054
|
+
},
|
|
9055
|
+
options: { labelSize: 96 }
|
|
9056
|
+
}
|
|
9057
|
+
});
|
|
9058
|
+
//#endregion
|
|
8236
9059
|
//#region src/generators/cm15-maneuver-areas/supportByFire.ts
|
|
8237
9060
|
const DEFAULT_SUPPORT_BY_FIRE_OPTIONS = {
|
|
8238
9061
|
arrowheadWidthRatio: .3,
|
|
@@ -8571,11 +9394,25 @@ const DEFINITIONS = {
|
|
|
8571
9394
|
"final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
|
|
8572
9395
|
"search-area": SEARCH_AREA,
|
|
8573
9396
|
encirclement: ENCIRCLEMENT,
|
|
9397
|
+
area: AREA,
|
|
8574
9398
|
"assembly-area": ASSEMBLY_AREA,
|
|
8575
9399
|
"joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
|
|
9400
|
+
"submarine-action-area": SUBMARINE_ACTION_AREA,
|
|
9401
|
+
"submarine-generated-action-area": SUBMARINE_GENERATED_ACTION_AREA,
|
|
9402
|
+
"drop-zone": DROP_ZONE,
|
|
9403
|
+
"extraction-zone": EXTRACTION_ZONE,
|
|
8576
9404
|
"landing-zone": LANDING_ZONE,
|
|
8577
9405
|
"pickup-zone": PICKUP_ZONE,
|
|
9406
|
+
"assault-position": ASSAULT_POSITION,
|
|
9407
|
+
"attack-position": ATTACK_POSITION,
|
|
9408
|
+
"objective-area": OBJECTIVE_AREA,
|
|
8578
9409
|
"area-of-operations": AREA_OF_OPERATIONS,
|
|
9410
|
+
"named-area-of-interest": NAMED_AREA_OF_INTEREST,
|
|
9411
|
+
"target-area-of-interest": TARGET_AREA_OF_INTEREST,
|
|
9412
|
+
"airfield-zone": AIRFIELD_ZONE,
|
|
9413
|
+
"base-camp": BASE_CAMP,
|
|
9414
|
+
"guerrilla-base": GUERRILLA_BASE,
|
|
9415
|
+
"generic-c2-area": GENERIC_C2_AREA,
|
|
8579
9416
|
"main-attack": MAIN_ATTACK,
|
|
8580
9417
|
"supporting-attack": SUPPORTING_ATTACK,
|
|
8581
9418
|
"classic-arrow": CLASSIC_ARROW,
|
|
@@ -8895,8 +9732,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
8895
9732
|
const text = typeof sourceProps.text === "string" ? sourceProps.text : void 0;
|
|
8896
9733
|
const rotation = typeof sourceProps.rotation === "number" ? sourceProps.rotation : void 0;
|
|
8897
9734
|
const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
|
|
8898
|
-
const
|
|
8899
|
-
const textSizeResolution = typeof sourceProps.textSizeResolution === "number" ? sourceProps.textSizeResolution : void 0;
|
|
9735
|
+
const textSizeMeters = typeof sourceProps.textSizeMeters === "number" ? sourceProps.textSizeMeters : void 0;
|
|
8900
9736
|
const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
|
|
8901
9737
|
return {
|
|
8902
9738
|
type: "Feature",
|
|
@@ -8909,8 +9745,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
8909
9745
|
...rotation !== void 0 ? { rotation } : {},
|
|
8910
9746
|
...textAnchor !== void 0 ? { textAnchor } : {},
|
|
8911
9747
|
...textSizePixels !== void 0 ? { textSizePixels } : {},
|
|
8912
|
-
...
|
|
8913
|
-
...textSizeResolution !== void 0 ? { textSizeResolution } : {}
|
|
9748
|
+
...textSizeMeters !== void 0 ? { textSizeMeters } : {}
|
|
8914
9749
|
},
|
|
8915
9750
|
geometry: feature.geometry
|
|
8916
9751
|
};
|