@motionscript/geo 0.0.0-stage → 0.1.0-alpha.3
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/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/dist/border/format.d.ts +73 -0
- package/dist/border/format.d.ts.map +1 -0
- package/dist/border/format.js +68 -0
- package/dist/border/format.js.map +1 -0
- package/dist/border/geo-border.d.ts +44 -0
- package/dist/border/geo-border.d.ts.map +1 -0
- package/dist/border/geo-border.js +234 -0
- package/dist/border/geo-border.js.map +1 -0
- package/dist/border/geometry.d.ts +40 -0
- package/dist/border/geometry.d.ts.map +1 -0
- package/dist/border/geometry.js +382 -0
- package/dist/border/geometry.js.map +1 -0
- package/dist/border/index.d.ts +15 -0
- package/dist/border/index.d.ts.map +1 -0
- package/dist/border/index.js +15 -0
- package/dist/border/index.js.map +1 -0
- package/dist/border/loader.d.ts +31 -0
- package/dist/border/loader.d.ts.map +1 -0
- package/dist/border/loader.js +87 -0
- package/dist/border/loader.js.map +1 -0
- package/dist/border/registry.d.ts +64 -0
- package/dist/border/registry.d.ts.map +1 -0
- package/dist/border/registry.js +152 -0
- package/dist/border/registry.js.map +1 -0
- package/dist/border/selection.d.ts +21 -0
- package/dist/border/selection.d.ts.map +1 -0
- package/dist/border/selection.js +86 -0
- package/dist/border/selection.js.map +1 -0
- package/dist/border/simplify.d.ts +12 -0
- package/dist/border/simplify.d.ts.map +1 -0
- package/dist/border/simplify.js +116 -0
- package/dist/border/simplify.js.map +1 -0
- package/dist/browser/index.js +25 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +11 -0
- package/dist/globe/atmosphere.d.ts +38 -0
- package/dist/globe/atmosphere.d.ts.map +1 -0
- package/dist/globe/atmosphere.js +91 -0
- package/dist/globe/atmosphere.js.map +1 -0
- package/dist/globe/borders.d.ts +39 -0
- package/dist/globe/borders.d.ts.map +1 -0
- package/dist/globe/borders.js +116 -0
- package/dist/globe/borders.js.map +1 -0
- package/dist/globe/data/ne-110m.d.ts +25 -0
- package/dist/globe/data/ne-110m.d.ts.map +1 -0
- package/dist/globe/data/ne-110m.js +198 -0
- package/dist/globe/data/ne-110m.js.map +1 -0
- package/dist/globe/globe-places.d.ts +140 -0
- package/dist/globe/globe-places.d.ts.map +1 -0
- package/dist/globe/globe-places.js +262 -0
- package/dist/globe/globe-places.js.map +1 -0
- package/dist/globe/globe.d.ts +193 -0
- package/dist/globe/globe.d.ts.map +1 -0
- package/dist/globe/globe.js +435 -0
- package/dist/globe/globe.js.map +1 -0
- package/dist/globe/index.d.ts +17 -0
- package/dist/globe/index.d.ts.map +1 -0
- package/dist/globe/index.js +17 -0
- package/dist/globe/index.js.map +1 -0
- package/dist/globe/places.d.ts +69 -0
- package/dist/globe/places.d.ts.map +1 -0
- package/dist/globe/places.js +94 -0
- package/dist/globe/places.js.map +1 -0
- package/dist/globe/projection.d.ts +182 -0
- package/dist/globe/projection.d.ts.map +1 -0
- package/dist/globe/projection.js +215 -0
- package/dist/globe/projection.js.map +1 -0
- package/dist/globe/world-map.d.ts +83 -0
- package/dist/globe/world-map.d.ts.map +1 -0
- package/dist/globe/world-map.js +169 -0
- package/dist/globe/world-map.js.map +1 -0
- package/dist/globe/world.d.ts +46 -0
- package/dist/globe/world.d.ts.map +1 -0
- package/dist/globe/world.js +71 -0
- package/dist/globe/world.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +31 -0
- package/src/border/format.ts +147 -0
- package/src/border/geo-border.ts +260 -0
- package/src/border/geometry.ts +463 -0
- package/src/border/index.ts +14 -0
- package/src/border/loader.ts +97 -0
- package/src/border/registry.ts +191 -0
- package/src/border/selection.ts +95 -0
- package/src/border/simplify.ts +110 -0
- package/src/globe/atmosphere.ts +98 -0
- package/src/globe/borders.ts +166 -0
- package/src/globe/data/ne-110m.ts +214 -0
- package/src/globe/globe-places.ts +352 -0
- package/src/globe/globe.ts +552 -0
- package/src/globe/index.ts +16 -0
- package/src/globe/places.ts +132 -0
- package/src/globe/projection.ts +261 -0
- package/src/globe/world-map.ts +227 -0
- package/src/globe/world.ts +111 -0
- package/src/index.ts +5 -0
- package/src/nodes.ts +19 -0
- package/README.md +0 -4
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { geoArea } from "d3-geo"
|
|
2
|
+
|
|
3
|
+
/** The on-disk shape `@motionscript/geo-data` writes; see its `generate.mjs`. */
|
|
4
|
+
export const GEO_DATA_FORMAT = 1
|
|
5
|
+
|
|
6
|
+
export interface GeoFileData {
|
|
7
|
+
format: number
|
|
8
|
+
source?: string
|
|
9
|
+
transform: { scale: [number, number]; translate: [number, number] }
|
|
10
|
+
/** Global arc id per local arc — identical across files for a shared border. */
|
|
11
|
+
arcIds: number[]
|
|
12
|
+
/** Per arc, flat `[x0, y0, w0, dx, dy, w, …]`: delta-encoded quantized lon/lat and a weight bucket. */
|
|
13
|
+
arcs: number[][]
|
|
14
|
+
features: GeoFeatureData[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GeoFeatureData {
|
|
18
|
+
code: string
|
|
19
|
+
name: string
|
|
20
|
+
nameNl?: string
|
|
21
|
+
parent: string | null
|
|
22
|
+
first: boolean
|
|
23
|
+
listed: boolean
|
|
24
|
+
remote?: boolean
|
|
25
|
+
area: number
|
|
26
|
+
mainland: number[] | null
|
|
27
|
+
areas: number[]
|
|
28
|
+
/** Polygons → rings → arc refs (`~i` is arc `i` reversed). */
|
|
29
|
+
polygons: number[][][]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `[code, name, nameNl, parent, first]` per listed region. */
|
|
33
|
+
export type GeoIndexEntry = [string, string, string, string | null, 0 | 1]
|
|
34
|
+
|
|
35
|
+
export interface GeoIndex {
|
|
36
|
+
format: number
|
|
37
|
+
source?: string
|
|
38
|
+
regions: GeoIndexEntry[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GeoArc {
|
|
42
|
+
/** Identity for de-duplicating a border drawn by two regions, across files. */
|
|
43
|
+
key: number | string
|
|
44
|
+
/** Interleaved lon/lat, in degrees. */
|
|
45
|
+
points: Float64Array
|
|
46
|
+
/** Per point: 0 always survives, higher buckets are less significant; see `thresholdBucket`. */
|
|
47
|
+
buckets: Uint8Array
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GeoRingRef {
|
|
51
|
+
arc: GeoArc
|
|
52
|
+
reversed: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GeoPolygon {
|
|
56
|
+
rings: GeoRingRef[][]
|
|
57
|
+
/** Per ring, in steradians — exterior first, then holes. */
|
|
58
|
+
ringAreas: number[]
|
|
59
|
+
area: number
|
|
60
|
+
mainland: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GeoFeature {
|
|
64
|
+
code: string
|
|
65
|
+
name: string
|
|
66
|
+
nameNl: string
|
|
67
|
+
parent: string | null
|
|
68
|
+
first: boolean
|
|
69
|
+
listed: boolean
|
|
70
|
+
remote: boolean
|
|
71
|
+
polygons: GeoPolygon[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DecodedGeoFile {
|
|
75
|
+
features: Map<string, GeoFeature>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decodeArc(encoded: number[], key: number, [kx, ky]: [number, number], [tx, ty]: [number, number]): GeoArc {
|
|
79
|
+
const count = encoded.length / 3
|
|
80
|
+
const points = new Float64Array(count * 2)
|
|
81
|
+
const buckets = new Uint8Array(count)
|
|
82
|
+
let x = 0
|
|
83
|
+
let y = 0
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
x += encoded[i * 3]
|
|
86
|
+
y += encoded[i * 3 + 1]
|
|
87
|
+
points[i * 2] = x * kx + tx
|
|
88
|
+
points[i * 2 + 1] = y * ky + ty
|
|
89
|
+
buckets[i] = encoded[i * 3 + 2]
|
|
90
|
+
}
|
|
91
|
+
return { key, points, buckets }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function ringCoordinates(ring: readonly GeoRingRef[]): [number, number][] {
|
|
95
|
+
const out: [number, number][] = []
|
|
96
|
+
for (const { arc, reversed } of ring) {
|
|
97
|
+
const n = arc.buckets.length
|
|
98
|
+
for (let j = out.length === 0 ? 0 : 1; j < n; j++) {
|
|
99
|
+
const i = reversed ? n - 1 - j : j
|
|
100
|
+
out.push([arc.points[i * 2], arc.points[i * 2 + 1]])
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A ring's area in steradians, whichever way it winds. */
|
|
107
|
+
export function ringArea(ring: readonly GeoRingRef[]): number {
|
|
108
|
+
const coordinates = ringCoordinates(ring)
|
|
109
|
+
if (coordinates.length < 4) return 0
|
|
110
|
+
const area = geoArea({ type: "Polygon", coordinates: [coordinates] })
|
|
111
|
+
return area > 2 * Math.PI ? 4 * Math.PI - area : area
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function decodeGeoFile(data: GeoFileData): DecodedGeoFile {
|
|
115
|
+
if (data.format !== GEO_DATA_FORMAT) {
|
|
116
|
+
throw new Error(`geo data format ${data.format} is not supported (expected ${GEO_DATA_FORMAT})`)
|
|
117
|
+
}
|
|
118
|
+
const arcs = data.arcs.map((encoded, i) =>
|
|
119
|
+
decodeArc(encoded, data.arcIds[i], data.transform.scale, data.transform.translate)
|
|
120
|
+
)
|
|
121
|
+
const ref = (index: number): GeoRingRef =>
|
|
122
|
+
index < 0 ? { arc: arcs[~index], reversed: true } : { arc: arcs[index], reversed: false }
|
|
123
|
+
|
|
124
|
+
const features = new Map<string, GeoFeature>()
|
|
125
|
+
for (const f of data.features) {
|
|
126
|
+
const mainland = f.mainland ? new Set(f.mainland) : null
|
|
127
|
+
features.set(f.code, {
|
|
128
|
+
code: f.code,
|
|
129
|
+
name: f.name,
|
|
130
|
+
nameNl: f.nameNl ?? f.name,
|
|
131
|
+
parent: f.parent,
|
|
132
|
+
first: f.first,
|
|
133
|
+
listed: f.listed,
|
|
134
|
+
remote: f.remote === true,
|
|
135
|
+
polygons: f.polygons.map((rings, i) => {
|
|
136
|
+
const resolved = rings.map((ring) => ring.map(ref))
|
|
137
|
+
return {
|
|
138
|
+
rings: resolved,
|
|
139
|
+
ringAreas: resolved.map((ring, r) => (r === 0 ? f.areas[i] : ringArea(ring))),
|
|
140
|
+
area: f.areas[i],
|
|
141
|
+
mainland: mainland === null || mainland.has(i),
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return { features }
|
|
147
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Clip,
|
|
3
|
+
Graphics2D,
|
|
4
|
+
ShapeNode,
|
|
5
|
+
node,
|
|
6
|
+
property,
|
|
7
|
+
type AssetScope,
|
|
8
|
+
type FillResolved,
|
|
9
|
+
type NodeConfig,
|
|
10
|
+
type PathBounds,
|
|
11
|
+
type RenderContext2D,
|
|
12
|
+
type ShadowResolved,
|
|
13
|
+
type ShapeProps,
|
|
14
|
+
type StrokeResolved,
|
|
15
|
+
} from "@motionscript/sdk"
|
|
16
|
+
import { snapValue } from "@motionscript/sdk/component"
|
|
17
|
+
|
|
18
|
+
import { geoGeometry, type GeoBorders, type GeoContour, type GeoGeometry, type GeoProjectionKind } from "./geometry"
|
|
19
|
+
import { geoDataVersion, geoLoader } from "./loader"
|
|
20
|
+
import { geoRegistry } from "./registry"
|
|
21
|
+
import { geoFilesFor, normalizeIso, selectGeoFeatures, type GeoFrame, type GeoLevel } from "./selection"
|
|
22
|
+
|
|
23
|
+
export interface GeoBorderProps extends ShapeProps {
|
|
24
|
+
/** ISO 3166-1 alpha-2 (`"FR"`) or ISO 3166-2 (`"US-CA"`) codes. */
|
|
25
|
+
iso: string | readonly string[]
|
|
26
|
+
level: GeoLevel
|
|
27
|
+
/** 0–1: 1 is full resolution, lower is a more stylised outline. */
|
|
28
|
+
detail: number
|
|
29
|
+
projection: GeoProjectionKind
|
|
30
|
+
frame: GeoFrame
|
|
31
|
+
borders: GeoBorders
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Chunk {
|
|
35
|
+
data: string
|
|
36
|
+
/** Where this chunk's contours sit along the whole drawing, as fractions of its length. */
|
|
37
|
+
from: number
|
|
38
|
+
to: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Drawn {
|
|
42
|
+
geometry: GeoGeometry
|
|
43
|
+
width: number
|
|
44
|
+
height: number
|
|
45
|
+
fills: Chunk[]
|
|
46
|
+
lines: Chunk[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Skia's cost for one path grows faster than its contour count, and a shape
|
|
50
|
+
// drawn as several paths under one paint is unioned every frame — so a region
|
|
51
|
+
// is cut into paths of bounded size, each painted on its own.
|
|
52
|
+
const CONTOURS_PER_PATH = 256
|
|
53
|
+
const POINTS_PER_PATH = 40_000
|
|
54
|
+
|
|
55
|
+
const coordinate = (value: number) => String(Math.round(value * 100) / 100)
|
|
56
|
+
|
|
57
|
+
function chunk(contours: readonly GeoContour[], total: number, scale: number): Chunk[] {
|
|
58
|
+
const chunks: Chunk[] = []
|
|
59
|
+
let parts: string[] = []
|
|
60
|
+
let count = 0
|
|
61
|
+
let points = 0
|
|
62
|
+
let from = 0
|
|
63
|
+
let length = 0
|
|
64
|
+
const flush = () => {
|
|
65
|
+
if (count === 0) return
|
|
66
|
+
const to = total > 0 ? Math.min(1, length / total) : 1
|
|
67
|
+
chunks.push({ data: parts.join(""), from, to })
|
|
68
|
+
from = to
|
|
69
|
+
parts = []
|
|
70
|
+
count = 0
|
|
71
|
+
points = 0
|
|
72
|
+
}
|
|
73
|
+
for (const contour of contours) {
|
|
74
|
+
const p = contour.points
|
|
75
|
+
// Path data is y-down; the unit geometry is y-up.
|
|
76
|
+
for (let i = 0; i < p.length; i += 2) {
|
|
77
|
+
parts.push(i === 0 ? "M" : "L", coordinate(p[i] * scale), " ", coordinate(-p[i + 1] * scale))
|
|
78
|
+
}
|
|
79
|
+
if (contour.closed) parts.push("Z")
|
|
80
|
+
count++
|
|
81
|
+
points += p.length / 2
|
|
82
|
+
length += contour.length
|
|
83
|
+
if (count >= CONTOURS_PER_PATH || points >= POINTS_PER_PATH) flush()
|
|
84
|
+
}
|
|
85
|
+
flush()
|
|
86
|
+
return chunks
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Country and subdivision outlines by ISO 3166 code, fitted into the node's box.
|
|
91
|
+
*
|
|
92
|
+
* <GeoBorder iso={["FR", "DE"]} detail={0.4} stroke={{ weight: 2, fill: "white" }} />
|
|
93
|
+
*
|
|
94
|
+
* Geometry comes from whatever `geoRegistry` has been given — a URL, a loader,
|
|
95
|
+
* or shapes defined by hand — and loads as a declared asset, so a frame never
|
|
96
|
+
* draws half a country. Fill and stroke are separate geometry: the fill is each
|
|
97
|
+
* region's polygons, the stroke is every border once, which is why a border two
|
|
98
|
+
* selected regions share is not drawn twice and why `stroke.align` does not
|
|
99
|
+
* apply.
|
|
100
|
+
*/
|
|
101
|
+
@node({
|
|
102
|
+
key: "geoBorder",
|
|
103
|
+
parentKey: "shape",
|
|
104
|
+
forkable: true,
|
|
105
|
+
layout: {
|
|
106
|
+
children: "freeform",
|
|
107
|
+
acceptsChildren: true,
|
|
108
|
+
defaultWidthMode: "fixed",
|
|
109
|
+
defaultHeightMode: "fixed",
|
|
110
|
+
},
|
|
111
|
+
seed: { width: 480, height: 480 },
|
|
112
|
+
})
|
|
113
|
+
export class GeoBorder extends ShapeNode<GeoBorderProps> {
|
|
114
|
+
@property({ default: ["FR"], mapper: normalizeIso, tween: snapValue })
|
|
115
|
+
declare iso: string | readonly string[]
|
|
116
|
+
|
|
117
|
+
@property({ default: "country", tween: snapValue, options: ["country", "state"] })
|
|
118
|
+
declare level: GeoLevel
|
|
119
|
+
|
|
120
|
+
@property({ default: 0.5, min: 0, max: 1, step: 0.01 })
|
|
121
|
+
declare detail: number
|
|
122
|
+
|
|
123
|
+
@property({
|
|
124
|
+
default: "conic",
|
|
125
|
+
tween: snapValue,
|
|
126
|
+
options: ["conic", "mercator", "equirectangular", "orthographic"],
|
|
127
|
+
})
|
|
128
|
+
declare projection: GeoProjectionKind
|
|
129
|
+
|
|
130
|
+
@property({ default: "mainland", tween: snapValue, options: ["mainland", "all"] })
|
|
131
|
+
declare frame: GeoFrame
|
|
132
|
+
|
|
133
|
+
@property({ default: "all", tween: snapValue, options: ["all", "outline"] })
|
|
134
|
+
declare borders: GeoBorders
|
|
135
|
+
|
|
136
|
+
constructor(props?: NodeConfig<GeoBorder, GeoBorderProps>) {
|
|
137
|
+
super(props as NodeConfig<ShapeNode<GeoBorderProps>, GeoBorderProps>)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override declareAssets(assets: AssetScope): void {
|
|
141
|
+
super.declareAssets(assets)
|
|
142
|
+
for (const file of geoFilesFor(this.iso as string[], this.level)) {
|
|
143
|
+
assets.loader(`geo:${file}`, geoLoader(file), { gate: "render" })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected override renderSelf(ctx: RenderContext2D): void {
|
|
148
|
+
const drawn = this._drawn()
|
|
149
|
+
const fill = this.fill as FillResolved[]
|
|
150
|
+
if (!drawn || drawn.fills.length === 0) return
|
|
151
|
+
const shadow = this.shadow as ShadowResolved[]
|
|
152
|
+
const g = new Graphics2D()
|
|
153
|
+
for (const chunk of drawn.fills) {
|
|
154
|
+
if (!this._pathOp(g, chunk)) continue
|
|
155
|
+
if (shadow.length > 0) g.shadow(shadow)
|
|
156
|
+
g.fill(fill)
|
|
157
|
+
}
|
|
158
|
+
ctx.draw(g)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected override renderOverlay(ctx: RenderContext2D): void {
|
|
162
|
+
const overlay = this.overlay as FillResolved[]
|
|
163
|
+
if (overlay.length === 0) return
|
|
164
|
+
const drawn = this._drawn()
|
|
165
|
+
if (!drawn) return
|
|
166
|
+
const g = new Graphics2D()
|
|
167
|
+
for (const chunk of drawn.fills) if (this._pathOp(g, chunk)) g.fill(overlay)
|
|
168
|
+
ctx.draw(g)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected override renderStroke(ctx: RenderContext2D): void {
|
|
172
|
+
const stroke = this.stroke as StrokeResolved[]
|
|
173
|
+
if (stroke.length === 0) return
|
|
174
|
+
const drawn = this._drawn()
|
|
175
|
+
if (!drawn || drawn.lines.length === 0) return
|
|
176
|
+
const centred = stroke.map((layer) => (layer.align === 0 ? layer : { ...layer, align: 0 }))
|
|
177
|
+
const shadow = this.shadow as ShadowResolved[]
|
|
178
|
+
const castsShadow = shadow.length > 0 && !(this.fill as FillResolved[]).some((layer) => (layer.opacity ?? 1) > 0)
|
|
179
|
+
const g = new Graphics2D()
|
|
180
|
+
for (const chunk of drawn.lines) {
|
|
181
|
+
if (!this._pathOp(g, chunk)) continue
|
|
182
|
+
if (castsShadow) g.shadow(shadow)
|
|
183
|
+
g.stroke(centred)
|
|
184
|
+
}
|
|
185
|
+
ctx.draw(g)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The box, not the outline: a path clip has no winding test, and a country's
|
|
189
|
+
// bounding box is what the selection shows.
|
|
190
|
+
protected override clipSelf(): Clip {
|
|
191
|
+
return new Clip().rect({
|
|
192
|
+
x: 0,
|
|
193
|
+
y: 0,
|
|
194
|
+
width: this.layoutBounds.width,
|
|
195
|
+
height: this.layoutBounds.height,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private _drawnCache: Drawn | null = null
|
|
200
|
+
private readonly _warned = new Set<string>()
|
|
201
|
+
|
|
202
|
+
private _pathOp(g: Graphics2D, chunk: Chunk): boolean {
|
|
203
|
+
const start = this.start
|
|
204
|
+
const end = this.end
|
|
205
|
+
const width = this.layoutBounds.width
|
|
206
|
+
const height = this.layoutBounds.height
|
|
207
|
+
const centerBounds: PathBounds = [-width / 2, -height / 2, width / 2, height / 2]
|
|
208
|
+
if (start <= 0 && end >= 1) {
|
|
209
|
+
g.path({ data: chunk.data, centerBounds })
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
const span = chunk.to - chunk.from
|
|
213
|
+
if (span <= 0) return false
|
|
214
|
+
const localStart = Math.max(0, (start - chunk.from) / span)
|
|
215
|
+
const localEnd = Math.min(1, (end - chunk.from) / span)
|
|
216
|
+
if (localEnd <= localStart) return false
|
|
217
|
+
g.path({ data: chunk.data, centerBounds, start: localStart, end: localEnd })
|
|
218
|
+
return true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private _drawn(): Drawn | null {
|
|
222
|
+
const codes = this.iso as string[]
|
|
223
|
+
const level = this.level
|
|
224
|
+
const frame = this.frame
|
|
225
|
+
const selection = selectGeoFeatures(codes, level, frame)
|
|
226
|
+
for (const code of selection.unknown) {
|
|
227
|
+
if (this._warned.has(code)) continue
|
|
228
|
+
this._warned.add(code)
|
|
229
|
+
console.warn(`[GeoBorder] no region with code ${code}`)
|
|
230
|
+
}
|
|
231
|
+
if (selection.features.length === 0) return null
|
|
232
|
+
|
|
233
|
+
const geometry = geoGeometry({
|
|
234
|
+
selectionKey: `${geoRegistry.generation}:${geoDataVersion()}:${level}:${frame}:${codes.join(",")}`,
|
|
235
|
+
features: selection.features,
|
|
236
|
+
frame,
|
|
237
|
+
projection: this.projection,
|
|
238
|
+
borders: this.borders,
|
|
239
|
+
detail: this.detail,
|
|
240
|
+
})
|
|
241
|
+
if (!geometry) return null
|
|
242
|
+
|
|
243
|
+
const width = this.layoutBounds.width
|
|
244
|
+
const height = this.layoutBounds.height
|
|
245
|
+
const cached = this._drawnCache
|
|
246
|
+
if (cached && cached.geometry === geometry && cached.width === width && cached.height === height) {
|
|
247
|
+
return cached
|
|
248
|
+
}
|
|
249
|
+
const scale = Math.min(width / geometry.width, height / geometry.height)
|
|
250
|
+
const drawn: Drawn = {
|
|
251
|
+
geometry,
|
|
252
|
+
width,
|
|
253
|
+
height,
|
|
254
|
+
fills: chunk(geometry.fills, geometry.fillLength, scale),
|
|
255
|
+
lines: chunk(geometry.lines, geometry.lineLength, scale),
|
|
256
|
+
}
|
|
257
|
+
this._drawnCache = drawn
|
|
258
|
+
return drawn
|
|
259
|
+
}
|
|
260
|
+
}
|