styled-map-package 1.1.0 → 2.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/bin/smp-download.js +103 -10
- package/bin/smp-mbtiles.js +21 -0
- package/bin/smp.js +1 -0
- package/dist/lib/from-mbtiles.d.ts +13 -0
- package/dist/{index.d.ts → lib/index.d.ts} +2 -0
- package/dist/lib/reader-watch.d.ts +13 -0
- package/dist/{reader.d.ts → lib/reader.d.ts} +1 -1
- package/dist/{server.d.ts → lib/server.d.ts} +8 -5
- package/dist/{style-downloader.d.ts → lib/style-downloader.d.ts} +1 -1
- package/dist/lib/utils/errors.d.ts +16 -0
- package/dist/{utils → lib/utils}/fetch.d.ts +0 -9
- package/dist/{utils → lib/utils}/mapbox.d.ts +7 -6
- package/dist/{utils → lib/utils}/streams.d.ts +2 -2
- package/dist/{writer.d.ts → lib/writer.d.ts} +1 -2
- package/lib/from-mbtiles.js +83 -0
- package/lib/index.js +2 -0
- package/lib/reader-watch.js +133 -0
- package/lib/server.js +22 -57
- package/lib/utils/errors.js +15 -0
- package/lib/utils/mapbox.js +1 -1
- package/package.json +32 -21
- package/dist/utils/errors.d.ts +0 -6
- /package/dist/{download.d.ts → lib/download.d.ts} +0 -0
- /package/dist/{reporters.d.ts → lib/reporters.d.ts} +0 -0
- /package/dist/{tile-downloader.d.ts → lib/tile-downloader.d.ts} +0 -0
- /package/dist/{types.d.ts → lib/types.d.ts} +0 -0
- /package/dist/{utils → lib/utils}/file-formats.d.ts +0 -0
- /package/dist/{utils → lib/utils}/geo.d.ts +0 -0
- /package/dist/{utils → lib/utils}/misc.d.ts +0 -0
- /package/dist/{utils → lib/utils}/style.d.ts +0 -0
- /package/dist/{utils → lib/utils}/templates.d.ts +0 -0
package/bin/smp-download.js
CHANGED
|
@@ -1,32 +1,125 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command, InvalidArgumentError } from 'commander'
|
|
2
|
+
import { Command, InvalidArgumentError } from '@commander-js/extra-typings'
|
|
3
|
+
import { input, number } from '@inquirer/prompts'
|
|
3
4
|
import fs from 'fs'
|
|
4
5
|
import { pipeline } from 'stream/promises'
|
|
5
6
|
|
|
6
7
|
import download from '../lib/download.js'
|
|
7
8
|
import { ttyReporter } from '../lib/reporters.js'
|
|
9
|
+
import { isMapboxURL, API_URL as MAPBOX_API_URL } from '../lib/utils/mapbox.js'
|
|
8
10
|
|
|
9
11
|
const program = new Command()
|
|
10
12
|
|
|
11
13
|
program
|
|
12
14
|
.description('Download a map style for offline usage')
|
|
13
|
-
.option('-o, --output
|
|
14
|
-
.
|
|
15
|
+
.option('-o, --output <file>', 'output file (if omitted, writes to stdout)')
|
|
16
|
+
.option(
|
|
15
17
|
'-b, --bbox <west,south,east,north>',
|
|
16
18
|
'bounding box of area to download e.g. 11,47,12,47.5',
|
|
17
19
|
parseBbox,
|
|
18
20
|
)
|
|
19
|
-
.
|
|
20
|
-
'-z, --zoom <number>',
|
|
21
|
-
'max zoom level to download',
|
|
22
|
-
parseZoom,
|
|
23
|
-
)
|
|
21
|
+
.option('-z, --zoom <number>', 'max zoom level to download', parseZoom)
|
|
24
22
|
.option(
|
|
25
23
|
'-t, --token <token>',
|
|
26
24
|
'Mapbox access token (necessary for Mapbox styles)',
|
|
27
25
|
)
|
|
28
|
-
.argument('
|
|
26
|
+
.argument('[styleUrl]', 'URL to style to download', parseUrl)
|
|
29
27
|
.action(async (styleUrl, { bbox, zoom, output, token }) => {
|
|
28
|
+
const promptOutput =
|
|
29
|
+
!output &&
|
|
30
|
+
process.stdout.isTTY &&
|
|
31
|
+
(!styleUrl || !bbox || zoom === undefined)
|
|
32
|
+
|
|
33
|
+
if (!styleUrl) {
|
|
34
|
+
styleUrl = await input({
|
|
35
|
+
message: 'Style URL to download',
|
|
36
|
+
required: true,
|
|
37
|
+
validate: (value) => {
|
|
38
|
+
try {
|
|
39
|
+
new URL(value)
|
|
40
|
+
return true
|
|
41
|
+
} catch {
|
|
42
|
+
return 'Please enter a valid URL.'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!bbox) {
|
|
49
|
+
const west = await number({
|
|
50
|
+
message: 'Bounding box west',
|
|
51
|
+
required: true,
|
|
52
|
+
step: 'any',
|
|
53
|
+
min: -180,
|
|
54
|
+
max: 180,
|
|
55
|
+
})
|
|
56
|
+
const south = await number({
|
|
57
|
+
message: 'Bounding box south',
|
|
58
|
+
required: true,
|
|
59
|
+
step: 'any',
|
|
60
|
+
min: -90,
|
|
61
|
+
max: 90,
|
|
62
|
+
})
|
|
63
|
+
const east = await number({
|
|
64
|
+
message: 'Bounding box east',
|
|
65
|
+
required: true,
|
|
66
|
+
step: 'any',
|
|
67
|
+
min: -180,
|
|
68
|
+
max: 180,
|
|
69
|
+
})
|
|
70
|
+
const north = await number({
|
|
71
|
+
message: 'Bounding box north',
|
|
72
|
+
required: true,
|
|
73
|
+
step: 'any',
|
|
74
|
+
min: -90,
|
|
75
|
+
max: 90,
|
|
76
|
+
})
|
|
77
|
+
if (
|
|
78
|
+
west === undefined ||
|
|
79
|
+
south === undefined ||
|
|
80
|
+
east === undefined ||
|
|
81
|
+
north === undefined
|
|
82
|
+
) {
|
|
83
|
+
throw new InvalidArgumentError('Bounding box values are required.')
|
|
84
|
+
}
|
|
85
|
+
bbox = [west, south, east, north]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (zoom === undefined) {
|
|
89
|
+
zoom = await number({
|
|
90
|
+
message: 'Max zoom level to download',
|
|
91
|
+
required: true,
|
|
92
|
+
min: 0,
|
|
93
|
+
max: 22,
|
|
94
|
+
})
|
|
95
|
+
if (zoom === undefined) {
|
|
96
|
+
throw new InvalidArgumentError('Zoom level is required.')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
(isMapboxURL(styleUrl) || styleUrl.startsWith(MAPBOX_API_URL)) &&
|
|
102
|
+
!token
|
|
103
|
+
) {
|
|
104
|
+
token = await input({
|
|
105
|
+
message: 'Mapbox access token',
|
|
106
|
+
required: true,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (promptOutput) {
|
|
111
|
+
output = await input({
|
|
112
|
+
message: 'Output filename (.smp extension will be added)',
|
|
113
|
+
required: true,
|
|
114
|
+
transformer: (value) =>
|
|
115
|
+
value.endsWith('.smp') ? value : `${value}.smp`,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (output && !output.endsWith('.smp')) {
|
|
120
|
+
output += '.smp'
|
|
121
|
+
}
|
|
122
|
+
|
|
30
123
|
const reporter = ttyReporter()
|
|
31
124
|
const readStream = download({
|
|
32
125
|
bbox,
|
|
@@ -63,7 +156,7 @@ function parseBbox(bbox) {
|
|
|
63
156
|
if (bounds.some(isNaN)) {
|
|
64
157
|
throw new InvalidArgumentError('Bounding box values must be numbers.')
|
|
65
158
|
}
|
|
66
|
-
return bounds
|
|
159
|
+
return /** @type {[number, number, number, number]} */ (bounds)
|
|
67
160
|
}
|
|
68
161
|
|
|
69
162
|
/** @param {string} url */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { pipeline } from 'stream/promises'
|
|
4
|
+
|
|
5
|
+
import fromMBTiles from '../lib/from-mbtiles.js'
|
|
6
|
+
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.description('Convert a MBTiles file to a styled map package file')
|
|
11
|
+
.option('-o, --output <file>', 'output smp file')
|
|
12
|
+
.argument('<mbtiles>', 'MBTiles file to convert')
|
|
13
|
+
.action(async (mbtilesPath, { output }) => {
|
|
14
|
+
if (output) {
|
|
15
|
+
await fromMBTiles(mbtilesPath, output)
|
|
16
|
+
} else {
|
|
17
|
+
await pipeline(fromMBTiles(mbtilesPath), process.stdout)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
program.parseAsync(process.argv)
|
package/bin/smp.js
CHANGED
|
@@ -7,5 +7,6 @@ program
|
|
|
7
7
|
.name('smp')
|
|
8
8
|
.command('download', 'Download a map style to a styled map package file')
|
|
9
9
|
.command('view', 'Preview a styled map package in a web browser')
|
|
10
|
+
.command('mbtiles', 'Convert a MBTiles file to a styled map package file')
|
|
10
11
|
|
|
11
12
|
program.parse(process.argv)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @overload
|
|
3
|
+
* @param {string} mbtilesPath
|
|
4
|
+
* @returns {import('stream').Readable}
|
|
5
|
+
*/
|
|
6
|
+
export default function fromMBTiles(mbtilesPath: string): import("stream").Readable;
|
|
7
|
+
/**
|
|
8
|
+
* @overload
|
|
9
|
+
* @param {string} mbtilesPath
|
|
10
|
+
* @param {string} outputPath
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
export default function fromMBTiles(mbtilesPath: string, outputPath: string): Promise<void>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { default as Reader } from "./reader.js";
|
|
2
|
+
export { default as ReaderWatch } from "./reader-watch.js";
|
|
2
3
|
export { default as Writer } from "./writer.js";
|
|
3
4
|
export { default as Server } from "./server.js";
|
|
4
5
|
export { default as StyleDownloader } from "./style-downloader.js";
|
|
5
6
|
export { downloadTiles } from "./tile-downloader.js";
|
|
6
7
|
export { default as download } from "./download.js";
|
|
8
|
+
export { default as fromMBTiles } from "./from-mbtiles.js";
|
|
7
9
|
export type SMPSource = import("./types.js").SMPSource;
|
|
8
10
|
export type SMPStyle = import("./types.js").SMPStyle;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @implements {Pick<Reader, keyof Reader>} */
|
|
2
|
+
export default class ReaderWatch implements Pick<Reader, keyof Reader> {
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} filepath
|
|
5
|
+
*/
|
|
6
|
+
constructor(filepath: string);
|
|
7
|
+
opened(): Promise<void>;
|
|
8
|
+
getStyle(baseUrl?: string | null): Promise<import("./types.js").SMPStyle>;
|
|
9
|
+
getResource(path: string): Promise<Resource>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
#private;
|
|
12
|
+
}
|
|
13
|
+
import Reader from './reader.js';
|
|
@@ -27,7 +27,7 @@ export default class Reader {
|
|
|
27
27
|
* @param {string | null} [baseUrl] Base URL where you plan to serve the resources in this styled map package, e.g. `http://localhost:3000/maps/styleA`
|
|
28
28
|
* @returns {Promise<import('./types.js').SMPStyle>}
|
|
29
29
|
*/
|
|
30
|
-
getStyle(baseUrl?: string | null
|
|
30
|
+
getStyle(baseUrl?: string | null): Promise<import("./types.js").SMPStyle>;
|
|
31
31
|
/**
|
|
32
32
|
* Get a resource from the styled map package. The path should be relative to
|
|
33
33
|
* the root of the package.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export default function _default(instance: import("fastify").FastifyInstance<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>, opts: PluginOptions, done: (err?: Error) => void): void;
|
|
2
|
-
export type
|
|
3
|
-
lazy?: boolean | undefined;
|
|
4
|
-
prefix?: string | undefined;
|
|
2
|
+
export type PluginOptionsFilepath = {
|
|
5
3
|
/**
|
|
6
4
|
* Path to styled map package (`.smp`) file
|
|
7
5
|
*/
|
|
6
|
+
filepath: string;
|
|
7
|
+
};
|
|
8
|
+
export type PluginOptionsReader = {
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* SMP Reader interface (also supports ReaderWatch)
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
reader: Pick<Reader, keyof Reader>;
|
|
12
13
|
};
|
|
14
|
+
export type PluginOptions = PluginOptionsFilepath | PluginOptionsReader;
|
|
15
|
+
import Reader from './reader.js';
|
|
@@ -24,7 +24,7 @@ export default class StyleDownloader {
|
|
|
24
24
|
constructor(style: string | StyleSpecification, { concurrency, mapboxAccessToken }?: {
|
|
25
25
|
concurrency?: number | undefined;
|
|
26
26
|
mapboxAccessToken?: string | undefined;
|
|
27
|
-
}
|
|
27
|
+
});
|
|
28
28
|
/**
|
|
29
29
|
* Number of active downloads.
|
|
30
30
|
*/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the error if because a file is not found. On Windows, some
|
|
3
|
+
* operations like fs.watch() throw an EPERM error rather than ENOENT.
|
|
4
|
+
*
|
|
5
|
+
* @param {unknown} error
|
|
6
|
+
* @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
|
|
7
|
+
*/
|
|
8
|
+
export function isFileNotThereError(error: unknown): error is Error & {
|
|
9
|
+
code: "ENOENT" | "EPERM";
|
|
10
|
+
};
|
|
11
|
+
export class ENOENT extends Error {
|
|
12
|
+
/** @param {string} path */
|
|
13
|
+
constructor(path: string);
|
|
14
|
+
code: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
@@ -30,23 +30,14 @@ export class FetchQueue {
|
|
|
30
30
|
#private;
|
|
31
31
|
}
|
|
32
32
|
export type DownloadResponse = {
|
|
33
|
-
/**
|
|
34
|
-
* Node ReadableStream of the response body
|
|
35
|
-
*/
|
|
36
33
|
/**
|
|
37
34
|
* Node ReadableStream of the response body
|
|
38
35
|
*/
|
|
39
36
|
body: import("stream").Readable;
|
|
40
|
-
/**
|
|
41
|
-
* Content mime-type (from http content-type header)
|
|
42
|
-
*/
|
|
43
37
|
/**
|
|
44
38
|
* Content mime-type (from http content-type header)
|
|
45
39
|
*/
|
|
46
40
|
mimeType: string | null;
|
|
47
|
-
/**
|
|
48
|
-
* Content length in bytes (from http content-length header)
|
|
49
|
-
*/
|
|
50
41
|
/**
|
|
51
42
|
* Content length in bytes (from http content-length header)
|
|
52
43
|
*/
|
|
@@ -4,34 +4,35 @@ export function isMapboxURL(url: string): boolean;
|
|
|
4
4
|
* @param {string} url
|
|
5
5
|
* @param {string} [accessToken]
|
|
6
6
|
*/
|
|
7
|
-
export function normalizeStyleURL(url: string, accessToken?: string
|
|
7
|
+
export function normalizeStyleURL(url: string, accessToken?: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* @param {string} url
|
|
10
10
|
* @param {string} [accessToken]
|
|
11
11
|
*/
|
|
12
|
-
export function normalizeGlyphsURL(url: string, accessToken?: string
|
|
12
|
+
export function normalizeGlyphsURL(url: string, accessToken?: string): string;
|
|
13
13
|
/**
|
|
14
14
|
* @param {string} url
|
|
15
15
|
* @param {string} [accessToken]
|
|
16
16
|
*/
|
|
17
|
-
export function normalizeSourceURL(url: string, accessToken?: string
|
|
17
|
+
export function normalizeSourceURL(url: string, accessToken?: string): string;
|
|
18
18
|
/**
|
|
19
19
|
* @param {string} url
|
|
20
20
|
* @param {'' | '@2x'} format
|
|
21
21
|
* @param {'.png' | '.json'} extension
|
|
22
22
|
* @param {string} [accessToken]
|
|
23
23
|
*/
|
|
24
|
-
export function normalizeSpriteURL(url: string, format: "" | "@2x", extension: ".png" | ".json", accessToken?: string
|
|
24
|
+
export function normalizeSpriteURL(url: string, format: "" | "@2x", extension: ".png" | ".json", accessToken?: string): string;
|
|
25
25
|
/**
|
|
26
26
|
* @param {any} tileURL
|
|
27
27
|
* @param {string} sourceURL
|
|
28
28
|
* @param {256 | 512} [tileSize]
|
|
29
29
|
* @param {{ devicePixelRatio?: number; supportsWebp?: boolean; }} [opts]
|
|
30
30
|
*/
|
|
31
|
-
export function normalizeTileURL(tileURL: any, sourceURL: string, tileSize?: 256 | 512
|
|
31
|
+
export function normalizeTileURL(tileURL: any, sourceURL: string, tileSize?: 256 | 512, { devicePixelRatio, supportsWebp }?: {
|
|
32
32
|
devicePixelRatio?: number;
|
|
33
33
|
supportsWebp?: boolean;
|
|
34
|
-
}
|
|
34
|
+
}): any;
|
|
35
|
+
export const API_URL: "https://api.mapbox.com";
|
|
35
36
|
export type URLObject = {
|
|
36
37
|
protocol: string;
|
|
37
38
|
authority: string;
|
|
@@ -28,7 +28,7 @@ export function fromWebReadableStream(readableStream: ReadableStream, options?:
|
|
|
28
28
|
encoding?: string;
|
|
29
29
|
objectMode?: boolean;
|
|
30
30
|
signal?: AbortSignal;
|
|
31
|
-
}
|
|
31
|
+
}): import("stream").Readable;
|
|
32
32
|
/**
|
|
33
33
|
* @param {unknown} obj
|
|
34
34
|
* @returns {obj is ReadableStream}
|
|
@@ -46,7 +46,7 @@ export class ProgressStream extends Transform {
|
|
|
46
46
|
/**
|
|
47
47
|
* @param {ProgressStreamOptions} [opts]
|
|
48
48
|
*/
|
|
49
|
-
constructor({ onprogress, ...opts }?: ProgressStreamOptions
|
|
49
|
+
constructor({ onprogress, ...opts }?: ProgressStreamOptions);
|
|
50
50
|
/** Total bytes that have passed through this stream */
|
|
51
51
|
get byteLength(): number;
|
|
52
52
|
/**
|
|
@@ -45,7 +45,7 @@ export default class Writer extends EventEmitter<[never]> {
|
|
|
45
45
|
/**
|
|
46
46
|
* @returns {import('stream').Readable} Readable stream of the styled map package
|
|
47
47
|
*/
|
|
48
|
-
get outputStream(): Readable;
|
|
48
|
+
get outputStream(): import("stream").Readable;
|
|
49
49
|
/**
|
|
50
50
|
* Add a tile to the styled map package
|
|
51
51
|
*
|
|
@@ -127,4 +127,3 @@ export type GlyphInfo = {
|
|
|
127
127
|
range: GlyphRange;
|
|
128
128
|
};
|
|
129
129
|
import { EventEmitter } from 'events';
|
|
130
|
-
import { Readable } from 'stream';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { MBTiles } from 'mbtiles-reader'
|
|
2
|
+
import SMPWriter from 'styled-map-package/writer'
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { Transform } from 'node:stream'
|
|
6
|
+
import { pipeline } from 'node:stream'
|
|
7
|
+
import { pipeline as pipelinePromise } from 'node:stream/promises'
|
|
8
|
+
|
|
9
|
+
const SOURCE_ID = 'mbtiles-source'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @overload
|
|
13
|
+
* @param {string} mbtilesPath
|
|
14
|
+
* @returns {import('stream').Readable}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @overload
|
|
19
|
+
* @param {string} mbtilesPath
|
|
20
|
+
* @param {string} outputPath
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} mbtilesPath
|
|
26
|
+
* @param {string} [outputPath]
|
|
27
|
+
* @returns {Promise<void> | import('stream').Readable}
|
|
28
|
+
*/
|
|
29
|
+
export default function fromMBTiles(mbtilesPath, outputPath) {
|
|
30
|
+
const reader = new MBTiles(mbtilesPath)
|
|
31
|
+
if (reader.metadata.format === 'pbf') {
|
|
32
|
+
throw new Error('Vector MBTiles are not yet supported')
|
|
33
|
+
}
|
|
34
|
+
const style = {
|
|
35
|
+
version: 8,
|
|
36
|
+
name: reader.metadata.name,
|
|
37
|
+
sources: {
|
|
38
|
+
[SOURCE_ID]: {
|
|
39
|
+
...reader.metadata,
|
|
40
|
+
type: 'raster',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
layers: [
|
|
44
|
+
{
|
|
45
|
+
id: 'background',
|
|
46
|
+
type: 'background',
|
|
47
|
+
paint: {
|
|
48
|
+
'background-color': 'white',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'raster',
|
|
53
|
+
type: 'raster',
|
|
54
|
+
source: SOURCE_ID,
|
|
55
|
+
paint: {
|
|
56
|
+
'raster-opacity': 1,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const writer = new SMPWriter(style)
|
|
63
|
+
|
|
64
|
+
const returnValue = outputPath
|
|
65
|
+
? pipelinePromise(writer.outputStream, fs.createWriteStream(outputPath))
|
|
66
|
+
: writer.outputStream
|
|
67
|
+
|
|
68
|
+
const tileWriteStream = writer.createTileWriteStream()
|
|
69
|
+
|
|
70
|
+
const transform = new Transform({
|
|
71
|
+
objectMode: true,
|
|
72
|
+
transform({ z, x, y, data, format }, encoding, callback) {
|
|
73
|
+
callback(null, [data, { z, x, y, format, sourceId: SOURCE_ID }])
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
pipeline(reader, transform, tileWriteStream, (err) => {
|
|
78
|
+
if (err) return writer.outputStream.destroy(err)
|
|
79
|
+
writer.finish()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return returnValue
|
|
83
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
/** @typedef {import('./types.js').SMPStyle} SMPStyle */
|
|
3
3
|
|
|
4
4
|
export { default as Reader } from './reader.js'
|
|
5
|
+
export { default as ReaderWatch } from './reader-watch.js'
|
|
5
6
|
export { default as Writer } from './writer.js'
|
|
6
7
|
export { default as Server } from './server.js'
|
|
7
8
|
export { default as StyleDownloader } from './style-downloader.js'
|
|
8
9
|
export { downloadTiles } from './tile-downloader.js'
|
|
9
10
|
export { default as download } from './download.js'
|
|
11
|
+
export { default as fromMBTiles } from './from-mbtiles.js'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { once } from 'events'
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import fsPromises from 'node:fs/promises'
|
|
5
|
+
|
|
6
|
+
import Reader from './reader.js'
|
|
7
|
+
import { ENOENT, isFileNotThereError } from './utils/errors.js'
|
|
8
|
+
import { noop } from './utils/misc.js'
|
|
9
|
+
|
|
10
|
+
/** @implements {Pick<Reader, keyof Reader>} */
|
|
11
|
+
export default class ReaderWatch {
|
|
12
|
+
/** @type {Reader | undefined} */
|
|
13
|
+
#reader
|
|
14
|
+
/** @type {Reader | undefined} */
|
|
15
|
+
#maybeReader
|
|
16
|
+
/** @type {Promise<Reader> | undefined} */
|
|
17
|
+
#readerOpeningPromise
|
|
18
|
+
#filepath
|
|
19
|
+
/** @type {fs.FSWatcher | undefined} */
|
|
20
|
+
#watch
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} filepath
|
|
24
|
+
*/
|
|
25
|
+
constructor(filepath) {
|
|
26
|
+
this.#filepath = filepath
|
|
27
|
+
// Call this now to catch any synchronous errors
|
|
28
|
+
this.#tryToWatchFile()
|
|
29
|
+
// eagerly open Reader
|
|
30
|
+
this.#get().catch(noop)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#tryToWatchFile() {
|
|
34
|
+
if (this.#watch) return
|
|
35
|
+
try {
|
|
36
|
+
this.#watch = fs
|
|
37
|
+
.watch(this.#filepath, { persistent: false }, () => {
|
|
38
|
+
this.#reader?.close().catch(noop)
|
|
39
|
+
this.#reader = undefined
|
|
40
|
+
this.#maybeReader = undefined
|
|
41
|
+
this.#readerOpeningPromise = undefined
|
|
42
|
+
// Close the watcher (which on some platforms will continue watching
|
|
43
|
+
// the previous file) so on the next request we will start watching
|
|
44
|
+
// the new file
|
|
45
|
+
this.#watch?.close()
|
|
46
|
+
this.#watch = undefined
|
|
47
|
+
})
|
|
48
|
+
.on('error', noop)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isFileNotThereError(error)) {
|
|
51
|
+
// Ignore: File does not exist yet, but we'll try to open it later
|
|
52
|
+
} else {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async #get() {
|
|
59
|
+
if (isWin() && (this.#reader || this.#readerOpeningPromise)) {
|
|
60
|
+
// On Windows, the file watcher does not recognize file deletions, so we
|
|
61
|
+
// need to check if the file still exists each time
|
|
62
|
+
try {
|
|
63
|
+
await fsPromises.stat(this.#filepath)
|
|
64
|
+
} catch {
|
|
65
|
+
this.#watch?.close()
|
|
66
|
+
this.#watch = undefined
|
|
67
|
+
this.#reader?.close().catch(noop)
|
|
68
|
+
this.#reader = undefined
|
|
69
|
+
this.#maybeReader = undefined
|
|
70
|
+
this.#readerOpeningPromise = undefined
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Need to retry this each time in case it failed initially because the file
|
|
74
|
+
// was not present, or if the file was moved or deleted.
|
|
75
|
+
this.#tryToWatchFile()
|
|
76
|
+
// A lovely promise tangle to confuse future readers... sorry.
|
|
77
|
+
//
|
|
78
|
+
// 1. If the reader is already open, return it.
|
|
79
|
+
// 2. If the reader is in the process of opening, return a promise that will
|
|
80
|
+
// return the reader instance if it opened without error, or throw.
|
|
81
|
+
// 3. If the reader threw an error during opening, try to open it again next
|
|
82
|
+
// time this is called.
|
|
83
|
+
if (this.#reader) return this.#reader
|
|
84
|
+
if (this.#readerOpeningPromise) return this.#readerOpeningPromise
|
|
85
|
+
this.#maybeReader = new Reader(this.#filepath)
|
|
86
|
+
this.#readerOpeningPromise = this.#maybeReader
|
|
87
|
+
.opened()
|
|
88
|
+
.then(() => {
|
|
89
|
+
if (!this.#maybeReader) {
|
|
90
|
+
throw new ENOENT(this.#filepath)
|
|
91
|
+
}
|
|
92
|
+
this.#reader = this.#maybeReader
|
|
93
|
+
return this.#reader
|
|
94
|
+
})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
this.#maybeReader = undefined
|
|
97
|
+
this.#readerOpeningPromise = undefined
|
|
98
|
+
})
|
|
99
|
+
return this.#readerOpeningPromise
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @type {Reader['opened']} */
|
|
103
|
+
async opened() {
|
|
104
|
+
const reader = await this.#get()
|
|
105
|
+
return reader.opened()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {Reader['getStyle']} */
|
|
109
|
+
async getStyle(baseUrl = null) {
|
|
110
|
+
const reader = await this.#get()
|
|
111
|
+
return reader.getStyle(baseUrl)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @type {Reader['getResource']} */
|
|
115
|
+
async getResource(path) {
|
|
116
|
+
const reader = await this.#get()
|
|
117
|
+
return reader.getResource(path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async close() {
|
|
121
|
+
const reader = await this.#get()
|
|
122
|
+
if (this.#watch) {
|
|
123
|
+
this.#watch.close()
|
|
124
|
+
await once(this.#watch, 'close')
|
|
125
|
+
}
|
|
126
|
+
await reader.close()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @returns {boolean} */
|
|
131
|
+
function isWin() {
|
|
132
|
+
return process.platform === 'win32'
|
|
133
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import createError from 'http-errors'
|
|
2
2
|
|
|
3
3
|
import Reader from './reader.js'
|
|
4
|
+
import { isFileNotThereError } from './utils/errors.js'
|
|
4
5
|
import { noop } from './utils/misc.js'
|
|
5
6
|
|
|
6
7
|
/** @import { FastifyPluginCallback, FastifyReply } from 'fastify' */
|
|
7
8
|
/** @import { Resource } from './reader.js' */
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* @typedef {object}
|
|
11
|
-
* @property {boolean} [lazy=false]
|
|
12
|
-
* @property {string} [prefix]
|
|
11
|
+
* @typedef {object} PluginOptionsFilepath
|
|
13
12
|
* @property {string} filepath Path to styled map package (`.smp`) file
|
|
14
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} PluginOptionsReader
|
|
16
|
+
* @property {Pick<Reader, keyof Reader>} reader SMP Reader interface (also supports ReaderWatch)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {PluginOptionsFilepath | PluginOptionsReader} PluginOptions
|
|
20
|
+
*/
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* @param {FastifyReply} reply
|
|
@@ -29,60 +35,28 @@ function sendResource(reply, resource) {
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
|
-
* Fastify plugin for serving a styled map package.
|
|
33
|
-
*
|
|
38
|
+
* Fastify plugin for serving a styled map package.
|
|
39
|
+
*
|
|
40
|
+
* If you provide a `Reader` (or `ReaderWatch`) instance via the `reader` opt,
|
|
41
|
+
* you must manually close the instance yourself.
|
|
34
42
|
*
|
|
35
43
|
* @type {FastifyPluginCallback<PluginOptions>}
|
|
36
44
|
*/
|
|
37
|
-
export default function (fastify,
|
|
38
|
-
|
|
39
|
-
let _reader
|
|
40
|
-
/** @type {Promise<Reader> | undefined} */
|
|
41
|
-
let _readerOpeningPromise
|
|
42
|
-
|
|
43
|
-
async function getReader() {
|
|
44
|
-
// A lovely promise tangle to confuse future readers... sorry.
|
|
45
|
-
//
|
|
46
|
-
// 1. If the reader is already open, return it.
|
|
47
|
-
// 2. If the reader is in the process of opening, return a promise that will
|
|
48
|
-
// return the reader instance if it opened without error, or throw.
|
|
49
|
-
// 3. If the reader threw an error during opening, try to open it again next
|
|
50
|
-
// time this is called.
|
|
51
|
-
if (_reader) return _reader
|
|
52
|
-
if (_readerOpeningPromise) return _readerOpeningPromise
|
|
53
|
-
const maybeReader = new Reader(filepath)
|
|
54
|
-
_readerOpeningPromise = maybeReader
|
|
55
|
-
.opened()
|
|
56
|
-
.then(() => {
|
|
57
|
-
_reader = maybeReader
|
|
58
|
-
return _reader
|
|
59
|
-
})
|
|
60
|
-
.finally(() => {
|
|
61
|
-
_readerOpeningPromise = undefined
|
|
62
|
-
})
|
|
63
|
-
return _readerOpeningPromise
|
|
64
|
-
}
|
|
45
|
+
export default function (fastify, opts, done) {
|
|
46
|
+
const reader = 'reader' in opts ? opts.reader : new Reader(opts.filepath)
|
|
65
47
|
|
|
66
|
-
if
|
|
67
|
-
|
|
48
|
+
// Only close the reader if it was created by this plugin
|
|
49
|
+
if (!('reader' in opts)) {
|
|
50
|
+
fastify.addHook('onClose', () => reader.close().catch(noop))
|
|
68
51
|
}
|
|
69
52
|
|
|
70
|
-
fastify.addHook('onClose', async () => {
|
|
71
|
-
try {
|
|
72
|
-
const reader = await getReader()
|
|
73
|
-
await reader.close()
|
|
74
|
-
} catch {
|
|
75
|
-
// ignore
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
53
|
fastify.get('/style.json', async () => {
|
|
80
54
|
try {
|
|
81
|
-
const reader = await getReader()
|
|
82
55
|
const baseUrl = new URL(fastify.prefix, fastify.listeningOrigin)
|
|
83
|
-
|
|
56
|
+
const style = await reader.getStyle(baseUrl.href)
|
|
57
|
+
return style
|
|
84
58
|
} catch (error) {
|
|
85
|
-
if (
|
|
59
|
+
if (isFileNotThereError(error)) {
|
|
86
60
|
throw createError(404, error.message)
|
|
87
61
|
}
|
|
88
62
|
throw error
|
|
@@ -94,11 +68,10 @@ export default function (fastify, { lazy = false, filepath }, done) {
|
|
|
94
68
|
const path = request.params['*']
|
|
95
69
|
|
|
96
70
|
try {
|
|
97
|
-
const reader = await getReader()
|
|
98
71
|
const resource = await reader.getResource(path)
|
|
99
72
|
return sendResource(reply, resource)
|
|
100
73
|
} catch (error) {
|
|
101
|
-
if (
|
|
74
|
+
if (isFileNotThereError(error)) {
|
|
102
75
|
throw createError(404, error.message)
|
|
103
76
|
}
|
|
104
77
|
throw error
|
|
@@ -106,11 +79,3 @@ export default function (fastify, { lazy = false, filepath }, done) {
|
|
|
106
79
|
})
|
|
107
80
|
done()
|
|
108
81
|
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @param {unknown} error
|
|
112
|
-
* @returns {error is Error & { code: 'ENOENT' }}
|
|
113
|
-
*/
|
|
114
|
-
function isENOENT(error) {
|
|
115
|
-
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
116
|
-
}
|
package/lib/utils/errors.js
CHANGED
|
@@ -7,3 +7,18 @@ export class ENOENT extends Error {
|
|
|
7
7
|
this.path = path
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if the error if because a file is not found. On Windows, some
|
|
13
|
+
* operations like fs.watch() throw an EPERM error rather than ENOENT.
|
|
14
|
+
*
|
|
15
|
+
* @param {unknown} error
|
|
16
|
+
* @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
|
|
17
|
+
*/
|
|
18
|
+
export function isFileNotThereError(error) {
|
|
19
|
+
return (
|
|
20
|
+
error instanceof Error &&
|
|
21
|
+
'code' in error &&
|
|
22
|
+
(error.code === 'ENOENT' || error.code === 'EPERM')
|
|
23
|
+
)
|
|
24
|
+
}
|
package/lib/utils/mapbox.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
// from https://github.com/mapbox/mapbox-gl-js/blob/495a695/src/util/mapbox.js
|
|
3
3
|
|
|
4
|
-
const API_URL = 'https://api.mapbox.com'
|
|
4
|
+
export const API_URL = 'https://api.mapbox.com'
|
|
5
5
|
const HELP = 'See https://www.mapbox.com/api-documentation/#access-tokens'
|
|
6
6
|
|
|
7
7
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "styled-map-package",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"types": "./dist/reader.d.ts",
|
|
12
12
|
"import": "./lib/reader.js"
|
|
13
13
|
},
|
|
14
|
+
"./reader-watch": {
|
|
15
|
+
"types": "./dist/reader-watch.d.ts",
|
|
16
|
+
"import": "./lib/reader-watch.js"
|
|
17
|
+
},
|
|
14
18
|
"./writer": {
|
|
15
19
|
"types": "./dist/writer.d.ts",
|
|
16
20
|
"import": "./lib/writer.js"
|
|
@@ -30,6 +34,10 @@
|
|
|
30
34
|
"./download": {
|
|
31
35
|
"types": "./dist/download.d.ts",
|
|
32
36
|
"import": "./lib/download.js"
|
|
37
|
+
},
|
|
38
|
+
"./from-mbtiles": {
|
|
39
|
+
"types": "./dist/from-mbtiles.d.ts",
|
|
40
|
+
"import": "./lib/from-mbtiles.js"
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"main": "./lib/index.js",
|
|
@@ -57,68 +65,71 @@
|
|
|
57
65
|
"author": "",
|
|
58
66
|
"license": "MIT",
|
|
59
67
|
"dependencies": {
|
|
68
|
+
"@commander-js/extra-typings": "^12.1.0",
|
|
60
69
|
"@fastify/static": "^7.0.4",
|
|
70
|
+
"@inquirer/prompts": "^6.0.1",
|
|
61
71
|
"@mapbox/sphericalmercator": "^1.2.0",
|
|
62
72
|
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
|
|
63
73
|
"@placemarkio/check-geojson": "^0.1.12",
|
|
64
|
-
"@turf/bbox": "^7.
|
|
65
|
-
"@turf/helpers": "^7.
|
|
74
|
+
"@turf/bbox": "^7.2.0",
|
|
75
|
+
"@turf/helpers": "^7.2.0",
|
|
66
76
|
"ansi-diff": "^1.2.0",
|
|
67
77
|
"archiver": "^7.0.1",
|
|
68
78
|
"buffer-peek-stream": "^1.1.0",
|
|
69
|
-
"chalk": "^5.
|
|
79
|
+
"chalk": "^5.4.1",
|
|
70
80
|
"commander": "^12.1.0",
|
|
71
81
|
"fastify": "^4.28.1",
|
|
72
82
|
"filter-obj": "^6.1.0",
|
|
73
83
|
"http-errors": "^2.0.0",
|
|
74
84
|
"into-stream": "^8.0.1",
|
|
75
85
|
"is-stream": "^4.0.1",
|
|
76
|
-
"ky": "^1.7.
|
|
86
|
+
"ky": "^1.7.5",
|
|
77
87
|
"log-symbols": "^7.0.0",
|
|
78
88
|
"map-obj": "^5.0.2",
|
|
89
|
+
"mbtiles-reader": "^1.0.0",
|
|
79
90
|
"open": "^10.1.0",
|
|
80
|
-
"ora": "^8.
|
|
91
|
+
"ora": "^8.2.0",
|
|
81
92
|
"p-event": "^6.0.1",
|
|
82
|
-
"p-limit": "^6.
|
|
93
|
+
"p-limit": "^6.2.0",
|
|
83
94
|
"pretty-bytes": "^6.1.1",
|
|
84
|
-
"pretty-ms": "^9.
|
|
85
|
-
"readable-stream": "^4.
|
|
95
|
+
"pretty-ms": "^9.2.0",
|
|
96
|
+
"readable-stream": "^4.7.0",
|
|
86
97
|
"temp-dir": "^3.0.0",
|
|
87
98
|
"yauzl-promise": "^4.0.0",
|
|
88
99
|
"yocto-queue": "^1.1.1"
|
|
89
100
|
},
|
|
90
101
|
"devDependencies": {
|
|
91
|
-
"@eslint/js": "^9.
|
|
102
|
+
"@eslint/js": "^9.21.0",
|
|
92
103
|
"@jsquash/jpeg": "^1.4.0",
|
|
93
104
|
"@jsquash/png": "^3.0.1",
|
|
94
105
|
"@stealthybox/jpg-stream": "^1.1.2",
|
|
95
106
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
|
96
|
-
"@types/archiver": "^6.0.
|
|
107
|
+
"@types/archiver": "^6.0.3",
|
|
97
108
|
"@types/eslint": "^9.6.1",
|
|
98
109
|
"@types/eslint__js": "^8.42.3",
|
|
99
|
-
"@types/geojson": "^7946.0.
|
|
110
|
+
"@types/geojson": "^7946.0.16",
|
|
100
111
|
"@types/http-errors": "^2.0.4",
|
|
101
112
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
|
102
113
|
"@types/node": "^20.16.3",
|
|
103
|
-
"@types/readable-stream": "^4.0.
|
|
114
|
+
"@types/readable-stream": "^4.0.18",
|
|
104
115
|
"@types/yauzl-promise": "^4.0.1",
|
|
105
|
-
"ava": "^6.
|
|
116
|
+
"ava": "^6.2.0",
|
|
106
117
|
"block-stream2": "^2.1.0",
|
|
107
|
-
"eslint": "^9.
|
|
118
|
+
"eslint": "^9.21.0",
|
|
108
119
|
"execa": "^9.4.0",
|
|
109
120
|
"globals": "^15.9.0",
|
|
110
|
-
"husky": "^9.1.
|
|
121
|
+
"husky": "^9.1.7",
|
|
111
122
|
"jpg-stream": "^1.1.2",
|
|
112
|
-
"lint-staged": "^15.
|
|
123
|
+
"lint-staged": "^15.4.3",
|
|
113
124
|
"pixel-stream": "^1.0.3",
|
|
114
|
-
"playwright": "^1.
|
|
125
|
+
"playwright": "^1.50.1",
|
|
115
126
|
"png-stream": "^1.0.5",
|
|
116
|
-
"prettier": "^3.
|
|
127
|
+
"prettier": "^3.5.2",
|
|
117
128
|
"random-bytes-readable-stream": "^3.0.0",
|
|
118
129
|
"rimraf": "^4.4.1",
|
|
119
130
|
"tempy": "^3.1.0",
|
|
120
|
-
"type-fest": "^4.
|
|
121
|
-
"typescript": "5.
|
|
131
|
+
"type-fest": "^4.35.0",
|
|
132
|
+
"typescript": "^5.7.3"
|
|
122
133
|
},
|
|
123
134
|
"prettier": {
|
|
124
135
|
"semi": false,
|
package/dist/utils/errors.d.ts
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|