@comapeo/core 1.0.0 → 2.0.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/dist/blob-store/index.d.ts +1 -1
- package/dist/config-import.d.ts.map +1 -1
- package/dist/core-manager/core-index.d.ts +1 -1
- package/dist/core-manager/core-index.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +1 -0
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/fastify-controller.d.ts.map +1 -1
- package/dist/fastify-plugins/{maps/index.d.ts → maps.d.ts} +8 -8
- package/dist/fastify-plugins/maps.d.ts.map +1 -0
- package/dist/fastify-plugins/utils.d.ts +4 -0
- package/dist/fastify-plugins/utils.d.ts.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/hashmap.d.ts +2 -2
- package/dist/lib/hashmap.d.ts.map +1 -1
- package/dist/lib/key-by.d.ts +15 -0
- package/dist/lib/key-by.d.ts.map +1 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -1
- package/dist/local-peers.d.ts +3 -2
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/logger.d.ts +12 -9
- package/dist/logger.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +9 -1
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +3 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/sync/core-sync-state.d.ts +10 -3
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/namespace-sync-state.d.ts +8 -12
- package/dist/sync/namespace-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/sync/sync-state.d.ts +7 -1
- package/dist/sync/sync-state.d.ts.map +1 -1
- package/dist/translation-api.d.ts +1 -3
- package/dist/translation-api.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +0 -13
- package/dist/utils.d.ts.map +1 -1
- package/package.json +14 -13
- package/src/core-manager/index.js +13 -10
- package/src/datastore/README.md +2 -2
- package/src/datatype/README.md +1 -1
- package/src/fastify-controller.js +7 -1
- package/src/fastify-plugins/maps.js +122 -0
- package/src/fastify-plugins/utils.js +6 -0
- package/src/index-writer/index.js +1 -1
- package/src/index.js +1 -3
- package/src/lib/hashmap.js +1 -1
- package/src/lib/key-by.js +24 -0
- package/src/local-peers.js +2 -1
- package/src/logger.js +52 -16
- package/src/mapeo-manager.js +36 -2
- package/src/mapeo-project.js +0 -2
- package/src/member-api.js +12 -5
- package/src/sync/core-sync-state.js +35 -7
- package/src/sync/namespace-sync-state.js +27 -25
- package/src/sync/peer-sync-controller.js +44 -37
- package/src/sync/sync-api.js +9 -6
- package/src/sync/sync-state.js +12 -1
- package/src/translation-api.js +1 -4
- package/src/types.ts +0 -1
- package/src/utils.js +0 -25
- package/dist/fastify-plugins/maps/index.d.ts.map +0 -1
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +0 -12
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +0 -1
- package/dist/fastify-plugins/maps/static-maps.d.ts +0 -11
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +0 -1
- package/src/fastify-plugins/maps/index.js +0 -173
- package/src/fastify-plugins/maps/offline-fallback-map.js +0 -114
- package/src/fastify-plugins/maps/static-maps.js +0 -271
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import fp from 'fastify-plugin'
|
|
2
|
-
import { Type as T } from '@sinclair/typebox'
|
|
3
|
-
import { fetch } from 'undici'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
NotFoundError,
|
|
7
|
-
createStyleJsonResponseHeaders,
|
|
8
|
-
getFastifyServerAddress,
|
|
9
|
-
} from '../utils.js'
|
|
10
|
-
import { PLUGIN_NAME as MAPEO_STATIC_MAPS } from './static-maps.js'
|
|
11
|
-
import { PLUGIN_NAME as MAPEO_OFFLINE_FALLBACK } from './offline-fallback-map.js'
|
|
12
|
-
|
|
13
|
-
export const PLUGIN_NAME = 'mapeo-maps'
|
|
14
|
-
export const DEFAULT_MAPBOX_STYLE_URL =
|
|
15
|
-
'https://api.mapbox.com/styles/v1/mapbox/outdoors-v12'
|
|
16
|
-
|
|
17
|
-
const MAP_PROVIDER_API_KEY_QUERY_PARAM_BY_HOSTNAME = new Map([
|
|
18
|
-
// Mapbox expects `access_token`: https://docs.mapbox.com/api/maps/styles/
|
|
19
|
-
['api.mapbox.com', 'access_token'],
|
|
20
|
-
// Protomaps expects `key` (no docs link yet)
|
|
21
|
-
['api.protomaps.com', 'key'],
|
|
22
|
-
// MapTiler expects `key`: https://docs.maptiler.com/cloud/api/maps/
|
|
23
|
-
['api.maptiler.com', 'key'],
|
|
24
|
-
// Stadia expects `api_key`: https://docs.stadiamaps.com/themes/
|
|
25
|
-
['tiles.stadiamaps.com', 'api_key'],
|
|
26
|
-
// ArcGIS expects `token`: https://developers.arcgis.com/documentation/mapping-apis-and-services/security/api-keys/
|
|
27
|
-
['basemapstyles-api.arcgis.com', 'token'],
|
|
28
|
-
])
|
|
29
|
-
|
|
30
|
-
export const plugin = fp(mapsPlugin, {
|
|
31
|
-
fastify: '4.x',
|
|
32
|
-
name: PLUGIN_NAME,
|
|
33
|
-
decorators: { fastify: ['mapeoStaticMaps', 'mapeoFallbackMap'] },
|
|
34
|
-
dependencies: [MAPEO_STATIC_MAPS, MAPEO_OFFLINE_FALLBACK],
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {object} MapsPluginOpts
|
|
39
|
-
* @property {string} [prefix]
|
|
40
|
-
* @property {string} [defaultOnlineStyleUrl]
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {object} MapsPluginContext
|
|
45
|
-
* @property {() => Promise<string>} getStyleJsonUrl
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/** @type {import('fastify').FastifyPluginAsync<MapsPluginOpts>} */
|
|
49
|
-
async function mapsPlugin(fastify, opts) {
|
|
50
|
-
fastify.decorate('mapeoMaps', {
|
|
51
|
-
async getStyleJsonUrl() {
|
|
52
|
-
const base = await getFastifyServerAddress(fastify.server, {
|
|
53
|
-
timeout: 5000,
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return new URL(`${opts.prefix || ''}/style.json`, base).href
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
fastify.register(routes, {
|
|
60
|
-
prefix: opts.prefix,
|
|
61
|
-
defaultOnlineStyleUrl: opts.defaultOnlineStyleUrl,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const GetStyleJsonQueryStringSchema = T.Object({
|
|
66
|
-
key: T.Optional(T.String()),
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
/** @type {import('fastify').FastifyPluginAsync<MapsPluginOpts, import('fastify').RawServerDefault, import('@fastify/type-provider-typebox').TypeBoxTypeProvider>} */
|
|
70
|
-
async function routes(fastify, opts) {
|
|
71
|
-
const { defaultOnlineStyleUrl = DEFAULT_MAPBOX_STYLE_URL } = opts
|
|
72
|
-
|
|
73
|
-
fastify.get(
|
|
74
|
-
'/style.json',
|
|
75
|
-
{ schema: { querystring: GetStyleJsonQueryStringSchema } },
|
|
76
|
-
async (req, rep) => {
|
|
77
|
-
const serverAddress = await getFastifyServerAddress(req.server.server)
|
|
78
|
-
|
|
79
|
-
// 1. Attempt to get "default" local static map's style.json
|
|
80
|
-
{
|
|
81
|
-
const styleId = 'default'
|
|
82
|
-
|
|
83
|
-
const results = await Promise.all([
|
|
84
|
-
fastify.mapeoStaticMaps.getStyleJsonStats(styleId),
|
|
85
|
-
fastify.mapeoStaticMaps.getResolvedStyleJson(styleId, serverAddress),
|
|
86
|
-
]).catch(() => {
|
|
87
|
-
fastify.log.warn('Cannot read default static map')
|
|
88
|
-
return null
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
if (results) {
|
|
92
|
-
const [stats, styleJson] = results
|
|
93
|
-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
|
|
94
|
-
return styleJson
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 2. Attempt to get a default style.json from online source
|
|
99
|
-
{
|
|
100
|
-
const { key } = req.query
|
|
101
|
-
|
|
102
|
-
const upstreamUrlObj = new URL(defaultOnlineStyleUrl)
|
|
103
|
-
const { hostname } = upstreamUrlObj
|
|
104
|
-
|
|
105
|
-
if (key) {
|
|
106
|
-
const paramToUpsert =
|
|
107
|
-
MAP_PROVIDER_API_KEY_QUERY_PARAM_BY_HOSTNAME.get(hostname)
|
|
108
|
-
|
|
109
|
-
if (paramToUpsert) {
|
|
110
|
-
// Note that even if the search param of interest already exists in the url
|
|
111
|
-
// it is overwritten by the key provided in the request's search params
|
|
112
|
-
upstreamUrlObj.searchParams.set(paramToUpsert, key)
|
|
113
|
-
} else {
|
|
114
|
-
fastify.log.warn(
|
|
115
|
-
`Provided API key will not be applied to unrecognized provider: ${hostname}`
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const upstreamResponse = await fetch(upstreamUrlObj.href, {
|
|
122
|
-
signal: AbortSignal.timeout(30_000),
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
if (upstreamResponse.ok) {
|
|
126
|
-
// Set up headers to forward
|
|
127
|
-
for (const [name, value] of upstreamResponse.headers) {
|
|
128
|
-
// Only forward headers related to caching
|
|
129
|
-
// https://www.rfc-editor.org/rfc/rfc9111#name-field-definitions
|
|
130
|
-
// e.g. usage from map renderer: https://github.com/maplibre/maplibre-gl-js/blob/26a7a6c2c142ef2e26db89f5fdf2338769494902/src/util/ajax.ts#L205
|
|
131
|
-
if (
|
|
132
|
-
['age', 'cache-control', 'expires'].includes(name.toLowerCase())
|
|
133
|
-
) {
|
|
134
|
-
rep.header(name, value)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// Some upstream providers will not set the 'application/json' content-type header despite the body being JSON e.g. Protomaps
|
|
138
|
-
// TODO: Should we forward the upstream 'content-type' header?
|
|
139
|
-
// We kind of assume that a Style Spec-compatible JSON payload will always be used by a provider
|
|
140
|
-
// Technically, there could be cases where a provider doesn't use the Mapbox Style Spec and has their own format,
|
|
141
|
-
// which may be delivered as some other content type
|
|
142
|
-
rep.header('content-type', 'application/json; charset=utf-8')
|
|
143
|
-
return upstreamResponse.json()
|
|
144
|
-
} else {
|
|
145
|
-
fastify.log.warn(
|
|
146
|
-
`Upstream style.json request returned non-2xx status: ${upstreamResponse.status} ${upstreamResponse.statusText}`
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
fastify.log.warn('Failed to make upstream style.json request', err)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 3. Provide offline fallback map's style.json
|
|
155
|
-
{
|
|
156
|
-
let results = null
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
results = await Promise.all([
|
|
160
|
-
fastify.mapeoFallbackMap.getStyleJsonStats(),
|
|
161
|
-
fastify.mapeoFallbackMap.getResolvedStyleJson(serverAddress),
|
|
162
|
-
])
|
|
163
|
-
} catch (err) {
|
|
164
|
-
throw new NotFoundError(`id = fallback, style.json`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const [stats, styleJson] = results
|
|
168
|
-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
|
|
169
|
-
return styleJson
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
)
|
|
173
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs/promises'
|
|
3
|
-
import FastifyStatic from '@fastify/static'
|
|
4
|
-
import fp from 'fastify-plugin'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
NotFoundError,
|
|
8
|
-
createStyleJsonResponseHeaders,
|
|
9
|
-
getFastifyServerAddress,
|
|
10
|
-
} from '../utils.js'
|
|
11
|
-
|
|
12
|
-
export const PLUGIN_NAME = 'mapeo-static-maps'
|
|
13
|
-
|
|
14
|
-
export const plugin = fp(offlineFallbackMapPlugin, {
|
|
15
|
-
fastify: '4.x',
|
|
16
|
-
name: PLUGIN_NAME,
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} OfflineFallbackMapPluginOpts
|
|
21
|
-
* @property {string} [prefix]
|
|
22
|
-
* @property {string} styleJsonPath
|
|
23
|
-
* @property {string} sourcesDir
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {object} FallbackMapPluginDecorator
|
|
28
|
-
* @property {(serverAddress: string) => Promise<any>} getResolvedStyleJson
|
|
29
|
-
* @property {() => Promise<import('node:fs').Stats>} getStyleJsonStats
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/** @type {import('fastify').FastifyPluginAsync<OfflineFallbackMapPluginOpts>} */
|
|
33
|
-
async function offlineFallbackMapPlugin(fastify, opts) {
|
|
34
|
-
const { styleJsonPath, sourcesDir } = opts
|
|
35
|
-
|
|
36
|
-
fastify.decorate(
|
|
37
|
-
'mapeoFallbackMap',
|
|
38
|
-
/** @type {FallbackMapPluginDecorator} */
|
|
39
|
-
({
|
|
40
|
-
async getResolvedStyleJson(serverAddress) {
|
|
41
|
-
const rawStyleJson = await fs.readFile(styleJsonPath, 'utf-8')
|
|
42
|
-
const styleJson = JSON.parse(rawStyleJson)
|
|
43
|
-
|
|
44
|
-
const sources = styleJson.sources || {}
|
|
45
|
-
|
|
46
|
-
const sourcesDirFiles = await fs.readdir(sourcesDir, {
|
|
47
|
-
withFileTypes: true,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
for (const file of sourcesDirFiles) {
|
|
51
|
-
if (!file.isFile()) continue
|
|
52
|
-
|
|
53
|
-
if (file.name === 'style.json') continue
|
|
54
|
-
|
|
55
|
-
const extension = path.extname(file.name).toLowerCase()
|
|
56
|
-
if (!(extension === '.json' || extension === '.geojson')) continue
|
|
57
|
-
|
|
58
|
-
const sourceName = path.basename(file.name, extension) + '-source'
|
|
59
|
-
|
|
60
|
-
sources[sourceName] = {
|
|
61
|
-
type: 'geojson',
|
|
62
|
-
data: new URL(`${opts.prefix || ''}/${file.name}`, serverAddress)
|
|
63
|
-
.href,
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
styleJson.sources = sources
|
|
68
|
-
|
|
69
|
-
return styleJson
|
|
70
|
-
},
|
|
71
|
-
getStyleJsonStats() {
|
|
72
|
-
return fs.stat(styleJsonPath)
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
fastify.register(routes, {
|
|
78
|
-
prefix: opts.prefix,
|
|
79
|
-
styleJsonPath: opts.styleJsonPath,
|
|
80
|
-
sourcesDir: opts.sourcesDir,
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** @type {import('fastify').FastifyPluginAsync<OfflineFallbackMapPluginOpts, import('fastify').RawServerDefault, import('@fastify/type-provider-typebox').TypeBoxTypeProvider>} */
|
|
85
|
-
async function routes(fastify, opts) {
|
|
86
|
-
const { sourcesDir } = opts
|
|
87
|
-
|
|
88
|
-
fastify.register(FastifyStatic, {
|
|
89
|
-
root: sourcesDir,
|
|
90
|
-
decorateReply: false,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
fastify.get('/style.json', async (req, rep) => {
|
|
94
|
-
const serverAddress = await getFastifyServerAddress(req.server.server)
|
|
95
|
-
|
|
96
|
-
let stats, styleJson
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const results = await Promise.all([
|
|
100
|
-
fastify.mapeoFallbackMap.getStyleJsonStats(),
|
|
101
|
-
fastify.mapeoFallbackMap.getResolvedStyleJson(serverAddress),
|
|
102
|
-
])
|
|
103
|
-
|
|
104
|
-
stats = results[0]
|
|
105
|
-
styleJson = results[1]
|
|
106
|
-
} catch (err) {
|
|
107
|
-
throw new NotFoundError(`id = fallback, style.json`)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
|
|
111
|
-
|
|
112
|
-
return styleJson
|
|
113
|
-
})
|
|
114
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs/promises'
|
|
3
|
-
import FastifyStatic from '@fastify/static'
|
|
4
|
-
import fp from 'fastify-plugin'
|
|
5
|
-
import { Type as T } from '@sinclair/typebox'
|
|
6
|
-
import asar from '@electron/asar'
|
|
7
|
-
import { Mime } from 'mime/lite'
|
|
8
|
-
import standardTypes from 'mime/types/standard.js'
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
NotFoundError,
|
|
12
|
-
createStyleJsonResponseHeaders,
|
|
13
|
-
getFastifyServerAddress,
|
|
14
|
-
} from '../utils.js'
|
|
15
|
-
|
|
16
|
-
export const PLUGIN_NAME = 'mapeo-static-maps'
|
|
17
|
-
|
|
18
|
-
export const plugin = fp(staticMapsPlugin, {
|
|
19
|
-
fastify: '4.x',
|
|
20
|
-
name: PLUGIN_NAME,
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {object} StaticMapsPluginOpts
|
|
25
|
-
* @property {string} [prefix]
|
|
26
|
-
* @property {string} staticRootDir
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @typedef {object} StaticMapsPluginDecorator
|
|
31
|
-
* @property {(styleId: string, serverAddress: string) => Promise<string>} getResolvedStyleJson
|
|
32
|
-
* @property {(styleId: string) => Promise<import('node:fs').Stats>} getStyleJsonStats
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/** @type {import('fastify').FastifyPluginAsync<StaticMapsPluginOpts>} */
|
|
36
|
-
async function staticMapsPlugin(fastify, opts) {
|
|
37
|
-
fastify.decorate('mapeoStaticMaps', {
|
|
38
|
-
async getResolvedStyleJson(styleId, serverAddress) {
|
|
39
|
-
const filePath = path.join(opts.staticRootDir, styleId, 'style.json')
|
|
40
|
-
|
|
41
|
-
const data = await fs.readFile(filePath, 'utf-8')
|
|
42
|
-
|
|
43
|
-
return data.replace(
|
|
44
|
-
/\{host\}/gm,
|
|
45
|
-
new URL(`${opts.prefix || ''}/${styleId}`, serverAddress).href
|
|
46
|
-
)
|
|
47
|
-
},
|
|
48
|
-
async getStyleJsonStats(styleId) {
|
|
49
|
-
const filePath = path.join(opts.staticRootDir, styleId, 'style.json')
|
|
50
|
-
const stats = await fs.stat(filePath)
|
|
51
|
-
return stats
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
fastify.register(routes, {
|
|
56
|
-
prefix: opts.prefix,
|
|
57
|
-
staticRootDir: opts.staticRootDir,
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const GetStaticMapTileParamsSchema = T.Object({
|
|
62
|
-
styleId: T.String(),
|
|
63
|
-
tileId: T.String(),
|
|
64
|
-
z: T.Number(),
|
|
65
|
-
y: T.Number(),
|
|
66
|
-
x: T.Number(),
|
|
67
|
-
ext: T.Optional(T.String()),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const ListStaticMapsReplySchema = T.Array(
|
|
71
|
-
T.Object({
|
|
72
|
-
id: T.String(),
|
|
73
|
-
name: T.Union([T.String(), T.Null()]),
|
|
74
|
-
styleUrl: T.String(),
|
|
75
|
-
})
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
const GetStyleJsonParamsSchema = T.Object({
|
|
79
|
-
styleId: T.String(),
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
/** @type {import('fastify').FastifyPluginAsync<StaticMapsPluginOpts, import('fastify').RawServerDefault, import('@fastify/type-provider-typebox').TypeBoxTypeProvider>} */
|
|
83
|
-
async function routes(fastify, opts) {
|
|
84
|
-
const { staticRootDir } = opts
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {import('fastify').FastifyRequest<{Params: import('@sinclair/typebox').Static<typeof GetStaticMapTileParamsSchema>}>} req
|
|
88
|
-
* @param {import('fastify').FastifyReply} rep
|
|
89
|
-
*/
|
|
90
|
-
async function handleStyleTileGet(req, rep) {
|
|
91
|
-
const result = getStyleTileInfo(staticRootDir, req.params)
|
|
92
|
-
|
|
93
|
-
if (!result) {
|
|
94
|
-
const { tileId, z, x, y, ext } = req.params
|
|
95
|
-
throw new NotFoundError(
|
|
96
|
-
`Tileset id = ${tileId}, ext=${ext}, [${z}, ${x}, ${y}]`
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const { data, mimeType } = result
|
|
101
|
-
|
|
102
|
-
if (mimeType) {
|
|
103
|
-
rep.header('Content-Type', mimeType)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
rep.send(data)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Serve static files
|
|
110
|
-
fastify.register(FastifyStatic, {
|
|
111
|
-
root: staticRootDir,
|
|
112
|
-
setHeaders: (res, path) => {
|
|
113
|
-
if (path.toLowerCase().endsWith('.pbf')) {
|
|
114
|
-
res.setHeader('Content-Type', 'application/x-protobuf')
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
/// List static maps
|
|
120
|
-
fastify.get(
|
|
121
|
-
'/',
|
|
122
|
-
{ schema: { response: { 200: ListStaticMapsReplySchema } } },
|
|
123
|
-
async (req) => {
|
|
124
|
-
const styleDirFiles = await fs.readdir(staticRootDir)
|
|
125
|
-
|
|
126
|
-
const serverAddress = await getFastifyServerAddress(req.server.server)
|
|
127
|
-
|
|
128
|
-
const result = (
|
|
129
|
-
await Promise.all(
|
|
130
|
-
styleDirFiles.map(async (filename) => {
|
|
131
|
-
const stat = await fs.stat(path.join(staticRootDir, filename))
|
|
132
|
-
if (!stat.isDirectory()) return null
|
|
133
|
-
|
|
134
|
-
let styleJson
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const styleJsonContent = await fs.readFile(
|
|
138
|
-
path.join(staticRootDir, filename, 'style.json'),
|
|
139
|
-
'utf-8'
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
styleJson = JSON.parse(styleJsonContent)
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
id: filename,
|
|
149
|
-
name: typeof styleJson.name === 'string' ? styleJson.name : null,
|
|
150
|
-
styleUrl: new URL(
|
|
151
|
-
`${req.server.prefix || ''}/${filename}/style.json`,
|
|
152
|
-
serverAddress
|
|
153
|
-
).href,
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
)
|
|
157
|
-
).filter(
|
|
158
|
-
/**
|
|
159
|
-
* @template {import('@sinclair/typebox').Static<typeof ListStaticMapsReplySchema>[number] | null} V
|
|
160
|
-
* @param {V} v
|
|
161
|
-
* @returns {v is NonNullable<V>}
|
|
162
|
-
*/
|
|
163
|
-
(v) => v !== null
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
/// Get a map's style.json
|
|
171
|
-
fastify.get(
|
|
172
|
-
`/:styleId/style.json`,
|
|
173
|
-
{ schema: { params: GetStyleJsonParamsSchema } },
|
|
174
|
-
async (req, rep) => {
|
|
175
|
-
const { styleId } = req.params
|
|
176
|
-
|
|
177
|
-
const serverAddress = await getFastifyServerAddress(req.server.server)
|
|
178
|
-
|
|
179
|
-
let stats, styleJson
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const results = await Promise.all([
|
|
183
|
-
fastify.mapeoStaticMaps.getStyleJsonStats(styleId),
|
|
184
|
-
fastify.mapeoStaticMaps.getResolvedStyleJson(styleId, serverAddress),
|
|
185
|
-
])
|
|
186
|
-
|
|
187
|
-
stats = results[0]
|
|
188
|
-
styleJson = results[1]
|
|
189
|
-
} catch (err) {
|
|
190
|
-
throw new NotFoundError(`id = ${styleId}, style.json`)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
|
|
194
|
-
|
|
195
|
-
return styleJson
|
|
196
|
-
}
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
// Get a tile (extension specified)
|
|
200
|
-
fastify.get(
|
|
201
|
-
`/:styleId/tiles/:tileId/:z/:x/:y.:ext`,
|
|
202
|
-
{ schema: { params: GetStaticMapTileParamsSchema } },
|
|
203
|
-
handleStyleTileGet
|
|
204
|
-
)
|
|
205
|
-
// Get a tile (extension not specified)
|
|
206
|
-
fastify.get(
|
|
207
|
-
`/:styleId/tiles/:tileId/:z/:x/:y`,
|
|
208
|
-
{ schema: { params: GetStaticMapTileParamsSchema } },
|
|
209
|
-
handleStyleTileGet
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @param {string} archive
|
|
215
|
-
* @param {string} filename
|
|
216
|
-
*/
|
|
217
|
-
function extractAsarFile(archive, filename) {
|
|
218
|
-
try {
|
|
219
|
-
return asar.extractFile(archive, filename)
|
|
220
|
-
} catch (err) {
|
|
221
|
-
return undefined
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const mime = new Mime(standardTypes, { 'application/x-protobuf': ['pbf'] })
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @param {string} baseDirectory
|
|
229
|
-
* @param {import('@sinclair/typebox').Static<typeof GetStaticMapTileParamsSchema>} params
|
|
230
|
-
* @returns {null | { data: Buffer, mimeType: string | null }}
|
|
231
|
-
*/
|
|
232
|
-
function getStyleTileInfo(baseDirectory, params) {
|
|
233
|
-
const { styleId, tileId, z, x, y } = params
|
|
234
|
-
let { ext } = params
|
|
235
|
-
|
|
236
|
-
// TODO: If necessary, need to flip the y value first if the TileJSON source is TMS scheme
|
|
237
|
-
// Doing this will depend on if we decide that the asar directory structure should only follow XYZ or if it should align with corresponding tilejson spec
|
|
238
|
-
|
|
239
|
-
const fileBasename = path.join(z.toString(), x.toString(), y.toString())
|
|
240
|
-
const asarPath = path.join(baseDirectory, styleId, 'tiles', tileId + '.asar')
|
|
241
|
-
|
|
242
|
-
/** @type {Buffer | undefined} */
|
|
243
|
-
let data
|
|
244
|
-
|
|
245
|
-
if (ext) {
|
|
246
|
-
data = extractAsarFile(asarPath, fileBasename + '.' + ext)
|
|
247
|
-
} else {
|
|
248
|
-
// Try common extensions
|
|
249
|
-
const extensions = ['png', 'jpg', 'jpeg']
|
|
250
|
-
|
|
251
|
-
for (const e of extensions) {
|
|
252
|
-
data = extractAsarFile(asarPath, fileBasename + '.' + e)
|
|
253
|
-
|
|
254
|
-
// Match found, use the corresponding extension moving forward
|
|
255
|
-
if (data) {
|
|
256
|
-
ext = e
|
|
257
|
-
break
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// extension check isn't fully necessary since the buffer will only exist if the extension exists
|
|
263
|
-
// but useful to check for types reasons
|
|
264
|
-
if (!data || !ext) {
|
|
265
|
-
return null
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const mimeType = mime.getType(ext)
|
|
269
|
-
|
|
270
|
-
return { data, mimeType }
|
|
271
|
-
}
|