styled-map-package 2.0.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.
@@ -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 [file]', 'output file (if omitted, writes to stdout)')
14
- .requiredOption(
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
- .requiredOption(
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('<styleUrl>', 'URL to style to download', parseUrl)
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>;
@@ -5,5 +5,6 @@ export { default as Server } from "./server.js";
5
5
  export { default as StyleDownloader } from "./style-downloader.js";
6
6
  export { downloadTiles } from "./tile-downloader.js";
7
7
  export { default as download } from "./download.js";
8
+ export { default as fromMBTiles } from "./from-mbtiles.js";
8
9
  export type SMPSource = import("./types.js").SMPSource;
9
10
  export type SMPStyle = import("./types.js").SMPStyle;
@@ -5,7 +5,7 @@ export default class ReaderWatch implements Pick<Reader, keyof Reader> {
5
5
  */
6
6
  constructor(filepath: string);
7
7
  opened(): Promise<void>;
8
- getStyle(baseUrl?: string | null | undefined): Promise<import("./types.js").SMPStyle>;
8
+ getStyle(baseUrl?: string | null): Promise<import("./types.js").SMPStyle>;
9
9
  getResource(path: string): Promise<Resource>;
10
10
  close(): Promise<void>;
11
11
  #private;
@@ -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 | undefined): Promise<import("./types.js").SMPStyle>;
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,17 +1,11 @@
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
2
  export type PluginOptionsFilepath = {
3
- /**
4
- * Path to styled map package (`.smp`) file
5
- */
6
3
  /**
7
4
  * Path to styled map package (`.smp`) file
8
5
  */
9
6
  filepath: string;
10
7
  };
11
8
  export type PluginOptionsReader = {
12
- /**
13
- * SMP Reader interface (also supports ReaderWatch)
14
- */
15
9
  /**
16
10
  * SMP Reader interface (also supports ReaderWatch)
17
11
  */
@@ -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
- } | undefined);
27
+ });
28
28
  /**
29
29
  * Number of active downloads.
30
30
  */
@@ -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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined, { devicePixelRatio, supportsWebp }?: {
31
+ export function normalizeTileURL(tileURL: any, sourceURL: string, tileSize?: 256 | 512, { devicePixelRatio, supportsWebp }?: {
32
32
  devicePixelRatio?: number;
33
33
  supportsWebp?: boolean;
34
- } | undefined): any;
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
- } | undefined): import("stream").Readable;
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 | undefined);
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
@@ -8,3 +8,4 @@ export { default as Server } from './server.js'
8
8
  export { default as StyleDownloader } from './style-downloader.js'
9
9
  export { downloadTiles } from './tile-downloader.js'
10
10
  export { default as download } from './download.js'
11
+ export { default as fromMBTiles } from './from-mbtiles.js'
@@ -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": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -34,6 +34,10 @@
34
34
  "./download": {
35
35
  "types": "./dist/download.d.ts",
36
36
  "import": "./lib/download.js"
37
+ },
38
+ "./from-mbtiles": {
39
+ "types": "./dist/from-mbtiles.d.ts",
40
+ "import": "./lib/from-mbtiles.js"
37
41
  }
38
42
  },
39
43
  "main": "./lib/index.js",
@@ -61,68 +65,71 @@
61
65
  "author": "",
62
66
  "license": "MIT",
63
67
  "dependencies": {
68
+ "@commander-js/extra-typings": "^12.1.0",
64
69
  "@fastify/static": "^7.0.4",
70
+ "@inquirer/prompts": "^6.0.1",
65
71
  "@mapbox/sphericalmercator": "^1.2.0",
66
72
  "@maplibre/maplibre-gl-style-spec": "^20.3.1",
67
73
  "@placemarkio/check-geojson": "^0.1.12",
68
- "@turf/bbox": "^7.1.0",
69
- "@turf/helpers": "^7.1.0",
74
+ "@turf/bbox": "^7.2.0",
75
+ "@turf/helpers": "^7.2.0",
70
76
  "ansi-diff": "^1.2.0",
71
77
  "archiver": "^7.0.1",
72
78
  "buffer-peek-stream": "^1.1.0",
73
- "chalk": "^5.3.0",
79
+ "chalk": "^5.4.1",
74
80
  "commander": "^12.1.0",
75
81
  "fastify": "^4.28.1",
76
82
  "filter-obj": "^6.1.0",
77
83
  "http-errors": "^2.0.0",
78
84
  "into-stream": "^8.0.1",
79
85
  "is-stream": "^4.0.1",
80
- "ky": "^1.7.1",
86
+ "ky": "^1.7.5",
81
87
  "log-symbols": "^7.0.0",
82
88
  "map-obj": "^5.0.2",
89
+ "mbtiles-reader": "^1.0.0",
83
90
  "open": "^10.1.0",
84
- "ora": "^8.1.0",
91
+ "ora": "^8.2.0",
85
92
  "p-event": "^6.0.1",
86
- "p-limit": "^6.1.0",
93
+ "p-limit": "^6.2.0",
87
94
  "pretty-bytes": "^6.1.1",
88
- "pretty-ms": "^9.1.0",
89
- "readable-stream": "^4.5.2",
95
+ "pretty-ms": "^9.2.0",
96
+ "readable-stream": "^4.7.0",
90
97
  "temp-dir": "^3.0.0",
91
98
  "yauzl-promise": "^4.0.0",
92
99
  "yocto-queue": "^1.1.1"
93
100
  },
94
101
  "devDependencies": {
95
- "@eslint/js": "^9.9.1",
102
+ "@eslint/js": "^9.21.0",
96
103
  "@jsquash/jpeg": "^1.4.0",
97
104
  "@jsquash/png": "^3.0.1",
98
105
  "@stealthybox/jpg-stream": "^1.1.2",
99
106
  "@trivago/prettier-plugin-sort-imports": "^4.3.0",
100
- "@types/archiver": "^6.0.2",
107
+ "@types/archiver": "^6.0.3",
101
108
  "@types/eslint": "^9.6.1",
102
109
  "@types/eslint__js": "^8.42.3",
103
- "@types/geojson": "^7946.0.14",
110
+ "@types/geojson": "^7946.0.16",
104
111
  "@types/http-errors": "^2.0.4",
105
112
  "@types/mapbox__sphericalmercator": "^1.2.3",
106
113
  "@types/node": "^20.16.3",
107
- "@types/readable-stream": "^4.0.15",
114
+ "@types/readable-stream": "^4.0.18",
108
115
  "@types/yauzl-promise": "^4.0.1",
109
- "ava": "^6.1.3",
116
+ "ava": "^6.2.0",
110
117
  "block-stream2": "^2.1.0",
111
- "eslint": "^9.9.1",
118
+ "eslint": "^9.21.0",
112
119
  "execa": "^9.4.0",
113
120
  "globals": "^15.9.0",
114
- "husky": "^9.1.5",
121
+ "husky": "^9.1.7",
115
122
  "jpg-stream": "^1.1.2",
116
- "lint-staged": "^15.2.10",
123
+ "lint-staged": "^15.4.3",
117
124
  "pixel-stream": "^1.0.3",
118
- "playwright": "^1.46.1",
125
+ "playwright": "^1.50.1",
119
126
  "png-stream": "^1.0.5",
120
- "prettier": "^3.3.3",
127
+ "prettier": "^3.5.2",
121
128
  "random-bytes-readable-stream": "^3.0.0",
122
129
  "rimraf": "^4.4.1",
123
130
  "tempy": "^3.1.0",
124
- "type-fest": "^4.26.0",
125
- "typescript": "5.5.4"
131
+ "type-fest": "^4.35.0",
132
+ "typescript": "^5.7.3"
126
133
  },
127
134
  "prettier": {
128
135
  "semi": false,
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