-
Notifications
You must be signed in to change notification settings - Fork 0
NRF2-418 Added OS base map #115
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
Changes from all commits
546c2ac
bab57ef
f5c1988
4509435
7012789
e94ae76
07a1035
44b8482
4a1d570
d5eaf9b
a6beebd
ffb3f4d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
robin-dunn marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import routes from './routes.js' | ||
|
|
||
| export const osBaseMap = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this naming really conveys that this is a proxy endpoint for map tiles |
||
| plugin: { | ||
| name: 'os-base-map', | ||
| register(server) { | ||
| server.route(routes) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import Wreck from '@hapi/wreck' | ||
| import { config } from '../../config/config.js' | ||
| import { createLogger } from '../common/helpers/logging/logger.js' | ||
| import { statusCodes } from '../common/constants/status-codes.js' | ||
|
|
||
| const logger = createLogger() | ||
|
|
||
| const ordnanceSurveyMapUrl = 'https://api.os.uk/maps/vector/v1/vts' | ||
|
|
||
| export const routePath = '/os-base-map' | ||
|
|
||
| function getOrdnanceSurveyMapUrl(path, query) { | ||
| const ordnanceSurveyApiKey = config.get('map.osApiKey') | ||
| const params = new URLSearchParams(query) | ||
| params.set('key', ordnanceSurveyApiKey) | ||
| params.set('srs', '3857') | ||
| const base = path ? `${ordnanceSurveyMapUrl}/${path}` : ordnanceSurveyMapUrl | ||
| return `${base}?${params.toString()}` | ||
| } | ||
|
|
||
| // Rewrites api.os.uk URLs in JSON responses to route through our proxy, stripping | ||
| // query strings so the API key isn't leaked to the client. | ||
| function rewriteOrdnanceSurveyMapUrls(body, host) { | ||
| const proxyBase = `${host}${routePath}` | ||
| const basePath = new URL(ordnanceSurveyMapUrl).pathname | ||
|
|
||
| try { | ||
| // Walk every value in the JSON using the parse reviver callback | ||
| const json = JSON.parse(body, (_key, value) => { | ||
| if (typeof value === 'string' && value.startsWith(ordnanceSurveyMapUrl)) { | ||
| // Extract the sub-path (e.g. /resources/styles) and discard the query string. | ||
| // decodeURIComponent restores MapLibre template tokens like {z}/{y}/{x} | ||
| // that new URL() percent-encodes. | ||
| const subPath = decodeURIComponent( | ||
| new URL(value).pathname.slice(basePath.length) | ||
| ) | ||
| return proxyBase + subPath | ||
| } | ||
| return value | ||
| }) | ||
|
|
||
| return JSON.stringify(json) | ||
| } catch { | ||
| return body | ||
| } | ||
| } | ||
|
|
||
| function isBinaryPath(path) { | ||
| return path.endsWith('.pbf') || path.endsWith('.png') || path.endsWith('.jpg') | ||
| } | ||
|
|
||
| const proxyHandler = { | ||
| method: 'GET', | ||
| path: `${routePath}/{path*}`, | ||
| options: { | ||
| auth: false | ||
| }, | ||
| async handler(request, h) { | ||
| const path = request.params.path || '' | ||
| const ordnanceSurveyUrl = getOrdnanceSurveyMapUrl(path, request.query) | ||
|
|
||
| try { | ||
| // For binary resources (tiles, sprites), don't decompress — pass through raw | ||
| const binary = isBinaryPath(path) | ||
| const { res, payload } = await Wreck.get(ordnanceSurveyUrl, { | ||
| redirects: 3, | ||
| maxBytes: 10 * 1024 * 1024, | ||
| gunzip: !binary | ||
| }) | ||
|
|
||
| const contentType = res.headers['content-type'] || '' | ||
| const cacheControl = res.headers['cache-control'] || 'no-cache' | ||
|
|
||
| if (binary) { | ||
| const response = h | ||
| .response(payload) | ||
| .type(contentType) | ||
| .header('cache-control', cacheControl) | ||
|
|
||
| if (res.headers['content-encoding']) { | ||
| response.header('content-encoding', res.headers['content-encoding']) | ||
| } | ||
|
|
||
| return response | ||
| } | ||
|
|
||
| // JSON responses — rewrite Ordnance Survey URLs to point to our proxy | ||
| const protocol = | ||
| request.headers['x-forwarded-proto'] || request.server.info.protocol | ||
| const host = `${protocol}://${request.info.host}` | ||
| const rewritten = rewriteOrdnanceSurveyMapUrls(payload.toString(), host) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wondering how this would scale with potentially 100's or 1000's of tile JSON responses being parsed simultaneously as JSON handling is blocking in node? |
||
| return h | ||
| .response(rewritten) | ||
| .type(contentType) | ||
| .header('cache-control', cacheControl) | ||
| } catch (err) { | ||
| // Wreck throws a Boom error on non-2xx responses (e.g. 403 for tiles outside UK coverage) | ||
| if (err.data?.isResponseError) { | ||
| const statusCode = err.data.res.statusCode | ||
| return h.response(err.data.payload).code(statusCode) | ||
| } | ||
|
|
||
| logger.error(`Map proxy error for ${path}: ${err.message}`) | ||
| return h.response('Map tile request failed').code(statusCodes.badGateway) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default [proxyHandler] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's not in this PR's changeset, but what is this sha value needed for? The script tags in the templates seem to use
noncerather than this?