Skip to content

Commit

Permalink
feat(lambda-tiler): relocated and revised licensor attribution functions
Browse files Browse the repository at this point in the history
  • Loading branch information
tawera-manaena committed Oct 20, 2024
1 parent 2fb2339 commit 5409852
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 47 deletions.
70 changes: 59 additions & 11 deletions packages/attribution/src/attribution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AttributionCollection, AttributionStac } from '@basemaps/geo';
import { BBox, intersection, MultiPolygon, Ring, Wgs84 } from '@linzjs/geojson';

import { createLicensorAttribution } from './utils/utils.js';

export interface AttributionFilter {
extent: BBox;
zoom: number;
Expand Down Expand Up @@ -181,20 +183,66 @@ export class Attribution {
isIgnored?: (attr: AttributionBounds) => boolean;

/**
* Render the filtered attributions as a simple string suitable to display as attribution
* Parse the filtered list of attributions into a formatted string comprising license information.
*
* @param filtered The filtered list of attributions.
*
* @returns A formatted license string.
*
* @example
* if (filtered[0] contains no providers or licensors):
* return "CC BY 4.0 LINZ - Otago 0.3 Rural Aerial Photos (2017-2019)"
*
* @example
* if (filtered[0] contains licensors):
* return "CC BY 4.0 Otago Regional Council - Otago 0.3 Rural Aerial Photos (2017-2019)"
*/
renderLicense(filtered: AttributionBounds[]): string {
const providers = filtered[0]?.collection.providers;
const attribution = createLicensorAttribution(providers);
const list = this.renderList(filtered);

if (list.length) {
return `${attribution} - ${list}`;
} else {
return attribution;
}
}

/**
* Render the filtered attributions as a simple string suitable to display as attribution.
*
* @param filtered The filtered list of attributions.
*
* @returns {string} An empty string, if the filtered list is empty.
* Otherwise, a formatted string comprising attribution details.
*
* @example
* if (filtered.length === 0):
* return ""
*
* @example
* if (filtered.length === 1):
* return "Ashburton 0.1m Urban Aerial Photos (2023)"
*
* @example
* if (filtered.length === 2):
* return "Wellington 0.3m Rural Aerial Photos (2021) & New Zealand 10m Satellite Imagery (2023-2024)"
*
* @param list the filtered list of attributions
* @example
* if (filtered.length > 2):
* return "Canterbury 0.2 Rural Aerial Photos (2020-2021) & others 2012-2024"
*/
renderList(list: AttributionBounds[]): string {
if (list.length === 0) return '';
let result = escapeHtml(list[0].collection.title);
if (list.length > 1) {
if (list.length === 2) {
result += ` & ${escapeHtml(list[1].collection.title)}`;
renderList(filtered: AttributionBounds[]): string {
if (filtered.length === 0) return '';
let result = escapeHtml(filtered[0].collection.title);
if (filtered.length > 1) {
if (filtered.length === 2) {
result += ` & ${escapeHtml(filtered[1].collection.title)}`;
} else {
let [minYear, maxYear] = getYears(list[1].collection);
for (let i = 1; i < list.length; ++i) {
const [a, b] = getYears(list[i].collection);
let [minYear, maxYear] = getYears(filtered[1].collection);
for (let i = 1; i < filtered.length; ++i) {
const [a, b] = getYears(filtered[i].collection);
if (a !== -1 && (minYear === -1 || a < minYear)) minYear = a;
if (b !== -1 && (maxYear === -1 || b > maxYear)) maxYear = b;
}
Expand Down
66 changes: 66 additions & 0 deletions packages/attribution/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { BasemapsConfigProvider, ConfigTileSet } from '@basemaps/config';
import { Epsg, Stac, StacProvider } from '@basemaps/geo';

export const copyright = ${Stac.License}`;

/**
* Construct a licensor attribution for a given tileSet.
*
* @param provider The BasemapsConfigProvider object.
* @param tileSet The tileset from which to build the attribution.
* @param projection The projection to consider.
*
* @returns A default attribution, if the tileset has more than one layer or no such imagery for the given projection exists.
* Otherwise, a copyright string comprising the names of the tileset's licensors.
*
* @example
* "CC BY 4.0 LINZ"
*
* @example
* "CC BY 4.0 Nelson City Council, Tasman District Council, Waka Kotahi"
*/
export async function getTileSetAttribution(
provider: BasemapsConfigProvider,
tileSet: ConfigTileSet,
projection: Epsg,
): Promise<string> {
// ensure the tileset has exactly one layer
if (tileSet.layers.length > 1 || tileSet.layers[0] === undefined) {
return createLicensorAttribution();
}

// ensure imagery exist for the given projection
const imgId = tileSet.layers[0][projection.code];
if (imgId === undefined) return '';

// attempt to load the imagery
const imagery = await provider.Imagery.get(imgId);
if (imagery == null || imagery.providers === undefined) {
return createLicensorAttribution();
}

// return a licensor attribution string
return createLicensorAttribution(imagery.providers);
}

/**
* Create a licensor attribution string.
*
* @param providers The optional list of providers.
*
* @returns A copyright string comprising the names of licensor providers.
*
* @example
* "CC BY 4.0 LINZ"
*
* @example
* "CC BY 4.0 Nelson City Council, Tasman District Council, Waka Kotahi"
*/
export function createLicensorAttribution(providers?: StacProvider[]): string {
if (providers === undefined) return `${copyright} LINZ`;

const licensors = providers.filter((p) => p.roles?.includes('licensor'));
if (licensors.length === 0) return `${copyright} LINZ`;

return `${copyright} ${licensors.map((l) => l.name).join(', ')}`;
}
22 changes: 22 additions & 0 deletions packages/config/src/config/imagery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,34 @@ export const ConfigImageryOverviewParser = z
})
.refine((obj) => obj.minZoom < obj.maxZoom);

/**
* Provides information about a provider.
*
* @link https://github.com/radiantearth/stac-spec/blob/master/commons/common-metadata.md#provider
*/
export const ProvidersParser = z.object({
/**
* The name of the organization or the individual.
*/
name: z.string(),

/**
* Multi-line description to add further provider information such as processing details
* for processors and producers, hosting details for hosts or basic contact information.
*/
description: z.string().optional(),

/**
* Roles of the provider. Any of `licensor`, `producer`, `processor` or `host`.
*/
roles: z.array(z.string()).optional(),

/**
* Homepage on which the provider describes the dataset and publishes contact information.
*/
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({
/**
Expand Down
24 changes: 23 additions & 1 deletion packages/geo/src/stac/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,31 @@ export interface StacAsset {
description?: string;
}

/**
* Provides information about a provider.
*
* @link https://github.com/radiantearth/stac-spec/blob/master/commons/common-metadata.md#provider
*/
export interface StacProvider {
/**
* The name of the organization or the individual.
*/
name: string;
roles: string[];

/**
* Multi-line description to add further provider information such as processing details
* for processors and producers, hosting details for hosts or basic contact information.
*/
description?: string;

/**
* Roles of the provider. Any of `licensor`, `producer`, `processor` or `host`.
*/
roles?: string[];

/**
* Homepage on which the provider describes the dataset and publishes contact information.
*/
url?: string;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/lambda-tiler/src/routes/attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,13 @@ 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,
providers: im.providers ?? getHost(host),
title,
description: 'No description',
extent,
Expand Down
19 changes: 4 additions & 15 deletions packages/lambda-tiler/src/routes/tile.style.json.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getTileSetAttribution } from '@basemaps/attribution/build/utils/utils.js';
import {
BasemapsConfigProvider,
ConfigId,
ConfigImagery,
ConfigPrefix,
ConfigTileSetRaster,
Layer,
Expand All @@ -10,7 +10,7 @@ import {
TileSetType,
} from '@basemaps/config';
import { DefaultExaggeration } from '@basemaps/config/build/config/vector.style.js';
import { Epsg, GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
import { GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
import { Env, toQueryString } from '@basemaps/shared';
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
import { URL } from 'url';
Expand Down Expand Up @@ -177,18 +177,7 @@ export async 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<ConfigImagery | null> {
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 attribution = await getTileSetAttribution(config, tileSet, tileMatrix.projection);

const styleId = `basemaps-${tileSet.name}`;
return {
Expand All @@ -200,7 +189,7 @@ export async function tileSetToStyle(
type: 'raster',
tiles: [tileUrl],
tileSize: 256,
attribution: licensor ?? undefined,
attribution,
},
},
layers: [{ id: styleId, type: 'raster', source: styleId }],
Expand Down
21 changes: 3 additions & 18 deletions packages/landing/src/attribution.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Attribution } from '@basemaps/attribution';
import { AttributionBounds } from '@basemaps/attribution/build/attribution.js';
import { GoogleTms, Stac } from '@basemaps/geo';
import { GoogleTms } from '@basemaps/geo';
import * as maplibre from 'maplibre-gl';

import { onMapLoaded } from './components/map.js';
import { Config } from './config.js';
import { mapToBoundingBox } from './tile.matrix.js';
import { MapOptionType } from './url.js';

const Copyright = ${Stac.License}`;

export class MapAttributionState {
/** Cache the loading of attribution */
attrs: Map<string, Promise<Attribution | null>> = new Map();
Expand Down Expand Up @@ -168,21 +166,8 @@ 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} ${licensor ?? 'LINZ'}`;
} else {
attributionHTML = `${Copyright} ${licensor ?? 'LINZ'} - ${attributionHTML}`;
}

this.setAttribution(attributionHTML);
const attributionHTML = attr.renderLicense(filtered);
this.setAttribution(attributionHTML ?? '');
};

setAttribution(text: string): void {
Expand Down

0 comments on commit 5409852

Please sign in to comment.