All checks were successful
Deploy production / deploy (push) Successful in 1m8s
212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
// Shared helpers for the wildlife pokedex scripts (scripts/wildlife-import.ts,
|
|
// scripts/wildlife-photos.ts): polite iNaturalist API access (about 1 request per second,
|
|
// as the API asks), the group definitions and photo licence handling.
|
|
import { getJson, sleep } from './planes-wiki';
|
|
|
|
export const UA =
|
|
'StanRungeWildlifeDex/1.0 (https://stanrunge.dev/trackers/wildlife; wildlife pokedex importer) bun';
|
|
export const INAT = 'https://api.inaturalist.org/v1';
|
|
export const PLACE_NL = 7506;
|
|
export const PLACE_EUROPE = 97391;
|
|
|
|
let last = 0;
|
|
/** GET an iNaturalist API path, at most one call per ~1.1 s (cached when --cache is set). */
|
|
export async function inat<T = InatList>(path: string, useCached = true): Promise<T> {
|
|
const url = `${INAT}${path}`;
|
|
const wait = last + 1100 - Date.now();
|
|
if (wait > 0) await sleep(wait);
|
|
last = Date.now();
|
|
return getJson<T>(url, { headers: { 'user-agent': UA } }, useCached);
|
|
}
|
|
|
|
export type InatPhoto = {
|
|
id: number;
|
|
license_code: string | null;
|
|
attribution: string;
|
|
attribution_name?: string | null;
|
|
url: string;
|
|
original_dimensions?: { width: number; height: number } | null;
|
|
};
|
|
export type InatTaxon = {
|
|
id: number;
|
|
name: string;
|
|
rank: string;
|
|
is_active?: boolean;
|
|
extinct?: boolean;
|
|
iconic_taxon_name?: string | null;
|
|
preferred_common_name?: string | null;
|
|
english_common_name?: string | null;
|
|
observations_count?: number;
|
|
wikipedia_url?: string | null;
|
|
wikipedia_summary?: string | null;
|
|
default_photo?: InatPhoto | null;
|
|
taxon_photos?: { photo: InatPhoto }[];
|
|
ancestors?: { id: number; rank: string; name: string }[];
|
|
ancestor_ids?: number[];
|
|
conservation_status?: { status?: string; iucn?: number; authority?: string } | null;
|
|
conservation_statuses?: {
|
|
status: string;
|
|
iucn: number | null;
|
|
authority: string | null;
|
|
description?: string | null;
|
|
place: { id: number } | null;
|
|
}[];
|
|
establishment_means?: { establishment_means?: string; place?: { id: number } } | null;
|
|
};
|
|
export type InatList = {
|
|
total_results: number;
|
|
page: number;
|
|
per_page: number;
|
|
results: { count: number; taxon: InatTaxon }[];
|
|
};
|
|
|
|
export const CC = [
|
|
'cc0',
|
|
'cc-by',
|
|
'cc-by-sa',
|
|
'cc-by-nc',
|
|
'cc-by-nc-sa',
|
|
'cc-by-nd',
|
|
'cc-by-nc-nd'
|
|
];
|
|
export const isCc = (code: string | null | undefined) => !!code && CC.includes(code.toLowerCase());
|
|
export const licenseLabel = (code: string) =>
|
|
code.toLowerCase() === 'cc0' ? 'CC0' : `${code.toUpperCase().replace(/^CC-/, 'CC ')} 4.0`;
|
|
export const licenseUrl = (code: string) =>
|
|
code.toLowerCase() === 'cc0'
|
|
? 'https://creativecommons.org/publicdomain/zero/1.0/'
|
|
: `https://creativecommons.org/licenses/${code.toLowerCase().replace(/^cc-/, '')}/4.0/`;
|
|
/** "(c) Jane Doe, some rights reserved (CC BY)" -> "Jane Doe". */
|
|
export const author = (p: InatPhoto) =>
|
|
p.attribution_name?.trim() ||
|
|
p.attribution
|
|
.replace(/^\(c\)\s*/i, '')
|
|
.replace(/,\s*(some|no) rights reserved.*$/i, '')
|
|
.replace(/^no known copyright restrictions.*$/i, '')
|
|
.trim() ||
|
|
null;
|
|
/** Any size URL -> the "medium" (500px) one. */
|
|
export const mediumUrl = (url: string) =>
|
|
url.replace(/\/(square|small|thumb|medium|large|original)\.(jpe?g|png|gif|webp)/i, '/medium.$2');
|
|
|
|
// Groups shown in the dex, first match wins (by ancestor names on iNaturalist).
|
|
export type GroupDef = {
|
|
slug: string;
|
|
name: string;
|
|
nameNl: string;
|
|
emoji: string;
|
|
match: string[];
|
|
};
|
|
export const GROUPS: GroupDef[] = [
|
|
{ slug: 'mammals', name: 'Mammals', nameNl: 'Zoogdieren', emoji: '🦊', match: ['Mammalia'] },
|
|
{ slug: 'birds', name: 'Birds', nameNl: 'Vogels', emoji: '🐦', match: ['Aves'] },
|
|
{
|
|
slug: 'reptiles',
|
|
name: 'Reptiles',
|
|
nameNl: 'Reptielen',
|
|
emoji: '🦎',
|
|
match: ['Reptilia', 'Testudines', 'Squamata', 'Crocodylia']
|
|
},
|
|
{ slug: 'amphibians', name: 'Amphibians', nameNl: 'Amfibieën', emoji: '🐸', match: ['Amphibia'] },
|
|
{
|
|
slug: 'sharks-and-rays',
|
|
name: 'Sharks & rays',
|
|
nameNl: 'Haaien en roggen',
|
|
emoji: '🦈',
|
|
match: ['Elasmobranchii', 'Chondrichthyes']
|
|
},
|
|
{
|
|
slug: 'fish',
|
|
name: 'Fish',
|
|
nameNl: 'Vissen',
|
|
emoji: '🐟',
|
|
match: ['Actinopterygii', 'Sarcopterygii', 'Petromyzonti', 'Myxini', 'Agnatha']
|
|
},
|
|
{
|
|
slug: 'butterflies-and-moths',
|
|
name: 'Butterflies & moths',
|
|
nameNl: 'Vlinders',
|
|
emoji: '🦋',
|
|
match: ['Lepidoptera']
|
|
},
|
|
{ slug: 'beetles', name: 'Beetles', nameNl: 'Kevers', emoji: '🪲', match: ['Coleoptera'] },
|
|
{
|
|
slug: 'bees-wasps-and-ants',
|
|
name: 'Bees, wasps & ants',
|
|
nameNl: 'Bijen, wespen en mieren',
|
|
emoji: '🐝',
|
|
match: ['Hymenoptera']
|
|
},
|
|
{
|
|
slug: 'dragonflies',
|
|
name: 'Dragonflies & damselflies',
|
|
nameNl: 'Libellen',
|
|
emoji: '🪽',
|
|
match: ['Odonata']
|
|
},
|
|
{
|
|
slug: 'flies',
|
|
name: 'Flies & mosquitoes',
|
|
nameNl: 'Vliegen en muggen',
|
|
emoji: '🪰',
|
|
match: ['Diptera']
|
|
},
|
|
{
|
|
slug: 'bugs-and-other-insects',
|
|
name: 'Bugs, crickets & other insects',
|
|
nameNl: 'Wantsen, sprinkhanen en andere insecten',
|
|
emoji: '🦗',
|
|
match: ['Insecta']
|
|
},
|
|
{
|
|
slug: 'spiders',
|
|
name: 'Spiders & kin',
|
|
nameNl: 'Spinnen en verwanten',
|
|
emoji: '🕷️',
|
|
match: ['Arachnida']
|
|
},
|
|
{
|
|
slug: 'crustaceans',
|
|
name: 'Crabs & other crustaceans',
|
|
nameNl: 'Kreeftachtigen',
|
|
emoji: '🦀',
|
|
match: [
|
|
'Crustacea',
|
|
'Malacostraca',
|
|
'Maxillopoda',
|
|
'Thecostraca',
|
|
'Branchiopoda',
|
|
'Hexanauplia'
|
|
]
|
|
},
|
|
{
|
|
slug: 'molluscs',
|
|
name: 'Snails, slugs & other molluscs',
|
|
nameNl: 'Weekdieren',
|
|
emoji: '🐌',
|
|
match: ['Mollusca']
|
|
},
|
|
{
|
|
slug: 'other-invertebrates',
|
|
name: 'Jellyfish, starfish, worms & co',
|
|
nameNl: 'Overige ongewervelden',
|
|
emoji: '🪼',
|
|
match: []
|
|
}
|
|
];
|
|
export function groupOf(ancestorNames: string[]): GroupDef {
|
|
for (const g of GROUPS) if (g.match.some((m) => ancestorNames.includes(m))) return g;
|
|
return GROUPS.at(-1)!;
|
|
}
|
|
|
|
export const IUCN_BY_NUMBER: Record<number, string> = {
|
|
5: 'DD',
|
|
10: 'LC',
|
|
20: 'NT',
|
|
30: 'VU',
|
|
40: 'EN',
|
|
50: 'CR',
|
|
60: 'EW',
|
|
70: 'EX'
|
|
};
|