styled-map-package 4.1.0 → 5.0.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +47 -7
  2. package/bin/smp-download.js +27 -151
  3. package/bin/smp-mbtiles.js +12 -7
  4. package/bin/smp-view.js +32 -54
  5. package/lib/commands/download.js +188 -0
  6. package/lib/commands/mbtiles.js +21 -0
  7. package/lib/commands/view.js +57 -0
  8. package/lib/reporters.js +92 -0
  9. package/package.json +9 -181
  10. package/LICENSE.md +0 -7
  11. package/dist/download.cjs +0 -101
  12. package/dist/download.d.cts +0 -65
  13. package/dist/download.d.ts +0 -65
  14. package/dist/download.js +0 -77
  15. package/dist/from-mbtiles.cjs +0 -91
  16. package/dist/from-mbtiles.d.cts +0 -17
  17. package/dist/from-mbtiles.d.ts +0 -17
  18. package/dist/from-mbtiles.js +0 -57
  19. package/dist/index.cjs +0 -49
  20. package/dist/index.d.cts +0 -27
  21. package/dist/index.d.ts +0 -27
  22. package/dist/index.js +0 -18
  23. package/dist/reader-watch.cjs +0 -135
  24. package/dist/reader-watch.d.cts +0 -24
  25. package/dist/reader-watch.d.ts +0 -24
  26. package/dist/reader-watch.js +0 -101
  27. package/dist/reader.cjs +0 -168
  28. package/dist/reader.d.cts +0 -62
  29. package/dist/reader.d.ts +0 -62
  30. package/dist/reader.js +0 -139
  31. package/dist/reporters.cjs +0 -122
  32. package/dist/reporters.d.cts +0 -10
  33. package/dist/reporters.d.ts +0 -10
  34. package/dist/reporters.js +0 -88
  35. package/dist/server.cjs +0 -78
  36. package/dist/server.d.cts +0 -48
  37. package/dist/server.d.ts +0 -48
  38. package/dist/server.js +0 -54
  39. package/dist/style-downloader.cjs +0 -312
  40. package/dist/style-downloader.d.cts +0 -120
  41. package/dist/style-downloader.d.ts +0 -120
  42. package/dist/style-downloader.js +0 -288
  43. package/dist/tile-downloader.cjs +0 -158
  44. package/dist/tile-downloader.d.cts +0 -84
  45. package/dist/tile-downloader.d.ts +0 -84
  46. package/dist/tile-downloader.js +0 -126
  47. package/dist/types-yLQy3AKR.d.cts +0 -192
  48. package/dist/types-yLQy3AKR.d.ts +0 -192
  49. package/dist/utils/errors.cjs +0 -41
  50. package/dist/utils/errors.d.cts +0 -18
  51. package/dist/utils/errors.d.ts +0 -18
  52. package/dist/utils/errors.js +0 -16
  53. package/dist/utils/fetch.cjs +0 -96
  54. package/dist/utils/fetch.d.cts +0 -51
  55. package/dist/utils/fetch.d.ts +0 -51
  56. package/dist/utils/fetch.js +0 -62
  57. package/dist/utils/file-formats.cjs +0 -98
  58. package/dist/utils/file-formats.d.cts +0 -35
  59. package/dist/utils/file-formats.d.ts +0 -35
  60. package/dist/utils/file-formats.js +0 -62
  61. package/dist/utils/geo.cjs +0 -84
  62. package/dist/utils/geo.d.cts +0 -46
  63. package/dist/utils/geo.d.ts +0 -46
  64. package/dist/utils/geo.js +0 -56
  65. package/dist/utils/mapbox.cjs +0 -121
  66. package/dist/utils/mapbox.d.cts +0 -43
  67. package/dist/utils/mapbox.d.ts +0 -43
  68. package/dist/utils/mapbox.js +0 -91
  69. package/dist/utils/misc.cjs +0 -39
  70. package/dist/utils/misc.d.cts +0 -22
  71. package/dist/utils/misc.d.ts +0 -22
  72. package/dist/utils/misc.js +0 -13
  73. package/dist/utils/streams.cjs +0 -130
  74. package/dist/utils/streams.d.cts +0 -73
  75. package/dist/utils/streams.d.ts +0 -73
  76. package/dist/utils/streams.js +0 -103
  77. package/dist/utils/style.cjs +0 -126
  78. package/dist/utils/style.d.cts +0 -69
  79. package/dist/utils/style.d.ts +0 -69
  80. package/dist/utils/style.js +0 -98
  81. package/dist/utils/templates.cjs +0 -118
  82. package/dist/utils/templates.d.cts +0 -80
  83. package/dist/utils/templates.d.ts +0 -80
  84. package/dist/utils/templates.js +0 -81
  85. package/dist/writer.cjs +0 -449
  86. package/dist/writer.d.cts +0 -7
  87. package/dist/writer.d.ts +0 -7
  88. package/dist/writer.js +0 -424
  89. package/node_modules/@node-rs/crc32/LICENSE +0 -21
  90. package/node_modules/@node-rs/crc32/README.md +0 -61
  91. package/node_modules/@node-rs/crc32/browser.js +0 -1
  92. package/node_modules/@node-rs/crc32/index.d.ts +0 -5
  93. package/node_modules/@node-rs/crc32/index.js +0 -368
  94. package/node_modules/@node-rs/crc32/package.json +0 -97
  95. package/node_modules/@node-rs/crc32-linux-x64-gnu/README.md +0 -3
  96. package/node_modules/@node-rs/crc32-linux-x64-gnu/crc32.linux-x64-gnu.node +0 -0
  97. package/node_modules/@node-rs/crc32-linux-x64-gnu/package.json +0 -43
  98. package/node_modules/@node-rs/crc32-linux-x64-musl/README.md +0 -3
  99. package/node_modules/@node-rs/crc32-linux-x64-musl/crc32.linux-x64-musl.node +0 -0
  100. package/node_modules/@node-rs/crc32-linux-x64-musl/package.json +0 -43
  101. package/node_modules/define-data-property/.eslintrc +0 -24
  102. package/node_modules/define-data-property/.github/FUNDING.yml +0 -12
  103. package/node_modules/define-data-property/.nycrc +0 -13
  104. package/node_modules/define-data-property/CHANGELOG.md +0 -70
  105. package/node_modules/define-data-property/LICENSE +0 -21
  106. package/node_modules/define-data-property/README.md +0 -67
  107. package/node_modules/define-data-property/index.d.ts +0 -12
  108. package/node_modules/define-data-property/index.js +0 -56
  109. package/node_modules/define-data-property/package.json +0 -106
  110. package/node_modules/define-data-property/test/index.js +0 -392
  111. package/node_modules/define-data-property/tsconfig.json +0 -59
  112. package/node_modules/define-properties/.editorconfig +0 -13
  113. package/node_modules/define-properties/.eslintrc +0 -19
  114. package/node_modules/define-properties/.github/FUNDING.yml +0 -12
  115. package/node_modules/define-properties/.nycrc +0 -9
  116. package/node_modules/define-properties/CHANGELOG.md +0 -91
  117. package/node_modules/define-properties/LICENSE +0 -21
  118. package/node_modules/define-properties/README.md +0 -84
  119. package/node_modules/define-properties/index.js +0 -47
  120. package/node_modules/define-properties/package.json +0 -88
  121. package/node_modules/es-define-property/.eslintrc +0 -13
  122. package/node_modules/es-define-property/.github/FUNDING.yml +0 -12
  123. package/node_modules/es-define-property/.nycrc +0 -9
  124. package/node_modules/es-define-property/CHANGELOG.md +0 -29
  125. package/node_modules/es-define-property/LICENSE +0 -21
  126. package/node_modules/es-define-property/README.md +0 -49
  127. package/node_modules/es-define-property/index.d.ts +0 -3
  128. package/node_modules/es-define-property/index.js +0 -14
  129. package/node_modules/es-define-property/package.json +0 -81
  130. package/node_modules/es-define-property/test/index.js +0 -56
  131. package/node_modules/es-define-property/tsconfig.json +0 -10
  132. package/node_modules/es-errors/.eslintrc +0 -5
  133. package/node_modules/es-errors/.github/FUNDING.yml +0 -12
  134. package/node_modules/es-errors/CHANGELOG.md +0 -40
  135. package/node_modules/es-errors/LICENSE +0 -21
  136. package/node_modules/es-errors/README.md +0 -55
  137. package/node_modules/es-errors/eval.d.ts +0 -3
  138. package/node_modules/es-errors/eval.js +0 -4
  139. package/node_modules/es-errors/index.d.ts +0 -3
  140. package/node_modules/es-errors/index.js +0 -4
  141. package/node_modules/es-errors/package.json +0 -80
  142. package/node_modules/es-errors/range.d.ts +0 -3
  143. package/node_modules/es-errors/range.js +0 -4
  144. package/node_modules/es-errors/ref.d.ts +0 -3
  145. package/node_modules/es-errors/ref.js +0 -4
  146. package/node_modules/es-errors/syntax.d.ts +0 -3
  147. package/node_modules/es-errors/syntax.js +0 -4
  148. package/node_modules/es-errors/test/index.js +0 -19
  149. package/node_modules/es-errors/tsconfig.json +0 -49
  150. package/node_modules/es-errors/type.d.ts +0 -3
  151. package/node_modules/es-errors/type.js +0 -4
  152. package/node_modules/es-errors/uri.d.ts +0 -3
  153. package/node_modules/es-errors/uri.js +0 -4
  154. package/node_modules/globalthis/.eslintrc +0 -18
  155. package/node_modules/globalthis/.nycrc +0 -10
  156. package/node_modules/globalthis/CHANGELOG.md +0 -109
  157. package/node_modules/globalthis/LICENSE +0 -21
  158. package/node_modules/globalthis/README.md +0 -70
  159. package/node_modules/globalthis/auto.js +0 -3
  160. package/node_modules/globalthis/implementation.browser.js +0 -11
  161. package/node_modules/globalthis/implementation.js +0 -3
  162. package/node_modules/globalthis/index.js +0 -19
  163. package/node_modules/globalthis/package.json +0 -99
  164. package/node_modules/globalthis/polyfill.js +0 -10
  165. package/node_modules/globalthis/shim.js +0 -29
  166. package/node_modules/globalthis/test/implementation.js +0 -11
  167. package/node_modules/globalthis/test/index.js +0 -11
  168. package/node_modules/globalthis/test/native.js +0 -26
  169. package/node_modules/globalthis/test/shimmed.js +0 -29
  170. package/node_modules/globalthis/test/tests.js +0 -36
  171. package/node_modules/gopd/.eslintrc +0 -16
  172. package/node_modules/gopd/.github/FUNDING.yml +0 -12
  173. package/node_modules/gopd/CHANGELOG.md +0 -45
  174. package/node_modules/gopd/LICENSE +0 -21
  175. package/node_modules/gopd/README.md +0 -40
  176. package/node_modules/gopd/gOPD.d.ts +0 -1
  177. package/node_modules/gopd/gOPD.js +0 -4
  178. package/node_modules/gopd/index.d.ts +0 -5
  179. package/node_modules/gopd/index.js +0 -15
  180. package/node_modules/gopd/package.json +0 -77
  181. package/node_modules/gopd/test/index.js +0 -36
  182. package/node_modules/gopd/tsconfig.json +0 -9
  183. package/node_modules/has-property-descriptors/.eslintrc +0 -13
  184. package/node_modules/has-property-descriptors/.github/FUNDING.yml +0 -12
  185. package/node_modules/has-property-descriptors/.nycrc +0 -9
  186. package/node_modules/has-property-descriptors/CHANGELOG.md +0 -35
  187. package/node_modules/has-property-descriptors/LICENSE +0 -21
  188. package/node_modules/has-property-descriptors/README.md +0 -43
  189. package/node_modules/has-property-descriptors/index.js +0 -22
  190. package/node_modules/has-property-descriptors/package.json +0 -77
  191. package/node_modules/has-property-descriptors/test/index.js +0 -57
  192. package/node_modules/is-it-type/License +0 -19
  193. package/node_modules/is-it-type/README.md +0 -102
  194. package/node_modules/is-it-type/changelog.md +0 -239
  195. package/node_modules/is-it-type/dist/cjs/is-it-type.js +0 -173
  196. package/node_modules/is-it-type/dist/cjs/is-it-type.js.map +0 -1
  197. package/node_modules/is-it-type/dist/cjs/is-it-type.min.js +0 -2
  198. package/node_modules/is-it-type/dist/cjs/is-it-type.min.js.map +0 -1
  199. package/node_modules/is-it-type/dist/esm/is-it-type.js +0 -143
  200. package/node_modules/is-it-type/dist/esm/is-it-type.js.map +0 -1
  201. package/node_modules/is-it-type/dist/esm/is-it-type.min.js +0 -2
  202. package/node_modules/is-it-type/dist/esm/is-it-type.min.js.map +0 -1
  203. package/node_modules/is-it-type/dist/esm/package.json +0 -3
  204. package/node_modules/is-it-type/dist/umd/is-it-type.js +0 -450
  205. package/node_modules/is-it-type/dist/umd/is-it-type.js.map +0 -1
  206. package/node_modules/is-it-type/dist/umd/is-it-type.min.js +0 -2
  207. package/node_modules/is-it-type/dist/umd/is-it-type.min.js.map +0 -1
  208. package/node_modules/is-it-type/es/index.js +0 -8
  209. package/node_modules/is-it-type/es/package.json +0 -3
  210. package/node_modules/is-it-type/index.js +0 -10
  211. package/node_modules/is-it-type/package.json +0 -87
  212. package/node_modules/is-it-type/src/index.js +0 -169
  213. package/node_modules/object-keys/.editorconfig +0 -13
  214. package/node_modules/object-keys/.eslintrc +0 -17
  215. package/node_modules/object-keys/.travis.yml +0 -277
  216. package/node_modules/object-keys/CHANGELOG.md +0 -232
  217. package/node_modules/object-keys/LICENSE +0 -21
  218. package/node_modules/object-keys/README.md +0 -76
  219. package/node_modules/object-keys/implementation.js +0 -122
  220. package/node_modules/object-keys/index.js +0 -32
  221. package/node_modules/object-keys/isArguments.js +0 -17
  222. package/node_modules/object-keys/package.json +0 -88
  223. package/node_modules/object-keys/test/index.js +0 -5
  224. package/node_modules/simple-invariant/License +0 -19
  225. package/node_modules/simple-invariant/README.md +0 -64
  226. package/node_modules/simple-invariant/changelog.md +0 -31
  227. package/node_modules/simple-invariant/index.js +0 -19
  228. package/node_modules/simple-invariant/package.json +0 -50
  229. package/node_modules/yauzl-promise/License +0 -19
  230. package/node_modules/yauzl-promise/README.md +0 -440
  231. package/node_modules/yauzl-promise/index.js +0 -10
  232. package/node_modules/yauzl-promise/lib/entry.js +0 -312
  233. package/node_modules/yauzl-promise/lib/index.js +0 -160
  234. package/node_modules/yauzl-promise/lib/reader.js +0 -289
  235. package/node_modules/yauzl-promise/lib/shared.js +0 -20
  236. package/node_modules/yauzl-promise/lib/utils.js +0 -105
  237. package/node_modules/yauzl-promise/lib/zip.js +0 -1224
  238. package/node_modules/yauzl-promise/package.json +0 -56
package/README.md CHANGED
@@ -1,18 +1,20 @@
1
- # Styled Map Package
1
+ # styled-map-package
2
2
 
3
- A Styled Map Package (`.smp`) file is a Zip archive containing all the resources needed to serve a Maplibre vector styled map offline. This includes the style JSON, vector and raster tiles, glyphs (fonts), the sprite image, and the sprite metadata.
3
+ CLI for creating, viewing, and converting Styled Map Package (`.smp`) files.
4
4
 
5
- ## Installation
5
+ An `.smp` file is a ZIP archive containing all the resources needed to serve a MapLibre vector styled map offline: style JSON, vector and raster tiles, glyphs (fonts), sprites, and metadata.
6
6
 
7
- Install globally to use the `smp` command.
7
+ ## Installation
8
8
 
9
9
  ```sh
10
10
  npm install --global styled-map-package
11
11
  ```
12
12
 
13
- ## Usage
13
+ ## Commands
14
+
15
+ ### `smp download`
14
16
 
15
- Download an online map to a styled map package file, specifying the bounding box (west, south, east, north) and max zoom level.
17
+ Download an online map style to a `.smp` file for offline use.
16
18
 
17
19
  ```sh
18
20
  smp download https://demotiles.maplibre.org/style.json \
@@ -21,8 +23,46 @@ smp download https://demotiles.maplibre.org/style.json \
21
23
  --output demotiles.smp
22
24
  ```
23
25
 
24
- Start a server and open in the default browser.
26
+ **Options:**
27
+
28
+ | Option | Description |
29
+ | ---------------------- | ------------------------------------------------ |
30
+ | `-o, --output <file>` | Output file (writes to stdout if omitted) |
31
+ | `-b, --bbox <w,s,e,n>` | Bounding box (west, south, east, north) |
32
+ | `-z, --zoom <number>` | Max zoom level (0-22) |
33
+ | `-t, --token <token>` | Mapbox access token (required for Mapbox styles) |
34
+
35
+ When run interactively, missing options are prompted for.
36
+
37
+ ### `smp view`
38
+
39
+ Preview a `.smp` file in a web browser.
25
40
 
26
41
  ```sh
27
42
  smp view demotiles.smp --open
28
43
  ```
44
+
45
+ **Options:**
46
+
47
+ | Option | Description |
48
+ | --------------------- | -------------------------------- |
49
+ | `-o, --open` | Open in the default web browser |
50
+ | `-p, --port <number>` | Port to serve on (default: 3000) |
51
+
52
+ ### `smp mbtiles`
53
+
54
+ Convert an MBTiles file to a `.smp` file.
55
+
56
+ ```sh
57
+ smp mbtiles tiles.mbtiles --output map.smp
58
+ ```
59
+
60
+ **Options:**
61
+
62
+ | Option | Description |
63
+ | --------------------- | ------------------------------------------------ |
64
+ | `-o, --output <file>` | Output `.smp` file (writes to stdout if omitted) |
65
+
66
+ ## License
67
+
68
+ MIT
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { input, number } from '@inquirer/prompts'
3
- import { Command, InvalidArgumentError } from 'commander'
3
+ import { Command } from 'commander'
4
4
  import fs from 'fs'
5
- import { pipeline } from 'stream/promises'
6
-
7
- import { download } from '../dist/download.js'
8
- import { ttyReporter } from '../dist/reporters.js'
9
- import { isMapboxURL, API_URL as MAPBOX_API_URL } from '../dist/utils/mapbox.js'
5
+ import { download } from 'styled-map-package-api/download'
6
+ import {
7
+ isMapboxURL,
8
+ API_URL as MAPBOX_API_URL,
9
+ } from 'styled-map-package-api/utils/mapbox'
10
+
11
+ import { Writable } from 'node:stream'
12
+
13
+ import {
14
+ parseBbox,
15
+ parseUrl,
16
+ parseZoom,
17
+ runDownload,
18
+ } from '../lib/commands/download.js'
19
+ import { ttyReporter } from '../lib/reporters.js'
10
20
 
11
21
  const program = new Command()
12
22
 
@@ -25,152 +35,18 @@ program
25
35
  )
26
36
  .argument('[styleUrl]', 'URL to style to download', parseUrl)
27
37
  .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
-
123
- const reporter = ttyReporter()
124
- const readStream = download({
125
- bbox,
126
- maxzoom: zoom,
127
- styleUrl,
128
- onprogress: (p) => reporter.write(p),
129
- accessToken: token,
38
+ await runDownload({ styleUrl, bbox, zoom, output, token }, {
39
+ download,
40
+ prompt: { input, number },
41
+ createOutputStream: (output) =>
42
+ output
43
+ ? Writable.toWeb(fs.createWriteStream(output))
44
+ : Writable.toWeb(process.stdout),
45
+ reporter: ttyReporter,
46
+ isMapboxURL,
47
+ mapboxApiUrl: MAPBOX_API_URL,
48
+ isTTY: !!process.stdout.isTTY,
130
49
  })
131
- const outputStream = output ? fs.createWriteStream(output) : process.stdout
132
- await pipeline(readStream, outputStream)
133
50
  })
134
51
 
135
52
  program.parseAsync(process.argv)
136
-
137
- /** @param {string} z */
138
- function parseZoom(z) {
139
- const zoom = parseInt(z)
140
- if (isNaN(zoom) || zoom < 0 || zoom > 22) {
141
- throw new InvalidArgumentError(
142
- 'Zoom must be a whole number (integer) between 0 and 22.',
143
- )
144
- }
145
- return zoom
146
- }
147
-
148
- /** @param {string} bbox */
149
- function parseBbox(bbox) {
150
- const bounds = bbox.split(',').map((s) => parseFloat(s.trim()))
151
- if (bounds.length !== 4) {
152
- throw new InvalidArgumentError(
153
- 'Bounding box must have 4 values separated by commas.',
154
- )
155
- }
156
- if (bounds.some(isNaN)) {
157
- throw new InvalidArgumentError('Bounding box values must be numbers.')
158
- }
159
- return /** @type {[number, number, number, number]} */ (bounds)
160
- }
161
-
162
- /** @param {string} url */
163
- function parseUrl(url) {
164
- try {
165
- return new URL(url).toString()
166
- } catch (e) {
167
- const message =
168
- e !== null &&
169
- typeof e === 'object' &&
170
- 'message' in e &&
171
- typeof e.message === 'string'
172
- ? e.message
173
- : 'Invalid URL'
174
- throw new InvalidArgumentError(message)
175
- }
176
- }
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
- import { pipeline } from 'stream/promises'
3
+ import { fromMBTiles } from 'styled-map-package-api/from-mbtiles'
4
4
 
5
- import { fromMBTiles } from '../dist/from-mbtiles.js'
5
+ import fs from 'node:fs'
6
+ import { Writable } from 'node:stream'
7
+
8
+ import { runMbtiles } from '../lib/commands/mbtiles.js'
6
9
 
7
10
  const program = new Command()
8
11
 
@@ -11,11 +14,13 @@ program
11
14
  .option('-o, --output <file>', 'output smp file')
12
15
  .argument('<mbtiles>', 'MBTiles file to convert')
13
16
  .action(async (mbtilesPath, { output }) => {
14
- if (output) {
15
- await fromMBTiles(mbtilesPath, output)
16
- } else {
17
- await pipeline(fromMBTiles(mbtilesPath), process.stdout)
18
- }
17
+ await runMbtiles({ mbtilesPath, output }, {
18
+ fromMBTiles,
19
+ createOutputStream: (output) =>
20
+ output
21
+ ? Writable.toWeb(fs.createWriteStream(output))
22
+ : Writable.toWeb(process.stdout),
23
+ })
19
24
  })
20
25
 
21
26
  program.parseAsync(process.argv)
package/bin/smp-view.js CHANGED
@@ -1,73 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServerAdapter } from '@whatwg-node/server'
3
3
  import { Command } from 'commander'
4
- import fsPromises from 'fs/promises'
5
4
  import http from 'http'
6
- import { AutoRouter } from 'itty-router'
7
5
  import openApp from 'open'
6
+ import { Reader } from 'styled-map-package-api/reader'
7
+ import { createServer } from 'styled-map-package-api/server'
8
8
 
9
+ import fsPromises from 'node:fs/promises'
9
10
  import path from 'node:path'
10
11
 
11
- import { Reader } from '../dist/reader.js'
12
- import { createServer } from '../dist/server.js'
12
+ import { runView } from '../lib/commands/view.js'
13
13
 
14
14
  const program = new Command()
15
15
 
16
16
  program
17
17
  .description('Preview a styled map package in a web browser')
18
18
  .option('-o, --open', 'open in the default web browser')
19
- .option('-p, --port <number>', 'port to serve on', parseInt, 3000)
19
+ .option('-p, --port <number>', 'port to serve on', (v) => parseInt(v), 3000)
20
20
  .argument('<file>', 'file to serve')
21
21
  .action(async (filepath, { open, port }) => {
22
- const address = await serve({ port, filepath })
23
- console.log(`server listening on ${address}`)
24
- if (open) {
25
- await openApp(address)
26
- }
22
+ await runView(
23
+ { port, filepath: path.relative(process.cwd(), filepath), open },
24
+ {
25
+ Reader,
26
+ createServer,
27
+ openApp,
28
+ log: (msg) => console.log(msg),
29
+ readViewerHtml: (filepath) => fsPromises.readFile(filepath),
30
+ listen: (port, handler) =>
31
+ new Promise((resolve, reject) => {
32
+ const server = http.createServer(
33
+ createServerAdapter(handler),
34
+ )
35
+ server.listen(port, '127.0.0.1', () => {
36
+ const address = server.address()
37
+ if (typeof address === 'string') {
38
+ resolve(`http://${address}`)
39
+ } else if (address === null) {
40
+ reject(new Error('Failed to get server address'))
41
+ } else {
42
+ resolve(`http://${address.address}:${address.port}`)
43
+ }
44
+ })
45
+ server.on('error', reject)
46
+ }),
47
+ },
48
+ )
27
49
  })
28
50
 
29
51
  program.parseAsync(process.argv)
30
-
31
- /**
32
- * Serve a styled map package on the given port (defaults to 3000).
33
- *
34
- * @param {object} opts
35
- * @param {number} [opts.port]
36
- * @param {string} opts.filepath
37
- * @returns
38
- */
39
- async function serve({ port = 3000, filepath }) {
40
- const reader = new Reader(path.relative(process.cwd(), filepath))
41
- const smpServer = createServer({ base: '/map' })
42
-
43
- const router = AutoRouter()
44
- router.get('/', async () => {
45
- const index = await fsPromises.readFile(
46
- new URL('../map-viewer/index.html', import.meta.url),
47
- )
48
- return new Response(new Uint8Array(index), {
49
- headers: {
50
- 'Content-Type': 'text/html',
51
- 'Content-Length': String(index.byteLength),
52
- 'Cache-Control': 'public, max-age=0',
53
- },
54
- })
55
- })
56
- router.all('/map/*', (request) => {
57
- return smpServer.fetch(request, reader)
58
- })
59
- const server = http.createServer(createServerAdapter(router.fetch))
60
- return new Promise((resolve, reject) => {
61
- server.listen(port, '127.0.0.1', () => {
62
- const address = server.address()
63
- if (typeof address === 'string') {
64
- resolve(`http://${address}`)
65
- } else if (address === null) {
66
- reject(new Error('Failed to get server address'))
67
- } else {
68
- resolve(`http://${address.address}:${address.port}`)
69
- }
70
- })
71
- server.on('error', reject)
72
- })
73
- }
@@ -0,0 +1,188 @@
1
+ import { InvalidArgumentError } from 'commander'
2
+
3
+ /**
4
+ * @param {string} z
5
+ * @returns {number}
6
+ */
7
+ export function parseZoom(z) {
8
+ const zoom = parseInt(z)
9
+ if (isNaN(zoom) || zoom < 0 || zoom > 22) {
10
+ throw new InvalidArgumentError(
11
+ 'Zoom must be a whole number (integer) between 0 and 22.',
12
+ )
13
+ }
14
+ return zoom
15
+ }
16
+
17
+ /**
18
+ * @param {string} bbox
19
+ * @returns {[number, number, number, number]}
20
+ */
21
+ export function parseBbox(bbox) {
22
+ const bounds = bbox.split(',').map((s) => parseFloat(s.trim()))
23
+ if (bounds.length !== 4) {
24
+ throw new InvalidArgumentError(
25
+ 'Bounding box must have 4 values separated by commas.',
26
+ )
27
+ }
28
+ if (bounds.some(isNaN)) {
29
+ throw new InvalidArgumentError('Bounding box values must be numbers.')
30
+ }
31
+ return /** @type {[number, number, number, number]} */ (bounds)
32
+ }
33
+
34
+ /**
35
+ * @param {string} url
36
+ * @returns {string}
37
+ */
38
+ export function parseUrl(url) {
39
+ try {
40
+ return new URL(url).toString()
41
+ } catch (e) {
42
+ const message =
43
+ e !== null &&
44
+ typeof e === 'object' &&
45
+ 'message' in e &&
46
+ typeof e.message === 'string'
47
+ ? e.message
48
+ : 'Invalid URL'
49
+ throw new InvalidArgumentError(message)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * @typedef {object} DownloadOptions
55
+ * @property {string | undefined} styleUrl
56
+ * @property {[number, number, number, number] | undefined} bbox
57
+ * @property {number | undefined} zoom
58
+ * @property {string | undefined} output
59
+ * @property {string | undefined} token
60
+ */
61
+
62
+ /**
63
+ * @typedef {object} DownloadDeps
64
+ * @property {(opts: any) => ReadableStream} download
65
+ * @property {{ input: (opts: any) => Promise<string>, number: (opts: any) => Promise<number | undefined> }} prompt
66
+ * @property {(output: string | undefined) => WritableStream | import('node:fs').WriteStream} createOutputStream
67
+ * @property {() => { write: (p: any) => void }} reporter
68
+ * @property {(url: string) => boolean} isMapboxURL
69
+ * @property {string} mapboxApiUrl
70
+ * @property {boolean} isTTY
71
+ */
72
+
73
+ /**
74
+ * @param {DownloadOptions} options
75
+ * @param {DownloadDeps} deps
76
+ */
77
+ export async function runDownload(
78
+ { styleUrl, bbox, zoom, output, token },
79
+ deps,
80
+ ) {
81
+ const { download, prompt, isMapboxURL, mapboxApiUrl, isTTY } = deps
82
+
83
+ const promptOutput =
84
+ !output && isTTY && (!styleUrl || !bbox || zoom === undefined)
85
+
86
+ if (!styleUrl) {
87
+ styleUrl = await prompt.input({
88
+ message: 'Style URL to download',
89
+ required: true,
90
+ validate: (/** @type {string} */ value) => {
91
+ try {
92
+ new URL(value)
93
+ return true
94
+ } catch {
95
+ return 'Please enter a valid URL.'
96
+ }
97
+ },
98
+ })
99
+ }
100
+
101
+ if (!bbox) {
102
+ const west = await prompt.number({
103
+ message: 'Bounding box west',
104
+ required: true,
105
+ step: 'any',
106
+ min: -180,
107
+ max: 180,
108
+ })
109
+ const south = await prompt.number({
110
+ message: 'Bounding box south',
111
+ required: true,
112
+ step: 'any',
113
+ min: -90,
114
+ max: 90,
115
+ })
116
+ const east = await prompt.number({
117
+ message: 'Bounding box east',
118
+ required: true,
119
+ step: 'any',
120
+ min: -180,
121
+ max: 180,
122
+ })
123
+ const north = await prompt.number({
124
+ message: 'Bounding box north',
125
+ required: true,
126
+ step: 'any',
127
+ min: -90,
128
+ max: 90,
129
+ })
130
+ if (
131
+ west === undefined ||
132
+ south === undefined ||
133
+ east === undefined ||
134
+ north === undefined
135
+ ) {
136
+ throw new InvalidArgumentError('Bounding box values are required.')
137
+ }
138
+ bbox = [west, south, east, north]
139
+ }
140
+
141
+ if (zoom === undefined) {
142
+ zoom = await prompt.number({
143
+ message: 'Max zoom level to download',
144
+ required: true,
145
+ min: 0,
146
+ max: 22,
147
+ })
148
+ if (zoom === undefined) {
149
+ throw new InvalidArgumentError('Zoom level is required.')
150
+ }
151
+ }
152
+
153
+ if ((isMapboxURL(styleUrl) || styleUrl.startsWith(mapboxApiUrl)) && !token) {
154
+ token = await prompt.input({
155
+ message: 'Mapbox access token',
156
+ required: true,
157
+ })
158
+ }
159
+
160
+ if (promptOutput) {
161
+ output = await prompt.input({
162
+ message: 'Output filename (.smp extension will be added)',
163
+ required: true,
164
+ transformer: (/** @type {string} */ value) =>
165
+ value.endsWith('.smp') ? value : `${value}.smp`,
166
+ })
167
+ }
168
+
169
+ if (output && !output.endsWith('.smp')) {
170
+ output += '.smp'
171
+ }
172
+
173
+ const reporter = deps.reporter()
174
+ const readStream = download({
175
+ bbox,
176
+ maxzoom: zoom,
177
+ styleUrl,
178
+ onprogress: (/** @type {any} */ p) => reporter.write(p),
179
+ accessToken: token,
180
+ })
181
+ const outputStream = deps.createOutputStream(output)
182
+ await readStream.pipeTo(
183
+ outputStream instanceof WritableStream
184
+ ? outputStream
185
+ : // @ts-ignore - Writable.toWeb compatibility
186
+ outputStream,
187
+ )
188
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @typedef {object} MbtilesOptions
3
+ * @property {string} mbtilesPath
4
+ * @property {string | undefined} output
5
+ */
6
+
7
+ /**
8
+ * @typedef {object} MbtilesDeps
9
+ * @property {(path: string) => ReadableStream} fromMBTiles
10
+ * @property {(output: string | undefined) => WritableStream} createOutputStream
11
+ */
12
+
13
+ /**
14
+ * @param {MbtilesOptions} options
15
+ * @param {MbtilesDeps} deps
16
+ */
17
+ export async function runMbtiles({ mbtilesPath, output }, deps) {
18
+ const readStream = deps.fromMBTiles(mbtilesPath)
19
+ const outputStream = deps.createOutputStream(output)
20
+ await readStream.pipeTo(outputStream)
21
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @typedef {object} ViewOptions
3
+ * @property {number} port
4
+ * @property {string} filepath
5
+ * @property {boolean} [open]
6
+ */
7
+
8
+ /**
9
+ * @typedef {object} ViewDeps
10
+ * @property {new (filepath: string) => any} Reader
11
+ * @property {(opts: any) => { fetch: (req: any, reader: any) => any }} createServer
12
+ * @property {(port: number, handler: any) => Promise<import('node:http').Server>} listen
13
+ * @property {(url: string) => Promise<void>} openApp
14
+ * @property {(url: string) => void} log
15
+ * @property {(url: string) => Promise<Uint8Array>} readViewerHtml
16
+ */
17
+
18
+ /**
19
+ * @param {ViewOptions} options
20
+ * @param {ViewDeps} deps
21
+ * @returns {Promise<string>} The address the server is listening on
22
+ */
23
+ export async function runView({ port, filepath, open }, deps) {
24
+ const { Reader, createServer, openApp, log, readViewerHtml } = deps
25
+
26
+ const reader = new Reader(filepath)
27
+ const smpServer = createServer({ base: '/map' })
28
+
29
+ /** @param {Request} request */
30
+ const handler = async (request) => {
31
+ const url = new URL(request.url)
32
+ if (url.pathname === '/') {
33
+ const index = await readViewerHtml(
34
+ new URL('../../map-viewer/index.html', import.meta.url).pathname,
35
+ )
36
+ return new Response(index, {
37
+ headers: {
38
+ 'Content-Type': 'text/html',
39
+ 'Content-Length': String(index.byteLength),
40
+ 'Cache-Control': 'public, max-age=0',
41
+ },
42
+ })
43
+ }
44
+ if (url.pathname.startsWith('/map/')) {
45
+ return smpServer.fetch(request, reader)
46
+ }
47
+ return new Response('Not found', { status: 404 })
48
+ }
49
+
50
+ const address = await deps.listen(port, handler)
51
+
52
+ log(`server listening on ${address}`)
53
+ if (open) {
54
+ await openApp(address)
55
+ }
56
+ return address
57
+ }