@orbat-mapper/control-measures 0.2.0-alpha.1 → 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 +7 -0
- package/dist/index-D7uBPw7l.d.mts +1653 -0
- package/dist/index.d.mts +1 -1440
- package/dist/index.mjs +1 -5516
- 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/battle-position.svg +1 -0
- package/media/block-arrow.svg +1 -1
- package/media/boundary.svg +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { q as ControlMeasureId } from "../index-D7uBPw7l.mjs";
|
|
2
|
+
import { FeatureCollection, Geometry } from "geojson";
|
|
3
|
+
|
|
4
|
+
//#region src/preview/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Renders a measure's representative preview to GeoJSON. Pure — no map engine,
|
|
7
|
+
* so it runs identically at build time (docs catalog, README generator) and in
|
|
8
|
+
* the browser (preview app). Returns an empty collection if the generator
|
|
9
|
+
* throws, so a single broken measure never breaks a whole gallery.
|
|
10
|
+
*/
|
|
11
|
+
declare function renderRepresentative(id: ControlMeasureId): FeatureCollection<Geometry, unknown>;
|
|
12
|
+
/** A styling-free preview primitive. Consumers supply stroke, radius, font,
|
|
13
|
+
* and colors when they draw it. */
|
|
14
|
+
interface PreviewShape {
|
|
15
|
+
type: "polyline" | "polygon" | "circle" | "text";
|
|
16
|
+
/** SVG path data for `polyline`/`polygon`. */
|
|
17
|
+
d?: string;
|
|
18
|
+
/** Center for `circle`/`text`. */
|
|
19
|
+
cx?: number;
|
|
20
|
+
cy?: number;
|
|
21
|
+
/** Whether a `polygon` is a doctrinal solid (arrowhead) vs an area outline. */
|
|
22
|
+
filled?: boolean;
|
|
23
|
+
/** Glyph text for `text` (e.g. breach's "B"). */
|
|
24
|
+
text?: string;
|
|
25
|
+
/** Generator-frame rotation (radians) for `text`. */
|
|
26
|
+
rotation?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Target viewBox the geometry is fit into. */
|
|
29
|
+
interface ProjectDimensions {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
pad: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fits a rendered `FeatureCollection` into a `width`×`height` viewBox (inset by
|
|
36
|
+
* `pad`) and emits styling-free {@link PreviewShape}s. North is up (y is
|
|
37
|
+
* flipped). Returns `ok: false` for an empty/degenerate render so consumers can
|
|
38
|
+
* show a geometry-typed fallback glyph.
|
|
39
|
+
*/
|
|
40
|
+
declare function projectRenderToShapes(fc: FeatureCollection<Geometry, unknown>, dims: ProjectDimensions): {
|
|
41
|
+
shapes: PreviewShape[];
|
|
42
|
+
ok: boolean;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { PreviewShape, ProjectDimensions, projectRenderToShapes, renderRepresentative };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { a as DEFINITIONS, t as renderControlMeasure } from "../renderControlMeasure-C7pY-TcL.mjs";
|
|
2
|
+
//#region src/preview/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Unitless `previewSample` points (~`[-1, 1]`) are scaled by this degree offset
|
|
5
|
+
* before reaching a generator. Most generators operate in projected meters, so
|
|
6
|
+
* small degree offsets near the origin yield visible geometry. The single place
|
|
7
|
+
* that knows preview points are degree offsets feeding projected-meter
|
|
8
|
+
* generators.
|
|
9
|
+
*/
|
|
10
|
+
const D = .005;
|
|
11
|
+
/**
|
|
12
|
+
* Generic, category-level layout for measures without a `previewSample`
|
|
13
|
+
* override — keyed off `geometry`/`minCoordinates`/`entityType` so a whole
|
|
14
|
+
* class shares one layout rather than each sibling repeating it. Per-measure
|
|
15
|
+
* quirks live on the definition (ADR-0025). Points are unitless.
|
|
16
|
+
*/
|
|
17
|
+
function fallbackControlPoints(m) {
|
|
18
|
+
const min = m.minCoordinates ?? 2;
|
|
19
|
+
if (m.geometry === "point" || min <= 1) return [[0, 0]];
|
|
20
|
+
if (min === 2) return [[-1, 0], [1, 0]];
|
|
21
|
+
if (m.entityType === "Axis of Advance") return [
|
|
22
|
+
[-1, -.4],
|
|
23
|
+
[1, -.4],
|
|
24
|
+
[1.8, -.4],
|
|
25
|
+
[0, .6]
|
|
26
|
+
];
|
|
27
|
+
if (min === 3) return [
|
|
28
|
+
[-1, -.4],
|
|
29
|
+
[1, -.4],
|
|
30
|
+
[0, .6]
|
|
31
|
+
];
|
|
32
|
+
return [
|
|
33
|
+
[-.3, -.3],
|
|
34
|
+
[.3, -.3],
|
|
35
|
+
[-.6, .1],
|
|
36
|
+
[.6, .1]
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a measure's representative input: its co-located `previewSample`
|
|
41
|
+
* override if present, otherwise the generic {@link fallbackControlPoints}.
|
|
42
|
+
* Unitless points are scaled by {@link D} to the degree offsets a generator
|
|
43
|
+
* expects.
|
|
44
|
+
*/
|
|
45
|
+
function representativeSample(id) {
|
|
46
|
+
const definition = DEFINITIONS[id];
|
|
47
|
+
const sample = definition.previewSample;
|
|
48
|
+
const controlPoints = (sample ? sample.controlPoints : fallbackControlPoints(definition.metadata)).map(([x, y]) => [x * D, y * D]);
|
|
49
|
+
const options = sample?.options;
|
|
50
|
+
return options ? {
|
|
51
|
+
controlPoints,
|
|
52
|
+
options
|
|
53
|
+
} : { controlPoints };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders a measure's representative preview to GeoJSON. Pure — no map engine,
|
|
57
|
+
* so it runs identically at build time (docs catalog, README generator) and in
|
|
58
|
+
* the browser (preview app). Returns an empty collection if the generator
|
|
59
|
+
* throws, so a single broken measure never breaks a whole gallery.
|
|
60
|
+
*/
|
|
61
|
+
function renderRepresentative(id) {
|
|
62
|
+
try {
|
|
63
|
+
const { controlPoints, options } = representativeSample(id);
|
|
64
|
+
return renderControlMeasure({
|
|
65
|
+
id: "preview",
|
|
66
|
+
kind: id,
|
|
67
|
+
controlPoints,
|
|
68
|
+
...options ? { options } : {}
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
return {
|
|
72
|
+
type: "FeatureCollection",
|
|
73
|
+
features: []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fits a rendered `FeatureCollection` into a `width`×`height` viewBox (inset by
|
|
79
|
+
* `pad`) and emits styling-free {@link PreviewShape}s. North is up (y is
|
|
80
|
+
* flipped). Returns `ok: false` for an empty/degenerate render so consumers can
|
|
81
|
+
* show a geometry-typed fallback glyph.
|
|
82
|
+
*/
|
|
83
|
+
function projectRenderToShapes(fc, dims) {
|
|
84
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
85
|
+
const walk = (p) => {
|
|
86
|
+
if (p[0] < minX) minX = p[0];
|
|
87
|
+
if (p[0] > maxX) maxX = p[0];
|
|
88
|
+
if (p[1] < minY) minY = p[1];
|
|
89
|
+
if (p[1] > maxY) maxY = p[1];
|
|
90
|
+
};
|
|
91
|
+
for (const f of fc.features) collectCoords(f.geometry, walk);
|
|
92
|
+
if (!isFinite(minX) || !isFinite(minY)) return {
|
|
93
|
+
shapes: [],
|
|
94
|
+
ok: false
|
|
95
|
+
};
|
|
96
|
+
const spanX = Math.max(maxX - minX, 1e-9);
|
|
97
|
+
const spanY = Math.max(maxY - minY, 1e-9);
|
|
98
|
+
const innerW = dims.width - dims.pad * 2;
|
|
99
|
+
const innerH = dims.height - dims.pad * 2;
|
|
100
|
+
const scale = Math.min(innerW / spanX, innerH / spanY);
|
|
101
|
+
const offsetX = dims.pad + (innerW - spanX * scale) / 2;
|
|
102
|
+
const offsetY = dims.pad + (innerH - spanY * scale) / 2;
|
|
103
|
+
const tx = (p) => [offsetX + (p[0] - minX) * scale, offsetY + (maxY - p[1]) * scale];
|
|
104
|
+
const shapes = [];
|
|
105
|
+
const bboxArea = spanX * spanY;
|
|
106
|
+
for (const f of fc.features) collectShapes(f.geometry, tx, shapes, bboxArea, f.properties);
|
|
107
|
+
return {
|
|
108
|
+
shapes,
|
|
109
|
+
ok: shapes.length > 0
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function ringArea(ring) {
|
|
113
|
+
let a = 0;
|
|
114
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) a += (ring[j][0] - ring[i][0]) * (ring[j][1] + ring[i][1]);
|
|
115
|
+
return Math.abs(a / 2);
|
|
116
|
+
}
|
|
117
|
+
function collectCoords(g, push) {
|
|
118
|
+
switch (g.type) {
|
|
119
|
+
case "Point":
|
|
120
|
+
push(g.coordinates);
|
|
121
|
+
break;
|
|
122
|
+
case "MultiPoint":
|
|
123
|
+
case "LineString":
|
|
124
|
+
g.coordinates.forEach(push);
|
|
125
|
+
break;
|
|
126
|
+
case "MultiLineString":
|
|
127
|
+
case "Polygon":
|
|
128
|
+
g.coordinates.forEach((ring) => ring.forEach(push));
|
|
129
|
+
break;
|
|
130
|
+
case "MultiPolygon":
|
|
131
|
+
g.coordinates.forEach((poly) => poly.forEach((ring) => ring.forEach(push)));
|
|
132
|
+
break;
|
|
133
|
+
case "GeometryCollection":
|
|
134
|
+
g.geometries.forEach((sub) => collectCoords(sub, push));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function pathFromRing(ring, tx, close) {
|
|
139
|
+
if (ring.length === 0) return "";
|
|
140
|
+
const parts = ring.map((p, i) => {
|
|
141
|
+
const [x, y] = tx(p);
|
|
142
|
+
return `${i === 0 ? "M" : "L"}${x.toFixed(2)} ${y.toFixed(2)}`;
|
|
143
|
+
});
|
|
144
|
+
if (close) parts.push("Z");
|
|
145
|
+
return parts.join(" ");
|
|
146
|
+
}
|
|
147
|
+
function collectShapes(g, tx, out, bboxArea, props) {
|
|
148
|
+
const labelProps = props && typeof props === "object" ? props : {};
|
|
149
|
+
const labelText = typeof labelProps.text === "string" ? labelProps.text : void 0;
|
|
150
|
+
const labelRotation = typeof labelProps.rotation === "number" ? labelProps.rotation : void 0;
|
|
151
|
+
const polygonFilled = (ring) => bboxArea > 0 && ringArea(ring) / bboxArea < .25;
|
|
152
|
+
switch (g.type) {
|
|
153
|
+
case "Point": {
|
|
154
|
+
const [x, y] = tx(g.coordinates);
|
|
155
|
+
if (labelText !== void 0) out.push({
|
|
156
|
+
type: "text",
|
|
157
|
+
cx: x,
|
|
158
|
+
cy: y,
|
|
159
|
+
text: labelText,
|
|
160
|
+
rotation: labelRotation
|
|
161
|
+
});
|
|
162
|
+
else out.push({
|
|
163
|
+
type: "circle",
|
|
164
|
+
cx: x,
|
|
165
|
+
cy: y
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "MultiPoint":
|
|
170
|
+
g.coordinates.forEach((p) => {
|
|
171
|
+
const [x, y] = tx(p);
|
|
172
|
+
out.push({
|
|
173
|
+
type: "circle",
|
|
174
|
+
cx: x,
|
|
175
|
+
cy: y
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
case "LineString":
|
|
180
|
+
out.push({
|
|
181
|
+
type: "polyline",
|
|
182
|
+
d: pathFromRing(g.coordinates, tx, false)
|
|
183
|
+
});
|
|
184
|
+
break;
|
|
185
|
+
case "MultiLineString":
|
|
186
|
+
g.coordinates.forEach((line) => out.push({
|
|
187
|
+
type: "polyline",
|
|
188
|
+
d: pathFromRing(line, tx, false)
|
|
189
|
+
}));
|
|
190
|
+
break;
|
|
191
|
+
case "Polygon":
|
|
192
|
+
g.coordinates.forEach((ring) => out.push({
|
|
193
|
+
type: "polygon",
|
|
194
|
+
d: pathFromRing(ring, tx, true),
|
|
195
|
+
filled: polygonFilled(ring)
|
|
196
|
+
}));
|
|
197
|
+
break;
|
|
198
|
+
case "MultiPolygon":
|
|
199
|
+
g.coordinates.forEach((poly) => poly.forEach((ring) => out.push({
|
|
200
|
+
type: "polygon",
|
|
201
|
+
d: pathFromRing(ring, tx, true),
|
|
202
|
+
filled: polygonFilled(ring)
|
|
203
|
+
})));
|
|
204
|
+
break;
|
|
205
|
+
case "GeometryCollection":
|
|
206
|
+
g.geometries.forEach((sub) => collectShapes(sub, tx, out, bboxArea, props));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { projectRenderToShapes, renderRepresentative };
|