/// A verified external reference for a species, built deterministically from the /// bundled catalog's identifiers — never hand-entered. These are read-only /// "learn more" pointers, distinct from the links a grower adds by hand. enum ReferenceSource { gbif, wikipedia, wikispecies } /// One reference pointer: which source it is (for the label/icon) and the URL. class ReferenceLink { const ReferenceLink({required this.source, required this.url}); final ReferenceSource source; final String url; @override bool operator ==(Object other) => other is ReferenceLink && other.source == source && other.url == url; @override int get hashCode => Object.hash(source, url); @override String toString() => 'ReferenceLink($source, $url)'; } /// Builds the verified reference links for a species from its bundled /// identifiers. Deterministic and offline — no titles are bundled and no /// network call is made; each URL is derived from a stable identifier: /// /// * **GBIF** — the taxon page for the backbone [gbifKey]. /// * **Wikipedia** — locale-aware via Wikidata's `Special:GoToLinkedPage` /// redirect, so we need only the [wikidataQid] and the user's [languageCode]: /// the redirect resolves to the article in that language, falling back to the /// Wikidata item when the language has no article. Works under RTL locales. /// * **Wikispecies** — the canonical article keyed by [scientificName]. /// /// A source is only included when its required identifier is present, so a /// species missing (say) a GBIF key simply yields fewer links. List speciesReferenceLinks({ required String scientificName, int? gbifKey, String? wikidataQid, String languageCode = 'en', }) { final links = []; if (gbifKey != null) { links.add( ReferenceLink( source: ReferenceSource.gbif, url: 'https://www.gbif.org/species/$gbifKey', ), ); } final qid = wikidataQid?.trim(); if (qid != null && qid.isNotEmpty) { final lang = _wikiLang(languageCode); links.add( ReferenceLink( source: ReferenceSource.wikipedia, url: 'https://www.wikidata.org/wiki/Special:GoToLinkedPage/${lang}wiki/$qid', ), ); } final title = scientificName.trim(); if (title.isNotEmpty) { links.add( ReferenceLink( source: ReferenceSource.wikispecies, url: 'https://species.wikimedia.org/wiki/${_wikiTitle(title)}', ), ); } return links; } /// The bare Wikipedia language subtag: drops any region (`pt_BR` → `pt`) and /// falls back to English for an empty/blank locale. String _wikiLang(String languageCode) { final base = languageCode.split(RegExp('[-_]')).first.trim().toLowerCase(); return base.isEmpty ? 'en' : base; } /// A MediaWiki page title: spaces become underscores, then non-ASCII is /// percent-encoded (scientific names are Latin, but stay safe for hybrids and /// diacritics). `_` is left intact by [Uri.encodeFull]. String _wikiTitle(String scientificName) => Uri.encodeFull(scientificName.replaceAll(' ', '_'));