From 2fb2339ec1477abb599d0d0e53a7753935715887 Mon Sep 17 00:00:00 2001 From: Tawera Manaena Date: Thu, 17 Oct 2024 09:16:04 +1300 Subject: [PATCH] Implement support for licensor attributions --- .../config-loader/src/json/tiff.config.ts | 1 + packages/config/src/config/imagery.ts | 11 +++++++ .../lambda-tiler/src/routes/attribution.ts | 3 +- .../src/routes/tile.style.json.ts | 32 ++++++++++++++++--- packages/landing/src/attribution.ts | 13 ++++++-- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/config-loader/src/json/tiff.config.ts b/packages/config-loader/src/json/tiff.config.ts index e92898642..9f3686375 100644 --- a/packages/config-loader/src/json/tiff.config.ts +++ b/packages/config-loader/src/json/tiff.config.ts @@ -384,6 +384,7 @@ export async function initImageryFromTiffUrl( noData: params.noData, files: params.files, collection: stac ?? undefined, + providers: stac?.providers, }; imagery.overviews = await ConfigJson.findImageryOverviews(imagery); log?.info({ title, imageryName, files: imagery.files.length }, 'Tiff:Loaded'); diff --git a/packages/config/src/config/imagery.ts b/packages/config/src/config/imagery.ts index d8d9458ae..a1087c98a 100644 --- a/packages/config/src/config/imagery.ts +++ b/packages/config/src/config/imagery.ts @@ -46,6 +46,12 @@ export const ConfigImageryOverviewParser = z }) .refine((obj) => obj.minZoom < obj.maxZoom); +export const ProvidersParser = z.object({ + name: z.string(), + description: z.string().optional(), + roles: z.array(z.string()).optional(), + url: z.string().optional(), +}); export const BoundingBoxParser = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() }); export const NamedBoundsParser = z.object({ /** @@ -140,6 +146,11 @@ export const ConfigImageryParser = ConfigBase.extend({ * Separate overview cache */ overviews: ConfigImageryOverviewParser.optional(), + + /** + * list of providers and their metadata + */ + providers: z.array(ProvidersParser).optional(), }); export type ConfigImagery = z.infer; diff --git a/packages/lambda-tiler/src/routes/attribution.ts b/packages/lambda-tiler/src/routes/attribution.ts index cd866bcd9..4a2f3a22b 100644 --- a/packages/lambda-tiler/src/routes/attribution.ts +++ b/packages/lambda-tiler/src/routes/attribution.ts @@ -136,13 +136,14 @@ async function tileSetAttribution( items.push(item); + const providers = im.providers?.map((p) => ({ ...p, roles: p.roles ?? [] })) ?? getHost(host); const zoomMin = TileMatrixSet.convertZoomLevel(layer.minZoom ? layer.minZoom : 0, GoogleTms, tileMatrix, true); const zoomMax = TileMatrixSet.convertZoomLevel(layer.maxZoom ? layer.maxZoom : 32, GoogleTms, tileMatrix, true); cols.push({ stac_version: Stac.Version, license: Stac.License, id: im.id, - providers: getHost(host), + providers, title, description: 'No description', extent, diff --git a/packages/lambda-tiler/src/routes/tile.style.json.ts b/packages/lambda-tiler/src/routes/tile.style.json.ts index f7b302eef..1055377d0 100644 --- a/packages/lambda-tiler/src/routes/tile.style.json.ts +++ b/packages/lambda-tiler/src/routes/tile.style.json.ts @@ -1,6 +1,7 @@ import { BasemapsConfigProvider, ConfigId, + ConfigImagery, ConfigPrefix, ConfigTileSetRaster, Layer, @@ -9,7 +10,7 @@ import { TileSetType, } from '@basemaps/config'; import { DefaultExaggeration } from '@basemaps/config/build/config/vector.style.js'; -import { GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo'; +import { Epsg, GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo'; import { Env, toQueryString } from '@basemaps/shared'; import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda'; import { URL } from 'url'; @@ -153,12 +154,13 @@ async function ensureTerrain( * Generate a StyleJSON from a tileset * @returns */ -export function tileSetToStyle( +export async function tileSetToStyle( req: LambdaHttpRequest, + config: BasemapsConfigProvider, tileSet: ConfigTileSetRaster, tileMatrix: TileMatrixSet, apiKey: string, -): StyleJson { +): Promise { // If the style has outputs defined it has a different process for generating the stylejson if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey); @@ -175,12 +177,32 @@ export function tileSetToStyle( (Env.get(Env.PublicUrlBase) ?? '') + `/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`; + // attempt to load the tileset's imagery + const imagery = await (function (): Promise { + if (tileSet.layers.length !== 1) return Promise.resolve(null); + + const imageryId = tileSet.layers[0][Epsg.Nztm2000.code]; + if (imageryId === undefined) return Promise.resolve(null); + + return config.Imagery.get(imageryId); + })(); + + // attempt to extract a licensor from the imagery's providers + const licensor = imagery?.providers?.find((p) => p?.roles?.includes('licensor'))?.name; + const styleId = `basemaps-${tileSet.name}`; return { id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name), name: tileSet.name, version: 8, - sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } }, + sources: { + [styleId]: { + type: 'raster', + tiles: [tileUrl], + tileSize: 256, + attribution: licensor ?? undefined, + }, + }, layers: [{ id: styleId, type: 'raster', source: styleId }], }; } @@ -248,7 +270,7 @@ async function generateStyleFromTileSet( throw new LambdaHttpResponse(400, 'Only raster tile sets can generate style JSON'); } if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey); - else return tileSetToStyle(req, tileSet, tileMatrix, apiKey); + return tileSetToStyle(req, config, tileSet, tileMatrix, apiKey); } export interface StyleGet { diff --git a/packages/landing/src/attribution.ts b/packages/landing/src/attribution.ts index 2f56eb5e2..5228556bb 100644 --- a/packages/landing/src/attribution.ts +++ b/packages/landing/src/attribution.ts @@ -8,7 +8,7 @@ import { Config } from './config.js'; import { mapToBoundingBox } from './tile.matrix.js'; import { MapOptionType } from './url.js'; -const Copyright = `© ${Stac.License} LINZ`; +const Copyright = `© ${Stac.License}`; export class MapAttributionState { /** Cache the loading of attribution */ @@ -168,11 +168,18 @@ export class MapAttribution implements maplibre.IControl { const filteredLayerIds = filtered.map((x) => x.collection.id).join('_'); Config.map.emit('visibleLayers', filteredLayerIds); + const licensor = (function (): string | null { + const providers = filtered[0].collection.providers; + if (providers === undefined) return null; + + return providers.find((p) => p.roles.some((r) => r === 'licensor'))?.name ?? null; + })(); + let attributionHTML = attr.renderList(filtered); if (attributionHTML === '') { - attributionHTML = Copyright; + attributionHTML = `${Copyright} ${licensor ?? 'LINZ'}`; } else { - attributionHTML = Copyright + ' - ' + attributionHTML; + attributionHTML = `${Copyright} ${licensor ?? 'LINZ'} - ${attributionHTML}`; } this.setAttribution(attributionHTML);