@orbat-mapper/control-measures 0.2.0-alpha.12 → 0.2.0-alpha.14
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-CsJcNxFQ.d.mts → index-CG1Pvd7o.d.mts} +173 -26
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/preview/index.d.mts +1 -1
- package/dist/preview/index.mjs +1 -1
- package/dist/{renderControlMeasure-DHQO01PA.mjs → renderControlMeasure-DWjLu2kT.mjs} +1875 -617
- package/package.json +1 -1
|
@@ -510,7 +510,7 @@ function samePosition(a, b) {
|
|
|
510
510
|
}
|
|
511
511
|
//#endregion
|
|
512
512
|
//#region src/draw-rules/area1.ts
|
|
513
|
-
function derive$
|
|
513
|
+
function derive$9(points) {
|
|
514
514
|
return clonePositions(points);
|
|
515
515
|
}
|
|
516
516
|
/**
|
|
@@ -530,9 +530,9 @@ const area1DrawRule = {
|
|
|
530
530
|
id: "area1",
|
|
531
531
|
minimumUserPoints: 3,
|
|
532
532
|
closedRing: true,
|
|
533
|
-
derive: derive$
|
|
533
|
+
derive: derive$9,
|
|
534
534
|
transform(event) {
|
|
535
|
-
return derive$
|
|
535
|
+
return derive$9(event.next);
|
|
536
536
|
}
|
|
537
537
|
};
|
|
538
538
|
//#endregion
|
|
@@ -558,14 +558,14 @@ const disruptDrawRule = createMidpointPerpendicularDrawRule({
|
|
|
558
558
|
* that fixes both the radius and the bearing of the symbol's opening. Both
|
|
559
559
|
* points are user-clicked, so the canonical array is just the (clamped) input.
|
|
560
560
|
*/
|
|
561
|
-
function derive$
|
|
561
|
+
function derive$8(points) {
|
|
562
562
|
return clonePositions(points.slice(0, 2));
|
|
563
563
|
}
|
|
564
564
|
const centerRadiusDrawRule = {
|
|
565
565
|
id: "area15:center-radius",
|
|
566
566
|
minimumUserPoints: 2,
|
|
567
567
|
canonicalPointCount: 2,
|
|
568
|
-
derive: derive$
|
|
568
|
+
derive: derive$8,
|
|
569
569
|
transform(event) {
|
|
570
570
|
const { previous, next, activePointIndex } = event;
|
|
571
571
|
if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
|
|
@@ -574,7 +574,7 @@ const centerRadiusDrawRule = {
|
|
|
574
574
|
const origin = previous[1];
|
|
575
575
|
return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
|
|
576
576
|
}
|
|
577
|
-
return derive$
|
|
577
|
+
return derive$8(next);
|
|
578
578
|
}
|
|
579
579
|
};
|
|
580
580
|
/** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
|
|
@@ -619,20 +619,20 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
|
|
|
619
619
|
});
|
|
620
620
|
//#endregion
|
|
621
621
|
//#region src/draw-rules/line1.ts
|
|
622
|
-
function derive$
|
|
622
|
+
function derive$7(points) {
|
|
623
623
|
return clonePositions(points);
|
|
624
624
|
}
|
|
625
625
|
const line1DrawRule = {
|
|
626
626
|
id: "line1",
|
|
627
627
|
minimumUserPoints: 2,
|
|
628
|
-
derive: derive$
|
|
628
|
+
derive: derive$7,
|
|
629
629
|
transform(event) {
|
|
630
|
-
return derive$
|
|
630
|
+
return derive$7(event.next);
|
|
631
631
|
}
|
|
632
632
|
};
|
|
633
633
|
//#endregion
|
|
634
634
|
//#region src/draw-rules/line3.ts
|
|
635
|
-
function derive$
|
|
635
|
+
function derive$6(points) {
|
|
636
636
|
return clonePositions(points);
|
|
637
637
|
}
|
|
638
638
|
/**
|
|
@@ -647,23 +647,23 @@ const line3DrawRule = {
|
|
|
647
647
|
id: "line3",
|
|
648
648
|
minimumUserPoints: 3,
|
|
649
649
|
canonicalPointCount: 3,
|
|
650
|
-
derive: derive$
|
|
650
|
+
derive: derive$6,
|
|
651
651
|
transform(event) {
|
|
652
|
-
return derive$
|
|
652
|
+
return derive$6(event.next);
|
|
653
653
|
}
|
|
654
654
|
};
|
|
655
655
|
//#endregion
|
|
656
656
|
//#region src/draw-rules/line9.ts
|
|
657
|
-
function derive$
|
|
657
|
+
function derive$5(points) {
|
|
658
658
|
return clonePositions(points);
|
|
659
659
|
}
|
|
660
660
|
const line9DrawRule = {
|
|
661
661
|
id: "line9",
|
|
662
662
|
minimumUserPoints: 2,
|
|
663
663
|
canonicalPointCount: 2,
|
|
664
|
-
derive: derive$
|
|
664
|
+
derive: derive$5,
|
|
665
665
|
transform(event) {
|
|
666
|
-
return derive$
|
|
666
|
+
return derive$5(event.next);
|
|
667
667
|
}
|
|
668
668
|
};
|
|
669
669
|
//#endregion
|
|
@@ -768,6 +768,52 @@ const line24DrawRule = createMidpointPerpendicularDrawRule({
|
|
|
768
768
|
minimumPreviewPoints: 2
|
|
769
769
|
});
|
|
770
770
|
//#endregion
|
|
771
|
+
//#region src/draw-rules/line26.ts
|
|
772
|
+
function derive$4(points) {
|
|
773
|
+
if (points.length === 2) {
|
|
774
|
+
const [p1, p2] = points;
|
|
775
|
+
const dx = p2[0] - p1[0];
|
|
776
|
+
const dy = p2[1] - p1[1];
|
|
777
|
+
const p3 = [p2[0] + dx * .5, p2[1] + dy * .5];
|
|
778
|
+
return clonePositions([
|
|
779
|
+
p1,
|
|
780
|
+
p2,
|
|
781
|
+
p3,
|
|
782
|
+
[p3[0] + dx, p3[1] + dy]
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
if (points.length === 3) {
|
|
786
|
+
const [p1, p2, p3] = points;
|
|
787
|
+
return clonePositions([
|
|
788
|
+
p1,
|
|
789
|
+
p2,
|
|
790
|
+
p3,
|
|
791
|
+
[p3[0] + p2[0] - p1[0], p3[1] + p2[1] - p1[1]]
|
|
792
|
+
]);
|
|
793
|
+
}
|
|
794
|
+
return clonePositions(points);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Line26 anchor draw rule for the Cover, Guard, and Screen security tasks.
|
|
798
|
+
*
|
|
799
|
+
* PT. 1/PT. 4 are the outward arrow tips and PT. 2/PT. 3 bound the opening
|
|
800
|
+
* reserved for the supported tactical symbol. All four anchors are free: the
|
|
801
|
+
* two sides may independently touch the symbols that define the task limits.
|
|
802
|
+
* During drawing, the cursor after PT. 1 supplies a provisional PT. 2 and the
|
|
803
|
+
* rule mirrors that first leg across a temporary center opening, so the full
|
|
804
|
+
* symbol is visible before PT. 2 is committed.
|
|
805
|
+
*/
|
|
806
|
+
const line26DrawRule = {
|
|
807
|
+
id: "line26",
|
|
808
|
+
minimumUserPoints: 4,
|
|
809
|
+
minimumPreviewPoints: 2,
|
|
810
|
+
canonicalPointCount: 4,
|
|
811
|
+
derive: derive$4,
|
|
812
|
+
transform(event) {
|
|
813
|
+
return derive$4(event.next);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
//#endregion
|
|
771
817
|
//#region src/draw-rules/area8.ts
|
|
772
818
|
function derive$3(points) {
|
|
773
819
|
if (points.length < 2) return clonePositions(points);
|
|
@@ -1761,6 +1807,27 @@ function buildGappedLine(verts, gaps) {
|
|
|
1761
1807
|
return parts;
|
|
1762
1808
|
}
|
|
1763
1809
|
//#endregion
|
|
1810
|
+
//#region src/internal/angle-utils.ts
|
|
1811
|
+
/**
|
|
1812
|
+
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
1813
|
+
*/
|
|
1814
|
+
function normalizeRadians(angle) {
|
|
1815
|
+
let normalized = angle % (Math.PI * 2);
|
|
1816
|
+
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
1817
|
+
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
1818
|
+
return normalized;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
1822
|
+
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
1823
|
+
*/
|
|
1824
|
+
function keepTextLeftToRight(rotation) {
|
|
1825
|
+
const normalized = normalizeRadians(rotation);
|
|
1826
|
+
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
1827
|
+
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
1828
|
+
return normalized;
|
|
1829
|
+
}
|
|
1830
|
+
//#endregion
|
|
1764
1831
|
//#region src/internal/sketch.ts
|
|
1765
1832
|
/**
|
|
1766
1833
|
* Projects `coordinates` and derives the arrow's tip, heading and total length.
|
|
@@ -1916,177 +1983,6 @@ function closedCatmullRom(points, samples) {
|
|
|
1916
1983
|
return out;
|
|
1917
1984
|
}
|
|
1918
1985
|
//#endregion
|
|
1919
|
-
//#region src/internal/area-echelon.ts
|
|
1920
|
-
/**
|
|
1921
|
-
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
1922
|
-
* counter-clockwise in projected (y-up) coordinates. Exported for reuse by
|
|
1923
|
-
* `area-labels.ts`'s polygon-centroid label placement, so the area centroid
|
|
1924
|
-
* shares this one derivation instead of re-deriving it.
|
|
1925
|
-
*/
|
|
1926
|
-
function ringSignedArea2(verts) {
|
|
1927
|
-
let area2 = 0;
|
|
1928
|
-
for (let i = 0; i < verts.length - 1; i++) {
|
|
1929
|
-
const a = verts[i];
|
|
1930
|
-
const b = verts[i + 1];
|
|
1931
|
-
area2 += a[0] * b[1] - b[0] * a[1];
|
|
1932
|
-
}
|
|
1933
|
-
return area2;
|
|
1934
|
-
}
|
|
1935
|
-
/**
|
|
1936
|
-
* Projects `positions` and returns a closed ring (first == last), optionally
|
|
1937
|
-
* smoothed through the control points with a closed centripetal Catmull-Rom
|
|
1938
|
-
* spline. A trailing vertex that duplicates the first (already-closed input)
|
|
1939
|
-
* is dropped before closing/smoothing.
|
|
1940
|
-
*/
|
|
1941
|
-
function buildClosedRing(positions, smooth, smoothResolution, defaultSmoothResolution) {
|
|
1942
|
-
const projected = positions.map((p) => project(p[0], p[1]));
|
|
1943
|
-
const ringPts = projected.length > 1 && Math.abs(projected[0][0] - projected[projected.length - 1][0]) < 1e-6 && Math.abs(projected[0][1] - projected[projected.length - 1][1]) < 1e-6 ? projected.slice(0, -1) : projected;
|
|
1944
|
-
return smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, defaultSmoothResolution)) : [...ringPts, ringPts[0]];
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Walks a closed ring into per-segment metadata (unit direction, outward
|
|
1948
|
-
* normal, cumulative arc length) plus the total perimeter and the arc-length
|
|
1949
|
-
* anchor of the bottom edge — the segment whose midpoint sits lowest (min
|
|
1950
|
-
* projected y), the side opposite the front.
|
|
1951
|
-
*/
|
|
1952
|
-
function buildAreaPerimeter(verts) {
|
|
1953
|
-
const ccw = ringSignedArea2(verts) > 0;
|
|
1954
|
-
const segs = [];
|
|
1955
|
-
let totalLength = 0;
|
|
1956
|
-
let anchorTarget = 0;
|
|
1957
|
-
let lowest = Infinity;
|
|
1958
|
-
for (let i = 0; i < verts.length - 1; i++) {
|
|
1959
|
-
const a = verts[i];
|
|
1960
|
-
const b = verts[i + 1];
|
|
1961
|
-
const edge = vecSub(b, a);
|
|
1962
|
-
const length = vecMag(edge);
|
|
1963
|
-
if (length < 1e-6) continue;
|
|
1964
|
-
const along = vecNorm(edge);
|
|
1965
|
-
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
1966
|
-
const perp = ccw ? [along[1], -along[0]] : [-along[1], along[0]];
|
|
1967
|
-
segs.push({
|
|
1968
|
-
start: a,
|
|
1969
|
-
along,
|
|
1970
|
-
perp,
|
|
1971
|
-
length,
|
|
1972
|
-
arcStart: totalLength
|
|
1973
|
-
});
|
|
1974
|
-
if (mid[1] < lowest) {
|
|
1975
|
-
lowest = mid[1];
|
|
1976
|
-
anchorTarget = totalLength + length / 2;
|
|
1977
|
-
}
|
|
1978
|
-
totalLength += length;
|
|
1979
|
-
}
|
|
1980
|
-
return {
|
|
1981
|
-
verts,
|
|
1982
|
-
segs,
|
|
1983
|
-
totalLength,
|
|
1984
|
-
anchorTarget
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
/** Resolves the arc-length `target` to a point and the segment frame it lands on. */
|
|
1988
|
-
function pointOnRing(segs, target) {
|
|
1989
|
-
let seg = segs[segs.length - 1];
|
|
1990
|
-
for (const candidate of segs) if (target < candidate.arcStart + candidate.length) {
|
|
1991
|
-
seg = candidate;
|
|
1992
|
-
break;
|
|
1993
|
-
}
|
|
1994
|
-
return {
|
|
1995
|
-
point: vecAdd(seg.start, vecScale(seg.along, target - seg.arcStart)),
|
|
1996
|
-
seg
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* Effective echelon glyph height in meters. Screen-anchored when a pixel size
|
|
2001
|
-
* is supplied with a live resolution; ground-anchored (meters) otherwise.
|
|
2002
|
-
* Clamped strictly positive.
|
|
2003
|
-
*/
|
|
2004
|
-
function resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel) {
|
|
2005
|
-
let h = echelonSize;
|
|
2006
|
-
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
2007
|
-
return Math.max(EPSILON, h);
|
|
2008
|
-
}
|
|
2009
|
-
/** Wraps `x` into `[0, 1)`, treating `x` as a fraction (matches the arc-length wrap below). */
|
|
2010
|
-
function wrap01(x) {
|
|
2011
|
-
return (x % 1 + 1) % 1;
|
|
2012
|
-
}
|
|
2013
|
-
/**
|
|
2014
|
-
* Places the Field B echelon glyph on the perimeter and computes its masking
|
|
2015
|
-
* gap, anchored by default to the bottom edge and slid by `echelonPosition`
|
|
2016
|
-
* (a signed fraction of the perimeter, wrapping at the seam). When
|
|
2017
|
-
* `echelonAnchor` is supplied, it replaces the bottom-edge anchor with an
|
|
2018
|
-
* absolute perimeter fraction (measured from the ring seam) — see
|
|
2019
|
-
* `BattlePositionOptions.echelonAnchor` for why (ADR-0023 rotate-freeze).
|
|
2020
|
-
* Returns empty geometry when the echelon resolves to none or the ring is
|
|
2021
|
-
* degenerate.
|
|
2022
|
-
*/
|
|
2023
|
-
function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor) {
|
|
2024
|
-
const { segs, totalLength, anchorTarget } = perimeter;
|
|
2025
|
-
const spec = resolveEchelon(echelon);
|
|
2026
|
-
if (!spec || segs.length === 0 || totalLength < 1e-6) return {
|
|
2027
|
-
strokes: [],
|
|
2028
|
-
fills: [],
|
|
2029
|
-
gaps: []
|
|
2030
|
-
};
|
|
2031
|
-
const target = (((echelonAnchor !== void 0 ? wrap01(echelonAnchor) * totalLength : anchorTarget) + echelonPosition * totalLength) % totalLength + totalLength) % totalLength;
|
|
2032
|
-
const { point, seg } = pointOnRing(segs, target);
|
|
2033
|
-
const { strokes, fills, width } = buildEchelonGlyph(point, seg.along, seg.perp, h, spec);
|
|
2034
|
-
const gaps = [];
|
|
2035
|
-
const halfGap = width / 2 + Math.max(0, echelonPadding) * h;
|
|
2036
|
-
pushWrappedGap(gaps, target - halfGap, target + halfGap, totalLength);
|
|
2037
|
-
return {
|
|
2038
|
-
strokes,
|
|
2039
|
-
fills,
|
|
2040
|
-
gaps
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
/**
|
|
2044
|
-
* Factory for the `freezeOrientation` hook (ADR-0023) shared by every
|
|
2045
|
-
* echelon-carrying closed-area measure (Battle Position, Strong Point, Base
|
|
2046
|
-
* Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
|
|
2047
|
-
* fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
|
|
2048
|
-
* transform box's rotate grip) does not re-derive a different "bottom edge"
|
|
2049
|
-
* against the rotated geometry. `defaults` supplies the measure's own
|
|
2050
|
-
* `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
|
|
2051
|
-
* `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
|
|
2052
|
-
* vs. Base Camp/Guerrilla Base's `echelon: "none"`).
|
|
2053
|
-
*
|
|
2054
|
-
* The returned hook returns `undefined` — nothing to freeze — when:
|
|
2055
|
-
* - `echelonAnchor` is already set (previously frozen or user-authored), or
|
|
2056
|
-
* - the echelon resolves to none (nothing visible to pin), or
|
|
2057
|
-
* - the ring is degenerate (no segments, or a near-zero perimeter).
|
|
2058
|
-
*/
|
|
2059
|
-
function makeFreezeEchelonAnchor(defaults) {
|
|
2060
|
-
return function freezeEchelonAnchor(controlPoints, options) {
|
|
2061
|
-
if (options.echelonAnchor !== void 0) return void 0;
|
|
2062
|
-
if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
|
|
2063
|
-
const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
|
|
2064
|
-
if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
|
|
2065
|
-
return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
//#endregion
|
|
2069
|
-
//#region src/internal/angle-utils.ts
|
|
2070
|
-
/**
|
|
2071
|
-
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
2072
|
-
*/
|
|
2073
|
-
function normalizeRadians(angle) {
|
|
2074
|
-
let normalized = angle % (Math.PI * 2);
|
|
2075
|
-
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
2076
|
-
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
2077
|
-
return normalized;
|
|
2078
|
-
}
|
|
2079
|
-
/**
|
|
2080
|
-
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
2081
|
-
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
2082
|
-
*/
|
|
2083
|
-
function keepTextLeftToRight(rotation) {
|
|
2084
|
-
const normalized = normalizeRadians(rotation);
|
|
2085
|
-
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
2086
|
-
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
2087
|
-
return normalized;
|
|
2088
|
-
}
|
|
2089
|
-
//#endregion
|
|
2090
1986
|
//#region src/internal/line-labels.ts
|
|
2091
1987
|
/**
|
|
2092
1988
|
* Stored label rotation for text running along projected direction `dir`.
|
|
@@ -2390,29 +2286,675 @@ function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
|
2390
2286
|
};
|
|
2391
2287
|
}
|
|
2392
2288
|
//#endregion
|
|
2393
|
-
//#region src/
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
const
|
|
2399
|
-
/**
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2289
|
+
//#region src/generators/cm14-maneuver-lines/direction-of-attack-shared.ts
|
|
2290
|
+
const DEFAULT_DIRECTION_ARROWHEAD_LENGTH_RATIO = .12;
|
|
2291
|
+
const DEFAULT_DIRECTION_ARROWHEAD_WIDTH_RATIO = 1.15;
|
|
2292
|
+
const DEFAULT_DIRECTION_LABEL_POSITION = .5;
|
|
2293
|
+
/** Approximate sans-serif character width/height ratio used for the T knockout. */
|
|
2294
|
+
const LABEL_CHAR_ASPECT_RATIO = .6;
|
|
2295
|
+
/** Clear space on each side of T, as a ratio of the resolved label height. */
|
|
2296
|
+
const LABEL_GAP_CLEARANCE_RATIO = .3;
|
|
2297
|
+
function nonNegative$1(value, fallback) {
|
|
2298
|
+
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
2299
|
+
}
|
|
2300
|
+
/** Resolves an along-line position option onto the valid tail-to-tip range. */
|
|
2301
|
+
function clampLinePosition(value, fallback) {
|
|
2302
|
+
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
2303
|
+
}
|
|
2304
|
+
/** Arc-length knockout centered on an along-route text label. */
|
|
2305
|
+
function directionLabelGap(totalLength, fraction, text, options) {
|
|
2306
|
+
if (!text) return void 0;
|
|
2307
|
+
const textHeight = resolveLabelOffsetMeters(options, 1);
|
|
2308
|
+
const textWidth = text.length * LABEL_CHAR_ASPECT_RATIO * textHeight;
|
|
2309
|
+
const clearance = (LABEL_GAP_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * textHeight;
|
|
2310
|
+
const halfGap = textWidth / 2 + clearance;
|
|
2311
|
+
const center = fraction * totalLength;
|
|
2312
|
+
return {
|
|
2313
|
+
g0: Math.max(0, center - halfGap),
|
|
2314
|
+
g1: Math.min(totalLength, center + halfGap)
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function createDirectionOfAttackLine(coordinates, options, textAmplifiers, config) {
|
|
2318
|
+
const verts = smoothLineVerts(coordinates.slice().reverse().map((position) => project(position[0], position[1])), options, 12);
|
|
2319
|
+
const { segments, totalLength } = polylineSegments(verts);
|
|
2320
|
+
if (segments.length === 0) return {
|
|
2321
|
+
type: "FeatureCollection",
|
|
2322
|
+
features: []
|
|
2323
|
+
};
|
|
2324
|
+
const last = segments[segments.length - 1];
|
|
2325
|
+
const tip = vecAdd(last.start, vecScale(last.along, last.length));
|
|
2326
|
+
const length = Math.min(totalLength, totalLength * nonNegative$1(options.arrowheadLengthRatio, DEFAULT_DIRECTION_ARROWHEAD_LENGTH_RATIO));
|
|
2327
|
+
const innerHalfWidth = length * nonNegative$1(options.arrowheadWidthRatio, DEFAULT_DIRECTION_ARROWHEAD_WIDTH_RATIO) / 2;
|
|
2328
|
+
const base = vecSub(tip, vecScale(last.along, length));
|
|
2329
|
+
const innerLeft = vecAdd(base, vecScale(last.perp, innerHalfWidth));
|
|
2330
|
+
const innerRight = vecSub(base, vecScale(last.perp, innerHalfWidth));
|
|
2331
|
+
const arrowhead = config.outlineRatio ? (() => {
|
|
2332
|
+
const ratio = Math.max(0, config.outlineRatio);
|
|
2333
|
+
const outerHalfWidth = innerHalfWidth * (1 + ratio);
|
|
2334
|
+
const outerLeft = vecAdd(base, vecScale(last.perp, outerHalfWidth));
|
|
2335
|
+
return [
|
|
2336
|
+
innerLeft,
|
|
2337
|
+
tip,
|
|
2338
|
+
innerRight,
|
|
2339
|
+
vecSub(base, vecScale(last.perp, outerHalfWidth)),
|
|
2340
|
+
vecAdd(tip, vecScale(last.along, length * ratio)),
|
|
2341
|
+
outerLeft,
|
|
2342
|
+
innerLeft
|
|
2343
|
+
];
|
|
2344
|
+
})() : [
|
|
2345
|
+
innerLeft,
|
|
2346
|
+
tip,
|
|
2347
|
+
innerRight
|
|
2348
|
+
];
|
|
2349
|
+
const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
|
|
2350
|
+
const labelGap = directionLabelGap(totalLength, labelPosition, textAmplifiers.T, options);
|
|
2351
|
+
const lines = [...buildGappedLine(verts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
|
|
2352
|
+
const features = [{
|
|
2353
|
+
type: "Feature",
|
|
2354
|
+
properties: { part: config.part },
|
|
2355
|
+
geometry: {
|
|
2356
|
+
type: "MultiLineString",
|
|
2357
|
+
coordinates: lines
|
|
2358
|
+
}
|
|
2359
|
+
}];
|
|
2360
|
+
const frame = pointAlongPolyline(segments, totalLength, labelPosition);
|
|
2361
|
+
if (!frame) return {
|
|
2362
|
+
type: "FeatureCollection",
|
|
2363
|
+
features
|
|
2364
|
+
};
|
|
2365
|
+
const labels = [];
|
|
2366
|
+
const rotation = labelRotationAlong(frame.along);
|
|
2367
|
+
const sizeProps = labelSizeProps(options);
|
|
2368
|
+
if (textAmplifiers.T) pushLabel(labels, frame.point, textAmplifiers.T, rotation, sizeProps, void 0, "T");
|
|
2369
|
+
const rowOffset = resolveLabelOffsetMeters(options, 1 + Math.max(0, options.labelPadding ?? 0));
|
|
2370
|
+
if (textAmplifiers.W) pushLabel(labels, vecSub(frame.point, vecScale(frame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
|
|
2371
|
+
if (textAmplifiers.W1) pushLabel(labels, vecSub(frame.point, vecScale(frame.perp, rowOffset * 2)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
|
|
2372
|
+
features.push(...labels);
|
|
2373
|
+
return {
|
|
2374
|
+
type: "FeatureCollection",
|
|
2375
|
+
features
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
const DIRECTION_OF_ATTACK_PARAMS = [
|
|
2379
|
+
{
|
|
2380
|
+
key: "arrowheadLengthRatio",
|
|
2381
|
+
label: "Arrowhead length",
|
|
2382
|
+
description: "Arrowhead length as a ratio of the route's total length",
|
|
2383
|
+
type: "number",
|
|
2384
|
+
min: .01,
|
|
2385
|
+
max: .5,
|
|
2386
|
+
step: .01
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
key: "arrowheadWidthRatio",
|
|
2390
|
+
label: "Arrowhead width",
|
|
2391
|
+
description: "Arrowhead total width as a ratio of its length",
|
|
2392
|
+
type: "number",
|
|
2393
|
+
min: .1,
|
|
2394
|
+
max: 2,
|
|
2395
|
+
step: .05
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
key: "labelPosition",
|
|
2399
|
+
label: "Label position",
|
|
2400
|
+
description: "Position of the T/W/W1 group along the route, from tail (0) to tip (1)",
|
|
2401
|
+
type: "number",
|
|
2402
|
+
min: 0,
|
|
2403
|
+
max: 1,
|
|
2404
|
+
step: .01
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
key: "labelPadding",
|
|
2408
|
+
label: "Label padding",
|
|
2409
|
+
description: "Extra spacing between the route and the date-time rows, in label heights",
|
|
2410
|
+
type: "number",
|
|
2411
|
+
min: 0,
|
|
2412
|
+
max: 2,
|
|
2413
|
+
step: .05
|
|
2414
|
+
}
|
|
2415
|
+
];
|
|
2416
|
+
const DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS = [
|
|
2417
|
+
{
|
|
2418
|
+
key: "T",
|
|
2419
|
+
label: "Unique designation",
|
|
2420
|
+
description: "Field T — the attack route's unique designation.",
|
|
2421
|
+
placeholder: "MAIN",
|
|
2422
|
+
maxLength: 21
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
key: "W",
|
|
2426
|
+
label: "Start date-time group",
|
|
2427
|
+
description: "Field W — the route's start date-time group.",
|
|
2428
|
+
placeholder: "080400ZOCT2008"
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
key: "W1",
|
|
2432
|
+
label: "End date-time group",
|
|
2433
|
+
description: "Field W1 — the route's end date-time group.",
|
|
2434
|
+
placeholder: "120300ZOCT2008"
|
|
2435
|
+
}
|
|
2436
|
+
];
|
|
2437
|
+
const DIRECTION_OF_ATTACK_PREVIEW_TEXT = {
|
|
2438
|
+
W: "080400ZOCT2008",
|
|
2439
|
+
W1: "120300ZOCT2008"
|
|
2440
|
+
};
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/generators/cm14-maneuver-lines/direction-of-attack-aviation.ts
|
|
2443
|
+
const DEFAULT_ARROWHEAD_LENGTH_RATIO$1 = .12;
|
|
2444
|
+
const DEFAULT_ARROWHEAD_WIDTH_RATIO$1 = 1.15;
|
|
2445
|
+
const DEFAULT_BOW_TIE_LENGTH_RATIO = .1;
|
|
2446
|
+
const DEFAULT_BOW_TIE_WIDTH_RATIO = .5;
|
|
2447
|
+
const DEFAULT_BOW_TIE_POSITION = 1 / 3;
|
|
2448
|
+
const DEFAULT_LABEL_POSITION = 2 / 3;
|
|
2449
|
+
const DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS = {
|
|
2450
|
+
arrowheadLengthRatio: DEFAULT_ARROWHEAD_LENGTH_RATIO$1,
|
|
2451
|
+
arrowheadWidthRatio: DEFAULT_ARROWHEAD_WIDTH_RATIO$1,
|
|
2452
|
+
bowTieLengthRatio: DEFAULT_BOW_TIE_LENGTH_RATIO,
|
|
2453
|
+
bowTieWidthRatio: DEFAULT_BOW_TIE_WIDTH_RATIO,
|
|
2454
|
+
bowTiePosition: DEFAULT_BOW_TIE_POSITION,
|
|
2455
|
+
labelPosition: DEFAULT_LABEL_POSITION,
|
|
2456
|
+
labelPadding: 0,
|
|
2457
|
+
smooth: false,
|
|
2458
|
+
smoothResolution: 12
|
|
2459
|
+
};
|
|
2460
|
+
const DIRECTION_OF_ATTACK_AVIATION_METADATA = {
|
|
2461
|
+
id: "direction-of-attack-aviation",
|
|
2462
|
+
name: "Direction of Attack — Airborne/Aviation",
|
|
2463
|
+
description: "The direction or route of an airborne or aviation attack.",
|
|
2464
|
+
entity: "Maneuver Lines",
|
|
2465
|
+
entityType: "Direction of Attack",
|
|
2466
|
+
entitySubtype: "Airborne/Aviation",
|
|
2467
|
+
value: "140601",
|
|
2468
|
+
minCoordinates: 2,
|
|
2469
|
+
geometry: "line",
|
|
2470
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
2471
|
+
drawRule: "Line1",
|
|
2472
|
+
capturesLabelSize: true,
|
|
2473
|
+
params: [
|
|
2474
|
+
...SMOOTH_LINE_PARAMS,
|
|
2475
|
+
{
|
|
2476
|
+
key: "arrowheadLengthRatio",
|
|
2477
|
+
label: "Arrowhead length",
|
|
2478
|
+
description: "Arrowhead length as a ratio of the route's total length",
|
|
2479
|
+
type: "number",
|
|
2480
|
+
min: .01,
|
|
2481
|
+
max: .5,
|
|
2482
|
+
step: .01
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
key: "arrowheadWidthRatio",
|
|
2486
|
+
label: "Arrowhead width",
|
|
2487
|
+
description: "Arrowhead total width as a ratio of its length",
|
|
2488
|
+
type: "number",
|
|
2489
|
+
min: .1,
|
|
2490
|
+
max: 2,
|
|
2491
|
+
step: .05
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
key: "bowTieLengthRatio",
|
|
2495
|
+
label: "Bow-tie length",
|
|
2496
|
+
description: "Aviation mark length as a ratio of the route's total length",
|
|
2497
|
+
type: "number",
|
|
2498
|
+
min: .02,
|
|
2499
|
+
max: .3,
|
|
2500
|
+
step: .01
|
|
2501
|
+
},
|
|
2502
|
+
{
|
|
2503
|
+
key: "bowTieWidthRatio",
|
|
2504
|
+
label: "Bow-tie width",
|
|
2505
|
+
description: "Aviation mark total width as a ratio of its length",
|
|
2506
|
+
type: "number",
|
|
2507
|
+
min: .1,
|
|
2508
|
+
max: 1.5,
|
|
2509
|
+
step: .05
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
key: "bowTiePosition",
|
|
2513
|
+
label: "Bow-tie position",
|
|
2514
|
+
description: "Position of the aviation mark along the route, from tail (0) to tip (1)",
|
|
2515
|
+
type: "number",
|
|
2516
|
+
min: 0,
|
|
2517
|
+
max: 1,
|
|
2518
|
+
step: .01
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
key: "labelPosition",
|
|
2522
|
+
label: "Label position",
|
|
2523
|
+
description: "Position of the T/W/W1 group along the route, from tail (0) to tip (1)",
|
|
2524
|
+
type: "number",
|
|
2525
|
+
min: 0,
|
|
2526
|
+
max: 1,
|
|
2527
|
+
step: .01
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
key: "labelPadding",
|
|
2531
|
+
label: "Label padding",
|
|
2532
|
+
description: "Extra spacing between the route and the date-time rows, in label heights",
|
|
2533
|
+
type: "number",
|
|
2534
|
+
min: 0,
|
|
2535
|
+
max: 2,
|
|
2536
|
+
step: .05
|
|
2537
|
+
}
|
|
2538
|
+
],
|
|
2539
|
+
textAmplifiers: [
|
|
2540
|
+
{
|
|
2541
|
+
key: "T",
|
|
2542
|
+
label: "Unique designation",
|
|
2543
|
+
description: "Field T — the attack route's unique designation.",
|
|
2544
|
+
placeholder: "AVON",
|
|
2545
|
+
maxLength: 21
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
key: "W",
|
|
2549
|
+
label: "Start date-time group",
|
|
2550
|
+
description: "Field W — the route's start date-time group.",
|
|
2551
|
+
placeholder: "080400ZOCT2008"
|
|
2552
|
+
},
|
|
2553
|
+
{
|
|
2554
|
+
key: "W1",
|
|
2555
|
+
label: "End date-time group",
|
|
2556
|
+
description: "Field W1 — the route's end date-time group.",
|
|
2557
|
+
placeholder: "120300ZOCT2008"
|
|
2558
|
+
}
|
|
2559
|
+
]
|
|
2560
|
+
};
|
|
2561
|
+
function nonNegative(value, fallback) {
|
|
2562
|
+
return Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
2563
|
+
}
|
|
2564
|
+
function pointAtDistance(verts, distance) {
|
|
2565
|
+
let walked = 0;
|
|
2566
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
2567
|
+
const a = verts[i];
|
|
2568
|
+
const delta = vecSub(verts[i + 1], a);
|
|
2569
|
+
const length = vecMag(delta);
|
|
2570
|
+
if (length < 1e-6) continue;
|
|
2571
|
+
if (distance <= walked + length) return vecAdd(a, vecScale(delta, (distance - walked) / length));
|
|
2572
|
+
walked += length;
|
|
2573
|
+
}
|
|
2574
|
+
return verts[verts.length - 1];
|
|
2575
|
+
}
|
|
2576
|
+
/** Builds the route, aviation bow-tie, arrowhead, and optional T/W/W1 labels. */
|
|
2577
|
+
function createDirectionOfAttackAviation(coordinates, options = {}, textAmplifiers = {}) {
|
|
2578
|
+
const resolved = {
|
|
2579
|
+
...DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS,
|
|
2580
|
+
...options
|
|
2581
|
+
};
|
|
2582
|
+
const verts = smoothLineVerts(coordinates.slice().reverse().map((position) => project(position[0], position[1])), resolved, 12);
|
|
2583
|
+
const { segments, totalLength } = polylineSegments(verts);
|
|
2584
|
+
if (segments.length === 0 || totalLength < 1e-6) return {
|
|
2585
|
+
type: "FeatureCollection",
|
|
2586
|
+
features: []
|
|
2587
|
+
};
|
|
2588
|
+
const bowTieLength = Math.min(totalLength, totalLength * nonNegative(resolved.bowTieLengthRatio, DEFAULT_BOW_TIE_LENGTH_RATIO));
|
|
2589
|
+
const bowTiePosition = clampLinePosition(resolved.bowTiePosition, DEFAULT_BOW_TIE_POSITION);
|
|
2590
|
+
const labelPosition = clampLinePosition(resolved.labelPosition, DEFAULT_LABEL_POSITION);
|
|
2591
|
+
const bowTieCenterDistance = totalLength * bowTiePosition;
|
|
2592
|
+
const bowTieStartDistance = Math.max(0, bowTieCenterDistance - bowTieLength / 2);
|
|
2593
|
+
const bowTieEndDistance = Math.min(totalLength, bowTieCenterDistance + bowTieLength / 2);
|
|
2594
|
+
const bowTieStart = pointAtDistance(verts, bowTieStartDistance);
|
|
2595
|
+
const bowTieEnd = pointAtDistance(verts, bowTieEndDistance);
|
|
2596
|
+
const last = segments[segments.length - 1];
|
|
2597
|
+
const tip = vecAdd(last.start, vecScale(last.along, last.length));
|
|
2598
|
+
const arrowheadLength = Math.min(totalLength * nonNegative(resolved.arrowheadLengthRatio, DEFAULT_ARROWHEAD_LENGTH_RATIO$1), totalLength);
|
|
2599
|
+
const arrowheadHalfWidth = arrowheadLength * nonNegative(resolved.arrowheadWidthRatio, DEFAULT_ARROWHEAD_WIDTH_RATIO$1) / 2;
|
|
2600
|
+
const arrowBase = vecSub(tip, vecScale(last.along, arrowheadLength));
|
|
2601
|
+
const arrowLeft = vecAdd(arrowBase, vecScale(last.perp, arrowheadHalfWidth));
|
|
2602
|
+
const arrowRight = vecSub(arrowBase, vecScale(last.perp, arrowheadHalfWidth));
|
|
2603
|
+
if (!bowTieStart || !bowTieEnd) return {
|
|
2604
|
+
type: "FeatureCollection",
|
|
2605
|
+
features: []
|
|
2606
|
+
};
|
|
2607
|
+
const bowTieAlong = vecNorm(vecSub(bowTieEnd, bowTieStart));
|
|
2608
|
+
if (vecMag(bowTieAlong) < 1e-6) return {
|
|
2609
|
+
type: "FeatureCollection",
|
|
2610
|
+
features: []
|
|
2611
|
+
};
|
|
2612
|
+
const bowTiePerp = [-bowTieAlong[1], bowTieAlong[0]];
|
|
2613
|
+
const bowTieHalfWidth = bowTieLength * nonNegative(resolved.bowTieWidthRatio, DEFAULT_BOW_TIE_WIDTH_RATIO) / 2;
|
|
2614
|
+
const bowTieA = vecAdd(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
2615
|
+
const bowTieB = vecSub(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
2616
|
+
const bowTieC = vecAdd(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
2617
|
+
const bowTieD = vecSub(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
|
|
2618
|
+
const labelGap = directionLabelGap(totalLength, labelPosition, textAmplifiers.T, resolved);
|
|
2619
|
+
const features = [{
|
|
2620
|
+
type: "Feature",
|
|
2621
|
+
properties: { part: "direction-of-attack-aviation" },
|
|
2622
|
+
geometry: {
|
|
2623
|
+
type: "MultiLineString",
|
|
2624
|
+
coordinates: [
|
|
2625
|
+
...buildGappedLine(verts, [{
|
|
2626
|
+
g0: bowTieStartDistance,
|
|
2627
|
+
g1: bowTieEndDistance
|
|
2628
|
+
}, ...labelGap ? [labelGap] : []]),
|
|
2629
|
+
[
|
|
2630
|
+
arrowLeft,
|
|
2631
|
+
tip,
|
|
2632
|
+
arrowRight
|
|
2633
|
+
].map((point) => unproject(point[0], point[1])),
|
|
2634
|
+
[
|
|
2635
|
+
bowTieA,
|
|
2636
|
+
bowTieB,
|
|
2637
|
+
bowTieC,
|
|
2638
|
+
bowTieD,
|
|
2639
|
+
bowTieA
|
|
2640
|
+
].map((point) => unproject(point[0], point[1]))
|
|
2641
|
+
]
|
|
2642
|
+
}
|
|
2643
|
+
}];
|
|
2644
|
+
const labelFrame = pointAlongPolyline(segments, totalLength, labelPosition);
|
|
2645
|
+
if (labelFrame) {
|
|
2646
|
+
const labels = [];
|
|
2647
|
+
const rotation = labelRotationAlong(labelFrame.along);
|
|
2648
|
+
const sizeProps = labelSizeProps(resolved);
|
|
2649
|
+
if (textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, rotation, sizeProps, void 0, "T");
|
|
2650
|
+
const rowOffset = resolveLabelOffsetMeters(resolved, 1 + Math.max(0, resolved.labelPadding ?? 0));
|
|
2651
|
+
if (textAmplifiers.W) pushLabel(labels, vecSub(labelFrame.point, vecScale(labelFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
|
|
2652
|
+
if (textAmplifiers.W1) pushLabel(labels, vecSub(labelFrame.point, vecScale(labelFrame.perp, rowOffset * 2)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
|
|
2653
|
+
features.push(...labels);
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
type: "FeatureCollection",
|
|
2657
|
+
features
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
const DIRECTION_OF_ATTACK_AVIATION = defineControlMeasure({
|
|
2661
|
+
metadata: DIRECTION_OF_ATTACK_AVIATION_METADATA,
|
|
2662
|
+
generator: createDirectionOfAttackAviation,
|
|
2663
|
+
defaultOptions: DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS,
|
|
2664
|
+
rule: line1DrawRule,
|
|
2665
|
+
previewSample: {
|
|
2666
|
+
controlPoints: [[1, 0], [-1, 0]],
|
|
2667
|
+
textAmplifiers: {
|
|
2668
|
+
T: "AVON",
|
|
2669
|
+
W: "080400ZOCT2008",
|
|
2670
|
+
W1: "120300ZOCT2008"
|
|
2671
|
+
},
|
|
2672
|
+
options: { labelSize: 80 }
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
//#endregion
|
|
2676
|
+
//#region src/generators/cm14-maneuver-lines/direction-of-main-attack.ts
|
|
2677
|
+
const DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS = {
|
|
2678
|
+
arrowheadLengthRatio: DEFAULT_DIRECTION_ARROWHEAD_LENGTH_RATIO,
|
|
2679
|
+
arrowheadWidthRatio: DEFAULT_DIRECTION_ARROWHEAD_WIDTH_RATIO,
|
|
2680
|
+
arrowheadOutlineRatio: .3,
|
|
2681
|
+
labelPosition: DEFAULT_DIRECTION_LABEL_POSITION,
|
|
2682
|
+
labelPadding: 0,
|
|
2683
|
+
smooth: false,
|
|
2684
|
+
smoothResolution: 12
|
|
2685
|
+
};
|
|
2686
|
+
const DIRECTION_OF_MAIN_ATTACK_METADATA = {
|
|
2687
|
+
id: "direction-of-main-attack",
|
|
2688
|
+
name: "Direction of Main Attack",
|
|
2689
|
+
description: "The direction or route of the primary attack.",
|
|
2690
|
+
entity: "Maneuver Lines",
|
|
2691
|
+
entityType: "Direction of Attack",
|
|
2692
|
+
entitySubtype: "Main Attack",
|
|
2693
|
+
value: "140602",
|
|
2694
|
+
minCoordinates: 2,
|
|
2695
|
+
geometry: "line",
|
|
2696
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
2697
|
+
drawRule: "Line1",
|
|
2698
|
+
capturesLabelSize: true,
|
|
2699
|
+
params: [
|
|
2700
|
+
...SMOOTH_LINE_PARAMS,
|
|
2701
|
+
...DIRECTION_OF_ATTACK_PARAMS,
|
|
2702
|
+
{
|
|
2703
|
+
key: "arrowheadOutlineRatio",
|
|
2704
|
+
label: "Arrowhead outline",
|
|
2705
|
+
description: "Outlined arrowhead thickness as a ratio of its length",
|
|
2706
|
+
type: "number",
|
|
2707
|
+
min: .05,
|
|
2708
|
+
max: .75,
|
|
2709
|
+
step: .05
|
|
2710
|
+
}
|
|
2711
|
+
],
|
|
2712
|
+
textAmplifiers: DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS
|
|
2713
|
+
};
|
|
2714
|
+
function createDirectionOfMainAttack(coordinates, options = {}, textAmplifiers = {}) {
|
|
2715
|
+
const resolved = {
|
|
2716
|
+
...DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS,
|
|
2717
|
+
...options
|
|
2718
|
+
};
|
|
2719
|
+
return createDirectionOfAttackLine(coordinates, resolved, textAmplifiers, {
|
|
2720
|
+
part: "direction-of-main-attack",
|
|
2721
|
+
outlineRatio: resolved.arrowheadOutlineRatio
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
const DIRECTION_OF_MAIN_ATTACK = defineControlMeasure({
|
|
2725
|
+
metadata: DIRECTION_OF_MAIN_ATTACK_METADATA,
|
|
2726
|
+
generator: createDirectionOfMainAttack,
|
|
2727
|
+
defaultOptions: DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS,
|
|
2728
|
+
rule: line1DrawRule,
|
|
2729
|
+
previewSample: {
|
|
2730
|
+
controlPoints: [[1, 0], [-1, 0]],
|
|
2731
|
+
textAmplifiers: {
|
|
2732
|
+
T: "MAIN",
|
|
2733
|
+
...DIRECTION_OF_ATTACK_PREVIEW_TEXT
|
|
2734
|
+
},
|
|
2735
|
+
options: { labelSize: 80 }
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
//#endregion
|
|
2739
|
+
//#region src/generators/cm14-maneuver-lines/direction-of-supporting-attack.ts
|
|
2740
|
+
const DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS = {
|
|
2741
|
+
arrowheadLengthRatio: DEFAULT_DIRECTION_ARROWHEAD_LENGTH_RATIO,
|
|
2742
|
+
arrowheadWidthRatio: DEFAULT_DIRECTION_ARROWHEAD_WIDTH_RATIO,
|
|
2743
|
+
labelPosition: DEFAULT_DIRECTION_LABEL_POSITION,
|
|
2744
|
+
labelPadding: 0,
|
|
2745
|
+
smooth: false,
|
|
2746
|
+
smoothResolution: 12
|
|
2747
|
+
};
|
|
2748
|
+
const DIRECTION_OF_SUPPORTING_ATTACK_METADATA = {
|
|
2749
|
+
id: "direction-of-supporting-attack",
|
|
2750
|
+
name: "Direction of Supporting Attack",
|
|
2751
|
+
description: "The direction or route of an attack that supports the main effort.",
|
|
2752
|
+
entity: "Maneuver Lines",
|
|
2753
|
+
entityType: "Direction of Attack",
|
|
2754
|
+
entitySubtype: "Supporting Attack",
|
|
2755
|
+
value: "140603",
|
|
2756
|
+
minCoordinates: 2,
|
|
2757
|
+
geometry: "line",
|
|
2758
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
2759
|
+
drawRule: "Line1",
|
|
2760
|
+
capturesLabelSize: true,
|
|
2761
|
+
params: [...SMOOTH_LINE_PARAMS, ...DIRECTION_OF_ATTACK_PARAMS],
|
|
2762
|
+
textAmplifiers: DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS
|
|
2763
|
+
};
|
|
2764
|
+
function createDirectionOfSupportingAttack(coordinates, options = {}, textAmplifiers = {}) {
|
|
2765
|
+
return createDirectionOfAttackLine(coordinates, {
|
|
2766
|
+
...DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS,
|
|
2767
|
+
...options
|
|
2768
|
+
}, textAmplifiers, { part: "direction-of-supporting-attack" });
|
|
2769
|
+
}
|
|
2770
|
+
const DIRECTION_OF_SUPPORTING_ATTACK = defineControlMeasure({
|
|
2771
|
+
metadata: DIRECTION_OF_SUPPORTING_ATTACK_METADATA,
|
|
2772
|
+
generator: createDirectionOfSupportingAttack,
|
|
2773
|
+
defaultOptions: DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS,
|
|
2774
|
+
rule: line1DrawRule,
|
|
2775
|
+
previewSample: {
|
|
2776
|
+
controlPoints: [[1, 0], [-1, 0]],
|
|
2777
|
+
textAmplifiers: {
|
|
2778
|
+
T: "YALU",
|
|
2779
|
+
...DIRECTION_OF_ATTACK_PREVIEW_TEXT
|
|
2780
|
+
},
|
|
2781
|
+
options: { labelSize: 80 }
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/internal/area-echelon.ts
|
|
2786
|
+
/**
|
|
2787
|
+
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
2788
|
+
* counter-clockwise in projected (y-up) coordinates. Exported for reuse by
|
|
2789
|
+
* `area-labels.ts`'s polygon-centroid label placement, so the area centroid
|
|
2790
|
+
* shares this one derivation instead of re-deriving it.
|
|
2791
|
+
*/
|
|
2792
|
+
function ringSignedArea2(verts) {
|
|
2793
|
+
let area2 = 0;
|
|
2794
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
2795
|
+
const a = verts[i];
|
|
2796
|
+
const b = verts[i + 1];
|
|
2797
|
+
area2 += a[0] * b[1] - b[0] * a[1];
|
|
2798
|
+
}
|
|
2799
|
+
return area2;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Projects `positions` and returns a closed ring (first == last), optionally
|
|
2803
|
+
* smoothed through the control points with a closed centripetal Catmull-Rom
|
|
2804
|
+
* spline. A trailing vertex that duplicates the first (already-closed input)
|
|
2805
|
+
* is dropped before closing/smoothing.
|
|
2806
|
+
*/
|
|
2807
|
+
function buildClosedRing(positions, smooth, smoothResolution, defaultSmoothResolution) {
|
|
2808
|
+
const projected = positions.map((p) => project(p[0], p[1]));
|
|
2809
|
+
const ringPts = projected.length > 1 && Math.abs(projected[0][0] - projected[projected.length - 1][0]) < 1e-6 && Math.abs(projected[0][1] - projected[projected.length - 1][1]) < 1e-6 ? projected.slice(0, -1) : projected;
|
|
2810
|
+
return smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, defaultSmoothResolution)) : [...ringPts, ringPts[0]];
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Walks a closed ring into per-segment metadata (unit direction, outward
|
|
2814
|
+
* normal, cumulative arc length) plus the total perimeter and the arc-length
|
|
2815
|
+
* anchor of the bottom edge — the segment whose midpoint sits lowest (min
|
|
2816
|
+
* projected y), the side opposite the front.
|
|
2817
|
+
*/
|
|
2818
|
+
function buildAreaPerimeter(verts) {
|
|
2819
|
+
const ccw = ringSignedArea2(verts) > 0;
|
|
2820
|
+
const segs = [];
|
|
2821
|
+
let totalLength = 0;
|
|
2822
|
+
let anchorTarget = 0;
|
|
2823
|
+
let lowest = Infinity;
|
|
2824
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
2825
|
+
const a = verts[i];
|
|
2826
|
+
const b = verts[i + 1];
|
|
2827
|
+
const edge = vecSub(b, a);
|
|
2828
|
+
const length = vecMag(edge);
|
|
2829
|
+
if (length < 1e-6) continue;
|
|
2830
|
+
const along = vecNorm(edge);
|
|
2831
|
+
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
2832
|
+
const perp = ccw ? [along[1], -along[0]] : [-along[1], along[0]];
|
|
2833
|
+
segs.push({
|
|
2834
|
+
start: a,
|
|
2835
|
+
along,
|
|
2836
|
+
perp,
|
|
2837
|
+
length,
|
|
2838
|
+
arcStart: totalLength
|
|
2839
|
+
});
|
|
2840
|
+
if (mid[1] < lowest) {
|
|
2841
|
+
lowest = mid[1];
|
|
2842
|
+
anchorTarget = totalLength + length / 2;
|
|
2843
|
+
}
|
|
2844
|
+
totalLength += length;
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
verts,
|
|
2848
|
+
segs,
|
|
2849
|
+
totalLength,
|
|
2850
|
+
anchorTarget
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
/** Resolves the arc-length `target` to a point and the segment frame it lands on. */
|
|
2854
|
+
function pointOnRing(segs, target) {
|
|
2855
|
+
let seg = segs[segs.length - 1];
|
|
2856
|
+
for (const candidate of segs) if (target < candidate.arcStart + candidate.length) {
|
|
2857
|
+
seg = candidate;
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
point: vecAdd(seg.start, vecScale(seg.along, target - seg.arcStart)),
|
|
2862
|
+
seg
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Effective echelon glyph height in meters. Screen-anchored when a pixel size
|
|
2867
|
+
* is supplied with a live resolution; ground-anchored (meters) otherwise.
|
|
2868
|
+
* Clamped strictly positive.
|
|
2869
|
+
*/
|
|
2870
|
+
function resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel) {
|
|
2871
|
+
let h = echelonSize;
|
|
2872
|
+
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
2873
|
+
return Math.max(EPSILON, h);
|
|
2874
|
+
}
|
|
2875
|
+
/** Wraps `x` into `[0, 1)`, treating `x` as a fraction (matches the arc-length wrap below). */
|
|
2876
|
+
function wrap01(x) {
|
|
2877
|
+
return (x % 1 + 1) % 1;
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Places the Field B echelon glyph on the perimeter and computes its masking
|
|
2881
|
+
* gap, anchored by default to the bottom edge and slid by `echelonPosition`
|
|
2882
|
+
* (a signed fraction of the perimeter, wrapping at the seam). When
|
|
2883
|
+
* `echelonAnchor` is supplied, it replaces the bottom-edge anchor with an
|
|
2884
|
+
* absolute perimeter fraction (measured from the ring seam) — see
|
|
2885
|
+
* `BattlePositionOptions.echelonAnchor` for why (ADR-0023 rotate-freeze).
|
|
2886
|
+
* Returns empty geometry when the echelon resolves to none or the ring is
|
|
2887
|
+
* degenerate.
|
|
2888
|
+
*/
|
|
2889
|
+
function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor) {
|
|
2890
|
+
const { segs, totalLength, anchorTarget } = perimeter;
|
|
2891
|
+
const spec = resolveEchelon(echelon);
|
|
2892
|
+
if (!spec || segs.length === 0 || totalLength < 1e-6) return {
|
|
2893
|
+
strokes: [],
|
|
2894
|
+
fills: [],
|
|
2895
|
+
gaps: []
|
|
2896
|
+
};
|
|
2897
|
+
const target = (((echelonAnchor !== void 0 ? wrap01(echelonAnchor) * totalLength : anchorTarget) + echelonPosition * totalLength) % totalLength + totalLength) % totalLength;
|
|
2898
|
+
const { point, seg } = pointOnRing(segs, target);
|
|
2899
|
+
const { strokes, fills, width } = buildEchelonGlyph(point, seg.along, seg.perp, h, spec);
|
|
2900
|
+
const gaps = [];
|
|
2901
|
+
const halfGap = width / 2 + Math.max(0, echelonPadding) * h;
|
|
2902
|
+
pushWrappedGap(gaps, target - halfGap, target + halfGap, totalLength);
|
|
2903
|
+
return {
|
|
2904
|
+
strokes,
|
|
2905
|
+
fills,
|
|
2906
|
+
gaps
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Factory for the `freezeOrientation` hook (ADR-0023) shared by every
|
|
2911
|
+
* echelon-carrying closed-area measure (Battle Position, Strong Point, Base
|
|
2912
|
+
* Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
|
|
2913
|
+
* fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
|
|
2914
|
+
* transform box's rotate grip) does not re-derive a different "bottom edge"
|
|
2915
|
+
* against the rotated geometry. `defaults` supplies the measure's own
|
|
2916
|
+
* `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
|
|
2917
|
+
* `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
|
|
2918
|
+
* vs. Base Camp/Guerrilla Base's `echelon: "none"`).
|
|
2919
|
+
*
|
|
2920
|
+
* The returned hook returns `undefined` — nothing to freeze — when:
|
|
2921
|
+
* - `echelonAnchor` is already set (previously frozen or user-authored), or
|
|
2922
|
+
* - the echelon resolves to none (nothing visible to pin), or
|
|
2923
|
+
* - the ring is degenerate (no segments, or a near-zero perimeter).
|
|
2924
|
+
*/
|
|
2925
|
+
function makeFreezeEchelonAnchor(defaults) {
|
|
2926
|
+
return function freezeEchelonAnchor(controlPoints, options) {
|
|
2927
|
+
if (options.echelonAnchor !== void 0) return void 0;
|
|
2928
|
+
if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
|
|
2929
|
+
const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
|
|
2930
|
+
if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
|
|
2931
|
+
return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
//#endregion
|
|
2935
|
+
//#region src/internal/area-labels.ts
|
|
2936
|
+
/** Stored label rotation for horizontal (screen-upright) area text — adapters
|
|
2937
|
+
* render feature rotation as `π − rotation`, so `π` renders as `0`. Exported
|
|
2938
|
+
* for reuse by measures that place a horizontal label outside the area's
|
|
2939
|
+
* boundary (e.g. Airfield Zone's Field H). */
|
|
2940
|
+
const HORIZONTAL_LABEL_ROTATION = Math.PI;
|
|
2941
|
+
/** Row pitch, as a ratio of the resolved label height, between stacked
|
|
2942
|
+
* name/H/W-W1 rows (see {@link pushAreaLabels}). */
|
|
2943
|
+
const ROW_PITCH_RATIO = 1.2;
|
|
2944
|
+
/** Clearance, as a ratio of the resolved label height, added on top of each
|
|
2945
|
+
* ENY marker's own half-width/half-height when carving its boundary
|
|
2946
|
+
* knockout gap (see {@link pushAreaLabels}). */
|
|
2947
|
+
const ENY_CLEARANCE_RATIO = .3;
|
|
2948
|
+
/** Approximate sans-serif character aspect ratio (width/height) used to
|
|
2949
|
+
* estimate an ENY marker's rendered text width from its character count,
|
|
2950
|
+
* for sizing the boundary knockout gap — no live text-measurement is
|
|
2951
|
+
* available at generator time. */
|
|
2952
|
+
const ENY_CHAR_ASPECT_RATIO = .6;
|
|
2953
|
+
const DEFAULT_SMOOTH_RESOLUTION$9 = 16;
|
|
2954
|
+
/**
|
|
2955
|
+
* Composes an area's main label text from a `prefix` (e.g. `"AO"`) and a
|
|
2956
|
+
* `designation` (e.g. `"GREEN"`), mirroring Phase Line's "PL" precedent: a
|
|
2957
|
+
* non-empty trimmed `designation` joins onto `prefix` with `separator`; an
|
|
2416
2958
|
* empty/absent `designation` yields the bare `prefix` (no bare fallback of
|
|
2417
2959
|
* its own); both empty yields `""`.
|
|
2418
2960
|
*/
|
|
@@ -2662,20 +3204,217 @@ const AREA_METADATA = {
|
|
|
2662
3204
|
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
2663
3205
|
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
2664
3206
|
}
|
|
2665
|
-
const AREA = defineControlMeasure({
|
|
2666
|
-
metadata: AREA_METADATA,
|
|
2667
|
-
generator: createArea,
|
|
2668
|
-
defaultOptions: DEFAULT_AREA_OPTIONS,
|
|
2669
|
-
rule: area1DrawRule,
|
|
3207
|
+
const AREA = defineControlMeasure({
|
|
3208
|
+
metadata: AREA_METADATA,
|
|
3209
|
+
generator: createArea,
|
|
3210
|
+
defaultOptions: DEFAULT_AREA_OPTIONS,
|
|
3211
|
+
rule: area1DrawRule,
|
|
3212
|
+
previewSample: {
|
|
3213
|
+
controlPoints: [
|
|
3214
|
+
[-1, -.5],
|
|
3215
|
+
[1, -.5],
|
|
3216
|
+
[1, .5],
|
|
3217
|
+
[-1, .5]
|
|
3218
|
+
],
|
|
3219
|
+
textAmplifiers: {},
|
|
3220
|
+
options: { labelSize: 96 }
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
//#endregion
|
|
3224
|
+
//#region src/generators/cm15-maneuver-areas/area-defense.ts
|
|
3225
|
+
const DEFAULT_AREA_DEFENSE_OPTIONS = {
|
|
3226
|
+
arrowOpeningAngle: 30,
|
|
3227
|
+
labelOpeningAngle: 20,
|
|
3228
|
+
barbCount: 6,
|
|
3229
|
+
barbLengthRatio: .16,
|
|
3230
|
+
arrowheadLengthRatio: .18
|
|
3231
|
+
};
|
|
3232
|
+
const AREA_DEFENSE_METADATA = {
|
|
3233
|
+
id: "area-defense",
|
|
3234
|
+
name: "Area Defense",
|
|
3235
|
+
description: "A defensive posture oriented in all directions and focused on retaining specified terrain.",
|
|
3236
|
+
entity: "Maneuver Areas",
|
|
3237
|
+
entityType: "Area Defense",
|
|
3238
|
+
value: "152600",
|
|
3239
|
+
minCoordinates: 2,
|
|
3240
|
+
maxCoordinates: 2,
|
|
3241
|
+
geometry: "area",
|
|
3242
|
+
geometryTypes: [
|
|
3243
|
+
"MultiLineString",
|
|
3244
|
+
"MultiPolygon",
|
|
3245
|
+
"Point"
|
|
3246
|
+
],
|
|
3247
|
+
drawRule: "Area15",
|
|
3248
|
+
capturesLabelSize: true,
|
|
3249
|
+
params: [
|
|
3250
|
+
{
|
|
3251
|
+
key: "arrowOpeningAngle",
|
|
3252
|
+
label: "Arrow opening",
|
|
3253
|
+
description: "Angular opening between the two arrow tips, in degrees",
|
|
3254
|
+
type: "number",
|
|
3255
|
+
min: 5,
|
|
3256
|
+
max: 120,
|
|
3257
|
+
step: 1
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
key: "labelOpeningAngle",
|
|
3261
|
+
label: "AD opening",
|
|
3262
|
+
description: "Angular opening occupied by the AD label, in degrees",
|
|
3263
|
+
type: "number",
|
|
3264
|
+
min: 5,
|
|
3265
|
+
max: 90,
|
|
3266
|
+
step: 1
|
|
3267
|
+
},
|
|
3268
|
+
{
|
|
3269
|
+
key: "barbCount",
|
|
3270
|
+
label: "Barbs",
|
|
3271
|
+
description: "Number of filled outward barbs distributed around the defense",
|
|
3272
|
+
type: "number",
|
|
3273
|
+
min: 2,
|
|
3274
|
+
max: 20,
|
|
3275
|
+
step: 1
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
key: "barbLengthRatio",
|
|
3279
|
+
label: "Barb length",
|
|
3280
|
+
description: "Barb height as a ratio of the symbol radius",
|
|
3281
|
+
type: "number",
|
|
3282
|
+
min: .02,
|
|
3283
|
+
max: .5,
|
|
3284
|
+
step: .01
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
key: "arrowheadLengthRatio",
|
|
3288
|
+
label: "Arrowhead length",
|
|
3289
|
+
description: "Open-arrowhead length as a ratio of the symbol radius",
|
|
3290
|
+
type: "number",
|
|
3291
|
+
min: .02,
|
|
3292
|
+
max: .5,
|
|
3293
|
+
step: .01
|
|
3294
|
+
}
|
|
3295
|
+
]
|
|
3296
|
+
};
|
|
3297
|
+
function pointOnCircle(center, radius, angle) {
|
|
3298
|
+
return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
|
|
3299
|
+
}
|
|
3300
|
+
function sampleArc(center, radius, start, end) {
|
|
3301
|
+
const span = end - start;
|
|
3302
|
+
const segments = Math.max(8, Math.round(48 * Math.abs(span) / Math.PI));
|
|
3303
|
+
return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle(center, radius, start + span * index / segments));
|
|
3304
|
+
}
|
|
3305
|
+
function openArrowhead(tip, direction, length) {
|
|
3306
|
+
const base = vecSub(tip, [direction[0] * length, direction[1] * length]);
|
|
3307
|
+
const perp = [-direction[1], direction[0]];
|
|
3308
|
+
return [
|
|
3309
|
+
vecAdd(base, [perp[0] * length * .55, perp[1] * length * .55]),
|
|
3310
|
+
tip,
|
|
3311
|
+
vecSub(base, [perp[0] * length * .55, perp[1] * length * .55])
|
|
3312
|
+
];
|
|
3313
|
+
}
|
|
3314
|
+
function createAreaDefense(coordinates, options = {}) {
|
|
3315
|
+
const resolved = {
|
|
3316
|
+
...DEFAULT_AREA_DEFENSE_OPTIONS,
|
|
3317
|
+
...options
|
|
3318
|
+
};
|
|
3319
|
+
const center = project(coordinates[0][0], coordinates[0][1]);
|
|
3320
|
+
const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
|
|
3321
|
+
const radius = vecMag(radiusVector);
|
|
3322
|
+
if (radius < 1e-6) return {
|
|
3323
|
+
type: "FeatureCollection",
|
|
3324
|
+
features: []
|
|
3325
|
+
};
|
|
3326
|
+
const upperTipAngle = Math.atan2(radiusVector[1], radiusVector[0]);
|
|
3327
|
+
const arrowHalf = Math.min(120, Math.max(5, resolved.arrowOpeningAngle)) * Math.PI / 360;
|
|
3328
|
+
const labelHalf = Math.min(90, Math.max(5, resolved.labelOpeningAngle)) * Math.PI / 360;
|
|
3329
|
+
const labelCenter = upperTipAngle + arrowHalf - Math.PI;
|
|
3330
|
+
const upperStart = labelCenter + labelHalf;
|
|
3331
|
+
const upperEnd = upperTipAngle;
|
|
3332
|
+
const lowerStart = labelCenter - labelHalf;
|
|
3333
|
+
const lowerEnd = upperTipAngle + arrowHalf * 2 - Math.PI * 2;
|
|
3334
|
+
const upperArc = sampleArc(center, radius, upperStart, upperEnd);
|
|
3335
|
+
const lowerArc = sampleArc(center, radius, lowerStart, lowerEnd);
|
|
3336
|
+
const arrowLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
|
|
3337
|
+
const upperDirection = [-Math.sin(upperEnd), Math.cos(upperEnd)];
|
|
3338
|
+
const lowerDirection = [Math.sin(lowerEnd), -Math.cos(lowerEnd)];
|
|
3339
|
+
const lineParts = [
|
|
3340
|
+
upperArc,
|
|
3341
|
+
lowerArc,
|
|
3342
|
+
openArrowhead(upperArc.at(-1), upperDirection, arrowLength),
|
|
3343
|
+
openArrowhead(lowerArc.at(-1), lowerDirection, arrowLength)
|
|
3344
|
+
];
|
|
3345
|
+
const barbCount = Math.max(2, Math.round(resolved.barbCount));
|
|
3346
|
+
const barbLength = radius * Math.max(0, resolved.barbLengthRatio);
|
|
3347
|
+
const barbHalfAngle = Math.asin(Math.min(1, barbLength / 2 / radius));
|
|
3348
|
+
const barbArcs = [{
|
|
3349
|
+
start: upperStart,
|
|
3350
|
+
end: upperEnd,
|
|
3351
|
+
count: Math.ceil(barbCount / 2)
|
|
3352
|
+
}, {
|
|
3353
|
+
start: lowerStart,
|
|
3354
|
+
end: lowerEnd,
|
|
3355
|
+
count: Math.floor(barbCount / 2)
|
|
3356
|
+
}];
|
|
3357
|
+
const barbPolygons = [];
|
|
3358
|
+
for (const arc of barbArcs) for (let ordinal = 1; ordinal <= arc.count; ordinal++) {
|
|
3359
|
+
const angle = arc.start + (arc.end - arc.start) * ordinal / (arc.count + 1);
|
|
3360
|
+
const left = pointOnCircle(center, radius, angle - barbHalfAngle);
|
|
3361
|
+
const right = pointOnCircle(center, radius, angle + barbHalfAngle);
|
|
3362
|
+
const ring = [
|
|
3363
|
+
left,
|
|
3364
|
+
pointOnCircle(center, radius + barbLength, angle),
|
|
3365
|
+
right,
|
|
3366
|
+
left
|
|
3367
|
+
].map((point) => unproject(point[0], point[1]));
|
|
3368
|
+
barbPolygons.push([ring]);
|
|
3369
|
+
}
|
|
3370
|
+
const labelPoint = pointOnCircle(center, radius, labelCenter);
|
|
3371
|
+
return {
|
|
3372
|
+
type: "FeatureCollection",
|
|
3373
|
+
features: [
|
|
3374
|
+
{
|
|
3375
|
+
type: "Feature",
|
|
3376
|
+
properties: { part: "area-defense" },
|
|
3377
|
+
geometry: {
|
|
3378
|
+
type: "MultiLineString",
|
|
3379
|
+
coordinates: lineParts.map((line) => line.map((point) => unproject(point[0], point[1])))
|
|
3380
|
+
}
|
|
3381
|
+
},
|
|
3382
|
+
{
|
|
3383
|
+
type: "Feature",
|
|
3384
|
+
properties: {
|
|
3385
|
+
part: "barbs",
|
|
3386
|
+
fill: true
|
|
3387
|
+
},
|
|
3388
|
+
geometry: {
|
|
3389
|
+
type: "MultiPolygon",
|
|
3390
|
+
coordinates: barbPolygons
|
|
3391
|
+
}
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
type: "Feature",
|
|
3395
|
+
properties: {
|
|
3396
|
+
part: "label",
|
|
3397
|
+
labelPlacement: false,
|
|
3398
|
+
text: "AD",
|
|
3399
|
+
rotation: Math.PI,
|
|
3400
|
+
...labelSizeProps(options)
|
|
3401
|
+
},
|
|
3402
|
+
geometry: {
|
|
3403
|
+
type: "Point",
|
|
3404
|
+
coordinates: unproject(labelPoint[0], labelPoint[1])
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
]
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
const AREA_DEFENSE = defineControlMeasure({
|
|
3411
|
+
metadata: AREA_DEFENSE_METADATA,
|
|
3412
|
+
generator: createAreaDefense,
|
|
3413
|
+
defaultOptions: DEFAULT_AREA_DEFENSE_OPTIONS,
|
|
3414
|
+
rule: centerRadiusDrawRule,
|
|
2670
3415
|
previewSample: {
|
|
2671
|
-
controlPoints: [
|
|
2672
|
-
|
|
2673
|
-
[1, -.5],
|
|
2674
|
-
[1, .5],
|
|
2675
|
-
[-1, .5]
|
|
2676
|
-
],
|
|
2677
|
-
textAmplifiers: {},
|
|
2678
|
-
options: { labelSize: 96 }
|
|
3416
|
+
controlPoints: [[0, 0], [-.77, .21]],
|
|
3417
|
+
options: { labelSize: 90 }
|
|
2679
3418
|
}
|
|
2680
3419
|
});
|
|
2681
3420
|
//#endregion
|
|
@@ -5068,7 +5807,7 @@ const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
|
|
|
5068
5807
|
const BLOCK_MISSION_TASK_METADATA = {
|
|
5069
5808
|
id: "block-mission-task",
|
|
5070
5809
|
name: "Block",
|
|
5071
|
-
description: "
|
|
5810
|
+
description: "Denies the enemy access to an area or prevents the enemy's advance in a direction or along an avenue of approach.",
|
|
5072
5811
|
entity: "Mission Tasks",
|
|
5073
5812
|
entityType: "Block",
|
|
5074
5813
|
value: "340100",
|
|
@@ -5160,7 +5899,7 @@ const DEFAULT_BREACH_OPTIONS = {
|
|
|
5160
5899
|
const BREACH_METADATA = {
|
|
5161
5900
|
id: "breach",
|
|
5162
5901
|
name: "Breach",
|
|
5163
|
-
description: "
|
|
5902
|
+
description: "Employs all available means to break through or secure a passage through an enemy defense, obstacle, minefield, or fortification.",
|
|
5164
5903
|
entity: "Mission Tasks",
|
|
5165
5904
|
entityType: "Breach",
|
|
5166
5905
|
value: "340200",
|
|
@@ -5256,7 +5995,7 @@ const DEFAULT_BYPASS_OPTIONS = {
|
|
|
5256
5995
|
const BYPASS_METADATA = {
|
|
5257
5996
|
id: "bypass",
|
|
5258
5997
|
name: "Bypass",
|
|
5259
|
-
description: "
|
|
5998
|
+
description: "Maneuvers around an obstacle, position, or enemy force to maintain the momentum of the operation.",
|
|
5260
5999
|
entity: "Mission Tasks",
|
|
5261
6000
|
entityType: "Bypass",
|
|
5262
6001
|
value: "340300",
|
|
@@ -5331,7 +6070,7 @@ const DEFAULT_CANALIZE_OPTIONS = {
|
|
|
5331
6070
|
const CANALIZE_METADATA = {
|
|
5332
6071
|
id: "canalize",
|
|
5333
6072
|
name: "Canalize",
|
|
5334
|
-
description: "
|
|
6073
|
+
description: "Restricts enemy movement to a narrow zone by exploiting terrain coupled with the use of obstacles, fires, or friendly maneuver.",
|
|
5335
6074
|
entity: "Mission Tasks",
|
|
5336
6075
|
entityType: "Canalize",
|
|
5337
6076
|
value: "340400",
|
|
@@ -5952,184 +6691,576 @@ function createGenericRectangle(coordinates, options = {}) {
|
|
|
5952
6691
|
p1
|
|
5953
6692
|
].map((point) => unproject(point[0], point[1]));
|
|
5954
6693
|
return {
|
|
5955
|
-
type: "FeatureCollection",
|
|
5956
|
-
features: [{
|
|
5957
|
-
type: "Feature",
|
|
5958
|
-
properties: {
|
|
5959
|
-
part: "rectangle",
|
|
5960
|
-
fill
|
|
5961
|
-
},
|
|
5962
|
-
geometry: {
|
|
5963
|
-
type: "Polygon",
|
|
5964
|
-
coordinates: [ring]
|
|
5965
|
-
}
|
|
5966
|
-
}]
|
|
6694
|
+
type: "FeatureCollection",
|
|
6695
|
+
features: [{
|
|
6696
|
+
type: "Feature",
|
|
6697
|
+
properties: {
|
|
6698
|
+
part: "rectangle",
|
|
6699
|
+
fill
|
|
6700
|
+
},
|
|
6701
|
+
geometry: {
|
|
6702
|
+
type: "Polygon",
|
|
6703
|
+
coordinates: [ring]
|
|
6704
|
+
}
|
|
6705
|
+
}]
|
|
6706
|
+
};
|
|
6707
|
+
}
|
|
6708
|
+
const GENERIC_RECTANGLE = defineControlMeasure({
|
|
6709
|
+
metadata: GENERIC_RECTANGLE_METADATA,
|
|
6710
|
+
generator: createGenericRectangle,
|
|
6711
|
+
defaultOptions: DEFAULT_GENERIC_RECTANGLE_OPTIONS,
|
|
6712
|
+
rule: rectangleDrawRule,
|
|
6713
|
+
previewSample: { controlPoints: [
|
|
6714
|
+
[-.9, -.55],
|
|
6715
|
+
[.9, -.55],
|
|
6716
|
+
[.9, .55]
|
|
6717
|
+
] }
|
|
6718
|
+
});
|
|
6719
|
+
//#endregion
|
|
6720
|
+
//#region src/generators/cm34-mission-tasks/clear.ts
|
|
6721
|
+
/**
|
|
6722
|
+
* Default options for the CLEAR symbol.
|
|
6723
|
+
*/
|
|
6724
|
+
const DEFAULT_CLEAR_OPTIONS = {
|
|
6725
|
+
arrowInsetRatio: .15,
|
|
6726
|
+
arrowheadLengthRatio: .15,
|
|
6727
|
+
arrowheadWidthRatio: .2,
|
|
6728
|
+
labelGapRatio: .2
|
|
6729
|
+
};
|
|
6730
|
+
const CLEAR_METADATA = {
|
|
6731
|
+
id: "clear",
|
|
6732
|
+
name: "Clear",
|
|
6733
|
+
description: "Removes all enemy forces and eliminates organized resistance within an assigned area.",
|
|
6734
|
+
entity: "Mission Tasks",
|
|
6735
|
+
entityType: "Clear",
|
|
6736
|
+
value: "340500",
|
|
6737
|
+
minCoordinates: 3,
|
|
6738
|
+
maxCoordinates: 3,
|
|
6739
|
+
geometry: "line",
|
|
6740
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
6741
|
+
drawRule: "Line23",
|
|
6742
|
+
capturesLabelSize: true,
|
|
6743
|
+
params: [
|
|
6744
|
+
{
|
|
6745
|
+
key: "arrowInsetRatio",
|
|
6746
|
+
label: "Arrow padding",
|
|
6747
|
+
description: "Padding between the line endpoints and the outer arrows, as a ratio of height",
|
|
6748
|
+
type: "number",
|
|
6749
|
+
min: 0,
|
|
6750
|
+
max: .45,
|
|
6751
|
+
step: .01
|
|
6752
|
+
},
|
|
6753
|
+
{
|
|
6754
|
+
key: "arrowheadLengthRatio",
|
|
6755
|
+
label: "Arrowhead length",
|
|
6756
|
+
description: "Arrowhead length as a ratio of the symbol height",
|
|
6757
|
+
type: "number",
|
|
6758
|
+
min: .01,
|
|
6759
|
+
max: 1,
|
|
6760
|
+
step: .01
|
|
6761
|
+
},
|
|
6762
|
+
{
|
|
6763
|
+
key: "arrowheadWidthRatio",
|
|
6764
|
+
label: "Arrowhead width",
|
|
6765
|
+
description: "Arrowhead base width as a ratio of the symbol height",
|
|
6766
|
+
type: "number",
|
|
6767
|
+
min: .01,
|
|
6768
|
+
max: 1,
|
|
6769
|
+
step: .01
|
|
6770
|
+
},
|
|
6771
|
+
{
|
|
6772
|
+
key: "labelGapRatio",
|
|
6773
|
+
label: "Label gap",
|
|
6774
|
+
description: "Size of the shaft gap holding the 'C' label, as a ratio of the shaft length",
|
|
6775
|
+
type: "number",
|
|
6776
|
+
min: 0,
|
|
6777
|
+
max: 1,
|
|
6778
|
+
step: .01
|
|
6779
|
+
}
|
|
6780
|
+
]
|
|
6781
|
+
};
|
|
6782
|
+
/**
|
|
6783
|
+
* Generates a GeoJSON FeatureCollection for a CLEAR mission task symbol (340500).
|
|
6784
|
+
*
|
|
6785
|
+
* A vertical line (PT. 1 → PT. 2) with three arrows pointing at it,
|
|
6786
|
+
* perpendicular to the line. The arrows come from the rear — the side PT. 3
|
|
6787
|
+
* sits on — and their tips touch the vertical line; each carries a shaft
|
|
6788
|
+
* reaching back to the rear. The arrows are evenly spaced along the line, so
|
|
6789
|
+
* the middle arrow's tip sits at the midpoint of the vertical line and its
|
|
6790
|
+
* shaft carries the 'C' glyph. The arrows stay perpendicular to the vertical
|
|
6791
|
+
* line at any rotation.
|
|
6792
|
+
*
|
|
6793
|
+
* Input Points:
|
|
6794
|
+
* - PT. 1: Top endpoint of the vertical line
|
|
6795
|
+
* - PT. 2: Bottom endpoint of the vertical line — defines height with PT. 1
|
|
6796
|
+
* - PT. 3: Rear point — defines the shaft length (and which side the arrows
|
|
6797
|
+
* come from)
|
|
6798
|
+
*
|
|
6799
|
+
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
6800
|
+
* @param options - Configuration options
|
|
6801
|
+
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
6802
|
+
*/
|
|
6803
|
+
function createClearSymbol(coordinates, options = {}) {
|
|
6804
|
+
const arrowInsetRatio = Math.min(.49, Math.max(0, options.arrowInsetRatio ?? DEFAULT_CLEAR_OPTIONS.arrowInsetRatio));
|
|
6805
|
+
const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadLengthRatio;
|
|
6806
|
+
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
|
|
6807
|
+
const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
|
|
6808
|
+
const [c1, c2, c3] = coordinates;
|
|
6809
|
+
const p1 = project(c1[0], c1[1]);
|
|
6810
|
+
const p2 = project(c2[0], c2[1]);
|
|
6811
|
+
const p3 = project(c3[0], c3[1]);
|
|
6812
|
+
const vLine = vecSub(p2, p1);
|
|
6813
|
+
const height = vecMag(vLine);
|
|
6814
|
+
if (height < 1e-6) return {
|
|
6815
|
+
type: "FeatureCollection",
|
|
6816
|
+
features: []
|
|
6817
|
+
};
|
|
6818
|
+
const dirLine = vecScale(vLine, 1 / height);
|
|
6819
|
+
const midLine = vecScale(vecAdd(p1, p2), .5);
|
|
6820
|
+
const rawPerp = [-dirLine[1], dirLine[0]];
|
|
6821
|
+
const perpToRear = vecScale(rawPerp, vecDot(vecSub(p3, midLine), rawPerp) < 0 ? -1 : 1);
|
|
6822
|
+
const shaftLength = Math.abs(vecDot(vecSub(p3, midLine), rawPerp));
|
|
6823
|
+
const arrowDir = vecScale(perpToRear, -1);
|
|
6824
|
+
const arrowheadLength = height * arrowheadLengthRatio;
|
|
6825
|
+
const arrowheadWidth = height * arrowheadWidthRatio;
|
|
6826
|
+
const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
|
|
6827
|
+
const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
|
|
6828
|
+
const fullTs = [
|
|
6829
|
+
arrowInsetRatio,
|
|
6830
|
+
.5,
|
|
6831
|
+
1 - arrowInsetRatio
|
|
6832
|
+
];
|
|
6833
|
+
const labelIndex = 1;
|
|
6834
|
+
fullTs.forEach((t, i) => {
|
|
6835
|
+
const tip = tipAt(t);
|
|
6836
|
+
const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
|
|
6837
|
+
if (i === labelIndex && shaftLength > 1e-6) {
|
|
6838
|
+
const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
|
|
6839
|
+
const gapHalf = shaftLength * labelGapRatio / 2;
|
|
6840
|
+
const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
|
|
6841
|
+
const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
|
|
6842
|
+
lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
|
|
6843
|
+
lines.push([unproject(gapTip[0], gapTip[1]), unproject(tip[0], tip[1])]);
|
|
6844
|
+
} else if (shaftLength > 1e-6) lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(tip[0], tip[1])]);
|
|
6845
|
+
lines.push(createArrowHead$1(tip, arrowDir, arrowheadLength, arrowheadWidth));
|
|
6846
|
+
});
|
|
6847
|
+
const mainFeature = {
|
|
6848
|
+
type: "Feature",
|
|
6849
|
+
properties: { part: "clear" },
|
|
6850
|
+
geometry: {
|
|
6851
|
+
type: "MultiLineString",
|
|
6852
|
+
coordinates: lines
|
|
6853
|
+
}
|
|
6854
|
+
};
|
|
6855
|
+
const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
|
|
6856
|
+
const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
|
|
6857
|
+
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
6858
|
+
const labelTip = tipAt(fullTs[labelIndex]);
|
|
6859
|
+
return {
|
|
6860
|
+
type: "FeatureCollection",
|
|
6861
|
+
features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
|
|
6862
|
+
labelSize: options.labelSize,
|
|
6863
|
+
labelSizePixels: options.labelSizePixels
|
|
6864
|
+
})]
|
|
6865
|
+
};
|
|
6866
|
+
}
|
|
6867
|
+
const CLEAR = defineControlMeasure({
|
|
6868
|
+
metadata: CLEAR_METADATA,
|
|
6869
|
+
generator: createClearSymbol,
|
|
6870
|
+
defaultOptions: DEFAULT_CLEAR_OPTIONS,
|
|
6871
|
+
rule: line23DrawRule
|
|
6872
|
+
});
|
|
6873
|
+
//#endregion
|
|
6874
|
+
//#region src/internal/field-of-fire.ts
|
|
6875
|
+
/**
|
|
6876
|
+
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
6877
|
+
* barbs are sized relative to that arm's own length, so each arm's head scales
|
|
6878
|
+
* independently (Line3 — arms vary independently). Returns `null` when the arm
|
|
6879
|
+
* has collapsed to zero length.
|
|
6880
|
+
*/
|
|
6881
|
+
function arrowheadAt(vertex, tip, hSize, hWidth) {
|
|
6882
|
+
const v = vecSub(tip, vertex);
|
|
6883
|
+
const armLength = vecMag(v);
|
|
6884
|
+
if (armLength < 1e-6) return null;
|
|
6885
|
+
const dir = vecNorm(v);
|
|
6886
|
+
const perp = [-dir[1], dir[0]];
|
|
6887
|
+
const headLength = armLength * hSize;
|
|
6888
|
+
const halfWidth = headLength * hWidth;
|
|
6889
|
+
const base = vecSub(tip, vecScale(dir, headLength));
|
|
6890
|
+
return [
|
|
6891
|
+
vecAdd(base, vecScale(perp, halfWidth)),
|
|
6892
|
+
tip,
|
|
6893
|
+
vecSub(base, vecScale(perp, halfWidth))
|
|
6894
|
+
];
|
|
6895
|
+
}
|
|
6896
|
+
/**
|
|
6897
|
+
* Projects the vertex (PT 1) and up to two tips and builds the V of arms, each
|
|
6898
|
+
* arm ending in an open arrowhead. Renders one arm per tip, so two points
|
|
6899
|
+
* (vertex + one tip) draws the single arm being placed — the live feedback
|
|
6900
|
+
* after PT 1 — and three points draws the full V. Points beyond the third are
|
|
6901
|
+
* ignored (input contract, ADR-0014).
|
|
6902
|
+
*
|
|
6903
|
+
* Returns `null` if any arm is degenerate (collapsed to zero length), the
|
|
6904
|
+
* silent ADR-0014 empty-render signal.
|
|
6905
|
+
*/
|
|
6906
|
+
function buildFieldOfFireV(coordinates, hSize, hWidth) {
|
|
6907
|
+
const vertexCoord = coordinates[0];
|
|
6908
|
+
const vertex = project(vertexCoord[0], vertexCoord[1]);
|
|
6909
|
+
const tips = coordinates.slice(1, 3).map((c) => project(c[0], c[1]));
|
|
6910
|
+
const lines = [];
|
|
6911
|
+
for (const tip of tips) {
|
|
6912
|
+
const head = arrowheadAt(vertex, tip, hSize, hWidth);
|
|
6913
|
+
if (!head) return null;
|
|
6914
|
+
lines.push([vertex, tip], head);
|
|
6915
|
+
}
|
|
6916
|
+
return {
|
|
6917
|
+
vertex,
|
|
6918
|
+
tips,
|
|
6919
|
+
lines
|
|
5967
6920
|
};
|
|
5968
6921
|
}
|
|
5969
|
-
const GENERIC_RECTANGLE = defineControlMeasure({
|
|
5970
|
-
metadata: GENERIC_RECTANGLE_METADATA,
|
|
5971
|
-
generator: createGenericRectangle,
|
|
5972
|
-
defaultOptions: DEFAULT_GENERIC_RECTANGLE_OPTIONS,
|
|
5973
|
-
rule: rectangleDrawRule,
|
|
5974
|
-
previewSample: { controlPoints: [
|
|
5975
|
-
[-.9, -.55],
|
|
5976
|
-
[.9, -.55],
|
|
5977
|
-
[.9, .55]
|
|
5978
|
-
] }
|
|
5979
|
-
});
|
|
5980
6922
|
//#endregion
|
|
5981
|
-
//#region src/generators/
|
|
6923
|
+
//#region src/generators/cm15-maneuver-areas/searchArea.ts
|
|
6924
|
+
const DEFAULT_ARROWHEAD_LENGTH_RATIO = .12;
|
|
6925
|
+
const DEFAULT_ARROWHEAD_WIDTH_RATIO = .5;
|
|
6926
|
+
const DEFAULT_ZIGZAG_START = .55;
|
|
6927
|
+
const DEFAULT_ZIGZAG_END = .45;
|
|
6928
|
+
const DEFAULT_ZIGZAG_AMPLITUDE_RATIO = .07;
|
|
5982
6929
|
/**
|
|
5983
|
-
* Default options for the
|
|
6930
|
+
* Default options for the tactical arrow.
|
|
5984
6931
|
*/
|
|
5985
|
-
const
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
6932
|
+
const DEFAULT_TACTICAL_ARROW_OPTIONS = {
|
|
6933
|
+
arrowheadLengthRatio: DEFAULT_ARROWHEAD_LENGTH_RATIO,
|
|
6934
|
+
arrowheadWidthRatio: DEFAULT_ARROWHEAD_WIDTH_RATIO,
|
|
6935
|
+
zigzagStart: DEFAULT_ZIGZAG_START,
|
|
6936
|
+
zigzagEnd: DEFAULT_ZIGZAG_END,
|
|
6937
|
+
zigzagAmplitudeRatio: DEFAULT_ZIGZAG_AMPLITUDE_RATIO
|
|
5990
6938
|
};
|
|
5991
|
-
const
|
|
5992
|
-
id: "
|
|
5993
|
-
name: "
|
|
5994
|
-
description: "
|
|
5995
|
-
entity: "
|
|
5996
|
-
entityType: "
|
|
5997
|
-
value: "
|
|
6939
|
+
const SEARCH_AREA_METADATA = {
|
|
6940
|
+
id: "search-area",
|
|
6941
|
+
name: "Search Area",
|
|
6942
|
+
description: "Search area graphic with paired lightning-style arrows.",
|
|
6943
|
+
entity: "Maneuver Areas",
|
|
6944
|
+
entityType: "Search Area",
|
|
6945
|
+
value: "152200",
|
|
5998
6946
|
minCoordinates: 3,
|
|
5999
6947
|
maxCoordinates: 3,
|
|
6000
|
-
geometry: "
|
|
6001
|
-
geometryTypes: ["MultiLineString", "
|
|
6002
|
-
drawRule: "
|
|
6003
|
-
capturesLabelSize: true,
|
|
6948
|
+
geometry: "area",
|
|
6949
|
+
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
6950
|
+
drawRule: "Area21",
|
|
6004
6951
|
params: [
|
|
6005
|
-
{
|
|
6006
|
-
key: "arrowInsetRatio",
|
|
6007
|
-
label: "Arrow padding",
|
|
6008
|
-
description: "Padding between the line endpoints and the outer arrows, as a ratio of height",
|
|
6009
|
-
type: "number",
|
|
6010
|
-
min: 0,
|
|
6011
|
-
max: .45,
|
|
6012
|
-
step: .01
|
|
6013
|
-
},
|
|
6014
6952
|
{
|
|
6015
6953
|
key: "arrowheadLengthRatio",
|
|
6016
6954
|
label: "Arrowhead length",
|
|
6017
|
-
description: "Arrowhead length as a ratio of the
|
|
6955
|
+
description: "Arrowhead length as a ratio of the total leg length",
|
|
6018
6956
|
type: "number",
|
|
6019
|
-
min:
|
|
6020
|
-
max:
|
|
6957
|
+
min: 0,
|
|
6958
|
+
max: .5,
|
|
6021
6959
|
step: .01
|
|
6022
6960
|
},
|
|
6023
6961
|
{
|
|
6024
6962
|
key: "arrowheadWidthRatio",
|
|
6025
6963
|
label: "Arrowhead width",
|
|
6026
|
-
description: "Arrowhead base width as a
|
|
6964
|
+
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
6027
6965
|
type: "number",
|
|
6028
|
-
min: .
|
|
6966
|
+
min: .05,
|
|
6967
|
+
max: 2,
|
|
6968
|
+
step: .1
|
|
6969
|
+
},
|
|
6970
|
+
{
|
|
6971
|
+
key: "zigzagStart",
|
|
6972
|
+
label: "Zigzag start",
|
|
6973
|
+
description: "Point along the leg where the zigzag begins (0-1).",
|
|
6974
|
+
type: "number",
|
|
6975
|
+
min: 0,
|
|
6029
6976
|
max: 1,
|
|
6030
|
-
step: .
|
|
6977
|
+
step: .05
|
|
6031
6978
|
},
|
|
6032
6979
|
{
|
|
6033
|
-
key: "
|
|
6034
|
-
label: "
|
|
6035
|
-
description: "
|
|
6980
|
+
key: "zigzagEnd",
|
|
6981
|
+
label: "Zigzag end",
|
|
6982
|
+
description: "Point along the leg where the zigzag returns (0-1).",
|
|
6036
6983
|
type: "number",
|
|
6037
6984
|
min: 0,
|
|
6038
6985
|
max: 1,
|
|
6986
|
+
step: .05
|
|
6987
|
+
},
|
|
6988
|
+
{
|
|
6989
|
+
key: "zigzagAmplitudeRatio",
|
|
6990
|
+
label: "Zigzag amplitude",
|
|
6991
|
+
description: "Perpendicular kink offset as a ratio of leg length.",
|
|
6992
|
+
type: "number",
|
|
6993
|
+
min: 0,
|
|
6994
|
+
max: .3,
|
|
6039
6995
|
step: .01
|
|
6040
6996
|
}
|
|
6041
6997
|
]
|
|
6042
6998
|
};
|
|
6043
6999
|
/**
|
|
6044
|
-
* Generates a GeoJSON FeatureCollection
|
|
6045
|
-
*
|
|
6046
|
-
* A vertical line (PT. 1 → PT. 2) with three arrows pointing at it,
|
|
6047
|
-
* perpendicular to the line. The arrows come from the rear — the side PT. 3
|
|
6048
|
-
* sits on — and their tips touch the vertical line; each carries a shaft
|
|
6049
|
-
* reaching back to the rear. The arrows are evenly spaced along the line, so
|
|
6050
|
-
* the middle arrow's tip sits at the midpoint of the vertical line and its
|
|
6051
|
-
* shaft carries the 'C' glyph. The arrows stay perpendicular to the vertical
|
|
6052
|
-
* line at any rotation.
|
|
6053
|
-
*
|
|
6054
|
-
* Input Points:
|
|
6055
|
-
* - PT. 1: Top endpoint of the vertical line
|
|
6056
|
-
* - PT. 2: Bottom endpoint of the vertical line — defines height with PT. 1
|
|
6057
|
-
* - PT. 3: Rear point — defines the shaft length (and which side the arrows
|
|
6058
|
-
* come from)
|
|
7000
|
+
* Generates a GeoJSON FeatureCollection of a tactical "lightning" arrow.
|
|
7001
|
+
* Handles internal projection to maintain correct perpendicularity.
|
|
6059
7002
|
*
|
|
6060
|
-
* @param coordinates - [
|
|
6061
|
-
* @param options -
|
|
6062
|
-
* @returns
|
|
7003
|
+
* @param coordinates - [Apex Point (PT1), Top Tail (PT2), Bottom Tail (PT3)]
|
|
7004
|
+
* @param options - Geometric configuration parameters
|
|
7005
|
+
* @returns A FeatureCollection containing a MultiLineString for bolts and a MultiPolygon for heads
|
|
7006
|
+
*/
|
|
7007
|
+
function createSearchArea(coordinates, options = {}) {
|
|
7008
|
+
const { arrowheadLengthRatio = DEFAULT_ARROWHEAD_LENGTH_RATIO, arrowheadWidthRatio = DEFAULT_ARROWHEAD_WIDTH_RATIO, zigzagStart = DEFAULT_ZIGZAG_START, zigzagEnd = DEFAULT_ZIGZAG_END, zigzagAmplitudeRatio = DEFAULT_ZIGZAG_AMPLITUDE_RATIO } = options;
|
|
7009
|
+
const safeArrowheadLengthRatio = Math.max(0, arrowheadLengthRatio);
|
|
7010
|
+
const safeArrowheadWidthRatio = Math.max(0, arrowheadWidthRatio);
|
|
7011
|
+
const safeZigzagStart = clamp01(zigzagStart);
|
|
7012
|
+
const safeZigzagEnd = clamp01(zigzagEnd);
|
|
7013
|
+
const safeZigzagAmplitudeRatio = Math.max(0, zigzagAmplitudeRatio);
|
|
7014
|
+
const [c0, c1, c2] = coordinates;
|
|
7015
|
+
const [p1, p2, p3] = [
|
|
7016
|
+
project(c0[0], c0[1]),
|
|
7017
|
+
project(c1[0], c1[1]),
|
|
7018
|
+
project(c2[0], c2[1])
|
|
7019
|
+
];
|
|
7020
|
+
const shaftSegments = [];
|
|
7021
|
+
const headPolygons = [];
|
|
7022
|
+
const addLeg = (origin, tip, opposite) => {
|
|
7023
|
+
const v = vecSub(tip, origin);
|
|
7024
|
+
const totalLen = vecMag(v);
|
|
7025
|
+
if (totalLen < 1e-6) return;
|
|
7026
|
+
const u = vecNorm(v);
|
|
7027
|
+
let n = [-u[1], u[0]];
|
|
7028
|
+
const toOpp = vecSub(opposite, origin);
|
|
7029
|
+
if (vecDot(n, toOpp) > 0) n = vecScale(n, -1);
|
|
7030
|
+
const offset = totalLen * safeZigzagAmplitudeRatio;
|
|
7031
|
+
const ptA = vecAdd(vecAdd(origin, vecScale(v, safeZigzagStart)), vecScale(n, offset));
|
|
7032
|
+
const ptB = vecAdd(vecAdd(origin, vecScale(v, safeZigzagEnd)), vecScale(n, -offset));
|
|
7033
|
+
const fv = vecSub(tip, ptB);
|
|
7034
|
+
const fLen = vecMag(fv);
|
|
7035
|
+
let fu = vecNorm(fv);
|
|
7036
|
+
if (fLen < 1e-6) fu = u;
|
|
7037
|
+
const headLen = totalLen * safeArrowheadLengthRatio;
|
|
7038
|
+
const pBase = vecSub(tip, vecScale(fu, headLen));
|
|
7039
|
+
const wingWidth = headLen * safeArrowheadWidthRatio;
|
|
7040
|
+
const perp = [-fu[1], fu[0]];
|
|
7041
|
+
const pWing1 = vecAdd(pBase, vecScale(perp, wingWidth));
|
|
7042
|
+
const pWing2 = vecSub(pBase, vecScale(perp, wingWidth));
|
|
7043
|
+
shaftSegments.push([
|
|
7044
|
+
origin,
|
|
7045
|
+
ptA,
|
|
7046
|
+
ptB,
|
|
7047
|
+
pBase
|
|
7048
|
+
].map((p) => unproject(p[0], p[1])));
|
|
7049
|
+
headPolygons.push([[
|
|
7050
|
+
tip,
|
|
7051
|
+
pWing1,
|
|
7052
|
+
pWing2,
|
|
7053
|
+
tip
|
|
7054
|
+
].map((p) => unproject(p[0], p[1]))]);
|
|
7055
|
+
};
|
|
7056
|
+
addLeg(p1, p2, p3);
|
|
7057
|
+
addLeg(p1, p3, p2);
|
|
7058
|
+
const features = [];
|
|
7059
|
+
if (shaftSegments.length > 0) features.push({
|
|
7060
|
+
type: "Feature",
|
|
7061
|
+
properties: { part: "shaft" },
|
|
7062
|
+
geometry: {
|
|
7063
|
+
type: "MultiLineString",
|
|
7064
|
+
coordinates: shaftSegments
|
|
7065
|
+
}
|
|
7066
|
+
});
|
|
7067
|
+
if (headPolygons.length > 0) features.push({
|
|
7068
|
+
type: "Feature",
|
|
7069
|
+
properties: {
|
|
7070
|
+
part: "head",
|
|
7071
|
+
fill: true
|
|
7072
|
+
},
|
|
7073
|
+
geometry: {
|
|
7074
|
+
type: "MultiPolygon",
|
|
7075
|
+
coordinates: headPolygons
|
|
7076
|
+
}
|
|
7077
|
+
});
|
|
7078
|
+
return {
|
|
7079
|
+
type: "FeatureCollection",
|
|
7080
|
+
features
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
7083
|
+
const SEARCH_AREA = defineControlMeasure({
|
|
7084
|
+
metadata: SEARCH_AREA_METADATA,
|
|
7085
|
+
generator: (controlPoints, options) => createSearchArea(asThreePoints(controlPoints), options),
|
|
7086
|
+
defaultOptions: DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
7087
|
+
rule: searchAreaDrawRule
|
|
7088
|
+
});
|
|
7089
|
+
//#endregion
|
|
7090
|
+
//#region src/generators/cm34-mission-tasks/security-task-shared.ts
|
|
7091
|
+
const DEFAULT_SECURITY_TASK_OPTIONS = {
|
|
7092
|
+
...DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
7093
|
+
labelInsetRatio: .2
|
|
7094
|
+
};
|
|
7095
|
+
const SECURITY_TASK_PARAMS = [
|
|
7096
|
+
{
|
|
7097
|
+
key: "arrowheadLengthRatio",
|
|
7098
|
+
label: "Arrowhead length",
|
|
7099
|
+
description: "Arrowhead length as a ratio of the total leg length",
|
|
7100
|
+
type: "number",
|
|
7101
|
+
min: 0,
|
|
7102
|
+
max: .5,
|
|
7103
|
+
step: .01
|
|
7104
|
+
},
|
|
7105
|
+
{
|
|
7106
|
+
key: "arrowheadWidthRatio",
|
|
7107
|
+
label: "Arrowhead width",
|
|
7108
|
+
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
7109
|
+
type: "number",
|
|
7110
|
+
min: .05,
|
|
7111
|
+
max: 2,
|
|
7112
|
+
step: .1
|
|
7113
|
+
},
|
|
7114
|
+
{
|
|
7115
|
+
key: "zigzagStart",
|
|
7116
|
+
label: "Zigzag start",
|
|
7117
|
+
description: "Point along the leg where the zigzag begins (0-1).",
|
|
7118
|
+
type: "number",
|
|
7119
|
+
min: 0,
|
|
7120
|
+
max: 1,
|
|
7121
|
+
step: .05
|
|
7122
|
+
},
|
|
7123
|
+
{
|
|
7124
|
+
key: "zigzagEnd",
|
|
7125
|
+
label: "Zigzag end",
|
|
7126
|
+
description: "Point along the leg where the zigzag returns (0-1).",
|
|
7127
|
+
type: "number",
|
|
7128
|
+
min: 0,
|
|
7129
|
+
max: 1,
|
|
7130
|
+
step: .05
|
|
7131
|
+
},
|
|
7132
|
+
{
|
|
7133
|
+
key: "zigzagAmplitudeRatio",
|
|
7134
|
+
label: "Zigzag amplitude",
|
|
7135
|
+
description: "Perpendicular kink offset as a ratio of leg length.",
|
|
7136
|
+
type: "number",
|
|
7137
|
+
min: 0,
|
|
7138
|
+
max: .3,
|
|
7139
|
+
step: .01
|
|
7140
|
+
},
|
|
7141
|
+
{
|
|
7142
|
+
key: "labelInsetRatio",
|
|
7143
|
+
label: "Letter position",
|
|
7144
|
+
description: "Inset of each task letter from PT. 2 or PT. 3 across the center opening",
|
|
7145
|
+
type: "number",
|
|
7146
|
+
min: .05,
|
|
7147
|
+
max: .45,
|
|
7148
|
+
step: .01
|
|
7149
|
+
}
|
|
7150
|
+
];
|
|
7151
|
+
/**
|
|
7152
|
+
* Shared Line26 geometry for the Cover, Guard, and Screen task family.
|
|
7153
|
+
* PT. 1/PT. 4 are the outward tips; PT. 2/PT. 3 are the inner shaft ends.
|
|
7154
|
+
* The opening between PT. 2 and PT. 3 is deliberately left clear for the
|
|
7155
|
+
* supported tactical symbol, which is explanatory rather than part of this
|
|
7156
|
+
* control measure. The task glyph is repeated on either side of that opening.
|
|
6063
7157
|
*/
|
|
6064
|
-
function
|
|
6065
|
-
const
|
|
6066
|
-
const
|
|
6067
|
-
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
|
|
6068
|
-
const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
|
|
6069
|
-
const [c1, c2, c3] = coordinates;
|
|
7158
|
+
function createSecurityTaskSymbol(coordinates, config, options = {}) {
|
|
7159
|
+
const { taskGlyph, part } = config;
|
|
7160
|
+
const [c1, c2, c3, c4] = coordinates;
|
|
6070
7161
|
const p1 = project(c1[0], c1[1]);
|
|
6071
7162
|
const p2 = project(c2[0], c2[1]);
|
|
6072
7163
|
const p3 = project(c3[0], c3[1]);
|
|
6073
|
-
const
|
|
6074
|
-
const
|
|
6075
|
-
|
|
7164
|
+
const p4 = project(c4[0], c4[1]);
|
|
7165
|
+
const resolved = {
|
|
7166
|
+
...DEFAULT_SECURITY_TASK_OPTIONS,
|
|
7167
|
+
...options
|
|
7168
|
+
};
|
|
7169
|
+
const arrowOptions = {
|
|
7170
|
+
arrowheadLengthRatio: Math.max(0, resolved.arrowheadLengthRatio),
|
|
7171
|
+
arrowheadWidthRatio: Math.max(0, resolved.arrowheadWidthRatio),
|
|
7172
|
+
zigzagStart: clamp(resolved.zigzagStart, 0, 1),
|
|
7173
|
+
zigzagEnd: clamp(resolved.zigzagEnd, 0, 1),
|
|
7174
|
+
zigzagAmplitudeRatio: Math.max(0, resolved.zigzagAmplitudeRatio)
|
|
7175
|
+
};
|
|
7176
|
+
const lines = [createTacticalArrow(p2, p1, p3, arrowOptions), createTacticalArrow(p3, p4, p2, arrowOptions)].flatMap((arrow) => arrow ?? []);
|
|
7177
|
+
if (lines.length === 0) return {
|
|
6076
7178
|
type: "FeatureCollection",
|
|
6077
7179
|
features: []
|
|
6078
7180
|
};
|
|
6079
|
-
const
|
|
6080
|
-
const midLine = vecScale(vecAdd(p1, p2), .5);
|
|
6081
|
-
const rawPerp = [-dirLine[1], dirLine[0]];
|
|
6082
|
-
const perpToRear = vecScale(rawPerp, vecDot(vecSub(p3, midLine), rawPerp) < 0 ? -1 : 1);
|
|
6083
|
-
const shaftLength = Math.abs(vecDot(vecSub(p3, midLine), rawPerp));
|
|
6084
|
-
const arrowDir = vecScale(perpToRear, -1);
|
|
6085
|
-
const arrowheadLength = height * arrowheadLengthRatio;
|
|
6086
|
-
const arrowheadWidth = height * arrowheadWidthRatio;
|
|
6087
|
-
const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
|
|
6088
|
-
const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
|
|
6089
|
-
const fullTs = [
|
|
6090
|
-
arrowInsetRatio,
|
|
6091
|
-
.5,
|
|
6092
|
-
1 - arrowInsetRatio
|
|
6093
|
-
];
|
|
6094
|
-
const labelIndex = 1;
|
|
6095
|
-
fullTs.forEach((t, i) => {
|
|
6096
|
-
const tip = tipAt(t);
|
|
6097
|
-
const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
|
|
6098
|
-
if (i === labelIndex && shaftLength > 1e-6) {
|
|
6099
|
-
const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
|
|
6100
|
-
const gapHalf = shaftLength * labelGapRatio / 2;
|
|
6101
|
-
const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
|
|
6102
|
-
const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
|
|
6103
|
-
lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
|
|
6104
|
-
lines.push([unproject(gapTip[0], gapTip[1]), unproject(tip[0], tip[1])]);
|
|
6105
|
-
} else if (shaftLength > 1e-6) lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(tip[0], tip[1])]);
|
|
6106
|
-
lines.push(createArrowHead$1(tip, arrowDir, arrowheadLength, arrowheadWidth));
|
|
6107
|
-
});
|
|
6108
|
-
const mainFeature = {
|
|
7181
|
+
const features = [{
|
|
6109
7182
|
type: "Feature",
|
|
6110
|
-
properties: { part
|
|
7183
|
+
properties: { part },
|
|
6111
7184
|
geometry: {
|
|
6112
7185
|
type: "MultiLineString",
|
|
6113
7186
|
coordinates: lines
|
|
6114
7187
|
}
|
|
6115
|
-
};
|
|
6116
|
-
const
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
|
|
7188
|
+
}];
|
|
7189
|
+
const gap = vecSub(p3, p2);
|
|
7190
|
+
if (vecMag(gap) >= 1e-6) {
|
|
7191
|
+
const gapDir = vecNorm(gap);
|
|
7192
|
+
const inset = clamp(resolved.labelInsetRatio, .05, .45);
|
|
7193
|
+
const rotation = labelRotationAlong(gapDir);
|
|
7194
|
+
const size = {
|
|
6123
7195
|
labelSize: options.labelSize,
|
|
6124
7196
|
labelSizePixels: options.labelSizePixels
|
|
6125
|
-
}
|
|
7197
|
+
};
|
|
7198
|
+
features.push(createLabelFeature(vecAdd(p2, vecScale(gap, inset)), taskGlyph, rotation, size), createLabelFeature(vecAdd(p3, vecScale(gap, -inset)), taskGlyph, rotation, size));
|
|
7199
|
+
}
|
|
7200
|
+
return {
|
|
7201
|
+
type: "FeatureCollection",
|
|
7202
|
+
features
|
|
6126
7203
|
};
|
|
6127
7204
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
7205
|
+
/** A Search Area-style lightning shaft with a Principal Direction of Fire open head. */
|
|
7206
|
+
function createTacticalArrow(origin, tip, opposite, options) {
|
|
7207
|
+
const leg = vecSub(tip, origin);
|
|
7208
|
+
const totalLegLength = vecMag(leg);
|
|
7209
|
+
if (totalLegLength < 1e-6) return void 0;
|
|
7210
|
+
const legDirection = vecNorm(leg);
|
|
7211
|
+
let normal = [-legDirection[1], legDirection[0]];
|
|
7212
|
+
if (vecDot(normal, vecSub(opposite, origin)) > 0) normal = vecScale(normal, -1);
|
|
7213
|
+
const amplitude = totalLegLength * options.zigzagAmplitudeRatio;
|
|
7214
|
+
const zigzagPointA = vecAdd(vecAdd(origin, vecScale(leg, options.zigzagStart)), vecScale(normal, amplitude));
|
|
7215
|
+
const zigzagPointB = vecAdd(vecAdd(origin, vecScale(leg, options.zigzagEnd)), vecScale(normal, -amplitude));
|
|
7216
|
+
const arrowhead = arrowheadAt(origin, tip, options.arrowheadLengthRatio, options.arrowheadWidthRatio);
|
|
7217
|
+
if (!arrowhead) return void 0;
|
|
7218
|
+
return [[
|
|
7219
|
+
origin,
|
|
7220
|
+
zigzagPointA,
|
|
7221
|
+
zigzagPointB,
|
|
7222
|
+
tip
|
|
7223
|
+
], arrowhead].map((line) => line.map((point) => unproject(point[0], point[1])));
|
|
7224
|
+
}
|
|
7225
|
+
//#endregion
|
|
7226
|
+
//#region src/generators/cm34-mission-tasks/cover.ts
|
|
7227
|
+
const DEFAULT_COVER_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
|
|
7228
|
+
const COVER_METADATA = {
|
|
7229
|
+
id: "cover",
|
|
7230
|
+
name: "Cover",
|
|
7231
|
+
description: "Protects the main body by fighting to gain time while observing and reporting information and preventing enemy ground observation of and direct fire against the main body; unlike a screen or guard, a covering force is tactically self-contained and operates independently of the main body.",
|
|
7232
|
+
entity: "Mission Tasks",
|
|
7233
|
+
entityType: "Security",
|
|
7234
|
+
entitySubtype: "Cover",
|
|
7235
|
+
value: "342201",
|
|
7236
|
+
minCoordinates: 4,
|
|
7237
|
+
maxCoordinates: 4,
|
|
7238
|
+
geometry: "line",
|
|
7239
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
7240
|
+
drawRule: "Line26",
|
|
7241
|
+
capturesLabelSize: true,
|
|
7242
|
+
params: SECURITY_TASK_PARAMS
|
|
7243
|
+
};
|
|
7244
|
+
function createCoverSymbol(coordinates, options = {}) {
|
|
7245
|
+
return createSecurityTaskSymbol(coordinates, {
|
|
7246
|
+
taskGlyph: "C",
|
|
7247
|
+
part: "cover"
|
|
7248
|
+
}, options);
|
|
7249
|
+
}
|
|
7250
|
+
const COVER = defineControlMeasure({
|
|
7251
|
+
metadata: COVER_METADATA,
|
|
7252
|
+
generator: createCoverSymbol,
|
|
7253
|
+
defaultOptions: DEFAULT_COVER_OPTIONS,
|
|
7254
|
+
rule: line26DrawRule,
|
|
7255
|
+
previewSample: {
|
|
7256
|
+
controlPoints: [
|
|
7257
|
+
[-1, 0],
|
|
7258
|
+
[-.3, 0],
|
|
7259
|
+
[.3, 0],
|
|
7260
|
+
[1, 0]
|
|
7261
|
+
],
|
|
7262
|
+
options: { labelSize: 90 }
|
|
7263
|
+
}
|
|
6133
7264
|
});
|
|
6134
7265
|
//#endregion
|
|
6135
7266
|
//#region src/generators/cm34-mission-tasks/delay.ts
|
|
@@ -6142,7 +7273,7 @@ const DEFAULT_DELAY_OPTIONS = {
|
|
|
6142
7273
|
const DELAY_METADATA = {
|
|
6143
7274
|
id: "delay",
|
|
6144
7275
|
name: "Delay",
|
|
6145
|
-
description: "
|
|
7276
|
+
description: "Trades space for time while under pressure, slowing the enemy's momentum and inflicting maximum damage without becoming decisively engaged.",
|
|
6146
7277
|
entity: "Mission Tasks",
|
|
6147
7278
|
entityType: "Delay",
|
|
6148
7279
|
value: "340800",
|
|
@@ -6257,6 +7388,46 @@ const DELAY = defineControlMeasure({
|
|
|
6257
7388
|
rule: line24DrawRule
|
|
6258
7389
|
});
|
|
6259
7390
|
//#endregion
|
|
7391
|
+
//#region src/generators/cm34-mission-tasks/guard.ts
|
|
7392
|
+
const DEFAULT_GUARD_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
|
|
7393
|
+
const GUARD_METADATA = {
|
|
7394
|
+
id: "guard",
|
|
7395
|
+
name: "Guard",
|
|
7396
|
+
description: "Protects the main body by fighting to gain time while observing and reporting information and preventing enemy ground observation of and direct fire against the main body; a guard force operates within supporting range of the main body and cannot operate independently of it.",
|
|
7397
|
+
entity: "Mission Tasks",
|
|
7398
|
+
entityType: "Security",
|
|
7399
|
+
entitySubtype: "Guard",
|
|
7400
|
+
value: "342202",
|
|
7401
|
+
minCoordinates: 4,
|
|
7402
|
+
maxCoordinates: 4,
|
|
7403
|
+
geometry: "line",
|
|
7404
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
7405
|
+
drawRule: "Line26",
|
|
7406
|
+
capturesLabelSize: true,
|
|
7407
|
+
params: SECURITY_TASK_PARAMS
|
|
7408
|
+
};
|
|
7409
|
+
function createGuardSymbol(coordinates, options = {}) {
|
|
7410
|
+
return createSecurityTaskSymbol(coordinates, {
|
|
7411
|
+
taskGlyph: "G",
|
|
7412
|
+
part: "guard"
|
|
7413
|
+
}, options);
|
|
7414
|
+
}
|
|
7415
|
+
const GUARD = defineControlMeasure({
|
|
7416
|
+
metadata: GUARD_METADATA,
|
|
7417
|
+
generator: createGuardSymbol,
|
|
7418
|
+
defaultOptions: DEFAULT_GUARD_OPTIONS,
|
|
7419
|
+
rule: line26DrawRule,
|
|
7420
|
+
previewSample: {
|
|
7421
|
+
controlPoints: [
|
|
7422
|
+
[-1, 0],
|
|
7423
|
+
[-.3, 0],
|
|
7424
|
+
[.3, 0],
|
|
7425
|
+
[1, 0]
|
|
7426
|
+
],
|
|
7427
|
+
options: { labelSize: 90 }
|
|
7428
|
+
}
|
|
7429
|
+
});
|
|
7430
|
+
//#endregion
|
|
6260
7431
|
//#region src/internal/arrowhead.ts
|
|
6261
7432
|
/**
|
|
6262
7433
|
* Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
|
|
@@ -6719,55 +7890,6 @@ const EXTRACTION_ZONE = defineControlMeasure({
|
|
|
6719
7890
|
}
|
|
6720
7891
|
});
|
|
6721
7892
|
//#endregion
|
|
6722
|
-
//#region src/internal/field-of-fire.ts
|
|
6723
|
-
/**
|
|
6724
|
-
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
6725
|
-
* barbs are sized relative to that arm's own length, so each arm's head scales
|
|
6726
|
-
* independently (Line3 — arms vary independently). Returns `null` when the arm
|
|
6727
|
-
* has collapsed to zero length.
|
|
6728
|
-
*/
|
|
6729
|
-
function arrowheadAt(vertex, tip, hSize, hWidth) {
|
|
6730
|
-
const v = vecSub(tip, vertex);
|
|
6731
|
-
const armLength = vecMag(v);
|
|
6732
|
-
if (armLength < 1e-6) return null;
|
|
6733
|
-
const dir = vecNorm(v);
|
|
6734
|
-
const perp = [-dir[1], dir[0]];
|
|
6735
|
-
const headLength = armLength * hSize;
|
|
6736
|
-
const halfWidth = headLength * hWidth;
|
|
6737
|
-
const base = vecSub(tip, vecScale(dir, headLength));
|
|
6738
|
-
return [
|
|
6739
|
-
vecAdd(base, vecScale(perp, halfWidth)),
|
|
6740
|
-
tip,
|
|
6741
|
-
vecSub(base, vecScale(perp, halfWidth))
|
|
6742
|
-
];
|
|
6743
|
-
}
|
|
6744
|
-
/**
|
|
6745
|
-
* Projects the vertex (PT 1) and up to two tips and builds the V of arms, each
|
|
6746
|
-
* arm ending in an open arrowhead. Renders one arm per tip, so two points
|
|
6747
|
-
* (vertex + one tip) draws the single arm being placed — the live feedback
|
|
6748
|
-
* after PT 1 — and three points draws the full V. Points beyond the third are
|
|
6749
|
-
* ignored (input contract, ADR-0014).
|
|
6750
|
-
*
|
|
6751
|
-
* Returns `null` if any arm is degenerate (collapsed to zero length), the
|
|
6752
|
-
* silent ADR-0014 empty-render signal.
|
|
6753
|
-
*/
|
|
6754
|
-
function buildFieldOfFireV(coordinates, hSize, hWidth) {
|
|
6755
|
-
const vertexCoord = coordinates[0];
|
|
6756
|
-
const vertex = project(vertexCoord[0], vertexCoord[1]);
|
|
6757
|
-
const tips = coordinates.slice(1, 3).map((c) => project(c[0], c[1]));
|
|
6758
|
-
const lines = [];
|
|
6759
|
-
for (const tip of tips) {
|
|
6760
|
-
const head = arrowheadAt(vertex, tip, hSize, hWidth);
|
|
6761
|
-
if (!head) return null;
|
|
6762
|
-
lines.push([vertex, tip], head);
|
|
6763
|
-
}
|
|
6764
|
-
return {
|
|
6765
|
-
vertex,
|
|
6766
|
-
tips,
|
|
6767
|
-
lines
|
|
6768
|
-
};
|
|
6769
|
-
}
|
|
6770
|
-
//#endregion
|
|
6771
7893
|
//#region src/generators/cm14-maneuver-lines/finalProtectiveFire.ts
|
|
6772
7894
|
const BLADE_LENGTH_FRACTION = .8;
|
|
6773
7895
|
const DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS = {
|
|
@@ -7681,41 +8803,235 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
7681
8803
|
if (vecMag(vecSub(firstPt, lastPt)) > 1e-6) boundaryPoints.push(firstPt);
|
|
7682
8804
|
else boundaryPoints[boundaryPoints.length - 1] = firstPt;
|
|
7683
8805
|
}
|
|
7684
|
-
const features = [{
|
|
7685
|
-
type: "Feature",
|
|
7686
|
-
properties: {},
|
|
7687
|
-
geometry: {
|
|
7688
|
-
type: "Polygon",
|
|
7689
|
-
coordinates: [boundaryPoints.map((p) => unproject(p[0], p[1]))]
|
|
7690
|
-
}
|
|
7691
|
-
}];
|
|
7692
|
-
const labelFeatures = [];
|
|
7693
|
-
pushAreaLabels(labelFeatures, boundaryInput, {
|
|
7694
|
-
name: textAmplifiers.T,
|
|
7695
|
-
additionalInformation: textAmplifiers.H,
|
|
7696
|
-
dtgStart: textAmplifiers.W,
|
|
7697
|
-
dtgEnd: textAmplifiers.W1
|
|
7698
|
-
}, options);
|
|
7699
|
-
features.push(...labelFeatures);
|
|
7700
|
-
return {
|
|
7701
|
-
type: "FeatureCollection",
|
|
7702
|
-
features
|
|
7703
|
-
};
|
|
8806
|
+
const features = [{
|
|
8807
|
+
type: "Feature",
|
|
8808
|
+
properties: {},
|
|
8809
|
+
geometry: {
|
|
8810
|
+
type: "Polygon",
|
|
8811
|
+
coordinates: [boundaryPoints.map((p) => unproject(p[0], p[1]))]
|
|
8812
|
+
}
|
|
8813
|
+
}];
|
|
8814
|
+
const labelFeatures = [];
|
|
8815
|
+
pushAreaLabels(labelFeatures, boundaryInput, {
|
|
8816
|
+
name: textAmplifiers.T,
|
|
8817
|
+
additionalInformation: textAmplifiers.H,
|
|
8818
|
+
dtgStart: textAmplifiers.W,
|
|
8819
|
+
dtgEnd: textAmplifiers.W1
|
|
8820
|
+
}, options);
|
|
8821
|
+
features.push(...labelFeatures);
|
|
8822
|
+
return {
|
|
8823
|
+
type: "FeatureCollection",
|
|
8824
|
+
features
|
|
8825
|
+
};
|
|
8826
|
+
}
|
|
8827
|
+
const FORTIFIED_AREA = defineControlMeasure({
|
|
8828
|
+
metadata: FORTIFIED_AREA_METADATA,
|
|
8829
|
+
generator: createFortifiedArea,
|
|
8830
|
+
defaultOptions: DEFAULT_FORTIFIED_AREA_OPTIONS,
|
|
8831
|
+
rule: area1DrawRule,
|
|
8832
|
+
previewSample: {
|
|
8833
|
+
controlPoints: [
|
|
8834
|
+
[-1, -.5],
|
|
8835
|
+
[1, -.5],
|
|
8836
|
+
[1, .5],
|
|
8837
|
+
[-1, .5]
|
|
8838
|
+
],
|
|
8839
|
+
textAmplifiers: { T: "TANGO" },
|
|
8840
|
+
options: { labelSize: 96 }
|
|
8841
|
+
}
|
|
8842
|
+
});
|
|
8843
|
+
//#endregion
|
|
8844
|
+
//#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
|
|
8845
|
+
const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
|
|
8846
|
+
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
8847
|
+
smooth: false,
|
|
8848
|
+
smoothResolution: 5,
|
|
8849
|
+
crossbarLengthRatio: 1.1,
|
|
8850
|
+
labelPosition: .25,
|
|
8851
|
+
dtgPosition: .62,
|
|
8852
|
+
labelPadding: 0
|
|
8853
|
+
};
|
|
8854
|
+
function midpoint(a, b) {
|
|
8855
|
+
return vecScale(vecAdd(a, b), .5);
|
|
8856
|
+
}
|
|
8857
|
+
function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
|
|
8858
|
+
const resolved = {
|
|
8859
|
+
...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
|
|
8860
|
+
...options
|
|
8861
|
+
};
|
|
8862
|
+
const { geometry } = processAttackGeometry(coordinates, resolved);
|
|
8863
|
+
if (!geometry) return {
|
|
8864
|
+
type: "FeatureCollection",
|
|
8865
|
+
features: []
|
|
8866
|
+
};
|
|
8867
|
+
const outline = [
|
|
8868
|
+
...geometry.shaftLeft,
|
|
8869
|
+
geometry.headRing[0],
|
|
8870
|
+
geometry.headRing[1],
|
|
8871
|
+
geometry.headRing[2],
|
|
8872
|
+
...[...geometry.shaftRight].reverse()
|
|
8873
|
+
];
|
|
8874
|
+
const centerline = geometry.shaftLeft.map((left, index) => midpoint(left, geometry.shaftRight[index]));
|
|
8875
|
+
const { segments, totalLength } = polylineSegments(centerline);
|
|
8876
|
+
if (segments.length === 0) return {
|
|
8877
|
+
type: "FeatureCollection",
|
|
8878
|
+
features: []
|
|
8879
|
+
};
|
|
8880
|
+
const tipVector = vecSub(geometry.ptTip, geometry.ptBase);
|
|
8881
|
+
const tipDirection = vecNorm(tipVector);
|
|
8882
|
+
const crossbarLength = vecMag(vecSub(geometry.headRing[0], geometry.headRing[2])) * Math.max(1, resolved.crossbarLengthRatio);
|
|
8883
|
+
let crossbarCenter;
|
|
8884
|
+
let crossbarAlong;
|
|
8885
|
+
if (config.crossbarAt === "tip") {
|
|
8886
|
+
crossbarCenter = geometry.ptTip;
|
|
8887
|
+
crossbarAlong = tipDirection;
|
|
8888
|
+
} else {
|
|
8889
|
+
crossbarCenter = centerline[0];
|
|
8890
|
+
crossbarAlong = segments[0].along;
|
|
8891
|
+
}
|
|
8892
|
+
const crossbarPerp = [-crossbarAlong[1], crossbarAlong[0]];
|
|
8893
|
+
const crossbar = [vecAdd(crossbarCenter, vecScale(crossbarPerp, crossbarLength / 2)), vecSub(crossbarCenter, vecScale(crossbarPerp, crossbarLength / 2))];
|
|
8894
|
+
const features = [{
|
|
8895
|
+
type: "Feature",
|
|
8896
|
+
properties: { part: config.part },
|
|
8897
|
+
geometry: {
|
|
8898
|
+
type: "MultiLineString",
|
|
8899
|
+
coordinates: [outline, crossbar].map((line) => line.map((point) => unproject(point[0], point[1])))
|
|
8900
|
+
}
|
|
8901
|
+
}];
|
|
8902
|
+
const sizeProps = labelSizeProps(resolved);
|
|
8903
|
+
const codePoint = vecAdd(geometry.ptBase, vecScale(tipVector, .55));
|
|
8904
|
+
const labels = [{
|
|
8905
|
+
type: "Feature",
|
|
8906
|
+
properties: {
|
|
8907
|
+
part: "label",
|
|
8908
|
+
labelPlacement: false,
|
|
8909
|
+
text: config.code,
|
|
8910
|
+
rotation: labelRotationAlong(tipDirection),
|
|
8911
|
+
...sizeProps
|
|
8912
|
+
},
|
|
8913
|
+
geometry: {
|
|
8914
|
+
type: "Point",
|
|
8915
|
+
coordinates: unproject(codePoint[0], codePoint[1])
|
|
8916
|
+
}
|
|
8917
|
+
}];
|
|
8918
|
+
const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
|
|
8919
|
+
if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
|
|
8920
|
+
const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
|
|
8921
|
+
if (dtgFrame) {
|
|
8922
|
+
const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
|
|
8923
|
+
const rotation = labelRotationAlong(dtgFrame.along);
|
|
8924
|
+
if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
|
|
8925
|
+
if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
|
|
8926
|
+
}
|
|
8927
|
+
features.push(...labels);
|
|
8928
|
+
return {
|
|
8929
|
+
type: "FeatureCollection",
|
|
8930
|
+
features
|
|
8931
|
+
};
|
|
8932
|
+
}
|
|
8933
|
+
const MANEUVER_ARROW_TASK_PARAMS = [
|
|
8934
|
+
{
|
|
8935
|
+
key: "crossbarLengthRatio",
|
|
8936
|
+
label: "Straight line length",
|
|
8937
|
+
description: "Crossbar length as a multiple of the arrowhead base width",
|
|
8938
|
+
type: "number",
|
|
8939
|
+
min: 1,
|
|
8940
|
+
max: 2.5,
|
|
8941
|
+
step: .1
|
|
8942
|
+
},
|
|
8943
|
+
{
|
|
8944
|
+
key: "labelPosition",
|
|
8945
|
+
label: "Designation position",
|
|
8946
|
+
description: "Position of Field T along the shaft, from tail (0) to head (1)",
|
|
8947
|
+
type: "number",
|
|
8948
|
+
min: 0,
|
|
8949
|
+
max: 1,
|
|
8950
|
+
step: .01
|
|
8951
|
+
},
|
|
8952
|
+
{
|
|
8953
|
+
key: "dtgPosition",
|
|
8954
|
+
label: "Date-time position",
|
|
8955
|
+
description: "Position of Fields W/W1 along the shaft, from tail (0) to head (1)",
|
|
8956
|
+
type: "number",
|
|
8957
|
+
min: 0,
|
|
8958
|
+
max: 1,
|
|
8959
|
+
step: .01
|
|
8960
|
+
},
|
|
8961
|
+
{
|
|
8962
|
+
key: "labelPadding",
|
|
8963
|
+
label: "Label padding",
|
|
8964
|
+
description: "Extra spacing between the W and W1 rows, in label heights",
|
|
8965
|
+
type: "number",
|
|
8966
|
+
min: 0,
|
|
8967
|
+
max: 2,
|
|
8968
|
+
step: .05
|
|
8969
|
+
}
|
|
8970
|
+
];
|
|
8971
|
+
const MANEUVER_ARROW_TASK_TEXT_AMPLIFIERS = [
|
|
8972
|
+
{
|
|
8973
|
+
key: "T",
|
|
8974
|
+
label: "Unique designation",
|
|
8975
|
+
description: "Field T — the maneuver's unique designation.",
|
|
8976
|
+
placeholder: "DAVID",
|
|
8977
|
+
maxLength: 21
|
|
8978
|
+
},
|
|
8979
|
+
{
|
|
8980
|
+
key: "W",
|
|
8981
|
+
label: "Start date-time group",
|
|
8982
|
+
description: "Field W — the maneuver's start date-time group.",
|
|
8983
|
+
placeholder: "080400ZOCT2008"
|
|
8984
|
+
},
|
|
8985
|
+
{
|
|
8986
|
+
key: "W1",
|
|
8987
|
+
label: "End date-time group",
|
|
8988
|
+
description: "Field W1 — the maneuver's end date-time group.",
|
|
8989
|
+
placeholder: "120300ZOCT2008"
|
|
8990
|
+
}
|
|
8991
|
+
];
|
|
8992
|
+
//#endregion
|
|
8993
|
+
//#region src/generators/cm15-maneuver-areas/frontal-attack.ts
|
|
8994
|
+
const DEFAULT_FRONTAL_ATTACK_OPTIONS = DEFAULT_MANEUVER_ARROW_TASK_OPTIONS;
|
|
8995
|
+
const FRONTAL_ATTACK_METADATA = {
|
|
8996
|
+
id: "frontal-attack",
|
|
8997
|
+
name: "Frontal Attack",
|
|
8998
|
+
description: "An offensive maneuver in which the main action is directed against the front.",
|
|
8999
|
+
entity: "Maneuver Areas",
|
|
9000
|
+
entityType: "Frontal Attack",
|
|
9001
|
+
value: "152700",
|
|
9002
|
+
minCoordinates: 3,
|
|
9003
|
+
geometry: "line",
|
|
9004
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
9005
|
+
drawRule: "Axis1",
|
|
9006
|
+
capturesLabelSize: true,
|
|
9007
|
+
params: [...ATTACK_SHAFT_PARAMS, ...MANEUVER_ARROW_TASK_PARAMS],
|
|
9008
|
+
textAmplifiers: MANEUVER_ARROW_TASK_TEXT_AMPLIFIERS
|
|
9009
|
+
};
|
|
9010
|
+
function createFrontalAttack(coordinates, options = {}, textAmplifiers = {}) {
|
|
9011
|
+
return createManeuverArrowTask(coordinates, options, textAmplifiers, {
|
|
9012
|
+
part: "frontal-attack",
|
|
9013
|
+
code: "A",
|
|
9014
|
+
crossbarAt: "tip"
|
|
9015
|
+
});
|
|
7704
9016
|
}
|
|
7705
|
-
const
|
|
7706
|
-
metadata:
|
|
7707
|
-
generator:
|
|
7708
|
-
defaultOptions:
|
|
7709
|
-
rule:
|
|
9017
|
+
const FRONTAL_ATTACK = defineControlMeasure({
|
|
9018
|
+
metadata: FRONTAL_ATTACK_METADATA,
|
|
9019
|
+
generator: createFrontalAttack,
|
|
9020
|
+
defaultOptions: DEFAULT_FRONTAL_ATTACK_OPTIONS,
|
|
9021
|
+
rule: axis1DrawRule,
|
|
7710
9022
|
previewSample: {
|
|
7711
9023
|
controlPoints: [
|
|
7712
|
-
[
|
|
7713
|
-
[
|
|
7714
|
-
[1,
|
|
7715
|
-
[
|
|
9024
|
+
[1, 0],
|
|
9025
|
+
[-.4, -.35],
|
|
9026
|
+
[-1, -.8],
|
|
9027
|
+
[.45, .55]
|
|
7716
9028
|
],
|
|
7717
|
-
textAmplifiers: {
|
|
7718
|
-
|
|
9029
|
+
textAmplifiers: {
|
|
9030
|
+
T: "FOX",
|
|
9031
|
+
W: "080400Z",
|
|
9032
|
+
W1: "120300Z"
|
|
9033
|
+
},
|
|
9034
|
+
options: { labelSize: 80 }
|
|
7719
9035
|
}
|
|
7720
9036
|
});
|
|
7721
9037
|
//#endregion
|
|
@@ -7731,7 +9047,7 @@ const DEFAULT_ISOLATE_OPTIONS = {
|
|
|
7731
9047
|
const ISOLATE_METADATA = {
|
|
7732
9048
|
id: "isolate",
|
|
7733
9049
|
name: "Isolate",
|
|
7734
|
-
description: "
|
|
9050
|
+
description: "Seals off an enemy, physically and psychologically, from sources of support, denies it freedom of movement, and prevents it from having contact with other enemy forces.",
|
|
7735
9051
|
entity: "Mission Tasks",
|
|
7736
9052
|
entityType: "Isolate",
|
|
7737
9053
|
value: "341500",
|
|
@@ -7849,6 +9165,46 @@ const ISOLATE = defineControlMeasure({
|
|
|
7849
9165
|
rule: isolateDrawRule
|
|
7850
9166
|
});
|
|
7851
9167
|
//#endregion
|
|
9168
|
+
//#region src/generators/cm34-mission-tasks/screen.ts
|
|
9169
|
+
const DEFAULT_SCREEN_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
|
|
9170
|
+
const SCREEN_METADATA = {
|
|
9171
|
+
id: "screen",
|
|
9172
|
+
name: "Screen",
|
|
9173
|
+
description: "Provides early warning to the protected force through observation, identification, reporting, and continuous surveillance of the enemy.",
|
|
9174
|
+
entity: "Mission Tasks",
|
|
9175
|
+
entityType: "Security",
|
|
9176
|
+
entitySubtype: "Screen",
|
|
9177
|
+
value: "342203",
|
|
9178
|
+
minCoordinates: 4,
|
|
9179
|
+
maxCoordinates: 4,
|
|
9180
|
+
geometry: "line",
|
|
9181
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
9182
|
+
drawRule: "Line26",
|
|
9183
|
+
capturesLabelSize: true,
|
|
9184
|
+
params: SECURITY_TASK_PARAMS
|
|
9185
|
+
};
|
|
9186
|
+
function createScreenSymbol(coordinates, options = {}) {
|
|
9187
|
+
return createSecurityTaskSymbol(coordinates, {
|
|
9188
|
+
taskGlyph: "S",
|
|
9189
|
+
part: "screen"
|
|
9190
|
+
}, options);
|
|
9191
|
+
}
|
|
9192
|
+
const SCREEN = defineControlMeasure({
|
|
9193
|
+
metadata: SCREEN_METADATA,
|
|
9194
|
+
generator: createScreenSymbol,
|
|
9195
|
+
defaultOptions: DEFAULT_SCREEN_OPTIONS,
|
|
9196
|
+
rule: line26DrawRule,
|
|
9197
|
+
previewSample: {
|
|
9198
|
+
controlPoints: [
|
|
9199
|
+
[-1, 0],
|
|
9200
|
+
[-.3, 0],
|
|
9201
|
+
[.3, 0],
|
|
9202
|
+
[1, 0]
|
|
9203
|
+
],
|
|
9204
|
+
options: { labelSize: 90 }
|
|
9205
|
+
}
|
|
9206
|
+
});
|
|
9207
|
+
//#endregion
|
|
7852
9208
|
//#region src/generators/cm15-maneuver-areas/jointTacticalActionArea.ts
|
|
7853
9209
|
/** Default options for the Joint tactical action area control measure. */
|
|
7854
9210
|
const DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS = {
|
|
@@ -8695,173 +10051,6 @@ const PRINCIPAL_DIRECTION_OF_FIRE = defineControlMeasure({
|
|
|
8695
10051
|
] }
|
|
8696
10052
|
});
|
|
8697
10053
|
//#endregion
|
|
8698
|
-
//#region src/generators/cm15-maneuver-areas/searchArea.ts
|
|
8699
|
-
const DEFAULT_ARROWHEAD_LENGTH_RATIO = .12;
|
|
8700
|
-
const DEFAULT_ARROWHEAD_WIDTH_RATIO = .5;
|
|
8701
|
-
const DEFAULT_ZIGZAG_START = .55;
|
|
8702
|
-
const DEFAULT_ZIGZAG_END = .45;
|
|
8703
|
-
const DEFAULT_ZIGZAG_AMPLITUDE_RATIO = .07;
|
|
8704
|
-
/**
|
|
8705
|
-
* Default options for the tactical arrow.
|
|
8706
|
-
*/
|
|
8707
|
-
const DEFAULT_TACTICAL_ARROW_OPTIONS = {
|
|
8708
|
-
arrowheadLengthRatio: DEFAULT_ARROWHEAD_LENGTH_RATIO,
|
|
8709
|
-
arrowheadWidthRatio: DEFAULT_ARROWHEAD_WIDTH_RATIO,
|
|
8710
|
-
zigzagStart: DEFAULT_ZIGZAG_START,
|
|
8711
|
-
zigzagEnd: DEFAULT_ZIGZAG_END,
|
|
8712
|
-
zigzagAmplitudeRatio: DEFAULT_ZIGZAG_AMPLITUDE_RATIO
|
|
8713
|
-
};
|
|
8714
|
-
const SEARCH_AREA_METADATA = {
|
|
8715
|
-
id: "search-area",
|
|
8716
|
-
name: "Search Area",
|
|
8717
|
-
description: "Search area graphic with paired lightning-style arrows.",
|
|
8718
|
-
entity: "Maneuver Areas",
|
|
8719
|
-
entityType: "Search Area",
|
|
8720
|
-
value: "152200",
|
|
8721
|
-
minCoordinates: 3,
|
|
8722
|
-
maxCoordinates: 3,
|
|
8723
|
-
geometry: "area",
|
|
8724
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
8725
|
-
drawRule: "Area21",
|
|
8726
|
-
params: [
|
|
8727
|
-
{
|
|
8728
|
-
key: "arrowheadLengthRatio",
|
|
8729
|
-
label: "Arrowhead length",
|
|
8730
|
-
description: "Arrowhead length as a ratio of the total leg length",
|
|
8731
|
-
type: "number",
|
|
8732
|
-
min: 0,
|
|
8733
|
-
max: .5,
|
|
8734
|
-
step: .01
|
|
8735
|
-
},
|
|
8736
|
-
{
|
|
8737
|
-
key: "arrowheadWidthRatio",
|
|
8738
|
-
label: "Arrowhead width",
|
|
8739
|
-
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
8740
|
-
type: "number",
|
|
8741
|
-
min: .05,
|
|
8742
|
-
max: 2,
|
|
8743
|
-
step: .1
|
|
8744
|
-
},
|
|
8745
|
-
{
|
|
8746
|
-
key: "zigzagStart",
|
|
8747
|
-
label: "Zigzag start",
|
|
8748
|
-
description: "Point along the leg where the zigzag begins (0-1).",
|
|
8749
|
-
type: "number",
|
|
8750
|
-
min: 0,
|
|
8751
|
-
max: 1,
|
|
8752
|
-
step: .05
|
|
8753
|
-
},
|
|
8754
|
-
{
|
|
8755
|
-
key: "zigzagEnd",
|
|
8756
|
-
label: "Zigzag end",
|
|
8757
|
-
description: "Point along the leg where the zigzag returns (0-1).",
|
|
8758
|
-
type: "number",
|
|
8759
|
-
min: 0,
|
|
8760
|
-
max: 1,
|
|
8761
|
-
step: .05
|
|
8762
|
-
},
|
|
8763
|
-
{
|
|
8764
|
-
key: "zigzagAmplitudeRatio",
|
|
8765
|
-
label: "Zigzag amplitude",
|
|
8766
|
-
description: "Perpendicular kink offset as a ratio of leg length.",
|
|
8767
|
-
type: "number",
|
|
8768
|
-
min: 0,
|
|
8769
|
-
max: .3,
|
|
8770
|
-
step: .01
|
|
8771
|
-
}
|
|
8772
|
-
]
|
|
8773
|
-
};
|
|
8774
|
-
/**
|
|
8775
|
-
* Generates a GeoJSON FeatureCollection of a tactical "lightning" arrow.
|
|
8776
|
-
* Handles internal projection to maintain correct perpendicularity.
|
|
8777
|
-
*
|
|
8778
|
-
* @param coordinates - [Apex Point (PT1), Top Tail (PT2), Bottom Tail (PT3)]
|
|
8779
|
-
* @param options - Geometric configuration parameters
|
|
8780
|
-
* @returns A FeatureCollection containing a MultiLineString for bolts and a MultiPolygon for heads
|
|
8781
|
-
*/
|
|
8782
|
-
function createSearchArea(coordinates, options = {}) {
|
|
8783
|
-
const { arrowheadLengthRatio = DEFAULT_ARROWHEAD_LENGTH_RATIO, arrowheadWidthRatio = DEFAULT_ARROWHEAD_WIDTH_RATIO, zigzagStart = DEFAULT_ZIGZAG_START, zigzagEnd = DEFAULT_ZIGZAG_END, zigzagAmplitudeRatio = DEFAULT_ZIGZAG_AMPLITUDE_RATIO } = options;
|
|
8784
|
-
const safeArrowheadLengthRatio = Math.max(0, arrowheadLengthRatio);
|
|
8785
|
-
const safeArrowheadWidthRatio = Math.max(0, arrowheadWidthRatio);
|
|
8786
|
-
const safeZigzagStart = clamp01(zigzagStart);
|
|
8787
|
-
const safeZigzagEnd = clamp01(zigzagEnd);
|
|
8788
|
-
const safeZigzagAmplitudeRatio = Math.max(0, zigzagAmplitudeRatio);
|
|
8789
|
-
const [c0, c1, c2] = coordinates;
|
|
8790
|
-
const [p1, p2, p3] = [
|
|
8791
|
-
project(c0[0], c0[1]),
|
|
8792
|
-
project(c1[0], c1[1]),
|
|
8793
|
-
project(c2[0], c2[1])
|
|
8794
|
-
];
|
|
8795
|
-
const shaftSegments = [];
|
|
8796
|
-
const headPolygons = [];
|
|
8797
|
-
const addLeg = (origin, tip, opposite) => {
|
|
8798
|
-
const v = vecSub(tip, origin);
|
|
8799
|
-
const totalLen = vecMag(v);
|
|
8800
|
-
if (totalLen < 1e-6) return;
|
|
8801
|
-
const u = vecNorm(v);
|
|
8802
|
-
let n = [-u[1], u[0]];
|
|
8803
|
-
const toOpp = vecSub(opposite, origin);
|
|
8804
|
-
if (vecDot(n, toOpp) > 0) n = vecScale(n, -1);
|
|
8805
|
-
const offset = totalLen * safeZigzagAmplitudeRatio;
|
|
8806
|
-
const ptA = vecAdd(vecAdd(origin, vecScale(v, safeZigzagStart)), vecScale(n, offset));
|
|
8807
|
-
const ptB = vecAdd(vecAdd(origin, vecScale(v, safeZigzagEnd)), vecScale(n, -offset));
|
|
8808
|
-
const fv = vecSub(tip, ptB);
|
|
8809
|
-
const fLen = vecMag(fv);
|
|
8810
|
-
let fu = vecNorm(fv);
|
|
8811
|
-
if (fLen < 1e-6) fu = u;
|
|
8812
|
-
const headLen = totalLen * safeArrowheadLengthRatio;
|
|
8813
|
-
const pBase = vecSub(tip, vecScale(fu, headLen));
|
|
8814
|
-
const wingWidth = headLen * safeArrowheadWidthRatio;
|
|
8815
|
-
const perp = [-fu[1], fu[0]];
|
|
8816
|
-
const pWing1 = vecAdd(pBase, vecScale(perp, wingWidth));
|
|
8817
|
-
const pWing2 = vecSub(pBase, vecScale(perp, wingWidth));
|
|
8818
|
-
shaftSegments.push([
|
|
8819
|
-
origin,
|
|
8820
|
-
ptA,
|
|
8821
|
-
ptB,
|
|
8822
|
-
pBase
|
|
8823
|
-
].map((p) => unproject(p[0], p[1])));
|
|
8824
|
-
headPolygons.push([[
|
|
8825
|
-
tip,
|
|
8826
|
-
pWing1,
|
|
8827
|
-
pWing2,
|
|
8828
|
-
tip
|
|
8829
|
-
].map((p) => unproject(p[0], p[1]))]);
|
|
8830
|
-
};
|
|
8831
|
-
addLeg(p1, p2, p3);
|
|
8832
|
-
addLeg(p1, p3, p2);
|
|
8833
|
-
const features = [];
|
|
8834
|
-
if (shaftSegments.length > 0) features.push({
|
|
8835
|
-
type: "Feature",
|
|
8836
|
-
properties: { part: "shaft" },
|
|
8837
|
-
geometry: {
|
|
8838
|
-
type: "MultiLineString",
|
|
8839
|
-
coordinates: shaftSegments
|
|
8840
|
-
}
|
|
8841
|
-
});
|
|
8842
|
-
if (headPolygons.length > 0) features.push({
|
|
8843
|
-
type: "Feature",
|
|
8844
|
-
properties: {
|
|
8845
|
-
part: "head",
|
|
8846
|
-
fill: true
|
|
8847
|
-
},
|
|
8848
|
-
geometry: {
|
|
8849
|
-
type: "MultiPolygon",
|
|
8850
|
-
coordinates: headPolygons
|
|
8851
|
-
}
|
|
8852
|
-
});
|
|
8853
|
-
return {
|
|
8854
|
-
type: "FeatureCollection",
|
|
8855
|
-
features
|
|
8856
|
-
};
|
|
8857
|
-
}
|
|
8858
|
-
const SEARCH_AREA = defineControlMeasure({
|
|
8859
|
-
metadata: SEARCH_AREA_METADATA,
|
|
8860
|
-
generator: (controlPoints, options) => createSearchArea(asThreePoints(controlPoints), options),
|
|
8861
|
-
defaultOptions: DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
8862
|
-
rule: searchAreaDrawRule
|
|
8863
|
-
});
|
|
8864
|
-
//#endregion
|
|
8865
10054
|
//#region src/generators/cm15-maneuver-areas/strongPoint.ts
|
|
8866
10055
|
const DEFAULT_SMOOTH_RESOLUTION = 16;
|
|
8867
10056
|
/** Default options for the Strong Point control measure. */
|
|
@@ -9421,6 +10610,37 @@ function rotate(vector, angle) {
|
|
|
9421
10610
|
const sin = Math.sin(angle);
|
|
9422
10611
|
return [vector[0] * cos - vector[1] * sin, vector[0] * sin + vector[1] * cos];
|
|
9423
10612
|
}
|
|
10613
|
+
const TURN = defineControlMeasure({
|
|
10614
|
+
metadata: TURN_METADATA,
|
|
10615
|
+
generator: createTurn,
|
|
10616
|
+
defaultOptions: DEFAULT_TURN_OPTIONS,
|
|
10617
|
+
rule: turnDrawRule
|
|
10618
|
+
});
|
|
10619
|
+
//#endregion
|
|
10620
|
+
//#region src/generators/cm15-maneuver-areas/turning-movement.ts
|
|
10621
|
+
const DEFAULT_TURNING_MOVEMENT_OPTIONS = DEFAULT_MANEUVER_ARROW_TASK_OPTIONS;
|
|
10622
|
+
const TURNING_MOVEMENT_METADATA = {
|
|
10623
|
+
id: "turning-movement",
|
|
10624
|
+
name: "Turning Movement",
|
|
10625
|
+
description: "A maneuver that passes around or over an enemy's principal defensive positions to secure objectives to the enemy's rear.",
|
|
10626
|
+
entity: "Maneuver Areas",
|
|
10627
|
+
entityType: "Turning Movement",
|
|
10628
|
+
value: "152900",
|
|
10629
|
+
minCoordinates: 3,
|
|
10630
|
+
geometry: "line",
|
|
10631
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
10632
|
+
drawRule: "Axis1",
|
|
10633
|
+
capturesLabelSize: true,
|
|
10634
|
+
params: [...ATTACK_SHAFT_PARAMS, ...MANEUVER_ARROW_TASK_PARAMS],
|
|
10635
|
+
textAmplifiers: MANEUVER_ARROW_TASK_TEXT_AMPLIFIERS
|
|
10636
|
+
};
|
|
10637
|
+
function createTurningMovement(coordinates, options = {}, textAmplifiers = {}) {
|
|
10638
|
+
return createManeuverArrowTask(coordinates, options, textAmplifiers, {
|
|
10639
|
+
part: "turning-movement",
|
|
10640
|
+
code: "T",
|
|
10641
|
+
crossbarAt: "tail"
|
|
10642
|
+
});
|
|
10643
|
+
}
|
|
9424
10644
|
//#endregion
|
|
9425
10645
|
//#region src/registry.ts
|
|
9426
10646
|
/**
|
|
@@ -9439,12 +10659,16 @@ const DEFINITIONS = {
|
|
|
9439
10659
|
"battle-position": BATTLE_POSITION,
|
|
9440
10660
|
"strong-point": STRONG_POINT,
|
|
9441
10661
|
ambush: AMBUSH,
|
|
10662
|
+
"direction-of-attack-aviation": DIRECTION_OF_ATTACK_AVIATION,
|
|
10663
|
+
"direction-of-main-attack": DIRECTION_OF_MAIN_ATTACK,
|
|
10664
|
+
"direction-of-supporting-attack": DIRECTION_OF_SUPPORTING_ATTACK,
|
|
9442
10665
|
"principal-direction-of-fire": PRINCIPAL_DIRECTION_OF_FIRE,
|
|
9443
10666
|
"final-protective-fire-left": FINAL_PROTECTIVE_FIRE_LEFT,
|
|
9444
10667
|
"final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
|
|
9445
10668
|
"search-area": SEARCH_AREA,
|
|
9446
10669
|
encirclement: ENCIRCLEMENT,
|
|
9447
10670
|
area: AREA,
|
|
10671
|
+
"area-defense": AREA_DEFENSE,
|
|
9448
10672
|
"assembly-area": ASSEMBLY_AREA,
|
|
9449
10673
|
"joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
|
|
9450
10674
|
"submarine-action-area": SUBMARINE_ACTION_AREA,
|
|
@@ -9484,22 +10708,41 @@ const DEFINITIONS = {
|
|
|
9484
10708
|
bypass: BYPASS,
|
|
9485
10709
|
canalize: CANALIZE,
|
|
9486
10710
|
clear: CLEAR,
|
|
10711
|
+
cover: COVER,
|
|
9487
10712
|
delay: DELAY,
|
|
10713
|
+
guard: GUARD,
|
|
9488
10714
|
isolate: ISOLATE,
|
|
10715
|
+
screen: SCREEN,
|
|
9489
10716
|
"antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
|
|
9490
10717
|
"antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
|
|
9491
10718
|
"antitank-wall": ANTITANK_WALL,
|
|
9492
10719
|
"fortified-line": FORTIFIED_LINE,
|
|
9493
10720
|
"fortified-area": FORTIFIED_AREA,
|
|
10721
|
+
"frontal-attack": FRONTAL_ATTACK,
|
|
10722
|
+
"turning-movement": defineControlMeasure({
|
|
10723
|
+
metadata: TURNING_MOVEMENT_METADATA,
|
|
10724
|
+
generator: createTurningMovement,
|
|
10725
|
+
defaultOptions: DEFAULT_TURNING_MOVEMENT_OPTIONS,
|
|
10726
|
+
rule: axis1DrawRule,
|
|
10727
|
+
previewSample: {
|
|
10728
|
+
controlPoints: [
|
|
10729
|
+
[1, 0],
|
|
10730
|
+
[-.4, -.35],
|
|
10731
|
+
[-1, -.8],
|
|
10732
|
+
[.45, .55]
|
|
10733
|
+
],
|
|
10734
|
+
textAmplifiers: {
|
|
10735
|
+
T: "DAVID",
|
|
10736
|
+
W: "080400Z",
|
|
10737
|
+
W1: "120300Z"
|
|
10738
|
+
},
|
|
10739
|
+
options: { labelSize: 80 }
|
|
10740
|
+
}
|
|
10741
|
+
}),
|
|
9494
10742
|
block: BLOCK,
|
|
9495
10743
|
disrupt: DISRUPT,
|
|
9496
10744
|
fix: FIX,
|
|
9497
|
-
turn:
|
|
9498
|
-
metadata: TURN_METADATA,
|
|
9499
|
-
generator: createTurn,
|
|
9500
|
-
defaultOptions: DEFAULT_TURN_OPTIONS,
|
|
9501
|
-
rule: turnDrawRule
|
|
9502
|
-
}),
|
|
10745
|
+
turn: TURN,
|
|
9503
10746
|
"obstacle-bypass-easy": OBSTACLE_BYPASS_EASY,
|
|
9504
10747
|
"obstacle-bypass-difficult": OBSTACLE_BYPASS_DIFFICULT,
|
|
9505
10748
|
"obstacle-bypass-impossible": OBSTACLE_BYPASS_IMPOSSIBLE
|
|
@@ -9675,6 +10918,20 @@ const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code)
|
|
|
9675
10918
|
`${code}1`,
|
|
9676
10919
|
`${code}2`
|
|
9677
10920
|
]));
|
|
10921
|
+
/**
|
|
10922
|
+
* Resolves a raw placement entry to its object form: a bare `Position` is
|
|
10923
|
+
* legacy shorthand for `{ position }`; an object form passes through only the
|
|
10924
|
+
* fields that are actually valid (`position` must be a finite `[lon, lat]`,
|
|
10925
|
+
* `rotation` a finite number); `undefined` resolves to `{}`.
|
|
10926
|
+
*/
|
|
10927
|
+
function resolveAmplifierPlacement(value) {
|
|
10928
|
+
if (value === void 0) return {};
|
|
10929
|
+
if (Array.isArray(value)) return isValidPosition(value) ? { position: value } : {};
|
|
10930
|
+
const result = {};
|
|
10931
|
+
if (value.position !== void 0 && isValidPosition(value.position)) result.position = value.position;
|
|
10932
|
+
if (typeof value.rotation === "number" && Number.isFinite(value.rotation)) result.rotation = value.rotation;
|
|
10933
|
+
return result;
|
|
10934
|
+
}
|
|
9678
10935
|
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
9679
10936
|
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
9680
10937
|
function splitSuffix(key) {
|
|
@@ -9808,11 +11065,12 @@ function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyl
|
|
|
9808
11065
|
const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
|
|
9809
11066
|
const amplifierField = typeof sourceProps.amplifierField === "string" ? canonicalTextAmplifierKey(sourceProps.amplifierField) : void 0;
|
|
9810
11067
|
const labelPlacementKey = resolveLabelPlacementKey(sourceProps, amplifierField, text, feature.geometry.type, part, generatedLabelOrdinal);
|
|
9811
|
-
const
|
|
9812
|
-
const geometry =
|
|
11068
|
+
const resolvedPlacement = labelPlacementKey ? resolveAmplifierPlacement(amplifierPlacements?.[labelPlacementKey]) : void 0;
|
|
11069
|
+
const geometry = resolvedPlacement?.position && feature.geometry.type === "Point" ? {
|
|
9813
11070
|
...feature.geometry,
|
|
9814
|
-
coordinates: [...
|
|
11071
|
+
coordinates: [...resolvedPlacement.position]
|
|
9815
11072
|
} : feature.geometry;
|
|
11073
|
+
const resolvedRotation = resolvedPlacement?.rotation !== void 0 && labelPlacementKey && feature.geometry.type === "Point" ? resolvedPlacement.rotation : rotation;
|
|
9816
11074
|
return {
|
|
9817
11075
|
type: "Feature",
|
|
9818
11076
|
id: `${id}:${part}:${index}`,
|
|
@@ -9821,7 +11079,7 @@ function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyl
|
|
|
9821
11079
|
index,
|
|
9822
11080
|
style,
|
|
9823
11081
|
...text !== void 0 ? { text } : {},
|
|
9824
|
-
...
|
|
11082
|
+
...resolvedRotation !== void 0 ? { rotation: resolvedRotation } : {},
|
|
9825
11083
|
...textAnchor !== void 0 ? { textAnchor } : {},
|
|
9826
11084
|
...amplifierField !== void 0 ? { amplifierField } : {},
|
|
9827
11085
|
...labelPlacementKey !== void 0 ? { labelPlacementKey } : {},
|
|
@@ -9877,4 +11135,4 @@ function assertNever(value) {
|
|
|
9877
11135
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
9878
11136
|
}
|
|
9879
11137
|
//#endregion
|
|
9880
|
-
export {
|
|
11138
|
+
export { DEFAULT_GENERIC_C2_LINE_OPTIONS as $, DEFAULT_FORTIFIED_LINE_OPTIONS as A, disruptDrawRule as At, DEFAULT_DELAY_OPTIONS as B, project as Bt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as C, line23DrawRule as Ct, DEFAULT_SCREEN_OPTIONS as D, attackByFireDrawRule as Dt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as E, ambushDrawRule as Et, DEFAULT_FIX_OPTIONS as F, pointOnMidpointPerpendicularAxis as Ft, DEFAULT_GENERIC_POLYGON_OPTIONS as G, DEFAULT_TACTICAL_ARROW_OPTIONS as H, EPSILON as Ht, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as I, snapToMidpointPerpendicular as It, DEFAULT_CLASSIC_ARROW_OPTIONS as J, DEFAULT_GENERIC_LINE_OPTIONS as K, DEFAULT_ENCIRCLEMENT_OPTIONS as L, createBaselineFrame as Lt, DEFAULT_HANDOVER_LINE_OPTIONS as M, computeDefaultMidpointPerpendicularPoint as Mt, DEFAULT_PHASE_LINE_OPTIONS as N, createMidpointPerpendicularDrawRule as Nt, DEFAULT_FRONTAL_ATTACK_OPTIONS as O, point12DrawRule as Ot, DEFAULT_FLOT_OPTIONS as P, getMidpointPerpendicularSignedDistance as Pt, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Q, DEFAULT_DISRUPT_OPTIONS as R, calculateMetrics as Rt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as S, line24DrawRule as St, DEFAULT_LANDING_ZONE_OPTIONS as T, line1DrawRule as Tt, DEFAULT_CLEAR_OPTIONS as U, getMetersPerPixel as Ut, DEFAULT_COVER_OPTIONS as V, unproject as Vt, DEFAULT_GENERIC_RECTANGLE_OPTIONS as W, roundToFixed as Wt, DEFAULT_BYPASS_OPTIONS as X, DEFAULT_CANALIZE_OPTIONS as Y, DEFAULT_BREACH_OPTIONS as Z, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as _, DEFAULT_AIRBORNE_ATTACK_OPTIONS as _t, resolveAmplifierPlacement as a, DEFAULT_BATTLE_POSITION_OPTIONS as at, DEFAULT_PICKUP_ZONE_OPTIONS as b, supportByFireDrawRule as bt, CONTROL_MEASURE_METADATA as c, DEFAULT_ANTITANK_WALL_OPTIONS as ct, getControlMeasureMetadataByValue as d, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as dt, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as et, getDefaultOptions as f, DEFAULT_AREA_DEFENSE_OPTIONS as ft, DEFAULT_SUPPORTING_ATTACK_OPTIONS as g, DEFAULT_AMBUSH_OPTIONS as gt, DEFAULT_TURN_OPTIONS as h, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as ht, normalizeTextAmplifiers as i, DEFAULT_BLOCK_OPTIONS as it, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as j, blockDrawRule as jt, DEFAULT_FORTIFIED_AREA_OPTIONS as k, centerRadiusDrawRule as kt, DEFINITIONS as l, DEFAULT_ANTITANK_DITCH_OPTIONS as lt, DEFAULT_TURNING_MOVEMENT_OPTIONS as m, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as mt, TEXT_AMPLIFIER_FIELDS as n, DEFAULT_BOUNDARY_OPTIONS as nt, resolveStyleHints as o, DEFAULT_ATTACK_HELICOPTER_OPTIONS as ot, listControlMeasureMetadata as p, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as pt, DEFAULT_GENERIC_CIRCLE_OPTIONS as q, canonicalTextAmplifierKey as r, DEFAULT_BLOCK_ARROW_OPTIONS as rt, CONTROL_MEASURE_IDS as s, DEFAULT_ATTACK_BY_FIRE_OPTIONS as st, renderControlMeasure as t, DEFAULT_LIGHT_LINE_OPTIONS as tt, getControlMeasureMetadata as u, DEFAULT_ASSEMBLY_AREA_OPTIONS as ut, DEFAULT_STRONG_POINT_OPTIONS as v, rectangleDrawRule as vt, DEFAULT_MAIN_ATTACK_OPTIONS as w, turnDrawRule as wt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as x, line26DrawRule as xt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as y, axis1DrawRule as yt, DEFAULT_GUARD_OPTIONS as z, computeInitialWidthPoint as zt };
|