@orbat-mapper/control-measures 0.2.0-alpha.11 → 0.2.0-alpha.13
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-C6nqkw1A.d.mts → index-h8S1sTjc.d.mts} +169 -23
- 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-NKEYq3Fg.mjs → renderControlMeasure-CoBRpbJT.mjs} +1952 -628
- 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.
|
|
@@ -1826,6 +1893,46 @@ const SMOOTH_LINE_PARAMS = [{
|
|
|
1826
1893
|
function smoothLineVerts(verts, options, defaultResolution) {
|
|
1827
1894
|
return options.smooth ? catmullRom(verts, normalizeSmoothResolution$2(options.smoothResolution ?? defaultResolution, defaultResolution)) : verts;
|
|
1828
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Resamples a path into equal arc-length segments.
|
|
1898
|
+
*
|
|
1899
|
+
* This is useful for a repeated motif (such as a fortified tooth or FLOT
|
|
1900
|
+
* scallop) after smoothing. Catmull-Rom samples are evenly spaced in spline
|
|
1901
|
+
* parameter, not distance, so using them directly as motif boundaries causes
|
|
1902
|
+
* the motifs to bunch up where the curve's samples are closer together.
|
|
1903
|
+
*/
|
|
1904
|
+
function evenlySpacePath(points, targetSegmentLength) {
|
|
1905
|
+
if (points.length < 2) return points;
|
|
1906
|
+
const segmentLengths = [];
|
|
1907
|
+
let totalLength = 0;
|
|
1908
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1909
|
+
const length = vecMag(vecSub(points[i + 1], points[i]));
|
|
1910
|
+
segmentLengths.push(length);
|
|
1911
|
+
totalLength += length;
|
|
1912
|
+
}
|
|
1913
|
+
if (totalLength < 1e-6) return [points[0]];
|
|
1914
|
+
const segmentCount = Math.max(1, Math.round(totalLength / targetSegmentLength));
|
|
1915
|
+
const segmentLength = totalLength / segmentCount;
|
|
1916
|
+
const out = [points[0]];
|
|
1917
|
+
let sourceIndex = 0;
|
|
1918
|
+
let sourceStart = points[0];
|
|
1919
|
+
let consumed = 0;
|
|
1920
|
+
for (let segment = 1; segment < segmentCount; segment++) {
|
|
1921
|
+
const target = segment * segmentLength;
|
|
1922
|
+
while (sourceIndex < segmentLengths.length - 1 && consumed + segmentLengths[sourceIndex] < target) {
|
|
1923
|
+
consumed += segmentLengths[sourceIndex];
|
|
1924
|
+
sourceIndex++;
|
|
1925
|
+
sourceStart = points[sourceIndex];
|
|
1926
|
+
}
|
|
1927
|
+
const length = segmentLengths[sourceIndex];
|
|
1928
|
+
if (length < 1e-6) continue;
|
|
1929
|
+
const t = (target - consumed) / length;
|
|
1930
|
+
const sourceEnd = points[sourceIndex + 1];
|
|
1931
|
+
out.push([sourceStart[0] + (sourceEnd[0] - sourceStart[0]) * t, sourceStart[1] + (sourceEnd[1] - sourceStart[1]) * t]);
|
|
1932
|
+
}
|
|
1933
|
+
out.push(points[points.length - 1]);
|
|
1934
|
+
return out;
|
|
1935
|
+
}
|
|
1829
1936
|
/** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
|
|
1830
1937
|
function catmullRomAt(p0, p1, p2, p3, t) {
|
|
1831
1938
|
const t2 = t * t;
|
|
@@ -1876,177 +1983,6 @@ function closedCatmullRom(points, samples) {
|
|
|
1876
1983
|
return out;
|
|
1877
1984
|
}
|
|
1878
1985
|
//#endregion
|
|
1879
|
-
//#region src/internal/area-echelon.ts
|
|
1880
|
-
/**
|
|
1881
|
-
* Twice the ring's signed area (shoelace). Positive when the ring winds
|
|
1882
|
-
* counter-clockwise in projected (y-up) coordinates. Exported for reuse by
|
|
1883
|
-
* `area-labels.ts`'s polygon-centroid label placement, so the area centroid
|
|
1884
|
-
* shares this one derivation instead of re-deriving it.
|
|
1885
|
-
*/
|
|
1886
|
-
function ringSignedArea2(verts) {
|
|
1887
|
-
let area2 = 0;
|
|
1888
|
-
for (let i = 0; i < verts.length - 1; i++) {
|
|
1889
|
-
const a = verts[i];
|
|
1890
|
-
const b = verts[i + 1];
|
|
1891
|
-
area2 += a[0] * b[1] - b[0] * a[1];
|
|
1892
|
-
}
|
|
1893
|
-
return area2;
|
|
1894
|
-
}
|
|
1895
|
-
/**
|
|
1896
|
-
* Projects `positions` and returns a closed ring (first == last), optionally
|
|
1897
|
-
* smoothed through the control points with a closed centripetal Catmull-Rom
|
|
1898
|
-
* spline. A trailing vertex that duplicates the first (already-closed input)
|
|
1899
|
-
* is dropped before closing/smoothing.
|
|
1900
|
-
*/
|
|
1901
|
-
function buildClosedRing(positions, smooth, smoothResolution, defaultSmoothResolution) {
|
|
1902
|
-
const projected = positions.map((p) => project(p[0], p[1]));
|
|
1903
|
-
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;
|
|
1904
|
-
return smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, defaultSmoothResolution)) : [...ringPts, ringPts[0]];
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Walks a closed ring into per-segment metadata (unit direction, outward
|
|
1908
|
-
* normal, cumulative arc length) plus the total perimeter and the arc-length
|
|
1909
|
-
* anchor of the bottom edge — the segment whose midpoint sits lowest (min
|
|
1910
|
-
* projected y), the side opposite the front.
|
|
1911
|
-
*/
|
|
1912
|
-
function buildAreaPerimeter(verts) {
|
|
1913
|
-
const ccw = ringSignedArea2(verts) > 0;
|
|
1914
|
-
const segs = [];
|
|
1915
|
-
let totalLength = 0;
|
|
1916
|
-
let anchorTarget = 0;
|
|
1917
|
-
let lowest = Infinity;
|
|
1918
|
-
for (let i = 0; i < verts.length - 1; i++) {
|
|
1919
|
-
const a = verts[i];
|
|
1920
|
-
const b = verts[i + 1];
|
|
1921
|
-
const edge = vecSub(b, a);
|
|
1922
|
-
const length = vecMag(edge);
|
|
1923
|
-
if (length < 1e-6) continue;
|
|
1924
|
-
const along = vecNorm(edge);
|
|
1925
|
-
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
1926
|
-
const perp = ccw ? [along[1], -along[0]] : [-along[1], along[0]];
|
|
1927
|
-
segs.push({
|
|
1928
|
-
start: a,
|
|
1929
|
-
along,
|
|
1930
|
-
perp,
|
|
1931
|
-
length,
|
|
1932
|
-
arcStart: totalLength
|
|
1933
|
-
});
|
|
1934
|
-
if (mid[1] < lowest) {
|
|
1935
|
-
lowest = mid[1];
|
|
1936
|
-
anchorTarget = totalLength + length / 2;
|
|
1937
|
-
}
|
|
1938
|
-
totalLength += length;
|
|
1939
|
-
}
|
|
1940
|
-
return {
|
|
1941
|
-
verts,
|
|
1942
|
-
segs,
|
|
1943
|
-
totalLength,
|
|
1944
|
-
anchorTarget
|
|
1945
|
-
};
|
|
1946
|
-
}
|
|
1947
|
-
/** Resolves the arc-length `target` to a point and the segment frame it lands on. */
|
|
1948
|
-
function pointOnRing(segs, target) {
|
|
1949
|
-
let seg = segs[segs.length - 1];
|
|
1950
|
-
for (const candidate of segs) if (target < candidate.arcStart + candidate.length) {
|
|
1951
|
-
seg = candidate;
|
|
1952
|
-
break;
|
|
1953
|
-
}
|
|
1954
|
-
return {
|
|
1955
|
-
point: vecAdd(seg.start, vecScale(seg.along, target - seg.arcStart)),
|
|
1956
|
-
seg
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
/**
|
|
1960
|
-
* Effective echelon glyph height in meters. Screen-anchored when a pixel size
|
|
1961
|
-
* is supplied with a live resolution; ground-anchored (meters) otherwise.
|
|
1962
|
-
* Clamped strictly positive.
|
|
1963
|
-
*/
|
|
1964
|
-
function resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel) {
|
|
1965
|
-
let h = echelonSize;
|
|
1966
|
-
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
1967
|
-
return Math.max(EPSILON, h);
|
|
1968
|
-
}
|
|
1969
|
-
/** Wraps `x` into `[0, 1)`, treating `x` as a fraction (matches the arc-length wrap below). */
|
|
1970
|
-
function wrap01(x) {
|
|
1971
|
-
return (x % 1 + 1) % 1;
|
|
1972
|
-
}
|
|
1973
|
-
/**
|
|
1974
|
-
* Places the Field B echelon glyph on the perimeter and computes its masking
|
|
1975
|
-
* gap, anchored by default to the bottom edge and slid by `echelonPosition`
|
|
1976
|
-
* (a signed fraction of the perimeter, wrapping at the seam). When
|
|
1977
|
-
* `echelonAnchor` is supplied, it replaces the bottom-edge anchor with an
|
|
1978
|
-
* absolute perimeter fraction (measured from the ring seam) — see
|
|
1979
|
-
* `BattlePositionOptions.echelonAnchor` for why (ADR-0023 rotate-freeze).
|
|
1980
|
-
* Returns empty geometry when the echelon resolves to none or the ring is
|
|
1981
|
-
* degenerate.
|
|
1982
|
-
*/
|
|
1983
|
-
function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, echelonAnchor) {
|
|
1984
|
-
const { segs, totalLength, anchorTarget } = perimeter;
|
|
1985
|
-
const spec = resolveEchelon(echelon);
|
|
1986
|
-
if (!spec || segs.length === 0 || totalLength < 1e-6) return {
|
|
1987
|
-
strokes: [],
|
|
1988
|
-
fills: [],
|
|
1989
|
-
gaps: []
|
|
1990
|
-
};
|
|
1991
|
-
const target = (((echelonAnchor !== void 0 ? wrap01(echelonAnchor) * totalLength : anchorTarget) + echelonPosition * totalLength) % totalLength + totalLength) % totalLength;
|
|
1992
|
-
const { point, seg } = pointOnRing(segs, target);
|
|
1993
|
-
const { strokes, fills, width } = buildEchelonGlyph(point, seg.along, seg.perp, h, spec);
|
|
1994
|
-
const gaps = [];
|
|
1995
|
-
const halfGap = width / 2 + Math.max(0, echelonPadding) * h;
|
|
1996
|
-
pushWrappedGap(gaps, target - halfGap, target + halfGap, totalLength);
|
|
1997
|
-
return {
|
|
1998
|
-
strokes,
|
|
1999
|
-
fills,
|
|
2000
|
-
gaps
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Factory for the `freezeOrientation` hook (ADR-0023) shared by every
|
|
2005
|
-
* echelon-carrying closed-area measure (Battle Position, Strong Point, Base
|
|
2006
|
-
* Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
|
|
2007
|
-
* fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
|
|
2008
|
-
* transform box's rotate grip) does not re-derive a different "bottom edge"
|
|
2009
|
-
* against the rotated geometry. `defaults` supplies the measure's own
|
|
2010
|
-
* `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
|
|
2011
|
-
* `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
|
|
2012
|
-
* vs. Base Camp/Guerrilla Base's `echelon: "none"`).
|
|
2013
|
-
*
|
|
2014
|
-
* The returned hook returns `undefined` — nothing to freeze — when:
|
|
2015
|
-
* - `echelonAnchor` is already set (previously frozen or user-authored), or
|
|
2016
|
-
* - the echelon resolves to none (nothing visible to pin), or
|
|
2017
|
-
* - the ring is degenerate (no segments, or a near-zero perimeter).
|
|
2018
|
-
*/
|
|
2019
|
-
function makeFreezeEchelonAnchor(defaults) {
|
|
2020
|
-
return function freezeEchelonAnchor(controlPoints, options) {
|
|
2021
|
-
if (options.echelonAnchor !== void 0) return void 0;
|
|
2022
|
-
if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
|
|
2023
|
-
const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
|
|
2024
|
-
if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
|
|
2025
|
-
return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
//#endregion
|
|
2029
|
-
//#region src/internal/angle-utils.ts
|
|
2030
|
-
/**
|
|
2031
|
-
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
2032
|
-
*/
|
|
2033
|
-
function normalizeRadians(angle) {
|
|
2034
|
-
let normalized = angle % (Math.PI * 2);
|
|
2035
|
-
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
2036
|
-
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
2037
|
-
return normalized;
|
|
2038
|
-
}
|
|
2039
|
-
/**
|
|
2040
|
-
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
2041
|
-
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
2042
|
-
*/
|
|
2043
|
-
function keepTextLeftToRight(rotation) {
|
|
2044
|
-
const normalized = normalizeRadians(rotation);
|
|
2045
|
-
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
2046
|
-
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
2047
|
-
return normalized;
|
|
2048
|
-
}
|
|
2049
|
-
//#endregion
|
|
2050
1986
|
//#region src/internal/line-labels.ts
|
|
2051
1987
|
/**
|
|
2052
1988
|
* Stored label rotation for text running along projected direction `dir`.
|
|
@@ -2067,13 +2003,13 @@ function labelRotationAlong(dir) {
|
|
|
2067
2003
|
*/
|
|
2068
2004
|
function pushUnitLabels(out, frame, units) {
|
|
2069
2005
|
const { center, perp, offset, rotation, labelSize } = frame;
|
|
2070
|
-
for (const { side, designator, country, textAnchor } of units) {
|
|
2006
|
+
for (const { side, designator, country, textAnchor, amplifierField } of units) {
|
|
2071
2007
|
let text;
|
|
2072
2008
|
if (designator.length > 0 && country.length > 0) text = `${designator} (${country})`;
|
|
2073
2009
|
else if (designator.length > 0) text = designator;
|
|
2074
2010
|
else if (country.length > 0) text = `(${country})`;
|
|
2075
2011
|
else continue;
|
|
2076
|
-
pushLabel(out, vecAdd(center, vecScale(perp, side * offset)), text, rotation, labelSize, textAnchor);
|
|
2012
|
+
pushLabel(out, vecAdd(center, vecScale(perp, side * offset)), text, rotation, labelSize, textAnchor, amplifierField);
|
|
2077
2013
|
}
|
|
2078
2014
|
}
|
|
2079
2015
|
/**
|
|
@@ -2199,7 +2135,7 @@ function labelSizeProps(options) {
|
|
|
2199
2135
|
return {};
|
|
2200
2136
|
}
|
|
2201
2137
|
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2202
|
-
function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
2138
|
+
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField) {
|
|
2203
2139
|
out.push({
|
|
2204
2140
|
type: "Feature",
|
|
2205
2141
|
properties: {
|
|
@@ -2207,6 +2143,7 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
|
|
|
2207
2143
|
text,
|
|
2208
2144
|
rotation,
|
|
2209
2145
|
...textAnchor ? { textAnchor } : {},
|
|
2146
|
+
...amplifierField ? { amplifierField } : {},
|
|
2210
2147
|
...sizeProps
|
|
2211
2148
|
},
|
|
2212
2149
|
geometry: {
|
|
@@ -2349,16 +2286,662 @@ function fixedLabelLineFeatures(positions, options, textForEnd) {
|
|
|
2349
2286
|
};
|
|
2350
2287
|
}
|
|
2351
2288
|
//#endregion
|
|
2352
|
-
//#region src/
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
const
|
|
2358
|
-
/**
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
|
2362
2945
|
* ENY marker's own half-width/half-height when carving its boundary
|
|
2363
2946
|
* knockout gap (see {@link pushAreaLabels}). */
|
|
2364
2947
|
const ENY_CLEARANCE_RATIO = .3;
|
|
@@ -2431,12 +3014,6 @@ function ringCentroid(verts) {
|
|
|
2431
3014
|
const factor = 1 / (3 * area2);
|
|
2432
3015
|
return [cx * factor, cy * factor];
|
|
2433
3016
|
}
|
|
2434
|
-
/** Joins the W/W1 date-time-group pair as `"W - W1"`; a single supplied value
|
|
2435
|
-
* passes through unchanged; neither set yields `""`. */
|
|
2436
|
-
function joinDtg(start, end) {
|
|
2437
|
-
if (start && end) return `${start} - ${end}`;
|
|
2438
|
-
return start ?? end ?? "";
|
|
2439
|
-
}
|
|
2440
3017
|
/**
|
|
2441
3018
|
* Points where the closed ring `verts` crosses the horizontal line
|
|
2442
3019
|
* `y = target`, one per crossing edge, plus the ring's total arc length —
|
|
@@ -2480,7 +3057,7 @@ function ringCrossingsAtY(verts, target) {
|
|
|
2480
3057
|
* {@link buildGappedLine} uses) an "ENY" marker knocks out of the boundary —
|
|
2481
3058
|
* empty unless `texts.hostile` was placed.
|
|
2482
3059
|
*
|
|
2483
|
-
* Rows are `[name, additionalInformation,
|
|
3060
|
+
* Rows are `[name (T), additionalInformation (H), dtgStart (W), dtgEnd (W1)]`,
|
|
2484
3061
|
* filtered to non-empty text, stacked around the ring's {@link ringCentroid}
|
|
2485
3062
|
* at `pitch = resolveLabelOffsetMeters(options, 1.2 + labelPadding)` (the 50m
|
|
2486
3063
|
* no-size fallback applies until a live label size is captured — see
|
|
@@ -2514,14 +3091,18 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
2514
3091
|
const padding = Math.max(0, options.labelPadding ?? 0);
|
|
2515
3092
|
const [cx, cy] = ringCentroid(ringVerts);
|
|
2516
3093
|
const rows = [
|
|
2517
|
-
texts.name,
|
|
2518
|
-
texts.additionalInformation,
|
|
2519
|
-
|
|
2520
|
-
|
|
3094
|
+
[texts.name, "T"],
|
|
3095
|
+
[texts.additionalInformation, "H"],
|
|
3096
|
+
[texts.dtgStart, "W"],
|
|
3097
|
+
[texts.dtgEnd, "W1"]
|
|
3098
|
+
].flatMap(([text, field]) => text ? [{
|
|
3099
|
+
text,
|
|
3100
|
+
field
|
|
3101
|
+
}] : []);
|
|
2521
3102
|
if (rows.length > 0) {
|
|
2522
3103
|
const pitch = resolveLabelOffsetMeters(options, ROW_PITCH_RATIO + padding);
|
|
2523
|
-
rows.forEach((text, i) => {
|
|
2524
|
-
pushLabel(out, [cx, cy + ((rows.length - 1) / 2 - i) * pitch], text, HORIZONTAL_LABEL_ROTATION, sizeProps);
|
|
3104
|
+
rows.forEach(({ text, field }, i) => {
|
|
3105
|
+
pushLabel(out, [cx, cy + ((rows.length - 1) / 2 - i) * pitch], text, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, field);
|
|
2525
3106
|
});
|
|
2526
3107
|
}
|
|
2527
3108
|
const gaps = [];
|
|
@@ -2535,7 +3116,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}) {
|
|
|
2535
3116
|
const textWidth = texts.hostile.length * ENY_CHAR_ASPECT_RATIO * textHeight;
|
|
2536
3117
|
const clearance = (ENY_CLEARANCE_RATIO + padding) * textHeight;
|
|
2537
3118
|
for (const crossing of [west, east]) {
|
|
2538
|
-
pushLabel(out, [crossing.x, cy], texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps);
|
|
3119
|
+
pushLabel(out, [crossing.x, cy], texts.hostile, HORIZONTAL_LABEL_ROTATION, sizeProps, void 0, "N");
|
|
2539
3120
|
const halfExtent = .5 * (textWidth * Math.abs(crossing.along[0]) + textHeight * Math.abs(crossing.along[1])) + clearance;
|
|
2540
3121
|
pushWrappedGap(gaps, crossing.arc - halfExtent, crossing.arc + halfExtent, totalArc);
|
|
2541
3122
|
}
|
|
@@ -2623,20 +3204,217 @@ const AREA_METADATA = {
|
|
|
2623
3204
|
function createArea(positions, options = {}, textAmplifiers = {}) {
|
|
2624
3205
|
return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
|
|
2625
3206
|
}
|
|
2626
|
-
const AREA = defineControlMeasure({
|
|
2627
|
-
metadata: AREA_METADATA,
|
|
2628
|
-
generator: createArea,
|
|
2629
|
-
defaultOptions: DEFAULT_AREA_OPTIONS,
|
|
2630
|
-
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,
|
|
2631
3415
|
previewSample: {
|
|
2632
|
-
controlPoints: [
|
|
2633
|
-
|
|
2634
|
-
[1, -.5],
|
|
2635
|
-
[1, .5],
|
|
2636
|
-
[-1, .5]
|
|
2637
|
-
],
|
|
2638
|
-
textAmplifiers: {},
|
|
2639
|
-
options: { labelSize: 96 }
|
|
3416
|
+
controlPoints: [[0, 0], [-.77, .21]],
|
|
3417
|
+
options: { labelSize: 90 }
|
|
2640
3418
|
}
|
|
2641
3419
|
});
|
|
2642
3420
|
//#endregion
|
|
@@ -2893,7 +3671,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
|
|
|
2893
3671
|
}
|
|
2894
3672
|
if (textAmplifiers.H) {
|
|
2895
3673
|
const labelFeatures = [];
|
|
2896
|
-
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start");
|
|
3674
|
+
pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start", "H");
|
|
2897
3675
|
features.push(...labelFeatures);
|
|
2898
3676
|
}
|
|
2899
3677
|
return {
|
|
@@ -3339,7 +4117,11 @@ function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidth
|
|
|
3339
4117
|
* unchanged (no project/unproject round trip) when smoothing is off.
|
|
3340
4118
|
*/
|
|
3341
4119
|
function smoothedDitchBasePath(positions, options) {
|
|
3342
|
-
|
|
4120
|
+
const projectedPoints = positions.map((position) => project(position[0], position[1]));
|
|
4121
|
+
const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
|
|
4122
|
+
const toothHeight = calculateAntitankDitchHeight(options);
|
|
4123
|
+
const toothWidthRatio = Math.max(EPSILON, options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
4124
|
+
return options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, toothWidthRatio * toothHeight) : smoothedPath;
|
|
3343
4125
|
}
|
|
3344
4126
|
function buildAntitankDitchTriangles(basePath, options) {
|
|
3345
4127
|
return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
|
|
@@ -3447,6 +4229,8 @@ function createAntitankWall(positions, options = {}) {
|
|
|
3447
4229
|
const toothHeight = calculateAntitankDitchHeight(options);
|
|
3448
4230
|
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
|
|
3449
4231
|
const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
|
|
4232
|
+
const projectedPoints = positions.map((position) => project(position[0], position[1]));
|
|
4233
|
+
const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
|
|
3450
4234
|
return {
|
|
3451
4235
|
type: "FeatureCollection",
|
|
3452
4236
|
features: [{
|
|
@@ -3454,7 +4238,7 @@ function createAntitankWall(positions, options = {}) {
|
|
|
3454
4238
|
properties: { part: "wall" },
|
|
3455
4239
|
geometry: {
|
|
3456
4240
|
type: "MultiLineString",
|
|
3457
|
-
coordinates: [generateAntitankWallPoints(
|
|
4241
|
+
coordinates: [generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
|
|
3458
4242
|
}
|
|
3459
4243
|
}]
|
|
3460
4244
|
};
|
|
@@ -4716,11 +5500,13 @@ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
|
|
|
4716
5500
|
const units = [{
|
|
4717
5501
|
side: 1,
|
|
4718
5502
|
designator: textAmplifiers.T ?? "",
|
|
4719
|
-
country: textAmplifiers.AS ?? ""
|
|
5503
|
+
country: textAmplifiers.AS ?? "",
|
|
5504
|
+
amplifierField: "T"
|
|
4720
5505
|
}, {
|
|
4721
5506
|
side: -1,
|
|
4722
5507
|
designator: textAmplifiers.T1 ?? "",
|
|
4723
|
-
country: textAmplifiers.AS1 ?? ""
|
|
5508
|
+
country: textAmplifiers.AS1 ?? "",
|
|
5509
|
+
amplifierField: "T1"
|
|
4724
5510
|
}];
|
|
4725
5511
|
const labelFeatures = [];
|
|
4726
5512
|
pushUnitLabels(labelFeatures, {
|
|
@@ -4920,6 +5706,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
|
4920
5706
|
type: "Feature",
|
|
4921
5707
|
properties: {
|
|
4922
5708
|
part: "label",
|
|
5709
|
+
labelPlacement: false,
|
|
4923
5710
|
text,
|
|
4924
5711
|
rotation,
|
|
4925
5712
|
...labelSizeProps(labelSize)
|
|
@@ -5020,7 +5807,7 @@ const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
|
|
|
5020
5807
|
const BLOCK_MISSION_TASK_METADATA = {
|
|
5021
5808
|
id: "block-mission-task",
|
|
5022
5809
|
name: "Block",
|
|
5023
|
-
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.",
|
|
5024
5811
|
entity: "Mission Tasks",
|
|
5025
5812
|
entityType: "Block",
|
|
5026
5813
|
value: "340100",
|
|
@@ -5112,7 +5899,7 @@ const DEFAULT_BREACH_OPTIONS = {
|
|
|
5112
5899
|
const BREACH_METADATA = {
|
|
5113
5900
|
id: "breach",
|
|
5114
5901
|
name: "Breach",
|
|
5115
|
-
description: "
|
|
5902
|
+
description: "Employs all available means to break through or secure a passage through an enemy defense, obstacle, minefield, or fortification.",
|
|
5116
5903
|
entity: "Mission Tasks",
|
|
5117
5904
|
entityType: "Breach",
|
|
5118
5905
|
value: "340200",
|
|
@@ -5208,7 +5995,7 @@ const DEFAULT_BYPASS_OPTIONS = {
|
|
|
5208
5995
|
const BYPASS_METADATA = {
|
|
5209
5996
|
id: "bypass",
|
|
5210
5997
|
name: "Bypass",
|
|
5211
|
-
description: "
|
|
5998
|
+
description: "Maneuvers around an obstacle, position, or enemy force to maintain the momentum of the operation.",
|
|
5212
5999
|
entity: "Mission Tasks",
|
|
5213
6000
|
entityType: "Bypass",
|
|
5214
6001
|
value: "340300",
|
|
@@ -5283,7 +6070,7 @@ const DEFAULT_CANALIZE_OPTIONS = {
|
|
|
5283
6070
|
const CANALIZE_METADATA = {
|
|
5284
6071
|
id: "canalize",
|
|
5285
6072
|
name: "Canalize",
|
|
5286
|
-
description: "
|
|
6073
|
+
description: "Restricts enemy movement to a narrow zone by exploiting terrain coupled with the use of obstacles, fires, or friendly maneuver.",
|
|
5287
6074
|
entity: "Mission Tasks",
|
|
5288
6075
|
entityType: "Canalize",
|
|
5289
6076
|
value: "340400",
|
|
@@ -5904,184 +6691,576 @@ function createGenericRectangle(coordinates, options = {}) {
|
|
|
5904
6691
|
p1
|
|
5905
6692
|
].map((point) => unproject(point[0], point[1]));
|
|
5906
6693
|
return {
|
|
5907
|
-
type: "FeatureCollection",
|
|
5908
|
-
features: [{
|
|
5909
|
-
type: "Feature",
|
|
5910
|
-
properties: {
|
|
5911
|
-
part: "rectangle",
|
|
5912
|
-
fill
|
|
5913
|
-
},
|
|
5914
|
-
geometry: {
|
|
5915
|
-
type: "Polygon",
|
|
5916
|
-
coordinates: [ring]
|
|
5917
|
-
}
|
|
5918
|
-
}]
|
|
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
|
|
5919
6920
|
};
|
|
5920
6921
|
}
|
|
5921
|
-
const GENERIC_RECTANGLE = defineControlMeasure({
|
|
5922
|
-
metadata: GENERIC_RECTANGLE_METADATA,
|
|
5923
|
-
generator: createGenericRectangle,
|
|
5924
|
-
defaultOptions: DEFAULT_GENERIC_RECTANGLE_OPTIONS,
|
|
5925
|
-
rule: rectangleDrawRule,
|
|
5926
|
-
previewSample: { controlPoints: [
|
|
5927
|
-
[-.9, -.55],
|
|
5928
|
-
[.9, -.55],
|
|
5929
|
-
[.9, .55]
|
|
5930
|
-
] }
|
|
5931
|
-
});
|
|
5932
6922
|
//#endregion
|
|
5933
|
-
//#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;
|
|
5934
6929
|
/**
|
|
5935
|
-
* Default options for the
|
|
6930
|
+
* Default options for the tactical arrow.
|
|
5936
6931
|
*/
|
|
5937
|
-
const
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
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
|
|
5942
6938
|
};
|
|
5943
|
-
const
|
|
5944
|
-
id: "
|
|
5945
|
-
name: "
|
|
5946
|
-
description: "
|
|
5947
|
-
entity: "
|
|
5948
|
-
entityType: "
|
|
5949
|
-
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",
|
|
5950
6946
|
minCoordinates: 3,
|
|
5951
6947
|
maxCoordinates: 3,
|
|
5952
|
-
geometry: "
|
|
5953
|
-
geometryTypes: ["MultiLineString", "
|
|
5954
|
-
drawRule: "
|
|
5955
|
-
capturesLabelSize: true,
|
|
6948
|
+
geometry: "area",
|
|
6949
|
+
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
6950
|
+
drawRule: "Area21",
|
|
5956
6951
|
params: [
|
|
5957
|
-
{
|
|
5958
|
-
key: "arrowInsetRatio",
|
|
5959
|
-
label: "Arrow padding",
|
|
5960
|
-
description: "Padding between the line endpoints and the outer arrows, as a ratio of height",
|
|
5961
|
-
type: "number",
|
|
5962
|
-
min: 0,
|
|
5963
|
-
max: .45,
|
|
5964
|
-
step: .01
|
|
5965
|
-
},
|
|
5966
6952
|
{
|
|
5967
6953
|
key: "arrowheadLengthRatio",
|
|
5968
6954
|
label: "Arrowhead length",
|
|
5969
|
-
description: "Arrowhead length as a ratio of the
|
|
6955
|
+
description: "Arrowhead length as a ratio of the total leg length",
|
|
5970
6956
|
type: "number",
|
|
5971
|
-
min:
|
|
5972
|
-
max:
|
|
6957
|
+
min: 0,
|
|
6958
|
+
max: .5,
|
|
5973
6959
|
step: .01
|
|
5974
6960
|
},
|
|
5975
6961
|
{
|
|
5976
6962
|
key: "arrowheadWidthRatio",
|
|
5977
6963
|
label: "Arrowhead width",
|
|
5978
|
-
description: "Arrowhead base width as a
|
|
6964
|
+
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
5979
6965
|
type: "number",
|
|
5980
|
-
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,
|
|
5981
6976
|
max: 1,
|
|
5982
|
-
step: .
|
|
6977
|
+
step: .05
|
|
5983
6978
|
},
|
|
5984
6979
|
{
|
|
5985
|
-
key: "
|
|
5986
|
-
label: "
|
|
5987
|
-
description: "
|
|
6980
|
+
key: "zigzagEnd",
|
|
6981
|
+
label: "Zigzag end",
|
|
6982
|
+
description: "Point along the leg where the zigzag returns (0-1).",
|
|
5988
6983
|
type: "number",
|
|
5989
6984
|
min: 0,
|
|
5990
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,
|
|
5991
6995
|
step: .01
|
|
5992
6996
|
}
|
|
5993
6997
|
]
|
|
5994
6998
|
};
|
|
5995
6999
|
/**
|
|
5996
|
-
* Generates a GeoJSON FeatureCollection
|
|
5997
|
-
*
|
|
5998
|
-
* A vertical line (PT. 1 → PT. 2) with three arrows pointing at it,
|
|
5999
|
-
* perpendicular to the line. The arrows come from the rear — the side PT. 3
|
|
6000
|
-
* sits on — and their tips touch the vertical line; each carries a shaft
|
|
6001
|
-
* reaching back to the rear. The arrows are evenly spaced along the line, so
|
|
6002
|
-
* the middle arrow's tip sits at the midpoint of the vertical line and its
|
|
6003
|
-
* shaft carries the 'C' glyph. The arrows stay perpendicular to the vertical
|
|
6004
|
-
* line at any rotation.
|
|
6005
|
-
*
|
|
6006
|
-
* Input Points:
|
|
6007
|
-
* - PT. 1: Top endpoint of the vertical line
|
|
6008
|
-
* - PT. 2: Bottom endpoint of the vertical line — defines height with PT. 1
|
|
6009
|
-
* - PT. 3: Rear point — defines the shaft length (and which side the arrows
|
|
6010
|
-
* come from)
|
|
7000
|
+
* Generates a GeoJSON FeatureCollection of a tactical "lightning" arrow.
|
|
7001
|
+
* Handles internal projection to maintain correct perpendicularity.
|
|
6011
7002
|
*
|
|
6012
|
-
* @param coordinates - [
|
|
6013
|
-
* @param options -
|
|
6014
|
-
* @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.
|
|
6015
7157
|
*/
|
|
6016
|
-
function
|
|
6017
|
-
const
|
|
6018
|
-
const
|
|
6019
|
-
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
|
|
6020
|
-
const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
|
|
6021
|
-
const [c1, c2, c3] = coordinates;
|
|
7158
|
+
function createSecurityTaskSymbol(coordinates, config, options = {}) {
|
|
7159
|
+
const { taskGlyph, part } = config;
|
|
7160
|
+
const [c1, c2, c3, c4] = coordinates;
|
|
6022
7161
|
const p1 = project(c1[0], c1[1]);
|
|
6023
7162
|
const p2 = project(c2[0], c2[1]);
|
|
6024
7163
|
const p3 = project(c3[0], c3[1]);
|
|
6025
|
-
const
|
|
6026
|
-
const
|
|
6027
|
-
|
|
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 {
|
|
6028
7178
|
type: "FeatureCollection",
|
|
6029
7179
|
features: []
|
|
6030
7180
|
};
|
|
6031
|
-
const
|
|
6032
|
-
const midLine = vecScale(vecAdd(p1, p2), .5);
|
|
6033
|
-
const rawPerp = [-dirLine[1], dirLine[0]];
|
|
6034
|
-
const perpToRear = vecScale(rawPerp, vecDot(vecSub(p3, midLine), rawPerp) < 0 ? -1 : 1);
|
|
6035
|
-
const shaftLength = Math.abs(vecDot(vecSub(p3, midLine), rawPerp));
|
|
6036
|
-
const arrowDir = vecScale(perpToRear, -1);
|
|
6037
|
-
const arrowheadLength = height * arrowheadLengthRatio;
|
|
6038
|
-
const arrowheadWidth = height * arrowheadWidthRatio;
|
|
6039
|
-
const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
|
|
6040
|
-
const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
|
|
6041
|
-
const fullTs = [
|
|
6042
|
-
arrowInsetRatio,
|
|
6043
|
-
.5,
|
|
6044
|
-
1 - arrowInsetRatio
|
|
6045
|
-
];
|
|
6046
|
-
const labelIndex = 1;
|
|
6047
|
-
fullTs.forEach((t, i) => {
|
|
6048
|
-
const tip = tipAt(t);
|
|
6049
|
-
const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
|
|
6050
|
-
if (i === labelIndex && shaftLength > 1e-6) {
|
|
6051
|
-
const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
|
|
6052
|
-
const gapHalf = shaftLength * labelGapRatio / 2;
|
|
6053
|
-
const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
|
|
6054
|
-
const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
|
|
6055
|
-
lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
|
|
6056
|
-
lines.push([unproject(gapTip[0], gapTip[1]), unproject(tip[0], tip[1])]);
|
|
6057
|
-
} else if (shaftLength > 1e-6) lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(tip[0], tip[1])]);
|
|
6058
|
-
lines.push(createArrowHead$1(tip, arrowDir, arrowheadLength, arrowheadWidth));
|
|
6059
|
-
});
|
|
6060
|
-
const mainFeature = {
|
|
7181
|
+
const features = [{
|
|
6061
7182
|
type: "Feature",
|
|
6062
|
-
properties: { part
|
|
7183
|
+
properties: { part },
|
|
6063
7184
|
geometry: {
|
|
6064
7185
|
type: "MultiLineString",
|
|
6065
7186
|
coordinates: lines
|
|
6066
7187
|
}
|
|
6067
|
-
};
|
|
6068
|
-
const
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
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 = {
|
|
6075
7195
|
labelSize: options.labelSize,
|
|
6076
7196
|
labelSizePixels: options.labelSizePixels
|
|
6077
|
-
}
|
|
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
|
|
6078
7203
|
};
|
|
6079
7204
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
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
|
+
}
|
|
6085
7264
|
});
|
|
6086
7265
|
//#endregion
|
|
6087
7266
|
//#region src/generators/cm34-mission-tasks/delay.ts
|
|
@@ -6094,7 +7273,7 @@ const DEFAULT_DELAY_OPTIONS = {
|
|
|
6094
7273
|
const DELAY_METADATA = {
|
|
6095
7274
|
id: "delay",
|
|
6096
7275
|
name: "Delay",
|
|
6097
|
-
description: "
|
|
7276
|
+
description: "Trades space for time while under pressure, slowing the enemy's momentum and inflicting maximum damage without becoming decisively engaged.",
|
|
6098
7277
|
entity: "Mission Tasks",
|
|
6099
7278
|
entityType: "Delay",
|
|
6100
7279
|
value: "340800",
|
|
@@ -6209,6 +7388,46 @@ const DELAY = defineControlMeasure({
|
|
|
6209
7388
|
rule: line24DrawRule
|
|
6210
7389
|
});
|
|
6211
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
|
|
6212
7431
|
//#region src/internal/arrowhead.ts
|
|
6213
7432
|
/**
|
|
6214
7433
|
* Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
|
|
@@ -6671,55 +7890,6 @@ const EXTRACTION_ZONE = defineControlMeasure({
|
|
|
6671
7890
|
}
|
|
6672
7891
|
});
|
|
6673
7892
|
//#endregion
|
|
6674
|
-
//#region src/internal/field-of-fire.ts
|
|
6675
|
-
/**
|
|
6676
|
-
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
6677
|
-
* barbs are sized relative to that arm's own length, so each arm's head scales
|
|
6678
|
-
* independently (Line3 — arms vary independently). Returns `null` when the arm
|
|
6679
|
-
* has collapsed to zero length.
|
|
6680
|
-
*/
|
|
6681
|
-
function arrowheadAt(vertex, tip, hSize, hWidth) {
|
|
6682
|
-
const v = vecSub(tip, vertex);
|
|
6683
|
-
const armLength = vecMag(v);
|
|
6684
|
-
if (armLength < 1e-6) return null;
|
|
6685
|
-
const dir = vecNorm(v);
|
|
6686
|
-
const perp = [-dir[1], dir[0]];
|
|
6687
|
-
const headLength = armLength * hSize;
|
|
6688
|
-
const halfWidth = headLength * hWidth;
|
|
6689
|
-
const base = vecSub(tip, vecScale(dir, headLength));
|
|
6690
|
-
return [
|
|
6691
|
-
vecAdd(base, vecScale(perp, halfWidth)),
|
|
6692
|
-
tip,
|
|
6693
|
-
vecSub(base, vecScale(perp, halfWidth))
|
|
6694
|
-
];
|
|
6695
|
-
}
|
|
6696
|
-
/**
|
|
6697
|
-
* Projects the vertex (PT 1) and up to two tips and builds the V of arms, each
|
|
6698
|
-
* arm ending in an open arrowhead. Renders one arm per tip, so two points
|
|
6699
|
-
* (vertex + one tip) draws the single arm being placed — the live feedback
|
|
6700
|
-
* after PT 1 — and three points draws the full V. Points beyond the third are
|
|
6701
|
-
* ignored (input contract, ADR-0014).
|
|
6702
|
-
*
|
|
6703
|
-
* Returns `null` if any arm is degenerate (collapsed to zero length), the
|
|
6704
|
-
* silent ADR-0014 empty-render signal.
|
|
6705
|
-
*/
|
|
6706
|
-
function buildFieldOfFireV(coordinates, hSize, hWidth) {
|
|
6707
|
-
const vertexCoord = coordinates[0];
|
|
6708
|
-
const vertex = project(vertexCoord[0], vertexCoord[1]);
|
|
6709
|
-
const tips = coordinates.slice(1, 3).map((c) => project(c[0], c[1]));
|
|
6710
|
-
const lines = [];
|
|
6711
|
-
for (const tip of tips) {
|
|
6712
|
-
const head = arrowheadAt(vertex, tip, hSize, hWidth);
|
|
6713
|
-
if (!head) return null;
|
|
6714
|
-
lines.push([vertex, tip], head);
|
|
6715
|
-
}
|
|
6716
|
-
return {
|
|
6717
|
-
vertex,
|
|
6718
|
-
tips,
|
|
6719
|
-
lines
|
|
6720
|
-
};
|
|
6721
|
-
}
|
|
6722
|
-
//#endregion
|
|
6723
7893
|
//#region src/generators/cm14-maneuver-lines/finalProtectiveFire.ts
|
|
6724
7894
|
const BLADE_LENGTH_FRACTION = .8;
|
|
6725
7895
|
const DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS = {
|
|
@@ -7177,10 +8347,12 @@ function createFLOT(positions, options = {}) {
|
|
|
7177
8347
|
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
7178
8348
|
const safeRadius = Math.max(EPSILON, effectiveRadius);
|
|
7179
8349
|
const safeArcSegments = Math.max(3, Math.round(arcSegments));
|
|
7180
|
-
const
|
|
8350
|
+
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
8351
|
+
const smoothedPath = smoothLineVerts(projectedPoints, {
|
|
7181
8352
|
smooth,
|
|
7182
8353
|
smoothResolution
|
|
7183
8354
|
}, 16);
|
|
8355
|
+
const basePath = smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, 2 * safeRadius) : smoothedPath;
|
|
7184
8356
|
const allPoints = [];
|
|
7185
8357
|
for (let i = 0; i < basePath.length - 1; i++) {
|
|
7186
8358
|
const p1 = basePath[i];
|
|
@@ -7571,7 +8743,7 @@ function createFortifiedLine(positions, options = {}) {
|
|
|
7571
8743
|
properties: {},
|
|
7572
8744
|
geometry: {
|
|
7573
8745
|
type: "LineString",
|
|
7574
|
-
coordinates: generateFortifiedPoints(smooth ? catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)) : projectedPoints, safeSize).map((p) => unproject(p[0], p[1]))
|
|
8746
|
+
coordinates: generateFortifiedPoints(smooth ? evenlySpacePath(catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)), 2 * safeSize) : projectedPoints, safeSize).map((p) => unproject(p[0], p[1]))
|
|
7575
8747
|
}
|
|
7576
8748
|
}]
|
|
7577
8749
|
};
|
|
@@ -7623,7 +8795,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
7623
8795
|
const safeSize = calculateEffectiveSize(options);
|
|
7624
8796
|
const projected = positions.map((p) => project(p[0], p[1]));
|
|
7625
8797
|
const ringPts = projected.length > 1 && vecMag(vecSub(projected[0], projected[projected.length - 1])) < 1e-6 ? projected.slice(0, -1) : projected;
|
|
7626
|
-
const boundaryInput = smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution)) : [...ringPts, ringPts[0]];
|
|
8798
|
+
const boundaryInput = smooth ? evenlySpacePath(closedCatmullRom(ringPts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution)), 2 * safeSize) : [...ringPts, ringPts[0]];
|
|
7627
8799
|
const boundaryPoints = generateFortifiedPoints(boundaryInput, safeSize);
|
|
7628
8800
|
if (boundaryPoints.length > 0) {
|
|
7629
8801
|
const firstPt = boundaryPoints[0];
|
|
@@ -7631,41 +8803,235 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}) {
|
|
|
7631
8803
|
if (vecMag(vecSub(firstPt, lastPt)) > 1e-6) boundaryPoints.push(firstPt);
|
|
7632
8804
|
else boundaryPoints[boundaryPoints.length - 1] = firstPt;
|
|
7633
8805
|
}
|
|
7634
|
-
const features = [{
|
|
7635
|
-
type: "Feature",
|
|
7636
|
-
properties: {},
|
|
7637
|
-
geometry: {
|
|
7638
|
-
type: "Polygon",
|
|
7639
|
-
coordinates: [boundaryPoints.map((p) => unproject(p[0], p[1]))]
|
|
7640
|
-
}
|
|
7641
|
-
}];
|
|
7642
|
-
const labelFeatures = [];
|
|
7643
|
-
pushAreaLabels(labelFeatures, boundaryInput, {
|
|
7644
|
-
name: textAmplifiers.T,
|
|
7645
|
-
additionalInformation: textAmplifiers.H,
|
|
7646
|
-
dtgStart: textAmplifiers.W,
|
|
7647
|
-
dtgEnd: textAmplifiers.W1
|
|
7648
|
-
}, options);
|
|
7649
|
-
features.push(...labelFeatures);
|
|
7650
|
-
return {
|
|
7651
|
-
type: "FeatureCollection",
|
|
7652
|
-
features
|
|
7653
|
-
};
|
|
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
|
+
});
|
|
7654
9016
|
}
|
|
7655
|
-
const
|
|
7656
|
-
metadata:
|
|
7657
|
-
generator:
|
|
7658
|
-
defaultOptions:
|
|
7659
|
-
rule:
|
|
9017
|
+
const FRONTAL_ATTACK = defineControlMeasure({
|
|
9018
|
+
metadata: FRONTAL_ATTACK_METADATA,
|
|
9019
|
+
generator: createFrontalAttack,
|
|
9020
|
+
defaultOptions: DEFAULT_FRONTAL_ATTACK_OPTIONS,
|
|
9021
|
+
rule: axis1DrawRule,
|
|
7660
9022
|
previewSample: {
|
|
7661
9023
|
controlPoints: [
|
|
7662
|
-
[
|
|
7663
|
-
[
|
|
7664
|
-
[1,
|
|
7665
|
-
[
|
|
9024
|
+
[1, 0],
|
|
9025
|
+
[-.4, -.35],
|
|
9026
|
+
[-1, -.8],
|
|
9027
|
+
[.45, .55]
|
|
7666
9028
|
],
|
|
7667
|
-
textAmplifiers: {
|
|
7668
|
-
|
|
9029
|
+
textAmplifiers: {
|
|
9030
|
+
T: "FOX",
|
|
9031
|
+
W: "080400Z",
|
|
9032
|
+
W1: "120300Z"
|
|
9033
|
+
},
|
|
9034
|
+
options: { labelSize: 80 }
|
|
7669
9035
|
}
|
|
7670
9036
|
});
|
|
7671
9037
|
//#endregion
|
|
@@ -7681,7 +9047,7 @@ const DEFAULT_ISOLATE_OPTIONS = {
|
|
|
7681
9047
|
const ISOLATE_METADATA = {
|
|
7682
9048
|
id: "isolate",
|
|
7683
9049
|
name: "Isolate",
|
|
7684
|
-
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.",
|
|
7685
9051
|
entity: "Mission Tasks",
|
|
7686
9052
|
entityType: "Isolate",
|
|
7687
9053
|
value: "341500",
|
|
@@ -7799,6 +9165,46 @@ const ISOLATE = defineControlMeasure({
|
|
|
7799
9165
|
rule: isolateDrawRule
|
|
7800
9166
|
});
|
|
7801
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
|
|
7802
9208
|
//#region src/generators/cm15-maneuver-areas/jointTacticalActionArea.ts
|
|
7803
9209
|
/** Default options for the Joint tactical action area control measure. */
|
|
7804
9210
|
const DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS = {
|
|
@@ -8645,173 +10051,6 @@ const PRINCIPAL_DIRECTION_OF_FIRE = defineControlMeasure({
|
|
|
8645
10051
|
] }
|
|
8646
10052
|
});
|
|
8647
10053
|
//#endregion
|
|
8648
|
-
//#region src/generators/cm15-maneuver-areas/searchArea.ts
|
|
8649
|
-
const DEFAULT_ARROWHEAD_LENGTH_RATIO = .12;
|
|
8650
|
-
const DEFAULT_ARROWHEAD_WIDTH_RATIO = .5;
|
|
8651
|
-
const DEFAULT_ZIGZAG_START = .55;
|
|
8652
|
-
const DEFAULT_ZIGZAG_END = .45;
|
|
8653
|
-
const DEFAULT_ZIGZAG_AMPLITUDE_RATIO = .07;
|
|
8654
|
-
/**
|
|
8655
|
-
* Default options for the tactical arrow.
|
|
8656
|
-
*/
|
|
8657
|
-
const DEFAULT_TACTICAL_ARROW_OPTIONS = {
|
|
8658
|
-
arrowheadLengthRatio: DEFAULT_ARROWHEAD_LENGTH_RATIO,
|
|
8659
|
-
arrowheadWidthRatio: DEFAULT_ARROWHEAD_WIDTH_RATIO,
|
|
8660
|
-
zigzagStart: DEFAULT_ZIGZAG_START,
|
|
8661
|
-
zigzagEnd: DEFAULT_ZIGZAG_END,
|
|
8662
|
-
zigzagAmplitudeRatio: DEFAULT_ZIGZAG_AMPLITUDE_RATIO
|
|
8663
|
-
};
|
|
8664
|
-
const SEARCH_AREA_METADATA = {
|
|
8665
|
-
id: "search-area",
|
|
8666
|
-
name: "Search Area",
|
|
8667
|
-
description: "Search area graphic with paired lightning-style arrows.",
|
|
8668
|
-
entity: "Maneuver Areas",
|
|
8669
|
-
entityType: "Search Area",
|
|
8670
|
-
value: "152200",
|
|
8671
|
-
minCoordinates: 3,
|
|
8672
|
-
maxCoordinates: 3,
|
|
8673
|
-
geometry: "area",
|
|
8674
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
8675
|
-
drawRule: "Area21",
|
|
8676
|
-
params: [
|
|
8677
|
-
{
|
|
8678
|
-
key: "arrowheadLengthRatio",
|
|
8679
|
-
label: "Arrowhead length",
|
|
8680
|
-
description: "Arrowhead length as a ratio of the total leg length",
|
|
8681
|
-
type: "number",
|
|
8682
|
-
min: 0,
|
|
8683
|
-
max: .5,
|
|
8684
|
-
step: .01
|
|
8685
|
-
},
|
|
8686
|
-
{
|
|
8687
|
-
key: "arrowheadWidthRatio",
|
|
8688
|
-
label: "Arrowhead width",
|
|
8689
|
-
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
8690
|
-
type: "number",
|
|
8691
|
-
min: .05,
|
|
8692
|
-
max: 2,
|
|
8693
|
-
step: .1
|
|
8694
|
-
},
|
|
8695
|
-
{
|
|
8696
|
-
key: "zigzagStart",
|
|
8697
|
-
label: "Zigzag start",
|
|
8698
|
-
description: "Point along the leg where the zigzag begins (0-1).",
|
|
8699
|
-
type: "number",
|
|
8700
|
-
min: 0,
|
|
8701
|
-
max: 1,
|
|
8702
|
-
step: .05
|
|
8703
|
-
},
|
|
8704
|
-
{
|
|
8705
|
-
key: "zigzagEnd",
|
|
8706
|
-
label: "Zigzag end",
|
|
8707
|
-
description: "Point along the leg where the zigzag returns (0-1).",
|
|
8708
|
-
type: "number",
|
|
8709
|
-
min: 0,
|
|
8710
|
-
max: 1,
|
|
8711
|
-
step: .05
|
|
8712
|
-
},
|
|
8713
|
-
{
|
|
8714
|
-
key: "zigzagAmplitudeRatio",
|
|
8715
|
-
label: "Zigzag amplitude",
|
|
8716
|
-
description: "Perpendicular kink offset as a ratio of leg length.",
|
|
8717
|
-
type: "number",
|
|
8718
|
-
min: 0,
|
|
8719
|
-
max: .3,
|
|
8720
|
-
step: .01
|
|
8721
|
-
}
|
|
8722
|
-
]
|
|
8723
|
-
};
|
|
8724
|
-
/**
|
|
8725
|
-
* Generates a GeoJSON FeatureCollection of a tactical "lightning" arrow.
|
|
8726
|
-
* Handles internal projection to maintain correct perpendicularity.
|
|
8727
|
-
*
|
|
8728
|
-
* @param coordinates - [Apex Point (PT1), Top Tail (PT2), Bottom Tail (PT3)]
|
|
8729
|
-
* @param options - Geometric configuration parameters
|
|
8730
|
-
* @returns A FeatureCollection containing a MultiLineString for bolts and a MultiPolygon for heads
|
|
8731
|
-
*/
|
|
8732
|
-
function createSearchArea(coordinates, options = {}) {
|
|
8733
|
-
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;
|
|
8734
|
-
const safeArrowheadLengthRatio = Math.max(0, arrowheadLengthRatio);
|
|
8735
|
-
const safeArrowheadWidthRatio = Math.max(0, arrowheadWidthRatio);
|
|
8736
|
-
const safeZigzagStart = clamp01(zigzagStart);
|
|
8737
|
-
const safeZigzagEnd = clamp01(zigzagEnd);
|
|
8738
|
-
const safeZigzagAmplitudeRatio = Math.max(0, zigzagAmplitudeRatio);
|
|
8739
|
-
const [c0, c1, c2] = coordinates;
|
|
8740
|
-
const [p1, p2, p3] = [
|
|
8741
|
-
project(c0[0], c0[1]),
|
|
8742
|
-
project(c1[0], c1[1]),
|
|
8743
|
-
project(c2[0], c2[1])
|
|
8744
|
-
];
|
|
8745
|
-
const shaftSegments = [];
|
|
8746
|
-
const headPolygons = [];
|
|
8747
|
-
const addLeg = (origin, tip, opposite) => {
|
|
8748
|
-
const v = vecSub(tip, origin);
|
|
8749
|
-
const totalLen = vecMag(v);
|
|
8750
|
-
if (totalLen < 1e-6) return;
|
|
8751
|
-
const u = vecNorm(v);
|
|
8752
|
-
let n = [-u[1], u[0]];
|
|
8753
|
-
const toOpp = vecSub(opposite, origin);
|
|
8754
|
-
if (vecDot(n, toOpp) > 0) n = vecScale(n, -1);
|
|
8755
|
-
const offset = totalLen * safeZigzagAmplitudeRatio;
|
|
8756
|
-
const ptA = vecAdd(vecAdd(origin, vecScale(v, safeZigzagStart)), vecScale(n, offset));
|
|
8757
|
-
const ptB = vecAdd(vecAdd(origin, vecScale(v, safeZigzagEnd)), vecScale(n, -offset));
|
|
8758
|
-
const fv = vecSub(tip, ptB);
|
|
8759
|
-
const fLen = vecMag(fv);
|
|
8760
|
-
let fu = vecNorm(fv);
|
|
8761
|
-
if (fLen < 1e-6) fu = u;
|
|
8762
|
-
const headLen = totalLen * safeArrowheadLengthRatio;
|
|
8763
|
-
const pBase = vecSub(tip, vecScale(fu, headLen));
|
|
8764
|
-
const wingWidth = headLen * safeArrowheadWidthRatio;
|
|
8765
|
-
const perp = [-fu[1], fu[0]];
|
|
8766
|
-
const pWing1 = vecAdd(pBase, vecScale(perp, wingWidth));
|
|
8767
|
-
const pWing2 = vecSub(pBase, vecScale(perp, wingWidth));
|
|
8768
|
-
shaftSegments.push([
|
|
8769
|
-
origin,
|
|
8770
|
-
ptA,
|
|
8771
|
-
ptB,
|
|
8772
|
-
pBase
|
|
8773
|
-
].map((p) => unproject(p[0], p[1])));
|
|
8774
|
-
headPolygons.push([[
|
|
8775
|
-
tip,
|
|
8776
|
-
pWing1,
|
|
8777
|
-
pWing2,
|
|
8778
|
-
tip
|
|
8779
|
-
].map((p) => unproject(p[0], p[1]))]);
|
|
8780
|
-
};
|
|
8781
|
-
addLeg(p1, p2, p3);
|
|
8782
|
-
addLeg(p1, p3, p2);
|
|
8783
|
-
const features = [];
|
|
8784
|
-
if (shaftSegments.length > 0) features.push({
|
|
8785
|
-
type: "Feature",
|
|
8786
|
-
properties: { part: "shaft" },
|
|
8787
|
-
geometry: {
|
|
8788
|
-
type: "MultiLineString",
|
|
8789
|
-
coordinates: shaftSegments
|
|
8790
|
-
}
|
|
8791
|
-
});
|
|
8792
|
-
if (headPolygons.length > 0) features.push({
|
|
8793
|
-
type: "Feature",
|
|
8794
|
-
properties: {
|
|
8795
|
-
part: "head",
|
|
8796
|
-
fill: true
|
|
8797
|
-
},
|
|
8798
|
-
geometry: {
|
|
8799
|
-
type: "MultiPolygon",
|
|
8800
|
-
coordinates: headPolygons
|
|
8801
|
-
}
|
|
8802
|
-
});
|
|
8803
|
-
return {
|
|
8804
|
-
type: "FeatureCollection",
|
|
8805
|
-
features
|
|
8806
|
-
};
|
|
8807
|
-
}
|
|
8808
|
-
const SEARCH_AREA = defineControlMeasure({
|
|
8809
|
-
metadata: SEARCH_AREA_METADATA,
|
|
8810
|
-
generator: (controlPoints, options) => createSearchArea(asThreePoints(controlPoints), options),
|
|
8811
|
-
defaultOptions: DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
8812
|
-
rule: searchAreaDrawRule
|
|
8813
|
-
});
|
|
8814
|
-
//#endregion
|
|
8815
10054
|
//#region src/generators/cm15-maneuver-areas/strongPoint.ts
|
|
8816
10055
|
const DEFAULT_SMOOTH_RESOLUTION = 16;
|
|
8817
10056
|
/** Default options for the Strong Point control measure. */
|
|
@@ -9371,6 +10610,37 @@ function rotate(vector, angle) {
|
|
|
9371
10610
|
const sin = Math.sin(angle);
|
|
9372
10611
|
return [vector[0] * cos - vector[1] * sin, vector[0] * sin + vector[1] * cos];
|
|
9373
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
|
+
}
|
|
9374
10644
|
//#endregion
|
|
9375
10645
|
//#region src/registry.ts
|
|
9376
10646
|
/**
|
|
@@ -9389,12 +10659,16 @@ const DEFINITIONS = {
|
|
|
9389
10659
|
"battle-position": BATTLE_POSITION,
|
|
9390
10660
|
"strong-point": STRONG_POINT,
|
|
9391
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,
|
|
9392
10665
|
"principal-direction-of-fire": PRINCIPAL_DIRECTION_OF_FIRE,
|
|
9393
10666
|
"final-protective-fire-left": FINAL_PROTECTIVE_FIRE_LEFT,
|
|
9394
10667
|
"final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
|
|
9395
10668
|
"search-area": SEARCH_AREA,
|
|
9396
10669
|
encirclement: ENCIRCLEMENT,
|
|
9397
10670
|
area: AREA,
|
|
10671
|
+
"area-defense": AREA_DEFENSE,
|
|
9398
10672
|
"assembly-area": ASSEMBLY_AREA,
|
|
9399
10673
|
"joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
|
|
9400
10674
|
"submarine-action-area": SUBMARINE_ACTION_AREA,
|
|
@@ -9434,22 +10708,41 @@ const DEFINITIONS = {
|
|
|
9434
10708
|
bypass: BYPASS,
|
|
9435
10709
|
canalize: CANALIZE,
|
|
9436
10710
|
clear: CLEAR,
|
|
10711
|
+
cover: COVER,
|
|
9437
10712
|
delay: DELAY,
|
|
10713
|
+
guard: GUARD,
|
|
9438
10714
|
isolate: ISOLATE,
|
|
10715
|
+
screen: SCREEN,
|
|
9439
10716
|
"antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
|
|
9440
10717
|
"antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
|
|
9441
10718
|
"antitank-wall": ANTITANK_WALL,
|
|
9442
10719
|
"fortified-line": FORTIFIED_LINE,
|
|
9443
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
|
+
}),
|
|
9444
10742
|
block: BLOCK,
|
|
9445
10743
|
disrupt: DISRUPT,
|
|
9446
10744
|
fix: FIX,
|
|
9447
|
-
turn:
|
|
9448
|
-
metadata: TURN_METADATA,
|
|
9449
|
-
generator: createTurn,
|
|
9450
|
-
defaultOptions: DEFAULT_TURN_OPTIONS,
|
|
9451
|
-
rule: turnDrawRule
|
|
9452
|
-
}),
|
|
10745
|
+
turn: TURN,
|
|
9453
10746
|
"obstacle-bypass-easy": OBSTACLE_BYPASS_EASY,
|
|
9454
10747
|
"obstacle-bypass-difficult": OBSTACLE_BYPASS_DIFFICULT,
|
|
9455
10748
|
"obstacle-bypass-impossible": OBSTACLE_BYPASS_IMPOSSIBLE
|
|
@@ -9691,7 +10984,16 @@ function normalizeTextAmplifiers(input) {
|
|
|
9691
10984
|
* `properties.style` when non-empty.
|
|
9692
10985
|
*/
|
|
9693
10986
|
function renderControlMeasure(cm, opts) {
|
|
9694
|
-
const
|
|
10987
|
+
const raw = dispatchControlMeasure(cm, opts);
|
|
10988
|
+
const generatedLabelOrdinals = /* @__PURE__ */ new Map();
|
|
10989
|
+
const features = raw.features.map((feature, index) => {
|
|
10990
|
+
const sourceProps = feature.properties ?? {};
|
|
10991
|
+
const part = typeof sourceProps.part === "string" && sourceProps.part.length > 0 ? sourceProps.part : "main";
|
|
10992
|
+
const isGeneratedLabel = sourceProps.labelPlacement !== false && !(typeof sourceProps.amplifierField === "string" && canonicalTextAmplifierKey(sourceProps.amplifierField)) && !(typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) && typeof sourceProps.text === "string" && feature.geometry.type === "Point";
|
|
10993
|
+
const generatedLabelOrdinal = isGeneratedLabel ? generatedLabelOrdinals.get(part) ?? 0 : index;
|
|
10994
|
+
if (isGeneratedLabel) generatedLabelOrdinals.set(part, generatedLabelOrdinal + 1);
|
|
10995
|
+
return normalizeFeature(cm.id, feature, index, generatedLabelOrdinal, cm.style, opts?.graphicsStyle, cm.amplifierPlacements);
|
|
10996
|
+
});
|
|
9695
10997
|
const bbox = computeBBox(features);
|
|
9696
10998
|
return {
|
|
9697
10999
|
type: "FeatureCollection",
|
|
@@ -9723,7 +11025,20 @@ function validateInputContract(points, metadata, mode) {
|
|
|
9723
11025
|
if (points.some((point) => !isValidPosition(point))) return reportValidationIssue(mode, `"${metadata.id}" control points must contain finite [lon, lat] values.`);
|
|
9724
11026
|
return true;
|
|
9725
11027
|
}
|
|
9726
|
-
|
|
11028
|
+
/**
|
|
11029
|
+
* The `amplifierPlacements` key that repositions this feature, if any: the
|
|
11030
|
+
* canonical amplifier field when the generator declared one, an explicit
|
|
11031
|
+
* `labelPlacementKey`, or a synthesized `label:<part>:<ordinal>` key for any
|
|
11032
|
+
* other point label — unless the generator opted out via
|
|
11033
|
+
* `labelPlacement: false`.
|
|
11034
|
+
*/
|
|
11035
|
+
function resolveLabelPlacementKey(sourceProps, amplifierField, text, geometryType, part, generatedLabelOrdinal) {
|
|
11036
|
+
if (sourceProps.labelPlacement === false) return void 0;
|
|
11037
|
+
if (amplifierField) return amplifierField;
|
|
11038
|
+
if (typeof sourceProps.labelPlacementKey === "string" && sourceProps.labelPlacementKey.length > 0) return sourceProps.labelPlacementKey;
|
|
11039
|
+
if (text !== void 0 && geometryType === "Point") return `label:${part}:${generatedLabelOrdinal}`;
|
|
11040
|
+
}
|
|
11041
|
+
function normalizeFeature(id, feature, index, generatedLabelOrdinal, measureStyle, graphicsStyle, amplifierPlacements) {
|
|
9727
11042
|
const sourceProps = feature.properties ?? {};
|
|
9728
11043
|
const part = typeof sourceProps.part === "string" && sourceProps.part.length > 0 ? sourceProps.part : "main";
|
|
9729
11044
|
const generatorStyle = typeof sourceProps.style === "object" && sourceProps.style !== null ? sourceProps.style : void 0;
|
|
@@ -9734,6 +11049,13 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
9734
11049
|
const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
|
|
9735
11050
|
const textSizeMeters = typeof sourceProps.textSizeMeters === "number" ? sourceProps.textSizeMeters : void 0;
|
|
9736
11051
|
const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
|
|
11052
|
+
const amplifierField = typeof sourceProps.amplifierField === "string" ? canonicalTextAmplifierKey(sourceProps.amplifierField) : void 0;
|
|
11053
|
+
const labelPlacementKey = resolveLabelPlacementKey(sourceProps, amplifierField, text, feature.geometry.type, part, generatedLabelOrdinal);
|
|
11054
|
+
const placement = labelPlacementKey ? amplifierPlacements?.[labelPlacementKey] : void 0;
|
|
11055
|
+
const geometry = placement && isValidPosition(placement) && feature.geometry.type === "Point" ? {
|
|
11056
|
+
...feature.geometry,
|
|
11057
|
+
coordinates: [...placement]
|
|
11058
|
+
} : feature.geometry;
|
|
9737
11059
|
return {
|
|
9738
11060
|
type: "Feature",
|
|
9739
11061
|
id: `${id}:${part}:${index}`,
|
|
@@ -9744,10 +11066,12 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
9744
11066
|
...text !== void 0 ? { text } : {},
|
|
9745
11067
|
...rotation !== void 0 ? { rotation } : {},
|
|
9746
11068
|
...textAnchor !== void 0 ? { textAnchor } : {},
|
|
11069
|
+
...amplifierField !== void 0 ? { amplifierField } : {},
|
|
11070
|
+
...labelPlacementKey !== void 0 ? { labelPlacementKey } : {},
|
|
9747
11071
|
...textSizePixels !== void 0 ? { textSizePixels } : {},
|
|
9748
11072
|
...textSizeMeters !== void 0 ? { textSizeMeters } : {}
|
|
9749
11073
|
},
|
|
9750
|
-
geometry
|
|
11074
|
+
geometry
|
|
9751
11075
|
};
|
|
9752
11076
|
}
|
|
9753
11077
|
function computeBBox(features) {
|
|
@@ -9796,4 +11120,4 @@ function assertNever(value) {
|
|
|
9796
11120
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
9797
11121
|
}
|
|
9798
11122
|
//#endregion
|
|
9799
|
-
export {
|
|
11123
|
+
export { DEFAULT_ENGINEER_WORK_LINE_OPTIONS as $, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as A, blockDrawRule as At, DEFAULT_COVER_OPTIONS as B, unproject as Bt, DEFAULT_MAIN_ATTACK_OPTIONS as C, turnDrawRule as Ct, DEFAULT_FRONTAL_ATTACK_OPTIONS as D, point12DrawRule as Dt, DEFAULT_SCREEN_OPTIONS as E, attackByFireDrawRule as Et, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as F, snapToMidpointPerpendicular as Ft, DEFAULT_GENERIC_LINE_OPTIONS as G, DEFAULT_CLEAR_OPTIONS as H, getMetersPerPixel as Ht, DEFAULT_ENCIRCLEMENT_OPTIONS as I, createBaselineFrame as It, DEFAULT_CANALIZE_OPTIONS as J, DEFAULT_GENERIC_CIRCLE_OPTIONS as K, DEFAULT_DISRUPT_OPTIONS as L, calculateMetrics as Lt, DEFAULT_PHASE_LINE_OPTIONS as M, createMidpointPerpendicularDrawRule as Mt, DEFAULT_FLOT_OPTIONS as N, getMidpointPerpendicularSignedDistance as Nt, DEFAULT_FORTIFIED_AREA_OPTIONS as O, centerRadiusDrawRule as Ot, DEFAULT_FIX_OPTIONS as P, pointOnMidpointPerpendicularAxis as Pt, DEFAULT_GENERIC_C2_LINE_OPTIONS as Q, DEFAULT_GUARD_OPTIONS as R, computeInitialWidthPoint as Rt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as S, line23DrawRule as St, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as T, ambushDrawRule as Tt, DEFAULT_GENERIC_RECTANGLE_OPTIONS as U, roundToFixed as Ut, DEFAULT_TACTICAL_ARROW_OPTIONS as V, EPSILON as Vt, DEFAULT_GENERIC_POLYGON_OPTIONS as W, DEFAULT_BREACH_OPTIONS as X, DEFAULT_BYPASS_OPTIONS as Y, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Z, DEFAULT_STRONG_POINT_OPTIONS as _, rectangleDrawRule as _t, resolveStyleHints as a, DEFAULT_ATTACK_HELICOPTER_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as b, line26DrawRule as bt, DEFINITIONS as c, DEFAULT_ANTITANK_DITCH_OPTIONS as ct, getDefaultOptions as d, DEFAULT_AREA_DEFENSE_OPTIONS as dt, DEFAULT_LIGHT_LINE_OPTIONS as et, listControlMeasureMetadata as f, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as ft, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as g, DEFAULT_AIRBORNE_ATTACK_OPTIONS as gt, DEFAULT_SUPPORTING_ATTACK_OPTIONS as h, DEFAULT_AMBUSH_OPTIONS as ht, normalizeTextAmplifiers as i, DEFAULT_BATTLE_POSITION_OPTIONS as it, DEFAULT_HANDOVER_LINE_OPTIONS as j, computeDefaultMidpointPerpendicularPoint as jt, DEFAULT_FORTIFIED_LINE_OPTIONS as k, disruptDrawRule as kt, getControlMeasureMetadata as l, DEFAULT_ASSEMBLY_AREA_OPTIONS as lt, DEFAULT_TURN_OPTIONS as m, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as mt, TEXT_AMPLIFIER_FIELDS as n, DEFAULT_BLOCK_ARROW_OPTIONS as nt, CONTROL_MEASURE_IDS as o, DEFAULT_ATTACK_BY_FIRE_OPTIONS as ot, DEFAULT_TURNING_MOVEMENT_OPTIONS as p, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as pt, DEFAULT_CLASSIC_ARROW_OPTIONS as q, canonicalTextAmplifierKey as r, DEFAULT_BLOCK_OPTIONS as rt, CONTROL_MEASURE_METADATA as s, DEFAULT_ANTITANK_WALL_OPTIONS as st, renderControlMeasure as t, DEFAULT_BOUNDARY_OPTIONS as tt, getControlMeasureMetadataByValue as u, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as ut, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as v, axis1DrawRule as vt, DEFAULT_LANDING_ZONE_OPTIONS as w, line1DrawRule as wt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as x, line24DrawRule as xt, DEFAULT_PICKUP_ZONE_OPTIONS as y, supportByFireDrawRule as yt, DEFAULT_DELAY_OPTIONS as z, project as zt };
|