@orbat-mapper/control-measures 0.6.0 → 0.8.0
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-DCgWC2Q8.d.mts → index-QyKWkgNo.d.mts} +144 -86
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +3 -2
- package/dist/patterns-CcQmmOuJ.d.mts +116 -0
- package/dist/patterns.d.mts +2 -0
- package/dist/patterns.mjs +205 -0
- package/dist/preview/index.d.mts +15 -11
- package/dist/preview/index.mjs +67 -84
- package/dist/{renderControlMeasure-D6VScGE0.mjs → renderControlMeasure-BHIGHuop.mjs} +909 -273
- package/media/limited-access-area.svg +1 -1
- package/media/mined-area.svg +1 -0
- package/media/minefield-dynamic.svg +1 -0
- package/media/minefield.svg +1 -0
- package/media/no-fire-area-irregular.svg +1 -1
- package/package.json +2 -1
|
@@ -586,20 +586,16 @@ const area1DrawRule = {
|
|
|
586
586
|
}
|
|
587
587
|
};
|
|
588
588
|
//#endregion
|
|
589
|
-
//#region src/draw-rules/
|
|
590
|
-
const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
|
|
591
|
-
//#endregion
|
|
592
|
-
//#region src/draw-rules/area12.ts
|
|
589
|
+
//#region src/draw-rules/area5.ts
|
|
593
590
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
|
|
597
|
-
* to the perpendicular axis through P1 (not the baseline midpoint).
|
|
591
|
+
* Area5 — PT. 1/PT. 2 define the open diameter and PT. 3 defines the arrow end.
|
|
592
|
+
* The arrow stays on the perpendicular bisector of the diameter.
|
|
598
593
|
*/
|
|
599
|
-
const
|
|
600
|
-
id: "
|
|
601
|
-
|
|
602
|
-
|
|
594
|
+
const containDrawRule = createMidpointPerpendicularDrawRule({
|
|
595
|
+
id: "area5:contain",
|
|
596
|
+
minimumUserPoints: 3,
|
|
597
|
+
defaultDistanceRatio: -1.5,
|
|
598
|
+
minimumPreviewPoints: 2
|
|
603
599
|
});
|
|
604
600
|
//#endregion
|
|
605
601
|
//#region src/draw-rules/area15.ts
|
|
@@ -630,6 +626,34 @@ const centerRadiusDrawRule = {
|
|
|
630
626
|
/** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
|
|
631
627
|
const isolateDrawRule = centerRadiusDrawRule;
|
|
632
628
|
//#endregion
|
|
629
|
+
//#region src/draw-rules/area6.ts
|
|
630
|
+
/**
|
|
631
|
+
* Area6 — center + start point, mechanically identical to Area15. It re-labels
|
|
632
|
+
* the shared behaviour rather than aliasing it because a definition's `rule.id`
|
|
633
|
+
* must carry its `metadata.drawRule` as a prefix (see the registry invariant in
|
|
634
|
+
* `__tests__/metadata-registry.spec.ts`, ADR-0013).
|
|
635
|
+
*/
|
|
636
|
+
const retainDrawRule = {
|
|
637
|
+
...centerRadiusDrawRule,
|
|
638
|
+
id: "area6:center-radius"
|
|
639
|
+
};
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/draw-rules/area11.ts
|
|
642
|
+
const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/draw-rules/area12.ts
|
|
645
|
+
/**
|
|
646
|
+
* Area12 anchor draw rule for Disrupt.
|
|
647
|
+
*
|
|
648
|
+
* P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
|
|
649
|
+
* to the perpendicular axis through P1 (not the baseline midpoint).
|
|
650
|
+
*/
|
|
651
|
+
const disruptDrawRule = createMidpointPerpendicularDrawRule({
|
|
652
|
+
id: "area12:disrupt",
|
|
653
|
+
origin: "p1",
|
|
654
|
+
normal: "left"
|
|
655
|
+
});
|
|
656
|
+
//#endregion
|
|
633
657
|
//#region src/draw-rules/area17.ts
|
|
634
658
|
/**
|
|
635
659
|
* Area17 anchor draw rule for Penetrate.
|
|
@@ -689,6 +713,81 @@ const staticPointDrawRule = {
|
|
|
689
713
|
id: "point2:static-anchor"
|
|
690
714
|
};
|
|
691
715
|
//#endregion
|
|
716
|
+
//#region src/draw-rules/sector.ts
|
|
717
|
+
const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
|
|
718
|
+
/**
|
|
719
|
+
* Sector — anchor + inner-left edge + outer-right edge. The two radial points
|
|
720
|
+
* independently control a radius and a true-north azimuth. Dragging the anchor
|
|
721
|
+
* translates both edge points so the sector retains its size and orientation.
|
|
722
|
+
*/
|
|
723
|
+
function derive$9(points) {
|
|
724
|
+
return clonePositions(points.slice(0, 3));
|
|
725
|
+
}
|
|
726
|
+
function guidePoints$1(points) {
|
|
727
|
+
return points.length < 3 ? clonePositions(points) : [];
|
|
728
|
+
}
|
|
729
|
+
/** Snap one radial Sector handle to a true-north angular increment. */
|
|
730
|
+
function snapSectorAngle(points, activePointIndex, incrementRadians) {
|
|
731
|
+
const next = clonePositions(points);
|
|
732
|
+
if (activePointIndex !== 1 && activePointIndex !== 2) return next;
|
|
733
|
+
const anchor = next[0];
|
|
734
|
+
const point = next[activePointIndex];
|
|
735
|
+
if (!anchor || !point) return next;
|
|
736
|
+
const range = haversineDistance(anchor, point);
|
|
737
|
+
const bearing = sphericalBearing(anchor, point);
|
|
738
|
+
next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / incrementRadians) * incrementRadians), ...point.slice(2)];
|
|
739
|
+
return next;
|
|
740
|
+
}
|
|
741
|
+
function constrainAngles(points, activePointIndex) {
|
|
742
|
+
return snapSectorAngle(points, activePointIndex, SECTOR_ANGLE_SNAP_RADIANS);
|
|
743
|
+
}
|
|
744
|
+
const sectorDrawRule = {
|
|
745
|
+
id: "sector:anchor-radii",
|
|
746
|
+
minimumUserPoints: 3,
|
|
747
|
+
canonicalPointCount: 3,
|
|
748
|
+
showGuide: true,
|
|
749
|
+
guidePoints: guidePoints$1,
|
|
750
|
+
constrainAngles,
|
|
751
|
+
derive: derive$9,
|
|
752
|
+
transform(event) {
|
|
753
|
+
const { previous, next, activePointIndex } = event;
|
|
754
|
+
if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
|
|
755
|
+
const dx = next[0][0] - previous[0][0];
|
|
756
|
+
const dy = next[0][1] - previous[0][1];
|
|
757
|
+
return [
|
|
758
|
+
clonePosition(next[0]),
|
|
759
|
+
[previous[1][0] + dx, previous[1][1] + dy],
|
|
760
|
+
[previous[2][0] + dx, previous[2][1] + dy]
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
return derive$9(next);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/draw-rules/point18.ts
|
|
768
|
+
const WHOLE_DEGREE_RADIANS = Math.PI / 180;
|
|
769
|
+
function snapToWholeDegrees(points) {
|
|
770
|
+
let snapped = snapSectorAngle(points, 1, WHOLE_DEGREE_RADIANS);
|
|
771
|
+
if (snapped.length >= 3) snapped = snapSectorAngle(snapped, 2, WHOLE_DEGREE_RADIANS);
|
|
772
|
+
return snapped;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Point18 — doctrinally a numeric, single-anchor symbol. The interactive
|
|
776
|
+
* authoring model exposes those dimensions as Sector-style radial handles:
|
|
777
|
+
* P1 is the anchor, P2 fixes the start range/left bearing, and P3 fixes the
|
|
778
|
+
* stop range/right bearing.
|
|
779
|
+
*/
|
|
780
|
+
const dynamicPointDrawRule = {
|
|
781
|
+
...sectorDrawRule,
|
|
782
|
+
id: "point18:anchor-radii",
|
|
783
|
+
derive(points) {
|
|
784
|
+
return snapToWholeDegrees(sectorDrawRule.derive(points));
|
|
785
|
+
},
|
|
786
|
+
transform(event) {
|
|
787
|
+
return snapSectorAngle(sectorDrawRule.transform(event), event.activePointIndex, WHOLE_DEGREE_RADIANS);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
//#endregion
|
|
692
791
|
//#region src/draw-rules/area7.ts
|
|
693
792
|
/**
|
|
694
793
|
* Area7 anchor draw rule for Attack By Fire.
|
|
@@ -716,20 +815,20 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
|
|
|
716
815
|
});
|
|
717
816
|
//#endregion
|
|
718
817
|
//#region src/draw-rules/line1.ts
|
|
719
|
-
function derive$
|
|
818
|
+
function derive$8(points) {
|
|
720
819
|
return clonePositions(points);
|
|
721
820
|
}
|
|
722
821
|
const line1DrawRule = {
|
|
723
822
|
id: "line1",
|
|
724
823
|
minimumUserPoints: 2,
|
|
725
|
-
derive: derive$
|
|
824
|
+
derive: derive$8,
|
|
726
825
|
transform(event) {
|
|
727
|
-
return derive$
|
|
826
|
+
return derive$8(event.next);
|
|
728
827
|
}
|
|
729
828
|
};
|
|
730
829
|
//#endregion
|
|
731
830
|
//#region src/draw-rules/line3.ts
|
|
732
|
-
function derive$
|
|
831
|
+
function derive$7(points) {
|
|
733
832
|
return clonePositions(points);
|
|
734
833
|
}
|
|
735
834
|
/**
|
|
@@ -744,23 +843,23 @@ const line3DrawRule = {
|
|
|
744
843
|
id: "line3",
|
|
745
844
|
minimumUserPoints: 3,
|
|
746
845
|
canonicalPointCount: 3,
|
|
747
|
-
derive: derive$
|
|
846
|
+
derive: derive$7,
|
|
748
847
|
transform(event) {
|
|
749
|
-
return derive$
|
|
848
|
+
return derive$7(event.next);
|
|
750
849
|
}
|
|
751
850
|
};
|
|
752
851
|
//#endregion
|
|
753
852
|
//#region src/draw-rules/line9.ts
|
|
754
|
-
function derive$
|
|
853
|
+
function derive$6(points) {
|
|
755
854
|
return clonePositions(points);
|
|
756
855
|
}
|
|
757
856
|
const line9DrawRule = {
|
|
758
857
|
id: "line9",
|
|
759
858
|
minimumUserPoints: 2,
|
|
760
859
|
canonicalPointCount: 2,
|
|
761
|
-
derive: derive$
|
|
860
|
+
derive: derive$6,
|
|
762
861
|
transform(event) {
|
|
763
|
-
return derive$
|
|
862
|
+
return derive$6(event.next);
|
|
764
863
|
}
|
|
765
864
|
};
|
|
766
865
|
//#endregion
|
|
@@ -867,7 +966,7 @@ const line24DrawRule = createMidpointPerpendicularDrawRule({
|
|
|
867
966
|
});
|
|
868
967
|
//#endregion
|
|
869
968
|
//#region src/draw-rules/line26.ts
|
|
870
|
-
function derive$
|
|
969
|
+
function derive$5(points) {
|
|
871
970
|
if (points.length === 2) {
|
|
872
971
|
const [p1, p2] = points;
|
|
873
972
|
const dx = p2[0] - p1[0];
|
|
@@ -906,9 +1005,9 @@ const line26DrawRule = {
|
|
|
906
1005
|
minimumUserPoints: 4,
|
|
907
1006
|
minimumPreviewPoints: 2,
|
|
908
1007
|
canonicalPointCount: 4,
|
|
909
|
-
derive: derive$
|
|
1008
|
+
derive: derive$5,
|
|
910
1009
|
transform(event) {
|
|
911
|
-
return derive$
|
|
1010
|
+
return derive$5(event.next);
|
|
912
1011
|
}
|
|
913
1012
|
};
|
|
914
1013
|
//#endregion
|
|
@@ -921,7 +1020,7 @@ function deriveArcPoint(p1, p2, p4) {
|
|
|
921
1020
|
if (!arc) return [(p2[0] + p4[0]) / 2, (p2[1] + p4[1]) / 2];
|
|
922
1021
|
return unproject(arc.midpoint[0], arc.midpoint[1]);
|
|
923
1022
|
}
|
|
924
|
-
function derive$
|
|
1023
|
+
function derive$4(points) {
|
|
925
1024
|
if (points.length < 2) return clonePositions(points);
|
|
926
1025
|
const [p1, p2] = points;
|
|
927
1026
|
if (points.length === 2) {
|
|
@@ -960,7 +1059,7 @@ const line27DrawRule = {
|
|
|
960
1059
|
minimumUserPoints: 3,
|
|
961
1060
|
minimumPreviewPoints: 2,
|
|
962
1061
|
canonicalPointCount: 4,
|
|
963
|
-
derive: derive$
|
|
1062
|
+
derive: derive$4,
|
|
964
1063
|
transform(event) {
|
|
965
1064
|
const { previous, next, activePointIndex } = event;
|
|
966
1065
|
if (activePointIndex === 0 && previous.length >= 4 && next.length >= 4) {
|
|
@@ -993,7 +1092,7 @@ const line27DrawRule = {
|
|
|
993
1092
|
};
|
|
994
1093
|
//#endregion
|
|
995
1094
|
//#region src/draw-rules/area8.ts
|
|
996
|
-
function derive$
|
|
1095
|
+
function derive$3(points) {
|
|
997
1096
|
if (points.length < 2) return clonePositions(points);
|
|
998
1097
|
if (points.length !== 2) return clonePositions(points.slice(0, 4));
|
|
999
1098
|
const p1 = clonePosition(points[0]);
|
|
@@ -1017,16 +1116,16 @@ const supportByFireDrawRule = {
|
|
|
1017
1116
|
id: "area8:support-by-fire",
|
|
1018
1117
|
minimumUserPoints: 2,
|
|
1019
1118
|
canonicalPointCount: 4,
|
|
1020
|
-
derive: derive$
|
|
1119
|
+
derive: derive$3,
|
|
1021
1120
|
transform(event) {
|
|
1022
1121
|
const { next } = event;
|
|
1023
1122
|
if (next.length === 4) return clonePositions(next);
|
|
1024
|
-
return derive$
|
|
1123
|
+
return derive$3(next);
|
|
1025
1124
|
}
|
|
1026
1125
|
};
|
|
1027
1126
|
//#endregion
|
|
1028
1127
|
//#region src/draw-rules/axis1.ts
|
|
1029
|
-
function derive$
|
|
1128
|
+
function derive$2(points) {
|
|
1030
1129
|
if (points.length < 2) return clonePositions(points);
|
|
1031
1130
|
if (points.length === 2) {
|
|
1032
1131
|
const tip = clonePosition(points[0]);
|
|
@@ -1064,10 +1163,10 @@ const axis1DrawRule = {
|
|
|
1064
1163
|
id: "axis1",
|
|
1065
1164
|
minimumUserPoints: 2,
|
|
1066
1165
|
trailingFixedSlots: 1,
|
|
1067
|
-
derive: derive$
|
|
1166
|
+
derive: derive$2,
|
|
1068
1167
|
transform(event) {
|
|
1069
1168
|
const { previous, next, activePointIndex } = event;
|
|
1070
|
-
if (next.length < 3 || previous.length < 3) return derive$
|
|
1169
|
+
if (next.length < 3 || previous.length < 3) return derive$2(next);
|
|
1071
1170
|
if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
|
|
1072
1171
|
const metrics = calculateMetrics(previous);
|
|
1073
1172
|
if (!metrics) return clonePositions(next);
|
|
@@ -1120,7 +1219,7 @@ const counterattackByFireDrawRule = {
|
|
|
1120
1219
|
};
|
|
1121
1220
|
//#endregion
|
|
1122
1221
|
//#region src/draw-rules/area21.ts
|
|
1123
|
-
function derive$
|
|
1222
|
+
function derive$1(points) {
|
|
1124
1223
|
if (points.length === 2) return clonePositions([
|
|
1125
1224
|
points[0],
|
|
1126
1225
|
points[1],
|
|
@@ -1133,9 +1232,9 @@ const searchAreaDrawRule = {
|
|
|
1133
1232
|
minimumUserPoints: 3,
|
|
1134
1233
|
minimumPreviewPoints: 2,
|
|
1135
1234
|
canonicalPointCount: 3,
|
|
1136
|
-
derive: derive$
|
|
1235
|
+
derive: derive$1,
|
|
1137
1236
|
transform(event) {
|
|
1138
|
-
return derive$
|
|
1237
|
+
return derive$1(event.next);
|
|
1139
1238
|
}
|
|
1140
1239
|
};
|
|
1141
1240
|
//#endregion
|
|
@@ -1145,7 +1244,7 @@ const searchAreaDrawRule = {
|
|
|
1145
1244
|
* edge. P3 is the next corner, constrained to the perpendicular through P2.
|
|
1146
1245
|
* The generator derives the fourth corner.
|
|
1147
1246
|
*/
|
|
1148
|
-
function derive
|
|
1247
|
+
function derive(points) {
|
|
1149
1248
|
if (points.length < 2) return clonePositions(points);
|
|
1150
1249
|
const p1 = clonePosition(points[0]);
|
|
1151
1250
|
const p2 = clonePosition(points[1]);
|
|
@@ -1164,8 +1263,8 @@ function derive$1(points) {
|
|
|
1164
1263
|
frame?.pointAtNormalDistance(frame.signedNormalDistance(points[2])) ?? clonePosition(points[2])
|
|
1165
1264
|
];
|
|
1166
1265
|
}
|
|
1167
|
-
function guidePoints
|
|
1168
|
-
const canonical = derive
|
|
1266
|
+
function guidePoints(points) {
|
|
1267
|
+
const canonical = derive(points);
|
|
1169
1268
|
if (canonical.length < 3) return canonical;
|
|
1170
1269
|
const [c1, c2, c3] = canonical;
|
|
1171
1270
|
if (!c1 || !c2 || !c3) return canonical;
|
|
@@ -1191,16 +1290,16 @@ const rectangleDrawRule = {
|
|
|
1191
1290
|
minimumPreviewPoints: 2,
|
|
1192
1291
|
canonicalPointCount: 3,
|
|
1193
1292
|
showGuide: true,
|
|
1194
|
-
guidePoints
|
|
1195
|
-
derive
|
|
1293
|
+
guidePoints,
|
|
1294
|
+
derive,
|
|
1196
1295
|
transform(event) {
|
|
1197
1296
|
const { previous, next, activePointIndex } = event;
|
|
1198
|
-
if (previous.length < 3 || next.length < 3) return derive
|
|
1297
|
+
if (previous.length < 3 || next.length < 3) return derive(next);
|
|
1199
1298
|
const frame = createBaselineFrame(previous[0], previous[1], {
|
|
1200
1299
|
origin: "p2",
|
|
1201
1300
|
normal: "right"
|
|
1202
1301
|
});
|
|
1203
|
-
if (!frame) return derive
|
|
1302
|
+
if (!frame) return derive(next);
|
|
1204
1303
|
if (activePointIndex === 0) return [
|
|
1205
1304
|
snapToAxis(next[0], frame.p2, frame.direction),
|
|
1206
1305
|
clonePosition(previous[1]),
|
|
@@ -1225,53 +1324,6 @@ const rectangleDrawRule = {
|
|
|
1225
1324
|
p3
|
|
1226
1325
|
];
|
|
1227
1326
|
}
|
|
1228
|
-
return derive$1(next);
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
//#endregion
|
|
1232
|
-
//#region src/draw-rules/sector.ts
|
|
1233
|
-
const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
|
|
1234
|
-
/**
|
|
1235
|
-
* Sector — anchor + inner-left edge + outer-right edge. The two radial points
|
|
1236
|
-
* independently control a radius and a true-north azimuth. Dragging the anchor
|
|
1237
|
-
* translates both edge points so the sector retains its size and orientation.
|
|
1238
|
-
*/
|
|
1239
|
-
function derive(points) {
|
|
1240
|
-
return clonePositions(points.slice(0, 3));
|
|
1241
|
-
}
|
|
1242
|
-
function guidePoints(points) {
|
|
1243
|
-
return points.length < 3 ? clonePositions(points) : [];
|
|
1244
|
-
}
|
|
1245
|
-
function constrainAngles(points, activePointIndex) {
|
|
1246
|
-
const next = clonePositions(points);
|
|
1247
|
-
if (activePointIndex !== 1 && activePointIndex !== 2) return next;
|
|
1248
|
-
const anchor = next[0];
|
|
1249
|
-
const point = next[activePointIndex];
|
|
1250
|
-
if (!anchor || !point) return next;
|
|
1251
|
-
const range = haversineDistance(anchor, point);
|
|
1252
|
-
const bearing = sphericalBearing(anchor, point);
|
|
1253
|
-
next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / SECTOR_ANGLE_SNAP_RADIANS) * SECTOR_ANGLE_SNAP_RADIANS), ...point.slice(2)];
|
|
1254
|
-
return next;
|
|
1255
|
-
}
|
|
1256
|
-
const sectorDrawRule = {
|
|
1257
|
-
id: "sector:anchor-radii",
|
|
1258
|
-
minimumUserPoints: 3,
|
|
1259
|
-
canonicalPointCount: 3,
|
|
1260
|
-
showGuide: true,
|
|
1261
|
-
guidePoints,
|
|
1262
|
-
constrainAngles,
|
|
1263
|
-
derive,
|
|
1264
|
-
transform(event) {
|
|
1265
|
-
const { previous, next, activePointIndex } = event;
|
|
1266
|
-
if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
|
|
1267
|
-
const dx = next[0][0] - previous[0][0];
|
|
1268
|
-
const dy = next[0][1] - previous[0][1];
|
|
1269
|
-
return [
|
|
1270
|
-
clonePosition(next[0]),
|
|
1271
|
-
[previous[1][0] + dx, previous[1][1] + dy],
|
|
1272
|
-
[previous[2][0] + dx, previous[2][1] + dy]
|
|
1273
|
-
];
|
|
1274
|
-
}
|
|
1275
1327
|
return derive(next);
|
|
1276
1328
|
}
|
|
1277
1329
|
};
|
|
@@ -2103,6 +2155,43 @@ function buildGappedLine(verts, gaps) {
|
|
|
2103
2155
|
return parts;
|
|
2104
2156
|
}
|
|
2105
2157
|
//#endregion
|
|
2158
|
+
//#region src/style.ts
|
|
2159
|
+
/**
|
|
2160
|
+
* Per-feature style hint pinning a filled part to a solid interior. Attach to
|
|
2161
|
+
* a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
|
|
2162
|
+
* glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
|
|
2163
|
+
* or measure layer never bleeds into a small silhouette and ruins its
|
|
2164
|
+
* legibility. The renderer only reads style hints, so one shared instance is
|
|
2165
|
+
* safe to reuse across every accent feature.
|
|
2166
|
+
*/
|
|
2167
|
+
const SOLID_ACCENT_FILL = { fillPattern: "solid" };
|
|
2168
|
+
/**
|
|
2169
|
+
* Ultimate fallback for the symbol color when no `color` or per-channel
|
|
2170
|
+
* override is supplied at any layer. Keeps a zero-config render monocolor
|
|
2171
|
+
* black and guarantees filled parts still render filled. See ADR-0011.
|
|
2172
|
+
*/
|
|
2173
|
+
const DEFAULT_SYMBOL_COLOR = "#000000";
|
|
2174
|
+
//#endregion
|
|
2175
|
+
//#region src/portrayal.ts
|
|
2176
|
+
const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
|
|
2177
|
+
const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
|
|
2178
|
+
const DEFAULT_LINE_CAP = "round";
|
|
2179
|
+
const DEFAULT_LINE_JOIN = "round";
|
|
2180
|
+
const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
|
|
2181
|
+
const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
|
|
2182
|
+
min: 8,
|
|
2183
|
+
max: 24
|
|
2184
|
+
};
|
|
2185
|
+
/** Shared absent-value portrayal defaults; render output remains sparse. */
|
|
2186
|
+
const DEFAULT_PORTRAYAL = Object.freeze({
|
|
2187
|
+
symbolColor: DEFAULT_SYMBOL_COLOR,
|
|
2188
|
+
strokeWidthCssPixels: 2,
|
|
2189
|
+
strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
|
|
2190
|
+
lineCap: DEFAULT_LINE_CAP,
|
|
2191
|
+
lineJoin: DEFAULT_LINE_JOIN,
|
|
2192
|
+
labelHeightCssPixels: 14
|
|
2193
|
+
});
|
|
2194
|
+
//#endregion
|
|
2106
2195
|
//#region src/internal/angle-utils.ts
|
|
2107
2196
|
/**
|
|
2108
2197
|
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
@@ -2609,10 +2698,50 @@ function pushEndLabels(out, verts, textForEnd, options = {}) {
|
|
|
2609
2698
|
* measurement is available at generator time.
|
|
2610
2699
|
*/
|
|
2611
2700
|
const LABEL_CHAR_ASPECT_RATIO = .6;
|
|
2701
|
+
/**
|
|
2702
|
+
* Half a single glyph's width, as a ratio of its height — the base boundary
|
|
2703
|
+
* gap a measure reserves for one intrinsic capital letter (Retain's R,
|
|
2704
|
+
* Contain's C). Wider than half `LABEL_CHAR_ASPECT_RATIO` because these
|
|
2705
|
+
* glyphs are stamped alone rather than averaged over a word.
|
|
2706
|
+
*/
|
|
2707
|
+
const LABEL_HALF_GLYPH_RATIO = .45;
|
|
2612
2708
|
/** Estimated rendered width of `text` given its resolved label height in meters. */
|
|
2613
2709
|
function estimatedTextWidth(text, textHeightMeters) {
|
|
2614
2710
|
return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
|
|
2615
2711
|
}
|
|
2712
|
+
/** Resolve the label's final display size, then measure and convert it to construction metres. */
|
|
2713
|
+
function resolveTextMetrics(text, options, context, style = "regular") {
|
|
2714
|
+
const legacyHeight = resolveLabelOffsetMeters(options, 1);
|
|
2715
|
+
const constructionScale = context?.constructionMetersPerCssPixel;
|
|
2716
|
+
if (!(constructionScale !== void 0 && constructionScale > 0)) return {
|
|
2717
|
+
width: estimatedTextWidth(text, legacyHeight),
|
|
2718
|
+
height: legacyHeight
|
|
2719
|
+
};
|
|
2720
|
+
let sizeCssPixels;
|
|
2721
|
+
if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
|
|
2722
|
+
else if (options.labelSize !== void 0) {
|
|
2723
|
+
const requested = options.labelSize / constructionScale;
|
|
2724
|
+
const requestedBand = context?.labelSizeClampCssPixels;
|
|
2725
|
+
const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
|
|
2726
|
+
const min = Math.min(band.min, band.max);
|
|
2727
|
+
const max = Math.max(band.min, band.max);
|
|
2728
|
+
sizeCssPixels = Math.min(max, Math.max(min, requested));
|
|
2729
|
+
} else sizeCssPixels = 14;
|
|
2730
|
+
const fallbackHeight = sizeCssPixels * constructionScale;
|
|
2731
|
+
const measured = context?.measureText?.({
|
|
2732
|
+
text,
|
|
2733
|
+
style,
|
|
2734
|
+
sizeCssPixels
|
|
2735
|
+
});
|
|
2736
|
+
if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
|
|
2737
|
+
width: estimatedTextWidth(text, fallbackHeight),
|
|
2738
|
+
height: fallbackHeight
|
|
2739
|
+
};
|
|
2740
|
+
return {
|
|
2741
|
+
width: measured.widthCssPixels * constructionScale,
|
|
2742
|
+
height: measured.heightCssPixels * constructionScale
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2616
2745
|
/** The end-label text for a phase-line naming, or `""` when unnamed. */
|
|
2617
2746
|
function phaseLineLabelText(name, includePrefix) {
|
|
2618
2747
|
if (!name) return "";
|
|
@@ -2624,17 +2753,17 @@ function phaseLineLabelText(name, includePrefix) {
|
|
|
2624
2753
|
* {@link pushEndLabels}. When another beyond-tip label is present, its
|
|
2625
2754
|
* estimated width is added so the hostile marker remains outermost.
|
|
2626
2755
|
*/
|
|
2627
|
-
function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "") {
|
|
2756
|
+
function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "", context) {
|
|
2628
2757
|
if (!hostileText) return;
|
|
2629
2758
|
const frames = endFrames(verts);
|
|
2630
2759
|
const sizeProps = labelSizeProps(options);
|
|
2631
|
-
const textHeight = resolveLabelOffsetMeters(options, 1);
|
|
2632
|
-
const clearance = resolveLabelOffsetMeters(options, END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0));
|
|
2633
2760
|
for (const end of ["start", "end"]) {
|
|
2634
2761
|
const frame = frames[end];
|
|
2635
2762
|
if (!frame) continue;
|
|
2636
2763
|
const occupiedText = typeof occupiedTextForEnd === "string" ? occupiedTextForEnd : occupiedTextForEnd(end);
|
|
2637
|
-
const
|
|
2764
|
+
const metrics = resolveTextMetrics(occupiedText || hostileText, options, context);
|
|
2765
|
+
const clearance = (END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * metrics.height;
|
|
2766
|
+
const distance = clearance + (occupiedText ? metrics.width + clearance : 0);
|
|
2638
2767
|
pushLabel(out, vecAdd(frame.point, vecScale(frame.along, distance)), hostileText, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end", "N", `label:N:${end}`);
|
|
2639
2768
|
}
|
|
2640
2769
|
}
|
|
@@ -2693,7 +2822,7 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
|
|
|
2693
2822
|
* (via {@link pushEndLabels} or {@link pushEndLabelsAbove}, per `placement`)
|
|
2694
2823
|
* as a complete FeatureCollection.
|
|
2695
2824
|
*/
|
|
2696
|
-
function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
|
|
2825
|
+
function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
|
|
2697
2826
|
const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
2698
2827
|
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
2699
2828
|
smooth,
|
|
@@ -2713,7 +2842,7 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
|
|
|
2713
2842
|
else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
|
|
2714
2843
|
const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
|
|
2715
2844
|
if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
|
|
2716
|
-
pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""));
|
|
2845
|
+
pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
|
|
2717
2846
|
features.push(...labelFeatures);
|
|
2718
2847
|
return {
|
|
2719
2848
|
type: "FeatureCollection",
|
|
@@ -2879,10 +3008,9 @@ function nonNegative$1(value, fallback) {
|
|
|
2879
3008
|
* later `buildGappedLine` walk); `segments`/`totalLength` describe the full
|
|
2880
3009
|
* route that anchors the default label placement.
|
|
2881
3010
|
*/
|
|
2882
|
-
function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}) {
|
|
3011
|
+
function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}, context) {
|
|
2883
3012
|
if (!text) return void 0;
|
|
2884
|
-
const textHeight =
|
|
2885
|
-
const textWidth = estimatedTextWidth(text, textHeight);
|
|
3013
|
+
const { width: textWidth, height: textHeight } = resolveTextMetrics(text, options, context);
|
|
2886
3014
|
const clearance = (LABEL_GAP_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * textHeight;
|
|
2887
3015
|
const frame = pointAlongPolyline(segments, totalLength, fraction);
|
|
2888
3016
|
const rotation = placement.rotation ?? (frame ? labelRotationAlong(frame.along) : 0);
|
|
@@ -2943,7 +3071,7 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
|
|
|
2943
3071
|
shaftVerts.push(innerTip);
|
|
2944
3072
|
}
|
|
2945
3073
|
const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
|
|
2946
|
-
const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T));
|
|
3074
|
+
const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
|
|
2947
3075
|
const lines = [...buildGappedLine(shaftVerts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
|
|
2948
3076
|
const features = [{
|
|
2949
3077
|
type: "Feature",
|
|
@@ -3227,7 +3355,7 @@ function createDirectionOfAttackAviation(coordinates, options = {}, textAmplifie
|
|
|
3227
3355
|
const bowTieB = vecSub(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
3228
3356
|
const bowTieC = vecAdd(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
3229
3357
|
const bowTieD = vecSub(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
3230
|
-
const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T));
|
|
3358
|
+
const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
|
|
3231
3359
|
const features = [{
|
|
3232
3360
|
type: "Feature",
|
|
3233
3361
|
properties: { part: "direction-of-attack-aviation" },
|
|
@@ -3807,9 +3935,12 @@ function ringLabelAnchor(verts) {
|
|
|
3807
3935
|
* overlaps the finite contacted edge; intrinsic markers always mask. Callers
|
|
3808
3936
|
* own marker names, anchors, rotation, clearance, placement keys, and fields.
|
|
3809
3937
|
*/
|
|
3810
|
-
function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
|
|
3938
|
+
function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
|
|
3811
3939
|
const { rotation, textHeight, clearance, sizeProps } = options;
|
|
3812
|
-
const
|
|
3940
|
+
const metrics = resolveTextMetrics(text, labelOptions ?? { labelSize: textHeight }, context);
|
|
3941
|
+
const textWidth = metrics.width;
|
|
3942
|
+
const resolvedTextHeight = metrics.height;
|
|
3943
|
+
const resolvedClearance = textHeight > 0 ? clearance * (resolvedTextHeight / textHeight) : clearance;
|
|
3813
3944
|
const gaps = [];
|
|
3814
3945
|
for (const anchor of anchors) {
|
|
3815
3946
|
const labelPlacementKey = options.placementKeyPrefix ? `${options.placementKeyPrefix}:${anchor.side}` : void 0;
|
|
@@ -3819,21 +3950,21 @@ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPl
|
|
|
3819
3950
|
pushLabel(out, labelPoint, text, options.followPlacementRotation ? boxRotation : rotation, sizeProps, void 0, options.amplifierField, labelPlacementKey);
|
|
3820
3951
|
const contact = placement.position !== void 0 || options.rescanDefaultAnchors === true || anchor.contact === void 0 ? nearestPointOnPolyline(maskVerts, labelPoint) : anchor.contact;
|
|
3821
3952
|
if (!contact) continue;
|
|
3822
|
-
if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth,
|
|
3823
|
-
const tangentExtent = labelHalfExtent(contact.along, textWidth,
|
|
3953
|
+
if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, resolvedTextHeight, boxRotation)) continue;
|
|
3954
|
+
const tangentExtent = labelHalfExtent(contact.along, textWidth, resolvedTextHeight, boxRotation) + resolvedClearance;
|
|
3824
3955
|
pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
|
|
3825
3956
|
}
|
|
3826
3957
|
return gaps;
|
|
3827
3958
|
}
|
|
3828
3959
|
/** Emits movable Field N markers using the generic boundary-marker knockout path. */
|
|
3829
|
-
function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
|
|
3960
|
+
function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
|
|
3830
3961
|
return pushBoundaryMarkers(out, maskVerts, text, anchors, {
|
|
3831
3962
|
...options,
|
|
3832
3963
|
placementKeyPrefix: "label:N",
|
|
3833
3964
|
amplifierField: "N"
|
|
3834
|
-
}, amplifierPlacements);
|
|
3965
|
+
}, amplifierPlacements, context, labelOptions);
|
|
3835
3966
|
}
|
|
3836
|
-
function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary) {
|
|
3967
|
+
function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary, context) {
|
|
3837
3968
|
if (ringVerts.length < 2) return [];
|
|
3838
3969
|
const sizeProps = labelSizeProps(options);
|
|
3839
3970
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
@@ -3876,7 +4007,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements
|
|
|
3876
4007
|
clearance: (ENY_CLEARANCE_RATIO + padding) * textHeight + Math.max(0, maskBoundary?.extraClearanceMeters ?? 0),
|
|
3877
4008
|
sizeProps,
|
|
3878
4009
|
rescanDefaultAnchors: maskRingVerts !== ringVerts
|
|
3879
|
-
}, amplifierPlacements);
|
|
4010
|
+
}, amplifierPlacements, context, options);
|
|
3880
4011
|
}
|
|
3881
4012
|
/**
|
|
3882
4013
|
* Boundary + fill features for a pattern-filled area (e.g. Limited Access Area,
|
|
@@ -3955,11 +4086,11 @@ function boundaryFeature(verts, gaps) {
|
|
|
3955
4086
|
* echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
|
|
3956
4087
|
* common (no `N`) case is unaffected.
|
|
3957
4088
|
*/
|
|
3958
|
-
function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
4089
|
+
function labeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
|
|
3959
4090
|
const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
|
|
3960
4091
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
|
|
3961
4092
|
const labelFeatures = [];
|
|
3962
|
-
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
|
|
4093
|
+
const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context))];
|
|
3963
4094
|
features.push(...labelFeatures);
|
|
3964
4095
|
return {
|
|
3965
4096
|
type: "FeatureCollection",
|
|
@@ -4003,7 +4134,7 @@ const AREA_METADATA = {
|
|
|
4003
4134
|
* marker, per {@link labeledAreaFeatures}.
|
|
4004
4135
|
*/
|
|
4005
4136
|
function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4006
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
|
|
4137
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements, context);
|
|
4007
4138
|
}
|
|
4008
4139
|
const AREA = defineControlMeasure({
|
|
4009
4140
|
metadata: AREA_METADATA,
|
|
@@ -4022,23 +4153,6 @@ const AREA = defineControlMeasure({
|
|
|
4022
4153
|
}
|
|
4023
4154
|
});
|
|
4024
4155
|
//#endregion
|
|
4025
|
-
//#region src/style.ts
|
|
4026
|
-
/**
|
|
4027
|
-
* Per-feature style hint pinning a filled part to a solid interior. Attach to
|
|
4028
|
-
* a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
|
|
4029
|
-
* glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
|
|
4030
|
-
* or measure layer never bleeds into a small silhouette and ruins its
|
|
4031
|
-
* legibility. The renderer only reads style hints, so one shared instance is
|
|
4032
|
-
* safe to reuse across every accent feature.
|
|
4033
|
-
*/
|
|
4034
|
-
const SOLID_ACCENT_FILL = { fillPattern: "solid" };
|
|
4035
|
-
/**
|
|
4036
|
-
* Ultimate fallback for the symbol color when no `color` or per-channel
|
|
4037
|
-
* override is supplied at any layer. Keeps a zero-config render monocolor
|
|
4038
|
-
* black and guarantees filled parts still render filled. See ADR-0011.
|
|
4039
|
-
*/
|
|
4040
|
-
const DEFAULT_SYMBOL_COLOR = "#000000";
|
|
4041
|
-
//#endregion
|
|
4042
4156
|
//#region src/generators/cm15-maneuver-areas/area-defense.ts
|
|
4043
4157
|
const DEFAULT_AREA_DEFENSE_OPTIONS = {
|
|
4044
4158
|
arrowOpeningAngle: 30,
|
|
@@ -4122,13 +4236,13 @@ const AREA_DEFENSE_METADATA = {
|
|
|
4122
4236
|
}
|
|
4123
4237
|
]
|
|
4124
4238
|
};
|
|
4125
|
-
function pointOnCircle(center, radius, angle) {
|
|
4239
|
+
function pointOnCircle$1(center, radius, angle) {
|
|
4126
4240
|
return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
|
|
4127
4241
|
}
|
|
4128
|
-
function sampleArc$
|
|
4242
|
+
function sampleArc$2(center, radius, start, end) {
|
|
4129
4243
|
const span = end - start;
|
|
4130
4244
|
const segments = Math.max(8, Math.round(48 * Math.abs(span) / Math.PI));
|
|
4131
|
-
return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle(center, radius, start + span * index / segments));
|
|
4245
|
+
return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle$1(center, radius, start + span * index / segments));
|
|
4132
4246
|
}
|
|
4133
4247
|
function openArrowhead(tip, direction, length) {
|
|
4134
4248
|
const base = vecSub(tip, [direction[0] * length, direction[1] * length]);
|
|
@@ -4159,8 +4273,8 @@ function createAreaDefense(coordinates, options = {}) {
|
|
|
4159
4273
|
const upperEnd = upperTipAngle;
|
|
4160
4274
|
const lowerStart = labelCenter - labelHalf;
|
|
4161
4275
|
const lowerEnd = upperTipAngle + arrowHalf * 2 - Math.PI * 2;
|
|
4162
|
-
const upperArc = sampleArc$
|
|
4163
|
-
const lowerArc = sampleArc$
|
|
4276
|
+
const upperArc = sampleArc$2(center, radius, upperStart, upperEnd);
|
|
4277
|
+
const lowerArc = sampleArc$2(center, radius, lowerStart, lowerEnd);
|
|
4164
4278
|
const arrowLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
|
|
4165
4279
|
const upperDirection = [-Math.sin(upperEnd), Math.cos(upperEnd)];
|
|
4166
4280
|
const lowerDirection = [Math.sin(lowerEnd), -Math.cos(lowerEnd)];
|
|
@@ -4185,17 +4299,17 @@ function createAreaDefense(coordinates, options = {}) {
|
|
|
4185
4299
|
const barbPolygons = [];
|
|
4186
4300
|
for (const arc of barbArcs) for (let ordinal = 1; ordinal <= arc.count; ordinal++) {
|
|
4187
4301
|
const angle = arc.start + (arc.end - arc.start) * ordinal / (arc.count + 1);
|
|
4188
|
-
const left = pointOnCircle(center, radius, angle - barbHalfAngle);
|
|
4189
|
-
const right = pointOnCircle(center, radius, angle + barbHalfAngle);
|
|
4302
|
+
const left = pointOnCircle$1(center, radius, angle - barbHalfAngle);
|
|
4303
|
+
const right = pointOnCircle$1(center, radius, angle + barbHalfAngle);
|
|
4190
4304
|
const ring = [
|
|
4191
4305
|
left,
|
|
4192
|
-
pointOnCircle(center, radius + barbLength, angle),
|
|
4306
|
+
pointOnCircle$1(center, radius + barbLength, angle),
|
|
4193
4307
|
right,
|
|
4194
4308
|
left
|
|
4195
4309
|
].map((point) => unproject(point[0], point[1]));
|
|
4196
4310
|
barbPolygons.push([ring]);
|
|
4197
4311
|
}
|
|
4198
|
-
const labelPoint = pointOnCircle(center, radius, labelCenter);
|
|
4312
|
+
const labelPoint = pointOnCircle$1(center, radius, labelCenter);
|
|
4199
4313
|
return {
|
|
4200
4314
|
type: "FeatureCollection",
|
|
4201
4315
|
features: [
|
|
@@ -4283,7 +4397,7 @@ const AREA_OF_OPERATIONS_METADATA = {
|
|
|
4283
4397
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
4284
4398
|
*/
|
|
4285
4399
|
function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4286
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
|
|
4400
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements, context);
|
|
4287
4401
|
}
|
|
4288
4402
|
const AREA_OF_OPERATIONS = defineControlMeasure({
|
|
4289
4403
|
metadata: AREA_OF_OPERATIONS_METADATA,
|
|
@@ -4342,7 +4456,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
|
|
|
4342
4456
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
4343
4457
|
*/
|
|
4344
4458
|
function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4345
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
|
|
4459
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements, context);
|
|
4346
4460
|
}
|
|
4347
4461
|
const NAMED_AREA_OF_INTEREST = defineControlMeasure({
|
|
4348
4462
|
metadata: NAMED_AREA_OF_INTEREST_METADATA,
|
|
@@ -4401,7 +4515,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
|
|
|
4401
4515
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
4402
4516
|
*/
|
|
4403
4517
|
function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4404
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
|
|
4518
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements, context);
|
|
4405
4519
|
}
|
|
4406
4520
|
const TARGET_AREA_OF_INTEREST = defineControlMeasure({
|
|
4407
4521
|
metadata: TARGET_AREA_OF_INTEREST_METADATA,
|
|
@@ -4496,7 +4610,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}, contex
|
|
|
4496
4610
|
const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
|
|
4497
4611
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
|
|
4498
4612
|
const enyLabels = [];
|
|
4499
|
-
const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
|
|
4613
|
+
const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements, void 0, context) : [])];
|
|
4500
4614
|
let minX = Infinity;
|
|
4501
4615
|
let maxX = -Infinity;
|
|
4502
4616
|
for (const [x] of verts) {
|
|
@@ -4572,13 +4686,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
|
|
|
4572
4686
|
* before carving the boundary, so either (or both) opens it into a gapped
|
|
4573
4687
|
* `MultiLineString`; with neither, the boundary is a closed `Polygon`.
|
|
4574
4688
|
*/
|
|
4575
|
-
function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
|
|
4689
|
+
function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
|
|
4576
4690
|
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;
|
|
4577
4691
|
const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
|
|
4578
4692
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
|
|
4579
4693
|
const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
|
|
4580
4694
|
const labelFeatures = [];
|
|
4581
|
-
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
|
|
4695
|
+
const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context);
|
|
4582
4696
|
const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
|
|
4583
4697
|
if (strokes.length > 0) features.push({
|
|
4584
4698
|
type: "Feature",
|
|
@@ -4654,7 +4768,7 @@ const BASE_CAMP_METADATA = {
|
|
|
4654
4768
|
* boundary, per {@link echelonLabeledAreaFeatures}.
|
|
4655
4769
|
*/
|
|
4656
4770
|
function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4657
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
|
|
4771
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements, context);
|
|
4658
4772
|
}
|
|
4659
4773
|
const BASE_CAMP = defineControlMeasure({
|
|
4660
4774
|
metadata: BASE_CAMP_METADATA,
|
|
@@ -4712,7 +4826,7 @@ const GUERRILLA_BASE_METADATA = {
|
|
|
4712
4826
|
* straddling the boundary, per {@link echelonLabeledAreaFeatures}.
|
|
4713
4827
|
*/
|
|
4714
4828
|
function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4715
|
-
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
|
|
4829
|
+
return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements, context);
|
|
4716
4830
|
}
|
|
4717
4831
|
const GUERRILLA_BASE = defineControlMeasure({
|
|
4718
4832
|
metadata: GUERRILLA_BASE_METADATA,
|
|
@@ -4774,7 +4888,7 @@ const GENERIC_C2_AREA_METADATA = {
|
|
|
4774
4888
|
* `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
|
|
4775
4889
|
*/
|
|
4776
4890
|
function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4777
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
|
|
4891
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements, context);
|
|
4778
4892
|
}
|
|
4779
4893
|
const GENERIC_C2_AREA = defineControlMeasure({
|
|
4780
4894
|
metadata: GENERIC_C2_AREA_METADATA,
|
|
@@ -4829,7 +4943,7 @@ const ASSEMBLY_AREA_METADATA = {
|
|
|
4829
4943
|
* `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
4830
4944
|
*/
|
|
4831
4945
|
function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
4832
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
|
|
4946
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements, context);
|
|
4833
4947
|
}
|
|
4834
4948
|
const ASSEMBLY_AREA = defineControlMeasure({
|
|
4835
4949
|
metadata: ASSEMBLY_AREA_METADATA,
|
|
@@ -5021,13 +5135,13 @@ function ditchPaths(projectedPoints, options) {
|
|
|
5021
5135
|
function buildAntitankDitchTriangles(basePath, options) {
|
|
5022
5136
|
return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
5023
5137
|
}
|
|
5024
|
-
function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}) {
|
|
5138
|
+
function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
5025
5139
|
const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
|
|
5026
5140
|
const triangles = buildAntitankDitchTriangles(basePath, options);
|
|
5027
5141
|
const baseLine = options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]);
|
|
5028
5142
|
const triangleLines = triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])));
|
|
5029
5143
|
const labels = [];
|
|
5030
|
-
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
|
|
5144
|
+
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
|
|
5031
5145
|
return {
|
|
5032
5146
|
type: "FeatureCollection",
|
|
5033
5147
|
features: [{
|
|
@@ -5040,13 +5154,13 @@ function createAntitankDitchUnderConstruction(positions, options = {}, textAmpli
|
|
|
5040
5154
|
}, ...labels]
|
|
5041
5155
|
};
|
|
5042
5156
|
}
|
|
5043
|
-
function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}) {
|
|
5157
|
+
function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
5044
5158
|
const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
|
|
5045
5159
|
const polygons = buildAntitankDitchTriangles(basePath, options).map((triangle) => {
|
|
5046
5160
|
return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
|
|
5047
5161
|
});
|
|
5048
5162
|
const labels = [];
|
|
5049
|
-
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
|
|
5163
|
+
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
|
|
5050
5164
|
return {
|
|
5051
5165
|
type: "FeatureCollection",
|
|
5052
5166
|
features: [{
|
|
@@ -5136,7 +5250,7 @@ function generateAntitankWallPoints(projectedPoints, toothHeight, toothWidthRati
|
|
|
5136
5250
|
}
|
|
5137
5251
|
return wallPoints;
|
|
5138
5252
|
}
|
|
5139
|
-
function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
|
|
5253
|
+
function createAntitankWall(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
5140
5254
|
const toothHeight = calculateAntitankDitchHeight(options);
|
|
5141
5255
|
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
|
|
5142
5256
|
const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
|
|
@@ -5144,7 +5258,7 @@ function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
|
|
|
5144
5258
|
const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
|
|
5145
5259
|
const wallLine = generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]));
|
|
5146
5260
|
const labels = [];
|
|
5147
|
-
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
|
|
5261
|
+
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
|
|
5148
5262
|
return {
|
|
5149
5263
|
type: "FeatureCollection",
|
|
5150
5264
|
features: [{
|
|
@@ -5591,7 +5705,7 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
|
|
|
5591
5705
|
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
5592
5706
|
name: textAmplifiers.T,
|
|
5593
5707
|
hostile: textAmplifiers.N
|
|
5594
|
-
}, options, context.amplifierPlacements);
|
|
5708
|
+
}, options, context.amplifierPlacements, void 0, context);
|
|
5595
5709
|
const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
|
|
5596
5710
|
const features = [];
|
|
5597
5711
|
if (boundaryCoords.length > 0) features.push({
|
|
@@ -5628,41 +5742,253 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
|
|
|
5628
5742
|
features
|
|
5629
5743
|
};
|
|
5630
5744
|
}
|
|
5631
|
-
/**
|
|
5632
|
-
* `freezeOrientation` hook (ADR-0023) shared by Battle Position and Strong
|
|
5633
|
-
* Point: pins the echelon anchor to its current bottom-edge fraction so a
|
|
5634
|
-
* subsequent rigid rotation of `controlPoints` (e.g. the transform box's
|
|
5635
|
-
* rotate grip) does not re-derive a different "bottom edge" against the
|
|
5636
|
-
* rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
|
|
5637
|
-
* factory every echelon-carrying area measure (Battle Position, Strong
|
|
5638
|
-
* Point, Base Camp, Guerrilla Base) builds its own instance from — this
|
|
5639
|
-
* export just binds Battle Position's own defaults, keeping strongPoint.ts's
|
|
5640
|
-
* existing import (and any other importer) unchanged.
|
|
5641
|
-
*/
|
|
5642
|
-
const freezeEchelonAnchor = makeFreezeEchelonAnchor({
|
|
5643
|
-
echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
|
|
5644
|
-
smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
|
|
5645
|
-
smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
|
|
5646
|
-
});
|
|
5647
|
-
const BATTLE_POSITION = defineControlMeasure({
|
|
5648
|
-
metadata: BATTLE_POSITION_METADATA,
|
|
5649
|
-
generator: createBattlePosition,
|
|
5650
|
-
defaultOptions: DEFAULT_BATTLE_POSITION_OPTIONS,
|
|
5651
|
-
rule: area1DrawRule,
|
|
5652
|
-
freezeOrientation: freezeEchelonAnchor,
|
|
5745
|
+
/**
|
|
5746
|
+
* `freezeOrientation` hook (ADR-0023) shared by Battle Position and Strong
|
|
5747
|
+
* Point: pins the echelon anchor to its current bottom-edge fraction so a
|
|
5748
|
+
* subsequent rigid rotation of `controlPoints` (e.g. the transform box's
|
|
5749
|
+
* rotate grip) does not re-derive a different "bottom edge" against the
|
|
5750
|
+
* rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
|
|
5751
|
+
* factory every echelon-carrying area measure (Battle Position, Strong
|
|
5752
|
+
* Point, Base Camp, Guerrilla Base) builds its own instance from — this
|
|
5753
|
+
* export just binds Battle Position's own defaults, keeping strongPoint.ts's
|
|
5754
|
+
* existing import (and any other importer) unchanged.
|
|
5755
|
+
*/
|
|
5756
|
+
const freezeEchelonAnchor = makeFreezeEchelonAnchor({
|
|
5757
|
+
echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
|
|
5758
|
+
smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
|
|
5759
|
+
smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
|
|
5760
|
+
});
|
|
5761
|
+
const BATTLE_POSITION = defineControlMeasure({
|
|
5762
|
+
metadata: BATTLE_POSITION_METADATA,
|
|
5763
|
+
generator: createBattlePosition,
|
|
5764
|
+
defaultOptions: DEFAULT_BATTLE_POSITION_OPTIONS,
|
|
5765
|
+
rule: area1DrawRule,
|
|
5766
|
+
freezeOrientation: freezeEchelonAnchor,
|
|
5767
|
+
previewSample: {
|
|
5768
|
+
controlPoints: [
|
|
5769
|
+
[-1, -.5],
|
|
5770
|
+
[1, -.5],
|
|
5771
|
+
[1, .5],
|
|
5772
|
+
[-1, .5]
|
|
5773
|
+
],
|
|
5774
|
+
textAmplifiers: { T: "ALFA" },
|
|
5775
|
+
options: {
|
|
5776
|
+
echelonSize: 200,
|
|
5777
|
+
smooth: true,
|
|
5778
|
+
labelSize: 96
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
});
|
|
5782
|
+
//#endregion
|
|
5783
|
+
//#region src/internal/arrowhead.ts
|
|
5784
|
+
/**
|
|
5785
|
+
* Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
|
|
5786
|
+
*
|
|
5787
|
+
* `tip` is the point of the arrow, `dir` the unit direction the arrow points,
|
|
5788
|
+
* `length` the distance from tip back to the base, and `width` the base width.
|
|
5789
|
+
* The returned ring is `[left, tip, right, left]`.
|
|
5790
|
+
*/
|
|
5791
|
+
function createArrowHeadRing(tip, dir, length, width) {
|
|
5792
|
+
const perp = [-dir[1], dir[0]];
|
|
5793
|
+
const base = vecSub(tip, vecScale(dir, length));
|
|
5794
|
+
const left = vecAdd(base, vecScale(perp, width / 2));
|
|
5795
|
+
const right = vecSub(base, vecScale(perp, width / 2));
|
|
5796
|
+
return [
|
|
5797
|
+
unproject(left[0], left[1]),
|
|
5798
|
+
unproject(tip[0], tip[1]),
|
|
5799
|
+
unproject(right[0], right[1]),
|
|
5800
|
+
unproject(left[0], left[1])
|
|
5801
|
+
];
|
|
5802
|
+
}
|
|
5803
|
+
//#endregion
|
|
5804
|
+
//#region src/internal/projected-arc.ts
|
|
5805
|
+
/** The arc point at `angle`, in projected meters. */
|
|
5806
|
+
function arcPointAt(center, startRadius, bulgeRadius, angle) {
|
|
5807
|
+
return vecAdd(center, vecAdd(vecScale(startRadius, Math.cos(angle)), vecScale(bulgeRadius, Math.sin(angle))));
|
|
5808
|
+
}
|
|
5809
|
+
/**
|
|
5810
|
+
* Samples the arc from `start` to `end` radians as unprojected positions.
|
|
5811
|
+
* `resolution` is the segment count a full half-turn would use, so the density
|
|
5812
|
+
* of a partial arc stays proportional to how much of the circle it covers.
|
|
5813
|
+
*/
|
|
5814
|
+
function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolution) {
|
|
5815
|
+
const segments = Math.max(2, Math.ceil(resolution * Math.abs(end - start) / Math.PI));
|
|
5816
|
+
return Array.from({ length: segments + 1 }, (_, index) => {
|
|
5817
|
+
const point = arcPointAt(center, startRadius, bulgeRadius, start + (end - start) * index / segments);
|
|
5818
|
+
return unproject(point[0], point[1]);
|
|
5819
|
+
});
|
|
5820
|
+
}
|
|
5821
|
+
//#endregion
|
|
5822
|
+
//#region src/generators/cm15-maneuver-areas/contain.ts
|
|
5823
|
+
const DEFAULT_CONTAIN_OPTIONS = {
|
|
5824
|
+
labelPadding: .2,
|
|
5825
|
+
resolution: 32
|
|
5826
|
+
};
|
|
5827
|
+
const CONTAIN_METADATA = {
|
|
5828
|
+
id: "contain",
|
|
5829
|
+
name: "Contain",
|
|
5830
|
+
description: "Stops, holds, or surrounds enemy forces and prevents them from withdrawing or using them elsewhere.",
|
|
5831
|
+
entity: "Maneuver Areas",
|
|
5832
|
+
entityType: "Battle Position",
|
|
5833
|
+
entitySubtype: "Contain",
|
|
5834
|
+
value: "151204",
|
|
5835
|
+
minCoordinates: 3,
|
|
5836
|
+
maxCoordinates: 3,
|
|
5837
|
+
geometry: "area",
|
|
5838
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
5839
|
+
paints: {
|
|
5840
|
+
stroke: true,
|
|
5841
|
+
fill: "none",
|
|
5842
|
+
text: true
|
|
5843
|
+
},
|
|
5844
|
+
drawRule: "Area5",
|
|
5845
|
+
capturesLabelSize: true,
|
|
5846
|
+
textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
|
|
5847
|
+
params: [{
|
|
5848
|
+
key: "labelPadding",
|
|
5849
|
+
presentationTier: "advanced",
|
|
5850
|
+
label: "Label padding",
|
|
5851
|
+
description: "Extra clearance around C and ENY, as a ratio of label height",
|
|
5852
|
+
type: "number",
|
|
5853
|
+
min: 0,
|
|
5854
|
+
max: 2,
|
|
5855
|
+
step: .05
|
|
5856
|
+
}, {
|
|
5857
|
+
key: "resolution",
|
|
5858
|
+
presentationTier: "advanced",
|
|
5859
|
+
label: "Resolution",
|
|
5860
|
+
description: "Number of segments used to approximate the semicircle",
|
|
5861
|
+
type: "number",
|
|
5862
|
+
min: 8,
|
|
5863
|
+
max: 128,
|
|
5864
|
+
step: 1
|
|
5865
|
+
}]
|
|
5866
|
+
};
|
|
5867
|
+
/** Where the ENY label sits along the arrow, as a ratio of its full length. */
|
|
5868
|
+
const HOSTILE_LABEL_POSITION_RATIO = .55;
|
|
5869
|
+
/** Creates the Contain semicircle, inward tics, center-pointing arrow, and C label. */
|
|
5870
|
+
function createContain(coordinates, options = {}, textAmplifiers = {}, context = {}) {
|
|
5871
|
+
const p1 = project(coordinates[0][0], coordinates[0][1]);
|
|
5872
|
+
const p2 = project(coordinates[1][0], coordinates[1][1]);
|
|
5873
|
+
const p3 = project(coordinates[2][0], coordinates[2][1]);
|
|
5874
|
+
const diameterLength = vecMag(vecSub(p2, p1));
|
|
5875
|
+
if (diameterLength < 1e-6) return {
|
|
5876
|
+
type: "FeatureCollection",
|
|
5877
|
+
features: []
|
|
5878
|
+
};
|
|
5879
|
+
const center = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
|
|
5880
|
+
const arrowVector = vecSub(p3, center);
|
|
5881
|
+
const arrowLength = vecMag(arrowVector);
|
|
5882
|
+
if (arrowLength < 1e-6) return {
|
|
5883
|
+
type: "FeatureCollection",
|
|
5884
|
+
features: []
|
|
5885
|
+
};
|
|
5886
|
+
const radius = diameterLength / 2;
|
|
5887
|
+
const openingDirection = vecNorm(arrowVector);
|
|
5888
|
+
const labelRotation = labelRotationAlong(openingDirection);
|
|
5889
|
+
const startRadius = vecSub(p1, center);
|
|
5890
|
+
const bulgeRadius = vecScale(openingDirection, -radius);
|
|
5891
|
+
const h = Math.min(radius * .45, Math.max(EPSILON, resolveLabelOffsetMeters(options, 1, radius * .12)));
|
|
5892
|
+
const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_CONTAIN_OPTIONS.labelPadding);
|
|
5893
|
+
const resolution = Math.round(Math.min(128, Math.max(8, options.resolution ?? DEFAULT_CONTAIN_OPTIONS.resolution)));
|
|
5894
|
+
const hostileText = textAmplifiers.N ?? "ENY";
|
|
5895
|
+
const labelHalfAngle = Math.min(Math.PI / 3, h * (LABEL_HALF_GLYPH_RATIO + labelPadding) / radius);
|
|
5896
|
+
const labelAngle = Math.PI / 2;
|
|
5897
|
+
const boundary = [sampleProjectedArc(center, startRadius, bulgeRadius, 0, labelAngle - labelHalfAngle, resolution), sampleProjectedArc(center, startRadius, bulgeRadius, labelAngle + labelHalfAngle, Math.PI, resolution)];
|
|
5898
|
+
const tics = [];
|
|
5899
|
+
const spacingAngle = h / radius;
|
|
5900
|
+
for (let angle = spacingAngle / 2; angle < Math.PI; angle += spacingAngle) {
|
|
5901
|
+
if (Math.abs(angle - labelAngle) <= labelHalfAngle) continue;
|
|
5902
|
+
const outer = arcPointAt(center, startRadius, bulgeRadius, angle);
|
|
5903
|
+
const inner = vecAdd(outer, vecScale(vecNorm(vecSub(center, outer)), h));
|
|
5904
|
+
tics.push([unproject(outer[0], outer[1]), unproject(inner[0], inner[1])]);
|
|
5905
|
+
}
|
|
5906
|
+
const arrowhead = createArrowHeadRing(center, vecScale(openingDirection, -1), h, h).slice(0, 3);
|
|
5907
|
+
const arrow = [];
|
|
5908
|
+
const hostilePoint = vecAdd(center, vecScale(arrowVector, HOSTILE_LABEL_POSITION_RATIO));
|
|
5909
|
+
if (hostileText) {
|
|
5910
|
+
const metrics = resolveTextMetrics(hostileText, options, context);
|
|
5911
|
+
const maximumHalfGap = arrowLength * Math.min(HOSTILE_LABEL_POSITION_RATIO, 1 - HOSTILE_LABEL_POSITION_RATIO);
|
|
5912
|
+
const halfGap = Math.min(maximumHalfGap, metrics.width / 2 + labelPadding * metrics.height);
|
|
5913
|
+
const farGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, halfGap));
|
|
5914
|
+
const nearGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, -halfGap));
|
|
5915
|
+
arrow.push([unproject(p3[0], p3[1]), unproject(farGapEdge[0], farGapEdge[1])], [unproject(nearGapEdge[0], nearGapEdge[1]), unproject(center[0], center[1])]);
|
|
5916
|
+
} else arrow.push([unproject(p3[0], p3[1]), unproject(center[0], center[1])]);
|
|
5917
|
+
arrow.push(arrowhead);
|
|
5918
|
+
const labelPoint = arcPointAt(center, startRadius, bulgeRadius, labelAngle);
|
|
5919
|
+
const sizeProps = labelSizeProps(options);
|
|
5920
|
+
const features = [
|
|
5921
|
+
{
|
|
5922
|
+
type: "Feature",
|
|
5923
|
+
properties: { part: "boundary" },
|
|
5924
|
+
geometry: {
|
|
5925
|
+
type: "MultiLineString",
|
|
5926
|
+
coordinates: boundary
|
|
5927
|
+
}
|
|
5928
|
+
},
|
|
5929
|
+
{
|
|
5930
|
+
type: "Feature",
|
|
5931
|
+
properties: { part: "tics" },
|
|
5932
|
+
geometry: {
|
|
5933
|
+
type: "MultiLineString",
|
|
5934
|
+
coordinates: tics
|
|
5935
|
+
}
|
|
5936
|
+
},
|
|
5937
|
+
{
|
|
5938
|
+
type: "Feature",
|
|
5939
|
+
properties: { part: "arrow" },
|
|
5940
|
+
geometry: {
|
|
5941
|
+
type: "MultiLineString",
|
|
5942
|
+
coordinates: arrow
|
|
5943
|
+
}
|
|
5944
|
+
},
|
|
5945
|
+
{
|
|
5946
|
+
type: "Feature",
|
|
5947
|
+
properties: {
|
|
5948
|
+
part: "label",
|
|
5949
|
+
labelPlacement: false,
|
|
5950
|
+
text: "C",
|
|
5951
|
+
rotation: labelRotation,
|
|
5952
|
+
...sizeProps
|
|
5953
|
+
},
|
|
5954
|
+
geometry: {
|
|
5955
|
+
type: "Point",
|
|
5956
|
+
coordinates: unproject(labelPoint[0], labelPoint[1])
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
];
|
|
5960
|
+
if (hostileText) features.push({
|
|
5961
|
+
type: "Feature",
|
|
5962
|
+
properties: {
|
|
5963
|
+
part: "label",
|
|
5964
|
+
labelPlacement: false,
|
|
5965
|
+
amplifierField: "N",
|
|
5966
|
+
text: hostileText,
|
|
5967
|
+
rotation: labelRotation,
|
|
5968
|
+
...sizeProps
|
|
5969
|
+
},
|
|
5970
|
+
geometry: {
|
|
5971
|
+
type: "Point",
|
|
5972
|
+
coordinates: unproject(hostilePoint[0], hostilePoint[1])
|
|
5973
|
+
}
|
|
5974
|
+
});
|
|
5975
|
+
return {
|
|
5976
|
+
type: "FeatureCollection",
|
|
5977
|
+
features
|
|
5978
|
+
};
|
|
5979
|
+
}
|
|
5980
|
+
const CONTAIN = defineControlMeasure({
|
|
5981
|
+
metadata: CONTAIN_METADATA,
|
|
5982
|
+
generator: createContain,
|
|
5983
|
+
defaultOptions: DEFAULT_CONTAIN_OPTIONS,
|
|
5984
|
+
rule: containDrawRule,
|
|
5653
5985
|
previewSample: {
|
|
5654
5986
|
controlPoints: [
|
|
5655
|
-
[
|
|
5656
|
-
[
|
|
5657
|
-
[1,
|
|
5658
|
-
[-1, .5]
|
|
5987
|
+
[0, .7],
|
|
5988
|
+
[0, -.7],
|
|
5989
|
+
[1.5, 0]
|
|
5659
5990
|
],
|
|
5660
|
-
|
|
5661
|
-
options: {
|
|
5662
|
-
echelonSize: 200,
|
|
5663
|
-
smooth: true,
|
|
5664
|
-
labelSize: 96
|
|
5665
|
-
}
|
|
5991
|
+
options: { labelSize: 90 }
|
|
5666
5992
|
}
|
|
5667
5993
|
});
|
|
5668
5994
|
//#endregion
|
|
@@ -6189,7 +6515,7 @@ const BOUNDARY_METADATA = {
|
|
|
6189
6515
|
* @param textAmplifiers - Normalized text amplifiers (ADR-0027): `T`/`AS` label
|
|
6190
6516
|
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
6191
6517
|
*/
|
|
6192
|
-
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
6518
|
+
function createBoundary(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
6193
6519
|
const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPosition = DEFAULT_BOUNDARY_OPTIONS.labelPosition, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
|
|
6194
6520
|
const unit1Designator = textAmplifiers.T ?? "";
|
|
6195
6521
|
const unit1Country = textAmplifiers.AS ?? "";
|
|
@@ -6273,7 +6599,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
|
6273
6599
|
labelSize: h,
|
|
6274
6600
|
labelPadding
|
|
6275
6601
|
};
|
|
6276
|
-
pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions);
|
|
6602
|
+
pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions, "", context);
|
|
6277
6603
|
const boundaryCoords = buildGappedLine(verts, gaps);
|
|
6278
6604
|
const features = [];
|
|
6279
6605
|
if (boundaryCoords.length > 0) features.push({
|
|
@@ -6389,12 +6715,12 @@ const LIGHT_LINE_METADATA = {
|
|
|
6389
6715
|
* Creates a Light line control measure: a polyline through `positions`
|
|
6390
6716
|
* labeled "LL" above the line near each end.
|
|
6391
6717
|
*/
|
|
6392
|
-
function createLightLine(positions, options = {}, textAmplifiers = {}) {
|
|
6718
|
+
function createLightLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
6393
6719
|
return fixedLabelLineFeatures(positions, {
|
|
6394
6720
|
part: "light-line",
|
|
6395
6721
|
placement: "above",
|
|
6396
6722
|
...options
|
|
6397
|
-
}, "LL", textAmplifiers.N);
|
|
6723
|
+
}, "LL", textAmplifiers.N, context);
|
|
6398
6724
|
}
|
|
6399
6725
|
const LIGHT_LINE = defineControlMeasure({
|
|
6400
6726
|
metadata: LIGHT_LINE_METADATA,
|
|
@@ -6500,12 +6826,12 @@ const CENTER_LABEL_OFFSET_RATIO = .7;
|
|
|
6500
6826
|
* above the line, T1/AS1 below). No center group is emitted when all four
|
|
6501
6827
|
* amplifiers are empty.
|
|
6502
6828
|
*/
|
|
6503
|
-
function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
|
|
6829
|
+
function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
6504
6830
|
const features = [...fixedLabelLineFeatures(positions, {
|
|
6505
6831
|
part: "engineer-work-line",
|
|
6506
6832
|
placement: "above",
|
|
6507
6833
|
...options
|
|
6508
|
-
}, "EWL", textAmplifiers.N).features];
|
|
6834
|
+
}, "EWL", textAmplifiers.N, context).features];
|
|
6509
6835
|
const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
|
|
6510
6836
|
const frame = pointAlongPolyline(segments, totalLength, .5);
|
|
6511
6837
|
if (frame) {
|
|
@@ -6642,7 +6968,7 @@ const GENERIC_C2_LINE_METADATA = {
|
|
|
6642
6968
|
* (inset from the tip, anchored toward the line's interior), one at
|
|
6643
6969
|
* `base + perp·offset` and the other at `base − perp·offset`.
|
|
6644
6970
|
*/
|
|
6645
|
-
function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
|
|
6971
|
+
function createGenericC2Line(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
6646
6972
|
const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
|
|
6647
6973
|
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
6648
6974
|
smooth,
|
|
@@ -6674,7 +7000,7 @@ function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
|
|
|
6674
7000
|
}
|
|
6675
7001
|
const nameText = phaseLineLabelText(trimmedPhaseLineName, includePrefix);
|
|
6676
7002
|
if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
|
|
6677
|
-
pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText);
|
|
7003
|
+
pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText, context);
|
|
6678
7004
|
features.push(...labelFeatures);
|
|
6679
7005
|
}
|
|
6680
7006
|
return {
|
|
@@ -7526,6 +7852,8 @@ const ARC_RESOLUTION_PARAMS = [{
|
|
|
7526
7852
|
max: 360,
|
|
7527
7853
|
step: 1
|
|
7528
7854
|
}];
|
|
7855
|
+
//#endregion
|
|
7856
|
+
//#region src/generators/cm99-generic-graphics/params.ts
|
|
7529
7857
|
const SMOOTH_PATH_PARAMS = [{
|
|
7530
7858
|
key: "smooth",
|
|
7531
7859
|
label: "Smooth",
|
|
@@ -7862,6 +8190,41 @@ const GENERIC_RECTANGLE = defineControlMeasure({
|
|
|
7862
8190
|
] }
|
|
7863
8191
|
});
|
|
7864
8192
|
//#endregion
|
|
8193
|
+
//#region src/internal/annular-sector.ts
|
|
8194
|
+
/**
|
|
8195
|
+
* Clockwise sweep in degrees from `leftAzimuth` to `rightAzimuth`. A non-zero
|
|
8196
|
+
* raw difference that normalizes to 0 means a full 360° fan.
|
|
8197
|
+
*
|
|
8198
|
+
* `collapseToleranceDegrees` (default 0, i.e. off) treats a near-zero or
|
|
8199
|
+
* near-360 sweep as a collapsed sector. Callers whose handle coordinates are
|
|
8200
|
+
* rounded need this: two handles placed on the same ray can recover bearings a
|
|
8201
|
+
* few thousandths of a degree apart, which would otherwise read as an
|
|
8202
|
+
* almost-360° fan.
|
|
8203
|
+
*/
|
|
8204
|
+
function clockwiseSweepDegrees(leftAzimuth, rightAzimuth, collapseToleranceDegrees = 0) {
|
|
8205
|
+
const rawSweep = rightAzimuth - leftAzimuth;
|
|
8206
|
+
const sweep = normalizeDegrees(rawSweep);
|
|
8207
|
+
if (collapseToleranceDegrees > 0 && Math.min(sweep, 360 - sweep) < collapseToleranceDegrees) return 0;
|
|
8208
|
+
return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
|
|
8209
|
+
}
|
|
8210
|
+
/** Samples an arc of `sweepDegrees` at `radius`, starting from `leftAzimuth`. */
|
|
8211
|
+
function sampleArc$1(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
|
|
8212
|
+
const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
|
|
8213
|
+
return Array.from({ length: segmentCount + 1 }, (_, index) => {
|
|
8214
|
+
return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
|
|
8215
|
+
});
|
|
8216
|
+
}
|
|
8217
|
+
/** Closed ring tracing the outer arc, back along the inner arc, and closed. */
|
|
8218
|
+
function annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, resolution) {
|
|
8219
|
+
const outerArc = sampleArc$1(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
|
|
8220
|
+
const innerArc = sampleArc$1(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
|
|
8221
|
+
return [
|
|
8222
|
+
...outerArc,
|
|
8223
|
+
...innerArc,
|
|
8224
|
+
outerArc[0]
|
|
8225
|
+
];
|
|
8226
|
+
}
|
|
8227
|
+
//#endregion
|
|
7865
8228
|
//#region src/generators/cm99-generic-graphics/sector.ts
|
|
7866
8229
|
const DEFAULT_GENERIC_SECTOR_OPTIONS = {
|
|
7867
8230
|
filled: false,
|
|
@@ -7887,17 +8250,6 @@ const GENERIC_SECTOR_METADATA = {
|
|
|
7887
8250
|
drawRule: "Sector",
|
|
7888
8251
|
params: [...FILLED_AREA_PARAMS, ...ARC_RESOLUTION_PARAMS]
|
|
7889
8252
|
};
|
|
7890
|
-
function clockwiseSweepDegrees(leftAzimuth, rightAzimuth) {
|
|
7891
|
-
const rawSweep = rightAzimuth - leftAzimuth;
|
|
7892
|
-
const sweep = normalizeDegrees(rawSweep);
|
|
7893
|
-
return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
|
|
7894
|
-
}
|
|
7895
|
-
function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
|
|
7896
|
-
const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
|
|
7897
|
-
return Array.from({ length: segmentCount + 1 }, (_, index) => {
|
|
7898
|
-
return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
|
|
7899
|
-
});
|
|
7900
|
-
}
|
|
7901
8253
|
function createGenericSector(coordinates, options = {}) {
|
|
7902
8254
|
const anchor = coordinates[0];
|
|
7903
8255
|
const innerLeftPoint = coordinates[1];
|
|
@@ -7906,11 +8258,13 @@ function createGenericSector(coordinates, options = {}) {
|
|
|
7906
8258
|
type: "FeatureCollection",
|
|
7907
8259
|
features: []
|
|
7908
8260
|
};
|
|
7909
|
-
const
|
|
7910
|
-
const
|
|
8261
|
+
const firstRadius = haversineDistance(anchor, innerLeftPoint);
|
|
8262
|
+
const secondRadius = haversineDistance(anchor, outerRightPoint);
|
|
8263
|
+
const innerRadius = Math.min(firstRadius, secondRadius);
|
|
8264
|
+
const outerRadius = Math.max(firstRadius, secondRadius);
|
|
7911
8265
|
const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, innerLeftPoint) * 180 / Math.PI);
|
|
7912
8266
|
const rightAzimuth = normalizeDegrees(sphericalBearing(anchor, outerRightPoint) * 180 / Math.PI);
|
|
7913
|
-
if (outerRadius
|
|
8267
|
+
if (outerRadius - innerRadius < 1e-6 || innerRadius < 1e-6) return {
|
|
7914
8268
|
type: "FeatureCollection",
|
|
7915
8269
|
features: []
|
|
7916
8270
|
};
|
|
@@ -7919,13 +8273,7 @@ function createGenericSector(coordinates, options = {}) {
|
|
|
7919
8273
|
type: "FeatureCollection",
|
|
7920
8274
|
features: []
|
|
7921
8275
|
};
|
|
7922
|
-
const
|
|
7923
|
-
const outerArc = sampleArc(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
|
|
7924
|
-
const ring = [
|
|
7925
|
-
...outerArc,
|
|
7926
|
-
...sampleArc(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse(),
|
|
7927
|
-
outerArc[0]
|
|
7928
|
-
];
|
|
8276
|
+
const ring = annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
|
|
7929
8277
|
return {
|
|
7930
8278
|
type: "FeatureCollection",
|
|
7931
8279
|
features: [{
|
|
@@ -8128,6 +8476,114 @@ const GENERIC_TEXT = defineControlMeasure({
|
|
|
8128
8476
|
}
|
|
8129
8477
|
});
|
|
8130
8478
|
//#endregion
|
|
8479
|
+
//#region src/generators/cm20-maritime-control-areas/radar-search-doctrine.ts
|
|
8480
|
+
const DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS = { resolution: 64 };
|
|
8481
|
+
/** Doctrinal RSD colors from the symbol specification. */
|
|
8482
|
+
const RADAR_SEARCH_DOCTRINE_STROKE_COLOR = "rgb(51, 136, 136)";
|
|
8483
|
+
const RADAR_SEARCH_DOCTRINE_FILL_COLOR = "rgba(51, 136, 136, 0.25)";
|
|
8484
|
+
const RADAR_SEARCH_DOCTRINE_METADATA = {
|
|
8485
|
+
id: "radar-search-doctrine",
|
|
8486
|
+
name: "Radar Search Doctrine",
|
|
8487
|
+
description: "A maritime radar search area defined by an axis and annular-sector limits.",
|
|
8488
|
+
entity: "Maritime Control Areas",
|
|
8489
|
+
entityType: "Radar Search Doctrine",
|
|
8490
|
+
value: "200700",
|
|
8491
|
+
minCoordinates: 3,
|
|
8492
|
+
maxCoordinates: 3,
|
|
8493
|
+
geometry: "area",
|
|
8494
|
+
geometryTypes: ["Polygon", "Point"],
|
|
8495
|
+
paints: {
|
|
8496
|
+
stroke: true,
|
|
8497
|
+
fill: "fixed",
|
|
8498
|
+
text: true
|
|
8499
|
+
},
|
|
8500
|
+
drawRule: "Point18",
|
|
8501
|
+
capturesLabelSize: true,
|
|
8502
|
+
params: ARC_RESOLUTION_PARAMS,
|
|
8503
|
+
textAmplifiers: [{
|
|
8504
|
+
key: "T",
|
|
8505
|
+
label: "Unique designation",
|
|
8506
|
+
description: "Field T — designation centered in the search area along its axis.",
|
|
8507
|
+
placeholder: "FF",
|
|
8508
|
+
maxLength: 20
|
|
8509
|
+
}]
|
|
8510
|
+
};
|
|
8511
|
+
/**
|
|
8512
|
+
* GeoJSON positions are rounded by `destinationPoint`, so two handles placed on
|
|
8513
|
+
* the same ray can recover bearings a few thousandths of a degree apart. Treat
|
|
8514
|
+
* that numerical noise as a collapsed sector, not an almost-360° fan.
|
|
8515
|
+
*/
|
|
8516
|
+
const SWEEP_COLLAPSE_TOLERANCE_DEGREES = .01;
|
|
8517
|
+
function createRadarSearchDoctrine(coordinates, options = {}, textAmplifiers = {}) {
|
|
8518
|
+
const anchor = coordinates[0];
|
|
8519
|
+
const startLimit = coordinates[1];
|
|
8520
|
+
const stopLimit = coordinates[2];
|
|
8521
|
+
const firstRange = haversineDistance(anchor, startLimit);
|
|
8522
|
+
const secondRange = haversineDistance(anchor, stopLimit);
|
|
8523
|
+
const startRange = Math.min(firstRange, secondRange);
|
|
8524
|
+
const stopRange = Math.max(firstRange, secondRange);
|
|
8525
|
+
const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, startLimit) * 180 / Math.PI);
|
|
8526
|
+
const sweepDegrees = clockwiseSweepDegrees(leftAzimuth, normalizeDegrees(sphericalBearing(anchor, stopLimit) * 180 / Math.PI), SWEEP_COLLAPSE_TOLERANCE_DEGREES);
|
|
8527
|
+
if (stopRange - startRange < 1e-6 || sweepDegrees < 1e-6) return {
|
|
8528
|
+
type: "FeatureCollection",
|
|
8529
|
+
features: []
|
|
8530
|
+
};
|
|
8531
|
+
const axis = normalizeDegrees(leftAzimuth + sweepDegrees / 2) * Math.PI / 180;
|
|
8532
|
+
const ring = annularSectorRing(anchor, startRange, stopRange, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
|
|
8533
|
+
const features = [{
|
|
8534
|
+
type: "Feature",
|
|
8535
|
+
properties: {
|
|
8536
|
+
part: "search-area",
|
|
8537
|
+
fill: true,
|
|
8538
|
+
style: {
|
|
8539
|
+
strokeColor: RADAR_SEARCH_DOCTRINE_STROKE_COLOR,
|
|
8540
|
+
fillColor: RADAR_SEARCH_DOCTRINE_FILL_COLOR,
|
|
8541
|
+
fillPattern: "solid"
|
|
8542
|
+
}
|
|
8543
|
+
},
|
|
8544
|
+
geometry: {
|
|
8545
|
+
type: "Polygon",
|
|
8546
|
+
coordinates: [ring]
|
|
8547
|
+
}
|
|
8548
|
+
}];
|
|
8549
|
+
if (textAmplifiers.T) {
|
|
8550
|
+
const labelAnchor = destinationPoint(anchor, (startRange + stopRange) / 2, axis);
|
|
8551
|
+
features.push({
|
|
8552
|
+
type: "Feature",
|
|
8553
|
+
properties: {
|
|
8554
|
+
part: "label",
|
|
8555
|
+
text: textAmplifiers.T,
|
|
8556
|
+
amplifierField: "T",
|
|
8557
|
+
labelPlacementKey: "T",
|
|
8558
|
+
...labelSizeProps(options),
|
|
8559
|
+
rotation: labelRotationAlong([Math.cos(axis), -Math.sin(axis)])
|
|
8560
|
+
},
|
|
8561
|
+
geometry: {
|
|
8562
|
+
type: "Point",
|
|
8563
|
+
coordinates: labelAnchor
|
|
8564
|
+
}
|
|
8565
|
+
});
|
|
8566
|
+
}
|
|
8567
|
+
return {
|
|
8568
|
+
type: "FeatureCollection",
|
|
8569
|
+
features
|
|
8570
|
+
};
|
|
8571
|
+
}
|
|
8572
|
+
const RADAR_SEARCH_DOCTRINE = defineControlMeasure({
|
|
8573
|
+
metadata: RADAR_SEARCH_DOCTRINE_METADATA,
|
|
8574
|
+
generator: createRadarSearchDoctrine,
|
|
8575
|
+
defaultOptions: DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS,
|
|
8576
|
+
rule: dynamicPointDrawRule,
|
|
8577
|
+
previewSample: {
|
|
8578
|
+
controlPoints: [
|
|
8579
|
+
[0, -1],
|
|
8580
|
+
[-.55, -.15],
|
|
8581
|
+
[1, .45]
|
|
8582
|
+
],
|
|
8583
|
+
textAmplifiers: { T: "FF" }
|
|
8584
|
+
}
|
|
8585
|
+
});
|
|
8586
|
+
//#endregion
|
|
8131
8587
|
//#region src/generators/cm34-mission-tasks/clear.ts
|
|
8132
8588
|
/**
|
|
8133
8589
|
* Default options for the CLEAR symbol.
|
|
@@ -8467,27 +8923,6 @@ const COUNTERATTACK = defineControlMeasure({
|
|
|
8467
8923
|
}
|
|
8468
8924
|
});
|
|
8469
8925
|
//#endregion
|
|
8470
|
-
//#region src/internal/arrowhead.ts
|
|
8471
|
-
/**
|
|
8472
|
-
* Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
|
|
8473
|
-
*
|
|
8474
|
-
* `tip` is the point of the arrow, `dir` the unit direction the arrow points,
|
|
8475
|
-
* `length` the distance from tip back to the base, and `width` the base width.
|
|
8476
|
-
* The returned ring is `[left, tip, right, left]`.
|
|
8477
|
-
*/
|
|
8478
|
-
function createArrowHeadRing(tip, dir, length, width) {
|
|
8479
|
-
const perp = [-dir[1], dir[0]];
|
|
8480
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
8481
|
-
const left = vecAdd(base, vecScale(perp, width / 2));
|
|
8482
|
-
const right = vecSub(base, vecScale(perp, width / 2));
|
|
8483
|
-
return [
|
|
8484
|
-
unproject(left[0], left[1]),
|
|
8485
|
-
unproject(tip[0], tip[1]),
|
|
8486
|
-
unproject(right[0], right[1]),
|
|
8487
|
-
unproject(left[0], left[1])
|
|
8488
|
-
];
|
|
8489
|
-
}
|
|
8490
|
-
//#endregion
|
|
8491
8926
|
//#region src/generators/cm34-mission-tasks/counterattack-by-fire.ts
|
|
8492
8927
|
const DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS = { ...DEFAULT_COUNTERATTACK_OPTIONS };
|
|
8493
8928
|
/** Length of the solid fire arrowhead relative to the recessed P1 extension. */
|
|
@@ -9699,7 +10134,7 @@ const DROP_ZONE_METADATA = {
|
|
|
9699
10134
|
* (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9700
10135
|
*/
|
|
9701
10136
|
function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9702
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
|
|
10137
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements, context);
|
|
9703
10138
|
}
|
|
9704
10139
|
const DROP_ZONE = defineControlMeasure({
|
|
9705
10140
|
metadata: DROP_ZONE_METADATA,
|
|
@@ -9835,7 +10270,7 @@ function createEncirclement(positions, options = {}, textAmplifiers = {}, contex
|
|
|
9835
10270
|
const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
|
|
9836
10271
|
const perimeter = buildAreaPerimeter(verts);
|
|
9837
10272
|
const labelFeatures = [];
|
|
9838
|
-
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
|
|
10273
|
+
const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context);
|
|
9839
10274
|
const radius = meanRadius(verts);
|
|
9840
10275
|
const barbLength = radius * Math.max(0, barbLengthRatio);
|
|
9841
10276
|
const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
|
|
@@ -9916,7 +10351,7 @@ const EXTRACTION_ZONE_METADATA = {
|
|
|
9916
10351
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
9917
10352
|
*/
|
|
9918
10353
|
function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
9919
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
|
|
10354
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements, context);
|
|
9920
10355
|
}
|
|
9921
10356
|
const EXTRACTION_ZONE = defineControlMeasure({
|
|
9922
10357
|
metadata: EXTRACTION_ZONE_METADATA,
|
|
@@ -10588,7 +11023,7 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
|
|
|
10588
11023
|
* );
|
|
10589
11024
|
* ```
|
|
10590
11025
|
*/
|
|
10591
|
-
function createFLOT(positions, options = {}, textAmplifiers = {}) {
|
|
11026
|
+
function createFLOT(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10592
11027
|
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
|
|
10593
11028
|
let effectiveRadius = radius;
|
|
10594
11029
|
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
@@ -10626,7 +11061,7 @@ function createFLOT(positions, options = {}, textAmplifiers = {}) {
|
|
|
10626
11061
|
}
|
|
10627
11062
|
};
|
|
10628
11063
|
const labels = [];
|
|
10629
|
-
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
|
|
11064
|
+
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
|
|
10630
11065
|
return {
|
|
10631
11066
|
type: "FeatureCollection",
|
|
10632
11067
|
features: [feature, ...labels]
|
|
@@ -10695,14 +11130,14 @@ const PHASE_LINE_METADATA = {
|
|
|
10695
11130
|
* Creates a Phase line control measure: a polyline through `positions`
|
|
10696
11131
|
* labeled at each end with `T` (optionally "PL"-prefixed).
|
|
10697
11132
|
*/
|
|
10698
|
-
function createPhaseLine(positions, options = {}, textAmplifiers = {}) {
|
|
11133
|
+
function createPhaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10699
11134
|
const { includePrefix = true, ...sizeOptions } = options;
|
|
10700
11135
|
const designation = textAmplifiers.T ?? "";
|
|
10701
11136
|
const text = includePrefix ? designation.length > 0 ? `PL ${designation}` : "PL" : designation;
|
|
10702
11137
|
return fixedLabelLineFeatures(positions, {
|
|
10703
11138
|
part: "phase-line",
|
|
10704
11139
|
...sizeOptions
|
|
10705
|
-
}, text, textAmplifiers.N);
|
|
11140
|
+
}, text, textAmplifiers.N, context);
|
|
10706
11141
|
}
|
|
10707
11142
|
const PHASE_LINE = defineControlMeasure({
|
|
10708
11143
|
metadata: PHASE_LINE_METADATA,
|
|
@@ -10774,12 +11209,12 @@ const BRIDGEHEAD_LINE_METADATA = {
|
|
|
10774
11209
|
textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
|
|
10775
11210
|
};
|
|
10776
11211
|
/** Creates a Bridgehead line labeled "BL" above both ends. */
|
|
10777
|
-
function createBridgeheadLine(positions, options = {}, textAmplifiers = {}) {
|
|
11212
|
+
function createBridgeheadLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10778
11213
|
return fixedLabelLineFeatures(positions, {
|
|
10779
11214
|
part: "bridgehead-line",
|
|
10780
11215
|
placement: "above",
|
|
10781
11216
|
...options
|
|
10782
|
-
}, "BL", textAmplifiers.N);
|
|
11217
|
+
}, "BL", textAmplifiers.N, context);
|
|
10783
11218
|
}
|
|
10784
11219
|
const BRIDGEHEAD_LINE = defineControlMeasure({
|
|
10785
11220
|
metadata: BRIDGEHEAD_LINE_METADATA,
|
|
@@ -10850,12 +11285,12 @@ const HOLDING_LINE_METADATA = {
|
|
|
10850
11285
|
textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
|
|
10851
11286
|
};
|
|
10852
11287
|
/** Creates a Holding line labeled "HL" above both ends. */
|
|
10853
|
-
function createHoldingLine(positions, options = {}, textAmplifiers = {}) {
|
|
11288
|
+
function createHoldingLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10854
11289
|
return fixedLabelLineFeatures(positions, {
|
|
10855
11290
|
part: "holding-line",
|
|
10856
11291
|
placement: "above",
|
|
10857
11292
|
...options
|
|
10858
|
-
}, "HL", textAmplifiers.N);
|
|
11293
|
+
}, "HL", textAmplifiers.N, context);
|
|
10859
11294
|
}
|
|
10860
11295
|
const HOLDING_LINE = defineControlMeasure({
|
|
10861
11296
|
metadata: HOLDING_LINE_METADATA,
|
|
@@ -10926,12 +11361,12 @@ const RELEASE_LINE_METADATA = {
|
|
|
10926
11361
|
textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
|
|
10927
11362
|
};
|
|
10928
11363
|
/** Creates a Release line labeled "RL" above both ends. */
|
|
10929
|
-
function createReleaseLine(positions, options = {}, textAmplifiers = {}) {
|
|
11364
|
+
function createReleaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
10930
11365
|
return fixedLabelLineFeatures(positions, {
|
|
10931
11366
|
part: "release-line",
|
|
10932
11367
|
placement: "above",
|
|
10933
11368
|
...options
|
|
10934
|
-
}, "RL", textAmplifiers.N);
|
|
11369
|
+
}, "RL", textAmplifiers.N, context);
|
|
10935
11370
|
}
|
|
10936
11371
|
const RELEASE_LINE = defineControlMeasure({
|
|
10937
11372
|
metadata: RELEASE_LINE_METADATA,
|
|
@@ -11005,12 +11440,12 @@ const FORWARD_EDGE_OF_BATTLE_AREA_METADATA = {
|
|
|
11005
11440
|
* Creates a Forward edge of the battle area control measure: a polyline
|
|
11006
11441
|
* through `positions` labeled "FEBA" above the line near each end.
|
|
11007
11442
|
*/
|
|
11008
|
-
function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}) {
|
|
11443
|
+
function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
11009
11444
|
return fixedLabelLineFeatures(positions, {
|
|
11010
11445
|
part: "forward-edge-of-battle-area",
|
|
11011
11446
|
placement: "above",
|
|
11012
11447
|
...options
|
|
11013
|
-
}, "FEBA", textAmplifiers.N);
|
|
11448
|
+
}, "FEBA", textAmplifiers.N, context);
|
|
11014
11449
|
}
|
|
11015
11450
|
const FORWARD_EDGE_OF_BATTLE_AREA = defineControlMeasure({
|
|
11016
11451
|
metadata: FORWARD_EDGE_OF_BATTLE_AREA_METADATA,
|
|
@@ -11084,12 +11519,12 @@ const HANDOVER_LINE_METADATA = {
|
|
|
11084
11519
|
* Creates a Handover line control measure: a polyline through `positions`
|
|
11085
11520
|
* labeled "HOL" above the line near each end.
|
|
11086
11521
|
*/
|
|
11087
|
-
function createHandoverLine(positions, options = {}, textAmplifiers = {}) {
|
|
11522
|
+
function createHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
11088
11523
|
return fixedLabelLineFeatures(positions, {
|
|
11089
11524
|
part: "handover-line",
|
|
11090
11525
|
placement: "above",
|
|
11091
11526
|
...options
|
|
11092
|
-
}, "HOL", textAmplifiers.N);
|
|
11527
|
+
}, "HOL", textAmplifiers.N, context);
|
|
11093
11528
|
}
|
|
11094
11529
|
const HANDOVER_LINE = defineControlMeasure({
|
|
11095
11530
|
metadata: HANDOVER_LINE_METADATA,
|
|
@@ -11163,12 +11598,12 @@ const BATTLE_HANDOVER_LINE_METADATA = {
|
|
|
11163
11598
|
* Creates a Battle handover line control measure: a polyline through
|
|
11164
11599
|
* `positions` labeled "BHL" above the line near each end.
|
|
11165
11600
|
*/
|
|
11166
|
-
function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}) {
|
|
11601
|
+
function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
11167
11602
|
return fixedLabelLineFeatures(positions, {
|
|
11168
11603
|
part: "battle-handover-line",
|
|
11169
11604
|
placement: "above",
|
|
11170
11605
|
...options
|
|
11171
|
-
}, "BHL", textAmplifiers.N);
|
|
11606
|
+
}, "BHL", textAmplifiers.N, context);
|
|
11172
11607
|
}
|
|
11173
11608
|
const BATTLE_HANDOVER_LINE = defineControlMeasure({
|
|
11174
11609
|
metadata: BATTLE_HANDOVER_LINE_METADATA,
|
|
@@ -11320,7 +11755,7 @@ function generateFortifiedPoints(projectedPoints, effectiveSize) {
|
|
|
11320
11755
|
* @param options - Configuration options for the graphic
|
|
11321
11756
|
* @returns A GeoJSON FeatureCollection containing the castellated LineString
|
|
11322
11757
|
*/
|
|
11323
|
-
function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
|
|
11758
|
+
function createFortifiedLine(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
11324
11759
|
const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
|
|
11325
11760
|
const safeSize = calculateEffectiveSize(options);
|
|
11326
11761
|
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
@@ -11334,7 +11769,7 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
|
|
|
11334
11769
|
}
|
|
11335
11770
|
};
|
|
11336
11771
|
const labels = [];
|
|
11337
|
-
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
|
|
11772
|
+
pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
|
|
11338
11773
|
return {
|
|
11339
11774
|
type: "FeatureCollection",
|
|
11340
11775
|
features: [feature, ...labels]
|
|
@@ -11418,7 +11853,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}, conte
|
|
|
11418
11853
|
}, options, context.amplifierPlacements, {
|
|
11419
11854
|
ringVerts: boundaryPoints,
|
|
11420
11855
|
extraClearanceMeters: 3 * safeSize
|
|
11421
|
-
})),
|
|
11856
|
+
}, context)),
|
|
11422
11857
|
properties: {}
|
|
11423
11858
|
}];
|
|
11424
11859
|
features.push(...labelFeatures);
|
|
@@ -12343,7 +12778,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
|
|
|
12343
12778
|
* {@link labeledAreaFeatures}.
|
|
12344
12779
|
*/
|
|
12345
12780
|
function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
12346
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
|
|
12781
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements, context);
|
|
12347
12782
|
}
|
|
12348
12783
|
const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
|
|
12349
12784
|
metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
|
|
@@ -12402,7 +12837,7 @@ const LANDING_ZONE_METADATA = {
|
|
|
12402
12837
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
12403
12838
|
*/
|
|
12404
12839
|
function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
12405
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
|
|
12840
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements, context);
|
|
12406
12841
|
}
|
|
12407
12842
|
const LANDING_ZONE = defineControlMeasure({
|
|
12408
12843
|
metadata: LANDING_ZONE_METADATA,
|
|
@@ -12473,7 +12908,7 @@ function createLimitedAccessArea(positions, options = {}, textAmplifiers = {}, c
|
|
|
12473
12908
|
}];
|
|
12474
12909
|
return {
|
|
12475
12910
|
type: "FeatureCollection",
|
|
12476
|
-
features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labelFeatures]
|
|
12911
|
+
features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labelFeatures]
|
|
12477
12912
|
};
|
|
12478
12913
|
}
|
|
12479
12914
|
const LIMITED_ACCESS_AREA = defineControlMeasure({
|
|
@@ -12584,7 +13019,7 @@ function createNoFireAreaIrregular(positions, options = {}, textAmplifiers = {},
|
|
|
12584
13019
|
}
|
|
12585
13020
|
return {
|
|
12586
13021
|
type: "FeatureCollection",
|
|
12587
|
-
features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labels]
|
|
13022
|
+
features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labels]
|
|
12588
13023
|
};
|
|
12589
13024
|
}
|
|
12590
13025
|
const NO_FIRE_AREA_IRREGULAR = defineControlMeasure({
|
|
@@ -12929,7 +13364,7 @@ function createMinefield(coordinates, options = {}, textAmplifiers = {}, context
|
|
|
12929
13364
|
clearance: ENY_CLEARANCE_RATIO * labelSize,
|
|
12930
13365
|
sizeProps,
|
|
12931
13366
|
followPlacementRotation: true
|
|
12932
|
-
}, context.amplifierPlacements);
|
|
13367
|
+
}, context.amplifierPlacements, context, { labelSize });
|
|
12933
13368
|
if (textAmplifiers.W) pushLabel(labelFeatures, offset(center, 0, -halfHeight - width * LABEL_CLEARANCE_RATIO), textAmplifiers.W, labelRotation, sizeProps, void 0, "W");
|
|
12934
13369
|
const features = [{
|
|
12935
13370
|
type: "Feature",
|
|
@@ -13146,7 +13581,7 @@ function createMineArea(positions, options, minedArea, textAmplifiers, context)
|
|
|
13146
13581
|
textHeight: resolveLabelOffsetMeters(options, 1),
|
|
13147
13582
|
clearance: resolveLabelOffsetMeters(options, .12 + labelPadding),
|
|
13148
13583
|
sizeProps: labelSizeProps(options)
|
|
13149
|
-
}) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements)], mineFillPattern(mineType)), ...labelFeatures]
|
|
13584
|
+
}, void 0, context, options) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context)], mineFillPattern(mineType)), ...labelFeatures]
|
|
13150
13585
|
};
|
|
13151
13586
|
}
|
|
13152
13587
|
function createDynamicMinefield(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
@@ -13649,7 +14084,7 @@ const PICKUP_ZONE_METADATA = {
|
|
|
13649
14084
|
* amplifier rows, per {@link labeledAreaFeatures}.
|
|
13650
14085
|
*/
|
|
13651
14086
|
function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
13652
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
|
|
14087
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements, context);
|
|
13653
14088
|
}
|
|
13654
14089
|
const PICKUP_ZONE = defineControlMeasure({
|
|
13655
14090
|
metadata: PICKUP_ZONE_METADATA,
|
|
@@ -13704,7 +14139,7 @@ const ASSAULT_POSITION_METADATA = {
|
|
|
13704
14139
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
13705
14140
|
*/
|
|
13706
14141
|
function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
13707
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
|
|
14142
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements, context);
|
|
13708
14143
|
}
|
|
13709
14144
|
const ASSAULT_POSITION = defineControlMeasure({
|
|
13710
14145
|
metadata: ASSAULT_POSITION_METADATA,
|
|
@@ -13759,7 +14194,7 @@ const ATTACK_POSITION_METADATA = {
|
|
|
13759
14194
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
13760
14195
|
*/
|
|
13761
14196
|
function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
13762
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
|
|
14197
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements, context);
|
|
13763
14198
|
}
|
|
13764
14199
|
const ATTACK_POSITION = defineControlMeasure({
|
|
13765
14200
|
metadata: ATTACK_POSITION_METADATA,
|
|
@@ -13814,7 +14249,7 @@ const OBJECTIVE_AREA_METADATA = {
|
|
|
13814
14249
|
* an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
|
|
13815
14250
|
*/
|
|
13816
14251
|
function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
13817
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
|
|
14252
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements, context);
|
|
13818
14253
|
}
|
|
13819
14254
|
const OBJECTIVE_AREA = defineControlMeasure({
|
|
13820
14255
|
metadata: OBJECTIVE_AREA_METADATA,
|
|
@@ -14097,7 +14532,7 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}, context
|
|
|
14097
14532
|
const enyGaps = pushAreaLabels(labelFeatures, verts, {
|
|
14098
14533
|
name: textAmplifiers.T,
|
|
14099
14534
|
hostile: textAmplifiers.N
|
|
14100
|
-
}, options, context.amplifierPlacements);
|
|
14535
|
+
}, options, context.amplifierPlacements, void 0, context);
|
|
14101
14536
|
const allGaps = [...gaps, ...enyGaps];
|
|
14102
14537
|
const boundaryCoords = buildGappedLine(verts, allGaps);
|
|
14103
14538
|
const tics = buildStrongPointTics(perimeter, allGaps, h);
|
|
@@ -14166,6 +14601,180 @@ const STRONG_POINT = defineControlMeasure({
|
|
|
14166
14601
|
}
|
|
14167
14602
|
});
|
|
14168
14603
|
//#endregion
|
|
14604
|
+
//#region src/generators/cm15-maneuver-areas/retain.ts
|
|
14605
|
+
const DEFAULT_RETAIN_OPTIONS = {
|
|
14606
|
+
openingAngle: 30,
|
|
14607
|
+
arrowheadLengthRatio: .1,
|
|
14608
|
+
arrowheadWidthRatio: .12,
|
|
14609
|
+
labelPadding: 0
|
|
14610
|
+
};
|
|
14611
|
+
const RETAIN_METADATA = {
|
|
14612
|
+
id: "retain",
|
|
14613
|
+
name: "Retain",
|
|
14614
|
+
description: "Ensures that a terrain feature controlled by a friendly force remains secure.",
|
|
14615
|
+
entity: "Maneuver Areas",
|
|
14616
|
+
entityType: "Battle Position",
|
|
14617
|
+
entitySubtype: "Retain",
|
|
14618
|
+
value: "151205",
|
|
14619
|
+
minCoordinates: 2,
|
|
14620
|
+
maxCoordinates: 2,
|
|
14621
|
+
geometry: "area",
|
|
14622
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
14623
|
+
paints: {
|
|
14624
|
+
stroke: true,
|
|
14625
|
+
fill: "none",
|
|
14626
|
+
text: true
|
|
14627
|
+
},
|
|
14628
|
+
drawRule: "Area6",
|
|
14629
|
+
capturesLabelSize: true,
|
|
14630
|
+
params: [
|
|
14631
|
+
{
|
|
14632
|
+
key: "openingAngle",
|
|
14633
|
+
presentationTier: "advanced",
|
|
14634
|
+
label: "Opening",
|
|
14635
|
+
description: "Angular opening on the friendly side, in degrees",
|
|
14636
|
+
type: "number",
|
|
14637
|
+
min: 5,
|
|
14638
|
+
max: 120,
|
|
14639
|
+
step: 1
|
|
14640
|
+
},
|
|
14641
|
+
{
|
|
14642
|
+
key: "arrowheadLengthRatio",
|
|
14643
|
+
presentationTier: "advanced",
|
|
14644
|
+
label: "Arrowhead length",
|
|
14645
|
+
description: "Open-arrowhead length as a ratio of the symbol radius",
|
|
14646
|
+
type: "number",
|
|
14647
|
+
min: .02,
|
|
14648
|
+
max: .5,
|
|
14649
|
+
step: .01
|
|
14650
|
+
},
|
|
14651
|
+
{
|
|
14652
|
+
key: "arrowheadWidthRatio",
|
|
14653
|
+
presentationTier: "advanced",
|
|
14654
|
+
label: "Arrowhead width",
|
|
14655
|
+
description: "Open-arrowhead base width as a ratio of the symbol radius",
|
|
14656
|
+
type: "number",
|
|
14657
|
+
min: .02,
|
|
14658
|
+
max: .5,
|
|
14659
|
+
step: .01
|
|
14660
|
+
},
|
|
14661
|
+
{
|
|
14662
|
+
key: "labelPadding",
|
|
14663
|
+
presentationTier: "advanced",
|
|
14664
|
+
label: "R padding",
|
|
14665
|
+
description: "Extra boundary clearance on each side of R, as a ratio of its height",
|
|
14666
|
+
type: "number",
|
|
14667
|
+
min: 0,
|
|
14668
|
+
max: 2,
|
|
14669
|
+
step: .05
|
|
14670
|
+
}
|
|
14671
|
+
]
|
|
14672
|
+
};
|
|
14673
|
+
const TAU = Math.PI * 2;
|
|
14674
|
+
/** Smallest visible boundary gap, in projected meters, for tiny label heights. */
|
|
14675
|
+
const MIN_LABEL_GAP_METERS = 1;
|
|
14676
|
+
function pointOnCircle(center, radius, angle) {
|
|
14677
|
+
return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
|
|
14678
|
+
}
|
|
14679
|
+
function sampleArc(center, radius, start, end) {
|
|
14680
|
+
const segments = Math.max(8, Math.ceil(48 * Math.abs(end - start) / Math.PI));
|
|
14681
|
+
return Array.from({ length: segments + 1 }, (_, index) => {
|
|
14682
|
+
const point = pointOnCircle(center, radius, start + (end - start) * index / segments);
|
|
14683
|
+
return unproject(point[0], point[1]);
|
|
14684
|
+
});
|
|
14685
|
+
}
|
|
14686
|
+
/** Creates the Retain boundary, outward tics, and intrinsic R label. */
|
|
14687
|
+
function createRetain(coordinates, options = {}) {
|
|
14688
|
+
const center = project(coordinates[0][0], coordinates[0][1]);
|
|
14689
|
+
const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
|
|
14690
|
+
const radius = vecMag(radiusVector);
|
|
14691
|
+
if (radius < 1e-6) return {
|
|
14692
|
+
type: "FeatureCollection",
|
|
14693
|
+
features: []
|
|
14694
|
+
};
|
|
14695
|
+
const resolved = {
|
|
14696
|
+
...DEFAULT_RETAIN_OPTIONS,
|
|
14697
|
+
...options
|
|
14698
|
+
};
|
|
14699
|
+
const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
|
|
14700
|
+
const opening = Math.min(120, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
|
|
14701
|
+
const h = resolveLabelOffsetMeters(options, 1, radius * .1);
|
|
14702
|
+
const labelPadding = Math.max(0, resolved.labelPadding);
|
|
14703
|
+
const boundarySpan = TAU - opening;
|
|
14704
|
+
const labelDistance = Math.PI - opening / 2;
|
|
14705
|
+
const labelCenter = startAngle - labelDistance;
|
|
14706
|
+
const labelHalfGap = Math.min(Math.PI / 2, Math.max(h * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS) / radius);
|
|
14707
|
+
const labelStart = startAngle - labelDistance + labelHalfGap;
|
|
14708
|
+
const labelEnd = startAngle - labelDistance - labelHalfGap;
|
|
14709
|
+
const boundaryEnd = startAngle - boundarySpan;
|
|
14710
|
+
const arrowTip = pointOnCircle(center, radius, boundaryEnd);
|
|
14711
|
+
const arrowheadLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
|
|
14712
|
+
const arrowheadWidth = radius * Math.max(0, resolved.arrowheadWidthRatio);
|
|
14713
|
+
const boundary = [
|
|
14714
|
+
sampleArc(center, radius, startAngle, labelStart),
|
|
14715
|
+
sampleArc(center, radius, labelEnd, boundaryEnd),
|
|
14716
|
+
createArrowHeadRing(arrowTip, [Math.sin(boundaryEnd), -Math.cos(boundaryEnd)], arrowheadLength, arrowheadWidth).slice(0, 3)
|
|
14717
|
+
];
|
|
14718
|
+
const tics = [];
|
|
14719
|
+
const spacingAngle = Math.max(h / radius, Math.PI / 180);
|
|
14720
|
+
const arrowClearanceAngle = (arrowheadLength + arrowheadWidth / 2 + h / 2) / radius;
|
|
14721
|
+
for (let distance = spacingAngle / 2; distance < boundarySpan - arrowClearanceAngle; distance += spacingAngle) {
|
|
14722
|
+
if (distance > labelDistance - labelHalfGap && distance < labelDistance + labelHalfGap) continue;
|
|
14723
|
+
const angle = startAngle - distance;
|
|
14724
|
+
const cos = Math.cos(angle);
|
|
14725
|
+
const sin = Math.sin(angle);
|
|
14726
|
+
const innerX = center[0] + radius * cos;
|
|
14727
|
+
const innerY = center[1] + radius * sin;
|
|
14728
|
+
tics.push([unproject(innerX, innerY), unproject(innerX + h * cos, innerY + h * sin)]);
|
|
14729
|
+
}
|
|
14730
|
+
const labelPoint = pointOnCircle(center, radius, labelCenter);
|
|
14731
|
+
return {
|
|
14732
|
+
type: "FeatureCollection",
|
|
14733
|
+
features: [
|
|
14734
|
+
{
|
|
14735
|
+
type: "Feature",
|
|
14736
|
+
properties: { part: "boundary" },
|
|
14737
|
+
geometry: {
|
|
14738
|
+
type: "MultiLineString",
|
|
14739
|
+
coordinates: boundary
|
|
14740
|
+
}
|
|
14741
|
+
},
|
|
14742
|
+
{
|
|
14743
|
+
type: "Feature",
|
|
14744
|
+
properties: { part: "tics" },
|
|
14745
|
+
geometry: {
|
|
14746
|
+
type: "MultiLineString",
|
|
14747
|
+
coordinates: tics
|
|
14748
|
+
}
|
|
14749
|
+
},
|
|
14750
|
+
{
|
|
14751
|
+
type: "Feature",
|
|
14752
|
+
properties: {
|
|
14753
|
+
part: "label",
|
|
14754
|
+
labelPlacement: false,
|
|
14755
|
+
text: "R",
|
|
14756
|
+
rotation: Math.PI,
|
|
14757
|
+
...labelSizeProps(options)
|
|
14758
|
+
},
|
|
14759
|
+
geometry: {
|
|
14760
|
+
type: "Point",
|
|
14761
|
+
coordinates: unproject(labelPoint[0], labelPoint[1])
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
]
|
|
14765
|
+
};
|
|
14766
|
+
}
|
|
14767
|
+
const RETAIN = defineControlMeasure({
|
|
14768
|
+
metadata: RETAIN_METADATA,
|
|
14769
|
+
generator: createRetain,
|
|
14770
|
+
defaultOptions: DEFAULT_RETAIN_OPTIONS,
|
|
14771
|
+
rule: retainDrawRule,
|
|
14772
|
+
previewSample: {
|
|
14773
|
+
controlPoints: [[0, 0], [-.8, .2]],
|
|
14774
|
+
options: { labelSize: 90 }
|
|
14775
|
+
}
|
|
14776
|
+
});
|
|
14777
|
+
//#endregion
|
|
14169
14778
|
//#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
|
|
14170
14779
|
/** Default options for the Submarine action area control measure. */
|
|
14171
14780
|
const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
|
|
@@ -14203,7 +14812,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
|
|
|
14203
14812
|
* `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
14204
14813
|
*/
|
|
14205
14814
|
function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
14206
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
|
|
14815
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements, context);
|
|
14207
14816
|
}
|
|
14208
14817
|
const SUBMARINE_ACTION_AREA = defineControlMeasure({
|
|
14209
14818
|
metadata: SUBMARINE_ACTION_AREA_METADATA,
|
|
@@ -14263,7 +14872,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
|
|
|
14263
14872
|
* (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
|
|
14264
14873
|
*/
|
|
14265
14874
|
function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
|
|
14266
|
-
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
|
|
14875
|
+
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements, context);
|
|
14267
14876
|
}
|
|
14268
14877
|
const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
|
|
14269
14878
|
metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
|
|
@@ -14624,7 +15233,9 @@ const DEFINITIONS = {
|
|
|
14624
15233
|
"engineer-work-line": ENGINEER_WORK_LINE,
|
|
14625
15234
|
"generic-c2-line": GENERIC_C2_LINE,
|
|
14626
15235
|
"battle-position": BATTLE_POSITION,
|
|
15236
|
+
contain: CONTAIN,
|
|
14627
15237
|
"strong-point": STRONG_POINT,
|
|
15238
|
+
retain: RETAIN,
|
|
14628
15239
|
ambush: AMBUSH,
|
|
14629
15240
|
"direction-of-attack-aviation": DIRECTION_OF_ATTACK_AVIATION,
|
|
14630
15241
|
"direction-of-main-attack": DIRECTION_OF_MAIN_ATTACK,
|
|
@@ -14669,6 +15280,7 @@ const DEFINITIONS = {
|
|
|
14669
15280
|
circle: GENERIC_CIRCLE,
|
|
14670
15281
|
sector: GENERIC_SECTOR,
|
|
14671
15282
|
text: GENERIC_TEXT,
|
|
15283
|
+
"radar-search-doctrine": RADAR_SEARCH_DOCTRINE,
|
|
14672
15284
|
"airborne-attack": AIRBORNE_ATTACK,
|
|
14673
15285
|
"attack-helicopter": ATTACK_HELICOPTER,
|
|
14674
15286
|
"support-by-fire": SUPPORT_BY_FIRE,
|
|
@@ -14960,7 +15572,31 @@ function dispatchControlMeasure(cm, opts) {
|
|
|
14960
15572
|
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
14961
15573
|
const generator = definition.generator;
|
|
14962
15574
|
const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
|
|
14963
|
-
|
|
15575
|
+
const constructionMetersPerCssPixel = resolveConstructionScale(cm.controlPoints, opts?.context?.groundMetersPerCssPixel);
|
|
15576
|
+
const authoredOptions = cm.options ?? {};
|
|
15577
|
+
const generatorOptions = constructionMetersPerCssPixel === void 0 ? authoredOptions : {
|
|
15578
|
+
...authoredOptions,
|
|
15579
|
+
metersPerPixel: constructionMetersPerCssPixel
|
|
15580
|
+
};
|
|
15581
|
+
return generator(cm.controlPoints, generatorOptions, textAmplifiers, {
|
|
15582
|
+
amplifierPlacements: cm.amplifierPlacements,
|
|
15583
|
+
constructionMetersPerCssPixel,
|
|
15584
|
+
labelSizeClampCssPixels: opts?.context?.labelSizeClampCssPixels,
|
|
15585
|
+
measureText: opts?.context?.measureText
|
|
15586
|
+
});
|
|
15587
|
+
}
|
|
15588
|
+
/** Convert true-ground scale to the Web Mercator metres legacy generators construct in. */
|
|
15589
|
+
function resolveConstructionScale(points, groundMetersPerCssPixel) {
|
|
15590
|
+
if (!(groundMetersPerCssPixel !== void 0 && groundMetersPerCssPixel > 0)) return void 0;
|
|
15591
|
+
let minLatitude = Infinity;
|
|
15592
|
+
let maxLatitude = -Infinity;
|
|
15593
|
+
for (const point of points) {
|
|
15594
|
+
minLatitude = Math.min(minLatitude, point[1]);
|
|
15595
|
+
maxLatitude = Math.max(maxLatitude, point[1]);
|
|
15596
|
+
}
|
|
15597
|
+
const midpointLatitude = (minLatitude + maxLatitude) / 2;
|
|
15598
|
+
const mercatorScale = Math.cos(midpointLatitude * Math.PI / 180);
|
|
15599
|
+
return groundMetersPerCssPixel / Math.max(mercatorScale, Number.EPSILON);
|
|
14964
15600
|
}
|
|
14965
15601
|
/**
|
|
14966
15602
|
* The control-point *input contract*: `controlPoints` must be present and an
|
|
@@ -15099,4 +15735,4 @@ function assertNever(value) {
|
|
|
15099
15735
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
15100
15736
|
}
|
|
15101
15737
|
//#endregion
|
|
15102
|
-
export {
|
|
15738
|
+
export { DEFAULT_COVER_OPTIONS as $, supportByFireDrawRule as $t, DEFAULT_RETIRE_OPTIONS as A, getMetersPerPixel as An, DEFAULT_ANTITANK_DITCH_OPTIONS as At, DEFAULT_HOLDING_LINE_OPTIONS as B, resolveAmplifierPlacement as Bt, DEFAULT_MAIN_ATTACK_OPTIONS as C, calculateMetrics as Cn, DEFAULT_BLOCK_ARROW_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, sphericalBearing as Dn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, project as En, DEFAULT_BATTLE_POSITION_OPTIONS as Et, DEFAULT_FORTIFIED_LINE_OPTIONS as F, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Ft, DEFAULT_FIX_OPTIONS as G, DEFAULT_PORTRAYAL as Gt, DEFAULT_PHASE_LINE_OPTIONS as H, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Ht, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as I, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as It, DEFAULT_DISRUPT_OPTIONS as J, DEFAULT_SYMBOL_COLOR as Jt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as K, DEFAULT_STROKE_DASH_CSS_PIXELS as Kt, DEFAULT_HANDOVER_LINE_OPTIONS as L, TEXT_AMPLIFIER_FIELDS as Lt, DEFAULT_PENETRATE_OPTIONS as M, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Mt, DEFAULT_FRONTAL_ATTACK_OPTIONS as N, DEFAULT_AREA_DEFENSE_OPTIONS as Nt, DEFAULT_SEIZE_OPTIONS as O, unproject as On, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ot, DEFAULT_FORTIFIED_AREA_OPTIONS as P, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Pt, DEFAULT_DELAY_OPTIONS as Q, axis1DrawRule as Qt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as R, canonicalTextAmplifierKey as Rt, DEFAULT_MINEFIELD_OPTIONS as S, createBaselineFrame as Sn, DEFAULT_BOUNDARY_OPTIONS as St, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as T, haversineDistance as Tn, DEFAULT_CONTAIN_OPTIONS as Tt, DEFAULT_FLOT_OPTIONS as U, DEFAULT_LINE_CAP as Ut, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as V, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Vt, DEFAULT_FIX_MISSION_TASK_OPTIONS as W, DEFAULT_LINE_JOIN as Wt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as X, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Xt, DEFAULT_GUARD_OPTIONS as Y, DEFAULT_AMBUSH_OPTIONS as Yt, DEFAULT_DISENGAGE_OPTIONS as Z, rectangleDrawRule as Zt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as _, computeDefaultMidpointPerpendicularPoint as _n, DEFAULT_BREACH_OPTIONS as _t, DEFINITIONS as a, line1DrawRule as an, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as b, pointOnMidpointPerpendicularAxis as bn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as bt, getDefaultOptions as c, dynamicPointDrawRule as cn, DEFAULT_GENERIC_TEXT_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, point12DrawRule as dn, DEFAULT_GENERIC_POLYGON_OPTIONS as dt, line27DrawRule as en, DEFAULT_TACTICAL_ARROW_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, penetrateDrawRule as fn, DEFAULT_GENERIC_LINE_OPTIONS as ft, SECONDARY_DIRECTION_OF_FIRE_DASH as g, containDrawRule as gn, DEFAULT_BYPASS_OPTIONS as gt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as h, centerRadiusDrawRule as hn, DEFAULT_CANALIZE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, turnDrawRule as in, DEFAULT_CLEAR_OPTIONS as it, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as j, roundToFixed as jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as jt, DEFAULT_SCREEN_OPTIONS as k, EPSILON as kn, DEFAULT_ANTITANK_WALL_OPTIONS as kt, listControlMeasureMetadata as l, sectorDrawRule as ln, DEFAULT_GENERIC_SECTOR_OPTIONS as lt, DEFAULT_STRONG_POINT_OPTIONS as m, blockDrawRule as mn, DEFAULT_CLASSIC_ARROW_OPTIONS as mt, resolveStyleHints as n, line24DrawRule as nn, DEFAULT_COUNTERATTACK_OPTIONS as nt, getControlMeasureMetadata as o, ambushDrawRule as on, RADAR_SEARCH_DOCTRINE_FILL_COLOR as ot, DEFAULT_RETAIN_OPTIONS as p, disruptDrawRule as pn, DEFAULT_GENERIC_CIRCLE_OPTIONS as pt, DEFAULT_ENCIRCLEMENT_OPTIONS as q, DEFAULT_STROKE_WIDTH_CSS_PIXELS as qt, CONTROL_MEASURE_IDS as r, line23DrawRule as rn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as rt, getControlMeasureMetadataByValue as s, attackByFireDrawRule as sn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as st, renderControlMeasure as t, line26DrawRule as tn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, staticPointDrawRule as un, DEFAULT_GENERIC_RECTANGLE_OPTIONS as ut, DEFAULT_PICKUP_ZONE_OPTIONS as v, createMidpointPerpendicularDrawRule as vn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as vt, DEFAULT_LANDING_ZONE_OPTIONS as w, computeInitialWidthPoint as wn, DEFAULT_BLOCK_OPTIONS as wt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as x, snapToMidpointPerpendicular as xn, DEFAULT_LIGHT_LINE_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as y, getMidpointPerpendicularSignedDistance as yn, DEFAULT_GENERIC_C2_LINE_OPTIONS as yt, DEFAULT_RELEASE_LINE_OPTIONS as z, normalizeTextAmplifiers as zt };
|