Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support serving Style Map Package maps #881

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,495 changes: 1,448 additions & 47 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"sodium-universal": "^4.0.0",
"start-stop-state-machine": "^1.2.0",
"streamx": "^2.19.0",
"styled-map-package": "^1.0.1",
"sub-encoder": "^2.1.1",
"throttle-debounce": "^5.0.0",
"tiny-typed-emitter": "^2.1.0",
Expand Down
37 changes: 31 additions & 6 deletions src/fastify-plugins/maps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../utils.js'
import { PLUGIN_NAME as MAPEO_STATIC_MAPS } from './static-maps.js'
import { PLUGIN_NAME as MAPEO_OFFLINE_FALLBACK } from './offline-fallback-map.js'
import { PLUGIN_NAME as COMAPEO_STYLED_MAP_PACKAGE } from './styled-map-package.js'

export const PLUGIN_NAME = 'mapeo-maps'
export const DEFAULT_MAPBOX_STYLE_URL =
Expand All @@ -31,7 +32,11 @@ export const plugin = fp(mapsPlugin, {
fastify: '4.x',
name: PLUGIN_NAME,
decorators: { fastify: ['mapeoStaticMaps', 'mapeoFallbackMap'] },
dependencies: [MAPEO_STATIC_MAPS, MAPEO_OFFLINE_FALLBACK],
dependencies: [
COMAPEO_STYLED_MAP_PACKAGE,
MAPEO_STATIC_MAPS,
MAPEO_OFFLINE_FALLBACK,
],
})

/**
Expand Down Expand Up @@ -76,7 +81,19 @@ async function routes(fastify, opts) {
async (req, rep) => {
const serverAddress = await getFastifyServerAddress(req.server.server)

// 1. Attempt to get "default" local static map's style.json
// 1. Attempt to use the styled map package
{
const style = await fastify.comapeoSmp.getStyle().catch(() => {
fastify.log.warn('Cannot read styled map package archive')
return null
})

if (style) {
return style
}
}
Comment on lines +84 to +94
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the assumption that we want to give highest precedence to an SMP if possible


// 2. Attempt to get "default" local static map's style.json
{
const styleId = 'default'

Expand All @@ -90,12 +107,16 @@ async function routes(fastify, opts) {

if (results) {
const [stats, styleJson] = results
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
rep.headers(
createStyleJsonResponseHeaders({
'Last-Modified': stats.mtime.toUTCString(),
})
)
return styleJson
}
}

// 2. Attempt to get a default style.json from online source
// 3. Attempt to get a default style.json from online source
{
const { key } = req.query

Expand Down Expand Up @@ -151,7 +172,7 @@ async function routes(fastify, opts) {
}
}

// 3. Provide offline fallback map's style.json
// 4. Provide offline fallback map's style.json
{
let results = null

Expand All @@ -165,7 +186,11 @@ async function routes(fastify, opts) {
}

const [stats, styleJson] = results
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
rep.headers(
createStyleJsonResponseHeaders({
'Last-Modified': stats.mtime.toUTCString(),
})
)
return styleJson
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/fastify-plugins/maps/offline-fallback-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ async function routes(fastify, opts) {
throw new NotFoundError(`id = fallback, style.json`)
}

rep.headers(createStyleJsonResponseHeaders(stats.mtime))
rep.headers(
createStyleJsonResponseHeaders({
'Last-Modified': stats.mtime.toUTCString(),
})
)

return styleJson
})
Expand Down
6 changes: 5 additions & 1 deletion src/fastify-plugins/maps/static-maps.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,11 @@ async function routes(fastify, opts) {
throw new NotFoundError(`id = ${styleId}, style.json`)
}

rep.headers(createStyleJsonResponseHeaders(stats.mtime))
rep.headers(
createStyleJsonResponseHeaders({
'Last-Modified': stats.mtime.toUTCString(),
})
)

return styleJson
}
Expand Down
113 changes: 113 additions & 0 deletions src/fastify-plugins/maps/styled-map-package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import fp from 'fastify-plugin'
// @ts-expect-error Need to publish types for module
import { Reader } from 'styled-map-package'

/** @import {FastifyPluginAsync} from 'fastify' */
// @ts-expect-error Need to publish types for module
/** @import {Resource} from 'styled-map-package/reader' */
// @ts-expect-error Need to publish types for module
/** @import {SMPStyle} from 'styled-map-package/types' */
Comment on lines +2 to +9
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that these ignores are allowing tests to run without immediately failing


export const PLUGIN_NAME = 'comapeo-styled-map-package'

/**
* @typedef {object} StyledMapPackagePluginOpts
* @property {string} filepath
* @property {boolean} [lazy]
* @property {string} [prefix]
*
*/

/**
* @typedef {object} StyledMapPackagePluginDecorator
* @property {(baseUrl?: string) => Promise<SMPStyle>} getStyle
* @property {(path: string) => Promise<Resource>} getResource
*/

export const plugin = fp(styledMapPackagePlugin, {
fastify: '4.x',
name: PLUGIN_NAME,
})

/** @type {FastifyPluginAsync<StyledMapPackagePluginOpts>} */
async function styledMapPackagePlugin(fastify, opts) {
let reader = opts.lazy ? null : new Reader(opts.filepath)

fastify.addHook('onClose', async () => {
if (reader) {
// Can fail to close if `opts.filepath` used for instantiation is invalid
try {
await reader.close()
} catch (err) {
fastify.log.warn('Failed to close SMP reader instance', err)
}
}
})

fastify.decorate('comapeoSmp', {
async getStyle(baseUrl) {
if (!reader) {
reader = new Reader(opts.filepath)
}

const base =
baseUrl || new URL(opts.prefix || '', fastify.listeningOrigin).href

return reader.getStyle(base)
},

async getResource(path) {
if (!reader) {
reader = new Reader(opts.filepath)
}

return reader.getResource(path)
},
})

fastify.register(routes, {
prefix: opts.prefix,
})
}

/** @type {FastifyPluginAsync} */
async function routes(fastify) {
if (!fastify.hasDecorator('comapeoSmp')) {
throw new Error('Could not find `comapeoSmp` decorator')
}

fastify.get('/style.json', async () => {
const baseUrl = fastify.prefix
? new URL(fastify.prefix, fastify.listeningOrigin).href
: fastify.listeningOrigin

return fastify.comapeoSmp.getStyle(baseUrl)
})

fastify.get('*', async (request, reply) => {
/** @type {Resource} */
let resource
try {
// Removes the prefix that might have been registered by a consumer of the plugin
const normalizedPath = fastify.prefix
? request.url.replace(fastify.prefix, '')
: request.url

resource = await fastify.comapeoSmp.getResource(decodeURI(normalizedPath))
} catch (e) {
// @ts-expect-error
e.statusCode = 404
throw e
}

reply
.type(resource.contentType)
.header('content-length', resource.contentLength)

if (resource.contentEncoding) {
reply.header('content-encoding', resource.contentEncoding)
}

return reply.send(resource.stream)
})
}
6 changes: 3 additions & 3 deletions src/fastify-plugins/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export async function getFastifyServerAddress(server, { timeout } = {}) {
}

/**
* @param {Readonly<Date>} lastModified
* @param {Parameters<import('fastify').FastifyReply['headers']>[0]} [overrides]
*/
export function createStyleJsonResponseHeaders(lastModified) {
export function createStyleJsonResponseHeaders(overrides) {
return {
'Cache-Control': 'max-age=' + 5 * 60, // 5 minutes
'Access-Control-Allow-Headers':
'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since',
'Access-Control-Allow-Origin': '*',
'Last-Modified': lastModified.toUTCString(),
...overrides,
}
}
12 changes: 12 additions & 0 deletions test-e2e/manager-fastify-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { FastifyController } from '../src/fastify-controller.js'
import { plugin as StaticMapsPlugin } from '../src/fastify-plugins/maps/static-maps.js'
import { plugin as MapServerPlugin } from '../src/fastify-plugins/maps/index.js'
import { plugin as OfflineFallbackMapPlugin } from '../src/fastify-plugins/maps/offline-fallback-map.js'
import { plugin as StyledMapPackagePlugin } from '../src/fastify-plugins/maps/styled-map-package.js'

import { blobMetadata } from '../test/helpers/blob-store.js'

const BLOB_FIXTURES_DIR = fileURLToPath(
Expand All @@ -29,6 +31,11 @@ const MAPEO_FALLBACK_MAP_PATH = new URL(
import.meta.url
).pathname

const SMP_FIXTURE_PATH = new URL(
'../fixtures/styled-map-packages/demotiles-maplibre.smp',
import.meta.url
).pathname

const projectMigrationsFolder = new URL('../drizzle/project', import.meta.url)
.pathname
const clientMigrationsFolder = new URL('../drizzle/client', import.meta.url)
Expand Down Expand Up @@ -349,6 +356,11 @@ test('retrieving style.json using stable url', async (t) => {

const fastify = Fastify()

fastify.register(StyledMapPackagePlugin, {
prefix: 'smp',
lazy: true,
filepath: SMP_FIXTURE_PATH,
})
fastify.register(StaticMapsPlugin, {
prefix: 'static',
staticRootDir: MAP_FIXTURES_PATH,
Expand Down
Loading
Loading