@orbat-mapper/control-measures 0.2.0-alpha.0 → 0.2.0-alpha.2
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/README.md +90 -34
- package/dist/index-D7uBPw7l.d.mts +1653 -0
- package/dist/index.d.mts +1 -1353
- package/dist/index.mjs +1 -4964
- package/dist/preview/index.d.mts +45 -0
- package/dist/preview/index.mjs +211 -0
- package/dist/renderControlMeasure-C7pY-TcL.mjs +6465 -0
- package/media/airborne-attack.svg +1 -0
- package/media/ambush.svg +1 -0
- package/media/antitank-ditch-completed.svg +1 -0
- package/media/antitank-ditch-under-construction.svg +1 -0
- package/media/antitank-wall.svg +1 -0
- package/media/attack-by-fire.svg +1 -0
- package/media/attack-helicopter.svg +1 -0
- package/media/battle-position.svg +1 -0
- package/media/block-arrow.svg +1 -0
- package/media/block-mission-task.svg +1 -0
- package/media/block.svg +1 -0
- package/media/boundary.svg +1 -0
- package/media/breach.svg +1 -0
- package/media/bypass.svg +1 -0
- package/media/canalize.svg +1 -0
- package/media/classic-arrow.svg +1 -0
- package/media/clear.svg +1 -0
- package/media/delay.svg +1 -0
- package/media/disrupt.svg +1 -0
- package/media/final-protective-fire-left.svg +1 -0
- package/media/final-protective-fire-right.svg +1 -0
- package/media/fix.svg +1 -0
- package/media/flot.svg +1 -0
- package/media/fortified-area.svg +1 -0
- package/media/fortified-line.svg +1 -0
- package/media/isolate.svg +1 -0
- package/media/main-attack.svg +1 -0
- package/media/obstacle-bypass-difficult.svg +1 -0
- package/media/obstacle-bypass-easy.svg +1 -0
- package/media/obstacle-bypass-impossible.svg +1 -0
- package/media/principal-direction-of-fire.svg +1 -0
- package/media/search-area.svg +1 -0
- package/media/support-by-fire.svg +1 -0
- package/media/supporting-attack.svg +1 -0
- package/media/turn.svg +1 -0
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4967 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Small value used to avoid division by zero and floating-point precision issues.
|
|
4
|
-
*/
|
|
5
|
-
const EPSILON = 1e-6;
|
|
6
|
-
/**
|
|
7
|
-
* Clamps a value to the range [0, 1].
|
|
8
|
-
*/
|
|
9
|
-
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
10
|
-
/**
|
|
11
|
-
* Clamps a value to the range [min, max].
|
|
12
|
-
*/
|
|
13
|
-
const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
|
|
14
|
-
/**
|
|
15
|
-
* Type guard for finite numbers.
|
|
16
|
-
*/
|
|
17
|
-
const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
18
|
-
/**
|
|
19
|
-
* Validates that a position array contains at least two finite coordinates.
|
|
20
|
-
*/
|
|
21
|
-
const isValidPosition = (p) => Array.isArray(p) && p.length >= 2 && isFiniteNumber(p[0]) && isFiniteNumber(p[1]);
|
|
22
|
-
/**
|
|
23
|
-
* Rounds a number to a specified number of decimal places.
|
|
24
|
-
*/
|
|
25
|
-
const roundToFixed = (v, precision = 6) => {
|
|
26
|
-
const m = Math.pow(10, precision);
|
|
27
|
-
return Math.round(v * m) / m;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Earth's circumference at the equator in meters.
|
|
31
|
-
*/
|
|
32
|
-
const EARTH_CIRCUMFERENCE = 40075016.686;
|
|
33
|
-
/**
|
|
34
|
-
* Standard tile size in pixels for Web Mercator projection.
|
|
35
|
-
*/
|
|
36
|
-
const TILE_SIZE = 256;
|
|
37
|
-
/**
|
|
38
|
-
* Calculates the meters-per-pixel ratio at a given latitude and zoom level.
|
|
39
|
-
*
|
|
40
|
-
* This is essential for maintaining consistent visual sizes on the map
|
|
41
|
-
* regardless of zoom level. The formula accounts for:
|
|
42
|
-
* - The Web Mercator projection's tile-based structure
|
|
43
|
-
* - The latitude-dependent scale distortion in Mercator projection
|
|
44
|
-
*
|
|
45
|
-
* @param latitude - The latitude in degrees (affects scale due to projection)
|
|
46
|
-
* @param zoomLevel - The current map zoom level
|
|
47
|
-
* @returns The number of meters represented by one pixel at the given location and zoom
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```typescript
|
|
51
|
-
* // At zoom 10, latitude 45°
|
|
52
|
-
* const mpp = getMetersPerPixel(45, 10);
|
|
53
|
-
* // To get a 20-pixel radius in meters:
|
|
54
|
-
* const radiusMeters = 20 * mpp;
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
const getMetersPerPixel = (latitude, zoomLevel) => {
|
|
58
|
-
const metersPerPixelAtEquator = EARTH_CIRCUMFERENCE / (TILE_SIZE * Math.pow(2, zoomLevel));
|
|
59
|
-
const latitudeRadians = latitude * Math.PI / 180;
|
|
60
|
-
return metersPerPixelAtEquator * Math.cos(latitudeRadians);
|
|
61
|
-
};
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/projection.ts
|
|
64
|
-
/**
|
|
65
|
-
* Project WGS84 (Lon/Lat) to Web Mercator (Meters).
|
|
66
|
-
*/
|
|
67
|
-
const project = (lon, lat) => {
|
|
68
|
-
const x = lon * 20037508.34 / 180;
|
|
69
|
-
let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
|
|
70
|
-
y = y * 20037508.34 / 180;
|
|
71
|
-
return [x, y];
|
|
72
|
-
};
|
|
73
|
-
/**
|
|
74
|
-
* Unproject Web Mercator (Meters) back to WGS84 (Lon/Lat).
|
|
75
|
-
*/
|
|
76
|
-
const unproject = (x, y) => {
|
|
77
|
-
const lon = x * 180 / 20037508.34;
|
|
78
|
-
let lat = y * 180 / 20037508.34;
|
|
79
|
-
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);
|
|
80
|
-
return [roundToFixed(lon), roundToFixed(lat)];
|
|
81
|
-
};
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/internal/vector-utils.ts
|
|
84
|
-
/**
|
|
85
|
-
* Offsets a polyline using Miter Joins or Round Joins.
|
|
86
|
-
*/
|
|
87
|
-
function offsetPolyline(points, offset, rounded = false, segments = 5) {
|
|
88
|
-
if (points.length < 2) return points;
|
|
89
|
-
const result = [];
|
|
90
|
-
const N = points.length;
|
|
91
|
-
for (let i = 0; i < N; i++) {
|
|
92
|
-
const p = points[i];
|
|
93
|
-
if (!p) continue;
|
|
94
|
-
if (i === 0) {
|
|
95
|
-
const next = points[i + 1];
|
|
96
|
-
if (next) {
|
|
97
|
-
const dir = vecNorm(vecSub(next, p));
|
|
98
|
-
const normal = [-dir[1], dir[0]];
|
|
99
|
-
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
100
|
-
}
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (i === N - 1) {
|
|
104
|
-
const prev = points[i - 1];
|
|
105
|
-
if (prev) {
|
|
106
|
-
const dir = vecNorm(vecSub(p, prev));
|
|
107
|
-
const normal = [-dir[1], dir[0]];
|
|
108
|
-
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
109
|
-
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const prev = points[i - 1];
|
|
113
|
-
const next = points[i + 1];
|
|
114
|
-
if (!prev || !next) continue;
|
|
115
|
-
const v1 = vecNorm(vecSub(p, prev));
|
|
116
|
-
const v2 = vecNorm(vecSub(next, p));
|
|
117
|
-
const n1 = [-v1[1], v1[0]];
|
|
118
|
-
const n2 = [-v2[1], v2[0]];
|
|
119
|
-
const cross = v1[0] * v2[1] - v1[1] * v2[0];
|
|
120
|
-
if (rounded && (offset > 0 && cross < -1e-6 || offset < 0 && cross > 1e-6)) {
|
|
121
|
-
const angle1 = Math.atan2(n1[1], n1[0]);
|
|
122
|
-
let diff = Math.atan2(n2[1], n2[0]) - angle1;
|
|
123
|
-
if (offset > 0) {
|
|
124
|
-
while (diff > EPSILON) diff -= 2 * Math.PI;
|
|
125
|
-
while (diff < -2 * Math.PI) diff += 2 * Math.PI;
|
|
126
|
-
} else {
|
|
127
|
-
while (diff < -1e-6) diff += 2 * Math.PI;
|
|
128
|
-
while (diff > 2 * Math.PI) diff -= 2 * Math.PI;
|
|
129
|
-
}
|
|
130
|
-
const segmentCount = Math.max(1, segments);
|
|
131
|
-
for (let j = 0; j <= segmentCount; j++) {
|
|
132
|
-
const t = j / segmentCount;
|
|
133
|
-
const theta = angle1 + diff * t;
|
|
134
|
-
const nx = Math.cos(theta);
|
|
135
|
-
const ny = Math.sin(theta);
|
|
136
|
-
result.push([p[0] + nx * offset, p[1] + ny * offset]);
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
const tangent = vecNorm(vecAdd(v1, v2));
|
|
140
|
-
const miter = [-tangent[1], tangent[0]];
|
|
141
|
-
const dot = vecDot(miter, n1);
|
|
142
|
-
const miterLimit = 3;
|
|
143
|
-
const scaleFactor = Math.abs(dot) < 1e-6 ? 1 : 1 / dot;
|
|
144
|
-
const normal = vecScale(miter, Math.min(scaleFactor, miterLimit));
|
|
145
|
-
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
function vecAdd(a, b) {
|
|
151
|
-
return [a[0] + b[0], a[1] + b[1]];
|
|
152
|
-
}
|
|
153
|
-
function vecSub(a, b) {
|
|
154
|
-
return [a[0] - b[0], a[1] - b[1]];
|
|
155
|
-
}
|
|
156
|
-
function vecScale(v, s) {
|
|
157
|
-
return [v[0] * s, v[1] * s];
|
|
158
|
-
}
|
|
159
|
-
function vecDot(a, b) {
|
|
160
|
-
return a[0] * b[0] + a[1] * b[1];
|
|
161
|
-
}
|
|
162
|
-
function vecMag(v) {
|
|
163
|
-
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
|
164
|
-
}
|
|
165
|
-
function vecNorm(v) {
|
|
166
|
-
const m = vecMag(v);
|
|
167
|
-
return m === 0 ? [0, 0] : [v[0] / m, v[1] / m];
|
|
168
|
-
}
|
|
169
|
-
function pointToPointDistance(p1, p2) {
|
|
170
|
-
const dx = p2[0] - p1[0];
|
|
171
|
-
const dy = p2[1] - p1[1];
|
|
172
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
173
|
-
}
|
|
174
|
-
function pointToInfiniteLineDistance(p, a, b) {
|
|
175
|
-
const ab = vecSub(b, a);
|
|
176
|
-
const ap = vecSub(p, a);
|
|
177
|
-
const abLenSq = ab[0] * ab[0] + ab[1] * ab[1];
|
|
178
|
-
if (abLenSq < 1e-6) return pointToPointDistance(p, a);
|
|
179
|
-
const t = vecDot(ap, ab) / abLenSq;
|
|
180
|
-
return pointToPointDistance(p, [a[0] + t * ab[0], a[1] + t * ab[1]]);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Calculates intersection of two line segments p1-p2 and p3-p4.
|
|
184
|
-
* Returns null if parallel. Assuming they cross as per design.
|
|
185
|
-
*/
|
|
186
|
-
function lineIntersection(p1, p2, p3, p4) {
|
|
187
|
-
const x1 = p1[0], y1 = p1[1];
|
|
188
|
-
const x2 = p2[0], y2 = p2[1];
|
|
189
|
-
const x3 = p3[0], y3 = p3[1];
|
|
190
|
-
const x4 = p4[0], y4 = p4[1];
|
|
191
|
-
const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
|
192
|
-
if (Math.abs(denom) < 1e-6) return null;
|
|
193
|
-
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
|
|
194
|
-
return [x1 + ua * (x2 - x1), y1 + ua * (y2 - y1)];
|
|
195
|
-
}
|
|
196
|
-
//#endregion
|
|
197
|
-
//#region src/attack-utils.ts
|
|
198
|
-
const DEFAULT_SHAFT_WIDTH_RATIO = .6;
|
|
199
|
-
const SHAFT_MIN_RATIO = .1;
|
|
200
|
-
const SHAFT_MAX_RATIO = .9;
|
|
201
|
-
/**
|
|
202
|
-
* Processes input coordinates and options to generate the core symbol geometry.
|
|
203
|
-
* This handles validation, default values, projection, and geometry calculation.
|
|
204
|
-
*/
|
|
205
|
-
function processAttackGeometry(coordinates, options = {}) {
|
|
206
|
-
const shaftRatio = clamp(options.shaftWidthRatio ?? .6, SHAFT_MIN_RATIO, SHAFT_MAX_RATIO);
|
|
207
|
-
const roundedBends = options.roundedBends ?? false;
|
|
208
|
-
const bendSegments = options.bendSegments ?? 5;
|
|
209
|
-
const points = coordinates.map((c) => project(c[0], c[1]));
|
|
210
|
-
const numPoints = points.length;
|
|
211
|
-
const ptTip = points[0];
|
|
212
|
-
const ptWidth = points[numPoints - 1];
|
|
213
|
-
const spinePoints = [];
|
|
214
|
-
for (let i = numPoints - 2; i >= 1; i--) {
|
|
215
|
-
const p = points[i];
|
|
216
|
-
if (p) spinePoints.push(p);
|
|
217
|
-
}
|
|
218
|
-
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, roundedBends, bendSegments) };
|
|
219
|
-
}
|
|
220
|
-
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, roundedBends, bendSegments) {
|
|
221
|
-
const ptNeck = spine[spine.length - 1];
|
|
222
|
-
if (!ptNeck) return null;
|
|
223
|
-
const tipDir = vecNorm(vecSub(ptTip, ptNeck));
|
|
224
|
-
if (vecMag(tipDir) < 1e-6) return null;
|
|
225
|
-
const headHalfWidth = pointToInfiniteLineDistance(ptWidth, ptNeck, ptTip);
|
|
226
|
-
const vecTipToWidth = vecSub(ptTip, ptWidth);
|
|
227
|
-
const maxHeadLength = Math.max(0, vecMag(vecSub(ptTip, ptNeck)) - .1);
|
|
228
|
-
const headLength = clamp(vecDot(vecTipToWidth, tipDir), EPSILON, maxHeadLength);
|
|
229
|
-
const ptBase = vecSub(ptTip, vecScale(tipDir, headLength));
|
|
230
|
-
const shaftHalfWidth = headHalfWidth * shaftRatio;
|
|
231
|
-
const perpDir = [-tipDir[1], tipDir[0]];
|
|
232
|
-
const outerLeft = vecAdd(ptBase, vecScale(perpDir, headHalfWidth));
|
|
233
|
-
const outerRight = vecAdd(ptBase, vecScale(perpDir, -headHalfWidth));
|
|
234
|
-
const innerLeft = vecAdd(ptBase, vecScale(perpDir, shaftHalfWidth));
|
|
235
|
-
const innerRight = vecAdd(ptBase, vecScale(perpDir, -shaftHalfWidth));
|
|
236
|
-
const notchDepth = headLength * shaftRatio;
|
|
237
|
-
const innerTip = vecAdd(ptBase, vecScale(tipDir, Math.min(notchDepth, headLength - EPSILON)));
|
|
238
|
-
const shaftEndCenter = ptBase;
|
|
239
|
-
const headRing = [
|
|
240
|
-
outerLeft,
|
|
241
|
-
ptTip,
|
|
242
|
-
outerRight,
|
|
243
|
-
innerRight,
|
|
244
|
-
innerTip,
|
|
245
|
-
innerLeft,
|
|
246
|
-
outerLeft
|
|
247
|
-
];
|
|
248
|
-
const fullSpine = [...spine, shaftEndCenter];
|
|
249
|
-
return {
|
|
250
|
-
shaftLeft: offsetPolyline(fullSpine, shaftHalfWidth, roundedBends, bendSegments),
|
|
251
|
-
shaftRight: offsetPolyline(fullSpine, -shaftHalfWidth, roundedBends, bendSegments),
|
|
252
|
-
headRing,
|
|
253
|
-
ptTip,
|
|
254
|
-
ptNeck,
|
|
255
|
-
ptBase
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Calculates the relative metrics (longitudinal and lateral) of the width point
|
|
260
|
-
* relative to the Tip-Neck axis.
|
|
261
|
-
*
|
|
262
|
-
* @param pts - Array of positions [Tip, Neck, ..., WidthPoint]
|
|
263
|
-
* @returns { longitudinal: number, lateral: number } | null
|
|
264
|
-
*/
|
|
265
|
-
function calculateMetrics(pts) {
|
|
266
|
-
const tip = pts[0];
|
|
267
|
-
const neck = pts[1];
|
|
268
|
-
const widthPt = pts[pts.length - 1];
|
|
269
|
-
if (!tip || !neck || !widthPt) return null;
|
|
270
|
-
const tipM = project(tip[0], tip[1]);
|
|
271
|
-
const neckM = project(neck[0], neck[1]);
|
|
272
|
-
const widthM = project(widthPt[0], widthPt[1]);
|
|
273
|
-
const tx = tipM[0];
|
|
274
|
-
const ty = tipM[1];
|
|
275
|
-
const nx = neckM[0];
|
|
276
|
-
const ny = neckM[1];
|
|
277
|
-
const wx = widthM[0];
|
|
278
|
-
const wy = widthM[1];
|
|
279
|
-
const dx = tx - nx;
|
|
280
|
-
const dy = ty - ny;
|
|
281
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
282
|
-
if (len < 1e-6) return null;
|
|
283
|
-
const ax = dx / len;
|
|
284
|
-
const ay = dy / len;
|
|
285
|
-
const px = -ay;
|
|
286
|
-
const py = ax;
|
|
287
|
-
const vx = wx - tx;
|
|
288
|
-
const vy = wy - ty;
|
|
289
|
-
return {
|
|
290
|
-
longitudinal: vx * ax + vy * ay,
|
|
291
|
-
lateral: vx * px + vy * py
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Computes an initial Width Point based on the Tip and Neck positions.
|
|
296
|
-
* It places the point perpendicular to the neck at a default distance relative to the length.
|
|
297
|
-
*
|
|
298
|
-
* @param tip - Tip position (Lon/Lat)
|
|
299
|
-
* @param neck - Neck position (Lon/Lat)
|
|
300
|
-
* @returns Width Point position (Lon/Lat)
|
|
301
|
-
*/
|
|
302
|
-
function computeInitialWidthPoint(tip, neck) {
|
|
303
|
-
const tipM = project(tip[0], tip[1]);
|
|
304
|
-
const neckM = project(neck[0], neck[1]);
|
|
305
|
-
const tx = tipM[0];
|
|
306
|
-
const ty = tipM[1];
|
|
307
|
-
const nx = neckM[0];
|
|
308
|
-
const ny = neckM[1];
|
|
309
|
-
const dx = tx - nx;
|
|
310
|
-
const dy = ty - ny;
|
|
311
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
312
|
-
if (len < 1e-6) return unproject(nx + 100, ny);
|
|
313
|
-
const ax = dx / len;
|
|
314
|
-
const ay = dy / len;
|
|
315
|
-
const widthRatio = .35;
|
|
316
|
-
const recessionRatio = .25;
|
|
317
|
-
const px = -ay;
|
|
318
|
-
const py = ax;
|
|
319
|
-
return unproject(tx - dx * recessionRatio + len * widthRatio * px, ty - dy * recessionRatio + len * widthRatio * py);
|
|
320
|
-
}
|
|
321
|
-
//#endregion
|
|
322
|
-
//#region src/define.ts
|
|
323
|
-
/**
|
|
324
|
-
* Identity helper that infers a definition's literal `Id` and precise generator
|
|
325
|
-
* type `G`, so the registry's `typeof DEFINITIONS` carries each measure's exact
|
|
326
|
-
* options type. Internal authoring API — not part of the published surface.
|
|
327
|
-
*/
|
|
328
|
-
function defineControlMeasure(definition) {
|
|
329
|
-
return definition;
|
|
330
|
-
}
|
|
331
|
-
//#endregion
|
|
332
|
-
//#region src/draw-rules/baseline-frame.ts
|
|
333
|
-
function createBaselineFrame(p1, p2, options = {}) {
|
|
334
|
-
return createProjectedBaselineFrame(project(p1[0], p1[1]), project(p2[0], p2[1]), options);
|
|
335
|
-
}
|
|
336
|
-
function createProjectedBaselineFrame(p1, p2, options = {}) {
|
|
337
|
-
const baseline = vecSub(p2, p1);
|
|
338
|
-
const length = vecMag(baseline);
|
|
339
|
-
if (length < 1e-6) return null;
|
|
340
|
-
const direction = vecNorm(baseline);
|
|
341
|
-
const normalSign = options.normal === "left" ? -1 : 1;
|
|
342
|
-
const normal = vecScale([direction[1], -direction[0]], normalSign);
|
|
343
|
-
const origin = getOrigin(p1, p2, options.origin ?? "midpoint");
|
|
344
|
-
return {
|
|
345
|
-
p1,
|
|
346
|
-
p2,
|
|
347
|
-
origin,
|
|
348
|
-
direction,
|
|
349
|
-
normal,
|
|
350
|
-
length,
|
|
351
|
-
signedNormalDistance(point) {
|
|
352
|
-
return vecDot(vecSub(project(point[0], point[1]), origin), normal);
|
|
353
|
-
},
|
|
354
|
-
pointAtNormalDistance(distance) {
|
|
355
|
-
if (!Number.isFinite(distance)) return null;
|
|
356
|
-
const point = vecAdd(origin, vecScale(normal, distance));
|
|
357
|
-
return unproject(point[0], point[1]);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function getOrigin(p1, p2, origin) {
|
|
362
|
-
if (origin === "p1") return p1;
|
|
363
|
-
if (origin === "p2") return p2;
|
|
364
|
-
return vecScale(vecAdd(p1, p2), .5);
|
|
365
|
-
}
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/draw-rules/util.ts
|
|
368
|
-
function clonePosition(position) {
|
|
369
|
-
return [...position];
|
|
370
|
-
}
|
|
371
|
-
function clonePositions(positions) {
|
|
372
|
-
return positions.map(clonePosition);
|
|
373
|
-
}
|
|
374
|
-
function samePosition$1(a, b) {
|
|
375
|
-
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
376
|
-
}
|
|
377
|
-
//#endregion
|
|
378
|
-
//#region src/draw-rules/midpoint-perpendicular.ts
|
|
379
|
-
function createMidpointPerpendicularDrawRule(options) {
|
|
380
|
-
const defaultDistanceRatio = options.defaultDistanceRatio ?? 1.5;
|
|
381
|
-
const constrainedIndex = options.constrainedIndex ?? 2;
|
|
382
|
-
const baselineIndexA = constrainedIndex === 0 ? 1 : 0;
|
|
383
|
-
const baselineIndexB = constrainedIndex === 0 ? 2 : 1;
|
|
384
|
-
const frameOrigin = options.origin ?? "midpoint";
|
|
385
|
-
const frameNormal = options.normal ?? "right";
|
|
386
|
-
function frameFor(p1, p2) {
|
|
387
|
-
return createBaselineFrame(p1, p2, {
|
|
388
|
-
origin: frameOrigin,
|
|
389
|
-
normal: frameNormal
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
function packSlots(baselineA, baselineB, constrained) {
|
|
393
|
-
const out = new Array(3);
|
|
394
|
-
out[baselineIndexA] = baselineA;
|
|
395
|
-
out[baselineIndexB] = baselineB;
|
|
396
|
-
out[constrainedIndex] = constrained;
|
|
397
|
-
return out;
|
|
398
|
-
}
|
|
399
|
-
function snap(baselineA, baselineB, point) {
|
|
400
|
-
const frame = frameFor(baselineA, baselineB);
|
|
401
|
-
if (!frame) return null;
|
|
402
|
-
return frame.pointAtNormalDistance(frame.signedNormalDistance(point));
|
|
403
|
-
}
|
|
404
|
-
function derive(points) {
|
|
405
|
-
if (points.length < 2) return clonePositions(points);
|
|
406
|
-
if (points.length === 2) {
|
|
407
|
-
const baselineA = clonePosition(points[0]);
|
|
408
|
-
const baselineB = clonePosition(points[1]);
|
|
409
|
-
const frame = frameFor(baselineA, baselineB);
|
|
410
|
-
const derived = frame ? frame.pointAtNormalDistance(frame.length * defaultDistanceRatio) : null;
|
|
411
|
-
return derived ? packSlots(baselineA, baselineB, derived) : [baselineA, baselineB];
|
|
412
|
-
}
|
|
413
|
-
const baselineA = clonePosition(points[baselineIndexA]);
|
|
414
|
-
const baselineB = clonePosition(points[baselineIndexB]);
|
|
415
|
-
const rawConstrained = points[constrainedIndex];
|
|
416
|
-
return packSlots(baselineA, baselineB, snap(baselineA, baselineB, rawConstrained) ?? clonePosition(rawConstrained));
|
|
417
|
-
}
|
|
418
|
-
return {
|
|
419
|
-
id: options.id,
|
|
420
|
-
minimumUserPoints: options.minimumUserPoints ?? 2,
|
|
421
|
-
minimumPreviewPoints: options.minimumPreviewPoints,
|
|
422
|
-
canonicalPointCount: 3,
|
|
423
|
-
derive,
|
|
424
|
-
transform(event) {
|
|
425
|
-
const { previous, next, activePointIndex } = event;
|
|
426
|
-
if (previous.length < 3 || next.length < 3) return derive(next);
|
|
427
|
-
const oldBaselineA = previous[baselineIndexA];
|
|
428
|
-
const oldBaselineB = previous[baselineIndexB];
|
|
429
|
-
const oldConstrained = previous[constrainedIndex];
|
|
430
|
-
const newBaselineA = next[baselineIndexA];
|
|
431
|
-
const newBaselineB = next[baselineIndexB];
|
|
432
|
-
const newConstrained = next[constrainedIndex];
|
|
433
|
-
if (!oldBaselineA || !oldBaselineB || !oldConstrained || !newBaselineA || !newBaselineB || !newConstrained) return derive(next);
|
|
434
|
-
const baselineAMoved = !samePosition(newBaselineA, oldBaselineA);
|
|
435
|
-
const baselineBMoved = !samePosition(newBaselineB, oldBaselineB);
|
|
436
|
-
const constrainedMoved = !samePosition(newConstrained, oldConstrained);
|
|
437
|
-
const draggingBaseline = activePointIndex === baselineIndexA || activePointIndex === baselineIndexB;
|
|
438
|
-
const draggingConstrained = activePointIndex === constrainedIndex;
|
|
439
|
-
if (draggingBaseline || (baselineAMoved || baselineBMoved) && !constrainedMoved) {
|
|
440
|
-
const oldFrame = frameFor(oldBaselineA, oldBaselineB);
|
|
441
|
-
const newFrame = frameFor(newBaselineA, newBaselineB);
|
|
442
|
-
if (oldFrame && newFrame) {
|
|
443
|
-
const distance = oldFrame.signedNormalDistance(oldConstrained);
|
|
444
|
-
const adjusted = newFrame.pointAtNormalDistance(distance);
|
|
445
|
-
if (adjusted) return packSlots(clonePosition(newBaselineA), clonePosition(newBaselineB), adjusted);
|
|
446
|
-
}
|
|
447
|
-
} else if (draggingConstrained || !baselineAMoved && !baselineBMoved && constrainedMoved) {
|
|
448
|
-
const adjusted = snap(newBaselineA, newBaselineB, newConstrained);
|
|
449
|
-
if (adjusted) return packSlots(clonePosition(newBaselineA), clonePosition(newBaselineB), adjusted);
|
|
450
|
-
}
|
|
451
|
-
return derive(next);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
function computeDefaultMidpointPerpendicularPoint(p1, p2, distanceRatio = 1.5) {
|
|
456
|
-
const frame = frameFor(p1, p2);
|
|
457
|
-
if (!frame) return null;
|
|
458
|
-
return frame.pointAtNormalDistance(frame.length * distanceRatio);
|
|
459
|
-
}
|
|
460
|
-
function getMidpointPerpendicularSignedDistance(p1, p2, controlPoint) {
|
|
461
|
-
const frame = frameFor(p1, p2);
|
|
462
|
-
if (!frame) return null;
|
|
463
|
-
return frame.signedNormalDistance(controlPoint);
|
|
464
|
-
}
|
|
465
|
-
function pointOnMidpointPerpendicularAxis(p1, p2, distance) {
|
|
466
|
-
if (distance === null) return null;
|
|
467
|
-
const frame = frameFor(p1, p2);
|
|
468
|
-
if (!frame) return null;
|
|
469
|
-
return frame.pointAtNormalDistance(distance);
|
|
470
|
-
}
|
|
471
|
-
function snapToMidpointPerpendicular(p1, p2, controlPoint) {
|
|
472
|
-
const frame = frameFor(p1, p2);
|
|
473
|
-
if (!frame) return null;
|
|
474
|
-
return frame.pointAtNormalDistance(frame.signedNormalDistance(controlPoint));
|
|
475
|
-
}
|
|
476
|
-
function frameFor(p1, p2) {
|
|
477
|
-
return createBaselineFrame(p1, p2, {
|
|
478
|
-
origin: "midpoint",
|
|
479
|
-
normal: "right"
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
function samePosition(a, b) {
|
|
483
|
-
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
484
|
-
}
|
|
485
|
-
//#endregion
|
|
486
|
-
//#region src/draw-rules/area11.ts
|
|
487
|
-
const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
|
|
488
|
-
//#endregion
|
|
489
|
-
//#region src/draw-rules/area12.ts
|
|
490
|
-
/**
|
|
491
|
-
* Area12 anchor draw rule for Disrupt.
|
|
492
|
-
*
|
|
493
|
-
* P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
|
|
494
|
-
* to the perpendicular axis through P1 (not the baseline midpoint).
|
|
495
|
-
*/
|
|
496
|
-
const disruptDrawRule = createMidpointPerpendicularDrawRule({
|
|
497
|
-
id: "area12:disrupt",
|
|
498
|
-
origin: "p1",
|
|
499
|
-
normal: "left"
|
|
500
|
-
});
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/draw-rules/area15.ts
|
|
503
|
-
/**
|
|
504
|
-
* Area15 — center + radius. PT. 1 is the center point; PT. 2 is the start point
|
|
505
|
-
* that fixes both the radius and the bearing of the symbol's opening. Both
|
|
506
|
-
* points are user-clicked, so the canonical array is just the (clamped) input.
|
|
507
|
-
*/
|
|
508
|
-
function derive$6(points) {
|
|
509
|
-
return clonePositions(points.slice(0, 2));
|
|
510
|
-
}
|
|
511
|
-
const isolateDrawRule = {
|
|
512
|
-
id: "area15:isolate",
|
|
513
|
-
minimumUserPoints: 2,
|
|
514
|
-
canonicalPointCount: 2,
|
|
515
|
-
derive: derive$6,
|
|
516
|
-
transform(event) {
|
|
517
|
-
const { previous, next, activePointIndex } = event;
|
|
518
|
-
if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
|
|
519
|
-
const dx = next[0][0] - previous[0][0];
|
|
520
|
-
const dy = next[0][1] - previous[0][1];
|
|
521
|
-
const origin = previous[1];
|
|
522
|
-
return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
|
|
523
|
-
}
|
|
524
|
-
return derive$6(next);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
//#endregion
|
|
528
|
-
//#region src/draw-rules/point12.ts
|
|
529
|
-
/**
|
|
530
|
-
* Point12 anchor draw rule for the obstacle-bypass family.
|
|
531
|
-
*
|
|
532
|
-
* Shared by `obstacle-bypass-easy`, `obstacle-bypass-difficult`, and
|
|
533
|
-
* `obstacle-bypass-impossible`: P1/P2 are the arrowhead tips and define the
|
|
534
|
-
* opening; P3 is constrained to the perpendicular axis through
|
|
535
|
-
* `midpoint(P1, P2)` and defines the rear of the symbol. Rear-line decoration
|
|
536
|
-
* (plain, zigzag, barrier marks) is a generator concern, not a draw-rule one.
|
|
537
|
-
*/
|
|
538
|
-
const point12DrawRule = createMidpointPerpendicularDrawRule({ id: "point12:obstacle-bypass" });
|
|
539
|
-
//#endregion
|
|
540
|
-
//#region src/draw-rules/area7.ts
|
|
541
|
-
/**
|
|
542
|
-
* Area7 anchor draw rule for Attack By Fire.
|
|
543
|
-
*
|
|
544
|
-
* P2/P3 are the free back-line endpoints; P1 (slot index 0) is the arrowhead
|
|
545
|
-
* tip, constrained to the perpendicular axis through `midpoint(P2, P3)`.
|
|
546
|
-
*/
|
|
547
|
-
const attackByFireDrawRule = createMidpointPerpendicularDrawRule({
|
|
548
|
-
id: "area7:attack-by-fire",
|
|
549
|
-
constrainedIndex: 0,
|
|
550
|
-
defaultDistanceRatio: -1.5
|
|
551
|
-
});
|
|
552
|
-
//#endregion
|
|
553
|
-
//#region src/draw-rules/line29.ts
|
|
554
|
-
/**
|
|
555
|
-
* Line29 anchor draw rule for Ambush.
|
|
556
|
-
*
|
|
557
|
-
* P2/P3 are the free back-line endpoints; P1 (slot index 0) is the arrowhead
|
|
558
|
-
* tip, constrained to the perpendicular axis through `midpoint(P2, P3)`.
|
|
559
|
-
*/
|
|
560
|
-
const ambushDrawRule = createMidpointPerpendicularDrawRule({
|
|
561
|
-
id: "line29:ambush",
|
|
562
|
-
constrainedIndex: 0,
|
|
563
|
-
defaultDistanceRatio: -.5
|
|
564
|
-
});
|
|
565
|
-
//#endregion
|
|
566
|
-
//#region src/draw-rules/line1.ts
|
|
567
|
-
function derive$5(points) {
|
|
568
|
-
return clonePositions(points);
|
|
569
|
-
}
|
|
570
|
-
const line1DrawRule = {
|
|
571
|
-
id: "line1",
|
|
572
|
-
minimumUserPoints: 2,
|
|
573
|
-
derive: derive$5,
|
|
574
|
-
transform(event) {
|
|
575
|
-
return derive$5(event.next);
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
//#endregion
|
|
579
|
-
//#region src/draw-rules/line3.ts
|
|
580
|
-
function derive$4(points) {
|
|
581
|
-
return clonePositions(points);
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Line3 anchor draw rule (e.g. Final Protective Fire / Principal Direction of
|
|
585
|
-
* Fire).
|
|
586
|
-
*
|
|
587
|
-
* Three fully free anchor points: P1 (slot index 0) is the vertex, P2/P3 are
|
|
588
|
-
* the arrowhead tips. The arms' lengths and orientations vary independently, so
|
|
589
|
-
* there is no constraint between slots — every handle moves on its own.
|
|
590
|
-
*/
|
|
591
|
-
const line3DrawRule = {
|
|
592
|
-
id: "line3",
|
|
593
|
-
minimumUserPoints: 3,
|
|
594
|
-
canonicalPointCount: 3,
|
|
595
|
-
derive: derive$4,
|
|
596
|
-
transform(event) {
|
|
597
|
-
return derive$4(event.next);
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
//#endregion
|
|
601
|
-
//#region src/draw-rules/line9.ts
|
|
602
|
-
function derive$3(points) {
|
|
603
|
-
return clonePositions(points);
|
|
604
|
-
}
|
|
605
|
-
const line9DrawRule = {
|
|
606
|
-
id: "line9",
|
|
607
|
-
minimumUserPoints: 2,
|
|
608
|
-
canonicalPointCount: 2,
|
|
609
|
-
derive: derive$3,
|
|
610
|
-
transform(event) {
|
|
611
|
-
return derive$3(event.next);
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
//#endregion
|
|
615
|
-
//#region src/internal/quarter-arc.ts
|
|
616
|
-
function quarterArcSide(start, end, hint, fallback = 1) {
|
|
617
|
-
if (!hint) return fallback;
|
|
618
|
-
const chord = vecSub(end, start);
|
|
619
|
-
const offset = vecSub(hint, vecScale(vecAdd(start, end), .5));
|
|
620
|
-
const cross = chord[0] * offset[1] - chord[1] * offset[0];
|
|
621
|
-
return Math.abs(cross) < 1e-6 ? fallback : cross > 0 ? 1 : -1;
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Builds the minor 90-degree circular arc from `start` to `end`.
|
|
625
|
-
*
|
|
626
|
-
* `side` selects which side of the directed chord contains the arc itself:
|
|
627
|
-
* `1` is left of start -> end, and `-1` is right.
|
|
628
|
-
*/
|
|
629
|
-
function createQuarterArc(start, end, side) {
|
|
630
|
-
const chord = vecSub(end, start);
|
|
631
|
-
const chordLength = vecMag(chord);
|
|
632
|
-
if (chordLength < 1e-6) return null;
|
|
633
|
-
const chordDir = vecNorm(chord);
|
|
634
|
-
const leftNormal = [-chordDir[1], chordDir[0]];
|
|
635
|
-
const center = vecAdd(vecScale(vecAdd(start, end), .5), vecScale(leftNormal, -side * chordLength / 2));
|
|
636
|
-
const radius = chordLength / Math.SQRT2;
|
|
637
|
-
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
|
638
|
-
const sweepAngle = -side * Math.PI / 2;
|
|
639
|
-
const midpointAngle = startAngle + sweepAngle / 2;
|
|
640
|
-
return {
|
|
641
|
-
center,
|
|
642
|
-
radius,
|
|
643
|
-
startAngle,
|
|
644
|
-
sweepAngle,
|
|
645
|
-
midpoint: vecAdd(center, [radius * Math.cos(midpointAngle), radius * Math.sin(midpointAngle)])
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
//#endregion
|
|
649
|
-
//#region src/draw-rules/line10.ts
|
|
650
|
-
function deriveWithSide(points, preservedSide) {
|
|
651
|
-
if (points.length < 2) return clonePositions(points);
|
|
652
|
-
const p1 = clonePosition(points[0]);
|
|
653
|
-
const p2 = clonePosition(points[1]);
|
|
654
|
-
const tip = project(p1[0], p1[1]);
|
|
655
|
-
const rear = project(p2[0], p2[1]);
|
|
656
|
-
const hint = points[2] ? project(points[2][0], points[2][1]) : void 0;
|
|
657
|
-
const arc = createQuarterArc(rear, tip, preservedSide ?? quarterArcSide(rear, tip, hint));
|
|
658
|
-
return arc ? [
|
|
659
|
-
p1,
|
|
660
|
-
p2,
|
|
661
|
-
unproject(arc.midpoint[0], arc.midpoint[1])
|
|
662
|
-
] : [p1, p2];
|
|
663
|
-
}
|
|
664
|
-
function sideFrom(points) {
|
|
665
|
-
const tip = project(points[0][0], points[0][1]);
|
|
666
|
-
return quarterArcSide(project(points[1][0], points[1][1]), tip, project(points[2][0], points[2][1]));
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Line10 — PT1 is the arrow tip, PT2 is the rear, and derived PT3 selects
|
|
670
|
-
* which side of the PT2 -> PT1 chord contains the 90-degree arc.
|
|
671
|
-
*/
|
|
672
|
-
const turnDrawRule = {
|
|
673
|
-
id: "line10:turn",
|
|
674
|
-
minimumUserPoints: 2,
|
|
675
|
-
canonicalPointCount: 3,
|
|
676
|
-
derive: deriveWithSide,
|
|
677
|
-
transform(event) {
|
|
678
|
-
const { previous, next, activePointIndex } = event;
|
|
679
|
-
if (previous.length < 3 || next.length < 3) return deriveWithSide(next);
|
|
680
|
-
const endpointMoved = !samePosition$1(previous[0], next[0]) || !samePosition$1(previous[1], next[1]);
|
|
681
|
-
if (activePointIndex === 0 || activePointIndex === 1 || endpointMoved) return deriveWithSide(next, sideFrom(previous));
|
|
682
|
-
return deriveWithSide(next);
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/draw-rules/line23.ts
|
|
687
|
-
/**
|
|
688
|
-
* Line23 anchor draw rule for the Clear mission task.
|
|
689
|
-
*
|
|
690
|
-
* P1/P2 are the endpoints of the symbol's vertical line (its height); P3 is
|
|
691
|
-
* constrained to the perpendicular axis through `midpoint(P1, P2)` and defines
|
|
692
|
-
* the rear of the symbol (the shaft length). Geometrically identical to the
|
|
693
|
-
* obstacle-bypass `Point12` rule, but kept as its own spec id to match the
|
|
694
|
-
* doctrinal draw rule for Clear.
|
|
695
|
-
*/
|
|
696
|
-
const line23DrawRule = createMidpointPerpendicularDrawRule({ id: "line23:clear" });
|
|
697
|
-
//#endregion
|
|
698
|
-
//#region src/draw-rules/line24.ts
|
|
699
|
-
/**
|
|
700
|
-
* Line24 anchor draw rule for the Delay mission task.
|
|
701
|
-
*
|
|
702
|
-
* PT. 1 and PT. 2 are the free straight-line endpoints. PT. 3 is constrained to
|
|
703
|
-
* the perpendicular axis through PT. 2, where its signed distance from PT. 2
|
|
704
|
-
* defines the semicircular arc diameter and side. Unlike the sibling Line23
|
|
705
|
-
* (Clear) rule, the third point must be placed explicitly (three user clicks),
|
|
706
|
-
* with a two-point live preview before commit.
|
|
707
|
-
*/
|
|
708
|
-
const line24DrawRule = createMidpointPerpendicularDrawRule({
|
|
709
|
-
id: "line24:delay",
|
|
710
|
-
origin: "p2",
|
|
711
|
-
defaultDistanceRatio: .6,
|
|
712
|
-
minimumUserPoints: 3,
|
|
713
|
-
minimumPreviewPoints: 2
|
|
714
|
-
});
|
|
715
|
-
//#endregion
|
|
716
|
-
//#region src/draw-rules/area8.ts
|
|
717
|
-
function derive$2(points) {
|
|
718
|
-
if (points.length < 2) return clonePositions(points);
|
|
719
|
-
if (points.length !== 2) return clonePositions(points.slice(0, 4));
|
|
720
|
-
const p1 = clonePosition(points[0]);
|
|
721
|
-
const p2 = clonePosition(points[1]);
|
|
722
|
-
const frame = createBaselineFrame(p1, p2, {
|
|
723
|
-
origin: "p1",
|
|
724
|
-
normal: "left"
|
|
725
|
-
});
|
|
726
|
-
if (!frame) return [p1, p2];
|
|
727
|
-
const offset = vecScale(frame.normal, frame.length);
|
|
728
|
-
const p3Projected = vecAdd(frame.p1, offset);
|
|
729
|
-
const p4Projected = vecAdd(frame.p2, offset);
|
|
730
|
-
return [
|
|
731
|
-
p1,
|
|
732
|
-
p2,
|
|
733
|
-
unproject(p3Projected[0], p3Projected[1]),
|
|
734
|
-
unproject(p4Projected[0], p4Projected[1])
|
|
735
|
-
];
|
|
736
|
-
}
|
|
737
|
-
const supportByFireDrawRule = {
|
|
738
|
-
id: "area8:support-by-fire",
|
|
739
|
-
minimumUserPoints: 2,
|
|
740
|
-
canonicalPointCount: 4,
|
|
741
|
-
derive: derive$2,
|
|
742
|
-
transform(event) {
|
|
743
|
-
const { next } = event;
|
|
744
|
-
if (next.length === 4) return clonePositions(next);
|
|
745
|
-
return derive$2(next);
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/draw-rules/axis1.ts
|
|
750
|
-
function derive$1(points) {
|
|
751
|
-
if (points.length < 2) return clonePositions(points);
|
|
752
|
-
if (points.length === 2) {
|
|
753
|
-
const tip = clonePosition(points[0]);
|
|
754
|
-
const neck = clonePosition(points[1]);
|
|
755
|
-
return [
|
|
756
|
-
tip,
|
|
757
|
-
neck,
|
|
758
|
-
computeInitialWidthPoint(tip, neck)
|
|
759
|
-
];
|
|
760
|
-
}
|
|
761
|
-
return clonePositions(points);
|
|
762
|
-
}
|
|
763
|
-
function projectWidthIntoFrame(next, longitudinal, lateral) {
|
|
764
|
-
const tip = next[0];
|
|
765
|
-
const neck = next[1];
|
|
766
|
-
if (!tip || !neck) return null;
|
|
767
|
-
const tipM = project(tip[0], tip[1]);
|
|
768
|
-
const neckM = project(neck[0], neck[1]);
|
|
769
|
-
const dx = tipM[0] - neckM[0];
|
|
770
|
-
const dy = tipM[1] - neckM[1];
|
|
771
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
772
|
-
if (len < 1e-6) return null;
|
|
773
|
-
const ax = dx / len;
|
|
774
|
-
const ay = dy / len;
|
|
775
|
-
const px = -ay;
|
|
776
|
-
const py = ax;
|
|
777
|
-
return unproject(tipM[0] + longitudinal * ax + lateral * px, tipM[1] + longitudinal * ay + lateral * py);
|
|
778
|
-
}
|
|
779
|
-
function packWithWidth(next, width) {
|
|
780
|
-
const out = clonePositions(next);
|
|
781
|
-
out[out.length - 1] = width;
|
|
782
|
-
return out;
|
|
783
|
-
}
|
|
784
|
-
const axis1DrawRule = {
|
|
785
|
-
id: "axis1",
|
|
786
|
-
minimumUserPoints: 2,
|
|
787
|
-
trailingFixedSlots: 1,
|
|
788
|
-
derive: derive$1,
|
|
789
|
-
transform(event) {
|
|
790
|
-
const { previous, next, activePointIndex } = event;
|
|
791
|
-
if (next.length < 3 || previous.length < 3) return derive$1(next);
|
|
792
|
-
if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
|
|
793
|
-
const metrics = calculateMetrics(previous);
|
|
794
|
-
if (!metrics) return clonePositions(next);
|
|
795
|
-
const projected = projectWidthIntoFrame(next, metrics.longitudinal, metrics.lateral);
|
|
796
|
-
if (!projected) return clonePositions(next);
|
|
797
|
-
return packWithWidth(next, projected);
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
//#endregion
|
|
801
|
-
//#region src/draw-rules/area21.ts
|
|
802
|
-
function derive(points) {
|
|
803
|
-
if (points.length === 2) return clonePositions([
|
|
804
|
-
points[0],
|
|
805
|
-
points[1],
|
|
806
|
-
points[1]
|
|
807
|
-
]);
|
|
808
|
-
return clonePositions(points);
|
|
809
|
-
}
|
|
810
|
-
const searchAreaDrawRule = {
|
|
811
|
-
id: "area21:search-area",
|
|
812
|
-
minimumUserPoints: 3,
|
|
813
|
-
minimumPreviewPoints: 2,
|
|
814
|
-
canonicalPointCount: 3,
|
|
815
|
-
derive,
|
|
816
|
-
transform(event) {
|
|
817
|
-
return derive(event.next);
|
|
818
|
-
}
|
|
819
|
-
};
|
|
820
|
-
//#endregion
|
|
821
|
-
//#region src/generators/cm15-maneuver-areas/params.ts
|
|
822
|
-
const ATTACK_SHAFT_PARAMS = [
|
|
823
|
-
{
|
|
824
|
-
key: "shaftWidthRatio",
|
|
825
|
-
label: "Shaft width",
|
|
826
|
-
description: "Arrow shaft width as a ratio of the arrowhead width.",
|
|
827
|
-
type: "number",
|
|
828
|
-
min: .1,
|
|
829
|
-
max: 1,
|
|
830
|
-
step: .05
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
key: "roundedBends",
|
|
834
|
-
label: "Rounded bends",
|
|
835
|
-
description: "Round the corners where the shaft changes direction.",
|
|
836
|
-
type: "boolean"
|
|
837
|
-
},
|
|
838
|
-
{
|
|
839
|
-
key: "bendSegments",
|
|
840
|
-
label: "Bend segments",
|
|
841
|
-
description: "Number of segments approximating each rounded bend.",
|
|
842
|
-
type: "number",
|
|
843
|
-
min: 1,
|
|
844
|
-
max: 20,
|
|
845
|
-
step: 1,
|
|
846
|
-
visibleWhen: (opts) => opts.roundedBends === true
|
|
847
|
-
}
|
|
848
|
-
];
|
|
849
|
-
const FIRE_ARROW_PARAMS = [
|
|
850
|
-
{
|
|
851
|
-
key: "arrowTipWidthRatio",
|
|
852
|
-
label: "Arrow tip width",
|
|
853
|
-
description: "Width of the arrow tip as a ratio of the head width.",
|
|
854
|
-
type: "number",
|
|
855
|
-
min: .05,
|
|
856
|
-
max: 1,
|
|
857
|
-
step: .01
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
key: "backLineLengthRatio",
|
|
861
|
-
label: "Back line length",
|
|
862
|
-
description: "Length of the rear flare lines as a ratio of the head.",
|
|
863
|
-
type: "number",
|
|
864
|
-
min: .05,
|
|
865
|
-
max: 1,
|
|
866
|
-
step: .01
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
key: "backLineAngle",
|
|
870
|
-
label: "Back line angle",
|
|
871
|
-
description: "Angle of the rear flare lines, in degrees.",
|
|
872
|
-
type: "number",
|
|
873
|
-
min: 0,
|
|
874
|
-
max: 90,
|
|
875
|
-
step: 1
|
|
876
|
-
}
|
|
877
|
-
];
|
|
878
|
-
const FORTIFIED_PARAMS$1 = [{
|
|
879
|
-
key: "sizePixels",
|
|
880
|
-
label: "Size",
|
|
881
|
-
description: "Size of the fortified-line teeth, in pixels.",
|
|
882
|
-
type: "number",
|
|
883
|
-
min: 5,
|
|
884
|
-
max: 50,
|
|
885
|
-
step: 1,
|
|
886
|
-
unit: "px"
|
|
887
|
-
}, {
|
|
888
|
-
key: "size",
|
|
889
|
-
label: "Size",
|
|
890
|
-
description: "Size of the fortified-line teeth, in meters.",
|
|
891
|
-
type: "number",
|
|
892
|
-
min: 1,
|
|
893
|
-
max: 500,
|
|
894
|
-
step: 1,
|
|
895
|
-
unit: "m"
|
|
896
|
-
}];
|
|
897
|
-
//#endregion
|
|
898
|
-
//#region src/generators/cm15-maneuver-areas/airborneAttack.ts
|
|
899
|
-
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
900
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
901
|
-
roundedBends: false,
|
|
902
|
-
bendSegments: 5
|
|
903
|
-
};
|
|
904
|
-
const AIRBORNE_ATTACK_METADATA = {
|
|
905
|
-
id: "airborne-attack",
|
|
906
|
-
name: "Airborne Attack",
|
|
907
|
-
description: "Airborne or aviation axis of advance.",
|
|
908
|
-
entity: "Maneuver Areas",
|
|
909
|
-
entityType: "Axis of Advance",
|
|
910
|
-
entitySubtype: "Airborne Attack",
|
|
911
|
-
value: "151401",
|
|
912
|
-
minCoordinates: 3,
|
|
913
|
-
geometry: "line",
|
|
914
|
-
geometryTypes: ["LineString"],
|
|
915
|
-
drawRule: "Axis1",
|
|
916
|
-
params: ATTACK_SHAFT_PARAMS
|
|
917
|
-
};
|
|
918
|
-
/**
|
|
919
|
-
* Generates a GeoJSON FeatureCollection for an Airborne Attack tactical symbol.
|
|
920
|
-
*
|
|
921
|
-
* The symbol is similar to Supporting Attack but features a crossover point
|
|
922
|
-
* between the shaft and the head (Points 1 and 2/Neck).
|
|
923
|
-
*
|
|
924
|
-
* @param coordinates - An array of GeoJSON Positions.
|
|
925
|
-
* @param options - Configuration options.
|
|
926
|
-
* @returns A GeoJSON FeatureCollection<LineString>.
|
|
927
|
-
*/
|
|
928
|
-
function createAirborneAttack(coordinates, options = {}) {
|
|
929
|
-
const { geometry } = processAttackGeometry(coordinates, options);
|
|
930
|
-
if (!geometry) return {
|
|
931
|
-
type: "FeatureCollection",
|
|
932
|
-
features: []
|
|
933
|
-
};
|
|
934
|
-
const shaftLeftToNeck = geometry.shaftLeft.slice(0, -1);
|
|
935
|
-
const shaftRightToNeck = geometry.shaftRight.slice(0, -1);
|
|
936
|
-
return {
|
|
937
|
-
type: "FeatureCollection",
|
|
938
|
-
features: [{
|
|
939
|
-
type: "Feature",
|
|
940
|
-
properties: {},
|
|
941
|
-
geometry: {
|
|
942
|
-
type: "LineString",
|
|
943
|
-
coordinates: [
|
|
944
|
-
...shaftLeftToNeck,
|
|
945
|
-
geometry.headRing[3],
|
|
946
|
-
geometry.headRing[2],
|
|
947
|
-
geometry.headRing[1],
|
|
948
|
-
geometry.headRing[0],
|
|
949
|
-
geometry.headRing[5],
|
|
950
|
-
...[...shaftRightToNeck].reverse()
|
|
951
|
-
].map((p) => unproject(p[0], p[1]))
|
|
952
|
-
}
|
|
953
|
-
}]
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
const AIRBORNE_ATTACK = defineControlMeasure({
|
|
957
|
-
metadata: AIRBORNE_ATTACK_METADATA,
|
|
958
|
-
generator: createAirborneAttack,
|
|
959
|
-
defaultOptions: DEFAULT_AIRBORNE_ATTACK_OPTIONS,
|
|
960
|
-
rule: axis1DrawRule
|
|
961
|
-
});
|
|
962
|
-
/**
|
|
963
|
-
* Reacts to an **input-contract** violation according to `mode`: `throw`
|
|
964
|
-
* raises, `warn` logs, `silent` (the default) does neither. Always returns
|
|
965
|
-
* `false` so a caller can `return reportValidationIssue(...)` from a guard.
|
|
966
|
-
*
|
|
967
|
-
* Governs the *input contract* only (control-point count and finiteness),
|
|
968
|
-
* checked once at the render seam — not geometric degeneracy, which always
|
|
969
|
-
* yields an empty render silently. See ADR-0014 and the `CONTEXT.md` terms.
|
|
970
|
-
*/
|
|
971
|
-
function reportValidationIssue(mode, message) {
|
|
972
|
-
const resolved = mode ?? "silent";
|
|
973
|
-
if (resolved === "throw") throw new Error(message);
|
|
974
|
-
if (resolved === "warn") console.warn(message);
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Narrows a control-point array to the fixed `[PT1, PT2, PT3]` tuple the ambush
|
|
979
|
-
* and search-area generators require. The render seam has already validated
|
|
980
|
-
* that at least three valid points are present (ADR-0014), so this is a pure
|
|
981
|
-
* cast with no runtime guard. Used by those measures' definition adapters so
|
|
982
|
-
* the generators keep their precise tuple parameter (ADR-0013).
|
|
983
|
-
*/
|
|
984
|
-
function asThreePoints(points) {
|
|
985
|
-
return [
|
|
986
|
-
points[0],
|
|
987
|
-
points[1],
|
|
988
|
-
points[2]
|
|
989
|
-
];
|
|
990
|
-
}
|
|
991
|
-
const DEFAULT_AMBUSH_OPTIONS = {
|
|
992
|
-
hSize: .15,
|
|
993
|
-
hWidth: .5,
|
|
994
|
-
hLen: .15,
|
|
995
|
-
hCount: 5,
|
|
996
|
-
cDepth: .25,
|
|
997
|
-
resolution: 30
|
|
998
|
-
};
|
|
999
|
-
const AMBUSH_METADATA = {
|
|
1000
|
-
id: "ambush",
|
|
1001
|
-
name: "Ambush",
|
|
1002
|
-
description: "A variation of attack from concealed positions against a moving or temporarily halted enemy.",
|
|
1003
|
-
entity: "Maneuver Lines",
|
|
1004
|
-
entityType: "Ambush",
|
|
1005
|
-
value: "141700",
|
|
1006
|
-
minCoordinates: 3,
|
|
1007
|
-
maxCoordinates: 3,
|
|
1008
|
-
geometry: "line",
|
|
1009
|
-
geometryTypes: ["MultiLineString"],
|
|
1010
|
-
drawRule: "Line29",
|
|
1011
|
-
params: [
|
|
1012
|
-
{
|
|
1013
|
-
key: "hSize",
|
|
1014
|
-
label: "Head size",
|
|
1015
|
-
description: "Arrowhead length as a fraction of the chord span",
|
|
1016
|
-
type: "number",
|
|
1017
|
-
min: .01,
|
|
1018
|
-
max: .5,
|
|
1019
|
-
step: .01
|
|
1020
|
-
},
|
|
1021
|
-
{
|
|
1022
|
-
key: "hWidth",
|
|
1023
|
-
label: "Head width",
|
|
1024
|
-
description: "Arrowhead wing spread relative to its length",
|
|
1025
|
-
type: "number",
|
|
1026
|
-
min: .05,
|
|
1027
|
-
max: 2,
|
|
1028
|
-
step: .01
|
|
1029
|
-
},
|
|
1030
|
-
{
|
|
1031
|
-
key: "hLen",
|
|
1032
|
-
label: "Hatch length",
|
|
1033
|
-
description: "Length of the parallel hatches as a fraction of the chord span",
|
|
1034
|
-
type: "number",
|
|
1035
|
-
min: .01,
|
|
1036
|
-
max: .5,
|
|
1037
|
-
step: .01
|
|
1038
|
-
},
|
|
1039
|
-
{
|
|
1040
|
-
key: "hCount",
|
|
1041
|
-
label: "Hatch count",
|
|
1042
|
-
description: "Number of parallel hatches spaced along the curve",
|
|
1043
|
-
type: "number",
|
|
1044
|
-
min: 0,
|
|
1045
|
-
max: 20,
|
|
1046
|
-
step: 1
|
|
1047
|
-
},
|
|
1048
|
-
{
|
|
1049
|
-
key: "cDepth",
|
|
1050
|
-
label: "Curve depth",
|
|
1051
|
-
description: "How far the back line bends toward the tip, as a fraction of the chord span",
|
|
1052
|
-
type: "number",
|
|
1053
|
-
min: 0,
|
|
1054
|
-
max: 1,
|
|
1055
|
-
step: .01
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
key: "resolution",
|
|
1059
|
-
label: "Resolution",
|
|
1060
|
-
description: "Number of segments used to render the curve",
|
|
1061
|
-
type: "number",
|
|
1062
|
-
min: 4,
|
|
1063
|
-
max: 96,
|
|
1064
|
-
step: 1
|
|
1065
|
-
}
|
|
1066
|
-
]
|
|
1067
|
-
};
|
|
1068
|
-
const quadBezier = (t, a, b, c) => {
|
|
1069
|
-
const it = 1 - t;
|
|
1070
|
-
return [it * it * a[0] + 2 * it * t * b[0] + t * t * c[0], it * it * a[1] + 2 * it * t * b[1] + t * t * c[1]];
|
|
1071
|
-
};
|
|
1072
|
-
/**
|
|
1073
|
-
* Generates a GeoJSON FeatureCollection for an AMBUSH tactical symbol.
|
|
1074
|
-
* Handles internal projection to maintain strict perpendicularity and parallel hatching.
|
|
1075
|
-
*
|
|
1076
|
-
* @param coordinates - [PT 1 (Target Tip), PT 2 (Back Chord End), PT 3 (Back Chord End)]
|
|
1077
|
-
* @param options - Geometric and styling parameters
|
|
1078
|
-
* @returns GeoJSON FeatureCollection containing LineString components
|
|
1079
|
-
*/
|
|
1080
|
-
function createAmbushSymbol(coordinates, options = {}) {
|
|
1081
|
-
const { hSize, hWidth, hLen, hCount, cDepth, resolution } = {
|
|
1082
|
-
...DEFAULT_AMBUSH_OPTIONS,
|
|
1083
|
-
...options
|
|
1084
|
-
};
|
|
1085
|
-
const safeResolution = Math.max(1, Math.floor(resolution));
|
|
1086
|
-
const safeHCount = Math.max(0, Math.floor(hCount));
|
|
1087
|
-
const safeHSize = Math.max(0, hSize);
|
|
1088
|
-
const safeHWidth = Math.max(0, hWidth);
|
|
1089
|
-
const safeHLen = Math.max(0, hLen);
|
|
1090
|
-
const safeCDepth = Math.max(0, cDepth);
|
|
1091
|
-
const [p1, p2, p3] = coordinates.map((coord) => project(coord[0], coord[1]));
|
|
1092
|
-
const mid = vecScale(vecAdd(p2, p3), .5);
|
|
1093
|
-
const v23 = vecSub(p3, p2);
|
|
1094
|
-
const span = vecMag(v23);
|
|
1095
|
-
if (span < 1e-6) return {
|
|
1096
|
-
type: "FeatureCollection",
|
|
1097
|
-
features: []
|
|
1098
|
-
};
|
|
1099
|
-
const dir23 = vecNorm(v23);
|
|
1100
|
-
const n = [-dir23[1], dir23[0]];
|
|
1101
|
-
const dist = vecDot(vecSub(p1, mid), n);
|
|
1102
|
-
const side = dist >= 0 ? 1 : -1;
|
|
1103
|
-
const ctrl = vecAdd(mid, vecScale(n, side * span * safeCDepth));
|
|
1104
|
-
const shaftOrigin = quadBezier(.5, p2, ctrl, p3);
|
|
1105
|
-
const adjP1 = vecAdd(shaftOrigin, vecScale(n, side * Math.max(0, Math.abs(dist) - span * safeCDepth * .5)));
|
|
1106
|
-
const lines = [];
|
|
1107
|
-
lines.push([shaftOrigin, adjP1]);
|
|
1108
|
-
const hL = span * safeHSize;
|
|
1109
|
-
const hw = hL * safeHWidth;
|
|
1110
|
-
const hBase = vecSub(adjP1, vecScale(n, side * hL));
|
|
1111
|
-
const perp = [-n[1], n[0]];
|
|
1112
|
-
lines.push([
|
|
1113
|
-
vecAdd(hBase, vecScale(perp, hw)),
|
|
1114
|
-
adjP1,
|
|
1115
|
-
vecSub(hBase, vecScale(perp, hw))
|
|
1116
|
-
]);
|
|
1117
|
-
const curve = [];
|
|
1118
|
-
for (let i = 0; i <= safeResolution; i++) {
|
|
1119
|
-
const t = i / safeResolution;
|
|
1120
|
-
curve.push(quadBezier(t, p2, ctrl, p3));
|
|
1121
|
-
}
|
|
1122
|
-
lines.push(curve);
|
|
1123
|
-
const hLenActual = span * safeHLen;
|
|
1124
|
-
for (let i = 1; i <= safeHCount; i++) {
|
|
1125
|
-
const pt = quadBezier(i / (safeHCount + 1), p2, ctrl, p3);
|
|
1126
|
-
lines.push([pt, vecSub(pt, vecScale(n, side * hLenActual))]);
|
|
1127
|
-
}
|
|
1128
|
-
return {
|
|
1129
|
-
type: "FeatureCollection",
|
|
1130
|
-
features: [{
|
|
1131
|
-
type: "Feature",
|
|
1132
|
-
properties: {},
|
|
1133
|
-
geometry: {
|
|
1134
|
-
type: "MultiLineString",
|
|
1135
|
-
coordinates: lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
1136
|
-
}
|
|
1137
|
-
}]
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
const AMBUSH = defineControlMeasure({
|
|
1141
|
-
metadata: AMBUSH_METADATA,
|
|
1142
|
-
generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
|
|
1143
|
-
defaultOptions: DEFAULT_AMBUSH_OPTIONS,
|
|
1144
|
-
rule: ambushDrawRule
|
|
1145
|
-
});
|
|
1146
|
-
//#endregion
|
|
1147
|
-
//#region src/generators/cm29-protection-lines/params.ts
|
|
1148
|
-
const FORTIFIED_PARAMS = [{
|
|
1149
|
-
key: "sizePixels",
|
|
1150
|
-
label: "Size",
|
|
1151
|
-
description: "Side length of the square crenellations",
|
|
1152
|
-
type: "number",
|
|
1153
|
-
min: 5,
|
|
1154
|
-
max: 50,
|
|
1155
|
-
step: 1,
|
|
1156
|
-
unit: "px"
|
|
1157
|
-
}, {
|
|
1158
|
-
key: "size",
|
|
1159
|
-
label: "Size",
|
|
1160
|
-
description: "Side length of the square crenellations",
|
|
1161
|
-
type: "number",
|
|
1162
|
-
min: 1,
|
|
1163
|
-
max: 500,
|
|
1164
|
-
step: 1,
|
|
1165
|
-
unit: "m"
|
|
1166
|
-
}];
|
|
1167
|
-
const ANTITANK_DITCH_PARAMS = [
|
|
1168
|
-
{
|
|
1169
|
-
key: "toothHeightPixels",
|
|
1170
|
-
label: "Tooth height",
|
|
1171
|
-
description: "Perpendicular depth of each triangular tooth",
|
|
1172
|
-
type: "number",
|
|
1173
|
-
min: 5,
|
|
1174
|
-
max: 50,
|
|
1175
|
-
step: 1,
|
|
1176
|
-
unit: "px"
|
|
1177
|
-
},
|
|
1178
|
-
{
|
|
1179
|
-
key: "toothHeight",
|
|
1180
|
-
label: "Tooth height",
|
|
1181
|
-
description: "Perpendicular depth of each triangular tooth",
|
|
1182
|
-
type: "number",
|
|
1183
|
-
min: 1,
|
|
1184
|
-
max: 500,
|
|
1185
|
-
step: 1,
|
|
1186
|
-
unit: "m"
|
|
1187
|
-
},
|
|
1188
|
-
{
|
|
1189
|
-
key: "toothWidthRatio",
|
|
1190
|
-
label: "Tooth width ratio",
|
|
1191
|
-
description: "Tooth base span as a multiple of height",
|
|
1192
|
-
type: "number",
|
|
1193
|
-
min: .5,
|
|
1194
|
-
max: 3,
|
|
1195
|
-
step: .05,
|
|
1196
|
-
unit: "x"
|
|
1197
|
-
}
|
|
1198
|
-
];
|
|
1199
|
-
const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
|
|
1200
|
-
key: "toothSpacingRatio",
|
|
1201
|
-
label: "Tooth spacing ratio",
|
|
1202
|
-
description: "Gap between teeth as a multiple of base width",
|
|
1203
|
-
type: "number",
|
|
1204
|
-
min: 0,
|
|
1205
|
-
max: 3,
|
|
1206
|
-
step: .05,
|
|
1207
|
-
unit: "x"
|
|
1208
|
-
}];
|
|
1209
|
-
//#endregion
|
|
1210
|
-
//#region src/generators/cm29-protection-lines/antitankDitch.ts
|
|
1211
|
-
const DEFAULT_TOOTH_HEIGHT = 50;
|
|
1212
|
-
const DEFAULT_TOOTH_HEIGHT_PIXELS = 10;
|
|
1213
|
-
const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
|
|
1214
|
-
const DEFAULT_ANTITANK_DITCH_OPTIONS = {
|
|
1215
|
-
toothHeight: DEFAULT_TOOTH_HEIGHT,
|
|
1216
|
-
toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
|
|
1217
|
-
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1
|
|
1218
|
-
};
|
|
1219
|
-
const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
|
|
1220
|
-
id: "antitank-ditch-under-construction",
|
|
1221
|
-
name: "Ditch - Under Construction",
|
|
1222
|
-
description: "Antitank ditch obstacle under construction.",
|
|
1223
|
-
entity: "Protection Lines",
|
|
1224
|
-
entityType: "Antitank Obstacles",
|
|
1225
|
-
entitySubtype: "Ditch - Under Construction",
|
|
1226
|
-
value: "290201",
|
|
1227
|
-
minCoordinates: 2,
|
|
1228
|
-
geometry: "line",
|
|
1229
|
-
geometryTypes: ["MultiLineString"],
|
|
1230
|
-
drawRule: "Line1",
|
|
1231
|
-
params: ANTITANK_DITCH_PARAMS
|
|
1232
|
-
};
|
|
1233
|
-
const ANTITANK_DITCH_COMPLETED_METADATA = {
|
|
1234
|
-
id: "antitank-ditch-completed",
|
|
1235
|
-
name: "Ditch - Completed",
|
|
1236
|
-
description: "Completed antitank ditch obstacle.",
|
|
1237
|
-
entity: "Protection Lines",
|
|
1238
|
-
entityType: "Antitank Obstacles",
|
|
1239
|
-
entitySubtype: "Ditch - Completed",
|
|
1240
|
-
value: "290202",
|
|
1241
|
-
minCoordinates: 2,
|
|
1242
|
-
geometry: "line",
|
|
1243
|
-
geometryTypes: ["MultiPolygon"],
|
|
1244
|
-
drawRule: "Line1",
|
|
1245
|
-
params: ANTITANK_DITCH_PARAMS
|
|
1246
|
-
};
|
|
1247
|
-
function calculateAntitankDitchHeight(options) {
|
|
1248
|
-
const { toothHeight = DEFAULT_TOOTH_HEIGHT, toothHeightPixels, metersPerPixel } = options;
|
|
1249
|
-
if (toothHeightPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) return Math.max(EPSILON, toothHeightPixels * metersPerPixel);
|
|
1250
|
-
return Math.max(EPSILON, toothHeight);
|
|
1251
|
-
}
|
|
1252
|
-
function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidthRatio = DEFAULT_TOOTH_WIDTH_RATIO$1) {
|
|
1253
|
-
const triangles = [];
|
|
1254
|
-
const targetPeriod = Math.max(EPSILON, toothWidthRatio) * toothHeight;
|
|
1255
|
-
for (let i = 0; i < projectedPoints.length - 1; i++) {
|
|
1256
|
-
const p1 = projectedPoints[i];
|
|
1257
|
-
const p2 = projectedPoints[i + 1];
|
|
1258
|
-
const segmentVec = vecSub(p2, p1);
|
|
1259
|
-
const segmentLength = vecMag(segmentVec);
|
|
1260
|
-
if (segmentLength < 1e-6) continue;
|
|
1261
|
-
const segmentDir = vecNorm(segmentVec);
|
|
1262
|
-
const perpDir = [-segmentDir[1], segmentDir[0]];
|
|
1263
|
-
const toothCount = Math.max(1, Math.round(segmentLength / targetPeriod));
|
|
1264
|
-
const period = segmentLength / toothCount;
|
|
1265
|
-
for (let toothIndex = 0; toothIndex < toothCount; toothIndex++) {
|
|
1266
|
-
const baseStart = vecAdd(p1, vecScale(segmentDir, period * toothIndex));
|
|
1267
|
-
const baseEnd = vecAdd(p1, vecScale(segmentDir, period * (toothIndex + 1)));
|
|
1268
|
-
const tip = vecAdd(vecAdd(baseStart, vecScale(segmentDir, period / 2)), vecScale(perpDir, toothHeight));
|
|
1269
|
-
triangles.push([
|
|
1270
|
-
baseStart,
|
|
1271
|
-
tip,
|
|
1272
|
-
baseEnd
|
|
1273
|
-
]);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
return triangles;
|
|
1277
|
-
}
|
|
1278
|
-
function buildAntitankDitchTriangles(positions, options) {
|
|
1279
|
-
const toothHeight = calculateAntitankDitchHeight(options);
|
|
1280
|
-
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1;
|
|
1281
|
-
return generateAntitankDitchTriangles(positions.map((position) => project(position[0], position[1])), toothHeight, toothWidthRatio);
|
|
1282
|
-
}
|
|
1283
|
-
function createAntitankDitchUnderConstruction(positions, options = {}) {
|
|
1284
|
-
const triangles = buildAntitankDitchTriangles(positions, options);
|
|
1285
|
-
return {
|
|
1286
|
-
type: "FeatureCollection",
|
|
1287
|
-
features: [{
|
|
1288
|
-
type: "Feature",
|
|
1289
|
-
properties: { part: "ditch" },
|
|
1290
|
-
geometry: {
|
|
1291
|
-
type: "MultiLineString",
|
|
1292
|
-
coordinates: [positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
|
|
1293
|
-
}
|
|
1294
|
-
}]
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
function createAntitankDitchCompleted(positions, options = {}) {
|
|
1298
|
-
return {
|
|
1299
|
-
type: "FeatureCollection",
|
|
1300
|
-
features: [{
|
|
1301
|
-
type: "Feature",
|
|
1302
|
-
properties: {
|
|
1303
|
-
part: "teeth",
|
|
1304
|
-
fill: true
|
|
1305
|
-
},
|
|
1306
|
-
geometry: {
|
|
1307
|
-
type: "MultiPolygon",
|
|
1308
|
-
coordinates: buildAntitankDitchTriangles(positions, options).map((triangle) => {
|
|
1309
|
-
return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
|
|
1310
|
-
})
|
|
1311
|
-
}
|
|
1312
|
-
}]
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
const ANTITANK_DITCH_UNDER_CONSTRUCTION = defineControlMeasure({
|
|
1316
|
-
metadata: ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA,
|
|
1317
|
-
generator: createAntitankDitchUnderConstruction,
|
|
1318
|
-
defaultOptions: DEFAULT_ANTITANK_DITCH_OPTIONS,
|
|
1319
|
-
rule: line1DrawRule
|
|
1320
|
-
});
|
|
1321
|
-
const ANTITANK_DITCH_COMPLETED = defineControlMeasure({
|
|
1322
|
-
metadata: ANTITANK_DITCH_COMPLETED_METADATA,
|
|
1323
|
-
generator: createAntitankDitchCompleted,
|
|
1324
|
-
defaultOptions: DEFAULT_ANTITANK_DITCH_OPTIONS,
|
|
1325
|
-
rule: line1DrawRule
|
|
1326
|
-
});
|
|
1327
|
-
//#endregion
|
|
1328
|
-
//#region src/generators/cm29-protection-lines/antitankWall.ts
|
|
1329
|
-
const DEFAULT_TOOTH_SPACING_RATIO = 1.35;
|
|
1330
|
-
const DEFAULT_TOOTH_WIDTH_RATIO = 2.2;
|
|
1331
|
-
const DEFAULT_ANTITANK_WALL_OPTIONS = {
|
|
1332
|
-
...DEFAULT_ANTITANK_DITCH_OPTIONS,
|
|
1333
|
-
toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO,
|
|
1334
|
-
toothSpacingRatio: DEFAULT_TOOTH_SPACING_RATIO
|
|
1335
|
-
};
|
|
1336
|
-
const ANTITANK_WALL_METADATA = {
|
|
1337
|
-
id: "antitank-wall",
|
|
1338
|
-
name: "Antitank Wall",
|
|
1339
|
-
description: "Antitank wall obstacle.",
|
|
1340
|
-
entity: "Protection Lines",
|
|
1341
|
-
entityType: "Antitank Obstacles",
|
|
1342
|
-
entitySubtype: "Antitank Wall",
|
|
1343
|
-
value: "290204",
|
|
1344
|
-
minCoordinates: 2,
|
|
1345
|
-
geometry: "line",
|
|
1346
|
-
geometryTypes: ["MultiLineString"],
|
|
1347
|
-
drawRule: "Line1",
|
|
1348
|
-
params: ANTITANK_WALL_PARAMS
|
|
1349
|
-
};
|
|
1350
|
-
function generateAntitankWallPoints(projectedPoints, toothHeight, toothWidthRatio = DEFAULT_TOOTH_WIDTH_RATIO, toothSpacingRatio = DEFAULT_TOOTH_SPACING_RATIO) {
|
|
1351
|
-
if (projectedPoints.length < 2) return projectedPoints;
|
|
1352
|
-
const wallPoints = [];
|
|
1353
|
-
const safeToothHeight = Math.max(EPSILON, toothHeight);
|
|
1354
|
-
const safeToothWidthRatio = Math.max(EPSILON, toothWidthRatio);
|
|
1355
|
-
const safeToothSpacingRatio = Math.max(0, toothSpacingRatio);
|
|
1356
|
-
const targetPeriod = safeToothWidthRatio * safeToothHeight;
|
|
1357
|
-
for (let i = 0; i < projectedPoints.length - 1; i++) {
|
|
1358
|
-
const p1 = projectedPoints[i];
|
|
1359
|
-
const p2 = projectedPoints[i + 1];
|
|
1360
|
-
const segmentVec = vecSub(p2, p1);
|
|
1361
|
-
const segmentLength = vecMag(segmentVec);
|
|
1362
|
-
if (wallPoints.length === 0) wallPoints.push(p1);
|
|
1363
|
-
if (segmentLength < 1e-6) continue;
|
|
1364
|
-
const segmentDir = vecNorm(segmentVec);
|
|
1365
|
-
const perpDir = [segmentDir[1], -segmentDir[0]];
|
|
1366
|
-
const toothCount = Math.max(1, Math.round(segmentLength / targetPeriod));
|
|
1367
|
-
const period = segmentLength / toothCount;
|
|
1368
|
-
const toothBase = Math.max(EPSILON, period / (1 + safeToothSpacingRatio));
|
|
1369
|
-
const shoulder = (period - toothBase) / 2;
|
|
1370
|
-
for (let toothIndex = 0; toothIndex < toothCount; toothIndex++) {
|
|
1371
|
-
const baseStartDistance = period * toothIndex + shoulder;
|
|
1372
|
-
const baseStart = vecAdd(p1, vecScale(segmentDir, baseStartDistance));
|
|
1373
|
-
const baseEnd = vecAdd(p1, vecScale(segmentDir, baseStartDistance + toothBase));
|
|
1374
|
-
const tip = vecAdd(vecAdd(baseStart, vecScale(segmentDir, toothBase / 2)), vecScale(perpDir, safeToothHeight));
|
|
1375
|
-
wallPoints.push(baseStart, tip, baseEnd);
|
|
1376
|
-
}
|
|
1377
|
-
wallPoints.push(p2);
|
|
1378
|
-
}
|
|
1379
|
-
return wallPoints;
|
|
1380
|
-
}
|
|
1381
|
-
function createAntitankWall(positions, options = {}) {
|
|
1382
|
-
const toothHeight = calculateAntitankDitchHeight(options);
|
|
1383
|
-
const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
|
|
1384
|
-
const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
|
|
1385
|
-
return {
|
|
1386
|
-
type: "FeatureCollection",
|
|
1387
|
-
features: [{
|
|
1388
|
-
type: "Feature",
|
|
1389
|
-
properties: { part: "wall" },
|
|
1390
|
-
geometry: {
|
|
1391
|
-
type: "MultiLineString",
|
|
1392
|
-
coordinates: [generateAntitankWallPoints(positions.map((position) => project(position[0], position[1])), toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
|
|
1393
|
-
}
|
|
1394
|
-
}]
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
const ANTITANK_WALL = defineControlMeasure({
|
|
1398
|
-
metadata: ANTITANK_WALL_METADATA,
|
|
1399
|
-
generator: createAntitankWall,
|
|
1400
|
-
defaultOptions: DEFAULT_ANTITANK_WALL_OPTIONS,
|
|
1401
|
-
rule: line1DrawRule
|
|
1402
|
-
});
|
|
1403
|
-
//#endregion
|
|
1404
|
-
//#region src/generators/cm15-maneuver-areas/attackByFire.ts
|
|
1405
|
-
const DEFAULT_ATTACK_BY_FIRE_OPTIONS = {
|
|
1406
|
-
arrowTipWidthRatio: .3,
|
|
1407
|
-
backLineLengthRatio: .3,
|
|
1408
|
-
backLineAngle: 45
|
|
1409
|
-
};
|
|
1410
|
-
const ATTACK_BY_FIRE_METADATA = {
|
|
1411
|
-
id: "attack-by-fire",
|
|
1412
|
-
name: "Attack by Fire",
|
|
1413
|
-
description: "Attack by fire graphic with base, shaft, and arrowhead.",
|
|
1414
|
-
entity: "Maneuver Areas",
|
|
1415
|
-
entityType: "Attack by Fire",
|
|
1416
|
-
value: "152000",
|
|
1417
|
-
minCoordinates: 3,
|
|
1418
|
-
geometry: "line",
|
|
1419
|
-
geometryTypes: ["LineString"],
|
|
1420
|
-
drawRule: "Area7",
|
|
1421
|
-
params: FIRE_ARROW_PARAMS
|
|
1422
|
-
};
|
|
1423
|
-
/**
|
|
1424
|
-
* Generates a GeoJSON FeatureCollection for an Attack By Fire tactical graphic.
|
|
1425
|
-
*
|
|
1426
|
-
* Input:
|
|
1427
|
-
* - PT1: Arrow Tip (Target)
|
|
1428
|
-
* - PT2: Left Base
|
|
1429
|
-
* - PT3: Right Base
|
|
1430
|
-
*
|
|
1431
|
-
* Logic:
|
|
1432
|
-
* - Shaft: Midpoint(PT2, PT3) -> PT1
|
|
1433
|
-
* - Base Line: PT2 -> PT3
|
|
1434
|
-
* - Wings: Splayed out from PT2 and PT3
|
|
1435
|
-
*/
|
|
1436
|
-
function createAttackByFire(coordinates, options = {}) {
|
|
1437
|
-
const featureProps = {};
|
|
1438
|
-
const arrowTipWidthRatio = options.arrowTipWidthRatio ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.arrowTipWidthRatio;
|
|
1439
|
-
const backLineLengthRatio = options.backLineLengthRatio ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.backLineLengthRatio;
|
|
1440
|
-
const backLineAngle = options.backLineAngle ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.backLineAngle;
|
|
1441
|
-
const c1 = coordinates[0];
|
|
1442
|
-
const c2 = coordinates[1];
|
|
1443
|
-
const c3 = coordinates[2];
|
|
1444
|
-
const p1 = project(c1[0], c1[1]);
|
|
1445
|
-
const p2 = project(c2[0], c2[1]);
|
|
1446
|
-
const p3 = project(c3[0], c3[1]);
|
|
1447
|
-
const frame = createProjectedBaselineFrame(p2, p3, {
|
|
1448
|
-
origin: "midpoint",
|
|
1449
|
-
normal: "right"
|
|
1450
|
-
});
|
|
1451
|
-
if (!frame) return {
|
|
1452
|
-
type: "FeatureCollection",
|
|
1453
|
-
features: []
|
|
1454
|
-
};
|
|
1455
|
-
const pMid = frame.origin;
|
|
1456
|
-
const signedTipDistance = vecDot(vecSub(p1, pMid), frame.normal);
|
|
1457
|
-
if (Math.abs(signedTipDistance) < 1e-6) return {
|
|
1458
|
-
type: "FeatureCollection",
|
|
1459
|
-
features: []
|
|
1460
|
-
};
|
|
1461
|
-
const pTip = vecAdd(pMid, vecScale(frame.normal, signedTipDistance));
|
|
1462
|
-
const dirShaft = signedTipDistance >= 0 ? frame.normal : vecScale(frame.normal, -1);
|
|
1463
|
-
const lenShaft = Math.abs(signedTipDistance);
|
|
1464
|
-
const lenBase = frame.length;
|
|
1465
|
-
const dirBase = frame.direction;
|
|
1466
|
-
const angleRad = backLineAngle * Math.PI / 180;
|
|
1467
|
-
const baseAngle = -Math.PI / 2;
|
|
1468
|
-
const angleLeft = baseAngle - angleRad;
|
|
1469
|
-
const dirBackLeft = [dirBase[0] * Math.cos(angleLeft) - dirBase[1] * Math.sin(angleLeft), dirBase[0] * Math.sin(angleLeft) + dirBase[1] * Math.cos(angleLeft)];
|
|
1470
|
-
const angleRight = baseAngle + angleRad;
|
|
1471
|
-
const dirBackRight = [dirBase[0] * Math.cos(angleRight) - dirBase[1] * Math.sin(angleRight), dirBase[0] * Math.sin(angleRight) + dirBase[1] * Math.cos(angleRight)];
|
|
1472
|
-
const backLen = lenBase * backLineLengthRatio;
|
|
1473
|
-
const p2Back = vecAdd(p2, vecScale(dirBackLeft, backLen));
|
|
1474
|
-
const p3Back = vecAdd(p3, vecScale(dirBackRight, backLen));
|
|
1475
|
-
const headWidth = lenShaft * arrowTipWidthRatio;
|
|
1476
|
-
const headFeatures = createArrowHead$2(pTip, dirShaft, headWidth, headWidth);
|
|
1477
|
-
return {
|
|
1478
|
-
type: "FeatureCollection",
|
|
1479
|
-
features: [
|
|
1480
|
-
{
|
|
1481
|
-
type: "Feature",
|
|
1482
|
-
properties: featureProps,
|
|
1483
|
-
geometry: {
|
|
1484
|
-
type: "LineString",
|
|
1485
|
-
coordinates: [unproject(p2[0], p2[1]), unproject(p2Back[0], p2Back[1])]
|
|
1486
|
-
}
|
|
1487
|
-
},
|
|
1488
|
-
{
|
|
1489
|
-
type: "Feature",
|
|
1490
|
-
properties: featureProps,
|
|
1491
|
-
geometry: {
|
|
1492
|
-
type: "LineString",
|
|
1493
|
-
coordinates: [unproject(p3[0], p3[1]), unproject(p3Back[0], p3Back[1])]
|
|
1494
|
-
}
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
type: "Feature",
|
|
1498
|
-
properties: featureProps,
|
|
1499
|
-
geometry: {
|
|
1500
|
-
type: "LineString",
|
|
1501
|
-
coordinates: [unproject(pMid[0], pMid[1]), unproject(pTip[0], pTip[1])]
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
{
|
|
1505
|
-
type: "Feature",
|
|
1506
|
-
properties: featureProps,
|
|
1507
|
-
geometry: {
|
|
1508
|
-
type: "LineString",
|
|
1509
|
-
coordinates: [unproject(p2[0], p2[1]), unproject(p3[0], p3[1])]
|
|
1510
|
-
}
|
|
1511
|
-
},
|
|
1512
|
-
...headFeatures.map((f) => ({
|
|
1513
|
-
...f,
|
|
1514
|
-
properties: featureProps
|
|
1515
|
-
}))
|
|
1516
|
-
]
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
function createArrowHead$2(tip, dir, length, width) {
|
|
1520
|
-
const norm = [-dir[1], dir[0]];
|
|
1521
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
1522
|
-
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
1523
|
-
const right = vecSub(base, vecScale(norm, width / 2));
|
|
1524
|
-
return [{
|
|
1525
|
-
type: "Feature",
|
|
1526
|
-
geometry: {
|
|
1527
|
-
type: "LineString",
|
|
1528
|
-
coordinates: [
|
|
1529
|
-
unproject(left[0], left[1]),
|
|
1530
|
-
unproject(tip[0], tip[1]),
|
|
1531
|
-
unproject(right[0], right[1])
|
|
1532
|
-
]
|
|
1533
|
-
},
|
|
1534
|
-
properties: {}
|
|
1535
|
-
}];
|
|
1536
|
-
}
|
|
1537
|
-
const ATTACK_BY_FIRE = defineControlMeasure({
|
|
1538
|
-
metadata: ATTACK_BY_FIRE_METADATA,
|
|
1539
|
-
generator: createAttackByFire,
|
|
1540
|
-
defaultOptions: DEFAULT_ATTACK_BY_FIRE_OPTIONS,
|
|
1541
|
-
rule: attackByFireDrawRule
|
|
1542
|
-
});
|
|
1543
|
-
//#endregion
|
|
1544
|
-
//#region src/generators/cm15-maneuver-areas/attackHelicopter.ts
|
|
1545
|
-
const DEFAULT_ATTACK_HELICOPTER_OPTIONS = {
|
|
1546
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
1547
|
-
roundedBends: false,
|
|
1548
|
-
bendSegments: 5,
|
|
1549
|
-
symbolHeightRatio: .45,
|
|
1550
|
-
triangleSizeRatio: .25,
|
|
1551
|
-
bottomBarWidthRatio: 1.5,
|
|
1552
|
-
bowtieWidthRatio: .8
|
|
1553
|
-
};
|
|
1554
|
-
const ATTACK_HELICOPTER_METADATA = {
|
|
1555
|
-
id: "attack-helicopter",
|
|
1556
|
-
name: "Attack Helicopter",
|
|
1557
|
-
description: "Attack helicopter axis of advance.",
|
|
1558
|
-
entity: "Maneuver Areas",
|
|
1559
|
-
entityType: "Axis of Advance",
|
|
1560
|
-
entitySubtype: "Attack Helicopter",
|
|
1561
|
-
value: "151402",
|
|
1562
|
-
minCoordinates: 3,
|
|
1563
|
-
geometry: "line",
|
|
1564
|
-
geometryTypes: ["LineString", "Polygon"],
|
|
1565
|
-
drawRule: "Axis1",
|
|
1566
|
-
params: [
|
|
1567
|
-
...ATTACK_SHAFT_PARAMS,
|
|
1568
|
-
{
|
|
1569
|
-
key: "symbolHeightRatio",
|
|
1570
|
-
label: "Symbol height",
|
|
1571
|
-
description: "Height of the helicopter symbol as a ratio of the head width.",
|
|
1572
|
-
type: "number",
|
|
1573
|
-
min: .1,
|
|
1574
|
-
max: 1.5,
|
|
1575
|
-
step: .05
|
|
1576
|
-
},
|
|
1577
|
-
{
|
|
1578
|
-
key: "triangleSizeRatio",
|
|
1579
|
-
label: "Triangle size",
|
|
1580
|
-
description: "Size of the top triangle relative to the reference unit.",
|
|
1581
|
-
type: "number",
|
|
1582
|
-
min: .1,
|
|
1583
|
-
max: .8,
|
|
1584
|
-
step: .05
|
|
1585
|
-
},
|
|
1586
|
-
{
|
|
1587
|
-
key: "bottomBarWidthRatio",
|
|
1588
|
-
label: "Bottom bar width",
|
|
1589
|
-
description: "Width of the bottom bar relative to the reference triangle.",
|
|
1590
|
-
type: "number",
|
|
1591
|
-
min: .5,
|
|
1592
|
-
max: 3,
|
|
1593
|
-
step: .1
|
|
1594
|
-
},
|
|
1595
|
-
{
|
|
1596
|
-
key: "bowtieWidthRatio",
|
|
1597
|
-
label: "Bowtie width",
|
|
1598
|
-
description: "Size of the central bowtie relative to the reference triangle.",
|
|
1599
|
-
type: "number",
|
|
1600
|
-
min: .2,
|
|
1601
|
-
max: 2,
|
|
1602
|
-
step: .1
|
|
1603
|
-
}
|
|
1604
|
-
]
|
|
1605
|
-
};
|
|
1606
|
-
/**
|
|
1607
|
-
* Generates a GeoJSON FeatureCollection for an Attack Helicopter tactical symbol.
|
|
1608
|
-
*
|
|
1609
|
-
* The symbol is based on the Airborne Attack (bow tie) geometry but features
|
|
1610
|
-
* an additional symbol (vertical line + triangle) at the crossover point.
|
|
1611
|
-
*
|
|
1612
|
-
* @param coordinates - An array of GeoJSON Positions.
|
|
1613
|
-
* @param options - Configuration options.
|
|
1614
|
-
* @returns A GeoJSON FeatureCollection containing the main graphic and the symbol.
|
|
1615
|
-
*/
|
|
1616
|
-
function createAttackHelicopter(coordinates, options = {}) {
|
|
1617
|
-
const { geometry } = processAttackGeometry(coordinates, options);
|
|
1618
|
-
if (!geometry) return {
|
|
1619
|
-
type: "FeatureCollection",
|
|
1620
|
-
features: []
|
|
1621
|
-
};
|
|
1622
|
-
const shaftLeftToNeck = geometry.shaftLeft.slice(0, -1);
|
|
1623
|
-
const shaftRightToNeck = geometry.shaftRight.slice(0, -1);
|
|
1624
|
-
if (geometry.shaftLeft.length < 2 || geometry.shaftRight.length < 2) return {
|
|
1625
|
-
type: "FeatureCollection",
|
|
1626
|
-
features: []
|
|
1627
|
-
};
|
|
1628
|
-
const pL = geometry.shaftLeft[geometry.shaftLeft.length - 2];
|
|
1629
|
-
const pIR = geometry.headRing[3];
|
|
1630
|
-
const pR = geometry.shaftRight[geometry.shaftRight.length - 2];
|
|
1631
|
-
const pIL = geometry.headRing[5];
|
|
1632
|
-
if (!pL || !pIR || !pR || !pIL) return {
|
|
1633
|
-
type: "FeatureCollection",
|
|
1634
|
-
features: []
|
|
1635
|
-
};
|
|
1636
|
-
const center = lineIntersection(pL, pIR, pR, pIL);
|
|
1637
|
-
const features = [{
|
|
1638
|
-
type: "Feature",
|
|
1639
|
-
properties: {},
|
|
1640
|
-
geometry: {
|
|
1641
|
-
type: "LineString",
|
|
1642
|
-
coordinates: [
|
|
1643
|
-
...shaftLeftToNeck,
|
|
1644
|
-
pIR,
|
|
1645
|
-
geometry.headRing[2],
|
|
1646
|
-
geometry.headRing[1],
|
|
1647
|
-
geometry.headRing[0],
|
|
1648
|
-
pIL,
|
|
1649
|
-
...[...shaftRightToNeck].reverse()
|
|
1650
|
-
].map((p) => unproject(p[0], p[1]))
|
|
1651
|
-
}
|
|
1652
|
-
}];
|
|
1653
|
-
if (center) {
|
|
1654
|
-
const neck = geometry.ptNeck;
|
|
1655
|
-
const tip = geometry.ptTip;
|
|
1656
|
-
const d = vecSub(tip, neck);
|
|
1657
|
-
if (vecMag(d) > 0) {
|
|
1658
|
-
const u = vecNorm(d);
|
|
1659
|
-
const n = [-u[1], u[0]];
|
|
1660
|
-
const headWidth = Math.sqrt(Math.pow(geometry.headRing[0][0] - geometry.headRing[2][0], 2) + Math.pow(geometry.headRing[0][1] - geometry.headRing[2][1], 2));
|
|
1661
|
-
const symbolHeightRatio = options.symbolHeightRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.symbolHeightRatio;
|
|
1662
|
-
const triangleSizeRatio = options.triangleSizeRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.triangleSizeRatio;
|
|
1663
|
-
const bottomBarWidthRatio = options.bottomBarWidthRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.bottomBarWidthRatio;
|
|
1664
|
-
const bowtieWidthRatio = options.bowtieWidthRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.bowtieWidthRatio;
|
|
1665
|
-
const baseUnit = headWidth * .45;
|
|
1666
|
-
const shaftLength = headWidth * symbolHeightRatio;
|
|
1667
|
-
const triHeight = baseUnit * triangleSizeRatio;
|
|
1668
|
-
const triHalfWidth = triHeight * .8;
|
|
1669
|
-
const refTriHeight = baseUnit * .25;
|
|
1670
|
-
const refTriHalfWidth = refTriHeight * .8;
|
|
1671
|
-
const lineLengthUp = shaftLength - triHeight;
|
|
1672
|
-
const lineLengthDown = shaftLength;
|
|
1673
|
-
const pLineTop = vecAdd(center, vecScale(n, lineLengthUp));
|
|
1674
|
-
const pLineBottom = vecSub(center, vecScale(n, lineLengthDown));
|
|
1675
|
-
const verticalLineFeature = {
|
|
1676
|
-
type: "Feature",
|
|
1677
|
-
properties: {},
|
|
1678
|
-
geometry: {
|
|
1679
|
-
type: "LineString",
|
|
1680
|
-
coordinates: [unproject(pLineTop[0], pLineTop[1]), unproject(pLineBottom[0], pLineBottom[1])]
|
|
1681
|
-
}
|
|
1682
|
-
};
|
|
1683
|
-
const pTriTip = vecAdd(pLineTop, vecScale(n, triHeight));
|
|
1684
|
-
const pTriLeft = vecAdd(pLineTop, vecScale(u, triHalfWidth));
|
|
1685
|
-
const pTriRight = vecSub(pLineTop, vecScale(u, triHalfWidth));
|
|
1686
|
-
const triFeature = {
|
|
1687
|
-
type: "Feature",
|
|
1688
|
-
properties: { fill: "currentColor" },
|
|
1689
|
-
geometry: {
|
|
1690
|
-
type: "Polygon",
|
|
1691
|
-
coordinates: [[
|
|
1692
|
-
unproject(pTriTip[0], pTriTip[1]),
|
|
1693
|
-
unproject(pTriRight[0], pTriRight[1]),
|
|
1694
|
-
unproject(pTriLeft[0], pTriLeft[1]),
|
|
1695
|
-
unproject(pTriTip[0], pTriTip[1])
|
|
1696
|
-
]]
|
|
1697
|
-
}
|
|
1698
|
-
};
|
|
1699
|
-
const barHalfWidth = refTriHalfWidth * bottomBarWidthRatio;
|
|
1700
|
-
const pBarLeft = vecAdd(pLineBottom, vecScale(u, barHalfWidth));
|
|
1701
|
-
const pBarRight = vecSub(pLineBottom, vecScale(u, barHalfWidth));
|
|
1702
|
-
const barFeature = {
|
|
1703
|
-
type: "Feature",
|
|
1704
|
-
properties: {},
|
|
1705
|
-
geometry: {
|
|
1706
|
-
type: "LineString",
|
|
1707
|
-
coordinates: [unproject(pBarLeft[0], pBarLeft[1]), unproject(pBarRight[0], pBarRight[1])]
|
|
1708
|
-
}
|
|
1709
|
-
};
|
|
1710
|
-
const bowTieSize = refTriHeight * bowtieWidthRatio;
|
|
1711
|
-
const bowTieHalfWidth = bowTieSize * .6;
|
|
1712
|
-
const pBowTieRightBaseCenter = vecAdd(center, vecScale(u, bowTieSize));
|
|
1713
|
-
const pBowTieRightTop = vecAdd(pBowTieRightBaseCenter, vecScale(n, bowTieHalfWidth));
|
|
1714
|
-
const pBowTieRightBottom = vecSub(pBowTieRightBaseCenter, vecScale(n, bowTieHalfWidth));
|
|
1715
|
-
const bowTieRightFeature = {
|
|
1716
|
-
type: "Feature",
|
|
1717
|
-
properties: { fill: "currentColor" },
|
|
1718
|
-
geometry: {
|
|
1719
|
-
type: "Polygon",
|
|
1720
|
-
coordinates: [[
|
|
1721
|
-
unproject(center[0], center[1]),
|
|
1722
|
-
unproject(pBowTieRightTop[0], pBowTieRightTop[1]),
|
|
1723
|
-
unproject(pBowTieRightBottom[0], pBowTieRightBottom[1]),
|
|
1724
|
-
unproject(center[0], center[1])
|
|
1725
|
-
]]
|
|
1726
|
-
}
|
|
1727
|
-
};
|
|
1728
|
-
const pBowTieLeftBaseCenter = vecSub(center, vecScale(u, bowTieSize));
|
|
1729
|
-
const pBowTieLeftTop = vecAdd(pBowTieLeftBaseCenter, vecScale(n, bowTieHalfWidth));
|
|
1730
|
-
const pBowTieLeftBottom = vecSub(pBowTieLeftBaseCenter, vecScale(n, bowTieHalfWidth));
|
|
1731
|
-
const bowTieLeftFeature = {
|
|
1732
|
-
type: "Feature",
|
|
1733
|
-
properties: { fill: "currentColor" },
|
|
1734
|
-
geometry: {
|
|
1735
|
-
type: "Polygon",
|
|
1736
|
-
coordinates: [[
|
|
1737
|
-
unproject(center[0], center[1]),
|
|
1738
|
-
unproject(pBowTieLeftTop[0], pBowTieLeftTop[1]),
|
|
1739
|
-
unproject(pBowTieLeftBottom[0], pBowTieLeftBottom[1]),
|
|
1740
|
-
unproject(center[0], center[1])
|
|
1741
|
-
]]
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
features.push(verticalLineFeature, triFeature, barFeature, bowTieRightFeature, bowTieLeftFeature);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
return {
|
|
1748
|
-
type: "FeatureCollection",
|
|
1749
|
-
features
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
const ATTACK_HELICOPTER = defineControlMeasure({
|
|
1753
|
-
metadata: ATTACK_HELICOPTER_METADATA,
|
|
1754
|
-
generator: createAttackHelicopter,
|
|
1755
|
-
defaultOptions: DEFAULT_ATTACK_HELICOPTER_OPTIONS,
|
|
1756
|
-
rule: axis1DrawRule
|
|
1757
|
-
});
|
|
1758
|
-
//#endregion
|
|
1759
|
-
//#region src/generators/cm27-protection-areas/block.ts
|
|
1760
|
-
const DEFAULT_STEM_RATIO$1 = 1.5;
|
|
1761
|
-
const BLOCK_METADATA = {
|
|
1762
|
-
id: "block",
|
|
1763
|
-
name: "Block",
|
|
1764
|
-
description: "An obstacle effect that integrates fire planning and obstacle effort to stop an attacker along a specific avenue of approach or prevent the attacking force from passing through an engagement area. (FM 3-90)",
|
|
1765
|
-
entity: "Protection Areas",
|
|
1766
|
-
entityType: "Obstacle Effects",
|
|
1767
|
-
entitySubtype: "Block",
|
|
1768
|
-
value: "270501",
|
|
1769
|
-
minCoordinates: 2,
|
|
1770
|
-
geometry: "line",
|
|
1771
|
-
geometryTypes: ["LineString"],
|
|
1772
|
-
drawRule: "Area11"
|
|
1773
|
-
};
|
|
1774
|
-
const DEFAULT_BLOCK_OPTIONS = {};
|
|
1775
|
-
/**
|
|
1776
|
-
* Generates a GeoJSON FeatureCollection for a "Block" tactical symbol (T-shape).
|
|
1777
|
-
*
|
|
1778
|
-
* @param coordinates - An array of GeoJSON Positions (Lon/Lat).
|
|
1779
|
-
* Expects exactly 3 points:
|
|
1780
|
-
* - PT1 & PT2: Endpoints of the vertical line.
|
|
1781
|
-
* - PT3: Defines the endpoint of the horizontal line (stem).
|
|
1782
|
-
* @param options - Configuration options.
|
|
1783
|
-
* @returns A GeoJSON FeatureCollection containing a LineString.
|
|
1784
|
-
*/
|
|
1785
|
-
function createBlock(coordinates, _options = {}) {
|
|
1786
|
-
const p1 = project(coordinates[0][0], coordinates[0][1]);
|
|
1787
|
-
const p2 = project(coordinates[1][0], coordinates[1][1]);
|
|
1788
|
-
const frame = createProjectedBaselineFrame(p1, p2, {
|
|
1789
|
-
origin: "midpoint",
|
|
1790
|
-
normal: "right"
|
|
1791
|
-
});
|
|
1792
|
-
if (!frame) return {
|
|
1793
|
-
type: "FeatureCollection",
|
|
1794
|
-
features: []
|
|
1795
|
-
};
|
|
1796
|
-
const c3 = coordinates[2];
|
|
1797
|
-
const stemDistance = c3 ? frame.signedNormalDistance(c3) : frame.length * DEFAULT_STEM_RATIO$1;
|
|
1798
|
-
const mid = frame.origin;
|
|
1799
|
-
const pStemEnd = vecAdd(mid, vecScale(frame.normal, stemDistance));
|
|
1800
|
-
return {
|
|
1801
|
-
type: "FeatureCollection",
|
|
1802
|
-
features: [{
|
|
1803
|
-
type: "Feature",
|
|
1804
|
-
properties: {},
|
|
1805
|
-
geometry: {
|
|
1806
|
-
type: "LineString",
|
|
1807
|
-
coordinates: [
|
|
1808
|
-
unproject(p1[0], p1[1]),
|
|
1809
|
-
unproject(p2[0], p2[1]),
|
|
1810
|
-
unproject(mid[0], mid[1]),
|
|
1811
|
-
unproject(pStemEnd[0], pStemEnd[1])
|
|
1812
|
-
]
|
|
1813
|
-
}
|
|
1814
|
-
}]
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
const BLOCK = defineControlMeasure({
|
|
1818
|
-
metadata: BLOCK_METADATA,
|
|
1819
|
-
generator: createBlock,
|
|
1820
|
-
defaultOptions: DEFAULT_BLOCK_OPTIONS,
|
|
1821
|
-
rule: blockDrawRule
|
|
1822
|
-
});
|
|
1823
|
-
//#endregion
|
|
1824
|
-
//#region src/internal/angle-utils.ts
|
|
1825
|
-
/**
|
|
1826
|
-
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
1827
|
-
*/
|
|
1828
|
-
function normalizeRadians(angle) {
|
|
1829
|
-
let normalized = angle % (Math.PI * 2);
|
|
1830
|
-
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
1831
|
-
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
1832
|
-
return normalized;
|
|
1833
|
-
}
|
|
1834
|
-
/**
|
|
1835
|
-
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
1836
|
-
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
1837
|
-
*/
|
|
1838
|
-
function keepTextLeftToRight(rotation) {
|
|
1839
|
-
const normalized = normalizeRadians(rotation);
|
|
1840
|
-
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
1841
|
-
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
1842
|
-
return normalized;
|
|
1843
|
-
}
|
|
1844
|
-
//#endregion
|
|
1845
|
-
//#region src/generators/cm34-mission-tasks/shared.ts
|
|
1846
|
-
/**
|
|
1847
|
-
* Builds an open chevron arrowhead (left → tip → right) as unprojected
|
|
1848
|
-
* coordinates. The tip sits at `tip`, pointing along `dir`, with the base set
|
|
1849
|
-
* back by `length` and spanning `width` perpendicular to the shaft. Mirrors the
|
|
1850
|
-
* `createArrowHead` helper used by the attack-family generators.
|
|
1851
|
-
*/
|
|
1852
|
-
function createArrowHead$1(tip, dir, length, width) {
|
|
1853
|
-
const norm = [-dir[1], dir[0]];
|
|
1854
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
1855
|
-
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
1856
|
-
const right = vecSub(base, vecScale(norm, width / 2));
|
|
1857
|
-
return [
|
|
1858
|
-
unproject(left[0], left[1]),
|
|
1859
|
-
unproject(tip[0], tip[1]),
|
|
1860
|
-
unproject(right[0], right[1])
|
|
1861
|
-
];
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Builds the glyph label as a Point feature, forwarding any captured label-size
|
|
1865
|
-
* metadata to adapters for styling. `point` is in projected meters; the rotation
|
|
1866
|
-
* must already be in the generator's feature-rotation convention. Shared by every
|
|
1867
|
-
* mission-task measure that centers a glyph on its symbol.
|
|
1868
|
-
*/
|
|
1869
|
-
function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
1870
|
-
return {
|
|
1871
|
-
type: "Feature",
|
|
1872
|
-
properties: {
|
|
1873
|
-
part: "label",
|
|
1874
|
-
text,
|
|
1875
|
-
rotation,
|
|
1876
|
-
...labelSize.pixels !== void 0 ? { textSizePixels: labelSize.pixels } : {},
|
|
1877
|
-
...labelSize.zoom !== void 0 ? { textSizeZoom: labelSize.zoom } : {},
|
|
1878
|
-
...labelSize.resolution !== void 0 ? { textSizeResolution: labelSize.resolution } : {}
|
|
1879
|
-
},
|
|
1880
|
-
geometry: {
|
|
1881
|
-
type: "Point",
|
|
1882
|
-
coordinates: unproject(point[0], point[1])
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
/**
|
|
1887
|
-
* Shared scaffold for the "bracket" mission-task symbols (breach, bypass,
|
|
1888
|
-
* canalize). Owns the three-sided bracket: a front opening (PT. 1 / PT. 2), a
|
|
1889
|
-
* rear line parallel to it through PT. 3, and a glyph centered on the rear line.
|
|
1890
|
-
* Each measure supplies only its end caps and label glyph via
|
|
1891
|
-
* {@link BracketSymbolConfig}.
|
|
1892
|
-
*
|
|
1893
|
-
* Input Points:
|
|
1894
|
-
* - PT. 1: Front opening endpoint (top/left)
|
|
1895
|
-
* - PT. 2: Front opening endpoint (bottom/right) - defines height with PT. 1
|
|
1896
|
-
* - PT. 3: Rear point - defines length and orientation
|
|
1897
|
-
*
|
|
1898
|
-
* Geometric Construction:
|
|
1899
|
-
* 1. PT. 1 and PT. 2 define the front opening (facing enemy/objective)
|
|
1900
|
-
* 2. A parallel line is projected through PT. 3 with identical height
|
|
1901
|
-
* 3. LineString follows: PT. 1 → Rear Top → Rear Bottom → PT. 2
|
|
1902
|
-
* 4. Measure-specific end caps at PT. 1 and PT. 2
|
|
1903
|
-
* 5. Label point at midpoint of rear line (styled with OpenLayers Text)
|
|
1904
|
-
*
|
|
1905
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
1906
|
-
* @param config - Per-measure end caps, label glyph, and sizing
|
|
1907
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
1908
|
-
*/
|
|
1909
|
-
function createBracketSymbol(coordinates, config) {
|
|
1910
|
-
const [c1, c2, c3] = coordinates;
|
|
1911
|
-
const p1 = project(c1[0], c1[1]);
|
|
1912
|
-
const p2 = project(c2[0], c2[1]);
|
|
1913
|
-
const p3 = project(c3[0], c3[1]);
|
|
1914
|
-
const vFront = vecSub(p2, p1);
|
|
1915
|
-
const frontLength = vecMag(vFront);
|
|
1916
|
-
if (frontLength < 1e-6) return {
|
|
1917
|
-
type: "FeatureCollection",
|
|
1918
|
-
features: []
|
|
1919
|
-
};
|
|
1920
|
-
const dirFront = vecNorm(vFront);
|
|
1921
|
-
const frontMidpoint = vecScale(vecAdd(p1, p2), .5);
|
|
1922
|
-
const vToRear = vecSub(p3, frontMidpoint);
|
|
1923
|
-
const perpDir = [-dirFront[1], dirFront[0]];
|
|
1924
|
-
const perpDistance = vToRear[0] * perpDir[0] + vToRear[1] * perpDir[1];
|
|
1925
|
-
const halfFront = vecScale(dirFront, frontLength / 2);
|
|
1926
|
-
const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
|
|
1927
|
-
const rearTop = vecSub(rearMidpoint, halfFront);
|
|
1928
|
-
const rearBottom = vecAdd(rearMidpoint, halfFront);
|
|
1929
|
-
const gapHalf = frontLength * config.labelGapRatio / 2;
|
|
1930
|
-
const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
|
|
1931
|
-
const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
|
|
1932
|
-
const [topCap, bottomCap] = config.buildEndCaps({
|
|
1933
|
-
p1,
|
|
1934
|
-
p2,
|
|
1935
|
-
dirFront,
|
|
1936
|
-
perpDir,
|
|
1937
|
-
frontLength,
|
|
1938
|
-
rearTop
|
|
1939
|
-
});
|
|
1940
|
-
const mainFeature = {
|
|
1941
|
-
type: "Feature",
|
|
1942
|
-
properties: { part: config.part },
|
|
1943
|
-
geometry: {
|
|
1944
|
-
type: "MultiLineString",
|
|
1945
|
-
coordinates: [
|
|
1946
|
-
topCap,
|
|
1947
|
-
[
|
|
1948
|
-
unproject(p1[0], p1[1]),
|
|
1949
|
-
unproject(rearTop[0], rearTop[1]),
|
|
1950
|
-
unproject(gapPt1[0], gapPt1[1])
|
|
1951
|
-
],
|
|
1952
|
-
[
|
|
1953
|
-
unproject(gapPt2[0], gapPt2[1]),
|
|
1954
|
-
unproject(rearBottom[0], rearBottom[1]),
|
|
1955
|
-
unproject(p2[0], p2[1])
|
|
1956
|
-
],
|
|
1957
|
-
bottomCap
|
|
1958
|
-
]
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
|
|
1962
|
-
const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
|
|
1963
|
-
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
1964
|
-
return {
|
|
1965
|
-
type: "FeatureCollection",
|
|
1966
|
-
features: [mainFeature, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelSize)]
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
//#endregion
|
|
1970
|
-
//#region src/generators/cm34-mission-tasks/block.ts
|
|
1971
|
-
const DEFAULT_STEM_RATIO = 1.5;
|
|
1972
|
-
const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
|
|
1973
|
-
const BLOCK_MISSION_TASK_METADATA = {
|
|
1974
|
-
id: "block-mission-task",
|
|
1975
|
-
name: "Block",
|
|
1976
|
-
description: "A tactical mission task that denies the enemy access to an area or prevents enemy advance in a direction or along an avenue of approach.",
|
|
1977
|
-
entity: "Mission Tasks",
|
|
1978
|
-
entityType: "Block",
|
|
1979
|
-
value: "340100",
|
|
1980
|
-
minCoordinates: 2,
|
|
1981
|
-
geometry: "line",
|
|
1982
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
1983
|
-
drawRule: "Area11",
|
|
1984
|
-
capturesLabelSize: true,
|
|
1985
|
-
params: [{
|
|
1986
|
-
key: "labelGapRatio",
|
|
1987
|
-
label: "Label gap",
|
|
1988
|
-
description: "Size of the shaft gap holding the 'B' label, as a ratio of the front length.",
|
|
1989
|
-
type: "number",
|
|
1990
|
-
min: 0,
|
|
1991
|
-
max: 1,
|
|
1992
|
-
step: .01
|
|
1993
|
-
}]
|
|
1994
|
-
};
|
|
1995
|
-
/**
|
|
1996
|
-
* Generates a GeoJSON FeatureCollection for a BLOCK mission task symbol (340100).
|
|
1997
|
-
*
|
|
1998
|
-
* Uses the same T-shape geometry as the protection-area Block symbol, with a
|
|
1999
|
-
* "B" label centered on the shaft.
|
|
2000
|
-
*
|
|
2001
|
-
* @param coordinates - [PT. 1, PT. 2, optional PT. 3] as Position arrays
|
|
2002
|
-
* @param options - Configuration options
|
|
2003
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2004
|
-
*/
|
|
2005
|
-
function createBlockMissionTaskSymbol(coordinates, options = {}) {
|
|
2006
|
-
const labelGapRatio = options.labelGapRatio ?? DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelGapRatio;
|
|
2007
|
-
const p1 = project(coordinates[0][0], coordinates[0][1]);
|
|
2008
|
-
const p2 = project(coordinates[1][0], coordinates[1][1]);
|
|
2009
|
-
const frame = createProjectedBaselineFrame(p1, p2, {
|
|
2010
|
-
origin: "midpoint",
|
|
2011
|
-
normal: "right"
|
|
2012
|
-
});
|
|
2013
|
-
if (!frame) return {
|
|
2014
|
-
type: "FeatureCollection",
|
|
2015
|
-
features: []
|
|
2016
|
-
};
|
|
2017
|
-
const c3 = coordinates[2];
|
|
2018
|
-
const stemDistance = c3 ? frame.signedNormalDistance(c3) : frame.length * DEFAULT_STEM_RATIO;
|
|
2019
|
-
const mid = frame.origin;
|
|
2020
|
-
const pStemEnd = vecAdd(mid, vecScale(frame.normal, stemDistance));
|
|
2021
|
-
const labelPoint = vecAdd(mid, vecScale(frame.normal, stemDistance / 2));
|
|
2022
|
-
const stemDirection = vecScale(frame.normal, stemDistance < 0 ? -1 : 1);
|
|
2023
|
-
const gapHalf = Math.min(frame.length * labelGapRatio / 2, Math.abs(stemDistance) / 2);
|
|
2024
|
-
const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
|
|
2025
|
-
const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
|
|
2026
|
-
const shaftRotation = Math.atan2(frame.normal[1], frame.normal[0]);
|
|
2027
|
-
const renderedLabelRotation = Math.PI - shaftRotation;
|
|
2028
|
-
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
2029
|
-
return {
|
|
2030
|
-
type: "FeatureCollection",
|
|
2031
|
-
features: [{
|
|
2032
|
-
type: "Feature",
|
|
2033
|
-
properties: { part: "block" },
|
|
2034
|
-
geometry: {
|
|
2035
|
-
type: "MultiLineString",
|
|
2036
|
-
coordinates: [
|
|
2037
|
-
[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])],
|
|
2038
|
-
[unproject(mid[0], mid[1]), unproject(gapStart[0], gapStart[1])],
|
|
2039
|
-
[unproject(gapEnd[0], gapEnd[1]), unproject(pStemEnd[0], pStemEnd[1])]
|
|
2040
|
-
]
|
|
2041
|
-
}
|
|
2042
|
-
}, createLabelFeature(labelPoint, "B", labelRotation, {
|
|
2043
|
-
pixels: options.labelSizePixels,
|
|
2044
|
-
zoom: options.labelSizeZoom,
|
|
2045
|
-
resolution: options.labelSizeResolution
|
|
2046
|
-
})]
|
|
2047
|
-
};
|
|
2048
|
-
}
|
|
2049
|
-
const BLOCK_MISSION_TASK = defineControlMeasure({
|
|
2050
|
-
metadata: BLOCK_MISSION_TASK_METADATA,
|
|
2051
|
-
generator: createBlockMissionTaskSymbol,
|
|
2052
|
-
defaultOptions: DEFAULT_BLOCK_MISSION_TASK_OPTIONS,
|
|
2053
|
-
rule: blockDrawRule
|
|
2054
|
-
});
|
|
2055
|
-
//#endregion
|
|
2056
|
-
//#region src/generators/cm34-mission-tasks/breach.ts
|
|
2057
|
-
/**
|
|
2058
|
-
* Default options for the BREACH symbol.
|
|
2059
|
-
*/
|
|
2060
|
-
const DEFAULT_BREACH_OPTIONS = {
|
|
2061
|
-
tickLengthRatio: .15,
|
|
2062
|
-
tickAngle: 45,
|
|
2063
|
-
labelGapRatio: .2
|
|
2064
|
-
};
|
|
2065
|
-
const BREACH_METADATA = {
|
|
2066
|
-
id: "breach",
|
|
2067
|
-
name: "Breach",
|
|
2068
|
-
description: "A tactical mission task in which a unit breaks through or establishes a passage through an enemy obstacle.",
|
|
2069
|
-
entity: "Mission Tasks",
|
|
2070
|
-
entityType: "Breach",
|
|
2071
|
-
value: "340200",
|
|
2072
|
-
minCoordinates: 3,
|
|
2073
|
-
maxCoordinates: 3,
|
|
2074
|
-
geometry: "line",
|
|
2075
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
2076
|
-
drawRule: "Point12",
|
|
2077
|
-
capturesLabelSize: true,
|
|
2078
|
-
params: [
|
|
2079
|
-
{
|
|
2080
|
-
key: "tickLengthRatio",
|
|
2081
|
-
label: "Tick length",
|
|
2082
|
-
description: "Length of the outward end ticks, as a ratio of the front opening width.",
|
|
2083
|
-
type: "number",
|
|
2084
|
-
min: .01,
|
|
2085
|
-
max: 1,
|
|
2086
|
-
step: .01
|
|
2087
|
-
},
|
|
2088
|
-
{
|
|
2089
|
-
key: "tickAngle",
|
|
2090
|
-
label: "Tick angle",
|
|
2091
|
-
description: "Outward lean of the end ticks from perpendicular, in degrees.",
|
|
2092
|
-
type: "number",
|
|
2093
|
-
min: 0,
|
|
2094
|
-
max: 90,
|
|
2095
|
-
step: 1
|
|
2096
|
-
},
|
|
2097
|
-
{
|
|
2098
|
-
key: "labelGapRatio",
|
|
2099
|
-
label: "Label gap",
|
|
2100
|
-
description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length.",
|
|
2101
|
-
type: "number",
|
|
2102
|
-
min: 0,
|
|
2103
|
-
max: 1,
|
|
2104
|
-
step: .01
|
|
2105
|
-
}
|
|
2106
|
-
]
|
|
2107
|
-
};
|
|
2108
|
-
/**
|
|
2109
|
-
* Generates a GeoJSON FeatureCollection for a BREACH mission task symbol (340200).
|
|
2110
|
-
*
|
|
2111
|
-
* A three-sided bracket (see {@link createBracketSymbol}) whose front opening
|
|
2112
|
-
* endpoints terminate in tick marks pointing **outward** (away from the rear
|
|
2113
|
-
* "B" line).
|
|
2114
|
-
*
|
|
2115
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
2116
|
-
* @param options - Configuration options
|
|
2117
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2118
|
-
*/
|
|
2119
|
-
function createBreachSymbol(coordinates, options = {}) {
|
|
2120
|
-
const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_BREACH_OPTIONS.tickLengthRatio;
|
|
2121
|
-
const tickAngle = options.tickAngle ?? DEFAULT_BREACH_OPTIONS.tickAngle;
|
|
2122
|
-
return createBracketSymbol(coordinates, {
|
|
2123
|
-
part: "breach",
|
|
2124
|
-
labelText: "B",
|
|
2125
|
-
labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
|
|
2126
|
-
labelSize: {
|
|
2127
|
-
pixels: options.labelSizePixels,
|
|
2128
|
-
zoom: options.labelSizeZoom,
|
|
2129
|
-
resolution: options.labelSizeResolution
|
|
2130
|
-
},
|
|
2131
|
-
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
2132
|
-
const halfTick = frontLength * tickLengthRatio / 2;
|
|
2133
|
-
const tickAngleRad = tickAngle * Math.PI / 180;
|
|
2134
|
-
const cosTick = Math.cos(tickAngleRad);
|
|
2135
|
-
const sinTick = Math.sin(tickAngleRad);
|
|
2136
|
-
const outwardDir = [-perpDir[0], -perpDir[1]];
|
|
2137
|
-
const tick1Dir = vecAdd(vecScale(outwardDir, cosTick), vecScale(dirFront, -sinTick));
|
|
2138
|
-
const tick1Start = vecAdd(p1, vecScale(tick1Dir, -halfTick));
|
|
2139
|
-
const tick1End = vecAdd(p1, vecScale(tick1Dir, halfTick));
|
|
2140
|
-
const tick2Dir = vecAdd(vecScale(outwardDir, cosTick), vecScale(dirFront, sinTick));
|
|
2141
|
-
const tick2Start = vecAdd(p2, vecScale(tick2Dir, -halfTick));
|
|
2142
|
-
const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
|
|
2143
|
-
return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
|
|
2144
|
-
}
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
const BREACH = defineControlMeasure({
|
|
2148
|
-
metadata: BREACH_METADATA,
|
|
2149
|
-
generator: createBreachSymbol,
|
|
2150
|
-
defaultOptions: DEFAULT_BREACH_OPTIONS,
|
|
2151
|
-
rule: point12DrawRule
|
|
2152
|
-
});
|
|
2153
|
-
//#endregion
|
|
2154
|
-
//#region src/generators/cm34-mission-tasks/bypass.ts
|
|
2155
|
-
/**
|
|
2156
|
-
* Default options for the BYPASS symbol.
|
|
2157
|
-
*/
|
|
2158
|
-
const DEFAULT_BYPASS_OPTIONS = {
|
|
2159
|
-
arrowHeadLengthRatio: .12,
|
|
2160
|
-
labelGapRatio: .2
|
|
2161
|
-
};
|
|
2162
|
-
const BYPASS_METADATA = {
|
|
2163
|
-
id: "bypass",
|
|
2164
|
-
name: "Bypass",
|
|
2165
|
-
description: "A tactical mission task in which a unit maneuvers around an obstacle, position, or enemy force to maintain the momentum of the operation.",
|
|
2166
|
-
entity: "Mission Tasks",
|
|
2167
|
-
entityType: "Bypass",
|
|
2168
|
-
value: "340300",
|
|
2169
|
-
minCoordinates: 3,
|
|
2170
|
-
maxCoordinates: 3,
|
|
2171
|
-
geometry: "line",
|
|
2172
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
2173
|
-
drawRule: "Point12",
|
|
2174
|
-
capturesLabelSize: true,
|
|
2175
|
-
params: [{
|
|
2176
|
-
key: "arrowHeadLengthRatio",
|
|
2177
|
-
label: "Arrowhead length",
|
|
2178
|
-
description: "Length of the outward end chevrons, as a ratio of the front opening width.",
|
|
2179
|
-
type: "number",
|
|
2180
|
-
min: .01,
|
|
2181
|
-
max: 1,
|
|
2182
|
-
step: .01
|
|
2183
|
-
}, {
|
|
2184
|
-
key: "labelGapRatio",
|
|
2185
|
-
label: "Label gap",
|
|
2186
|
-
description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length.",
|
|
2187
|
-
type: "number",
|
|
2188
|
-
min: 0,
|
|
2189
|
-
max: 1,
|
|
2190
|
-
step: .01
|
|
2191
|
-
}]
|
|
2192
|
-
};
|
|
2193
|
-
/**
|
|
2194
|
-
* Generates a GeoJSON FeatureCollection for a BYPASS mission task symbol (340300).
|
|
2195
|
-
*
|
|
2196
|
-
* A three-sided bracket (see {@link createBracketSymbol}) with a "B" label on
|
|
2197
|
-
* the rear line, whose front opening endpoints terminate in outward-flaring
|
|
2198
|
-
* chevron arrowheads instead of tick marks.
|
|
2199
|
-
*
|
|
2200
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
2201
|
-
* @param options - Configuration options
|
|
2202
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2203
|
-
*/
|
|
2204
|
-
function createBypassSymbol(coordinates, options = {}) {
|
|
2205
|
-
const arrowHeadLengthRatio = options.arrowHeadLengthRatio ?? DEFAULT_BYPASS_OPTIONS.arrowHeadLengthRatio;
|
|
2206
|
-
return createBracketSymbol(coordinates, {
|
|
2207
|
-
part: "bypass",
|
|
2208
|
-
labelText: "B",
|
|
2209
|
-
labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
|
|
2210
|
-
labelSize: {
|
|
2211
|
-
pixels: options.labelSizePixels,
|
|
2212
|
-
zoom: options.labelSizeZoom,
|
|
2213
|
-
resolution: options.labelSizeResolution
|
|
2214
|
-
},
|
|
2215
|
-
buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
|
|
2216
|
-
const arrowHeadLength = frontLength * arrowHeadLengthRatio;
|
|
2217
|
-
const arrowDir = vecNorm(vecSub(p1, rearTop));
|
|
2218
|
-
return [createArrowHead$1(p1, arrowDir, arrowHeadLength, arrowHeadLength), createArrowHead$1(p2, arrowDir, arrowHeadLength, arrowHeadLength)];
|
|
2219
|
-
}
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
const BYPASS = defineControlMeasure({
|
|
2223
|
-
metadata: BYPASS_METADATA,
|
|
2224
|
-
generator: createBypassSymbol,
|
|
2225
|
-
defaultOptions: DEFAULT_BYPASS_OPTIONS,
|
|
2226
|
-
rule: point12DrawRule
|
|
2227
|
-
});
|
|
2228
|
-
//#endregion
|
|
2229
|
-
//#region src/generators/cm34-mission-tasks/canalize.ts
|
|
2230
|
-
/**
|
|
2231
|
-
* Default options for the CANALIZE symbol.
|
|
2232
|
-
*/
|
|
2233
|
-
const DEFAULT_CANALIZE_OPTIONS = {
|
|
2234
|
-
tickLengthRatio: .15,
|
|
2235
|
-
tickAngle: 45,
|
|
2236
|
-
labelGapRatio: .2
|
|
2237
|
-
};
|
|
2238
|
-
const CANALIZE_METADATA = {
|
|
2239
|
-
id: "canalize",
|
|
2240
|
-
name: "Canalize",
|
|
2241
|
-
description: "Canalize is a tactical mission task in which a unit restricts enemy movement to a narrow zone",
|
|
2242
|
-
entity: "Mission Tasks",
|
|
2243
|
-
entityType: "Canalize",
|
|
2244
|
-
value: "340400",
|
|
2245
|
-
minCoordinates: 3,
|
|
2246
|
-
maxCoordinates: 3,
|
|
2247
|
-
geometry: "line",
|
|
2248
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
2249
|
-
drawRule: "Point12",
|
|
2250
|
-
capturesLabelSize: true,
|
|
2251
|
-
params: [
|
|
2252
|
-
{
|
|
2253
|
-
key: "tickLengthRatio",
|
|
2254
|
-
label: "Tick length",
|
|
2255
|
-
description: "Length of the inward end ticks, as a ratio of the front opening width.",
|
|
2256
|
-
type: "number",
|
|
2257
|
-
min: .01,
|
|
2258
|
-
max: 1,
|
|
2259
|
-
step: .01
|
|
2260
|
-
},
|
|
2261
|
-
{
|
|
2262
|
-
key: "tickAngle",
|
|
2263
|
-
label: "Tick angle",
|
|
2264
|
-
description: "Inward lean of the end ticks from perpendicular, in degrees.",
|
|
2265
|
-
type: "number",
|
|
2266
|
-
min: 0,
|
|
2267
|
-
max: 90,
|
|
2268
|
-
step: 1
|
|
2269
|
-
},
|
|
2270
|
-
{
|
|
2271
|
-
key: "labelGapRatio",
|
|
2272
|
-
label: "Label gap",
|
|
2273
|
-
description: "Size of the rear-line gap holding the 'C' label, as a ratio of the front length.",
|
|
2274
|
-
type: "number",
|
|
2275
|
-
min: 0,
|
|
2276
|
-
max: 1,
|
|
2277
|
-
step: .01
|
|
2278
|
-
}
|
|
2279
|
-
]
|
|
2280
|
-
};
|
|
2281
|
-
/**
|
|
2282
|
-
* Generates a GeoJSON FeatureCollection for a CANALIZE mission task symbol (340400).
|
|
2283
|
-
*
|
|
2284
|
-
* A three-sided bracket (see {@link createBracketSymbol}) whose front opening
|
|
2285
|
-
* endpoints terminate in tick marks pointing **inward** (toward the rear "C"
|
|
2286
|
-
* line), opposite BREACH's outward-facing ticks.
|
|
2287
|
-
*
|
|
2288
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
2289
|
-
* @param options - Configuration options
|
|
2290
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2291
|
-
*/
|
|
2292
|
-
function createCanalizeSymbol(coordinates, options = {}) {
|
|
2293
|
-
const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_CANALIZE_OPTIONS.tickLengthRatio;
|
|
2294
|
-
const tickAngle = options.tickAngle ?? DEFAULT_CANALIZE_OPTIONS.tickAngle;
|
|
2295
|
-
return createBracketSymbol(coordinates, {
|
|
2296
|
-
part: "canalize",
|
|
2297
|
-
labelText: "C",
|
|
2298
|
-
labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
|
|
2299
|
-
labelSize: {
|
|
2300
|
-
pixels: options.labelSizePixels,
|
|
2301
|
-
zoom: options.labelSizeZoom,
|
|
2302
|
-
resolution: options.labelSizeResolution
|
|
2303
|
-
},
|
|
2304
|
-
buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
|
|
2305
|
-
const halfTick = frontLength * tickLengthRatio / 2;
|
|
2306
|
-
const tickAngleRad = tickAngle * Math.PI / 180;
|
|
2307
|
-
const cosTick = Math.cos(tickAngleRad);
|
|
2308
|
-
const sinTick = Math.sin(tickAngleRad);
|
|
2309
|
-
const tick1Dir = vecAdd(vecScale(perpDir, cosTick), vecScale(dirFront, -sinTick));
|
|
2310
|
-
const tick1Start = vecAdd(p1, vecScale(tick1Dir, -halfTick));
|
|
2311
|
-
const tick1End = vecAdd(p1, vecScale(tick1Dir, halfTick));
|
|
2312
|
-
const tick2Dir = vecAdd(vecScale(perpDir, cosTick), vecScale(dirFront, sinTick));
|
|
2313
|
-
const tick2Start = vecAdd(p2, vecScale(tick2Dir, -halfTick));
|
|
2314
|
-
const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
|
|
2315
|
-
return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
|
|
2316
|
-
}
|
|
2317
|
-
});
|
|
2318
|
-
}
|
|
2319
|
-
const CANALIZE = defineControlMeasure({
|
|
2320
|
-
metadata: CANALIZE_METADATA,
|
|
2321
|
-
generator: createCanalizeSymbol,
|
|
2322
|
-
defaultOptions: DEFAULT_CANALIZE_OPTIONS,
|
|
2323
|
-
rule: point12DrawRule
|
|
2324
|
-
});
|
|
2325
|
-
//#endregion
|
|
2326
|
-
//#region src/generators/cm34-mission-tasks/clear.ts
|
|
2327
|
-
/**
|
|
2328
|
-
* Default options for the CLEAR symbol.
|
|
2329
|
-
*/
|
|
2330
|
-
const DEFAULT_CLEAR_OPTIONS = {
|
|
2331
|
-
arrowInsetRatio: .15,
|
|
2332
|
-
arrowheadLengthRatio: .15,
|
|
2333
|
-
arrowheadWidthRatio: .2,
|
|
2334
|
-
labelGapRatio: .2
|
|
2335
|
-
};
|
|
2336
|
-
const CLEAR_METADATA = {
|
|
2337
|
-
id: "clear",
|
|
2338
|
-
name: "Clear",
|
|
2339
|
-
description: "Clear is a tactical mission task that requires a unit to remove all enemy forces and eliminate organized resistance within an assigned area.",
|
|
2340
|
-
entity: "Mission Tasks",
|
|
2341
|
-
entityType: "Clear",
|
|
2342
|
-
value: "340500",
|
|
2343
|
-
minCoordinates: 3,
|
|
2344
|
-
maxCoordinates: 3,
|
|
2345
|
-
geometry: "line",
|
|
2346
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
2347
|
-
drawRule: "Line23",
|
|
2348
|
-
capturesLabelSize: true,
|
|
2349
|
-
params: [
|
|
2350
|
-
{
|
|
2351
|
-
key: "arrowInsetRatio",
|
|
2352
|
-
label: "Arrow padding",
|
|
2353
|
-
description: "Padding between the line endpoints and the outer arrows, as a ratio of height.",
|
|
2354
|
-
type: "number",
|
|
2355
|
-
min: 0,
|
|
2356
|
-
max: .45,
|
|
2357
|
-
step: .01
|
|
2358
|
-
},
|
|
2359
|
-
{
|
|
2360
|
-
key: "arrowheadLengthRatio",
|
|
2361
|
-
label: "Arrowhead length",
|
|
2362
|
-
description: "Depth of each arrowhead toward the rear, as a ratio of the symbol height.",
|
|
2363
|
-
type: "number",
|
|
2364
|
-
min: .01,
|
|
2365
|
-
max: 1,
|
|
2366
|
-
step: .01
|
|
2367
|
-
},
|
|
2368
|
-
{
|
|
2369
|
-
key: "arrowheadWidthRatio",
|
|
2370
|
-
label: "Arrowhead width",
|
|
2371
|
-
description: "Width of each arrowhead along the vertical line, as a ratio of the height.",
|
|
2372
|
-
type: "number",
|
|
2373
|
-
min: .01,
|
|
2374
|
-
max: 1,
|
|
2375
|
-
step: .01
|
|
2376
|
-
},
|
|
2377
|
-
{
|
|
2378
|
-
key: "labelGapRatio",
|
|
2379
|
-
label: "Label gap",
|
|
2380
|
-
description: "Size of the shaft gap holding the 'C' label, as a ratio of the shaft length.",
|
|
2381
|
-
type: "number",
|
|
2382
|
-
min: 0,
|
|
2383
|
-
max: 1,
|
|
2384
|
-
step: .01
|
|
2385
|
-
}
|
|
2386
|
-
]
|
|
2387
|
-
};
|
|
2388
|
-
/**
|
|
2389
|
-
* Generates a GeoJSON FeatureCollection for a CLEAR mission task symbol (340500).
|
|
2390
|
-
*
|
|
2391
|
-
* A vertical line (PT. 1 → PT. 2) with three arrows pointing at it,
|
|
2392
|
-
* perpendicular to the line. The arrows come from the rear — the side PT. 3
|
|
2393
|
-
* sits on — and their tips touch the vertical line; each carries a shaft
|
|
2394
|
-
* reaching back to the rear. The arrows are evenly spaced along the line, so
|
|
2395
|
-
* the middle arrow's tip sits at the midpoint of the vertical line and its
|
|
2396
|
-
* shaft carries the 'C' glyph. The arrows stay perpendicular to the vertical
|
|
2397
|
-
* line at any rotation.
|
|
2398
|
-
*
|
|
2399
|
-
* Input Points:
|
|
2400
|
-
* - PT. 1: Top endpoint of the vertical line
|
|
2401
|
-
* - PT. 2: Bottom endpoint of the vertical line — defines height with PT. 1
|
|
2402
|
-
* - PT. 3: Rear point — defines the shaft length (and which side the arrows
|
|
2403
|
-
* come from)
|
|
2404
|
-
*
|
|
2405
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
2406
|
-
* @param options - Configuration options
|
|
2407
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2408
|
-
*/
|
|
2409
|
-
function createClearSymbol(coordinates, options = {}) {
|
|
2410
|
-
const arrowInsetRatio = Math.min(.49, Math.max(0, options.arrowInsetRatio ?? DEFAULT_CLEAR_OPTIONS.arrowInsetRatio));
|
|
2411
|
-
const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadLengthRatio;
|
|
2412
|
-
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
|
|
2413
|
-
const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
|
|
2414
|
-
const [c1, c2, c3] = coordinates;
|
|
2415
|
-
const p1 = project(c1[0], c1[1]);
|
|
2416
|
-
const p2 = project(c2[0], c2[1]);
|
|
2417
|
-
const p3 = project(c3[0], c3[1]);
|
|
2418
|
-
const vLine = vecSub(p2, p1);
|
|
2419
|
-
const height = vecMag(vLine);
|
|
2420
|
-
if (height < 1e-6) return {
|
|
2421
|
-
type: "FeatureCollection",
|
|
2422
|
-
features: []
|
|
2423
|
-
};
|
|
2424
|
-
const dirLine = vecScale(vLine, 1 / height);
|
|
2425
|
-
const midLine = vecScale(vecAdd(p1, p2), .5);
|
|
2426
|
-
const rawPerp = [-dirLine[1], dirLine[0]];
|
|
2427
|
-
const perpToRear = vecScale(rawPerp, vecDot(vecSub(p3, midLine), rawPerp) < 0 ? -1 : 1);
|
|
2428
|
-
const shaftLength = Math.abs(vecDot(vecSub(p3, midLine), rawPerp));
|
|
2429
|
-
const arrowDir = vecScale(perpToRear, -1);
|
|
2430
|
-
const arrowheadLength = height * arrowheadLengthRatio;
|
|
2431
|
-
const arrowheadWidth = height * arrowheadWidthRatio;
|
|
2432
|
-
const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
|
|
2433
|
-
const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
|
|
2434
|
-
const fullTs = [
|
|
2435
|
-
arrowInsetRatio,
|
|
2436
|
-
.5,
|
|
2437
|
-
1 - arrowInsetRatio
|
|
2438
|
-
];
|
|
2439
|
-
const labelIndex = 1;
|
|
2440
|
-
fullTs.forEach((t, i) => {
|
|
2441
|
-
const tip = tipAt(t);
|
|
2442
|
-
const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
|
|
2443
|
-
if (i === labelIndex && shaftLength > 1e-6) {
|
|
2444
|
-
const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
|
|
2445
|
-
const gapHalf = shaftLength * labelGapRatio / 2;
|
|
2446
|
-
const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
|
|
2447
|
-
const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
|
|
2448
|
-
lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
|
|
2449
|
-
lines.push([unproject(gapTip[0], gapTip[1]), unproject(tip[0], tip[1])]);
|
|
2450
|
-
} else if (shaftLength > 1e-6) lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(tip[0], tip[1])]);
|
|
2451
|
-
lines.push(createArrowHead$1(tip, arrowDir, arrowheadLength, arrowheadWidth));
|
|
2452
|
-
});
|
|
2453
|
-
const mainFeature = {
|
|
2454
|
-
type: "Feature",
|
|
2455
|
-
properties: { part: "clear" },
|
|
2456
|
-
geometry: {
|
|
2457
|
-
type: "MultiLineString",
|
|
2458
|
-
coordinates: lines
|
|
2459
|
-
}
|
|
2460
|
-
};
|
|
2461
|
-
const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
|
|
2462
|
-
const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
|
|
2463
|
-
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
2464
|
-
const labelTip = tipAt(fullTs[labelIndex]);
|
|
2465
|
-
return {
|
|
2466
|
-
type: "FeatureCollection",
|
|
2467
|
-
features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
|
|
2468
|
-
pixels: options.labelSizePixels,
|
|
2469
|
-
zoom: options.labelSizeZoom,
|
|
2470
|
-
resolution: options.labelSizeResolution
|
|
2471
|
-
})]
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
const CLEAR = defineControlMeasure({
|
|
2475
|
-
metadata: CLEAR_METADATA,
|
|
2476
|
-
generator: createClearSymbol,
|
|
2477
|
-
defaultOptions: DEFAULT_CLEAR_OPTIONS,
|
|
2478
|
-
rule: line23DrawRule
|
|
2479
|
-
});
|
|
2480
|
-
//#endregion
|
|
2481
|
-
//#region src/generators/cm34-mission-tasks/delay.ts
|
|
2482
|
-
const DEFAULT_DELAY_OPTIONS = {
|
|
2483
|
-
arrowheadLengthRatio: .12,
|
|
2484
|
-
arrowheadWidthRatio: .12,
|
|
2485
|
-
labelGapRatio: .18,
|
|
2486
|
-
arcSegments: 32
|
|
2487
|
-
};
|
|
2488
|
-
const DELAY_METADATA = {
|
|
2489
|
-
id: "delay",
|
|
2490
|
-
name: "Delay",
|
|
2491
|
-
description: "Delay is a tactical mission task in which a force under pressure trades space for time by slowing enemy momentum and inflicting maximum damage without becoming decisively engaged.",
|
|
2492
|
-
entity: "Mission Tasks",
|
|
2493
|
-
entityType: "Delay",
|
|
2494
|
-
value: "340800",
|
|
2495
|
-
minCoordinates: 3,
|
|
2496
|
-
maxCoordinates: 3,
|
|
2497
|
-
geometry: "line",
|
|
2498
|
-
geometryTypes: ["MultiLineString", "Point"],
|
|
2499
|
-
drawRule: "Line24",
|
|
2500
|
-
capturesLabelSize: true,
|
|
2501
|
-
params: [
|
|
2502
|
-
{
|
|
2503
|
-
key: "arrowheadLengthRatio",
|
|
2504
|
-
label: "Arrowhead length",
|
|
2505
|
-
description: "Depth of the arrowhead, as a ratio of the straight-line length.",
|
|
2506
|
-
type: "number",
|
|
2507
|
-
min: .01,
|
|
2508
|
-
max: 1,
|
|
2509
|
-
step: .01
|
|
2510
|
-
},
|
|
2511
|
-
{
|
|
2512
|
-
key: "arrowheadWidthRatio",
|
|
2513
|
-
label: "Arrowhead width",
|
|
2514
|
-
description: "Width of the arrowhead, as a ratio of the straight-line length.",
|
|
2515
|
-
type: "number",
|
|
2516
|
-
min: .01,
|
|
2517
|
-
max: 1,
|
|
2518
|
-
step: .01
|
|
2519
|
-
},
|
|
2520
|
-
{
|
|
2521
|
-
key: "labelGapRatio",
|
|
2522
|
-
label: "Label gap",
|
|
2523
|
-
description: "Size of the shaft gap holding the 'D' label, as a ratio of the shaft length.",
|
|
2524
|
-
type: "number",
|
|
2525
|
-
min: 0,
|
|
2526
|
-
max: 1,
|
|
2527
|
-
step: .01
|
|
2528
|
-
},
|
|
2529
|
-
{
|
|
2530
|
-
key: "arcSegments",
|
|
2531
|
-
label: "Arc segments",
|
|
2532
|
-
description: "Number of straight segments used to approximate the semicircular arc.",
|
|
2533
|
-
type: "number",
|
|
2534
|
-
min: 4,
|
|
2535
|
-
max: 96,
|
|
2536
|
-
step: 1
|
|
2537
|
-
}
|
|
2538
|
-
]
|
|
2539
|
-
};
|
|
2540
|
-
function createDelaySymbol(coordinates, options = {}) {
|
|
2541
|
-
const [c1, c2, c3] = coordinates;
|
|
2542
|
-
const p1 = project(c1[0], c1[1]);
|
|
2543
|
-
const p2 = project(c2[0], c2[1]);
|
|
2544
|
-
const p3 = project(c3[0], c3[1]);
|
|
2545
|
-
const line = vecSub(p2, p1);
|
|
2546
|
-
const lineLength = vecMag(line);
|
|
2547
|
-
if (lineLength < 1e-6) return {
|
|
2548
|
-
type: "FeatureCollection",
|
|
2549
|
-
features: []
|
|
2550
|
-
};
|
|
2551
|
-
const dirLine = vecScale(line, 1 / lineLength);
|
|
2552
|
-
const diameterAxis = [dirLine[1], -dirLine[0]];
|
|
2553
|
-
const signedDiameter = vecDot(vecSub(p3, p2), diameterAxis);
|
|
2554
|
-
const diameterEnd = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
|
|
2555
|
-
const diameter = Math.abs(signedDiameter);
|
|
2556
|
-
const gapHalf = lineLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_DELAY_OPTIONS.labelGapRatio)) / 2;
|
|
2557
|
-
const labelPoint = vecAdd(p1, vecScale(dirLine, lineLength / 2));
|
|
2558
|
-
const gapStart = vecAdd(labelPoint, vecScale(dirLine, -gapHalf));
|
|
2559
|
-
const gapEnd = vecAdd(labelPoint, vecScale(dirLine, gapHalf));
|
|
2560
|
-
const lines = [
|
|
2561
|
-
[unproject(p1[0], p1[1]), unproject(gapStart[0], gapStart[1])],
|
|
2562
|
-
[unproject(gapEnd[0], gapEnd[1]), unproject(p2[0], p2[1])],
|
|
2563
|
-
createArrowHead$1(p1, vecScale(dirLine, -1), lineLength * (options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio), lineLength * (options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio))
|
|
2564
|
-
];
|
|
2565
|
-
if (diameter >= 1e-6) lines.push(createSemicircleArc(p2, diameterEnd, dirLine, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments));
|
|
2566
|
-
const lineFeature = {
|
|
2567
|
-
type: "Feature",
|
|
2568
|
-
properties: { part: "delay" },
|
|
2569
|
-
geometry: {
|
|
2570
|
-
type: "MultiLineString",
|
|
2571
|
-
coordinates: lines
|
|
2572
|
-
}
|
|
2573
|
-
};
|
|
2574
|
-
const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
|
|
2575
|
-
return {
|
|
2576
|
-
type: "FeatureCollection",
|
|
2577
|
-
features: [lineFeature, createLabelFeature(labelPoint, "D", normalizeRadians(Math.PI + lineRotation), {
|
|
2578
|
-
pixels: options.labelSizePixels,
|
|
2579
|
-
zoom: options.labelSizeZoom,
|
|
2580
|
-
resolution: options.labelSizeResolution
|
|
2581
|
-
})]
|
|
2582
|
-
};
|
|
2583
|
-
}
|
|
2584
|
-
function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
|
|
2585
|
-
const diameterVector = vecSub(end, start);
|
|
2586
|
-
const diameter = vecMag(diameterVector);
|
|
2587
|
-
if (diameter < 1e-6) return [unproject(start[0], start[1])];
|
|
2588
|
-
const diameterDir = vecNorm(diameterVector);
|
|
2589
|
-
const center = vecAdd(start, vecScale(diameterVector, .5));
|
|
2590
|
-
const radius = diameter / 2;
|
|
2591
|
-
const segments = Math.max(4, Math.round(requestedSegments));
|
|
2592
|
-
const points = [];
|
|
2593
|
-
for (let i = 0; i <= segments; i += 1) {
|
|
2594
|
-
const theta = i / segments * Math.PI;
|
|
2595
|
-
const point = vecAdd(center, vecAdd(vecScale(diameterDir, -Math.cos(theta) * radius), vecScale(bulgeDir, Math.sin(theta) * radius)));
|
|
2596
|
-
points.push(unproject(point[0], point[1]));
|
|
2597
|
-
}
|
|
2598
|
-
return points;
|
|
2599
|
-
}
|
|
2600
|
-
const DELAY = defineControlMeasure({
|
|
2601
|
-
metadata: DELAY_METADATA,
|
|
2602
|
-
generator: createDelaySymbol,
|
|
2603
|
-
defaultOptions: DEFAULT_DELAY_OPTIONS,
|
|
2604
|
-
rule: line24DrawRule
|
|
2605
|
-
});
|
|
2606
|
-
//#endregion
|
|
2607
|
-
//#region src/internal/arrowhead.ts
|
|
2608
|
-
/**
|
|
2609
|
-
* Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
|
|
2610
|
-
*
|
|
2611
|
-
* `tip` is the point of the arrow, `dir` the unit direction the arrow points,
|
|
2612
|
-
* `length` the distance from tip back to the base, and `width` the base width.
|
|
2613
|
-
* The returned ring is `[left, tip, right, left]`.
|
|
2614
|
-
*/
|
|
2615
|
-
function createArrowHeadRing(tip, dir, length, width) {
|
|
2616
|
-
const perp = [-dir[1], dir[0]];
|
|
2617
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
2618
|
-
const left = vecAdd(base, vecScale(perp, width / 2));
|
|
2619
|
-
const right = vecSub(base, vecScale(perp, width / 2));
|
|
2620
|
-
return [
|
|
2621
|
-
unproject(left[0], left[1]),
|
|
2622
|
-
unproject(tip[0], tip[1]),
|
|
2623
|
-
unproject(right[0], right[1]),
|
|
2624
|
-
unproject(left[0], left[1])
|
|
2625
|
-
];
|
|
2626
|
-
}
|
|
2627
|
-
//#endregion
|
|
2628
|
-
//#region src/generators/cm27-protection-areas/disrupt.ts
|
|
2629
|
-
const DEFAULT_DISRUPT_OPTIONS = {
|
|
2630
|
-
middleArrowLengthRatio: .65,
|
|
2631
|
-
bottomArrowLengthRatio: .45,
|
|
2632
|
-
shaftLengthRatio: .65,
|
|
2633
|
-
arrowHeadLengthRatio: .2,
|
|
2634
|
-
arrowHeadWidthRatio: .2
|
|
2635
|
-
};
|
|
2636
|
-
const DISRUPT_METADATA = {
|
|
2637
|
-
id: "disrupt",
|
|
2638
|
-
name: "Disrupt",
|
|
2639
|
-
description: "Obstacle effect that disrupts enemy tempo, delays movement, forces early breaching, and fragments attacks.",
|
|
2640
|
-
entity: "Protection Areas",
|
|
2641
|
-
entityType: "Obstacle Effects",
|
|
2642
|
-
entitySubtype: "Disrupt",
|
|
2643
|
-
value: "270502",
|
|
2644
|
-
minCoordinates: 2,
|
|
2645
|
-
geometry: "line",
|
|
2646
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
2647
|
-
drawRule: "Area12",
|
|
2648
|
-
params: [
|
|
2649
|
-
{
|
|
2650
|
-
key: "middleArrowLengthRatio",
|
|
2651
|
-
label: "Middle arrow length",
|
|
2652
|
-
description: "Middle arrow length as a ratio of the longest (top) arrow.",
|
|
2653
|
-
type: "number",
|
|
2654
|
-
min: .01,
|
|
2655
|
-
max: 1,
|
|
2656
|
-
step: .01
|
|
2657
|
-
},
|
|
2658
|
-
{
|
|
2659
|
-
key: "bottomArrowLengthRatio",
|
|
2660
|
-
label: "Bottom arrow length",
|
|
2661
|
-
description: "Bottom arrow length as a ratio of the longest (top) arrow.",
|
|
2662
|
-
type: "number",
|
|
2663
|
-
min: .01,
|
|
2664
|
-
max: 1,
|
|
2665
|
-
step: .01
|
|
2666
|
-
},
|
|
2667
|
-
{
|
|
2668
|
-
key: "shaftLengthRatio",
|
|
2669
|
-
label: "Shaft length",
|
|
2670
|
-
description: "Center shaft length as a ratio of the longest arrow, extending opposite.",
|
|
2671
|
-
type: "number",
|
|
2672
|
-
min: .01,
|
|
2673
|
-
max: 1,
|
|
2674
|
-
step: .01
|
|
2675
|
-
},
|
|
2676
|
-
{
|
|
2677
|
-
key: "arrowHeadLengthRatio",
|
|
2678
|
-
label: "Arrowhead length",
|
|
2679
|
-
description: "Arrowhead length as a ratio of the longest arrow.",
|
|
2680
|
-
type: "number",
|
|
2681
|
-
min: .01,
|
|
2682
|
-
max: 1,
|
|
2683
|
-
step: .01
|
|
2684
|
-
},
|
|
2685
|
-
{
|
|
2686
|
-
key: "arrowHeadWidthRatio",
|
|
2687
|
-
label: "Arrowhead width",
|
|
2688
|
-
description: "Arrowhead base width as a ratio of the longest arrow.",
|
|
2689
|
-
type: "number",
|
|
2690
|
-
min: .01,
|
|
2691
|
-
max: 1,
|
|
2692
|
-
step: .01
|
|
2693
|
-
}
|
|
2694
|
-
]
|
|
2695
|
-
};
|
|
2696
|
-
/**
|
|
2697
|
-
* Generates a GeoJSON FeatureCollection for a DISRUPT tactical symbol.
|
|
2698
|
-
*
|
|
2699
|
-
* Input:
|
|
2700
|
-
* - PT1: Top endpoint of the vertical spine
|
|
2701
|
-
* - PT2: Bottom endpoint of the vertical spine
|
|
2702
|
-
* - PT3: Reference point used to determine the longest-arrow offset
|
|
2703
|
-
*
|
|
2704
|
-
* Geometry:
|
|
2705
|
-
* - Spine: PT1 -> PT2
|
|
2706
|
-
* - PT2 -> PT1 defines the baseline orientation; arrow direction is constrained to its perpendicular axis
|
|
2707
|
-
* - PT3 is decomposed and projected onto the perpendicular axis through PT1 to derive a valid longest-arrow tip
|
|
2708
|
-
* - Three arrows placed at PT1, midpoint(PT1, PT2), and PT2
|
|
2709
|
-
* - Center shaft extends opposite arrow direction from midpoint with the same length as the middle arrow
|
|
2710
|
-
* - Top arrow is longest
|
|
2711
|
-
* - Middle and bottom arrows are proportional to the longest arrow
|
|
2712
|
-
* - Arrow heads are returned as filled triangle polygons
|
|
2713
|
-
*/
|
|
2714
|
-
function createDisrupt(coordinates, options = {}) {
|
|
2715
|
-
const c1 = coordinates[0];
|
|
2716
|
-
const c2 = coordinates[1];
|
|
2717
|
-
const c3 = coordinates[2];
|
|
2718
|
-
const middleArrowLengthRatio = options.middleArrowLengthRatio ?? DEFAULT_DISRUPT_OPTIONS.middleArrowLengthRatio;
|
|
2719
|
-
const bottomArrowLengthRatio = options.bottomArrowLengthRatio ?? DEFAULT_DISRUPT_OPTIONS.bottomArrowLengthRatio;
|
|
2720
|
-
const shaftLengthRatio = options.shaftLengthRatio ?? DEFAULT_DISRUPT_OPTIONS.shaftLengthRatio;
|
|
2721
|
-
const arrowHeadLengthRatio = options.arrowHeadLengthRatio ?? DEFAULT_DISRUPT_OPTIONS.arrowHeadLengthRatio;
|
|
2722
|
-
const arrowHeadWidthRatio = options.arrowHeadWidthRatio ?? DEFAULT_DISRUPT_OPTIONS.arrowHeadWidthRatio;
|
|
2723
|
-
const p1 = project(c1[0], c1[1]);
|
|
2724
|
-
const p2 = project(c2[0], c2[1]);
|
|
2725
|
-
const v12 = vecSub(p2, p1);
|
|
2726
|
-
const spineLength = vecMag(v12);
|
|
2727
|
-
if (spineLength < 1e-6) return {
|
|
2728
|
-
type: "FeatureCollection",
|
|
2729
|
-
features: []
|
|
2730
|
-
};
|
|
2731
|
-
const spineDir = vecNorm(v12);
|
|
2732
|
-
const normal = [spineDir[1], -spineDir[0]];
|
|
2733
|
-
const spineMid = vecScale(vecAdd(p1, p2), .5);
|
|
2734
|
-
let topArrowTip;
|
|
2735
|
-
let longestArrowLength;
|
|
2736
|
-
let arrowSideSign;
|
|
2737
|
-
if (c3) {
|
|
2738
|
-
const normalFromP1Raw = vecDot(vecSub(project(c3[0], c3[1]), p1), normal);
|
|
2739
|
-
const hintedSign = normalFromP1Raw >= 0 ? 1 : -1;
|
|
2740
|
-
let normalFromP1 = Number.isFinite(normalFromP1Raw) ? normalFromP1Raw : 0;
|
|
2741
|
-
if (Math.abs(normalFromP1) < 1e-6) normalFromP1 = hintedSign * spineLength * 1.5;
|
|
2742
|
-
topArrowTip = vecAdd(p1, vecScale(normal, normalFromP1));
|
|
2743
|
-
const topVec = vecSub(topArrowTip, p1);
|
|
2744
|
-
longestArrowLength = vecMag(topVec);
|
|
2745
|
-
arrowSideSign = vecDot(topVec, normal) >= 0 ? 1 : -1;
|
|
2746
|
-
} else {
|
|
2747
|
-
longestArrowLength = spineLength * 1.5;
|
|
2748
|
-
arrowSideSign = -1;
|
|
2749
|
-
topArrowTip = vecAdd(p1, vecScale(normal, arrowSideSign * longestArrowLength));
|
|
2750
|
-
}
|
|
2751
|
-
if (longestArrowLength < 1e-6) return {
|
|
2752
|
-
type: "FeatureCollection",
|
|
2753
|
-
features: []
|
|
2754
|
-
};
|
|
2755
|
-
const arrowStarts = [
|
|
2756
|
-
p1,
|
|
2757
|
-
spineMid,
|
|
2758
|
-
p2
|
|
2759
|
-
];
|
|
2760
|
-
const arrowLengthRatios = [
|
|
2761
|
-
1,
|
|
2762
|
-
middleArrowLengthRatio,
|
|
2763
|
-
bottomArrowLengthRatio
|
|
2764
|
-
];
|
|
2765
|
-
const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
|
|
2766
|
-
const polygons = [];
|
|
2767
|
-
const commonHeadLength = Math.max(longestArrowLength * arrowHeadLengthRatio, EPSILON);
|
|
2768
|
-
const commonHeadWidth = Math.max(longestArrowLength * arrowHeadWidthRatio, EPSILON);
|
|
2769
|
-
for (let i = 0; i < arrowStarts.length; i++) {
|
|
2770
|
-
const start = arrowStarts[i];
|
|
2771
|
-
const ratio = arrowLengthRatios[i];
|
|
2772
|
-
const signedLength = arrowSideSign * longestArrowLength * ratio;
|
|
2773
|
-
if (Math.abs(signedLength) < 1e-6) continue;
|
|
2774
|
-
const tip = i === 0 ? topArrowTip : vecAdd(start, vecScale(normal, signedLength));
|
|
2775
|
-
const dir = vecNorm(vecSub(tip, start));
|
|
2776
|
-
const headBase = vecSub(tip, vecScale(dir, commonHeadLength));
|
|
2777
|
-
lines.push([unproject(start[0], start[1]), unproject(headBase[0], headBase[1])]);
|
|
2778
|
-
polygons.push([createArrowHeadRing(tip, dir, commonHeadLength, commonHeadWidth)]);
|
|
2779
|
-
}
|
|
2780
|
-
const shaftLengthSigned = arrowSideSign * longestArrowLength * shaftLengthRatio;
|
|
2781
|
-
if (Math.abs(shaftLengthSigned) > 1e-6) {
|
|
2782
|
-
const shaftStart = vecSub(spineMid, vecScale(normal, shaftLengthSigned));
|
|
2783
|
-
lines.push([unproject(shaftStart[0], shaftStart[1]), unproject(spineMid[0], spineMid[1])]);
|
|
2784
|
-
}
|
|
2785
|
-
return {
|
|
2786
|
-
type: "FeatureCollection",
|
|
2787
|
-
features: [{
|
|
2788
|
-
type: "Feature",
|
|
2789
|
-
properties: {},
|
|
2790
|
-
geometry: {
|
|
2791
|
-
type: "MultiLineString",
|
|
2792
|
-
coordinates: lines
|
|
2793
|
-
}
|
|
2794
|
-
}, {
|
|
2795
|
-
type: "Feature",
|
|
2796
|
-
properties: { fill: true },
|
|
2797
|
-
geometry: {
|
|
2798
|
-
type: "MultiPolygon",
|
|
2799
|
-
coordinates: polygons
|
|
2800
|
-
}
|
|
2801
|
-
}]
|
|
2802
|
-
};
|
|
2803
|
-
}
|
|
2804
|
-
const DISRUPT = defineControlMeasure({
|
|
2805
|
-
metadata: DISRUPT_METADATA,
|
|
2806
|
-
generator: createDisrupt,
|
|
2807
|
-
defaultOptions: DEFAULT_DISRUPT_OPTIONS,
|
|
2808
|
-
rule: disruptDrawRule
|
|
2809
|
-
});
|
|
2810
|
-
//#endregion
|
|
2811
|
-
//#region src/internal/field-of-fire.ts
|
|
2812
|
-
/**
|
|
2813
|
-
* Builds the open (stroked) arrowhead at the outward tip of one arm. The two
|
|
2814
|
-
* barbs are sized relative to that arm's own length, so each arm's head scales
|
|
2815
|
-
* independently (Line3 — arms vary independently). Returns `null` when the arm
|
|
2816
|
-
* has collapsed to zero length.
|
|
2817
|
-
*/
|
|
2818
|
-
function arrowheadAt(vertex, tip, hSize, hWidth) {
|
|
2819
|
-
const v = vecSub(tip, vertex);
|
|
2820
|
-
const armLength = vecMag(v);
|
|
2821
|
-
if (armLength < 1e-6) return null;
|
|
2822
|
-
const dir = vecNorm(v);
|
|
2823
|
-
const perp = [-dir[1], dir[0]];
|
|
2824
|
-
const headLength = armLength * hSize;
|
|
2825
|
-
const halfWidth = headLength * hWidth;
|
|
2826
|
-
const base = vecSub(tip, vecScale(dir, headLength));
|
|
2827
|
-
return [
|
|
2828
|
-
vecAdd(base, vecScale(perp, halfWidth)),
|
|
2829
|
-
tip,
|
|
2830
|
-
vecSub(base, vecScale(perp, halfWidth))
|
|
2831
|
-
];
|
|
2832
|
-
}
|
|
2833
|
-
/**
|
|
2834
|
-
* Projects the vertex (PT 1) and up to two tips and builds the V of arms, each
|
|
2835
|
-
* arm ending in an open arrowhead. Renders one arm per tip, so two points
|
|
2836
|
-
* (vertex + one tip) draws the single arm being placed — the live feedback
|
|
2837
|
-
* after PT 1 — and three points draws the full V. Points beyond the third are
|
|
2838
|
-
* ignored (input contract, ADR-0014).
|
|
2839
|
-
*
|
|
2840
|
-
* Returns `null` if any arm is degenerate (collapsed to zero length), the
|
|
2841
|
-
* silent ADR-0014 empty-render signal.
|
|
2842
|
-
*/
|
|
2843
|
-
function buildFieldOfFireV(coordinates, hSize, hWidth) {
|
|
2844
|
-
const vertexCoord = coordinates[0];
|
|
2845
|
-
const vertex = project(vertexCoord[0], vertexCoord[1]);
|
|
2846
|
-
const tips = coordinates.slice(1, 3).map((c) => project(c[0], c[1]));
|
|
2847
|
-
const lines = [];
|
|
2848
|
-
for (const tip of tips) {
|
|
2849
|
-
const head = arrowheadAt(vertex, tip, hSize, hWidth);
|
|
2850
|
-
if (!head) return null;
|
|
2851
|
-
lines.push([vertex, tip], head);
|
|
2852
|
-
}
|
|
2853
|
-
return {
|
|
2854
|
-
vertex,
|
|
2855
|
-
tips,
|
|
2856
|
-
lines
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
//#endregion
|
|
2860
|
-
//#region src/generators/cm14-maneuver-lines/finalProtectiveFire.ts
|
|
2861
|
-
const BLADE_LENGTH_FRACTION = .8;
|
|
2862
|
-
const DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS = {
|
|
2863
|
-
hSize: .07,
|
|
2864
|
-
hWidth: .67,
|
|
2865
|
-
bladeHeight: .03
|
|
2866
|
-
};
|
|
2867
|
-
const FPF_SHARED = {
|
|
2868
|
-
entity: "Maneuver Lines",
|
|
2869
|
-
entityType: "Field of Fire",
|
|
2870
|
-
minCoordinates: 2,
|
|
2871
|
-
maxCoordinates: 3,
|
|
2872
|
-
geometry: "line",
|
|
2873
|
-
geometryTypes: ["MultiLineString", "Polygon"],
|
|
2874
|
-
drawRule: "Line3",
|
|
2875
|
-
params: [
|
|
2876
|
-
{
|
|
2877
|
-
key: "hSize",
|
|
2878
|
-
label: "Head size",
|
|
2879
|
-
description: "Arrowhead length as a fraction of each arm's length",
|
|
2880
|
-
type: "number",
|
|
2881
|
-
min: .01,
|
|
2882
|
-
max: .5,
|
|
2883
|
-
step: .01
|
|
2884
|
-
},
|
|
2885
|
-
{
|
|
2886
|
-
key: "hWidth",
|
|
2887
|
-
label: "Head width",
|
|
2888
|
-
description: "Arrowhead wing spread relative to the arrowhead length",
|
|
2889
|
-
type: "number",
|
|
2890
|
-
min: .05,
|
|
2891
|
-
max: 2,
|
|
2892
|
-
step: .01
|
|
2893
|
-
},
|
|
2894
|
-
{
|
|
2895
|
-
key: "bladeHeight",
|
|
2896
|
-
label: "Blade height",
|
|
2897
|
-
description: "Blade's perpendicular reach into the V, as a fraction of its arm's length",
|
|
2898
|
-
type: "number",
|
|
2899
|
-
min: .01,
|
|
2900
|
-
max: .5,
|
|
2901
|
-
step: .01
|
|
2902
|
-
}
|
|
2903
|
-
]
|
|
2904
|
-
};
|
|
2905
|
-
const FINAL_PROTECTIVE_FIRE_LEFT_METADATA = {
|
|
2906
|
-
...FPF_SHARED,
|
|
2907
|
-
id: "final-protective-fire-left",
|
|
2908
|
-
name: "Final Protective Fire (Left)",
|
|
2909
|
-
description: "An immediately available, prearranged barrier of fire designed to impede enemy movement across defensive lines or areas. The blade marks the left arm (PT 1 to PT 2).",
|
|
2910
|
-
entitySubtype: "Final Protective Fire (Left)",
|
|
2911
|
-
value: "140501"
|
|
2912
|
-
};
|
|
2913
|
-
const FINAL_PROTECTIVE_FIRE_RIGHT_METADATA = {
|
|
2914
|
-
...FPF_SHARED,
|
|
2915
|
-
id: "final-protective-fire-right",
|
|
2916
|
-
name: "Final Protective Fire (Right)",
|
|
2917
|
-
description: "An immediately available, prearranged barrier of fire designed to impede enemy movement across defensive lines or areas. The blade marks the right arm (PT 1 to PT 3).",
|
|
2918
|
-
entitySubtype: "Final Protective Fire (Right)",
|
|
2919
|
-
value: "140502"
|
|
2920
|
-
};
|
|
2921
|
-
/**
|
|
2922
|
-
* Builds the filled "blade": a uniform-width rectangle hugging the inside of
|
|
2923
|
-
* one arm. One long edge lies on the central 80% of the arm line (connected to
|
|
2924
|
-
* the line); the strip bulges perpendicularly into the inside of the V by
|
|
2925
|
-
* `bladeHeight * armLength`.
|
|
2926
|
-
*
|
|
2927
|
-
* @param vertex - PT 1, the shared vertex of the V (projected)
|
|
2928
|
-
* @param tip - the outward tip of the bladed arm (projected)
|
|
2929
|
-
* @param otherTip - the opposite arm's tip, used to find the "inside" side; may
|
|
2930
|
-
* be undefined during live single-arm feedback, in which case a default side
|
|
2931
|
-
* is chosen
|
|
2932
|
-
*/
|
|
2933
|
-
function bladeRectangle(vertex, tip, otherTip, bladeHeight) {
|
|
2934
|
-
const v = vecSub(tip, vertex);
|
|
2935
|
-
const armLength = vecMag(v);
|
|
2936
|
-
if (armLength < 1e-6) return null;
|
|
2937
|
-
const dir = vecNorm(v);
|
|
2938
|
-
let perp = [-dir[1], dir[0]];
|
|
2939
|
-
if (otherTip) {
|
|
2940
|
-
const toOther = vecSub(otherTip, vertex);
|
|
2941
|
-
if (vecDot(perp, toOther) < 0) perp = vecScale(perp, -1);
|
|
2942
|
-
}
|
|
2943
|
-
const gap = (1 - BLADE_LENGTH_FRACTION) / 2;
|
|
2944
|
-
const onLineNear = vecAdd(vertex, vecScale(dir, armLength * gap));
|
|
2945
|
-
const onLineFar = vecAdd(vertex, vecScale(dir, armLength * (1 - gap)));
|
|
2946
|
-
const height = vecScale(perp, armLength * bladeHeight);
|
|
2947
|
-
return [
|
|
2948
|
-
onLineNear,
|
|
2949
|
-
onLineFar,
|
|
2950
|
-
vecAdd(onLineFar, height),
|
|
2951
|
-
vecAdd(onLineNear, height)
|
|
2952
|
-
];
|
|
2953
|
-
}
|
|
2954
|
-
/**
|
|
2955
|
-
* Generates a GeoJSON FeatureCollection for a FINAL PROTECTIVE FIRE tactical
|
|
2956
|
-
* symbol: the Principal-Direction-of-Fire "V" (arms + open arrowheads) plus a
|
|
2957
|
-
* filled blade along the inside of one arm.
|
|
2958
|
-
*
|
|
2959
|
-
* Renders one arm per tip, so two points (vertex + one tip) draws the single
|
|
2960
|
-
* arm being placed — the live feedback after PT 1 — and three points draws the
|
|
2961
|
-
* full V. Points beyond the third are ignored (input contract, ADR-0014).
|
|
2962
|
-
*
|
|
2963
|
-
* @param coordinates - [PT 1 (vertex), PT 2 (tip), PT 3 (tip)]
|
|
2964
|
-
* @param bladedArm - which arm carries the blade: 0 = PT 2 (left), 1 = PT 3 (right)
|
|
2965
|
-
* @param options - Arrowhead and blade sizing parameters
|
|
2966
|
-
*/
|
|
2967
|
-
function createFinalProtectiveFire(coordinates, bladedArm, options = {}) {
|
|
2968
|
-
const { hSize, hWidth, bladeHeight } = {
|
|
2969
|
-
...DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS,
|
|
2970
|
-
...options
|
|
2971
|
-
};
|
|
2972
|
-
const v = buildFieldOfFireV(coordinates, Math.max(0, hSize), Math.max(0, hWidth));
|
|
2973
|
-
if (!v) return {
|
|
2974
|
-
type: "FeatureCollection",
|
|
2975
|
-
features: []
|
|
2976
|
-
};
|
|
2977
|
-
const features = [{
|
|
2978
|
-
type: "Feature",
|
|
2979
|
-
properties: {},
|
|
2980
|
-
geometry: {
|
|
2981
|
-
type: "MultiLineString",
|
|
2982
|
-
coordinates: v.lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
2983
|
-
}
|
|
2984
|
-
}];
|
|
2985
|
-
const bladeTip = v.tips[bladedArm];
|
|
2986
|
-
if (bladeTip) {
|
|
2987
|
-
const otherTip = v.tips[bladedArm === 0 ? 1 : 0];
|
|
2988
|
-
const blade = bladeRectangle(v.vertex, bladeTip, otherTip, Math.max(0, bladeHeight));
|
|
2989
|
-
if (blade) {
|
|
2990
|
-
const ring = [...blade, blade[0]].map((c) => unproject(c[0], c[1]));
|
|
2991
|
-
features.push({
|
|
2992
|
-
type: "Feature",
|
|
2993
|
-
properties: {
|
|
2994
|
-
part: "blade",
|
|
2995
|
-
fill: true
|
|
2996
|
-
},
|
|
2997
|
-
geometry: {
|
|
2998
|
-
type: "Polygon",
|
|
2999
|
-
coordinates: [ring]
|
|
3000
|
-
}
|
|
3001
|
-
});
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
return {
|
|
3005
|
-
type: "FeatureCollection",
|
|
3006
|
-
features
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
function createFinalProtectiveFireLeft(coordinates, options = {}) {
|
|
3010
|
-
return createFinalProtectiveFire(coordinates, 0, options);
|
|
3011
|
-
}
|
|
3012
|
-
function createFinalProtectiveFireRight(coordinates, options = {}) {
|
|
3013
|
-
return createFinalProtectiveFire(coordinates, 1, options);
|
|
3014
|
-
}
|
|
3015
|
-
const FINAL_PROTECTIVE_FIRE_LEFT = defineControlMeasure({
|
|
3016
|
-
metadata: FINAL_PROTECTIVE_FIRE_LEFT_METADATA,
|
|
3017
|
-
generator: createFinalProtectiveFireLeft,
|
|
3018
|
-
defaultOptions: DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS,
|
|
3019
|
-
rule: line3DrawRule
|
|
3020
|
-
});
|
|
3021
|
-
const FINAL_PROTECTIVE_FIRE_RIGHT = defineControlMeasure({
|
|
3022
|
-
metadata: FINAL_PROTECTIVE_FIRE_RIGHT_METADATA,
|
|
3023
|
-
generator: createFinalProtectiveFireRight,
|
|
3024
|
-
defaultOptions: DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS,
|
|
3025
|
-
rule: line3DrawRule
|
|
3026
|
-
});
|
|
3027
|
-
//#endregion
|
|
3028
|
-
//#region src/generators/cm27-protection-areas/fix.ts
|
|
3029
|
-
const DEFAULT_FIX_OPTIONS = {
|
|
3030
|
-
arrowHeadAngle: 35,
|
|
3031
|
-
arrowHeadSizeRatio: .16,
|
|
3032
|
-
endSegmentRatio: .2
|
|
3033
|
-
};
|
|
3034
|
-
const FIX_METADATA = {
|
|
3035
|
-
id: "fix",
|
|
3036
|
-
name: "Fix",
|
|
3037
|
-
description: "Fix task symbol with zigzag shaft and open arrowhead.",
|
|
3038
|
-
entity: "Protection Areas",
|
|
3039
|
-
entityType: "Obstacle Effects",
|
|
3040
|
-
entitySubtype: "Fix",
|
|
3041
|
-
value: "270503",
|
|
3042
|
-
minCoordinates: 2,
|
|
3043
|
-
maxCoordinates: 2,
|
|
3044
|
-
geometry: "line",
|
|
3045
|
-
geometryTypes: ["MultiLineString"],
|
|
3046
|
-
drawRule: "Line9",
|
|
3047
|
-
params: [
|
|
3048
|
-
{
|
|
3049
|
-
key: "arrowHeadAngle",
|
|
3050
|
-
label: "Arrowhead angle",
|
|
3051
|
-
description: "Half-angle of the open arrowhead wings, in degrees.",
|
|
3052
|
-
type: "number",
|
|
3053
|
-
min: 0,
|
|
3054
|
-
max: 90,
|
|
3055
|
-
step: 1
|
|
3056
|
-
},
|
|
3057
|
-
{
|
|
3058
|
-
key: "arrowHeadSizeRatio",
|
|
3059
|
-
label: "Arrowhead size",
|
|
3060
|
-
description: "Wing length as a ratio of the overall axis length.",
|
|
3061
|
-
type: "number",
|
|
3062
|
-
min: .01,
|
|
3063
|
-
max: 1,
|
|
3064
|
-
step: .01
|
|
3065
|
-
},
|
|
3066
|
-
{
|
|
3067
|
-
key: "endSegmentRatio",
|
|
3068
|
-
label: "End segment",
|
|
3069
|
-
description: "Flat end segment length as a ratio of the axis (capped at 0.35).",
|
|
3070
|
-
type: "number",
|
|
3071
|
-
min: 0,
|
|
3072
|
-
max: 1,
|
|
3073
|
-
step: .01
|
|
3074
|
-
}
|
|
3075
|
-
]
|
|
3076
|
-
};
|
|
3077
|
-
/**
|
|
3078
|
-
* Generates a GeoJSON FeatureCollection for a FIX tactical symbol.
|
|
3079
|
-
*
|
|
3080
|
-
* Input:
|
|
3081
|
-
* - PT1: Arrow tip end
|
|
3082
|
-
* - PT2: Opposite end
|
|
3083
|
-
*
|
|
3084
|
-
* Notes:
|
|
3085
|
-
* - Requires exactly two points.
|
|
3086
|
-
* - Produces one MultiLineString feature containing the zigzag body and open arrowhead wings.
|
|
3087
|
-
*/
|
|
3088
|
-
function createFix(coordinates, options = {}) {
|
|
3089
|
-
const c1 = coordinates[0];
|
|
3090
|
-
const c2 = coordinates[1];
|
|
3091
|
-
const arrowHeadAngle = options.arrowHeadAngle ?? DEFAULT_FIX_OPTIONS.arrowHeadAngle;
|
|
3092
|
-
const arrowHeadSizeRatio = options.arrowHeadSizeRatio ?? DEFAULT_FIX_OPTIONS.arrowHeadSizeRatio;
|
|
3093
|
-
const endSegmentRatio = options.endSegmentRatio ?? DEFAULT_FIX_OPTIONS.endSegmentRatio;
|
|
3094
|
-
const pTip = project(c1[0], c1[1]);
|
|
3095
|
-
const pTail = project(c2[0], c2[1]);
|
|
3096
|
-
const axis = vecSub(pTip, pTail);
|
|
3097
|
-
const axisLength = vecMag(axis);
|
|
3098
|
-
if (axisLength < 1e-6) return {
|
|
3099
|
-
type: "FeatureCollection",
|
|
3100
|
-
features: []
|
|
3101
|
-
};
|
|
3102
|
-
const dir = vecNorm(axis);
|
|
3103
|
-
const perp = [-dir[1], dir[0]];
|
|
3104
|
-
const flatLength = Math.min(axisLength * endSegmentRatio, axisLength * .35);
|
|
3105
|
-
const zigStart = vecAdd(pTail, vecScale(dir, flatLength));
|
|
3106
|
-
const zigEnd = vecSub(pTip, vecScale(dir, flatLength));
|
|
3107
|
-
const zigLength = vecMag(vecSub(zigEnd, zigStart));
|
|
3108
|
-
if (zigLength < 1e-6) return {
|
|
3109
|
-
type: "FeatureCollection",
|
|
3110
|
-
features: []
|
|
3111
|
-
};
|
|
3112
|
-
const headLength = Math.max(axisLength * arrowHeadSizeRatio, EPSILON);
|
|
3113
|
-
const headAngleRad = arrowHeadAngle * Math.PI / 180;
|
|
3114
|
-
const amplitude = Math.max(Math.abs(headLength * Math.sin(headAngleRad)), EPSILON);
|
|
3115
|
-
const zigP1 = vecAdd(zigStart, vecAdd(vecScale(dir, zigLength * .2), vecScale(perp, -amplitude)));
|
|
3116
|
-
const zigP2 = vecAdd(zigStart, vecAdd(vecScale(dir, zigLength * .5), vecScale(perp, amplitude)));
|
|
3117
|
-
const zigP3 = vecAdd(zigStart, vecAdd(vecScale(dir, zigLength * .8), vecScale(perp, -amplitude)));
|
|
3118
|
-
const mainLine = [
|
|
3119
|
-
unproject(pTail[0], pTail[1]),
|
|
3120
|
-
unproject(zigStart[0], zigStart[1]),
|
|
3121
|
-
unproject(zigP1[0], zigP1[1]),
|
|
3122
|
-
unproject(zigP2[0], zigP2[1]),
|
|
3123
|
-
unproject(zigP3[0], zigP3[1]),
|
|
3124
|
-
unproject(zigEnd[0], zigEnd[1]),
|
|
3125
|
-
unproject(pTip[0], pTip[1])
|
|
3126
|
-
];
|
|
3127
|
-
const backDir = vecScale(dir, -1);
|
|
3128
|
-
const wing1Dir = rotate$1(backDir, headAngleRad);
|
|
3129
|
-
const wing2Dir = rotate$1(backDir, -headAngleRad);
|
|
3130
|
-
const wing1End = vecAdd(pTip, vecScale(wing1Dir, headLength));
|
|
3131
|
-
const wing2End = vecAdd(pTip, vecScale(wing2Dir, headLength));
|
|
3132
|
-
return {
|
|
3133
|
-
type: "FeatureCollection",
|
|
3134
|
-
features: [{
|
|
3135
|
-
type: "Feature",
|
|
3136
|
-
properties: {},
|
|
3137
|
-
geometry: {
|
|
3138
|
-
type: "MultiLineString",
|
|
3139
|
-
coordinates: [
|
|
3140
|
-
mainLine,
|
|
3141
|
-
[unproject(pTip[0], pTip[1]), unproject(wing1End[0], wing1End[1])],
|
|
3142
|
-
[unproject(pTip[0], pTip[1]), unproject(wing2End[0], wing2End[1])]
|
|
3143
|
-
]
|
|
3144
|
-
}
|
|
3145
|
-
}]
|
|
3146
|
-
};
|
|
3147
|
-
}
|
|
3148
|
-
function rotate$1(v, angle) {
|
|
3149
|
-
const cosA = Math.cos(angle);
|
|
3150
|
-
const sinA = Math.sin(angle);
|
|
3151
|
-
return [v[0] * cosA - v[1] * sinA, v[0] * sinA + v[1] * cosA];
|
|
3152
|
-
}
|
|
3153
|
-
const FIX = defineControlMeasure({
|
|
3154
|
-
metadata: FIX_METADATA,
|
|
3155
|
-
generator: createFix,
|
|
3156
|
-
defaultOptions: DEFAULT_FIX_OPTIONS,
|
|
3157
|
-
rule: line9DrawRule
|
|
3158
|
-
});
|
|
3159
|
-
//#endregion
|
|
3160
|
-
//#region src/generators/cm14-maneuver-lines/flot.ts
|
|
3161
|
-
const DEFAULT_RADIUS = 50;
|
|
3162
|
-
const DEFAULT_RADIUS_PIXELS = 10;
|
|
3163
|
-
const DEFAULT_RESOLUTION = 16;
|
|
3164
|
-
/**
|
|
3165
|
-
* Default options for the FLOT graphic.
|
|
3166
|
-
*/
|
|
3167
|
-
const DEFAULT_FLOT_OPTIONS = {
|
|
3168
|
-
radius: DEFAULT_RADIUS,
|
|
3169
|
-
radiusPixels: DEFAULT_RADIUS_PIXELS,
|
|
3170
|
-
resolution: DEFAULT_RESOLUTION
|
|
3171
|
-
};
|
|
3172
|
-
const FLOT_METADATA = {
|
|
3173
|
-
id: "flot",
|
|
3174
|
-
name: "Forward line of own troops (FLOT)",
|
|
3175
|
-
description: "Indicates the most forward positions of friendly forces at a specific time.",
|
|
3176
|
-
entity: "Maneuver Lines",
|
|
3177
|
-
entityType: "Forward Line of Own Troops",
|
|
3178
|
-
value: "140100",
|
|
3179
|
-
minCoordinates: 2,
|
|
3180
|
-
geometry: "line",
|
|
3181
|
-
geometryTypes: ["LineString"],
|
|
3182
|
-
drawRule: "Line1",
|
|
3183
|
-
params: [
|
|
3184
|
-
{
|
|
3185
|
-
key: "radiusPixels",
|
|
3186
|
-
label: "Radius",
|
|
3187
|
-
description: "Radius of each scallop arc in screen pixels",
|
|
3188
|
-
type: "number",
|
|
3189
|
-
min: 5,
|
|
3190
|
-
max: 50,
|
|
3191
|
-
step: 1,
|
|
3192
|
-
unit: "px"
|
|
3193
|
-
},
|
|
3194
|
-
{
|
|
3195
|
-
key: "radius",
|
|
3196
|
-
label: "Radius",
|
|
3197
|
-
description: "Radius of each scallop arc in meters",
|
|
3198
|
-
type: "number",
|
|
3199
|
-
min: 1,
|
|
3200
|
-
max: 500,
|
|
3201
|
-
step: 1,
|
|
3202
|
-
unit: "m"
|
|
3203
|
-
},
|
|
3204
|
-
{
|
|
3205
|
-
key: "resolution",
|
|
3206
|
-
label: "Resolution",
|
|
3207
|
-
description: "Number of points used to render each semicircle",
|
|
3208
|
-
type: "number",
|
|
3209
|
-
min: 4,
|
|
3210
|
-
max: 32,
|
|
3211
|
-
step: 2
|
|
3212
|
-
}
|
|
3213
|
-
]
|
|
3214
|
-
};
|
|
3215
|
-
/**
|
|
3216
|
-
* Generates points for a true semicircle arc along a segment.
|
|
3217
|
-
*
|
|
3218
|
-
* The semicircle is centered at the midpoint of the diameter span,
|
|
3219
|
-
* with the specified radius. Points are generated by rotating
|
|
3220
|
-
* from PI to 0 around this center.
|
|
3221
|
-
*
|
|
3222
|
-
* @param startPoint - Start point of the arc on the segment
|
|
3223
|
-
* @param segmentDir - Direction vector of the segment (normalized)
|
|
3224
|
-
* @param perpDir - Perpendicular direction vector (normalized)
|
|
3225
|
-
* @param radius - Radius of the semicircle
|
|
3226
|
-
* @param resolution - Number of points in the arc
|
|
3227
|
-
* @param includeStart - Whether to include the start point
|
|
3228
|
-
* @returns Array of arc points forming a true semicircle
|
|
3229
|
-
*/
|
|
3230
|
-
function generateArcPoints(startPoint, segmentDir, perpDir, radius, resolution, includeStart) {
|
|
3231
|
-
const points = [];
|
|
3232
|
-
const startIndex = includeStart ? 0 : 1;
|
|
3233
|
-
const center = vecAdd(startPoint, vecScale(segmentDir, radius));
|
|
3234
|
-
for (let i = startIndex; i <= resolution; i++) {
|
|
3235
|
-
const t = i / resolution;
|
|
3236
|
-
const angle = Math.PI * (1 - t);
|
|
3237
|
-
const alongSegment = Math.cos(angle) * radius;
|
|
3238
|
-
const perpOffset = Math.sin(angle) * radius;
|
|
3239
|
-
const point = vecAdd(vecAdd(center, vecScale(segmentDir, alongSegment)), vecScale(perpDir, perpOffset));
|
|
3240
|
-
points.push(point);
|
|
3241
|
-
}
|
|
3242
|
-
return points;
|
|
3243
|
-
}
|
|
3244
|
-
/**
|
|
3245
|
-
* Processes a single segment between two positions, generating scalloped arcs.
|
|
3246
|
-
*
|
|
3247
|
-
* @param p1 - Start position (projected)
|
|
3248
|
-
* @param p2 - End position (projected)
|
|
3249
|
-
* @param radius - Radius of each semicircle
|
|
3250
|
-
* @param resolution - Points per arc
|
|
3251
|
-
* @param isFirstSegment - Whether this is the first segment (include start point)
|
|
3252
|
-
* @returns Array of points forming the scalloped line for this segment
|
|
3253
|
-
*/
|
|
3254
|
-
function processSegment(p1, p2, radius, resolution, isFirstSegment) {
|
|
3255
|
-
const segmentVec = vecSub(p2, p1);
|
|
3256
|
-
const segmentLength = vecMag(segmentVec);
|
|
3257
|
-
if (segmentLength < 1e-6) return isFirstSegment ? [p1] : [];
|
|
3258
|
-
const segmentDir = vecNorm(segmentVec);
|
|
3259
|
-
const perpDir = [-segmentDir[1], segmentDir[0]];
|
|
3260
|
-
const points = [];
|
|
3261
|
-
const diameter = 2 * radius;
|
|
3262
|
-
const numFullArcs = Math.floor(segmentLength / diameter);
|
|
3263
|
-
const remainingLength = segmentLength - numFullArcs * diameter;
|
|
3264
|
-
let currentPos = p1;
|
|
3265
|
-
for (let arcIndex = 0; arcIndex < numFullArcs; arcIndex++) {
|
|
3266
|
-
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, radius, resolution, arcIndex === 0 && isFirstSegment);
|
|
3267
|
-
points.push(...arcPoints);
|
|
3268
|
-
currentPos = vecAdd(currentPos, vecScale(segmentDir, diameter));
|
|
3269
|
-
}
|
|
3270
|
-
if (remainingLength > diameter * .1) {
|
|
3271
|
-
const partialRadius = remainingLength / 2;
|
|
3272
|
-
const partialResolution = Math.max(3, Math.round(resolution * (partialRadius / radius)));
|
|
3273
|
-
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, partialRadius, partialResolution, numFullArcs === 0 && isFirstSegment);
|
|
3274
|
-
points.push(...arcPoints);
|
|
3275
|
-
}
|
|
3276
|
-
return points;
|
|
3277
|
-
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Creates a FLOT (Forward Line of Own Troops) tactical graphic.
|
|
3280
|
-
*
|
|
3281
|
-
* Generates a scalloped/arc line representing the forward edge of friendly forces.
|
|
3282
|
-
* The line consists of semi-circular arcs that bulge perpendicular to the base path,
|
|
3283
|
-
* creating a distinctive visual pattern used in military tactical graphics.
|
|
3284
|
-
*
|
|
3285
|
-
* @param positions - Array of GeoJSON positions defining the base path
|
|
3286
|
-
* @param options - Configuration options for the graphic
|
|
3287
|
-
* @returns A GeoJSON FeatureCollection containing the scalloped LineString
|
|
3288
|
-
*
|
|
3289
|
-
* @example
|
|
3290
|
-
* ```typescript
|
|
3291
|
-
* const flot = createFLOT(
|
|
3292
|
-
* [[-122.4194, 37.7749], [-122.4100, 37.7800], [-122.4000, 37.7750]],
|
|
3293
|
-
* { radius: 100, resolution: 20 }
|
|
3294
|
-
* );
|
|
3295
|
-
* ```
|
|
3296
|
-
*/
|
|
3297
|
-
function createFLOT(positions, options = {}) {
|
|
3298
|
-
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, resolution = DEFAULT_RESOLUTION } = options;
|
|
3299
|
-
let effectiveRadius = radius;
|
|
3300
|
-
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
3301
|
-
const safeRadius = Math.max(EPSILON, effectiveRadius);
|
|
3302
|
-
const safeResolution = Math.max(3, Math.round(resolution));
|
|
3303
|
-
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
3304
|
-
const allPoints = [];
|
|
3305
|
-
for (let i = 0; i < projectedPoints.length - 1; i++) {
|
|
3306
|
-
const p1 = projectedPoints[i];
|
|
3307
|
-
const p2 = projectedPoints[i + 1];
|
|
3308
|
-
const segmentPoints = processSegment(p1, p2, safeRadius, safeResolution, i === 0);
|
|
3309
|
-
allPoints.push(...segmentPoints);
|
|
3310
|
-
}
|
|
3311
|
-
if (allPoints.length > 0 && projectedPoints.length > 1) {
|
|
3312
|
-
const lastOriginal = projectedPoints[projectedPoints.length - 1];
|
|
3313
|
-
const lastGenerated = allPoints[allPoints.length - 1];
|
|
3314
|
-
if (vecMag(vecSub(lastOriginal, lastGenerated)) > 1e-6) allPoints.push(lastOriginal);
|
|
3315
|
-
}
|
|
3316
|
-
return {
|
|
3317
|
-
type: "FeatureCollection",
|
|
3318
|
-
features: [{
|
|
3319
|
-
type: "Feature",
|
|
3320
|
-
properties: {},
|
|
3321
|
-
geometry: {
|
|
3322
|
-
type: "LineString",
|
|
3323
|
-
coordinates: allPoints.map((p) => unproject(p[0], p[1]))
|
|
3324
|
-
}
|
|
3325
|
-
}]
|
|
3326
|
-
};
|
|
3327
|
-
}
|
|
3328
|
-
const FLOT = defineControlMeasure({
|
|
3329
|
-
metadata: FLOT_METADATA,
|
|
3330
|
-
generator: createFLOT,
|
|
3331
|
-
defaultOptions: DEFAULT_FLOT_OPTIONS,
|
|
3332
|
-
rule: line1DrawRule
|
|
3333
|
-
});
|
|
3334
|
-
//#endregion
|
|
3335
|
-
//#region src/generators/cm29-protection-lines/fortifiedLine.ts
|
|
3336
|
-
const DEFAULT_SIZE = 50;
|
|
3337
|
-
/**
|
|
3338
|
-
* Default options for the Fortified Line graphic.
|
|
3339
|
-
*/
|
|
3340
|
-
const DEFAULT_FORTIFIED_LINE_OPTIONS = {
|
|
3341
|
-
size: DEFAULT_SIZE,
|
|
3342
|
-
sizePixels: 10
|
|
3343
|
-
};
|
|
3344
|
-
const FORTIFIED_LINE_METADATA = {
|
|
3345
|
-
id: "fortified-line",
|
|
3346
|
-
name: "Fortified Line",
|
|
3347
|
-
description: "Fortified line with castellated square-wave boundary.",
|
|
3348
|
-
entity: "Protection Lines",
|
|
3349
|
-
entityType: "Fortified Line",
|
|
3350
|
-
value: "290900",
|
|
3351
|
-
minCoordinates: 2,
|
|
3352
|
-
geometry: "line",
|
|
3353
|
-
geometryTypes: ["LineString"],
|
|
3354
|
-
drawRule: "Line1",
|
|
3355
|
-
params: FORTIFIED_PARAMS
|
|
3356
|
-
};
|
|
3357
|
-
/**
|
|
3358
|
-
* Generates points for a square wave "tooth" along a segment.
|
|
3359
|
-
*
|
|
3360
|
-
* A tooth consists of:
|
|
3361
|
-
* 1. Move along segment by length/4 (flat)
|
|
3362
|
-
* 2. Move out perpendicular by height (rise)
|
|
3363
|
-
* 3. Move along segment direction by length/2 (top)
|
|
3364
|
-
* 4. Move back perpendicular by height (drop)
|
|
3365
|
-
* 5. Move along segment by length/4 (flat)
|
|
3366
|
-
*
|
|
3367
|
-
* Original 'size' logic (where length=2*size, height=size):
|
|
3368
|
-
* - Flat1: size/2 = length/4
|
|
3369
|
-
* - Rise: size = height
|
|
3370
|
-
* - Top: size = length/2
|
|
3371
|
-
* - Drop: size = height
|
|
3372
|
-
* - Flat2: size/2 = length/4
|
|
3373
|
-
*
|
|
3374
|
-
* Total length consumed on segment = length
|
|
3375
|
-
*
|
|
3376
|
-
* @param startPoint - Start point of the tooth on the segment
|
|
3377
|
-
* @param segmentDir - Direction vector of the segment (normalized)
|
|
3378
|
-
* @param perpDir - Perpendicular direction vector (normalized)
|
|
3379
|
-
* @param length - Length of the tooth along the segment (period)
|
|
3380
|
-
* @param height - Height of the tooth (amplitude)
|
|
3381
|
-
* @returns Array of points forming a square tooth
|
|
3382
|
-
*/
|
|
3383
|
-
function generateSquareToothPoints(startPoint, segmentDir, perpDir, length, height) {
|
|
3384
|
-
const points = [];
|
|
3385
|
-
const p1 = vecAdd(startPoint, vecScale(segmentDir, length / 4));
|
|
3386
|
-
points.push(p1);
|
|
3387
|
-
const p2 = vecAdd(p1, vecScale(perpDir, height));
|
|
3388
|
-
points.push(p2);
|
|
3389
|
-
const p3 = vecAdd(p2, vecScale(segmentDir, length / 2));
|
|
3390
|
-
points.push(p3);
|
|
3391
|
-
const p4 = vecAdd(p3, vecScale(perpDir, -height));
|
|
3392
|
-
points.push(p4);
|
|
3393
|
-
return points;
|
|
3394
|
-
}
|
|
3395
|
-
/**
|
|
3396
|
-
* Processes a single segment between two positions, generating square waves.
|
|
3397
|
-
*
|
|
3398
|
-
* @param p1 - Start position (projected)
|
|
3399
|
-
* @param p2 - End position (projected)
|
|
3400
|
-
* @param size - Size (height) of the square teeth. Also used for target length (2*size).
|
|
3401
|
-
* @param isFirstSegment - Whether this is the first segment (include start point)
|
|
3402
|
-
* @returns Array of points forming the castellated line for this segment
|
|
3403
|
-
*/
|
|
3404
|
-
function processFortifiedSegment(p1, p2, size, isFirstSegment) {
|
|
3405
|
-
const segmentVec = vecSub(p2, p1);
|
|
3406
|
-
const segmentLength = vecMag(segmentVec);
|
|
3407
|
-
if (segmentLength < 1e-6) return isFirstSegment ? [p1] : [];
|
|
3408
|
-
const segmentDir = vecNorm(segmentVec);
|
|
3409
|
-
const perpDir = [-segmentDir[1], segmentDir[0]];
|
|
3410
|
-
const points = [];
|
|
3411
|
-
if (isFirstSegment) points.push(p1);
|
|
3412
|
-
const targetPeriod = 2 * size;
|
|
3413
|
-
let numFullPeriods = Math.round(segmentLength / targetPeriod);
|
|
3414
|
-
if (numFullPeriods < 1) numFullPeriods = 1;
|
|
3415
|
-
const actualPeriod = segmentLength / numFullPeriods;
|
|
3416
|
-
const height = size;
|
|
3417
|
-
let currentPos = p1;
|
|
3418
|
-
for (let i = 0; i < numFullPeriods; i++) {
|
|
3419
|
-
const toothPoints = generateSquareToothPoints(currentPos, segmentDir, perpDir, actualPeriod, height);
|
|
3420
|
-
points.push(...toothPoints);
|
|
3421
|
-
currentPos = vecAdd(currentPos, vecScale(segmentDir, actualPeriod));
|
|
3422
|
-
points.push(currentPos);
|
|
3423
|
-
}
|
|
3424
|
-
return points;
|
|
3425
|
-
}
|
|
3426
|
-
/**
|
|
3427
|
-
* Calculates the effective size based on options and projection.
|
|
3428
|
-
*/
|
|
3429
|
-
function calculateEffectiveSize(options) {
|
|
3430
|
-
const { size = DEFAULT_SIZE, sizePixels, metersPerPixel } = options;
|
|
3431
|
-
let effectiveSize = size;
|
|
3432
|
-
if (sizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveSize = sizePixels * metersPerPixel;
|
|
3433
|
-
return Math.max(EPSILON, effectiveSize);
|
|
3434
|
-
}
|
|
3435
|
-
/**
|
|
3436
|
-
* Generates the raw points for a fortified line/area boundary.
|
|
3437
|
-
*/
|
|
3438
|
-
function generateFortifiedPoints(projectedPoints, effectiveSize) {
|
|
3439
|
-
const allPoints = [];
|
|
3440
|
-
for (let i = 0; i < projectedPoints.length - 1; i++) {
|
|
3441
|
-
const p1 = projectedPoints[i];
|
|
3442
|
-
const p2 = projectedPoints[i + 1];
|
|
3443
|
-
const segmentPoints = processFortifiedSegment(p1, p2, effectiveSize, i === 0);
|
|
3444
|
-
allPoints.push(...segmentPoints);
|
|
3445
|
-
}
|
|
3446
|
-
if (allPoints.length > 0 && projectedPoints.length > 1) {
|
|
3447
|
-
const lastOriginal = projectedPoints[projectedPoints.length - 1];
|
|
3448
|
-
const lastGenerated = allPoints[allPoints.length - 1];
|
|
3449
|
-
if (vecMag(vecSub(lastOriginal, lastGenerated)) > 1e-6) allPoints.push(lastOriginal);
|
|
3450
|
-
}
|
|
3451
|
-
return allPoints;
|
|
3452
|
-
}
|
|
3453
|
-
/**
|
|
3454
|
-
* Creates a Fortified Line tactical graphic.
|
|
3455
|
-
*
|
|
3456
|
-
* Generates a castellated/square-wave line representing a fortified line.
|
|
3457
|
-
* The line consists of square "teeth" that project perpendicular to the base path.
|
|
3458
|
-
*
|
|
3459
|
-
* @param positions - Array of GeoJSON positions defining the base path
|
|
3460
|
-
* @param options - Configuration options for the graphic
|
|
3461
|
-
* @returns A GeoJSON FeatureCollection containing the castellated LineString
|
|
3462
|
-
*/
|
|
3463
|
-
function createFortifiedLine(positions, options = {}) {
|
|
3464
|
-
const safeSize = calculateEffectiveSize(options);
|
|
3465
|
-
return {
|
|
3466
|
-
type: "FeatureCollection",
|
|
3467
|
-
features: [{
|
|
3468
|
-
type: "Feature",
|
|
3469
|
-
properties: {},
|
|
3470
|
-
geometry: {
|
|
3471
|
-
type: "LineString",
|
|
3472
|
-
coordinates: generateFortifiedPoints(positions.map((p) => project(p[0], p[1])), safeSize).map((p) => unproject(p[0], p[1]))
|
|
3473
|
-
}
|
|
3474
|
-
}]
|
|
3475
|
-
};
|
|
3476
|
-
}
|
|
3477
|
-
const FORTIFIED_LINE = defineControlMeasure({
|
|
3478
|
-
metadata: FORTIFIED_LINE_METADATA,
|
|
3479
|
-
generator: createFortifiedLine,
|
|
3480
|
-
defaultOptions: DEFAULT_FORTIFIED_LINE_OPTIONS,
|
|
3481
|
-
rule: line1DrawRule
|
|
3482
|
-
});
|
|
3483
|
-
//#endregion
|
|
3484
|
-
//#region src/generators/cm15-maneuver-areas/fortifiedArea.ts
|
|
3485
|
-
const DEFAULT_FORTIFIED_AREA_OPTIONS = DEFAULT_FORTIFIED_LINE_OPTIONS;
|
|
3486
|
-
const FORTIFIED_AREA_METADATA = {
|
|
3487
|
-
id: "fortified-area",
|
|
3488
|
-
name: "Fortified Area",
|
|
3489
|
-
description: "A specified geographical locality with constructed defensive works as protection against attack.",
|
|
3490
|
-
entity: "Maneuver Areas",
|
|
3491
|
-
entityType: "Fortified Area",
|
|
3492
|
-
value: "151000",
|
|
3493
|
-
minCoordinates: 3,
|
|
3494
|
-
geometry: "area",
|
|
3495
|
-
geometryTypes: ["Polygon"],
|
|
3496
|
-
drawRule: "Area1",
|
|
3497
|
-
params: FORTIFIED_PARAMS$1
|
|
3498
|
-
};
|
|
3499
|
-
/**
|
|
3500
|
-
* Creates a Fortified Area tactical graphic.
|
|
3501
|
-
*
|
|
3502
|
-
* Generates a polygon with a castellated/square-wave boundary representing a fortified area.
|
|
3503
|
-
* The boundary consists of square "teeth" that project perpendicular to the path.
|
|
3504
|
-
*
|
|
3505
|
-
* @param positions - Array of GeoJSON positions defining the area boundary.
|
|
3506
|
-
* The first and last positions should match to close the polygon,
|
|
3507
|
-
* or it will be closed automatically.
|
|
3508
|
-
* @param options - Configuration options for the graphic
|
|
3509
|
-
* @returns A GeoJSON FeatureCollection containing the castellated Polygon
|
|
3510
|
-
*/
|
|
3511
|
-
function createFortifiedArea(positions, options = {}) {
|
|
3512
|
-
const safeSize = calculateEffectiveSize(options);
|
|
3513
|
-
const validPositions = positions;
|
|
3514
|
-
const first = validPositions[0];
|
|
3515
|
-
const last = validPositions[validPositions.length - 1];
|
|
3516
|
-
const boundaryPoints = generateFortifiedPoints((Math.abs(first[0] - last[0]) < Number.EPSILON && Math.abs(first[1] - last[1]) < Number.EPSILON ? validPositions : [...validPositions, first]).map((p) => project(p[0], p[1])), safeSize);
|
|
3517
|
-
if (boundaryPoints.length > 0) {
|
|
3518
|
-
const firstPt = boundaryPoints[0];
|
|
3519
|
-
const lastPt = boundaryPoints[boundaryPoints.length - 1];
|
|
3520
|
-
if (vecMag(vecSub(firstPt, lastPt)) > 1e-6) boundaryPoints.push(firstPt);
|
|
3521
|
-
else boundaryPoints[boundaryPoints.length - 1] = firstPt;
|
|
3522
|
-
}
|
|
3523
|
-
return {
|
|
3524
|
-
type: "FeatureCollection",
|
|
3525
|
-
features: [{
|
|
3526
|
-
type: "Feature",
|
|
3527
|
-
properties: {},
|
|
3528
|
-
geometry: {
|
|
3529
|
-
type: "Polygon",
|
|
3530
|
-
coordinates: [boundaryPoints.map((p) => unproject(p[0], p[1]))]
|
|
3531
|
-
}
|
|
3532
|
-
}]
|
|
3533
|
-
};
|
|
3534
|
-
}
|
|
3535
|
-
const FORTIFIED_AREA = defineControlMeasure({
|
|
3536
|
-
metadata: FORTIFIED_AREA_METADATA,
|
|
3537
|
-
generator: createFortifiedArea,
|
|
3538
|
-
defaultOptions: DEFAULT_FORTIFIED_AREA_OPTIONS
|
|
3539
|
-
});
|
|
3540
|
-
//#endregion
|
|
3541
|
-
//#region src/generators/cm34-mission-tasks/isolate.ts
|
|
3542
|
-
/**
|
|
3543
|
-
* Default options for the ISOLATE symbol.
|
|
3544
|
-
*/
|
|
3545
|
-
const DEFAULT_ISOLATE_OPTIONS = {
|
|
3546
|
-
openingDegrees: 30,
|
|
3547
|
-
barbCount: 9,
|
|
3548
|
-
barbLengthRatio: .18
|
|
3549
|
-
};
|
|
3550
|
-
const ISOLATE_METADATA = {
|
|
3551
|
-
id: "isolate",
|
|
3552
|
-
name: "Isolate",
|
|
3553
|
-
description: "Isolate is a tactical mission task in which a unit seals off an enemy, physically and psychologically, from sources of support and denies it freedom of movement.",
|
|
3554
|
-
entity: "Mission Tasks",
|
|
3555
|
-
entityType: "Isolate",
|
|
3556
|
-
value: "341500",
|
|
3557
|
-
minCoordinates: 2,
|
|
3558
|
-
maxCoordinates: 2,
|
|
3559
|
-
geometry: "line",
|
|
3560
|
-
geometryTypes: ["MultiLineString"],
|
|
3561
|
-
drawRule: "Area15",
|
|
3562
|
-
params: [
|
|
3563
|
-
{
|
|
3564
|
-
key: "openingDegrees",
|
|
3565
|
-
label: "Opening",
|
|
3566
|
-
description: "Angular width of the gap on the friendly side, in degrees.",
|
|
3567
|
-
type: "number",
|
|
3568
|
-
min: 0,
|
|
3569
|
-
max: 180,
|
|
3570
|
-
step: 1
|
|
3571
|
-
},
|
|
3572
|
-
{
|
|
3573
|
-
key: "barbCount",
|
|
3574
|
-
label: "Barbs",
|
|
3575
|
-
description: "Number of inward-pointing barbs spaced along the closed arc.",
|
|
3576
|
-
type: "number",
|
|
3577
|
-
min: 1,
|
|
3578
|
-
max: 24,
|
|
3579
|
-
step: 1
|
|
3580
|
-
},
|
|
3581
|
-
{
|
|
3582
|
-
key: "barbLengthRatio",
|
|
3583
|
-
label: "Barb length",
|
|
3584
|
-
description: "Length of each inward barb, as a ratio of the symbol radius.",
|
|
3585
|
-
type: "number",
|
|
3586
|
-
min: .01,
|
|
3587
|
-
max: 1,
|
|
3588
|
-
step: .01
|
|
3589
|
-
}
|
|
3590
|
-
]
|
|
3591
|
-
};
|
|
3592
|
-
/**
|
|
3593
|
-
* Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
|
|
3594
|
-
*
|
|
3595
|
-
* A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
|
|
3596
|
-
* starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
|
|
3597
|
-
* portion to a single arrow tip at the far end; the wedge-shaped gap (default
|
|
3598
|
-
* 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
|
|
3599
|
-
* Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
|
|
3600
|
-
* both ends so they never touch PT. 2 or the arrow tip, conveying the
|
|
3601
|
-
* sealing-off of the enclosed force. Unlike the bracket mission tasks, isolate
|
|
3602
|
-
* carries no glyph label.
|
|
3603
|
-
*
|
|
3604
|
-
* Input Points:
|
|
3605
|
-
* - PT. 1: Center point
|
|
3606
|
-
* - PT. 2: Start point — fixes the radius and the bearing of the opening
|
|
3607
|
-
*
|
|
3608
|
-
* @param coordinates - [PT. 1, PT. 2] as Position arrays
|
|
3609
|
-
* @param options - Configuration options
|
|
3610
|
-
* @returns GeoJSON FeatureCollection containing a single MultiLineString feature
|
|
3611
|
-
*/
|
|
3612
|
-
function createIsolateSymbol(coordinates, options = {}) {
|
|
3613
|
-
const openingDegrees = options.openingDegrees ?? DEFAULT_ISOLATE_OPTIONS.openingDegrees;
|
|
3614
|
-
const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
|
|
3615
|
-
const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
|
|
3616
|
-
const center = project(coordinates[0][0], coordinates[0][1]);
|
|
3617
|
-
const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
|
|
3618
|
-
const radius = vecMag(vRadius);
|
|
3619
|
-
if (radius < 1e-6) return {
|
|
3620
|
-
type: "FeatureCollection",
|
|
3621
|
-
features: []
|
|
3622
|
-
};
|
|
3623
|
-
const startAngle = Math.atan2(vRadius[1], vRadius[0]);
|
|
3624
|
-
const openingRad = Math.min(Math.max(openingDegrees, 0), 359) * Math.PI / 180;
|
|
3625
|
-
const arcSpan = 2 * Math.PI - openingRad;
|
|
3626
|
-
const endAngle = startAngle - arcSpan;
|
|
3627
|
-
const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
|
|
3628
|
-
const segments = Math.max(8, Math.round(64 * arcSpan / (2 * Math.PI)));
|
|
3629
|
-
const arc = [];
|
|
3630
|
-
for (let i = 0; i <= segments; i++) {
|
|
3631
|
-
const p = pointOnCircle(startAngle - arcSpan * i / segments);
|
|
3632
|
-
arc.push(unproject(p[0], p[1]));
|
|
3633
|
-
}
|
|
3634
|
-
const barbLength = radius * barbLengthRatio;
|
|
3635
|
-
const lines = [arc];
|
|
3636
|
-
const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
|
|
3637
|
-
lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
|
|
3638
|
-
const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
|
|
3639
|
-
const tipRadius = Math.max(0, radius - barbLength);
|
|
3640
|
-
const steps = Math.max(1, barbCount);
|
|
3641
|
-
for (let k = 1; k <= steps; k++) {
|
|
3642
|
-
const angle = startAngle - arcSpan * k / (steps + 1);
|
|
3643
|
-
const left = pointOnCircle(angle - halfWidth);
|
|
3644
|
-
const right = pointOnCircle(angle + halfWidth);
|
|
3645
|
-
const tip = pointOnCircle(angle, tipRadius);
|
|
3646
|
-
lines.push([
|
|
3647
|
-
unproject(left[0], left[1]),
|
|
3648
|
-
unproject(tip[0], tip[1]),
|
|
3649
|
-
unproject(right[0], right[1])
|
|
3650
|
-
]);
|
|
3651
|
-
}
|
|
3652
|
-
return {
|
|
3653
|
-
type: "FeatureCollection",
|
|
3654
|
-
features: [{
|
|
3655
|
-
type: "Feature",
|
|
3656
|
-
properties: { part: "isolate" },
|
|
3657
|
-
geometry: {
|
|
3658
|
-
type: "MultiLineString",
|
|
3659
|
-
coordinates: lines
|
|
3660
|
-
}
|
|
3661
|
-
}]
|
|
3662
|
-
};
|
|
3663
|
-
}
|
|
3664
|
-
const ISOLATE = defineControlMeasure({
|
|
3665
|
-
metadata: ISOLATE_METADATA,
|
|
3666
|
-
generator: createIsolateSymbol,
|
|
3667
|
-
defaultOptions: DEFAULT_ISOLATE_OPTIONS,
|
|
3668
|
-
rule: isolateDrawRule
|
|
3669
|
-
});
|
|
3670
|
-
//#endregion
|
|
3671
|
-
//#region src/generators/cm15-maneuver-areas/mainAttack.ts
|
|
3672
|
-
const DEFAULT_MAIN_ATTACK_OPTIONS = {
|
|
3673
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
3674
|
-
roundedBends: false,
|
|
3675
|
-
bendSegments: 5
|
|
3676
|
-
};
|
|
3677
|
-
const MAIN_ATTACK_METADATA = {
|
|
3678
|
-
id: "main-attack",
|
|
3679
|
-
name: "Main Attack",
|
|
3680
|
-
description: "Primary offensive attack graphic.",
|
|
3681
|
-
entity: "Maneuver Areas",
|
|
3682
|
-
entityType: "Axis of Advance",
|
|
3683
|
-
entitySubtype: "Main Attack",
|
|
3684
|
-
value: "151403",
|
|
3685
|
-
minCoordinates: 3,
|
|
3686
|
-
geometry: "line",
|
|
3687
|
-
geometryTypes: ["MultiLineString"],
|
|
3688
|
-
drawRule: "Axis1",
|
|
3689
|
-
params: ATTACK_SHAFT_PARAMS
|
|
3690
|
-
};
|
|
3691
|
-
/**
|
|
3692
|
-
* Generates a GeoJSON FeatureCollection for a Main Attack tactical symbol.
|
|
3693
|
-
*
|
|
3694
|
-
* The symbol consists of a single MultiLineString feature containing:
|
|
3695
|
-
* 1. **Arrowhead**: A line forming a "chevron" or "roof" shape.
|
|
3696
|
-
* 2. **Shaft**: Two parallel lines behind the arrow.
|
|
3697
|
-
*
|
|
3698
|
-
* The shaft is calculated to terminate exactly where it touches the inner walls
|
|
3699
|
-
* of the arrowhead, creating a seamless connection.
|
|
3700
|
-
*
|
|
3701
|
-
* @param coordinates - An array of GeoJSON Positions (Lon/Lat).
|
|
3702
|
-
* Minimum 3 points required.
|
|
3703
|
-
* **Order matters:** * - Index 0: **Tip** (The sharp point of the arrow)
|
|
3704
|
-
* - Index 1 to N-2: **Spine** (The path the shaft follows)
|
|
3705
|
-
* - Index N-1: **Width Control** (Determines the width of the arrowhead)
|
|
3706
|
-
* @param options - Configuration for ratios and properties.
|
|
3707
|
-
* @returns A GeoJSON FeatureCollection containing a single MultiLineString.
|
|
3708
|
-
*/
|
|
3709
|
-
function createMainAttack(coordinates, options = {}) {
|
|
3710
|
-
const { geometry } = processAttackGeometry(coordinates, options);
|
|
3711
|
-
if (!geometry) return {
|
|
3712
|
-
type: "FeatureCollection",
|
|
3713
|
-
features: []
|
|
3714
|
-
};
|
|
3715
|
-
return {
|
|
3716
|
-
type: "FeatureCollection",
|
|
3717
|
-
features: [{
|
|
3718
|
-
type: "Feature",
|
|
3719
|
-
properties: {},
|
|
3720
|
-
geometry: {
|
|
3721
|
-
type: "MultiLineString",
|
|
3722
|
-
coordinates: [
|
|
3723
|
-
geometry.shaftLeft.map((p) => unproject(p[0], p[1])),
|
|
3724
|
-
geometry.shaftRight.map((p) => unproject(p[0], p[1])),
|
|
3725
|
-
geometry.headRing.map((p) => unproject(p[0], p[1]))
|
|
3726
|
-
]
|
|
3727
|
-
}
|
|
3728
|
-
}]
|
|
3729
|
-
};
|
|
3730
|
-
}
|
|
3731
|
-
const MAIN_ATTACK = defineControlMeasure({
|
|
3732
|
-
metadata: MAIN_ATTACK_METADATA,
|
|
3733
|
-
generator: createMainAttack,
|
|
3734
|
-
defaultOptions: DEFAULT_MAIN_ATTACK_OPTIONS,
|
|
3735
|
-
rule: axis1DrawRule
|
|
3736
|
-
});
|
|
3737
|
-
//#endregion
|
|
3738
|
-
//#region src/generators/cm27-protection-areas/params.ts
|
|
3739
|
-
const OBSTACLE_BYPASS_PARAMS = [
|
|
3740
|
-
{
|
|
3741
|
-
key: "arrowHeadLengthRatio",
|
|
3742
|
-
label: "Arrowhead length",
|
|
3743
|
-
description: "Arrowhead length as a ratio of the opening width.",
|
|
3744
|
-
type: "number",
|
|
3745
|
-
min: .05,
|
|
3746
|
-
max: 1,
|
|
3747
|
-
step: .01
|
|
3748
|
-
},
|
|
3749
|
-
{
|
|
3750
|
-
key: "arrowHeadWidthRatio",
|
|
3751
|
-
label: "Arrowhead width",
|
|
3752
|
-
description: "Arrowhead base width as a ratio of the opening width.",
|
|
3753
|
-
type: "number",
|
|
3754
|
-
min: .05,
|
|
3755
|
-
max: 1,
|
|
3756
|
-
step: .01
|
|
3757
|
-
},
|
|
3758
|
-
{
|
|
3759
|
-
key: "defaultLengthRatio",
|
|
3760
|
-
label: "Default length",
|
|
3761
|
-
description: "Symbol depth as a ratio of opening width when no hint point is given.",
|
|
3762
|
-
type: "number",
|
|
3763
|
-
min: .1,
|
|
3764
|
-
max: 4,
|
|
3765
|
-
step: .05
|
|
3766
|
-
}
|
|
3767
|
-
];
|
|
3768
|
-
//#endregion
|
|
3769
|
-
//#region src/generators/cm27-protection-areas/obstacleBypassDifficult.ts
|
|
3770
|
-
const DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS = {
|
|
3771
|
-
arrowHeadLengthRatio: .2,
|
|
3772
|
-
arrowHeadWidthRatio: .2,
|
|
3773
|
-
defaultLengthRatio: 1.5,
|
|
3774
|
-
zigzagAmplitudeRatio: .12,
|
|
3775
|
-
zigzagSpacingRatio: .18
|
|
3776
|
-
};
|
|
3777
|
-
const OBSTACLE_BYPASS_DIFFICULT_METADATA = {
|
|
3778
|
-
id: "obstacle-bypass-difficult",
|
|
3779
|
-
name: "Obstacle Bypass Difficult",
|
|
3780
|
-
description: "Obstacle bypass (difficult) with zigzag rear line and arrowheads.",
|
|
3781
|
-
entity: "Protection Areas",
|
|
3782
|
-
entityType: "Obstacle Bypass",
|
|
3783
|
-
entitySubtype: "Difficult",
|
|
3784
|
-
value: "270602",
|
|
3785
|
-
minCoordinates: 2,
|
|
3786
|
-
geometry: "line",
|
|
3787
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
3788
|
-
drawRule: "Point12",
|
|
3789
|
-
params: [
|
|
3790
|
-
...OBSTACLE_BYPASS_PARAMS,
|
|
3791
|
-
{
|
|
3792
|
-
key: "zigzagAmplitudeRatio",
|
|
3793
|
-
label: "Zigzag amplitude",
|
|
3794
|
-
description: "Zigzag tooth depth as a ratio of the opening width.",
|
|
3795
|
-
type: "number",
|
|
3796
|
-
min: .01,
|
|
3797
|
-
max: 1,
|
|
3798
|
-
step: .01
|
|
3799
|
-
},
|
|
3800
|
-
{
|
|
3801
|
-
key: "zigzagSpacingRatio",
|
|
3802
|
-
label: "Zigzag spacing",
|
|
3803
|
-
description: "Spacing between zigzag teeth as a ratio of the opening width.",
|
|
3804
|
-
type: "number",
|
|
3805
|
-
min: .01,
|
|
3806
|
-
max: 1,
|
|
3807
|
-
step: .01
|
|
3808
|
-
}
|
|
3809
|
-
]
|
|
3810
|
-
};
|
|
3811
|
-
function createObstacleBypassDifficult(coordinates, options = {}) {
|
|
3812
|
-
const c1 = coordinates[0];
|
|
3813
|
-
const c2 = coordinates[1];
|
|
3814
|
-
const c3 = coordinates[2];
|
|
3815
|
-
const arrowHeadLengthRatio = options.arrowHeadLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS.arrowHeadLengthRatio;
|
|
3816
|
-
const arrowHeadWidthRatio = options.arrowHeadWidthRatio ?? DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS.arrowHeadWidthRatio;
|
|
3817
|
-
const defaultLengthRatio = options.defaultLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS.defaultLengthRatio;
|
|
3818
|
-
const zigzagAmplitudeRatio = options.zigzagAmplitudeRatio ?? DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS.zigzagAmplitudeRatio;
|
|
3819
|
-
const zigzagSpacingRatio = options.zigzagSpacingRatio ?? DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS.zigzagSpacingRatio;
|
|
3820
|
-
const p1 = project(c1[0], c1[1]);
|
|
3821
|
-
const p2 = project(c2[0], c2[1]);
|
|
3822
|
-
const openingVec = vecSub(p2, p1);
|
|
3823
|
-
const openingLength = vecMag(openingVec);
|
|
3824
|
-
if (openingLength < 1e-6) return {
|
|
3825
|
-
type: "FeatureCollection",
|
|
3826
|
-
features: []
|
|
3827
|
-
};
|
|
3828
|
-
const openingDir = vecNorm(openingVec);
|
|
3829
|
-
const railDir = [openingDir[1], -openingDir[0]];
|
|
3830
|
-
const openingMid = vecScale(vecAdd(p1, p2), .5);
|
|
3831
|
-
let signedLength;
|
|
3832
|
-
if (c3) {
|
|
3833
|
-
const rawSignedLength = vecDot(vecSub(project(c3[0], c3[1]), openingMid), railDir);
|
|
3834
|
-
if (Math.abs(rawSignedLength) < 1e-6) signedLength = (rawSignedLength >= 0 ? 1 : -1) * openingLength * defaultLengthRatio;
|
|
3835
|
-
else signedLength = rawSignedLength;
|
|
3836
|
-
} else signedLength = openingLength * defaultLengthRatio;
|
|
3837
|
-
if (Math.abs(signedLength) < 1e-6) return {
|
|
3838
|
-
type: "FeatureCollection",
|
|
3839
|
-
features: []
|
|
3840
|
-
};
|
|
3841
|
-
const rearMid = vecAdd(openingMid, vecScale(railDir, signedLength));
|
|
3842
|
-
const halfOpening = vecScale(openingDir, openingLength / 2);
|
|
3843
|
-
const rearTop = vecSub(rearMid, halfOpening);
|
|
3844
|
-
const rearBottom = vecAdd(rearMid, halfOpening);
|
|
3845
|
-
const arrowHeadLength = Math.max(openingLength * arrowHeadLengthRatio, EPSILON);
|
|
3846
|
-
const arrowHeadWidth = Math.max(openingLength * arrowHeadWidthRatio, EPSILON);
|
|
3847
|
-
const topDir = vecNorm(vecSub(p1, rearTop));
|
|
3848
|
-
const topHeadBase = vecSub(p1, vecScale(topDir, arrowHeadLength));
|
|
3849
|
-
const bottomDir = vecNorm(vecSub(p2, rearBottom));
|
|
3850
|
-
const bottomHeadBase = vecSub(p2, vecScale(bottomDir, arrowHeadLength));
|
|
3851
|
-
const lineSegments = [
|
|
3852
|
-
createZigzagRear(rearTop, rearBottom, vecScale(railDir, signedLength >= 0 ? -1 : 1), openingLength, zigzagAmplitudeRatio, zigzagSpacingRatio).map((point) => unproject(point[0], point[1])),
|
|
3853
|
-
[unproject(rearTop[0], rearTop[1]), unproject(topHeadBase[0], topHeadBase[1])],
|
|
3854
|
-
[unproject(rearBottom[0], rearBottom[1]), unproject(bottomHeadBase[0], bottomHeadBase[1])]
|
|
3855
|
-
];
|
|
3856
|
-
const polygonRings = [[createArrowHeadRing(p1, topDir, arrowHeadLength, arrowHeadWidth)], [createArrowHeadRing(p2, bottomDir, arrowHeadLength, arrowHeadWidth)]];
|
|
3857
|
-
return {
|
|
3858
|
-
type: "FeatureCollection",
|
|
3859
|
-
features: [{
|
|
3860
|
-
type: "Feature",
|
|
3861
|
-
properties: {},
|
|
3862
|
-
geometry: {
|
|
3863
|
-
type: "MultiLineString",
|
|
3864
|
-
coordinates: lineSegments
|
|
3865
|
-
}
|
|
3866
|
-
}, {
|
|
3867
|
-
type: "Feature",
|
|
3868
|
-
properties: { fill: true },
|
|
3869
|
-
geometry: {
|
|
3870
|
-
type: "MultiPolygon",
|
|
3871
|
-
coordinates: polygonRings
|
|
3872
|
-
}
|
|
3873
|
-
}]
|
|
3874
|
-
};
|
|
3875
|
-
}
|
|
3876
|
-
function createZigzagRear(rearTop, rearBottom, inwardDir, openingLength, amplitudeRatio, spacingRatio) {
|
|
3877
|
-
const axis = vecSub(rearBottom, rearTop);
|
|
3878
|
-
const axisLength = vecMag(axis);
|
|
3879
|
-
if (axisLength < 1e-6) return [rearTop, rearBottom];
|
|
3880
|
-
const axisDir = vecNorm(axis);
|
|
3881
|
-
const amplitude = Math.max(openingLength * amplitudeRatio, EPSILON);
|
|
3882
|
-
const spacing = Math.max(openingLength * spacingRatio, EPSILON * 10);
|
|
3883
|
-
const teeth = Math.max(2, Math.round(axisLength / spacing));
|
|
3884
|
-
const step = axisLength / teeth;
|
|
3885
|
-
const points = [rearTop];
|
|
3886
|
-
for (let i = 0; i < teeth; i += 1) {
|
|
3887
|
-
const mid = vecAdd(rearTop, vecAdd(vecScale(axisDir, step * (i + .5)), vecScale(inwardDir, amplitude)));
|
|
3888
|
-
const base = vecAdd(rearTop, vecScale(axisDir, step * (i + 1)));
|
|
3889
|
-
points.push(mid, base);
|
|
3890
|
-
}
|
|
3891
|
-
return points;
|
|
3892
|
-
}
|
|
3893
|
-
const OBSTACLE_BYPASS_DIFFICULT = defineControlMeasure({
|
|
3894
|
-
metadata: OBSTACLE_BYPASS_DIFFICULT_METADATA,
|
|
3895
|
-
generator: createObstacleBypassDifficult,
|
|
3896
|
-
defaultOptions: DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS,
|
|
3897
|
-
rule: point12DrawRule
|
|
3898
|
-
});
|
|
3899
|
-
//#endregion
|
|
3900
|
-
//#region src/generators/cm27-protection-areas/obstacleBypassEasy.ts
|
|
3901
|
-
const DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS = {
|
|
3902
|
-
arrowHeadLengthRatio: .2,
|
|
3903
|
-
arrowHeadWidthRatio: .2,
|
|
3904
|
-
defaultLengthRatio: 1.5
|
|
3905
|
-
};
|
|
3906
|
-
const OBSTACLE_BYPASS_EASY_METADATA = {
|
|
3907
|
-
id: "obstacle-bypass-easy",
|
|
3908
|
-
name: "Obstacle Bypass Easy",
|
|
3909
|
-
description: "Obstacle bypass (easy) with parallel rails and two arrowheads.",
|
|
3910
|
-
entity: "Protection Areas",
|
|
3911
|
-
entityType: "Obstacle Bypass",
|
|
3912
|
-
entitySubtype: "Easy",
|
|
3913
|
-
value: "270601",
|
|
3914
|
-
minCoordinates: 2,
|
|
3915
|
-
geometry: "line",
|
|
3916
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
3917
|
-
drawRule: "Point12",
|
|
3918
|
-
params: OBSTACLE_BYPASS_PARAMS
|
|
3919
|
-
};
|
|
3920
|
-
/**
|
|
3921
|
-
* Generates a GeoJSON FeatureCollection for an "Obstacle Bypass Easy" symbol.
|
|
3922
|
-
*
|
|
3923
|
-
* Input:
|
|
3924
|
-
* - PT1 and PT2 define the tips of the two arrowheads (opening and symbol height)
|
|
3925
|
-
* - PT3 defines signed symbol length along the perpendicular axis through midpoint(PT1, PT2)
|
|
3926
|
-
*
|
|
3927
|
-
* Geometry:
|
|
3928
|
-
* - Rear line is parallel to opening(PT1-PT2) and has equal length
|
|
3929
|
-
* - Two side rails run from rear endpoints to the arrowheads and are parallel to each other
|
|
3930
|
-
* - Rear line is perpendicular to both side rails
|
|
3931
|
-
* - Arrow heads are filled triangle polygons at PT1 and PT2
|
|
3932
|
-
*/
|
|
3933
|
-
function createObstacleBypassEasy(coordinates, options = {}) {
|
|
3934
|
-
const c1 = coordinates[0];
|
|
3935
|
-
const c2 = coordinates[1];
|
|
3936
|
-
const c3 = coordinates[2];
|
|
3937
|
-
const arrowHeadLengthRatio = options.arrowHeadLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS.arrowHeadLengthRatio;
|
|
3938
|
-
const arrowHeadWidthRatio = options.arrowHeadWidthRatio ?? DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS.arrowHeadWidthRatio;
|
|
3939
|
-
const defaultLengthRatio = options.defaultLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS.defaultLengthRatio;
|
|
3940
|
-
const p1 = project(c1[0], c1[1]);
|
|
3941
|
-
const p2 = project(c2[0], c2[1]);
|
|
3942
|
-
const openingVec = vecSub(p2, p1);
|
|
3943
|
-
const openingLength = vecMag(openingVec);
|
|
3944
|
-
if (openingLength < 1e-6) return {
|
|
3945
|
-
type: "FeatureCollection",
|
|
3946
|
-
features: []
|
|
3947
|
-
};
|
|
3948
|
-
const openingDir = vecNorm(openingVec);
|
|
3949
|
-
const railDir = [openingDir[1], -openingDir[0]];
|
|
3950
|
-
const openingMid = vecScale(vecAdd(p1, p2), .5);
|
|
3951
|
-
let signedLength;
|
|
3952
|
-
if (c3) {
|
|
3953
|
-
const rawSignedLength = vecDot(vecSub(project(c3[0], c3[1]), openingMid), railDir);
|
|
3954
|
-
if (Math.abs(rawSignedLength) < 1e-6) signedLength = (rawSignedLength >= 0 ? 1 : -1) * openingLength * defaultLengthRatio;
|
|
3955
|
-
else signedLength = rawSignedLength;
|
|
3956
|
-
} else signedLength = openingLength * defaultLengthRatio;
|
|
3957
|
-
if (Math.abs(signedLength) < 1e-6) return {
|
|
3958
|
-
type: "FeatureCollection",
|
|
3959
|
-
features: []
|
|
3960
|
-
};
|
|
3961
|
-
const rearMid = vecAdd(openingMid, vecScale(railDir, signedLength));
|
|
3962
|
-
const halfOpening = vecScale(openingDir, openingLength / 2);
|
|
3963
|
-
const rearTop = vecSub(rearMid, halfOpening);
|
|
3964
|
-
const rearBottom = vecAdd(rearMid, halfOpening);
|
|
3965
|
-
const arrowHeadLength = Math.max(openingLength * arrowHeadLengthRatio, EPSILON);
|
|
3966
|
-
const arrowHeadWidth = Math.max(openingLength * arrowHeadWidthRatio, EPSILON);
|
|
3967
|
-
const topDir = vecNorm(vecSub(p1, rearTop));
|
|
3968
|
-
const topHeadBase = vecSub(p1, vecScale(topDir, arrowHeadLength));
|
|
3969
|
-
const bottomDir = vecNorm(vecSub(p2, rearBottom));
|
|
3970
|
-
const bottomHeadBase = vecSub(p2, vecScale(bottomDir, arrowHeadLength));
|
|
3971
|
-
const lineSegments = [
|
|
3972
|
-
[unproject(rearTop[0], rearTop[1]), unproject(rearBottom[0], rearBottom[1])],
|
|
3973
|
-
[unproject(rearTop[0], rearTop[1]), unproject(topHeadBase[0], topHeadBase[1])],
|
|
3974
|
-
[unproject(rearBottom[0], rearBottom[1]), unproject(bottomHeadBase[0], bottomHeadBase[1])]
|
|
3975
|
-
];
|
|
3976
|
-
const polygonRings = [[createArrowHeadRing(p1, topDir, arrowHeadLength, arrowHeadWidth)], [createArrowHeadRing(p2, bottomDir, arrowHeadLength, arrowHeadWidth)]];
|
|
3977
|
-
return {
|
|
3978
|
-
type: "FeatureCollection",
|
|
3979
|
-
features: [{
|
|
3980
|
-
type: "Feature",
|
|
3981
|
-
properties: {},
|
|
3982
|
-
geometry: {
|
|
3983
|
-
type: "MultiLineString",
|
|
3984
|
-
coordinates: lineSegments
|
|
3985
|
-
}
|
|
3986
|
-
}, {
|
|
3987
|
-
type: "Feature",
|
|
3988
|
-
properties: { fill: true },
|
|
3989
|
-
geometry: {
|
|
3990
|
-
type: "MultiPolygon",
|
|
3991
|
-
coordinates: polygonRings
|
|
3992
|
-
}
|
|
3993
|
-
}]
|
|
3994
|
-
};
|
|
3995
|
-
}
|
|
3996
|
-
const OBSTACLE_BYPASS_EASY = defineControlMeasure({
|
|
3997
|
-
metadata: OBSTACLE_BYPASS_EASY_METADATA,
|
|
3998
|
-
generator: createObstacleBypassEasy,
|
|
3999
|
-
defaultOptions: DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS,
|
|
4000
|
-
rule: point12DrawRule
|
|
4001
|
-
});
|
|
4002
|
-
//#endregion
|
|
4003
|
-
//#region src/generators/cm27-protection-areas/obstacleBypassImpossible.ts
|
|
4004
|
-
const DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS = {
|
|
4005
|
-
arrowHeadLengthRatio: .2,
|
|
4006
|
-
arrowHeadWidthRatio: .2,
|
|
4007
|
-
defaultLengthRatio: 1.5,
|
|
4008
|
-
rearBarLengthRatio: .12,
|
|
4009
|
-
rearGapRatio: .08
|
|
4010
|
-
};
|
|
4011
|
-
const OBSTACLE_BYPASS_IMPOSSIBLE_METADATA = {
|
|
4012
|
-
id: "obstacle-bypass-impossible",
|
|
4013
|
-
name: "Obstacle Bypass Impossible",
|
|
4014
|
-
description: "Obstacle bypass (impossible) with rear barrier marks and arrowheads.",
|
|
4015
|
-
entity: "Protection Areas",
|
|
4016
|
-
entityType: "Obstacle Bypass",
|
|
4017
|
-
entitySubtype: "Impossible",
|
|
4018
|
-
value: "270603",
|
|
4019
|
-
minCoordinates: 2,
|
|
4020
|
-
geometry: "line",
|
|
4021
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
4022
|
-
drawRule: "Point12",
|
|
4023
|
-
params: [
|
|
4024
|
-
...OBSTACLE_BYPASS_PARAMS,
|
|
4025
|
-
{
|
|
4026
|
-
key: "rearBarLengthRatio",
|
|
4027
|
-
label: "Rear bar length",
|
|
4028
|
-
description: "Length of each rear barrier bar as a ratio of the opening width.",
|
|
4029
|
-
type: "number",
|
|
4030
|
-
min: .01,
|
|
4031
|
-
max: 1,
|
|
4032
|
-
step: .01
|
|
4033
|
-
},
|
|
4034
|
-
{
|
|
4035
|
-
key: "rearGapRatio",
|
|
4036
|
-
label: "Rear gap",
|
|
4037
|
-
description: "Gap between the two rear bars as a ratio of the opening width.",
|
|
4038
|
-
type: "number",
|
|
4039
|
-
min: 0,
|
|
4040
|
-
max: 1,
|
|
4041
|
-
step: .01
|
|
4042
|
-
}
|
|
4043
|
-
]
|
|
4044
|
-
};
|
|
4045
|
-
/**
|
|
4046
|
-
* Generates a GeoJSON FeatureCollection for an "Obstacle Bypass Impossible" symbol.
|
|
4047
|
-
*
|
|
4048
|
-
* Input:
|
|
4049
|
-
* - PT1 and PT2 define the tips of the two arrowheads (opening and symbol height)
|
|
4050
|
-
* - PT3 defines signed symbol length along the perpendicular axis through midpoint(PT1, PT2)
|
|
4051
|
-
*
|
|
4052
|
-
* Geometry:
|
|
4053
|
-
* - Same base as obstacle bypass easy
|
|
4054
|
-
* - Rear line has two short perpendicular bars on the rear side
|
|
4055
|
-
*/
|
|
4056
|
-
function createObstacleBypassImpossible(coordinates, options = {}) {
|
|
4057
|
-
const c1 = coordinates[0];
|
|
4058
|
-
const c2 = coordinates[1];
|
|
4059
|
-
const c3 = coordinates[2];
|
|
4060
|
-
const arrowHeadLengthRatio = options.arrowHeadLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS.arrowHeadLengthRatio;
|
|
4061
|
-
const arrowHeadWidthRatio = options.arrowHeadWidthRatio ?? DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS.arrowHeadWidthRatio;
|
|
4062
|
-
const defaultLengthRatio = options.defaultLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS.defaultLengthRatio;
|
|
4063
|
-
const rearBarLengthRatio = options.rearBarLengthRatio ?? DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS.rearBarLengthRatio;
|
|
4064
|
-
const rearGapRatio = options.rearGapRatio ?? DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS.rearGapRatio;
|
|
4065
|
-
const p1 = project(c1[0], c1[1]);
|
|
4066
|
-
const p2 = project(c2[0], c2[1]);
|
|
4067
|
-
const openingVec = vecSub(p2, p1);
|
|
4068
|
-
const openingLength = vecMag(openingVec);
|
|
4069
|
-
if (openingLength < 1e-6) return {
|
|
4070
|
-
type: "FeatureCollection",
|
|
4071
|
-
features: []
|
|
4072
|
-
};
|
|
4073
|
-
const openingDir = vecNorm(openingVec);
|
|
4074
|
-
const railDir = [openingDir[1], -openingDir[0]];
|
|
4075
|
-
const openingMid = vecScale(vecAdd(p1, p2), .5);
|
|
4076
|
-
let signedLength;
|
|
4077
|
-
if (c3) {
|
|
4078
|
-
const rawSignedLength = vecDot(vecSub(project(c3[0], c3[1]), openingMid), railDir);
|
|
4079
|
-
if (Math.abs(rawSignedLength) < 1e-6) signedLength = (rawSignedLength >= 0 ? 1 : -1) * openingLength * defaultLengthRatio;
|
|
4080
|
-
else signedLength = rawSignedLength;
|
|
4081
|
-
} else signedLength = openingLength * defaultLengthRatio;
|
|
4082
|
-
if (Math.abs(signedLength) < 1e-6) return {
|
|
4083
|
-
type: "FeatureCollection",
|
|
4084
|
-
features: []
|
|
4085
|
-
};
|
|
4086
|
-
const rearMid = vecAdd(openingMid, vecScale(railDir, signedLength));
|
|
4087
|
-
const halfOpening = vecScale(openingDir, openingLength / 2);
|
|
4088
|
-
const rearTop = vecSub(rearMid, halfOpening);
|
|
4089
|
-
const rearBottom = vecAdd(rearMid, halfOpening);
|
|
4090
|
-
const arrowHeadLength = Math.max(openingLength * arrowHeadLengthRatio, EPSILON);
|
|
4091
|
-
const arrowHeadWidth = Math.max(openingLength * arrowHeadWidthRatio, EPSILON);
|
|
4092
|
-
const topDir = vecNorm(vecSub(p1, rearTop));
|
|
4093
|
-
const topHeadBase = vecSub(p1, vecScale(topDir, arrowHeadLength));
|
|
4094
|
-
const bottomDir = vecNorm(vecSub(p2, rearBottom));
|
|
4095
|
-
const bottomHeadBase = vecSub(p2, vecScale(bottomDir, arrowHeadLength));
|
|
4096
|
-
const barLength = Math.max(openingLength * rearBarLengthRatio, EPSILON);
|
|
4097
|
-
const gapHalf = Math.max(openingLength * rearGapRatio * .5, EPSILON * 5);
|
|
4098
|
-
const barHalf = barLength * .5;
|
|
4099
|
-
const upperBarAnchor = vecAdd(rearMid, vecScale(openingDir, -gapHalf));
|
|
4100
|
-
const lowerBarAnchor = vecAdd(rearMid, vecScale(openingDir, gapHalf));
|
|
4101
|
-
const upperGapEnd = upperBarAnchor;
|
|
4102
|
-
const lowerGapEnd = lowerBarAnchor;
|
|
4103
|
-
const lineSegments = [
|
|
4104
|
-
[unproject(rearTop[0], rearTop[1]), unproject(upperGapEnd[0], upperGapEnd[1])],
|
|
4105
|
-
[unproject(lowerGapEnd[0], lowerGapEnd[1]), unproject(rearBottom[0], rearBottom[1])],
|
|
4106
|
-
[unproject(rearTop[0], rearTop[1]), unproject(topHeadBase[0], topHeadBase[1])],
|
|
4107
|
-
[unproject(rearBottom[0], rearBottom[1]), unproject(bottomHeadBase[0], bottomHeadBase[1])],
|
|
4108
|
-
[unproject(upperBarAnchor[0] - railDir[0] * barHalf, upperBarAnchor[1] - railDir[1] * barHalf), unproject(upperBarAnchor[0] + railDir[0] * barHalf, upperBarAnchor[1] + railDir[1] * barHalf)],
|
|
4109
|
-
[unproject(lowerBarAnchor[0] - railDir[0] * barHalf, lowerBarAnchor[1] - railDir[1] * barHalf), unproject(lowerBarAnchor[0] + railDir[0] * barHalf, lowerBarAnchor[1] + railDir[1] * barHalf)]
|
|
4110
|
-
];
|
|
4111
|
-
const polygonRings = [[createArrowHeadRing(p1, topDir, arrowHeadLength, arrowHeadWidth)], [createArrowHeadRing(p2, bottomDir, arrowHeadLength, arrowHeadWidth)]];
|
|
4112
|
-
return {
|
|
4113
|
-
type: "FeatureCollection",
|
|
4114
|
-
features: [{
|
|
4115
|
-
type: "Feature",
|
|
4116
|
-
properties: {},
|
|
4117
|
-
geometry: {
|
|
4118
|
-
type: "MultiLineString",
|
|
4119
|
-
coordinates: lineSegments
|
|
4120
|
-
}
|
|
4121
|
-
}, {
|
|
4122
|
-
type: "Feature",
|
|
4123
|
-
properties: { fill: true },
|
|
4124
|
-
geometry: {
|
|
4125
|
-
type: "MultiPolygon",
|
|
4126
|
-
coordinates: polygonRings
|
|
4127
|
-
}
|
|
4128
|
-
}]
|
|
4129
|
-
};
|
|
4130
|
-
}
|
|
4131
|
-
const OBSTACLE_BYPASS_IMPOSSIBLE = defineControlMeasure({
|
|
4132
|
-
metadata: OBSTACLE_BYPASS_IMPOSSIBLE_METADATA,
|
|
4133
|
-
generator: createObstacleBypassImpossible,
|
|
4134
|
-
defaultOptions: DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS,
|
|
4135
|
-
rule: point12DrawRule
|
|
4136
|
-
});
|
|
4137
|
-
//#endregion
|
|
4138
|
-
//#region src/generators/cm14-maneuver-lines/principalDirectionOfFire.ts
|
|
4139
|
-
const DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS = {
|
|
4140
|
-
hSize: .07,
|
|
4141
|
-
hWidth: .67
|
|
4142
|
-
};
|
|
4143
|
-
const PRINCIPAL_DIRECTION_OF_FIRE_METADATA = {
|
|
4144
|
-
id: "principal-direction-of-fire",
|
|
4145
|
-
name: "Principal Direction of Fire",
|
|
4146
|
-
description: "A specific direction or sector toward which a weapon or unit directs fire for a tactical purpose.",
|
|
4147
|
-
entity: "Maneuver Lines",
|
|
4148
|
-
entityType: "Field of Fire",
|
|
4149
|
-
entitySubtype: "Principal Direction of Fire",
|
|
4150
|
-
value: "140503",
|
|
4151
|
-
minCoordinates: 2,
|
|
4152
|
-
maxCoordinates: 3,
|
|
4153
|
-
geometry: "line",
|
|
4154
|
-
geometryTypes: ["MultiLineString"],
|
|
4155
|
-
drawRule: "Line3",
|
|
4156
|
-
params: [{
|
|
4157
|
-
key: "hSize",
|
|
4158
|
-
label: "Head size",
|
|
4159
|
-
description: "Arrowhead length as a fraction of each arm's length",
|
|
4160
|
-
type: "number",
|
|
4161
|
-
min: .01,
|
|
4162
|
-
max: .5,
|
|
4163
|
-
step: .01
|
|
4164
|
-
}, {
|
|
4165
|
-
key: "hWidth",
|
|
4166
|
-
label: "Head width",
|
|
4167
|
-
description: "Arrowhead wing spread relative to the arrowhead length",
|
|
4168
|
-
type: "number",
|
|
4169
|
-
min: .05,
|
|
4170
|
-
max: 2,
|
|
4171
|
-
step: .01
|
|
4172
|
-
}]
|
|
4173
|
-
};
|
|
4174
|
-
/**
|
|
4175
|
-
* Generates a GeoJSON FeatureCollection for a PRINCIPAL DIRECTION OF FIRE
|
|
4176
|
-
* tactical symbol: a "V" of arms from the vertex out to the (independent) tips,
|
|
4177
|
-
* each ending in an open arrowhead.
|
|
4178
|
-
*
|
|
4179
|
-
* Renders one arm per tip, so two points (vertex + one tip) draws the single
|
|
4180
|
-
* arm being placed — the live feedback after PT 1 — and three points draws the
|
|
4181
|
-
* full V. Points beyond the third are ignored (input contract, ADR-0014).
|
|
4182
|
-
*
|
|
4183
|
-
* @param coordinates - [PT 1 (vertex), PT 2 (tip), PT 3 (tip)]
|
|
4184
|
-
* @param options - Arrowhead sizing parameters
|
|
4185
|
-
*/
|
|
4186
|
-
function createPrincipalDirectionOfFire(coordinates, options = {}) {
|
|
4187
|
-
const { hSize, hWidth } = {
|
|
4188
|
-
...DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS,
|
|
4189
|
-
...options
|
|
4190
|
-
};
|
|
4191
|
-
const v = buildFieldOfFireV(coordinates, Math.max(0, hSize), Math.max(0, hWidth));
|
|
4192
|
-
if (!v) return {
|
|
4193
|
-
type: "FeatureCollection",
|
|
4194
|
-
features: []
|
|
4195
|
-
};
|
|
4196
|
-
return {
|
|
4197
|
-
type: "FeatureCollection",
|
|
4198
|
-
features: [{
|
|
4199
|
-
type: "Feature",
|
|
4200
|
-
properties: {},
|
|
4201
|
-
geometry: {
|
|
4202
|
-
type: "MultiLineString",
|
|
4203
|
-
coordinates: v.lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
4204
|
-
}
|
|
4205
|
-
}]
|
|
4206
|
-
};
|
|
4207
|
-
}
|
|
4208
|
-
const PRINCIPAL_DIRECTION_OF_FIRE = defineControlMeasure({
|
|
4209
|
-
metadata: PRINCIPAL_DIRECTION_OF_FIRE_METADATA,
|
|
4210
|
-
generator: createPrincipalDirectionOfFire,
|
|
4211
|
-
defaultOptions: DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS,
|
|
4212
|
-
rule: line3DrawRule
|
|
4213
|
-
});
|
|
4214
|
-
//#endregion
|
|
4215
|
-
//#region src/generators/cm15-maneuver-areas/searchArea.ts
|
|
4216
|
-
const DEFAULT_HEAD_SIZE_RATIO = .12;
|
|
4217
|
-
const DEFAULT_HEAD_WIDTH_MULTIPLIER = .5;
|
|
4218
|
-
const DEFAULT_ZIGZAG_START = .55;
|
|
4219
|
-
const DEFAULT_ZIGZAG_END = .45;
|
|
4220
|
-
const DEFAULT_ZIGZAG_WIDTH = .07;
|
|
4221
|
-
/**
|
|
4222
|
-
* Default options for the tactical arrow.
|
|
4223
|
-
*/
|
|
4224
|
-
const DEFAULT_TACTICAL_ARROW_OPTIONS = {
|
|
4225
|
-
headSizeRatio: DEFAULT_HEAD_SIZE_RATIO,
|
|
4226
|
-
headWidthMultiplier: DEFAULT_HEAD_WIDTH_MULTIPLIER,
|
|
4227
|
-
zigzagStart: DEFAULT_ZIGZAG_START,
|
|
4228
|
-
zigzagEnd: DEFAULT_ZIGZAG_END,
|
|
4229
|
-
zigzagWidth: DEFAULT_ZIGZAG_WIDTH
|
|
4230
|
-
};
|
|
4231
|
-
const SEARCH_AREA_METADATA = {
|
|
4232
|
-
id: "search-area",
|
|
4233
|
-
name: "Search Area",
|
|
4234
|
-
description: "Search area graphic with paired lightning-style arrows.",
|
|
4235
|
-
entity: "Maneuver Areas",
|
|
4236
|
-
entityType: "Search Area",
|
|
4237
|
-
value: "152200",
|
|
4238
|
-
minCoordinates: 3,
|
|
4239
|
-
maxCoordinates: 3,
|
|
4240
|
-
geometry: "area",
|
|
4241
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
4242
|
-
drawRule: "Area21",
|
|
4243
|
-
params: [
|
|
4244
|
-
{
|
|
4245
|
-
key: "headSizeRatio",
|
|
4246
|
-
label: "Head size",
|
|
4247
|
-
description: "Arrowhead length as a ratio of the total leg length.",
|
|
4248
|
-
type: "number",
|
|
4249
|
-
min: 0,
|
|
4250
|
-
max: .5,
|
|
4251
|
-
step: .01
|
|
4252
|
-
},
|
|
4253
|
-
{
|
|
4254
|
-
key: "headWidthMultiplier",
|
|
4255
|
-
label: "Head width",
|
|
4256
|
-
description: "Arrowhead wing width as a multiple of the head length.",
|
|
4257
|
-
type: "number",
|
|
4258
|
-
min: .05,
|
|
4259
|
-
max: 2,
|
|
4260
|
-
step: .1
|
|
4261
|
-
},
|
|
4262
|
-
{
|
|
4263
|
-
key: "zigzagStart",
|
|
4264
|
-
label: "Zigzag start",
|
|
4265
|
-
description: "Point along the leg where the zigzag begins (0-1).",
|
|
4266
|
-
type: "number",
|
|
4267
|
-
min: 0,
|
|
4268
|
-
max: 1,
|
|
4269
|
-
step: .05
|
|
4270
|
-
},
|
|
4271
|
-
{
|
|
4272
|
-
key: "zigzagEnd",
|
|
4273
|
-
label: "Zigzag end",
|
|
4274
|
-
description: "Point along the leg where the zigzag returns (0-1).",
|
|
4275
|
-
type: "number",
|
|
4276
|
-
min: 0,
|
|
4277
|
-
max: 1,
|
|
4278
|
-
step: .05
|
|
4279
|
-
},
|
|
4280
|
-
{
|
|
4281
|
-
key: "zigzagWidth",
|
|
4282
|
-
label: "Zigzag width",
|
|
4283
|
-
description: "Perpendicular kink offset as a ratio of leg length.",
|
|
4284
|
-
type: "number",
|
|
4285
|
-
min: 0,
|
|
4286
|
-
max: .3,
|
|
4287
|
-
step: .01
|
|
4288
|
-
}
|
|
4289
|
-
]
|
|
4290
|
-
};
|
|
4291
|
-
/**
|
|
4292
|
-
* Generates a GeoJSON FeatureCollection of a tactical "lightning" arrow.
|
|
4293
|
-
* Handles internal projection to maintain correct perpendicularity.
|
|
4294
|
-
*
|
|
4295
|
-
* @param coordinates - [Apex Point (PT1), Top Tail (PT2), Bottom Tail (PT3)]
|
|
4296
|
-
* @param options - Geometric configuration parameters
|
|
4297
|
-
* @returns A FeatureCollection containing a MultiLineString for bolts and a MultiPolygon for heads
|
|
4298
|
-
*/
|
|
4299
|
-
function createSearchArea(coordinates, options = {}) {
|
|
4300
|
-
const { headSizeRatio = DEFAULT_HEAD_SIZE_RATIO, headWidthMultiplier = DEFAULT_HEAD_WIDTH_MULTIPLIER, zigzagStart = DEFAULT_ZIGZAG_START, zigzagEnd = DEFAULT_ZIGZAG_END, zigzagWidth = DEFAULT_ZIGZAG_WIDTH } = options;
|
|
4301
|
-
const safeHeadSizeRatio = Math.max(0, headSizeRatio);
|
|
4302
|
-
const safeHeadWidthMultiplier = Math.max(0, headWidthMultiplier);
|
|
4303
|
-
const safeZigzagStart = clamp01(zigzagStart);
|
|
4304
|
-
const safeZigzagEnd = clamp01(zigzagEnd);
|
|
4305
|
-
const safeZigzagWidth = Math.max(0, zigzagWidth);
|
|
4306
|
-
const [c0, c1, c2] = coordinates;
|
|
4307
|
-
const [p1, p2, p3] = [
|
|
4308
|
-
project(c0[0], c0[1]),
|
|
4309
|
-
project(c1[0], c1[1]),
|
|
4310
|
-
project(c2[0], c2[1])
|
|
4311
|
-
];
|
|
4312
|
-
const shaftSegments = [];
|
|
4313
|
-
const headPolygons = [];
|
|
4314
|
-
const addLeg = (origin, tip, opposite) => {
|
|
4315
|
-
const v = vecSub(tip, origin);
|
|
4316
|
-
const totalLen = vecMag(v);
|
|
4317
|
-
if (totalLen < 1e-6) return;
|
|
4318
|
-
const u = vecNorm(v);
|
|
4319
|
-
let n = [-u[1], u[0]];
|
|
4320
|
-
const toOpp = vecSub(opposite, origin);
|
|
4321
|
-
if (vecDot(n, toOpp) > 0) n = vecScale(n, -1);
|
|
4322
|
-
const offset = totalLen * safeZigzagWidth;
|
|
4323
|
-
const ptA = vecAdd(vecAdd(origin, vecScale(v, safeZigzagStart)), vecScale(n, offset));
|
|
4324
|
-
const ptB = vecAdd(vecAdd(origin, vecScale(v, safeZigzagEnd)), vecScale(n, -offset));
|
|
4325
|
-
const fv = vecSub(tip, ptB);
|
|
4326
|
-
const fLen = vecMag(fv);
|
|
4327
|
-
let fu = vecNorm(fv);
|
|
4328
|
-
if (fLen < 1e-6) fu = u;
|
|
4329
|
-
const headLen = totalLen * safeHeadSizeRatio;
|
|
4330
|
-
const pBase = vecSub(tip, vecScale(fu, headLen));
|
|
4331
|
-
const wingWidth = headLen * safeHeadWidthMultiplier;
|
|
4332
|
-
const perp = [-fu[1], fu[0]];
|
|
4333
|
-
const pWing1 = vecAdd(pBase, vecScale(perp, wingWidth));
|
|
4334
|
-
const pWing2 = vecSub(pBase, vecScale(perp, wingWidth));
|
|
4335
|
-
shaftSegments.push([
|
|
4336
|
-
origin,
|
|
4337
|
-
ptA,
|
|
4338
|
-
ptB,
|
|
4339
|
-
pBase
|
|
4340
|
-
].map((p) => unproject(p[0], p[1])));
|
|
4341
|
-
headPolygons.push([[
|
|
4342
|
-
tip,
|
|
4343
|
-
pWing1,
|
|
4344
|
-
pWing2,
|
|
4345
|
-
tip
|
|
4346
|
-
].map((p) => unproject(p[0], p[1]))]);
|
|
4347
|
-
};
|
|
4348
|
-
addLeg(p1, p2, p3);
|
|
4349
|
-
addLeg(p1, p3, p2);
|
|
4350
|
-
const features = [];
|
|
4351
|
-
if (shaftSegments.length > 0) features.push({
|
|
4352
|
-
type: "Feature",
|
|
4353
|
-
properties: { part: "shaft" },
|
|
4354
|
-
geometry: {
|
|
4355
|
-
type: "MultiLineString",
|
|
4356
|
-
coordinates: shaftSegments
|
|
4357
|
-
}
|
|
4358
|
-
});
|
|
4359
|
-
if (headPolygons.length > 0) features.push({
|
|
4360
|
-
type: "Feature",
|
|
4361
|
-
properties: {
|
|
4362
|
-
part: "head",
|
|
4363
|
-
fill: true
|
|
4364
|
-
},
|
|
4365
|
-
geometry: {
|
|
4366
|
-
type: "MultiPolygon",
|
|
4367
|
-
coordinates: headPolygons
|
|
4368
|
-
}
|
|
4369
|
-
});
|
|
4370
|
-
return {
|
|
4371
|
-
type: "FeatureCollection",
|
|
4372
|
-
features
|
|
4373
|
-
};
|
|
4374
|
-
}
|
|
4375
|
-
const SEARCH_AREA = defineControlMeasure({
|
|
4376
|
-
metadata: SEARCH_AREA_METADATA,
|
|
4377
|
-
generator: (controlPoints, options) => createSearchArea(asThreePoints(controlPoints), options),
|
|
4378
|
-
defaultOptions: DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
4379
|
-
rule: searchAreaDrawRule
|
|
4380
|
-
});
|
|
4381
|
-
//#endregion
|
|
4382
|
-
//#region src/generators/cm15-maneuver-areas/supportByFire.ts
|
|
4383
|
-
const DEFAULT_SUPPORT_BY_FIRE_OPTIONS = {
|
|
4384
|
-
arrowTipWidthRatio: .3,
|
|
4385
|
-
backLineLengthRatio: .3,
|
|
4386
|
-
backLineAngle: 45
|
|
4387
|
-
};
|
|
4388
|
-
const SUPPORT_BY_FIRE_METADATA = {
|
|
4389
|
-
id: "support-by-fire",
|
|
4390
|
-
name: "Support By Fire",
|
|
4391
|
-
description: "Support by fire graphic with paired arrows.",
|
|
4392
|
-
entity: "Maneuver Areas",
|
|
4393
|
-
entityType: "Support by Fire",
|
|
4394
|
-
value: "152100",
|
|
4395
|
-
minCoordinates: 4,
|
|
4396
|
-
geometry: "line",
|
|
4397
|
-
geometryTypes: ["LineString"],
|
|
4398
|
-
drawRule: "Area8",
|
|
4399
|
-
params: FIRE_ARROW_PARAMS
|
|
4400
|
-
};
|
|
4401
|
-
/**
|
|
4402
|
-
* Generates a GeoJSON FeatureCollection for a Support By Fire tactical graphic.
|
|
4403
|
-
*
|
|
4404
|
-
* Input:
|
|
4405
|
-
* - PT1: Left Base
|
|
4406
|
-
* - PT2: Right Base
|
|
4407
|
-
* - PT3: Left Tip
|
|
4408
|
-
* - PT4: Right Tip
|
|
4409
|
-
*/
|
|
4410
|
-
function createSupportByFire(coordinates, options = {}) {
|
|
4411
|
-
const featureProps = {};
|
|
4412
|
-
const arrowTipWidthRatio = options.arrowTipWidthRatio ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.arrowTipWidthRatio;
|
|
4413
|
-
const backLineLengthRatio = options.backLineLengthRatio ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.backLineLengthRatio;
|
|
4414
|
-
const backLineAngle = options.backLineAngle ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.backLineAngle;
|
|
4415
|
-
const c1 = coordinates[0];
|
|
4416
|
-
const c2 = coordinates[1];
|
|
4417
|
-
const c3 = coordinates[2];
|
|
4418
|
-
const c4 = coordinates[3];
|
|
4419
|
-
const p1 = project(c1[0], c1[1]);
|
|
4420
|
-
const p2 = project(c2[0], c2[1]);
|
|
4421
|
-
const p3 = project(c3[0], c3[1]);
|
|
4422
|
-
const p4 = project(c4[0], c4[1]);
|
|
4423
|
-
const vBase = vecSub(p2, p1);
|
|
4424
|
-
const lenBase = vecMag(vBase);
|
|
4425
|
-
const dirBase = vecNorm(vBase);
|
|
4426
|
-
if (lenBase < 1e-6) return {
|
|
4427
|
-
type: "FeatureCollection",
|
|
4428
|
-
features: []
|
|
4429
|
-
};
|
|
4430
|
-
const angleRad = backLineAngle * Math.PI / 180;
|
|
4431
|
-
const baseAngle = -Math.PI / 2;
|
|
4432
|
-
const angleLeft = baseAngle - angleRad;
|
|
4433
|
-
const dirBackLeft = [dirBase[0] * Math.cos(angleLeft) - dirBase[1] * Math.sin(angleLeft), dirBase[0] * Math.sin(angleLeft) + dirBase[1] * Math.cos(angleLeft)];
|
|
4434
|
-
const angleRight = baseAngle + angleRad;
|
|
4435
|
-
const dirBackRight = [dirBase[0] * Math.cos(angleRight) - dirBase[1] * Math.sin(angleRight), dirBase[0] * Math.sin(angleRight) + dirBase[1] * Math.cos(angleRight)];
|
|
4436
|
-
const backLen = lenBase * backLineLengthRatio;
|
|
4437
|
-
const p1Back = vecAdd(p1, vecScale(dirBackLeft, backLen));
|
|
4438
|
-
const p2Back = vecAdd(p2, vecScale(dirBackRight, backLen));
|
|
4439
|
-
const vLeft = vecSub(p3, p1);
|
|
4440
|
-
const lenLeft = vecMag(vLeft);
|
|
4441
|
-
const dirLeft = vecNorm(vLeft);
|
|
4442
|
-
const vRight = vecSub(p4, p2);
|
|
4443
|
-
const lenRight = vecMag(vRight);
|
|
4444
|
-
const dirRight = vecNorm(vRight);
|
|
4445
|
-
const commonHeadWidth = (lenLeft + lenRight) / 2 * arrowTipWidthRatio;
|
|
4446
|
-
const commonHeadLen = commonHeadWidth;
|
|
4447
|
-
const leftHeadFeatures = createArrowHead(p3, dirLeft, commonHeadLen, commonHeadWidth);
|
|
4448
|
-
const rightHeadFeatures = createArrowHead(p4, dirRight, commonHeadLen, commonHeadWidth);
|
|
4449
|
-
return {
|
|
4450
|
-
type: "FeatureCollection",
|
|
4451
|
-
features: [
|
|
4452
|
-
{
|
|
4453
|
-
type: "Feature",
|
|
4454
|
-
properties: featureProps,
|
|
4455
|
-
geometry: {
|
|
4456
|
-
type: "LineString",
|
|
4457
|
-
coordinates: [unproject(p1[0], p1[1]), unproject(p1Back[0], p1Back[1])]
|
|
4458
|
-
}
|
|
4459
|
-
},
|
|
4460
|
-
{
|
|
4461
|
-
type: "Feature",
|
|
4462
|
-
properties: featureProps,
|
|
4463
|
-
geometry: {
|
|
4464
|
-
type: "LineString",
|
|
4465
|
-
coordinates: [unproject(p2[0], p2[1]), unproject(p2Back[0], p2Back[1])]
|
|
4466
|
-
}
|
|
4467
|
-
},
|
|
4468
|
-
{
|
|
4469
|
-
type: "Feature",
|
|
4470
|
-
properties: featureProps,
|
|
4471
|
-
geometry: {
|
|
4472
|
-
type: "LineString",
|
|
4473
|
-
coordinates: [unproject(p1[0], p1[1]), unproject(p3[0], p3[1])]
|
|
4474
|
-
}
|
|
4475
|
-
},
|
|
4476
|
-
{
|
|
4477
|
-
type: "Feature",
|
|
4478
|
-
properties: featureProps,
|
|
4479
|
-
geometry: {
|
|
4480
|
-
type: "LineString",
|
|
4481
|
-
coordinates: [unproject(p2[0], p2[1]), unproject(p4[0], p4[1])]
|
|
4482
|
-
}
|
|
4483
|
-
},
|
|
4484
|
-
{
|
|
4485
|
-
type: "Feature",
|
|
4486
|
-
properties: featureProps,
|
|
4487
|
-
geometry: {
|
|
4488
|
-
type: "LineString",
|
|
4489
|
-
coordinates: [unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]
|
|
4490
|
-
}
|
|
4491
|
-
},
|
|
4492
|
-
...leftHeadFeatures.map((f) => ({
|
|
4493
|
-
...f,
|
|
4494
|
-
properties: featureProps
|
|
4495
|
-
})),
|
|
4496
|
-
...rightHeadFeatures.map((f) => ({
|
|
4497
|
-
...f,
|
|
4498
|
-
properties: featureProps
|
|
4499
|
-
}))
|
|
4500
|
-
]
|
|
4501
|
-
};
|
|
4502
|
-
}
|
|
4503
|
-
function createArrowHead(tip, dir, length, width) {
|
|
4504
|
-
const norm = [-dir[1], dir[0]];
|
|
4505
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
4506
|
-
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
4507
|
-
const right = vecSub(base, vecScale(norm, width / 2));
|
|
4508
|
-
return [{
|
|
4509
|
-
type: "Feature",
|
|
4510
|
-
geometry: {
|
|
4511
|
-
type: "LineString",
|
|
4512
|
-
coordinates: [
|
|
4513
|
-
unproject(left[0], left[1]),
|
|
4514
|
-
unproject(tip[0], tip[1]),
|
|
4515
|
-
unproject(right[0], right[1])
|
|
4516
|
-
]
|
|
4517
|
-
},
|
|
4518
|
-
properties: {}
|
|
4519
|
-
}];
|
|
4520
|
-
}
|
|
4521
|
-
const SUPPORT_BY_FIRE = defineControlMeasure({
|
|
4522
|
-
metadata: SUPPORT_BY_FIRE_METADATA,
|
|
4523
|
-
generator: createSupportByFire,
|
|
4524
|
-
defaultOptions: DEFAULT_SUPPORT_BY_FIRE_OPTIONS,
|
|
4525
|
-
rule: supportByFireDrawRule
|
|
4526
|
-
});
|
|
4527
|
-
//#endregion
|
|
4528
|
-
//#region src/generators/cm15-maneuver-areas/supportingAttack.ts
|
|
4529
|
-
const DEFAULT_SUPPORTING_ATTACK_OPTIONS = {
|
|
4530
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
4531
|
-
roundedBends: false,
|
|
4532
|
-
bendSegments: 5
|
|
4533
|
-
};
|
|
4534
|
-
const SUPPORTING_ATTACK_METADATA = {
|
|
4535
|
-
id: "supporting-attack",
|
|
4536
|
-
name: "Supporting Attack",
|
|
4537
|
-
description: "Secondary attack that supports the main effort.",
|
|
4538
|
-
entity: "Maneuver Areas",
|
|
4539
|
-
entityType: "Axis of Advance",
|
|
4540
|
-
entitySubtype: "Supporting Attack",
|
|
4541
|
-
value: "151404",
|
|
4542
|
-
minCoordinates: 3,
|
|
4543
|
-
geometry: "line",
|
|
4544
|
-
geometryTypes: ["LineString"],
|
|
4545
|
-
drawRule: "Axis1",
|
|
4546
|
-
params: ATTACK_SHAFT_PARAMS
|
|
4547
|
-
};
|
|
4548
|
-
/**
|
|
4549
|
-
* Generates a GeoJSON FeatureCollection for a Supporting Attack tactical symbol.
|
|
4550
|
-
*
|
|
4551
|
-
* The symbol differs from Main Attack in that it is a single LineString
|
|
4552
|
-
* outlining the arrow shape, rather than a filled Polygon header + Shaft lines.
|
|
4553
|
-
*
|
|
4554
|
-
* @param coordinates - An array of GeoJSON Positions.
|
|
4555
|
-
* @param options - Configuration options.
|
|
4556
|
-
* @returns A GeoJSON FeatureCollection<LineString>.
|
|
4557
|
-
*/
|
|
4558
|
-
function createSupportingAttack(coordinates, options = {}) {
|
|
4559
|
-
const { geometry } = processAttackGeometry(coordinates, options);
|
|
4560
|
-
if (!geometry) return {
|
|
4561
|
-
type: "FeatureCollection",
|
|
4562
|
-
features: []
|
|
4563
|
-
};
|
|
4564
|
-
return {
|
|
4565
|
-
type: "FeatureCollection",
|
|
4566
|
-
features: [{
|
|
4567
|
-
type: "Feature",
|
|
4568
|
-
properties: {},
|
|
4569
|
-
geometry: {
|
|
4570
|
-
type: "LineString",
|
|
4571
|
-
coordinates: [
|
|
4572
|
-
...geometry.shaftLeft,
|
|
4573
|
-
geometry.headRing[0],
|
|
4574
|
-
geometry.headRing[1],
|
|
4575
|
-
geometry.headRing[2],
|
|
4576
|
-
...[...geometry.shaftRight].reverse()
|
|
4577
|
-
].map((p) => unproject(p[0], p[1]))
|
|
4578
|
-
}
|
|
4579
|
-
}]
|
|
4580
|
-
};
|
|
4581
|
-
}
|
|
4582
|
-
const SUPPORTING_ATTACK = defineControlMeasure({
|
|
4583
|
-
metadata: SUPPORTING_ATTACK_METADATA,
|
|
4584
|
-
generator: createSupportingAttack,
|
|
4585
|
-
defaultOptions: DEFAULT_SUPPORTING_ATTACK_OPTIONS,
|
|
4586
|
-
rule: axis1DrawRule
|
|
4587
|
-
});
|
|
4588
|
-
//#endregion
|
|
4589
|
-
//#region src/generators/cm27-protection-areas/turn.ts
|
|
4590
|
-
const DEFAULT_TURN_OPTIONS = {
|
|
4591
|
-
arrowHeadLengthRatio: .16,
|
|
4592
|
-
arrowHeadWidthRatio: .16,
|
|
4593
|
-
arcSegments: 64
|
|
4594
|
-
};
|
|
4595
|
-
const TURN_METADATA = {
|
|
4596
|
-
id: "turn",
|
|
4597
|
-
name: "Turn",
|
|
4598
|
-
description: "An obstacle effect that integrates fire planning and obstacle effort to drive an enemy formation from one avenue of approach to an adjacent avenue of approach or into an engagement area. The rear of the symbol identifies the enemy's location and the arrow points in the direction the obstacle should force the enemy to turn.",
|
|
4599
|
-
entity: "Protection Areas",
|
|
4600
|
-
entityType: "Obstacle Effects",
|
|
4601
|
-
entitySubtype: "Turn",
|
|
4602
|
-
value: "270504",
|
|
4603
|
-
minCoordinates: 2,
|
|
4604
|
-
geometry: "line",
|
|
4605
|
-
geometryTypes: ["MultiLineString", "MultiPolygon"],
|
|
4606
|
-
drawRule: "Line10",
|
|
4607
|
-
params: [
|
|
4608
|
-
{
|
|
4609
|
-
key: "arrowHeadLengthRatio",
|
|
4610
|
-
label: "Arrowhead length",
|
|
4611
|
-
description: "Arrowhead length as a ratio of the rear-to-tip distance.",
|
|
4612
|
-
type: "number",
|
|
4613
|
-
min: .01,
|
|
4614
|
-
max: .5,
|
|
4615
|
-
step: .01
|
|
4616
|
-
},
|
|
4617
|
-
{
|
|
4618
|
-
key: "arrowHeadWidthRatio",
|
|
4619
|
-
label: "Arrowhead width",
|
|
4620
|
-
description: "Arrowhead base width as a ratio of the rear-to-tip distance.",
|
|
4621
|
-
type: "number",
|
|
4622
|
-
min: .01,
|
|
4623
|
-
max: .5,
|
|
4624
|
-
step: .01
|
|
4625
|
-
},
|
|
4626
|
-
{
|
|
4627
|
-
key: "arcSegments",
|
|
4628
|
-
label: "Arc segments",
|
|
4629
|
-
description: "Number of line segments approximating the quarter-circle shaft.",
|
|
4630
|
-
type: "number",
|
|
4631
|
-
min: 4,
|
|
4632
|
-
max: 128,
|
|
4633
|
-
step: 1
|
|
4634
|
-
}
|
|
4635
|
-
]
|
|
4636
|
-
};
|
|
4637
|
-
/**
|
|
4638
|
-
* Generates a quarter-circle shaft from PT2 (rear) to the arrowhead base.
|
|
4639
|
-
* The arrowhead continues tangent to the arc and places its tip at PT1.
|
|
4640
|
-
* PT3 selects which side of the PT2 -> PT1 chord contains the arc.
|
|
4641
|
-
*/
|
|
4642
|
-
function createTurn(coordinates, options = {}) {
|
|
4643
|
-
const tip = project(coordinates[0][0], coordinates[0][1]);
|
|
4644
|
-
const rear = project(coordinates[1][0], coordinates[1][1]);
|
|
4645
|
-
const side = quarterArcSide(rear, tip, coordinates[2] ? project(coordinates[2][0], coordinates[2][1]) : void 0);
|
|
4646
|
-
const rearToTip = vecSub(tip, rear);
|
|
4647
|
-
const totalLength = vecMag(rearToTip);
|
|
4648
|
-
if (totalLength < 1e-6) return {
|
|
4649
|
-
type: "FeatureCollection",
|
|
4650
|
-
features: []
|
|
4651
|
-
};
|
|
4652
|
-
const requestedHeadLength = totalLength * (options.arrowHeadLengthRatio ?? DEFAULT_TURN_OPTIONS.arrowHeadLengthRatio);
|
|
4653
|
-
const tangentComponent = Math.min(Math.max(requestedHeadLength, EPSILON), totalLength * .95) / Math.SQRT2;
|
|
4654
|
-
const arcChordLength = Math.sqrt(totalLength * totalLength - tangentComponent * tangentComponent) - tangentComponent;
|
|
4655
|
-
const chordOffset = Math.atan2(tangentComponent, arcChordLength + tangentComponent);
|
|
4656
|
-
const headBase = vecAdd(rear, vecScale(rotate(vecNorm(rearToTip), side * chordOffset), arcChordLength));
|
|
4657
|
-
const arc = createQuarterArc(rear, headBase, side);
|
|
4658
|
-
if (!arc) return {
|
|
4659
|
-
type: "FeatureCollection",
|
|
4660
|
-
features: []
|
|
4661
|
-
};
|
|
4662
|
-
const segmentCount = Math.round(Math.min(128, Math.max(4, options.arcSegments ?? DEFAULT_TURN_OPTIONS.arcSegments)));
|
|
4663
|
-
const arcCoordinates = [];
|
|
4664
|
-
for (let i = 0; i <= segmentCount; i++) {
|
|
4665
|
-
const angle = arc.startAngle + arc.sweepAngle * i / segmentCount;
|
|
4666
|
-
arcCoordinates.push(unproject(arc.center[0] + arc.radius * Math.cos(angle), arc.center[1] + arc.radius * Math.sin(angle)));
|
|
4667
|
-
}
|
|
4668
|
-
const headWidth = totalLength * (options.arrowHeadWidthRatio ?? DEFAULT_TURN_OPTIONS.arrowHeadWidthRatio);
|
|
4669
|
-
const arrowHead = createArrowHeadRing(tip, vecNorm(vecSub(tip, headBase)), vecMag(vecSub(tip, headBase)), headWidth);
|
|
4670
|
-
return {
|
|
4671
|
-
type: "FeatureCollection",
|
|
4672
|
-
features: [{
|
|
4673
|
-
type: "Feature",
|
|
4674
|
-
properties: { part: "arc" },
|
|
4675
|
-
geometry: {
|
|
4676
|
-
type: "MultiLineString",
|
|
4677
|
-
coordinates: [arcCoordinates]
|
|
4678
|
-
}
|
|
4679
|
-
}, {
|
|
4680
|
-
type: "Feature",
|
|
4681
|
-
properties: {
|
|
4682
|
-
part: "arrowhead",
|
|
4683
|
-
fill: true
|
|
4684
|
-
},
|
|
4685
|
-
geometry: {
|
|
4686
|
-
type: "MultiPolygon",
|
|
4687
|
-
coordinates: [[[...arrowHead]]]
|
|
4688
|
-
}
|
|
4689
|
-
}]
|
|
4690
|
-
};
|
|
4691
|
-
}
|
|
4692
|
-
function rotate(vector, angle) {
|
|
4693
|
-
const cos = Math.cos(angle);
|
|
4694
|
-
const sin = Math.sin(angle);
|
|
4695
|
-
return [vector[0] * cos - vector[1] * sin, vector[0] * sin + vector[1] * cos];
|
|
4696
|
-
}
|
|
4697
|
-
//#endregion
|
|
4698
|
-
//#region src/registry.ts
|
|
4699
|
-
/**
|
|
4700
|
-
* The single source of truth for the catalog: one **control measure
|
|
4701
|
-
* definition** record per measure, co-located with its generator and collected
|
|
4702
|
-
* here. The `ControlMeasureId` union, `OptionsByKind`, the generator dispatch,
|
|
4703
|
-
* the default options, and rule resolution are all *derived* from this map
|
|
4704
|
-
* rather than maintained as parallel tables. Adding a measure is a one-file
|
|
4705
|
-
* change plus one line here. See ADR-0013.
|
|
4706
|
-
*/
|
|
4707
|
-
const DEFINITIONS = {
|
|
4708
|
-
ambush: AMBUSH,
|
|
4709
|
-
"principal-direction-of-fire": PRINCIPAL_DIRECTION_OF_FIRE,
|
|
4710
|
-
"final-protective-fire-left": FINAL_PROTECTIVE_FIRE_LEFT,
|
|
4711
|
-
"final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
|
|
4712
|
-
"search-area": SEARCH_AREA,
|
|
4713
|
-
"main-attack": MAIN_ATTACK,
|
|
4714
|
-
"supporting-attack": SUPPORTING_ATTACK,
|
|
4715
|
-
"airborne-attack": AIRBORNE_ATTACK,
|
|
4716
|
-
"attack-helicopter": ATTACK_HELICOPTER,
|
|
4717
|
-
"support-by-fire": SUPPORT_BY_FIRE,
|
|
4718
|
-
"attack-by-fire": ATTACK_BY_FIRE,
|
|
4719
|
-
flot: FLOT,
|
|
4720
|
-
"block-mission-task": BLOCK_MISSION_TASK,
|
|
4721
|
-
breach: BREACH,
|
|
4722
|
-
bypass: BYPASS,
|
|
4723
|
-
canalize: CANALIZE,
|
|
4724
|
-
clear: CLEAR,
|
|
4725
|
-
delay: DELAY,
|
|
4726
|
-
isolate: ISOLATE,
|
|
4727
|
-
"antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
|
|
4728
|
-
"antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
|
|
4729
|
-
"antitank-wall": ANTITANK_WALL,
|
|
4730
|
-
"fortified-line": FORTIFIED_LINE,
|
|
4731
|
-
"fortified-area": FORTIFIED_AREA,
|
|
4732
|
-
block: BLOCK,
|
|
4733
|
-
disrupt: DISRUPT,
|
|
4734
|
-
fix: FIX,
|
|
4735
|
-
turn: defineControlMeasure({
|
|
4736
|
-
metadata: TURN_METADATA,
|
|
4737
|
-
generator: createTurn,
|
|
4738
|
-
defaultOptions: DEFAULT_TURN_OPTIONS,
|
|
4739
|
-
rule: turnDrawRule
|
|
4740
|
-
}),
|
|
4741
|
-
"obstacle-bypass-easy": OBSTACLE_BYPASS_EASY,
|
|
4742
|
-
"obstacle-bypass-difficult": OBSTACLE_BYPASS_DIFFICULT,
|
|
4743
|
-
"obstacle-bypass-impossible": OBSTACLE_BYPASS_IMPOSSIBLE
|
|
4744
|
-
};
|
|
4745
|
-
/** Insertion-ordered list of every control-measure id. */
|
|
4746
|
-
const CONTROL_MEASURE_IDS = Object.keys(DEFINITIONS);
|
|
4747
|
-
/**
|
|
4748
|
-
* Merges a definition's resolved `rule` object back onto its metadata,
|
|
4749
|
-
* preserving ADR-0005's `getControlMeasureMetadata` shape. Definitions whose
|
|
4750
|
-
* `drawRule` id has no implementation leave `rule` undefined.
|
|
4751
|
-
*/
|
|
4752
|
-
function withRule(definition) {
|
|
4753
|
-
return definition.rule ? {
|
|
4754
|
-
...definition.metadata,
|
|
4755
|
-
rule: definition.rule
|
|
4756
|
-
} : definition.metadata;
|
|
4757
|
-
}
|
|
4758
|
-
const CONTROL_MEASURE_METADATA = Object.fromEntries(Object.entries(DEFINITIONS).map(([id, definition]) => [id, withRule(definition)]));
|
|
4759
|
-
const CONTROL_MEASURE_METADATA_LIST = Object.values(CONTROL_MEASURE_METADATA);
|
|
4760
|
-
const CONTROL_MEASURE_METADATA_BY_VALUE = CONTROL_MEASURE_METADATA_LIST.reduce((acc, metadata) => {
|
|
4761
|
-
acc[metadata.value] = metadata;
|
|
4762
|
-
return acc;
|
|
4763
|
-
}, {});
|
|
4764
|
-
function listControlMeasureMetadata() {
|
|
4765
|
-
return CONTROL_MEASURE_METADATA_LIST;
|
|
4766
|
-
}
|
|
4767
|
-
function getControlMeasureMetadata(id) {
|
|
4768
|
-
return CONTROL_MEASURE_METADATA[id];
|
|
4769
|
-
}
|
|
4770
|
-
function getControlMeasureMetadataByValue(value) {
|
|
4771
|
-
return CONTROL_MEASURE_METADATA_BY_VALUE[value];
|
|
4772
|
-
}
|
|
4773
|
-
function getDefaultOptions(kind) {
|
|
4774
|
-
return structuredClone(DEFINITIONS[kind].defaultOptions);
|
|
4775
|
-
}
|
|
4776
|
-
//#endregion
|
|
4777
|
-
//#region src/style.ts
|
|
4778
|
-
/**
|
|
4779
|
-
* Ultimate fallback for the symbol color when no `color` or per-channel
|
|
4780
|
-
* override is supplied at any layer. Keeps a zero-config render monocolor
|
|
4781
|
-
* black and guarantees filled parts still render filled. See ADR-0011.
|
|
4782
|
-
*/
|
|
4783
|
-
const DEFAULT_SYMBOL_COLOR = "#000000";
|
|
4784
|
-
//#endregion
|
|
4785
|
-
//#region src/styleResolver.ts
|
|
4786
|
-
/**
|
|
4787
|
-
* Three-layer style precedence for `renderControlMeasure`.
|
|
4788
|
-
*
|
|
4789
|
-
* Lower-priority → higher-priority: `graphicsStyle` (façade default) →
|
|
4790
|
-
* `measureStyle` (`cm.style`) → `generatorStyle` (per-feature hint emitted
|
|
4791
|
-
* by a generator). Merge is shallow and per-property; higher-priority layers
|
|
4792
|
-
* win key by key. Explicit `null` / `undefined` in a higher-priority layer
|
|
4793
|
-
* is treated as "no opinion" and does not clear a lower-priority key.
|
|
4794
|
-
*
|
|
4795
|
-
* Returns `undefined` when all three layers are absent so the renderer can
|
|
4796
|
-
* omit `properties.style` entirely — keeps the no-style case byte-identical
|
|
4797
|
-
* to a property-less feature.
|
|
4798
|
-
*
|
|
4799
|
-
* Pure function: inputs are never mutated; the returned object is always a
|
|
4800
|
-
* fresh allocation when defined.
|
|
4801
|
-
*/
|
|
4802
|
-
function resolveStyleHints(graphicsStyle, measureStyle, generatorStyle) {
|
|
4803
|
-
if (!graphicsStyle && !measureStyle && !generatorStyle) return;
|
|
4804
|
-
const merged = {};
|
|
4805
|
-
applyLayer(merged, graphicsStyle);
|
|
4806
|
-
applyLayer(merged, measureStyle);
|
|
4807
|
-
applyLayer(merged, generatorStyle);
|
|
4808
|
-
return merged;
|
|
4809
|
-
}
|
|
4810
|
-
/**
|
|
4811
|
-
* Expand cascaded style hints into concrete per-feature paint, per the
|
|
4812
|
-
* monocolor model (ADR-0011).
|
|
4813
|
-
*
|
|
4814
|
-
* A control measure is monocolor: the single `color` paints both stroke and
|
|
4815
|
-
* fill. `strokeColor` / `fillColor` are optional per-channel overrides, and
|
|
4816
|
-
* `DEFAULT_SYMBOL_COLOR` is the ultimate fallback so a zero-config render is
|
|
4817
|
-
* still black and filled parts still render filled.
|
|
4818
|
-
*
|
|
4819
|
-
* `filled` is the generator's intrinsic, color-free marker that a part is a
|
|
4820
|
-
* solid shape. Non-filled parts never carry a `fillColor` — filledness is
|
|
4821
|
-
* intrinsic to the symbol and cannot be conjured by host styling. `color` is
|
|
4822
|
-
* input-only and is stripped from the result.
|
|
4823
|
-
*
|
|
4824
|
-
* Pure: the input is not mutated; a fresh object is always returned.
|
|
4825
|
-
*/
|
|
4826
|
-
function resolveSymbolColor(merged, filled) {
|
|
4827
|
-
const { color, strokeColor, fillColor, ...rest } = merged ?? {};
|
|
4828
|
-
const resolved = {
|
|
4829
|
-
...rest,
|
|
4830
|
-
strokeColor: firstColor(strokeColor, color)
|
|
4831
|
-
};
|
|
4832
|
-
if (filled) resolved.fillColor = firstColor(fillColor, color);
|
|
4833
|
-
return resolved;
|
|
4834
|
-
}
|
|
4835
|
-
/**
|
|
4836
|
-
* First usable color among the candidates, else `DEFAULT_SYMBOL_COLOR`. An
|
|
4837
|
-
* empty string is treated as "not set" (e.g. a cleared color input) rather
|
|
4838
|
-
* than kept as an invalid paint, so the fallback chain still reaches black.
|
|
4839
|
-
*/
|
|
4840
|
-
function firstColor(...candidates) {
|
|
4841
|
-
for (const candidate of candidates) if (candidate !== void 0 && candidate !== "") return candidate;
|
|
4842
|
-
return DEFAULT_SYMBOL_COLOR;
|
|
4843
|
-
}
|
|
4844
|
-
function applyLayer(target, layer) {
|
|
4845
|
-
if (!layer) return;
|
|
4846
|
-
for (const key of Object.keys(layer)) {
|
|
4847
|
-
const value = layer[key];
|
|
4848
|
-
if (value !== void 0 && value !== null) target[key] = value;
|
|
4849
|
-
}
|
|
4850
|
-
}
|
|
4851
|
-
//#endregion
|
|
4852
|
-
//#region src/renderControlMeasure.ts
|
|
4853
|
-
/**
|
|
4854
|
-
* Pure render: `ControlMeasure` → `ControlMeasureRender` (a
|
|
4855
|
-
* `FeatureCollection` of `FeaturePartProps`-typed features). Per-feature
|
|
4856
|
-
* stable ids follow `${cm.id}:${part}:${index}`. Style is resolved from the
|
|
4857
|
-
* three layers (graphicsStyle, cm.style, generator hint) and stamped on
|
|
4858
|
-
* `properties.style` when non-empty.
|
|
4859
|
-
*/
|
|
4860
|
-
function renderControlMeasure(cm, opts) {
|
|
4861
|
-
const features = dispatchControlMeasure(cm, opts).features.map((feature, index) => normalizeFeature(cm.id, feature, index, cm.style, opts?.graphicsStyle));
|
|
4862
|
-
const bbox = computeBBox(features);
|
|
4863
|
-
return {
|
|
4864
|
-
type: "FeatureCollection",
|
|
4865
|
-
...bbox ? { bbox } : {},
|
|
4866
|
-
features
|
|
4867
|
-
};
|
|
4868
|
-
}
|
|
4869
|
-
const EMPTY_COLLECTION = {
|
|
4870
|
-
type: "FeatureCollection",
|
|
4871
|
-
features: []
|
|
4872
|
-
};
|
|
4873
|
-
function dispatchControlMeasure(cm, opts) {
|
|
4874
|
-
const definition = DEFINITIONS[cm.kind];
|
|
4875
|
-
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
4876
|
-
const generator = definition.generator;
|
|
4877
|
-
return generator(cm.controlPoints, cm.options ?? {});
|
|
4878
|
-
}
|
|
4879
|
-
/**
|
|
4880
|
-
* The control-point *input contract*: at least `metadata.minCoordinates` points,
|
|
4881
|
-
* each a finite `[lon, lat]` position. Reports per `validationMode` on
|
|
4882
|
-
* violation (`silent` → empty render, `warn` → log, `throw` → raise) and
|
|
4883
|
-
* returns whether the points may be passed to the generator. Extra points
|
|
4884
|
-
* beyond `maxCoordinates` are ignored, not rejected (ADR-0014).
|
|
4885
|
-
*/
|
|
4886
|
-
function validateInputContract(points, metadata, mode) {
|
|
4887
|
-
const minimum = metadata.minCoordinates ?? 0;
|
|
4888
|
-
if (points.length < minimum) return reportValidationIssue(mode, `"${metadata.id}" requires at least ${minimum} control points; received ${points.length}.`);
|
|
4889
|
-
if (points.some((point) => !isValidPosition(point))) return reportValidationIssue(mode, `"${metadata.id}" control points must contain finite [lon, lat] values.`);
|
|
4890
|
-
return true;
|
|
4891
|
-
}
|
|
4892
|
-
function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
4893
|
-
const sourceProps = feature.properties ?? {};
|
|
4894
|
-
const part = typeof sourceProps.part === "string" && sourceProps.part.length > 0 ? sourceProps.part : "main";
|
|
4895
|
-
const generatorStyle = typeof sourceProps.style === "object" && sourceProps.style !== null ? sourceProps.style : void 0;
|
|
4896
|
-
const filled = sourceProps.fill === true;
|
|
4897
|
-
const style = resolveSymbolColor(resolveStyleHints(graphicsStyle, measureStyle, generatorStyle), filled);
|
|
4898
|
-
const text = typeof sourceProps.text === "string" ? sourceProps.text : void 0;
|
|
4899
|
-
const rotation = typeof sourceProps.rotation === "number" ? sourceProps.rotation : void 0;
|
|
4900
|
-
const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
|
|
4901
|
-
const textSizeZoom = typeof sourceProps.textSizeZoom === "number" ? sourceProps.textSizeZoom : void 0;
|
|
4902
|
-
const textSizeResolution = typeof sourceProps.textSizeResolution === "number" ? sourceProps.textSizeResolution : void 0;
|
|
4903
|
-
return {
|
|
4904
|
-
type: "Feature",
|
|
4905
|
-
id: `${id}:${part}:${index}`,
|
|
4906
|
-
properties: {
|
|
4907
|
-
part,
|
|
4908
|
-
index,
|
|
4909
|
-
style,
|
|
4910
|
-
...text !== void 0 ? { text } : {},
|
|
4911
|
-
...rotation !== void 0 ? { rotation } : {},
|
|
4912
|
-
...textSizePixels !== void 0 ? { textSizePixels } : {},
|
|
4913
|
-
...textSizeZoom !== void 0 ? { textSizeZoom } : {},
|
|
4914
|
-
...textSizeResolution !== void 0 ? { textSizeResolution } : {}
|
|
4915
|
-
},
|
|
4916
|
-
geometry: feature.geometry
|
|
4917
|
-
};
|
|
4918
|
-
}
|
|
4919
|
-
function computeBBox(features) {
|
|
4920
|
-
let bbox;
|
|
4921
|
-
for (const feature of features) visitGeometryPositions(feature.geometry, ([x, y]) => {
|
|
4922
|
-
if (bbox) bbox = [
|
|
4923
|
-
Math.min(bbox[0], x),
|
|
4924
|
-
Math.min(bbox[1], y),
|
|
4925
|
-
Math.max(bbox[2], x),
|
|
4926
|
-
Math.max(bbox[3], y)
|
|
4927
|
-
];
|
|
4928
|
-
else bbox = [
|
|
4929
|
-
x,
|
|
4930
|
-
y,
|
|
4931
|
-
x,
|
|
4932
|
-
y
|
|
4933
|
-
];
|
|
4934
|
-
});
|
|
4935
|
-
return bbox;
|
|
4936
|
-
}
|
|
4937
|
-
function visitGeometryPositions(geometry, visit) {
|
|
4938
|
-
switch (geometry.type) {
|
|
4939
|
-
case "Point":
|
|
4940
|
-
visit(geometry.coordinates);
|
|
4941
|
-
break;
|
|
4942
|
-
case "MultiPoint":
|
|
4943
|
-
case "LineString":
|
|
4944
|
-
geometry.coordinates.forEach(visit);
|
|
4945
|
-
break;
|
|
4946
|
-
case "MultiLineString":
|
|
4947
|
-
case "Polygon":
|
|
4948
|
-
geometry.coordinates.forEach((line) => line.forEach(visit));
|
|
4949
|
-
break;
|
|
4950
|
-
case "MultiPolygon":
|
|
4951
|
-
geometry.coordinates.forEach((polygon) => {
|
|
4952
|
-
polygon.forEach((line) => line.forEach(visit));
|
|
4953
|
-
});
|
|
4954
|
-
break;
|
|
4955
|
-
case "GeometryCollection":
|
|
4956
|
-
geometry.geometries.forEach((item) => visitGeometryPositions(item, visit));
|
|
4957
|
-
break;
|
|
4958
|
-
default: assertNever(geometry);
|
|
4959
|
-
}
|
|
4960
|
-
}
|
|
4961
|
-
function assertNever(value) {
|
|
4962
|
-
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
4963
|
-
}
|
|
4964
|
-
//#endregion
|
|
1
|
+
import { $ as snapToMidpointPerpendicular, A as DEFAULT_BLOCK_MISSION_TASK_OPTIONS, B as line24DrawRule, C as DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS, D as DEFAULT_CANALIZE_OPTIONS, E as DEFAULT_CLEAR_OPTIONS, F as DEFAULT_ANTITANK_DITCH_OPTIONS, G as attackByFireDrawRule, H as turnDrawRule, I as DEFAULT_AMBUSH_OPTIONS, J as blockDrawRule, K as point12DrawRule, L as DEFAULT_AIRBORNE_ATTACK_OPTIONS, M as DEFAULT_ATTACK_HELICOPTER_OPTIONS, N as DEFAULT_ATTACK_BY_FIRE_OPTIONS, O as DEFAULT_BYPASS_OPTIONS, P as DEFAULT_ANTITANK_WALL_OPTIONS, Q as pointOnMidpointPerpendicularAxis, R as axis1DrawRule, S as DEFAULT_FIX_OPTIONS, T as DEFAULT_DELAY_OPTIONS, U as line1DrawRule, V as line23DrawRule, W as ambushDrawRule, X as createMidpointPerpendicularDrawRule, Y as computeDefaultMidpointPerpendicularPoint, Z as getMidpointPerpendicularSignedDistance, _ as DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS, at as EPSILON, b as DEFAULT_FORTIFIED_LINE_OPTIONS, c as getDefaultOptions, d as DEFAULT_SUPPORTING_ATTACK_OPTIONS, et as createBaselineFrame, f as DEFAULT_SUPPORT_BY_FIRE_OPTIONS, g as DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS, h as DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS, i as CONTROL_MEASURE_METADATA, it as unproject, j as DEFAULT_BLOCK_OPTIONS, k as DEFAULT_BREACH_OPTIONS, l as listControlMeasureMetadata, m as DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS, n as resolveStyleHints, nt as computeInitialWidthPoint, o as getControlMeasureMetadata, ot as getMetersPerPixel, p as DEFAULT_TACTICAL_ARROW_OPTIONS, q as disruptDrawRule, r as CONTROL_MEASURE_IDS, rt as project, s as getControlMeasureMetadataByValue, st as roundToFixed, t as renderControlMeasure, tt as calculateMetrics, u as DEFAULT_TURN_OPTIONS, v as DEFAULT_MAIN_ATTACK_OPTIONS, w as DEFAULT_DISRUPT_OPTIONS, x as DEFAULT_FLOT_OPTIONS, y as DEFAULT_FORTIFIED_AREA_OPTIONS, z as supportByFireDrawRule } from "./renderControlMeasure-C7pY-TcL.mjs";
|
|
4965
2
|
//#region src/instance.ts
|
|
4966
3
|
function isKind(cm, kind) {
|
|
4967
4
|
return cm.kind === kind;
|