@motionscript/geography 0.1.0-alpha.7 → 0.1.0-alpha.8

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/NOTICE +15 -14
  3. package/README.md +78 -14
  4. package/dist/border/format.d.ts +73 -0
  5. package/dist/border/format.d.ts.map +1 -0
  6. package/dist/border/format.js +68 -0
  7. package/dist/border/format.js.map +1 -0
  8. package/dist/border/geo-border.d.ts +103 -0
  9. package/dist/border/geo-border.d.ts.map +1 -0
  10. package/dist/border/geo-border.js +203 -0
  11. package/dist/border/geo-border.js.map +1 -0
  12. package/dist/border/geometry.d.ts +40 -0
  13. package/dist/border/geometry.d.ts.map +1 -0
  14. package/dist/border/geometry.js +382 -0
  15. package/dist/border/geometry.js.map +1 -0
  16. package/dist/border/index.d.ts +15 -0
  17. package/dist/border/index.d.ts.map +1 -0
  18. package/dist/border/index.js +15 -0
  19. package/dist/border/index.js.map +1 -0
  20. package/dist/border/loader.d.ts +31 -0
  21. package/dist/border/loader.d.ts.map +1 -0
  22. package/dist/border/loader.js +87 -0
  23. package/dist/border/loader.js.map +1 -0
  24. package/dist/border/registry.d.ts +64 -0
  25. package/dist/border/registry.d.ts.map +1 -0
  26. package/dist/border/registry.js +163 -0
  27. package/dist/border/registry.js.map +1 -0
  28. package/dist/border/selection.d.ts +21 -0
  29. package/dist/border/selection.d.ts.map +1 -0
  30. package/dist/border/selection.js +86 -0
  31. package/dist/border/selection.js.map +1 -0
  32. package/dist/border/simplify.d.ts +12 -0
  33. package/dist/border/simplify.d.ts.map +1 -0
  34. package/dist/border/simplify.js +116 -0
  35. package/dist/border/simplify.js.map +1 -0
  36. package/dist/browser/THIRD_PARTY_LICENSES.txt +58 -0
  37. package/dist/browser/index.js +2 -0
  38. package/dist/browser/index.js.map +7 -0
  39. package/dist/browser/manifest.json +23 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +3 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/nodes.d.ts +15 -0
  45. package/dist/nodes.d.ts.map +1 -0
  46. package/dist/nodes.js +16 -0
  47. package/dist/nodes.js.map +1 -0
  48. package/package.json +46 -8
  49. package/registry.json +16 -0
  50. package/src/border/format.ts +147 -0
  51. package/src/border/geo-border.ts +268 -0
  52. package/src/border/geometry.ts +463 -0
  53. package/src/border/index.ts +14 -0
  54. package/src/border/loader.ts +97 -0
  55. package/src/border/registry.ts +201 -0
  56. package/src/border/selection.ts +95 -0
  57. package/src/border/simplify.ts +110 -0
  58. package/src/index.ts +2 -0
  59. package/src/nodes.ts +16 -0
@@ -0,0 +1,203 @@
1
+ import { Flow, Graphics2D, Paint, Shapes, Types, component2d, } from "@motionscript/sdk";
2
+ import { snapValue } from "@motionscript/sdk/component";
3
+ import { geoGeometry } from "./geometry.js";
4
+ import { geoDataVersion, geoLoader } from "./loader.js";
5
+ import { geoRegistry } from "./registry.js";
6
+ import { geoFilesFor, normalizeIso, selectGeoFeatures } from "./selection.js";
7
+ // A renderer's cost for one path grows faster than its contour count, and a shape
8
+ // drawn as several paths under one paint is unioned every frame — so a region
9
+ // is cut into paths of bounded size, each painted on its own.
10
+ const CONTOURS_PER_PATH = 256;
11
+ const POINTS_PER_PATH = 40_000;
12
+ const coordinate = (value) => String(Math.round(value * 100) / 100);
13
+ function chunk(contours, total, scale) {
14
+ const chunks = [];
15
+ let parts = [];
16
+ let count = 0;
17
+ let points = 0;
18
+ let from = 0;
19
+ let length = 0;
20
+ const flush = () => {
21
+ if (count === 0)
22
+ return;
23
+ const to = total > 0 ? Math.min(1, length / total) : 1;
24
+ chunks.push({ data: parts.join(""), from, to });
25
+ from = to;
26
+ parts = [];
27
+ count = 0;
28
+ points = 0;
29
+ };
30
+ for (const contour of contours) {
31
+ const p = contour.points;
32
+ // Path data is y-down; the unit geometry is y-up.
33
+ for (let i = 0; i < p.length; i += 2) {
34
+ parts.push(i === 0 ? "M" : "L", coordinate(p[i] * scale), " ", coordinate(-p[i + 1] * scale));
35
+ }
36
+ if (contour.closed)
37
+ parts.push("Z");
38
+ count++;
39
+ points += p.length / 2;
40
+ length += contour.length;
41
+ if (count >= CONTOURS_PER_PATH || points >= POINTS_PER_PATH)
42
+ flush();
43
+ }
44
+ flush();
45
+ return chunks;
46
+ }
47
+ /** One of `values`, switched at the end of a `to`, with its choices listed as its `options`. */
48
+ function choice(values, initial) {
49
+ return Types.custom(initial, { lerp: snapValue, options: values });
50
+ }
51
+ const kept = new WeakMap();
52
+ function keptBy(state) {
53
+ let entry = kept.get(state);
54
+ if (!entry)
55
+ kept.set(state, (entry = { drawn: null, warned: new Set() }));
56
+ return entry;
57
+ }
58
+ /** The selection's outlines cut into paths at `size`, or `null` while there is nothing to draw. */
59
+ function drawnOf(state, size) {
60
+ const own = keptBy(state);
61
+ const codes = state.iso;
62
+ const { level, frame } = state;
63
+ const selection = selectGeoFeatures(codes, level, frame);
64
+ for (const code of selection.unknown) {
65
+ if (own.warned.has(code))
66
+ continue;
67
+ own.warned.add(code);
68
+ console.warn(`[GeoBorder] no region with code ${code}`);
69
+ }
70
+ if (selection.features.length === 0)
71
+ return null;
72
+ const geometry = geoGeometry({
73
+ selectionKey: `${geoRegistry.generation}:${geoDataVersion()}:${level}:${frame}:${codes.join(",")}`,
74
+ features: selection.features,
75
+ frame,
76
+ projection: state.projection,
77
+ borders: state.borders,
78
+ detail: state.detail,
79
+ });
80
+ if (!geometry)
81
+ return null;
82
+ const { width, height } = size;
83
+ const cached = own.drawn;
84
+ if (cached && cached.geometry === geometry && cached.width === width && cached.height === height)
85
+ return cached;
86
+ const scale = Math.min(width / geometry.width, height / geometry.height);
87
+ const drawn = {
88
+ geometry,
89
+ width,
90
+ height,
91
+ fills: chunk(geometry.fills, geometry.fillLength, scale),
92
+ lines: chunk(geometry.lines, geometry.lineLength, scale),
93
+ };
94
+ own.drawn = drawn;
95
+ return drawn;
96
+ }
97
+ /** Adds `chunk` to `g`, trimmed by `start`/`end` along the whole drawing; `false` when none of it is drawn. */
98
+ function pathOp(g, chunk, size, start, end) {
99
+ const { width, height } = size;
100
+ const centerBounds = [-width / 2, -height / 2, width / 2, height / 2];
101
+ if (start <= 0 && end >= 1) {
102
+ g.path({ data: chunk.data, centerBounds });
103
+ return true;
104
+ }
105
+ const span = chunk.to - chunk.from;
106
+ if (span <= 0)
107
+ return false;
108
+ const localStart = Math.max(0, (start - chunk.from) / span);
109
+ const localEnd = Math.min(1, (end - chunk.from) / span);
110
+ if (localEnd <= localStart)
111
+ return false;
112
+ g.path({ data: chunk.data, centerBounds, start: localStart, end: localEnd });
113
+ return true;
114
+ }
115
+ /**
116
+ * Country and subdivision outlines by ISO 3166 code, fitted into the node's box.
117
+ *
118
+ * <GeoBorder iso={["FR", "DE"]} detail={0.4} stroke={{ weight: 2, fill: "white" }} />
119
+ *
120
+ * Geometry comes from whatever `geoRegistry` has been given — a URL, a loader,
121
+ * or shapes defined by hand — and loads as a declared asset, so a frame never
122
+ * draws half a country. Fill and stroke are separate geometry: the fill is each
123
+ * region's polygons, the stroke is every border once, which is why a border two
124
+ * selected regions share is not drawn twice and why `stroke.align` does not
125
+ * apply.
126
+ */
127
+ export const GeoBorder = component2d({
128
+ key: "geoBorder",
129
+ state: {
130
+ ...Paint.state,
131
+ ...Flow.state,
132
+ iso: Types.custom(["FR"], { resolve: normalizeIso, lerp: snapValue }),
133
+ level: choice(["country", "state"], "country"),
134
+ detail: Types.number(0.5, { min: 0, max: 1, step: 0.01 }),
135
+ projection: choice(["conic", "mercator", "equirectangular", "orthographic"], "conic"),
136
+ frame: choice(["mainland", "all"], "mainland"),
137
+ borders: choice(["all", "outline"], "all"),
138
+ },
139
+ commands: { ...Paint.commands },
140
+ // The data gates the draw, not layout: the box never waits on it.
141
+ load: (loader, state) => {
142
+ for (const file of geoFilesFor(state.iso, state.level))
143
+ loader.task(`geo:${file}`, geoLoader(file));
144
+ },
145
+ measure: (measurer, state, children) => Flow.measure(measurer, state, children, Paint.inset(state)),
146
+ layout: (box, state, children) => Flow.layout(box, state, children, Paint.inset(state)),
147
+ render: (ctx, state) => {
148
+ const size = ctx.size;
149
+ const { start, end } = state;
150
+ const fill = state.fill;
151
+ const shadow = state.shadow;
152
+ const drawn = () => drawnOf(state, size);
153
+ ctx.inFront(() => {
154
+ const shape = drawn();
155
+ if (!shape || shape.fills.length === 0)
156
+ return;
157
+ const g = new Graphics2D();
158
+ for (const part of shape.fills) {
159
+ if (!pathOp(g, part, size, start, end))
160
+ continue;
161
+ if (shadow.length > 0)
162
+ g.shadow(shadow);
163
+ g.fill(fill);
164
+ }
165
+ ctx.draw(g);
166
+ });
167
+ // The box, not the outline, confines the children and is what picking
168
+ // tests: a path clip has no winding test, and a country's bounding box is
169
+ // what the selection shows.
170
+ Paint.draw(ctx, state, null, { clipShape: Shapes.box(size) });
171
+ ctx.inFront(() => {
172
+ const overlay = state.overlay;
173
+ if (overlay.length > 0) {
174
+ const shape = drawn();
175
+ if (shape) {
176
+ const g = new Graphics2D();
177
+ for (const part of shape.fills)
178
+ if (pathOp(g, part, size, start, end))
179
+ g.fill(overlay);
180
+ ctx.draw(g);
181
+ }
182
+ }
183
+ const stroke = state.stroke;
184
+ if (stroke.length === 0)
185
+ return;
186
+ const shape = drawn();
187
+ if (!shape || shape.lines.length === 0)
188
+ return;
189
+ const centred = stroke.map((layer) => (layer.align === 0 ? layer : { ...layer, align: 0 }));
190
+ const castsShadow = shadow.length > 0 && !fill.some((layer) => (layer.opacity ?? 1) > 0);
191
+ const g = new Graphics2D();
192
+ for (const part of shape.lines) {
193
+ if (!pathOp(g, part, size, start, end))
194
+ continue;
195
+ if (castsShadow)
196
+ g.shadow(shadow);
197
+ g.stroke(centred);
198
+ }
199
+ ctx.draw(g);
200
+ });
201
+ },
202
+ });
203
+ //# sourceMappingURL=geo-border.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo-border.js","sourceRoot":"","sources":["../../src/border/geo-border.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n Flow,\r\n Graphics2D,\r\n Paint,\r\n Shapes,\r\n Types,\r\n component2d,\r\n type EntryOptions,\r\n type FillResolved,\r\n type NodeOf,\r\n // Named so this component's emitted type reaches the shape surface it spreads through the sdk, not core.\r\n type Alignment as _Alignment,\r\n type Fill as _Fill,\r\n type FlowMode as _FlowMode,\r\n type FlowResolved as _FlowResolved,\r\n type GapSize as _GapSize,\r\n type Node2D as _Node2D,\r\n type Node2DProps as _Node2DProps,\r\n type Shadow as _Shadow,\r\n type Stroke as _Stroke,\r\n type Vector2 as _Vector2,\r\n type PathBounds,\r\n type ShadowResolved,\r\n type ShapeProps,\r\n type Size2D,\r\n type StrokeResolved,\r\n type Type,\r\n} from \"@motionscript/sdk\"\r\nimport { snapValue } from \"@motionscript/sdk/component\"\r\n\r\nimport { geoGeometry, type GeoBorders, type GeoContour, type GeoGeometry, type GeoProjectionKind } from \"./geometry\"\r\nimport { geoDataVersion, geoLoader } from \"./loader\"\r\nimport { geoRegistry } from \"./registry\"\r\nimport { geoFilesFor, normalizeIso, selectGeoFeatures, type GeoFrame, type GeoLevel } from \"./selection\"\r\n\r\nexport interface GeoBorderProps extends ShapeProps {\r\n /** ISO 3166-1 alpha-2 (`\"FR\"`) or ISO 3166-2 (`\"US-CA\"`) codes. */\r\n iso: string | readonly string[]\r\n level: GeoLevel\r\n /** 0–1: 1 is full resolution, lower is a more stylised outline. */\r\n detail: number\r\n projection: GeoProjectionKind\r\n frame: GeoFrame\r\n borders: GeoBorders\r\n}\r\n\r\ninterface Chunk {\r\n data: string\r\n /** Where this chunk's contours sit along the whole drawing, as fractions of its length. */\r\n from: number\r\n to: number\r\n}\r\n\r\ninterface Drawn {\r\n geometry: GeoGeometry\r\n width: number\r\n height: number\r\n fills: Chunk[]\r\n lines: Chunk[]\r\n}\r\n\r\n// A renderer's cost for one path grows faster than its contour count, and a shape\r\n// drawn as several paths under one paint is unioned every frame — so a region\r\n// is cut into paths of bounded size, each painted on its own.\r\nconst CONTOURS_PER_PATH = 256\r\nconst POINTS_PER_PATH = 40_000\r\n\r\nconst coordinate = (value: number) => String(Math.round(value * 100) / 100)\r\n\r\nfunction chunk(contours: readonly GeoContour[], total: number, scale: number): Chunk[] {\r\n const chunks: Chunk[] = []\r\n let parts: string[] = []\r\n let count = 0\r\n let points = 0\r\n let from = 0\r\n let length = 0\r\n const flush = () => {\r\n if (count === 0) return\r\n const to = total > 0 ? Math.min(1, length / total) : 1\r\n chunks.push({ data: parts.join(\"\"), from, to })\r\n from = to\r\n parts = []\r\n count = 0\r\n points = 0\r\n }\r\n for (const contour of contours) {\r\n const p = contour.points\r\n // Path data is y-down; the unit geometry is y-up.\r\n for (let i = 0; i < p.length; i += 2) {\r\n parts.push(i === 0 ? \"M\" : \"L\", coordinate(p[i] * scale), \" \", coordinate(-p[i + 1] * scale))\r\n }\r\n if (contour.closed) parts.push(\"Z\")\r\n count++\r\n points += p.length / 2\r\n length += contour.length\r\n if (count >= CONTOURS_PER_PATH || points >= POINTS_PER_PATH) flush()\r\n }\r\n flush()\r\n return chunks\r\n}\r\n\r\n/** One of `values`, switched at the end of a `to`, with its choices listed as its `options`. */\r\nfunction choice<const V extends readonly string[]>(values: V, initial: V[number]): Type<V[number]> {\r\n return Types.custom<V[number], V[number], EntryOptions & { readonly options: V }>(initial, { lerp: snapValue, options: values })\r\n}\r\n\r\n/** A node's own drawing state, kept by its fields' view (one object per node, the same every frame). */\r\ninterface Kept {\r\n drawn: Drawn | null\r\n readonly warned: Set<string>\r\n}\r\n\r\nconst kept = new WeakMap<object, Kept>()\r\n\r\nfunction keptBy(state: object): Kept {\r\n let entry = kept.get(state)\r\n if (!entry) kept.set(state, (entry = { drawn: null, warned: new Set() }))\r\n return entry\r\n}\r\n\r\ninterface Selected {\r\n readonly iso: readonly string[]\r\n readonly level: GeoLevel\r\n readonly frame: GeoFrame\r\n readonly projection: GeoProjectionKind\r\n readonly borders: GeoBorders\r\n readonly detail: number\r\n}\r\n\r\n/** The selection's outlines cut into paths at `size`, or `null` while there is nothing to draw. */\r\nfunction drawnOf(state: Selected, size: Size2D): Drawn | null {\r\n const own = keptBy(state)\r\n const codes = state.iso\r\n const { level, frame } = state\r\n const selection = selectGeoFeatures(codes, level, frame)\r\n for (const code of selection.unknown) {\r\n if (own.warned.has(code)) continue\r\n own.warned.add(code)\r\n console.warn(`[GeoBorder] no region with code ${code}`)\r\n }\r\n if (selection.features.length === 0) return null\r\n\r\n const geometry = geoGeometry({\r\n selectionKey: `${geoRegistry.generation}:${geoDataVersion()}:${level}:${frame}:${codes.join(\",\")}`,\r\n features: selection.features,\r\n frame,\r\n projection: state.projection,\r\n borders: state.borders,\r\n detail: state.detail,\r\n })\r\n if (!geometry) return null\r\n\r\n const { width, height } = size\r\n const cached = own.drawn\r\n if (cached && cached.geometry === geometry && cached.width === width && cached.height === height) return cached\r\n const scale = Math.min(width / geometry.width, height / geometry.height)\r\n const drawn: Drawn = {\r\n geometry,\r\n width,\r\n height,\r\n fills: chunk(geometry.fills, geometry.fillLength, scale),\r\n lines: chunk(geometry.lines, geometry.lineLength, scale),\r\n }\r\n own.drawn = drawn\r\n return drawn\r\n}\r\n\r\n/** Adds `chunk` to `g`, trimmed by `start`/`end` along the whole drawing; `false` when none of it is drawn. */\r\nfunction pathOp(g: Graphics2D, chunk: Chunk, size: Size2D, start: number, end: number): boolean {\r\n const { width, height } = size\r\n const centerBounds: PathBounds = [-width / 2, -height / 2, width / 2, height / 2]\r\n if (start <= 0 && end >= 1) {\r\n g.path({ data: chunk.data, centerBounds })\r\n return true\r\n }\r\n const span = chunk.to - chunk.from\r\n if (span <= 0) return false\r\n const localStart = Math.max(0, (start - chunk.from) / span)\r\n const localEnd = Math.min(1, (end - chunk.from) / span)\r\n if (localEnd <= localStart) return false\r\n g.path({ data: chunk.data, centerBounds, start: localStart, end: localEnd })\r\n return true\r\n}\r\n\r\n/**\r\n * Country and subdivision outlines by ISO 3166 code, fitted into the node's box.\r\n *\r\n * <GeoBorder iso={[\"FR\", \"DE\"]} detail={0.4} stroke={{ weight: 2, fill: \"white\" }} />\r\n *\r\n * Geometry comes from whatever `geoRegistry` has been given — a URL, a loader,\r\n * or shapes defined by hand — and loads as a declared asset, so a frame never\r\n * draws half a country. Fill and stroke are separate geometry: the fill is each\r\n * region's polygons, the stroke is every border once, which is why a border two\r\n * selected regions share is not drawn twice and why `stroke.align` does not\r\n * apply.\r\n */\r\nexport const GeoBorder = component2d({\r\n key: \"geoBorder\",\r\n state: {\r\n ...Paint.state,\r\n ...Flow.state,\r\n iso: Types.custom<string | readonly string[], string[]>([\"FR\"], { resolve: normalizeIso, lerp: snapValue }),\r\n level: choice([\"country\", \"state\"], \"country\"),\r\n detail: Types.number(0.5, { min: 0, max: 1, step: 0.01 }),\r\n projection: choice([\"conic\", \"mercator\", \"equirectangular\", \"orthographic\"], \"conic\"),\r\n frame: choice([\"mainland\", \"all\"], \"mainland\"),\r\n borders: choice([\"all\", \"outline\"], \"all\"),\r\n },\r\n commands: { ...Paint.commands },\r\n // The data gates the draw, not layout: the box never waits on it.\r\n load: (loader, state) => {\r\n for (const file of geoFilesFor(state.iso, state.level)) loader.task(`geo:${file}`, geoLoader(file))\r\n },\r\n measure: (measurer, state, children) => Flow.measure(measurer, state, children, Paint.inset(state)),\r\n layout: (box, state, children) => Flow.layout(box, state, children, Paint.inset(state)),\r\n render: (ctx, state) => {\r\n const size = ctx.size\r\n const { start, end } = state\r\n const fill = state.fill as FillResolved[]\r\n const shadow = state.shadow as ShadowResolved[]\r\n const drawn = () => drawnOf(state, size)\r\n\r\n ctx.inFront(() => {\r\n const shape = drawn()\r\n if (!shape || shape.fills.length === 0) return\r\n const g = new Graphics2D()\r\n for (const part of shape.fills) {\r\n if (!pathOp(g, part, size, start, end)) continue\r\n if (shadow.length > 0) g.shadow(shadow)\r\n g.fill(fill)\r\n }\r\n ctx.draw(g)\r\n })\r\n\r\n // The box, not the outline, confines the children and is what picking\r\n // tests: a path clip has no winding test, and a country's bounding box is\r\n // what the selection shows.\r\n Paint.draw(ctx, state, null, { clipShape: Shapes.box(size) })\r\n\r\n ctx.inFront(() => {\r\n const overlay = state.overlay as FillResolved[]\r\n if (overlay.length > 0) {\r\n const shape = drawn()\r\n if (shape) {\r\n const g = new Graphics2D()\r\n for (const part of shape.fills) if (pathOp(g, part, size, start, end)) g.fill(overlay)\r\n ctx.draw(g)\r\n }\r\n }\r\n\r\n const stroke = state.stroke as StrokeResolved[]\r\n if (stroke.length === 0) return\r\n const shape = drawn()\r\n if (!shape || shape.lines.length === 0) return\r\n const centred = stroke.map((layer) => (layer.align === 0 ? layer : { ...layer, align: 0 }))\r\n const castsShadow = shadow.length > 0 && !fill.some((layer) => (layer.opacity ?? 1) > 0)\r\n const g = new Graphics2D()\r\n for (const part of shape.lines) {\r\n if (!pathOp(g, part, size, start, end)) continue\r\n if (castsShadow) g.shadow(shadow)\r\n g.stroke(centred)\r\n }\r\n ctx.draw(g)\r\n })\r\n },\r\n})\r\n\r\nexport type GeoBorder = NodeOf<typeof GeoBorder>\r\n"]}
@@ -0,0 +1,40 @@
1
+ import { type GeoFeature } from "./format.js";
2
+ import type { GeoFrame } from "./selection.js";
3
+ export type GeoProjectionKind = "conic" | "mercator" | "equirectangular" | "orthographic";
4
+ export type GeoBorders = "all" | "outline";
5
+ export interface GeoContour {
6
+ /** Interleaved x/y in unit space: the longer side spans 1, centred on the origin, y up. */
7
+ points: Float32Array;
8
+ closed: boolean;
9
+ length: number;
10
+ }
11
+ export interface GeoGeometry {
12
+ /** The drawing's extent in unit space; the longer of the two is 1. */
13
+ width: number;
14
+ height: number;
15
+ fills: GeoContour[];
16
+ lines: GeoContour[];
17
+ fillLength: number;
18
+ lineLength: number;
19
+ bytes: number;
20
+ }
21
+ /** The highest weight bucket a point may carry and still be drawn. */
22
+ export declare function pointBucketLimit(detail: number, area: number): number;
23
+ /** Rings smaller than this (steradians) are dropped, bar each region's largest. */
24
+ export declare function polygonAreaLimit(detail: number, area: number): number;
25
+ export interface GeoGeometryRequest {
26
+ /** Identifies `features` and `frame`: equal keys must mean equal selections. */
27
+ selectionKey: string;
28
+ features: readonly GeoFeature[];
29
+ frame: GeoFrame;
30
+ projection: GeoProjectionKind;
31
+ borders: GeoBorders;
32
+ detail: number;
33
+ }
34
+ /**
35
+ * The drawing for a selection, cached. The projection is fitted to the
36
+ * selection at full detail, so a `detail` tween never moves or rescales it.
37
+ */
38
+ export declare function geoGeometry(request: GeoGeometryRequest): GeoGeometry | null;
39
+ export declare function clearGeoGeometry(): void;
40
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/border/geometry.ts"],"names":[],"mappings":"AAaA,OAAO,EAAgC,KAAK,UAAU,EAAoC,MAAM,UAAU,CAAA;AAC1G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,iBAAiB,GAAG,cAAc,CAAA;AACzF,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,SAAS,CAAA;AAE1C,MAAM,WAAW,UAAU;IACvB,2FAA2F;IAC3F,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IACxB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;CAChB;AA6BD,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrE;AA+VD,MAAM,WAAW,kBAAkB;IAC/B,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAA;IACf,UAAU,EAAE,iBAAiB,CAAA;IAC7B,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,GAAG,IAAI,CAiB3E;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC"}
@@ -0,0 +1,382 @@
1
+ import { geoArea, geoBounds, geoConicEqualArea, geoEquirectangular, geoMercator, geoOrthographic, geoPath, geoStream, } from "d3-geo";
2
+ import { ringCoordinates } from "./format.js";
3
+ const FIT = 1024;
4
+ // `detail` maps onto a weight threshold that is a fraction of the selection's
5
+ // own area, on a log scale — which is what makes a value look equally stylised
6
+ // on Monaco and on Russia. 0 keeps points worth 1% of the area; just below 1,
7
+ // points worth a billionth of it.
8
+ const POINT_BASE = 2;
9
+ const POINT_RANGE = 7;
10
+ const POLYGON_BASE = 1.5;
11
+ const POLYGON_RANGE = 6;
12
+ const clamp01 = (value) => (value > 1 ? 1 : value > 0 ? value : 0);
13
+ /** The highest weight bucket a point may carry and still be drawn. */
14
+ export function pointBucketLimit(detail, area) {
15
+ if (detail >= 1 || !(area > 0))
16
+ return 255;
17
+ const minWeight = area * 10 ** -(POINT_BASE + POINT_RANGE * clamp01(detail));
18
+ return Math.floor(-2 * Math.log2(minWeight));
19
+ }
20
+ /** Rings smaller than this (steradians) are dropped, bar each region's largest. */
21
+ export function polygonAreaLimit(detail, area) {
22
+ if (detail >= 1)
23
+ return 0;
24
+ return area * 10 ** -(POLYGON_BASE + POLYGON_RANGE * clamp01(detail));
25
+ }
26
+ function partsOf(features, frame) {
27
+ const parts = [];
28
+ for (const feature of features) {
29
+ const mainland = frame === "mainland" ? feature.polygons.filter((p) => p.mainland) : [];
30
+ const polygons = mainland.length > 0 ? mainland : feature.polygons;
31
+ let largest = null;
32
+ for (const polygon of polygons)
33
+ if (!largest || polygon.area > largest.area)
34
+ largest = polygon;
35
+ for (const polygon of polygons)
36
+ parts.push({ polygon, largest: polygon === largest });
37
+ }
38
+ return parts;
39
+ }
40
+ function centerLongitude(west, east) {
41
+ if (west <= east)
42
+ return (west + east) / 2;
43
+ const mid = (west + east + 360) / 2;
44
+ return mid > 180 ? mid - 360 : mid;
45
+ }
46
+ function makeProjection(kind, west, south, east, north) {
47
+ const lon = centerLongitude(west, east);
48
+ const lat = (south + north) / 2;
49
+ switch (kind) {
50
+ case "mercator":
51
+ return geoMercator().rotate([-lon, 0]);
52
+ case "equirectangular":
53
+ return geoEquirectangular().rotate([-lon, 0]);
54
+ case "orthographic":
55
+ return geoOrthographic().rotate([-lon, -lat]);
56
+ case "conic": {
57
+ const span = north - south;
58
+ return geoConicEqualArea()
59
+ .rotate([-lon, 0])
60
+ .center([0, lat])
61
+ .parallels([south + span / 6, north - span / 6]);
62
+ }
63
+ }
64
+ }
65
+ function fitFrame(kind, parts) {
66
+ if (parts.length === 0)
67
+ return null;
68
+ const shape = {
69
+ type: "MultiPolygon",
70
+ coordinates: parts.map(({ polygon }) => [ringCoordinates(polygon.rings[0])]),
71
+ };
72
+ const [[west, south], [east, north]] = geoBounds(shape);
73
+ for (const candidate of [kind, "equirectangular"]) {
74
+ const projection = makeProjection(candidate, west, south, east, north).fitSize([FIT, FIT], shape);
75
+ const [[x0, y0], [x1, y1]] = geoPath(projection).bounds(shape);
76
+ const size = Math.max(x1 - x0, y1 - y0);
77
+ if (!Number.isFinite(size) || size <= 0)
78
+ continue;
79
+ return {
80
+ projection,
81
+ centerX: (x0 + x1) / 2,
82
+ centerY: (y0 + y1) / 2,
83
+ size,
84
+ width: (x1 - x0) / size,
85
+ height: (y1 - y0) / size,
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+ function project(frame, object, closed) {
91
+ const contours = [];
92
+ let current = null;
93
+ const { centerX, centerY, size } = frame;
94
+ const sink = {
95
+ point(x, y) {
96
+ current?.push((x - centerX) / size, -(y - centerY) / size);
97
+ },
98
+ lineStart() {
99
+ current = [];
100
+ },
101
+ lineEnd() {
102
+ const flat = current;
103
+ current = null;
104
+ if (!flat || flat.length < 4)
105
+ return;
106
+ let isClosed = closed;
107
+ if (!closed) {
108
+ const n = flat.length;
109
+ if (flat[0] === flat[n - 2] && flat[1] === flat[n - 1] && n >= 8) {
110
+ flat.length = n - 2;
111
+ isClosed = true;
112
+ }
113
+ }
114
+ const points = Float32Array.from(flat);
115
+ contours.push({ points, closed: isClosed, length: contourLength(points, isClosed) });
116
+ },
117
+ polygonStart() { },
118
+ polygonEnd() { },
119
+ sphere() { },
120
+ };
121
+ geoStream(object, frame.projection.stream(sink));
122
+ return contours;
123
+ }
124
+ function contourLength(points, closed) {
125
+ let length = 0;
126
+ const n = points.length / 2;
127
+ for (let i = 1; i < n; i++) {
128
+ length += Math.hypot(points[i * 2] - points[i * 2 - 2], points[i * 2 + 1] - points[i * 2 - 1]);
129
+ }
130
+ if (closed && n > 1)
131
+ length += Math.hypot(points[0] - points[n * 2 - 2], points[1] - points[n * 2 - 1]);
132
+ return length;
133
+ }
134
+ // Longest edge, in degrees, that simplification may leave. d3 takes an edge as
135
+ // the short way round the sphere, so an edge across more than half the planet
136
+ // (Kaliningrad to Chukotka, once the coast between is dropped) turns inside out.
137
+ const MAX_EDGE_DEGREES = 20;
138
+ /** Which of an arc's points survive `limit`, the same whichever way the arc is walked. */
139
+ function keepMask(arc, limit) {
140
+ const n = arc.buckets.length;
141
+ const mask = new Uint8Array(n);
142
+ let lastLon = arc.points[0];
143
+ let lastLat = arc.points[1];
144
+ for (let i = 0; i < n; i++) {
145
+ const lon = arc.points[i * 2];
146
+ const lat = arc.points[i * 2 + 1];
147
+ let far = false;
148
+ if (arc.buckets[i] > limit) {
149
+ let dLon = Math.abs(lon - lastLon);
150
+ if (dLon > 180)
151
+ dLon = 360 - dLon;
152
+ far = Math.abs(lat - lastLat) > MAX_EDGE_DEGREES || dLon * Math.cos((lat * Math.PI) / 180) > MAX_EDGE_DEGREES;
153
+ }
154
+ if (arc.buckets[i] <= limit || far) {
155
+ mask[i] = 1;
156
+ lastLon = lon;
157
+ lastLat = lat;
158
+ }
159
+ }
160
+ return mask;
161
+ }
162
+ function filteredPoints(refs, maskFor) {
163
+ const out = [];
164
+ for (const { arc, reversed } of refs) {
165
+ const n = arc.buckets.length;
166
+ const mask = maskFor(arc);
167
+ for (let j = out.length === 0 ? 0 : 1; j < n; j++) {
168
+ const i = reversed ? n - 1 - j : j;
169
+ if (mask[i])
170
+ out.push([arc.points[i * 2], arc.points[i * 2 + 1]]);
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ /** A ring keeps at least three vertices, raising its arcs' limits if the global one would collapse it. */
176
+ function ringLimit(ring, limit) {
177
+ let kept = 0;
178
+ for (const { arc } of ring) {
179
+ for (let i = 0; i < arc.buckets.length - 1; i++)
180
+ if (arc.buckets[i] <= limit)
181
+ kept++;
182
+ }
183
+ if (kept >= 3)
184
+ return limit;
185
+ const buckets = [];
186
+ for (const { arc } of ring)
187
+ for (let i = 0; i < arc.buckets.length - 1; i++)
188
+ buckets.push(arc.buckets[i]);
189
+ buckets.sort((a, b) => a - b);
190
+ return Math.max(limit, buckets[Math.min(2, buckets.length - 1)] ?? limit);
191
+ }
192
+ /**
193
+ * Whether a simplified ring still winds the way its original did. Dropping
194
+ * points can fold a small ring over itself, and d3 reads an exterior wound the
195
+ * wrong way as the rest of the planet — one such island fills the whole frame.
196
+ */
197
+ function windsAsOriginal(ring, exterior) {
198
+ if (ring.length < 4)
199
+ return true;
200
+ const inverted = geoArea({ type: "Polygon", coordinates: [ring] }) > 2 * Math.PI;
201
+ return inverted !== exterior;
202
+ }
203
+ const endKey = (arc, last) => {
204
+ const i = last ? arc.buckets.length - 1 : 0;
205
+ return `${arc.points[i * 2]},${arc.points[i * 2 + 1]}`;
206
+ };
207
+ /** Joins arcs that meet end to end with nothing else at the joint, so a border draws as one line with real joins. */
208
+ function chainArcs(arcs) {
209
+ const atNode = new Map();
210
+ for (const arc of arcs) {
211
+ for (const key of [endKey(arc, false), endKey(arc, true)]) {
212
+ const list = atNode.get(key);
213
+ if (list)
214
+ list.push(arc);
215
+ else
216
+ atNode.set(key, [arc]);
217
+ }
218
+ }
219
+ const visited = new Set();
220
+ const chains = [];
221
+ const refStart = ({ arc, reversed }) => endKey(arc, reversed);
222
+ const refEnd = ({ arc, reversed }) => endKey(arc, !reversed);
223
+ const nextAt = (node) => {
224
+ const list = atNode.get(node);
225
+ if (list.length !== 2)
226
+ return null;
227
+ return list.find((arc) => !visited.has(arc)) ?? null;
228
+ };
229
+ for (const arc of arcs) {
230
+ if (visited.has(arc))
231
+ continue;
232
+ visited.add(arc);
233
+ const refs = [{ arc, reversed: false }];
234
+ for (let next = nextAt(refEnd(refs[refs.length - 1])); next; next = nextAt(refEnd(refs[refs.length - 1]))) {
235
+ visited.add(next);
236
+ refs.push({ arc: next, reversed: endKey(next, false) !== refEnd(refs[refs.length - 1]) });
237
+ }
238
+ for (let prev = nextAt(refStart(refs[0])); prev; prev = nextAt(refStart(refs[0]))) {
239
+ visited.add(prev);
240
+ refs.unshift({ arc: prev, reversed: endKey(prev, true) !== refStart(refs[0]) });
241
+ }
242
+ chains.push({ refs, closed: refStart(refs[0]) === refEnd(refs[refs.length - 1]) });
243
+ }
244
+ return chains;
245
+ }
246
+ function buildGeometry(features, frame, fitted, borders, detail) {
247
+ const parts = partsOf(features, frame);
248
+ const area = parts.reduce((sum, { polygon }) => sum + polygon.area, 0);
249
+ const pointLimit = pointBucketLimit(detail, area);
250
+ const polygonLimit = polygonAreaLimit(detail, area);
251
+ const kept = [];
252
+ for (const { polygon, largest } of parts) {
253
+ if (!largest && polygon.area < polygonLimit)
254
+ continue;
255
+ kept.push(polygon.rings.filter((_, i) => i === 0 || polygon.ringAreas[i] >= polygonLimit));
256
+ }
257
+ const limits = new Map();
258
+ const usage = new Map();
259
+ for (const rings of kept) {
260
+ for (const ring of rings) {
261
+ const limit = ringLimit(ring, pointLimit);
262
+ for (const { arc } of ring) {
263
+ limits.set(arc.key, Math.max(limits.get(arc.key) ?? 0, limit));
264
+ const use = usage.get(arc.key);
265
+ if (use)
266
+ use.count++;
267
+ else
268
+ usage.set(arc.key, { arc, count: 1 });
269
+ }
270
+ }
271
+ }
272
+ const masks = new Map();
273
+ const maskFor = (arc) => {
274
+ let mask = masks.get(arc.key);
275
+ if (!mask) {
276
+ mask = keepMask(arc, limits.get(arc.key) ?? 255);
277
+ masks.set(arc.key, mask);
278
+ }
279
+ return mask;
280
+ };
281
+ if (pointLimit < 255) {
282
+ for (const rings of kept) {
283
+ rings.forEach((ring, index) => {
284
+ let limit = Math.min(...ring.map(({ arc }) => limits.get(arc.key) ?? pointLimit));
285
+ while (limit < 255 && !windsAsOriginal(filteredPoints(ring, maskFor), index === 0)) {
286
+ limit = Math.min(255, limit + 2);
287
+ for (const { arc } of ring) {
288
+ limits.set(arc.key, Math.max(limits.get(arc.key) ?? 0, limit));
289
+ masks.delete(arc.key);
290
+ }
291
+ }
292
+ });
293
+ }
294
+ }
295
+ const fills = project(fitted, {
296
+ type: "MultiPolygon",
297
+ coordinates: kept.flatMap((rings) => {
298
+ const [exterior, ...holes] = rings.map((ring) => filteredPoints(ring, maskFor));
299
+ return exterior.length < 4 ? [] : [[exterior, ...holes.filter((hole) => hole.length >= 4)]];
300
+ }),
301
+ }, true);
302
+ const lineArcs = [...usage.values()].filter(({ count }) => borders === "all" || count === 1).map(({ arc }) => arc);
303
+ const lines = [];
304
+ for (const chain of chainArcs(lineArcs)) {
305
+ lines.push(...project(fitted, { type: "LineString", coordinates: filteredPoints(chain.refs, maskFor) }, false));
306
+ }
307
+ const sum = (contours) => contours.reduce((total, c) => total + c.length, 0);
308
+ const bytes = [...fills, ...lines].reduce((total, c) => total + c.points.byteLength + 32, 0);
309
+ return {
310
+ width: fitted.width,
311
+ height: fitted.height,
312
+ fills,
313
+ lines,
314
+ fillLength: sum(fills),
315
+ lineLength: sum(lines),
316
+ bytes,
317
+ };
318
+ }
319
+ class ByteCache {
320
+ _budget;
321
+ _entries = new Map();
322
+ _bytes = 0;
323
+ constructor(_budget) {
324
+ this._budget = _budget;
325
+ }
326
+ get(key) {
327
+ const value = this._entries.get(key);
328
+ if (value) {
329
+ this._entries.delete(key);
330
+ this._entries.set(key, value);
331
+ }
332
+ return value;
333
+ }
334
+ set(key, value) {
335
+ const previous = this._entries.get(key);
336
+ if (previous)
337
+ this._bytes -= previous.bytes;
338
+ this._entries.delete(key);
339
+ this._entries.set(key, value);
340
+ this._bytes += value.bytes;
341
+ for (const [oldest, entry] of this._entries) {
342
+ if (this._bytes <= this._budget || this._entries.size <= 1)
343
+ break;
344
+ this._entries.delete(oldest);
345
+ this._bytes -= entry.bytes;
346
+ }
347
+ }
348
+ clear() {
349
+ this._entries.clear();
350
+ this._bytes = 0;
351
+ }
352
+ }
353
+ const frames = new ByteCache(64);
354
+ const geometries = new ByteCache(64 * 1024 * 1024);
355
+ /**
356
+ * The drawing for a selection, cached. The projection is fitted to the
357
+ * selection at full detail, so a `detail` tween never moves or rescales it.
358
+ */
359
+ export function geoGeometry(request) {
360
+ const detail = Math.round(clamp01(request.detail) * 100) / 100;
361
+ const frameKey = `${request.selectionKey}|${request.projection}`;
362
+ const key = `${frameKey}|${request.borders}|${detail}`;
363
+ const cached = geometries.get(key);
364
+ if (cached)
365
+ return cached;
366
+ let fitted = frames.get(frameKey);
367
+ if (!fitted) {
368
+ const made = fitFrame(request.projection, partsOf(request.features, request.frame));
369
+ if (!made)
370
+ return null;
371
+ fitted = made;
372
+ frames.set(frameKey, { ...made, bytes: 1 });
373
+ }
374
+ const geometry = buildGeometry(request.features, request.frame, fitted, request.borders, detail);
375
+ geometries.set(key, geometry);
376
+ return geometry;
377
+ }
378
+ export function clearGeoGeometry() {
379
+ frames.clear();
380
+ geometries.clear();
381
+ }
382
+ //# sourceMappingURL=geometry.js.map