@mailwoman/map-tui 9.1.1 → 9.2.0
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 +59 -0
- package/browser.ts +536 -0
- package/cli-args.ts +232 -0
- package/cli.ts +143 -0
- package/frame.ts +30 -1
- package/input.ts +288 -0
- package/out/browser.d.ts +126 -0
- package/out/browser.d.ts.map +1 -0
- package/out/browser.js +386 -0
- package/out/browser.js.map +1 -0
- package/out/cli-args.d.ts +52 -0
- package/out/cli-args.d.ts.map +1 -0
- package/out/cli-args.js +168 -0
- package/out/cli-args.js.map +1 -0
- package/out/cli.d.ts +8 -0
- package/out/cli.d.ts.map +1 -0
- package/out/cli.js +122 -0
- package/out/cli.js.map +1 -0
- package/out/frame.d.ts +18 -0
- package/out/frame.d.ts.map +1 -1
- package/out/frame.js +27 -1
- package/out/frame.js.map +1 -1
- package/out/input.d.ts +93 -0
- package/out/input.d.ts.map +1 -0
- package/out/input.js +214 -0
- package/out/input.js.map +1 -0
- package/out/renderer.d.ts +9 -2
- package/out/renderer.d.ts.map +1 -1
- package/out/renderer.js +42 -15
- package/out/renderer.js.map +1 -1
- package/out/style.d.ts +19 -8
- package/out/style.d.ts.map +1 -1
- package/out/style.js +47 -2
- package/out/style.js.map +1 -1
- package/out/tile-source.d.ts +23 -2
- package/out/tile-source.d.ts.map +1 -1
- package/out/tile-source.js +44 -11
- package/out/tile-source.js.map +1 -1
- package/out/tsconfig.test.tsbuildinfo +1 -1
- package/out/tsconfig.tsbuildinfo +1 -1
- package/package.json +67 -2
- package/renderer.ts +76 -24
- package/style.ts +63 -10
- package/tile-source.ts +71 -16
package/renderer.ts
CHANGED
|
@@ -19,8 +19,8 @@ import { type MapFrame, overlayText, rasterizeToFrame, rgbToPacked } from "./fra
|
|
|
19
19
|
import { lonLatToWorldPx, metersPerPixel, TILE_SIZE } from "./mercator.ts"
|
|
20
20
|
import type { DecodedFeature } from "./mvt.ts"
|
|
21
21
|
import { drawCircle, drawPolyline, fillPolygon, RGBAGrid } from "./raster.ts"
|
|
22
|
-
import { type LayerStyle, type RGB, stylesFor } from "./style.ts"
|
|
23
|
-
import type { DecodedTile,
|
|
22
|
+
import { type LayerStyle, type RGB, styleForFeatureKind, stylesFor } from "./style.ts"
|
|
23
|
+
import type { DecodedTile, TileProvider } from "./tile-source.ts"
|
|
24
24
|
|
|
25
25
|
export interface Viewport {
|
|
26
26
|
centerLon: number
|
|
@@ -84,22 +84,39 @@ interface GridOrigin {
|
|
|
84
84
|
* — that's what keeps `rasterizeFeature` under `max-params` (8) once `style`, `renderZoom`, and `pendingLabels` join
|
|
85
85
|
* it. Threaded through as a value rather than closed over so those rasterizers stay free functions (no nesting inside
|
|
86
86
|
* `renderFrame` deep enough to trip `max-depth`).
|
|
87
|
+
*
|
|
88
|
+
* `worldX`/`worldY` are the tile's top-left corner in render-zoom world pixels — for a native tile that is `tileX *
|
|
89
|
+
* TILE_SIZE`, for an overzoomed parent it is scaled by the tile's span, so the projection needs no zoom arithmetic of
|
|
90
|
+
* its own.
|
|
87
91
|
*/
|
|
88
92
|
interface TileProjection {
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
worldX: number
|
|
94
|
+
worldY: number
|
|
91
95
|
scale: number
|
|
92
96
|
origin: GridOrigin
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A tile chosen for a viewport slot: the native tile when the archive has one, otherwise the nearest ancestor that
|
|
101
|
+
* exists. `span` is how many render-zoom world pixels the tile covers (`TILE_SIZE << dz` for an ancestor `dz` levels
|
|
102
|
+
* up) — a spatially sparse archive (deep zooms only where people are) degrades to coarse geometry instead of blank
|
|
103
|
+
* cells.
|
|
104
|
+
*/
|
|
105
|
+
interface ResolvedTile {
|
|
106
|
+
tile: DecodedTile
|
|
107
|
+
tileX: number
|
|
108
|
+
tileY: number
|
|
109
|
+
span: number
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
function clamp(value: number, min: number, max: number): number {
|
|
96
113
|
return Math.min(Math.max(value, min), max)
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
function projectPoint(projection: TileProjection, gx: number, gy: number): ProjectedPoint {
|
|
100
117
|
return {
|
|
101
|
-
x: projection.
|
|
102
|
-
y: projection.
|
|
118
|
+
x: projection.worldX + gx * projection.scale - projection.origin.x,
|
|
119
|
+
y: projection.worldY + gy * projection.scale - projection.origin.y,
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
122
|
|
|
@@ -178,23 +195,28 @@ function rasterizeFeature(
|
|
|
178
195
|
*/
|
|
179
196
|
function rasterizeTileForKind(
|
|
180
197
|
grid: RGBAGrid,
|
|
181
|
-
|
|
182
|
-
tileX: number,
|
|
183
|
-
tileY: number,
|
|
198
|
+
resolved: ResolvedTile,
|
|
184
199
|
kind: LayerStyle["kind"],
|
|
185
200
|
renderZoom: number,
|
|
186
201
|
origin: GridOrigin,
|
|
187
202
|
pendingLabels: PendingLabel[]
|
|
188
203
|
): void {
|
|
189
|
-
for (const layer of tile.layers) {
|
|
204
|
+
for (const layer of resolved.tile.layers) {
|
|
190
205
|
const styles = stylesFor(layer.name, renderZoom).filter((style) => style.kind === kind)
|
|
191
206
|
|
|
192
207
|
if (!styles.length) continue
|
|
193
208
|
|
|
194
|
-
const projection: TileProjection = {
|
|
209
|
+
const projection: TileProjection = {
|
|
210
|
+
worldX: resolved.tileX * resolved.span,
|
|
211
|
+
worldY: resolved.tileY * resolved.span,
|
|
212
|
+
scale: resolved.span / layer.extent,
|
|
213
|
+
origin,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const feature of layer.features) {
|
|
217
|
+
const style = styleForFeatureKind(styles, feature.properties.kind)
|
|
195
218
|
|
|
196
|
-
|
|
197
|
-
for (const feature of layer.features) {
|
|
219
|
+
if (style) {
|
|
198
220
|
rasterizeFeature(grid, feature, style, renderZoom, projection, pendingLabels)
|
|
199
221
|
}
|
|
200
222
|
}
|
|
@@ -207,12 +229,48 @@ function rasterizeTileForKind(
|
|
|
207
229
|
* `TileSource` — it holds no per-frame state itself.
|
|
208
230
|
*/
|
|
209
231
|
export class MapRenderer {
|
|
210
|
-
private readonly source:
|
|
232
|
+
private readonly source: TileProvider
|
|
211
233
|
|
|
212
|
-
constructor(source:
|
|
234
|
+
constructor(source: TileProvider) {
|
|
213
235
|
this.source = source
|
|
214
236
|
}
|
|
215
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Resolves each viewport slot to its native tile or, when the archive has none — a spatially sparse deep band, a
|
|
240
|
+
* zoom-capped extract — the nearest existing ancestor. Ancestors shared by several absent slots are deduplicated so
|
|
241
|
+
* their geometry rasterizes once, and coarse tiles sort first so native detail paints over the fallback wherever both
|
|
242
|
+
* cover a cell.
|
|
243
|
+
*/
|
|
244
|
+
private async resolveTiles(
|
|
245
|
+
tileCoords: ReadonlyArray<{ x: number; y: number }>,
|
|
246
|
+
renderZoom: number
|
|
247
|
+
): Promise<ResolvedTile[]> {
|
|
248
|
+
const resolved = new Map<string, ResolvedTile>()
|
|
249
|
+
|
|
250
|
+
await Promise.all(
|
|
251
|
+
tileCoords.map(async ({ x, y }) => {
|
|
252
|
+
for (let zoom = renderZoom; zoom >= this.source.minZoom; zoom--) {
|
|
253
|
+
const shift = renderZoom - zoom
|
|
254
|
+
const tileX = x >> shift
|
|
255
|
+
const tileY = y >> shift
|
|
256
|
+
const key = `${zoom}/${tileX}/${tileY}`
|
|
257
|
+
|
|
258
|
+
if (resolved.has(key)) return
|
|
259
|
+
|
|
260
|
+
const tile = await this.source.getTile(zoom, tileX, tileY)
|
|
261
|
+
|
|
262
|
+
if (tile) {
|
|
263
|
+
resolved.set(key, { tile, tileX, tileY, span: TILE_SIZE * 2 ** shift })
|
|
264
|
+
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return [...resolved.values()].toSorted((a, b) => b.span - a.span)
|
|
272
|
+
}
|
|
273
|
+
|
|
216
274
|
async renderFrame(viewport: Viewport, overlays?: { markers?: MarkerSpec[]; ring?: RingSpec }): Promise<MapFrame> {
|
|
217
275
|
const { centerLon, centerLat, columns, rows } = viewport
|
|
218
276
|
|
|
@@ -237,20 +295,14 @@ export class MapRenderer {
|
|
|
237
295
|
}
|
|
238
296
|
}
|
|
239
297
|
|
|
240
|
-
const
|
|
298
|
+
const resolvedTiles = await this.resolveTiles(tileCoords, renderZoom)
|
|
241
299
|
|
|
242
300
|
const grid = new RGBAGrid(subpixelW, subpixelH)
|
|
243
301
|
const pendingLabels: PendingLabel[] = []
|
|
244
302
|
|
|
245
303
|
for (const kind of ["fill", "line", "label"] as const) {
|
|
246
|
-
for (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!tile) continue
|
|
250
|
-
|
|
251
|
-
const { x: tileX, y: tileY } = tileCoords[i]!
|
|
252
|
-
|
|
253
|
-
rasterizeTileForKind(grid, tile, tileX, tileY, kind, renderZoom, origin, pendingLabels)
|
|
304
|
+
for (const resolved of resolvedTiles) {
|
|
305
|
+
rasterizeTileForKind(grid, resolved, kind, renderZoom, origin, pendingLabels)
|
|
254
306
|
}
|
|
255
307
|
}
|
|
256
308
|
|
package/style.ts
CHANGED
|
@@ -14,23 +14,28 @@
|
|
|
14
14
|
|
|
15
15
|
export type RGB = readonly [red: number, green: number, blue: number]
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Fields shared by every style entry. `featureKinds` scopes an entry to features whose `kind` tile attribute is in the
|
|
19
|
+
* list; an entry without it is the layer's catch-all. Entries are consulted in table order and the first match wins, so
|
|
20
|
+
* kind-scoped entries precede the catch-all.
|
|
21
|
+
*/
|
|
22
|
+
interface StyleBase {
|
|
19
23
|
color: RGB
|
|
20
24
|
minZoom: number
|
|
25
|
+
featureKinds?: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FillStyle extends StyleBase {
|
|
29
|
+
kind: "fill"
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export interface LineStyle {
|
|
32
|
+
export interface LineStyle extends StyleBase {
|
|
24
33
|
kind: "line"
|
|
25
|
-
color: RGB
|
|
26
|
-
minZoom: number
|
|
27
34
|
width: (zoom: number) => number
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export interface LabelStyle {
|
|
37
|
+
export interface LabelStyle extends StyleBase {
|
|
31
38
|
kind: "label"
|
|
32
|
-
color: RGB
|
|
33
|
-
minZoom: number
|
|
34
39
|
property: string
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -39,15 +44,48 @@ export type LayerStyle = FillStyle | LineStyle | LabelStyle
|
|
|
39
44
|
// Zoom level at which roads render with 2px width instead of 1px.
|
|
40
45
|
const ROAD_WIDTH_THRESHOLD = 14
|
|
41
46
|
|
|
47
|
+
const roadWidth = (zoom: number) => (zoom >= ROAD_WIDTH_THRESHOLD ? 2 : 1)
|
|
48
|
+
|
|
49
|
+
// At braille scale every road is one dot wide and every fill is stipple, so CLASS has to ride on color instead of
|
|
50
|
+
// geometry: arteries brighten toward amber, paths dim toward the vegetation green, and urban landuse runs warmer and
|
|
51
|
+
// brighter than vegetation so a city reads as denser texture. Luminance is load-bearing — asciify's ordered dither
|
|
52
|
+
// turns it into stipple density.
|
|
53
|
+
const VEGETATION_KINDS = [
|
|
54
|
+
"allotments",
|
|
55
|
+
"cemetery",
|
|
56
|
+
"farmland",
|
|
57
|
+
"forest",
|
|
58
|
+
"garden",
|
|
59
|
+
"grass",
|
|
60
|
+
"meadow",
|
|
61
|
+
"nature_reserve",
|
|
62
|
+
"park",
|
|
63
|
+
"scrub",
|
|
64
|
+
"wetland",
|
|
65
|
+
"wood",
|
|
66
|
+
] as const
|
|
67
|
+
|
|
42
68
|
const STYLE_TABLE: Record<string, LayerStyle[]> = {
|
|
43
69
|
earth: [{ kind: "fill", color: [40, 44, 36], minZoom: 0 }],
|
|
44
70
|
landcover: [{ kind: "fill", color: [36, 52, 32], minZoom: 4 }],
|
|
45
|
-
landuse: [
|
|
71
|
+
landuse: [
|
|
72
|
+
{ kind: "fill", color: [66, 60, 52], minZoom: 10, featureKinds: ["residential"] },
|
|
73
|
+
{ kind: "fill", color: [72, 62, 50], minZoom: 10, featureKinds: ["commercial"] },
|
|
74
|
+
{ kind: "fill", color: [58, 60, 64], minZoom: 10, featureKinds: ["industrial"] },
|
|
75
|
+
{ kind: "fill", color: [36, 52, 32], minZoom: 10, featureKinds: VEGETATION_KINDS },
|
|
76
|
+
{ kind: "fill", color: [48, 48, 40], minZoom: 10 },
|
|
77
|
+
],
|
|
46
78
|
water: [{ kind: "fill", color: [24, 48, 90], minZoom: 0 }],
|
|
47
79
|
buildings: [{ kind: "fill", color: [70, 66, 60], minZoom: 13 }],
|
|
48
80
|
boundaries: [{ kind: "line", color: [140, 110, 160], minZoom: 0, width: () => 1 }],
|
|
49
81
|
roads: [
|
|
50
|
-
{ kind: "line", color: [
|
|
82
|
+
{ kind: "line", color: [255, 190, 90], minZoom: 6, featureKinds: ["highway"], width: roadWidth },
|
|
83
|
+
{ kind: "line", color: [215, 210, 180], minZoom: 6, featureKinds: ["major_road"], width: roadWidth },
|
|
84
|
+
{ kind: "line", color: [185, 182, 158], minZoom: 6, featureKinds: ["medium_road"], width: roadWidth },
|
|
85
|
+
{ kind: "line", color: [150, 150, 132], minZoom: 6, featureKinds: ["minor_road"], width: roadWidth },
|
|
86
|
+
{ kind: "line", color: [105, 115, 98], minZoom: 6, featureKinds: ["path"], width: roadWidth },
|
|
87
|
+
{ kind: "line", color: [115, 125, 140], minZoom: 6, featureKinds: ["rail"], width: () => 1 },
|
|
88
|
+
{ kind: "line", color: [170, 170, 150], minZoom: 6, width: roadWidth },
|
|
51
89
|
],
|
|
52
90
|
places: [{ kind: "label", color: [235, 235, 220], minZoom: 2, property: "name" }],
|
|
53
91
|
pois: [{ kind: "label", color: [180, 200, 160], minZoom: 14, property: "name" }],
|
|
@@ -60,3 +98,18 @@ const STYLE_TABLE: Record<string, LayerStyle[]> = {
|
|
|
60
98
|
export function stylesFor(layerName: string, zoom: number): LayerStyle[] {
|
|
61
99
|
return (STYLE_TABLE[layerName] ?? []).filter((style) => zoom >= style.minZoom)
|
|
62
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The one style painting a feature: the first entry whose `featureKinds` contains the feature's `kind` attribute, or
|
|
104
|
+
* the first catch-all. A feature is painted at most once per draw pass — kind-scoped entries recolor, they never
|
|
105
|
+
* double-paint.
|
|
106
|
+
*/
|
|
107
|
+
export function styleForFeatureKind(styles: readonly LayerStyle[], featureKind: unknown): LayerStyle | null {
|
|
108
|
+
for (const style of styles) {
|
|
109
|
+
if (style.featureKinds == null) return style
|
|
110
|
+
|
|
111
|
+
if (typeof featureKind === "string" && style.featureKinds.includes(featureKind)) return style
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null
|
|
115
|
+
}
|
package/tile-source.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface DecodedTile {
|
|
|
22
22
|
layers: DecodedLayer[]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* What a renderer needs from a tile archive — the read surface of {@link TileSource}, separated so a renderer can be
|
|
27
|
+
* driven by any provider: a single archive, a stub in tests, or a composite over several archives.
|
|
28
|
+
*/
|
|
29
|
+
export interface TileProvider {
|
|
30
|
+
readonly minZoom: number
|
|
31
|
+
readonly maxZoom: number
|
|
32
|
+
readonly attribution: string
|
|
33
|
+
getTile(z: number, x: number, y: number): Promise<DecodedTile | null>
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
class FilePMTilesSource implements Source {
|
|
26
37
|
private readonly path: string
|
|
27
38
|
private readonly handle: FileHandle
|
|
@@ -45,6 +56,38 @@ class FilePMTilesSource implements Source {
|
|
|
45
56
|
|
|
46
57
|
const TILE_CACHE_LIMIT = 64
|
|
47
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The named entities attribution strings actually carry — archive metadata is HTML (`<a>© OpenStreetMap</a>`), and
|
|
61
|
+
* a terminal shows entities raw unless they decode here.
|
|
62
|
+
*/
|
|
63
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
64
|
+
"&": "&",
|
|
65
|
+
"©": "©",
|
|
66
|
+
">": ">",
|
|
67
|
+
"<": "<",
|
|
68
|
+
""": '"',
|
|
69
|
+
"'": "'",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Plain-text attribution out of archive metadata (HTML tags stripped, entities decoded); empty string when absent.
|
|
74
|
+
*/
|
|
75
|
+
export function readAttribution(metadata: unknown): string {
|
|
76
|
+
if (
|
|
77
|
+
typeof metadata !== "object" ||
|
|
78
|
+
metadata === null ||
|
|
79
|
+
!("attribution" in metadata) ||
|
|
80
|
+
typeof (metadata as { attribution: unknown }).attribution !== "string"
|
|
81
|
+
) {
|
|
82
|
+
return ""
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (metadata as { attribution: string }).attribution
|
|
86
|
+
.replaceAll(/<[^>]+>/gu, "")
|
|
87
|
+
.replaceAll(/&[a-z]+;|&#\d+;/gu, (entity) => HTML_ENTITIES[entity] ?? entity)
|
|
88
|
+
.trim()
|
|
89
|
+
}
|
|
90
|
+
|
|
48
91
|
/**
|
|
49
92
|
* A single LRU cache slot. Wrapping the decoded tile in an object lets `getTile` tell "cached and known absent" (`{
|
|
50
93
|
* tile: null }`) apart from "not yet cached" (no entry in the Map) using plain presence, with no comparison against
|
|
@@ -54,7 +97,7 @@ interface CacheEntry {
|
|
|
54
97
|
tile: DecodedTile | null
|
|
55
98
|
}
|
|
56
99
|
|
|
57
|
-
export class TileSource {
|
|
100
|
+
export class TileSource implements TileProvider {
|
|
58
101
|
readonly minZoom: number
|
|
59
102
|
readonly maxZoom: number
|
|
60
103
|
|
|
@@ -63,11 +106,20 @@ export class TileSource {
|
|
|
63
106
|
*/
|
|
64
107
|
readonly attribution: string
|
|
65
108
|
|
|
66
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Null for HTTP sources — fetch connections have no handle to hold or close.
|
|
111
|
+
*/
|
|
112
|
+
private readonly handle: FileHandle | null
|
|
67
113
|
private readonly pmtiles: PMTiles
|
|
68
114
|
private readonly cache = new Map<string, CacheEntry>()
|
|
69
115
|
|
|
70
|
-
private constructor(
|
|
116
|
+
private constructor(
|
|
117
|
+
handle: FileHandle | null,
|
|
118
|
+
pmtiles: PMTiles,
|
|
119
|
+
minZoom: number,
|
|
120
|
+
maxZoom: number,
|
|
121
|
+
attribution: string
|
|
122
|
+
) {
|
|
71
123
|
this.handle = handle
|
|
72
124
|
this.pmtiles = pmtiles
|
|
73
125
|
this.minZoom = minZoom
|
|
@@ -75,21 +127,24 @@ export class TileSource {
|
|
|
75
127
|
this.attribution = attribution
|
|
76
128
|
}
|
|
77
129
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Opens a local `.pmtiles` path, or an `http(s)://` URL read via range requests — a hosted archive needs no tile
|
|
132
|
+
* server, only a host honoring `Range` (any static file server or object store does).
|
|
133
|
+
*/
|
|
134
|
+
static async open(pathOrURL: string): Promise<TileSource> {
|
|
135
|
+
if (/^https?:\/\//u.test(pathOrURL)) {
|
|
136
|
+
const pmtiles = new PMTiles(pathOrURL)
|
|
137
|
+
const [header, metadata] = await Promise.all([pmtiles.getHeader(), pmtiles.getMetadata()])
|
|
81
138
|
|
|
82
|
-
|
|
139
|
+
return new TileSource(null, pmtiles, header.minZoom, header.maxZoom, readAttribution(metadata))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handle = await open(pathOrURL, "r")
|
|
143
|
+
const pmtiles = new PMTiles(new FilePMTilesSource(pathOrURL, handle))
|
|
83
144
|
|
|
84
|
-
const
|
|
85
|
-
typeof metadata === "object" &&
|
|
86
|
-
metadata !== null &&
|
|
87
|
-
"attribution" in metadata &&
|
|
88
|
-
typeof (metadata as { attribution: unknown }).attribution === "string"
|
|
89
|
-
? (metadata as { attribution: string }).attribution.replaceAll(/<[^>]+>/gu, "").trim()
|
|
90
|
-
: ""
|
|
145
|
+
const [header, metadata] = await Promise.all([pmtiles.getHeader(), pmtiles.getMetadata()])
|
|
91
146
|
|
|
92
|
-
return new TileSource(handle, pmtiles, header.minZoom, header.maxZoom,
|
|
147
|
+
return new TileSource(handle, pmtiles, header.minZoom, header.maxZoom, readAttribution(metadata))
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
/**
|
|
@@ -125,6 +180,6 @@ export class TileSource {
|
|
|
125
180
|
}
|
|
126
181
|
|
|
127
182
|
async close(): Promise<void> {
|
|
128
|
-
await this.handle
|
|
183
|
+
await this.handle?.close()
|
|
129
184
|
}
|
|
130
185
|
}
|