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(lambda-tiler): update imagery layer attributions to show licensor details BM-897 #3357

Merged
merged 6 commits into from
Oct 24, 2024
65 changes: 65 additions & 0 deletions packages/attribution/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { strictEqual } from 'node:assert';
import { describe, it } from 'node:test';

import { StacProvider } from '@basemaps/geo';

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

const defaultAttribution = `${copyright} LINZ`;

describe('utils', () => {
const FakeHost: StacProvider = {
name: 'FakeHost',
roles: ['host'],
};
const FakeLicensor1: StacProvider = {
name: 'FakeLicensor1',
roles: ['licensor'],
};
const FakeLicensor2: StacProvider = {
name: 'FakeLicensor2',
roles: ['licensor'],
};

it('default attribution: no providers', () => {
const providers = undefined;
const attribution = createLicensorAttribution(providers);

strictEqual(attribution, defaultAttribution);
});

it('default attribution: empty providers', () => {
const providers: StacProvider[] = [];
const attribution = createLicensorAttribution(providers);

strictEqual(attribution, defaultAttribution);
});

it('default attribution: one provider, no licensors', () => {
const providers = [FakeHost];

const attribution = createLicensorAttribution(providers);
strictEqual(attribution, defaultAttribution);
});

it('custom attribution: one provider, one licensor', () => {
const providers = [FakeLicensor1];

const attribution = createLicensorAttribution(providers);
strictEqual(attribution, `${copyright} ${FakeLicensor1.name}`);
});

it('custom attribution: two providers, one licensor', () => {
const providers = [FakeHost, FakeLicensor1];

const attribution = createLicensorAttribution(providers);
strictEqual(attribution, `${copyright} ${FakeLicensor1.name}`);
});

it('custom attribution: two providers, two licensors', () => {
const providers = [FakeLicensor1, FakeLicensor2];

const attribution = createLicensorAttribution(providers);
strictEqual(attribution, `${copyright} ${FakeLicensor1.name}, ${FakeLicensor2.name}`);
});
});
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.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 > 0) {
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
25 changes: 25 additions & 0 deletions packages/attribution/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Stac, StacProvider } from '@basemaps/geo';

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

/**
* 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 == null) 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(', ')}`;
}
1 change: 1 addition & 0 deletions packages/config-loader/src/json/tiff.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
33 changes: 33 additions & 0 deletions packages/config/src/config/imagery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +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(),
tawera-manaena marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 Expand Up @@ -140,6 +168,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<typeof ConfigImageryParser>;
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
Loading
Loading