From 5da326a26e1883e5e89532d8326de1484a5b1f74 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:16:06 +0200 Subject: [PATCH 01/71] Foundation --- .gitignore | 2 + DESIGN.md | 57 +++ JustificationSet.ts | 55 +-- README.md | 2 +- SparqlEndpoint.ts | 74 ++++ SynonymGroup.ts | 873 +++++++------------------------------------- example/cli.ts | 0 main.ts | 200 ---------- mod.ts | 3 + 9 files changed, 305 insertions(+), 961 deletions(-) create mode 100644 DESIGN.md create mode 100644 SparqlEndpoint.ts create mode 100644 example/cli.ts delete mode 100644 main.ts create mode 100644 mod.ts diff --git a/.gitignore b/.gitignore index 03e5fd5..d6f48a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ synonym-group.js +docs + npm-package/node_modules npm-package/index.js npm-package/index.d.ts \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..4cd6135 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,57 @@ +# Design + + +> [!NOTE] +> This currently (2024-07-27) describes a potential future design, not +> the current one. + +## Overview + +The central taxonomic entity is one object `N` per latin name.\ +Synolib returns as results a list of `N`s. + +Each `N` exists because of a taxon-name, taxon-concept or col-taxon in the +data.\ +Each `N` is uniquely determined by its human-readable latin name (for taxa +ranking below genus, this is a multi-part name — binomial or trinomial) and +kingdom.\ +Each `N` contains `N+A` objects which represent latin names with an authority.\ +Each `N` contains, if present, `treatment`s directly associated with the +respective taxon-name.\ +Other metadata (if present) of a `N` are the list of its parent names (family, +order, ...); vernacular names; and taxon-name URI. + +Each `N+A` exists because of a taxon-concept or col-taxon in the data. It always +has a parent `N`.\ +Each `N+A` is uniquely determined by its human-readable latin name (as above), +kingdom and (normalized [^1]) authority.\ +Each `N+A` contains, if present, `treatment`s directly associated with the +respective taxon-concept.\ +Other metadata (if present) of a `N` are CoL IDs; and taxon-concept URI. + +A `treatment` exists because it is in the data, and is identifed by its RDF +URI.\ +A `treatment` may _define_, _augment_, _deprecate_ or _cite_ a `N+A`, and +_treat_ or _cite_ a `N`.\ +If a `treatment` does _define_, _augment_, _deprecate_ or _treat_ different `N` +and/or `N+A`s, they are considered synonyms.\ +Note that _cite_ does not create synonmic links.\ +Other metadata of a `treatment` are its authors, material citations, and images. + +Starting point of the algorithm is a latin name or the URI of either a +taxon-name, tacon-conecpt or col-taxon.\ +It will first try to find the respective `N` and all associated metadata, `N+A`s +and `treatment`s.\ +This `N` is the first result.\ +Then it will recursively use all synonyms indicated by the found `treatment`s to +find new `N`s.\ +For each new `N`, it will find all associated metadata, `N+A`s and `treatment`s; +and return it as the next result.\ +Then it will continue to expand recursively until no more new `N`s are found. + +The algorithm keeps track of which treatment links it followed and other reasons +it added a `N` to the results.\ +This "justification" is also proved as metadata of a `N`. + +[^1]: I.e. ignoring differences in punctuation, diacritics, capitalization and +such. diff --git a/JustificationSet.ts b/JustificationSet.ts index a3260c8..4076aac 100644 --- a/JustificationSet.ts +++ b/JustificationSet.ts @@ -1,28 +1,33 @@ // @ts-ignore: Import unneccesary for typings, will collate .d.ts files -import type { JustifiedSynonym, Treatment } from "./SynonymGroup.ts"; +import type { JustifiedSynonym, Treatment } from "./mod.ts"; -interface Justification { +/** //TODO */ +export type Justification = { + /** //TODO */ toString: () => string; + /** //TODO */ + treatment?: Treatment; + /** //TODO */ precedingSynonym?: JustifiedSynonym; // eslint-disable-line no-use-before-define } -interface TreatmentJustification extends Justification { - treatment: Treatment; -} -type LexicalJustification = Justification; -export type anyJustification = TreatmentJustification | LexicalJustification; - -export class JustificationSet implements AsyncIterable { +/** //TODO */ +export class JustificationSet implements AsyncIterable { + /** @internal */ private monitor = new EventTarget(); - contents: anyJustification[] = []; + /** @internal */ + contents: Justification[] = []; + /** @internal */ isFinished = false; + /** @internal */ isAborted = false; + /** @internal */ entries = ((Array.from(this.contents.values()).map((v) => [v, v])) as [ - anyJustification, - anyJustification, + Justification, + Justification, ][]).values; - - constructor(iterable?: Iterable) { + /** @internal */ + constructor(iterable?: Iterable) { if (iterable) { for (const el of iterable) { this.add(el); @@ -30,7 +35,7 @@ export class JustificationSet implements AsyncIterable { } return this; } - + /** @internal */ get size() { return new Promise((resolve, reject) => { if (this.isAborted) { @@ -49,7 +54,8 @@ export class JustificationSet implements AsyncIterable { }); } - add(value: anyJustification) { + /** @internal */ + add(value: Justification) { if ( this.contents.findIndex((c) => c.toString() === value.toString()) === -1 ) { @@ -59,18 +65,21 @@ export class JustificationSet implements AsyncIterable { return this; } - finish() { + /** @internal */ + finish(): void { //console.info("%cJustificationSet finished", "color: #69F0AE;"); this.isFinished = true; this.monitor.dispatchEvent(new CustomEvent("updated")); } - forEachCurrent(cb: (val: anyJustification) => void) { + /** @internal */ + forEachCurrent(cb: (val: Justification) => void): void { this.contents.forEach(cb); } - first() { - return new Promise((resolve) => { + /** @internal */ + first(): Promise { + return new Promise((resolve) => { if (this.contents[0]) { resolve(this.contents[0]); } else { @@ -81,13 +90,13 @@ export class JustificationSet implements AsyncIterable { }); } - [Symbol.toStringTag] = ""; - [Symbol.asyncIterator]() { + /** //TODO */ + [Symbol.asyncIterator](): AsyncIterator { // this.monitor.addEventListener("updated", () => console.log("ARA")); let returnedSoFar = 0; return { next: () => { - return new Promise>( + return new Promise>( (resolve, reject) => { const _ = () => { if (this.isAborted) { diff --git a/README.md b/README.md index 92bc389..28e41ed 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ See `index.html` for an example of a webpage using the library. Go to [http://plazi.github.io/synolib/](http://plazi.github.io/synolib/) to open the example page in the browser and execute the script. -For a simple command line example using the library see: `main.ts`. +For a simple command line example using the library see: `example/cli.ts`. ## building diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts new file mode 100644 index 0000000..fd7849c --- /dev/null +++ b/SparqlEndpoint.ts @@ -0,0 +1,74 @@ +async function sleep(ms: number): Promise { + const p = new Promise((resolve) => { + setTimeout(resolve, ms); + }); + return await p; +} + +/** Describes the format of the JSON return by SPARQL endpoints */ +export type SparqlJson = { + head: { + vars: string[]; + }; + results: { + bindings: { + [key: string]: { type: string; value: string; "xml:lang"?: string }; + }[]; + }; +}; + +/** + * Represents a remote sparql endpoint and provides a uniform way to run queries. + */ +export class SparqlEndpoint { + /** Create a new SparqlEndpoint with the given URI */ + constructor(private sparqlEnpointUri: string) {} + + /** + * Run a query against the sparql endpoint + * + * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time. + * Retries are logged to the console (`console.warn`) + * + * @throws In case of non-ok response status codes or if fetch failed 10 times. + * @param query The sparql query to run against the endpoint + * @param fetchOptions Additional options for the `fetch` request + * @param _reason (Currently ignored, used internally for debugging purposes) + * @returns Results of the query + */ + async getSparqlResultSet( + query: string, + fetchOptions: RequestInit = {}, + _reason = "", + ): Promise { + fetchOptions.headers = fetchOptions.headers || {}; + (fetchOptions.headers as Record)["Accept"] = + "application/sparql-results+json"; + let retryCount = 0; + const sendRequest = async (): Promise => { + try { + // console.info(`SPARQL ${_reason} (${retryCount + 1})`); + const response = await fetch( + this.sparqlEnpointUri + "?query=" + encodeURIComponent(query), + fetchOptions, + ); + if (!response.ok) { + throw new Error("Response not ok. Status " + response.status); + } + return await response.json(); + } catch (error) { + if (fetchOptions.signal?.aborted) { + throw error; + } else if (retryCount < 10) { + const wait = 50 * (1 << retryCount++); + console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); + await sleep(wait); + return await sendRequest(); + } + console.warn("!! Fetch Error:", query, "\n---\n", error); + throw error; + } + }; + return await sendRequest(); + } +} diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 2b63e77..cb9943b 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,8 +1,139 @@ -// @ts-ignore: Import unneccesary for typings, will collate .d.ts files -import { JustificationSet } from "./JustificationSet.ts"; -// @ts-ignore: Import unneccesary for typings, will collate .d.ts files -export * from "./JustificationSet.ts"; +import { JustificationSet } from "./mod.ts"; +/** Finds all synonyms of a taxon */ +export class SynonymGroup implements AsyncIterable { + /** Indicates whether the SynonymGroup has found all synonyms. + * + * @readonly + */ + isFinished = false; + /** Indicates whether the SynonymGroup has been aborted. + * + * @readonly + */ + isAborted = false; + /** Used internally to watch for new names found */ + private monitor = new EventTarget(); + + /** + * List of names found so-far. + * + * Contains full list of synonyms _if_ .isFinished and not .isAborted + * + * @readonly + */ + names = new Array(); + + /** Allows iterating over the synonyms while they are found */ + [Symbol.asyncIterator](): AsyncIterator { + let returnedSoFar = 0; + return { + next: () => + new Promise>( + (resolve, reject) => { + const callback = () => { + if (this.isAborted) { + reject(new Error("SynyonymGroup has been aborted")); + } else if (returnedSoFar < this.names.length) { + resolve({ value: this.names[returnedSoFar++] }); + } else if (this.isFinished) { + resolve({ done: true, value: true }); + } else { + const listener = () => { + this.monitor.removeEventListener("updated", listener); + callback(); + }; + this.monitor.addEventListener("updated", listener); + } + }; + callback(); + }, + ), + }; + } +} + +/** The central object. + * + * Each `Name` exists because of a taxon-name, taxon-concept or col-taxon in the data. + * Each `Name` is uniquely determined by its human-readable latin name (for taxa ranking below genus, this is a multi-part name — binomial or trinomial) and kingdom. + */ +export type Name = { + /** taxonomic kingdom */ + kingdom: string; + /** Human-readable name */ + displayName: string; + + /** //TODO Promise? */ + // vernacularNames: Promise; + // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank. + // * //TODO Promise? */ + // trees: Promise<{ + // col?: Tree; + // tb?: Tree; + // }>; + + /** The URI of the respective `dwcFP:TaxonName` if it exists */ + taxonNameURI?: string; + /** All `AuthorizedName`s with this name */ + authorizedNames: AuthorizedName[]; + + /** How this name was found */ + justification: JustificationSet; + + /** treatments directly associated with .taxonNameUri */ + treatments: { + aug: Set; + cite: Set; + }; +}; + +/** + * Corresponds to a taxon-concept or a CoL-Taxon + */ +export type AuthorizedName = { + // TODO: neccesary? + /** this may not be neccesary, as `AuthorizedName`s should only appear within a `Name` */ + name: Name; + /** Human-readable authority */ + taxonConceptAuthority?: string; + + /** The URI of the respective `dwcFP:TaxonConcept` if it exists */ + taxonConceptURI?: string; + /** The URI of the respective CoL-taxon if it exists */ + colURI?: string; + + // TODO: sensible? + // /** these are CoL-taxa linked in the rdf, which differ lexically */ + // seeAlsoCol: string[]; + + /** treatments directly associated with .taxonConceptURI */ + treatments: { + def: Set; + aug: Set; + dpr: Set; + cite: Set; + }; +}; + +/** A plazi-treatment */ +export type Treatment = { + url: string; + + /** Details are behind a promise becuase they are loaded with a separate query. */ + details: Promise; +}; + +/** Details of a treatment */ +export type TreatmentDetails = { + materialCitations: MaterialCitation[]; + figureCitations: FigureCitation[]; + date?: number; + creators?: string; + title?: string; +}; + +/** A cited material */ export type MaterialCitation = { "catalogNumber": string; "collectionCode"?: string; @@ -24,740 +155,8 @@ export type MaterialCitation = { "httpUri"?: string[]; }; +/** A cited figure */ export type FigureCitation = { url: string; description?: string; }; - -export type TreatmentDetails = { - materialCitations: MaterialCitation[]; - figureCitations: FigureCitation[]; - date?: number; - creators?: string; - title?: string; -}; - -export type Treatment = { - url: string; - details: Promise; -}; - -/** - * Describes a taxonomic name (http://filteredpush.org/ontologies/oa/dwcFP#TaxonName) - */ -export type TaxonName = { - uri: string; - treatments: { - aug: Set; - cite: Set; - }; - /** Human-readable taxon-name */ displayName: string; - vernacularNames: Promise; - loading: boolean; -}; - -/** - * A map from language tags (IETF) to an array of vernacular names. - */ -export type vernacularNames = Record; - -type Treatments = { - def: Set; - aug: Set; - dpr: Set; - cite: Set; -}; -export type JustifiedSynonym = { - taxonConceptUri: string; - taxonName: TaxonName; - /** Human-readable authority */ taxonConceptAuthority?: string; - justifications: JustificationSet; - treatments: Treatments; - loading: boolean; -}; - -async function sleep(ms: number): Promise { - const p = new Promise((resolve) => { - setTimeout(resolve, ms); - }); - return await p; -} - -type SparqlJson = { - head: { - vars: string[]; - }; - results: { - bindings: { - [key: string]: { type: string; value: string; "xml:lang"?: string }; - }[]; - }; -}; - -/** - * Represents a remote sparql endpoint and provides a uniform way to run queries. - */ -export class SparqlEndpoint { - constructor(private sparqlEnpointUri: string) { - } - - /** - * Run a query against the sparql endpoint - * - * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time. - * Retries are logged to the console (`console.warn`) - * - * @throws In case of non-ok response status codes or if fetch failed 10 times. - * @param query The sparql query to run against the endpoint - * @param fetchOptions Additional options for the `fetch` request - * @param _reason (Currently ignored, used internally for debugging purposes) - * @returns Results of the query - */ - async getSparqlResultSet( - query: string, - fetchOptions: RequestInit = {}, - _reason = "", - ) { - fetchOptions.headers = fetchOptions.headers || {}; - (fetchOptions.headers as Record)["Accept"] = - "application/sparql-results+json"; - let retryCount = 0; - const sendRequest = async (): Promise => { - try { - // console.info(`SPARQL ${_reason} (${retryCount + 1})`); - const response = await fetch( - this.sparqlEnpointUri + "?query=" + encodeURIComponent(query), - fetchOptions, - ); - if (!response.ok) { - throw new Error("Response not ok. Status " + response.status); - } - return await response.json(); - } catch (error) { - if (fetchOptions.signal?.aborted) { - throw error; - } else if (retryCount < 10) { - const wait = 50 * (1 << retryCount++); - console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); - await sleep(wait); - return await sendRequest(); - } - console.warn("!! Fetch Error:", query, "\n---\n", error); - throw error; - } - }; - return await sendRequest(); - } -} - -export default class SynonymGroup implements AsyncIterable { - justifiedArray: JustifiedSynonym[] = []; - monitor = new EventTarget(); - isFinished = false; - isAborted = false; - - /** Maps from url to object */ - treatments: Map = new Map(); - taxonNames: Map = new Map(); - - private controller = new AbortController(); - - constructor( - sparqlEndpoint: SparqlEndpoint, - taxonName: string, - ignoreRank = false, - ) { - /** Maps from taxonConceptUris to their synonyms */ - const justifiedSynonyms: Map = new Map(); - const expandedTaxonNames: Set = new Set(); - - const resolver = (value: JustifiedSynonym | true) => { - if (value === true) { - //console.info("%cSynogroup finished", "color: #00E676;"); - this.isFinished = true; - } - this.monitor.dispatchEvent(new CustomEvent("updated")); - }; - - const fetchInit = { signal: this.controller.signal }; - - async function getTreatmentDetails( - treatmentUri: string, - ): Promise { - const query = ` -PREFIX dc: -PREFIX dwc: -PREFIX trt: -SELECT DISTINCT - ?date ?title ?mc - (group_concat(DISTINCT ?catalogNumber;separator=" / ") as ?catalogNumbers) - (group_concat(DISTINCT ?collectionCode;separator=" / ") as ?collectionCodes) - (group_concat(DISTINCT ?typeStatus;separator=" / ") as ?typeStatuss) - (group_concat(DISTINCT ?countryCode;separator=" / ") as ?countryCodes) - (group_concat(DISTINCT ?stateProvince;separator=" / ") as ?stateProvinces) - (group_concat(DISTINCT ?municipality;separator=" / ") as ?municipalitys) - (group_concat(DISTINCT ?county;separator=" / ") as ?countys) - (group_concat(DISTINCT ?locality;separator=" / ") as ?localitys) - (group_concat(DISTINCT ?verbatimLocality;separator=" / ") as ?verbatimLocalitys) - (group_concat(DISTINCT ?recordedBy;separator=" / ") as ?recordedBys) - (group_concat(DISTINCT ?eventDate;separator=" / ") as ?eventDates) - (group_concat(DISTINCT ?samplingProtocol;separator=" / ") as ?samplingProtocols) - (group_concat(DISTINCT ?decimalLatitude;separator=" / ") as ?decimalLatitudes) - (group_concat(DISTINCT ?decimalLongitude;separator=" / ") as ?decimalLongitudes) - (group_concat(DISTINCT ?verbatimElevation;separator=" / ") as ?verbatimElevations) - (group_concat(DISTINCT ?gbifOccurrenceId;separator=" / ") as ?gbifOccurrenceIds) - (group_concat(DISTINCT ?gbifSpecimenId;separator=" / ") as ?gbifSpecimenIds) - (group_concat(DISTINCT ?creator;separator="; ") as ?creators) - (group_concat(DISTINCT ?httpUri;separator="|") as ?httpUris) -WHERE { -<${treatmentUri}> dc:creator ?creator . -OPTIONAL { <${treatmentUri}> trt:publishedIn/dc:date ?date . } -OPTIONAL { <${treatmentUri}> dc:title ?title } -OPTIONAL { - <${treatmentUri}> dwc:basisOfRecord ?mc . - ?mc dwc:catalogNumber ?catalogNumber . - OPTIONAL { ?mc dwc:collectionCode ?collectionCode . } - OPTIONAL { ?mc dwc:typeStatus ?typeStatus . } - OPTIONAL { ?mc dwc:countryCode ?countryCode . } - OPTIONAL { ?mc dwc:stateProvince ?stateProvince . } - OPTIONAL { ?mc dwc:municipality ?municipality . } - OPTIONAL { ?mc dwc:county ?county . } - OPTIONAL { ?mc dwc:locality ?locality . } - OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . } - OPTIONAL { ?mc dwc:recordedBy ?recordedBy . } - OPTIONAL { ?mc dwc:eventDate ?eventDate . } - OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . } - OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . } - OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . } - OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . } - OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . } - OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . } - OPTIONAL { ?mc trt:httpUri ?httpUri . } -} -} -GROUP BY ?date ?title ?mc`; - if (fetchInit.signal.aborted) { - return { materialCitations: [], figureCitations: [] }; - } - try { - const json = await sparqlEndpoint.getSparqlResultSet( - query, - fetchInit, - `Treatment Details for ${treatmentUri}`, - ); - const materialCitations: MaterialCitation[] = json.results.bindings - .filter((t) => t.mc && t.catalogNumbers?.value).map((t) => { - const httpUri = t.httpUris?.value?.split("|"); - return { - "catalogNumber": t.catalogNumbers.value, - "collectionCode": t.collectionCodes?.value || undefined, - "typeStatus": t.typeStatuss?.value || undefined, - "countryCode": t.countryCodes?.value || undefined, - "stateProvince": t.stateProvinces?.value || undefined, - "municipality": t.municipalitys?.value || undefined, - "county": t.countys?.value || undefined, - "locality": t.localitys?.value || undefined, - "verbatimLocality": t.verbatimLocalitys?.value || undefined, - "recordedBy": t.recordedBys?.value || undefined, - "eventDate": t.eventDates?.value || undefined, - "samplingProtocol": t.samplingProtocols?.value || undefined, - "decimalLatitude": t.decimalLatitudes?.value || undefined, - "decimalLongitude": t.decimalLongitudes?.value || undefined, - "verbatimElevation": t.verbatimElevations?.value || undefined, - "gbifOccurrenceId": t.gbifOccurrenceIds?.value || undefined, - "gbifSpecimenId": t.gbifSpecimenIds?.value || undefined, - httpUri: httpUri?.length ? httpUri : undefined, - }; - }); - const figureQuery = `PREFIX cito: -PREFIX fabio: -PREFIX dc: -SELECT DISTINCT ?url ?description WHERE { - <${treatmentUri}> cito:cites ?cites . - ?cites a fabio:Figure ; - fabio:hasRepresentation ?url . - OPTIONAL { ?cites dc:description ?description . } -} `; - const figures = (await sparqlEndpoint.getSparqlResultSet( - figureQuery, - fetchInit, - `Figures for ${treatmentUri}`, - )).results.bindings; - const figureCitations = figures.filter((f) => f.url?.value).map( - (f) => { - return { url: f.url.value, description: f.description?.value }; - }, - ); - return { - creators: json.results.bindings[0]?.creators?.value, - date: json.results.bindings[0]?.date?.value - ? parseInt(json.results.bindings[0].date.value, 10) - : undefined, - title: json.results.bindings[0]?.title?.value, - materialCitations, - figureCitations, - }; - } catch (error) { - console.warn("SPARQL Error: " + error); - return { materialCitations: [], figureCitations: [] }; - } - } - - const makeTreatmentSet = (urls?: string[]): Set => { - if (!urls) return new Set(); - return new Set( - urls.filter((url) => !!url).map((url) => { - if (!this.treatments.has(url)) { - this.treatments.set(url, { - url, - details: getTreatmentDetails(url), - }); - } - return this.treatments.get(url) as Treatment; - }), - ); - }; - - async function getVernacular( - uri: string, - ): Promise> { - const result: Record = {}; - const query = - `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`; - const bindings = - (await sparqlEndpoint.getSparqlResultSet(query)).results.bindings; - for (const b of bindings) { - if (b.n.value) { - if (b.n["xml:lang"]) { - if (!result[b.n["xml:lang"]]) result[b.n["xml:lang"]] = []; - result[b.n["xml:lang"]].push(b.n.value); - } else { - if (!result["??"]) result["??"] = []; - result["??"].push(b.n.value); - } - } - } - return result; - } - - const makeTaxonName = ( - uri: string, - name: string, - aug?: string[], - cite?: string[], - ) => { - if (!this.taxonNames.has(uri)) { - this.taxonNames.set(uri, { - uri, - loading: true, - displayName: name, - vernacularNames: getVernacular(uri), - treatments: { - aug: makeTreatmentSet(aug), - cite: makeTreatmentSet(cite), - }, - }); - } - return this.taxonNames.get(uri) as TaxonName; - }; - - const build = async () => { - const getStartingPoints = ( - taxonName: string, - ): Promise => { - if (fetchInit.signal.aborted) return Promise.resolve([]); - const [genus, species, subspecies] = taxonName.split(" "); - // subspecies could also be variety - // ignoreRank has no effect when there is a 'subspecies', as this is assumed to be the lowest rank & should thus not be able to return results in another rank - const query = `PREFIX cito: -PREFIX dc: -PREFIX dwc: -PREFIX treat: -SELECT DISTINCT - ?tn ?name ?tc (group_concat(DISTINCT ?auth; separator=" / ") as ?authority) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) (group_concat(DISTINCT ?cite;separator="|") as ?cites) (group_concat(DISTINCT ?trtn;separator="|") as ?trtns) (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) -WHERE { - ?tc dwc:genus "${genus}"; - treat:hasTaxonName ?tn; - ${species ? `dwc:species "${species}";` : ""} - ${subspecies ? `(dwc:subspecies|dwc:variety) "${subspecies}";` : ""} - ${ - ignoreRank || !!subspecies - ? "" - : `dwc:rank "${species ? "species" : "genus"}";` - } - a . - ?tn dwc:genus ?genus . - OPTIONAL { ?tn dwc:subGenus ?subgenus . } - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies ?subspecies . } - OPTIONAL { ?tn dwc:variety ?variety . } - } - BIND(CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?subspecies), ""), COALESCE(CONCAT(" var. ", ?variety), "")) as ?name) - OPTIONAL { ?tc dwc:scientificNameAuthorship ?auth . } - OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def treat:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr treat:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } - OPTIONAL { ?trtn treat:treatsTaxonName ?tn . } - OPTIONAL { ?citetn treat:citesTaxonName ?tn . } -} -GROUP BY ?tn ?name ?tc`; - // console.info('%cREQ', 'background: red; font-weight: bold; color: white;', `getStartingPoints('${taxonName}')`) - if (fetchInit.signal.aborted) return Promise.resolve([]); - return sparqlEndpoint.getSparqlResultSet( - query, - fetchInit, - "Starting Points", - ) - .then( - (json: SparqlJson) => - json.results.bindings.filter((t) => (t.tc && t.tn)) - .map((t) => { - return { - taxonConceptUri: t.tc.value, - taxonName: makeTaxonName( - t.tn.value, - t.name?.value, - t.trtns?.value.split("|"), - t.citetns?.value.split("|"), - ), - taxonConceptAuthority: t.authority?.value, - justifications: new JustificationSet([ - `${t.tc.value} matches "${taxonName}"`, - ]), - treatments: { - def: makeTreatmentSet(t.defs?.value.split("|")), - aug: makeTreatmentSet(t.augs?.value.split("|")), - dpr: makeTreatmentSet(t.dprs?.value.split("|")), - cite: makeTreatmentSet(t.cites?.value.split("|")), - }, - loading: true, - }; - }), - (error) => { - console.warn("SPARQL Error: " + error); - return []; - }, - ); - }; - - const synonymFinders = [ - /** Get the Synonyms having the same {taxon-name} */ - (taxon: JustifiedSynonym): Promise => { - const query = `PREFIX cito: -PREFIX dc: -PREFIX dwc: -PREFIX treat: -SELECT DISTINCT - ?tc (group_concat(DISTINCT ?auth; separator=" / ") as ?authority) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) (group_concat(DISTINCT ?cite;separator="|") as ?cites) -WHERE { - ?tc treat:hasTaxonName <${taxon.taxonName.uri}> . - OPTIONAL { ?tc dwc:scientificNameAuthorship ?auth . } - OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def treat:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr treat:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } -} -GROUP BY ?tc`; - // console.info('%cREQ', 'background: red; font-weight: bold; color: white;', `synonymFinder[0]( ${taxon.taxonConceptUri} )`) - // Check wether we already expanded this taxon name horizontally - otherwise add - if (expandedTaxonNames.has(taxon.taxonName.uri)) { - return Promise.resolve([]); - } - expandedTaxonNames.add(taxon.taxonName.uri); - if (fetchInit.signal.aborted) return Promise.resolve([]); - return sparqlEndpoint.getSparqlResultSet( - query, - fetchInit, - `Same taxon name ${taxon.taxonConceptUri}`, - ).then(( - json: SparqlJson, - ) => { - taxon.taxonName.loading = false; - return json.results.bindings.filter((t) => t.tc).map( - (t): JustifiedSynonym => { - return { - taxonConceptUri: t.tc.value, - taxonName: taxon.taxonName, - taxonConceptAuthority: t.authority?.value, - justifications: new JustificationSet([{ - toString: () => - `${t.tc.value} has taxon name ${taxon.taxonName.uri}`, - precedingSynonym: taxon, - }]), - treatments: { - def: makeTreatmentSet(t.defs?.value.split("|")), - aug: makeTreatmentSet(t.augs?.value.split("|")), - dpr: makeTreatmentSet(t.dprs?.value.split("|")), - cite: makeTreatmentSet(t.cites?.value.split("|")), - }, - loading: true, - }; - }, - ); - }, (error) => { - console.warn("SPARQL Error: " + error); - return []; - }); - }, - /** Get the Synonyms deprecating {taxon} */ - (taxon: JustifiedSynonym): Promise => { - const query = `PREFIX cito: -PREFIX dc: -PREFIX dwc: -PREFIX treat: -SELECT DISTINCT - ?tn ?name ?tc (group_concat(DISTINCT ?auth; separator=" / ") as ?authority) (group_concat(DISTINCT ?justification; separator="|") as ?justs) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) (group_concat(DISTINCT ?cite;separator="|") as ?cites) (group_concat(DISTINCT ?trtn;separator="|") as ?trtns) (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) -WHERE { - ?justification treat:deprecates <${taxon.taxonConceptUri}> ; - (treat:augmentsTaxonConcept|treat:definesTaxonConcept) ?tc . - ?tc ?tn . - ?tn dwc:genus ?genus . - OPTIONAL { ?tn dwc:subGenus ?subgenus . } - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies ?subspecies . } - OPTIONAL { ?tn dwc:variety ?variety . } - } - BIND(CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?subspecies), ""), COALESCE(CONCAT(" var. ", ?variety), "")) as ?name) - OPTIONAL { ?tc dwc:scientificNameAuthorship ?auth . } - OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def treat:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr treat:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } - OPTIONAL { ?trtn treat:treatsTaxonName ?tn . } - OPTIONAL { ?citetn treat:citesTaxonName ?tn . } -} -GROUP BY ?tn ?name ?tc`; - // console.info('%cREQ', 'background: red; font-weight: bold; color: white;', `synonymFinder[1]( ${taxon.taxonConceptUri} )`) - if (fetchInit.signal.aborted) return Promise.resolve([]); - return sparqlEndpoint.getSparqlResultSet( - query, - fetchInit, - `Deprecating ${taxon.taxonConceptUri}`, - ).then(( - json: SparqlJson, - ) => - json.results.bindings.filter((t) => t.tc).map((t) => { - return { - taxonConceptUri: t.tc.value, - taxonName: makeTaxonName( - t.tn.value, - t.name?.value, - t.trtns?.value.split("|"), - t.citetns?.value.split("|"), - ), - taxonConceptAuthority: t.authority?.value, - justifications: new JustificationSet( - t.justs?.value.split("|").map((url) => { - if (!this.treatments.has(url)) { - this.treatments.set(url, { - url, - details: getTreatmentDetails(url), - }); - } - return { - toString: () => - `${t.tc.value} deprecates ${taxon.taxonConceptUri} according to ${url}`, - precedingSynonym: taxon, - treatment: this.treatments.get(url), - }; - }), - ), - treatments: { - def: makeTreatmentSet(t.defs?.value.split("|")), - aug: makeTreatmentSet(t.augs?.value.split("|")), - dpr: makeTreatmentSet(t.dprs?.value.split("|")), - cite: makeTreatmentSet(t.cites?.value.split("|")), - } as Treatments, - loading: true, - }; - }), (error) => { - console.warn("SPARQL Error: " + error); - return []; - }); - }, - /** Get the Synonyms deprecated by {taxon} */ - (taxon: JustifiedSynonym): Promise => { - const query = `PREFIX cito: -PREFIX dc: -PREFIX dwc: -PREFIX treat: -SELECT DISTINCT - ?tn ?name ?tc (group_concat(DISTINCT ?auth; separator=" / ") as ?authority) (group_concat(DISTINCT ?justification; separator="|") as ?justs) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) (group_concat(DISTINCT ?cite;separator="|") as ?cites) (group_concat(DISTINCT ?trtn;separator="|") as ?trtns) (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) -WHERE { - ?justification (treat:augmentsTaxonConcept|treat:definesTaxonConcept) <${taxon.taxonConceptUri}> ; - treat:deprecates ?tc . - ?tc ?tn . - ?tn dwc:genus ?genus . - OPTIONAL { ?tn dwc:subGenus ?subgenus . } - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies ?subspecies . } - OPTIONAL { ?tn dwc:variety ?variety . } - } - BIND(CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?subspecies), ""), COALESCE(CONCAT(" var. ", ?variety), "")) as ?name) - OPTIONAL { ?tc dwc:scientificNameAuthorship ?auth . } - OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def treat:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr treat:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } - OPTIONAL { ?trtn treat:treatsTaxonName ?tn . } - OPTIONAL { ?citetn treat:citesTaxonName ?tn . } -} -GROUP BY ?tn ?name ?tc`; - // console.info('%cREQ', 'background: red; font-weight: bold; color: white;', `synonymFinder[2]( ${taxon.taxonConceptUri} )`) - if (fetchInit.signal.aborted) return Promise.resolve([]); - return sparqlEndpoint.getSparqlResultSet( - query, - fetchInit, - `Deprecated by ${taxon.taxonConceptUri}`, - ).then(( - json: SparqlJson, - ) => - json.results.bindings.filter((t) => t.tc).map((t) => { - return { - taxonConceptUri: t.tc.value, - taxonName: makeTaxonName( - t.tn.value, - t.name?.value, - t.trtns?.value.split("|"), - t.citetns?.value.split("|"), - ), - taxonConceptAuthority: t.authority?.value, - justifications: new JustificationSet( - t.justs?.value.split("|").map((url) => { - if (!this.treatments.has(url)) { - this.treatments.set(url, { - url, - details: getTreatmentDetails(url), - }); - } - return { - toString: () => - `${t.tc.value} deprecates ${taxon.taxonConceptUri} according to ${url}`, - precedingSynonym: taxon, - treatment: this.treatments.get(url), - }; - }), - ), - treatments: { - def: makeTreatmentSet(t.defs?.value.split("|")), - aug: makeTreatmentSet(t.augs?.value.split("|")), - dpr: makeTreatmentSet(t.dprs?.value.split("|")), - cite: makeTreatmentSet(t.cites?.value.split("|")), - } as Treatments, - loading: true, - }; - }), (error) => { - console.warn("SPARQL Error: " + error); - return []; - }); - }, - ]; - - async function lookUpRound( - taxon: JustifiedSynonym, - ): Promise { - // await new Promise(resolve => setTimeout(resolve, 3000)) // 3 sec - // console.log('%cSYG', 'background: blue; font-weight: bold; color: white;', `lookupRound( ${taxon.taxonConceptUri} )`) - const foundGroupsP = synonymFinders.map((finder) => finder(taxon)); - const foundGroups = await Promise.all(foundGroupsP); - return foundGroups.reduce((a, b) => a.concat(b), []); - } - - const finish = (justsyn: JustifiedSynonym) => { - justsyn.justifications.finish(); - justsyn.loading = false; - }; - - let justifiedSynsToExpand: JustifiedSynonym[] = await getStartingPoints( - taxonName, - ); - justifiedSynsToExpand.forEach((justsyn) => { - finish(justsyn); - justifiedSynonyms.set( - justsyn.taxonConceptUri, - this.justifiedArray.push(justsyn) - 1, - ); - resolver(justsyn); - }); - const expandedTaxonConcepts: Set = new Set(); - while (justifiedSynsToExpand.length > 0) { - const foundThisRound: string[] = []; - const promises = justifiedSynsToExpand.map( - async (j): Promise => { - if (expandedTaxonConcepts.has(j.taxonConceptUri)) return false; - expandedTaxonConcepts.add(j.taxonConceptUri); - const newSynonyms = await lookUpRound(j); - newSynonyms.forEach((justsyn) => { - // Check whether we know about this synonym already - if (justifiedSynonyms.has(justsyn.taxonConceptUri)) { - // Check if we found that synonym in this round - if (~foundThisRound.indexOf(justsyn.taxonConceptUri)) { - justsyn.justifications.forEachCurrent((jsj) => { - this - .justifiedArray[ - justifiedSynonyms.get(justsyn.taxonConceptUri)! - ].justifications.add(jsj); - }); - } - } else { - finish(justsyn); - justifiedSynonyms.set( - justsyn.taxonConceptUri, - this.justifiedArray.push(justsyn) - 1, - ); - resolver(justsyn); - } - if (!expandedTaxonConcepts.has(justsyn.taxonConceptUri)) { - justifiedSynsToExpand.push(justsyn); - foundThisRound.push(justsyn.taxonConceptUri); - } - }); - return true; - }, - ); - justifiedSynsToExpand = []; - await Promise.allSettled(promises); - } - resolver(true); - }; - - build(); - } - - abort() { - this.isAborted = true; - this.controller.abort(); - } - - [Symbol.asyncIterator]() { - let returnedSoFar = 0; - return { - next: () => { - return new Promise>( - (resolve, reject) => { - const _ = () => { - if (this.isAborted) { - reject(new Error("SynyonymGroup has been aborted")); - } else if (returnedSoFar < this.justifiedArray.length) { - resolve({ value: this.justifiedArray[returnedSoFar++] }); - } else if (this.isFinished) { - resolve({ done: true, value: true }); - } else { - const listener = () => { - this.monitor.removeEventListener("updated", listener); - _(); - }; - this.monitor.addEventListener("updated", listener); - } - }; - _(); - }, - ); - }, - }; - } -} diff --git a/example/cli.ts b/example/cli.ts new file mode 100644 index 0000000..e69de29 diff --git a/main.ts b/main.ts deleted file mode 100644 index c41e4df..0000000 --- a/main.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** Command line tool that returns all information as it becomes available */ - -import SynoGroup, { - JustifiedSynonym, - SparqlEndpoint, - TaxonName, -} from "./SynonymGroup.ts"; -import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; - -const sparqlEndpoint = new SparqlEndpoint( - "https://treatment.ld.plazi.org/sparql", -); -const taxonName = Deno.args.length > 0 - ? Deno.args.join(" ") - : "Sadayoshia acroporae"; -const synoGroup = new SynoGroup(sparqlEndpoint, taxonName); - -console.log(Colors.blue(`Synonym Group For ${taxonName}`)); -try { - for await (const synonym of synoGroup) { - console.log( - Colors.red( - ` * Found synonym: ${tcName(synonym)} <${synonym.taxonConceptUri}>`, - ), - ); - console.log( - Colors.blue( - ` ... with taxon name: ${ - tnName(synonym.taxonName) - } <${synonym.taxonName.uri}>`, - ), - ); - synonym.taxonName.vernacularNames.then((v) => - console.log(JSON.stringify(v)) - ); - for (const treatment of synonym.taxonName.treatments.aug) { - console.log( - Colors.gray( - ` - Found treatment for ${ - tnName(synonym.taxonName) - }: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - for (const treatment of synonym.taxonName.treatments.cite) { - console.log( - Colors.gray( - ` - Found treatment citing ${ - tnName(synonym.taxonName) - }: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - - for await (const justification of synonym.justifications) { - console.log( - Colors.magenta( - ` - Found justification for ${tcName(synonym)}: ${justification}`, - ), - ); - } - for (const treatment of synonym.treatments!.aug) { - console.log( - Colors.gray( - ` - Found augmenting treatment for ${ - tcName(synonym) - }: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - for (const treatment of synonym.treatments.def) { - console.log( - Colors.gray( - ` - Found defining treatment for ${ - tcName(synonym) - }: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - for (const treatment of synonym.treatments.dpr) { - console.log( - Colors.gray( - ` - Found deprecating treatment for ${ - tcName(synonym) - }: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - for (const treatment of synonym.treatments.cite) { - console.log( - Colors.gray( - ` - Found treatment citing ${tcName(synonym)}: ${treatment.url}`, - ), - ); - treatment.details.then((details) => { - if (details.materialCitations.length) { - console.log( - Colors.gray( - ` - Found MCS for ${treatment.url}: ${ - details.materialCitations.map((mc) => mc.catalogNumber).join( - ", ", - ) - }`, - ), - ); - } - }); - } - } -} catch (error) { - console.error(Colors.red(error + "")); -} - -function tcName(synonym: JustifiedSynonym) { - if (synonym.taxonConceptAuthority) { - const name = synonym.taxonName.displayName || synonym.taxonName.uri.replace( - "http://taxon-name.plazi.org/id/", - "", - ); - return name.replaceAll("_", " ") + " " + synonym.taxonConceptAuthority; - } - const suffix = synonym.taxonConceptUri.replace( - "http://taxon-concept.plazi.org/id/", - "", - ); - return suffix.replaceAll("_", " "); -} - -function tnName(taxonName: TaxonName) { - const name = taxonName.displayName || taxonName.uri.replace( - "http://taxon-name.plazi.org/id/", - "", - ).replaceAll("_", " "); - return name; -} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..593c6a8 --- /dev/null +++ b/mod.ts @@ -0,0 +1,3 @@ +export * from "./SparqlEndpoint.ts"; +export * from "./JustificationSet.ts"; +export * from "./SynonymGroup.ts" \ No newline at end of file From 13c13d5ac9c0bb280ac3bfca261632e1345e2a85 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:28:13 +0200 Subject: [PATCH 02/71] finds initial name if given colUri --- SparqlEndpoint.ts | 2 +- SynonymGroup.ts | 338 +++++++++++++++++++++++++++++++++++++++++++++- example/cli.ts | 64 +++++++++ 3 files changed, 397 insertions(+), 7 deletions(-) diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts index fd7849c..d6fa295 100644 --- a/SparqlEndpoint.ts +++ b/SparqlEndpoint.ts @@ -12,7 +12,7 @@ export type SparqlJson = { }; results: { bindings: { - [key: string]: { type: string; value: string; "xml:lang"?: string }; + [key: string]: { type: string; value: string; "xml:lang"?: string } | undefined; }[]; }; }; diff --git a/SynonymGroup.ts b/SynonymGroup.ts index cb9943b..3559a91 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,4 +1,9 @@ -import { JustificationSet } from "./mod.ts"; +import { JustificationSet, SparqlEndpoint } from "./mod.ts"; + +enum NameStatus { + inProgress, + done, +} /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -15,6 +20,12 @@ export class SynonymGroup implements AsyncIterable { /** Used internally to watch for new names found */ private monitor = new EventTarget(); + /** Used internally to abort in-flight network requests when SynonymGroup is aborted */ + private controller = new AbortController(); + + /** The SparqlEndpoint used */ + private sparqlEndpoint: SparqlEndpoint; + /** * List of names found so-far. * @@ -22,7 +33,320 @@ export class SynonymGroup implements AsyncIterable { * * @readonly */ - names = new Array(); + names: Name[] = []; + private pushName(name: Name) { + this.names.push(name); + this.monitor.dispatchEvent(new CustomEvent("updated")); + } + private finish() { + this.isFinished = true; + this.monitor.dispatchEvent(new CustomEvent("updated")); + } + + /** contains TN, TC, CoL uris of synonyms whose `Name` we have not yet constructed */ + private queue = new Set(); + + /** contains TN, TC, CoL uris of synonyms whose `Name` is being constructed or has been constructed. */ + private expanded = new Map(); + + /** Used internally to deduplicate treatments, maps from URI to Object */ + private treatments = new Map(); + + /** + * Constructs a SynonymGroup + * + * @param sparqlEndpoint SPARQL-Endpoint to query + * @param taxonName either a string of the form "Genus species infraspecific" (species & infraspecific names optional), or an URI of a http://filteredpush.org/ontologies/oa/dwcFP#TaxonConcept or a CoL taxon URI + * @param [ignoreRank=false] if taxonName is "Genus" or "Genus species", by default it will ony search for taxons of rank genus/species. If set to true, sub-taxa are also considered as staring points. + */ + constructor( + sparqlEndpoint: SparqlEndpoint, + taxonName: string, + ignoreRank = false, + ) { + this.sparqlEndpoint = sparqlEndpoint; + + // TODO use queue + if (taxonName.startsWith("https://www.catalogueoflife.org")) { + this.getNameFromCol(taxonName).then((name) => { + this.pushName(name); + this.finish(); + }); + } + + // TODO + } + + private async getNameFromCol(colUri: string): Promise { + // Note: this query assumes that there is no sub-species taxa with missing dwc:species + // Note: the handling assumes that at most one taxon-name matches this colTaxon + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX treat: +SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + BIND(<${colUri}> as ?col) + ?col dwc:taxonRank ?rank . + ?col dwc:scientificNameAuthorship ?authority . + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + OPTIONAL { + ?col dwc:specificEpithet ?species . + OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } + } + + OPTIONAL { + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + + OPTIONAL { ?trtn treat:treatsTaxonName ?tn . } + OPTIONAL { ?citetn treat:citesTaxonName ?tn . } + + OPTIONAL { + ?tc treat:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def treat:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr treat:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } + } + } +} +GROUP BY ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority +LIMIT 500`; + // For unclear reasons, the query breaks if the limit is removed. + + if (this.controller.signal?.aborted) return Promise.reject(); + + /// ?tn ?tc !rank !genus ?species ?infrasp !fullName !authority ?tcAuth + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + "Starting Points", + ); + + const displayName: string = json.results.bindings[0].fullName!.value + .replace( + json.results.bindings[0].authority!.value, + "", + ).trim(); + + const colName: AuthorizedName = { + displayName, + authority: json.results.bindings[0].authority!.value, + colURI: colUri, + treatments: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + cite: new Set(), + }, + }; + + const authorizedNames = [colName]; + + const taxonNameURI = json.results.bindings[0].tn?.value; + + for (const t of json.results.bindings) { + if (t.tc) { + if (t.tcAuth?.value.split(" / ").includes(colName.authority)) { + // TODO tc is same as colName, get tretments and merge together + } else if (t.tcAuth?.value) { + authorizedNames.push({ + displayName, + authority: t.tcAuth.value, + taxonConceptURI: t.tc.value, + treatments: { + def: this.makeTreatmentSet(t.defs?.value.split("|")), + aug: this.makeTreatmentSet(t.augs?.value.split("|")), + dpr: this.makeTreatmentSet(t.dprs?.value.split("|")), + cite: this.makeTreatmentSet(t.cites?.value.split("|")), + }, + }); + // TODO get treatments + } + } + } + + // TODO treatments + // TODO handle queue/expandedNames/etc + + return { + displayName, + taxonNameURI, + authorizedNames, + justification: new JustificationSet("//TODO"), + treatments: { + treats: this.makeTreatmentSet(json.results.bindings[0].tntreats?.value.split("|")), + cite: this.makeTreatmentSet(json.results.bindings[0].tncites?.value.split("|")), + }, + }; + } + + private makeTreatmentSet (urls?: string[]): Set { + if (!urls) return new Set(); + return new Set( + urls.filter((url) => !!url).map((url) => { + if (!this.treatments.has(url)) { + this.treatments.set(url, { + url, + details: this.getTreatmentDetails(url), + }); + } + return this.treatments.get(url) as Treatment; + }), + ); + }; + + private async getTreatmentDetails( + treatmentUri: string, + ): Promise { + const query = ` +PREFIX dc: +PREFIX dwc: +PREFIX trt: +SELECT DISTINCT + ?date ?title ?mc + (group_concat(DISTINCT ?catalogNumber;separator=" / ") as ?catalogNumbers) + (group_concat(DISTINCT ?collectionCode;separator=" / ") as ?collectionCodes) + (group_concat(DISTINCT ?typeStatus;separator=" / ") as ?typeStatuss) + (group_concat(DISTINCT ?countryCode;separator=" / ") as ?countryCodes) + (group_concat(DISTINCT ?stateProvince;separator=" / ") as ?stateProvinces) + (group_concat(DISTINCT ?municipality;separator=" / ") as ?municipalitys) + (group_concat(DISTINCT ?county;separator=" / ") as ?countys) + (group_concat(DISTINCT ?locality;separator=" / ") as ?localitys) + (group_concat(DISTINCT ?verbatimLocality;separator=" / ") as ?verbatimLocalitys) + (group_concat(DISTINCT ?recordedBy;separator=" / ") as ?recordedBys) + (group_concat(DISTINCT ?eventDate;separator=" / ") as ?eventDates) + (group_concat(DISTINCT ?samplingProtocol;separator=" / ") as ?samplingProtocols) + (group_concat(DISTINCT ?decimalLatitude;separator=" / ") as ?decimalLatitudes) + (group_concat(DISTINCT ?decimalLongitude;separator=" / ") as ?decimalLongitudes) + (group_concat(DISTINCT ?verbatimElevation;separator=" / ") as ?verbatimElevations) + (group_concat(DISTINCT ?gbifOccurrenceId;separator=" / ") as ?gbifOccurrenceIds) + (group_concat(DISTINCT ?gbifSpecimenId;separator=" / ") as ?gbifSpecimenIds) + (group_concat(DISTINCT ?creator;separator="; ") as ?creators) + (group_concat(DISTINCT ?httpUri;separator="|") as ?httpUris) +WHERE { + BIND (<${treatmentUri}> as ?treatment) + ?treatment dc:creator ?creator . + OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . } + OPTIONAL { ?treatment dc:title ?title } + OPTIONAL { + ?treatment dwc:basisOfRecord ?mc . + ?mc dwc:catalogNumber ?catalogNumber . + OPTIONAL { ?mc dwc:collectionCode ?collectionCode . } + OPTIONAL { ?mc dwc:typeStatus ?typeStatus . } + OPTIONAL { ?mc dwc:countryCode ?countryCode . } + OPTIONAL { ?mc dwc:stateProvince ?stateProvince . } + OPTIONAL { ?mc dwc:municipality ?municipality . } + OPTIONAL { ?mc dwc:county ?county . } + OPTIONAL { ?mc dwc:locality ?locality . } + OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . } + OPTIONAL { ?mc dwc:recordedBy ?recordedBy . } + OPTIONAL { ?mc dwc:eventDate ?eventDate . } + OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . } + OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . } + OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . } + OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . } + OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . } + OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . } + OPTIONAL { ?mc trt:httpUri ?httpUri . } + } +} +GROUP BY ?date ?title ?mc`; + if (this.controller.signal.aborted) { + return { materialCitations: [], figureCitations: [] }; + } + try { + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `Treatment Details for ${treatmentUri}`, + ); + const materialCitations: MaterialCitation[] = json.results.bindings + .filter((t) => t.mc && t.catalogNumbers?.value) + .map((t) => { + const httpUri = t.httpUris?.value?.split("|"); + return { + "catalogNumber": t.catalogNumbers!.value, + "collectionCode": t.collectionCodes?.value || undefined, + "typeStatus": t.typeStatuss?.value || undefined, + "countryCode": t.countryCodes?.value || undefined, + "stateProvince": t.stateProvinces?.value || undefined, + "municipality": t.municipalitys?.value || undefined, + "county": t.countys?.value || undefined, + "locality": t.localitys?.value || undefined, + "verbatimLocality": t.verbatimLocalitys?.value || undefined, + "recordedBy": t.recordedBys?.value || undefined, + "eventDate": t.eventDates?.value || undefined, + "samplingProtocol": t.samplingProtocols?.value || undefined, + "decimalLatitude": t.decimalLatitudes?.value || undefined, + "decimalLongitude": t.decimalLongitudes?.value || undefined, + "verbatimElevation": t.verbatimElevations?.value || undefined, + "gbifOccurrenceId": t.gbifOccurrenceIds?.value || undefined, + "gbifSpecimenId": t.gbifSpecimenIds?.value || undefined, + httpUri: httpUri?.length ? httpUri : undefined, + }; + }); + const figureQuery = ` +PREFIX cito: +PREFIX fabio: +PREFIX dc: +SELECT DISTINCT ?url ?description WHERE { + <${treatmentUri}> cito:cites ?cites . + ?cites a fabio:Figure ; + fabio:hasRepresentation ?url . + OPTIONAL { ?cites dc:description ?description . } +} `; + const figures = (await this.sparqlEndpoint.getSparqlResultSet( + figureQuery, + { signal: this.controller.signal }, + `Figures for ${treatmentUri}`, + )).results.bindings; + const figureCitations = figures.filter((f) => f.url?.value).map( + (f) => { + return { url: f.url!.value, description: f.description?.value }; + }, + ); + return { + creators: json.results.bindings[0]?.creators?.value, + date: json.results.bindings[0]?.date?.value + ? parseInt(json.results.bindings[0].date.value, 10) + : undefined, + title: json.results.bindings[0]?.title?.value, + materialCitations, + figureCitations, + }; + } catch (error) { + console.warn("SPARQL Error: " + error); + return { materialCitations: [], figureCitations: [] }; + } + } /** Allows iterating over the synonyms while they are found */ [Symbol.asyncIterator](): AsyncIterator { @@ -60,7 +384,7 @@ export class SynonymGroup implements AsyncIterable { */ export type Name = { /** taxonomic kingdom */ - kingdom: string; + // kingdom: string; /** Human-readable name */ displayName: string; @@ -83,7 +407,7 @@ export type Name = { /** treatments directly associated with .taxonNameUri */ treatments: { - aug: Set; + treats: Set; cite: Set; }; }; @@ -94,9 +418,11 @@ export type Name = { export type AuthorizedName = { // TODO: neccesary? /** this may not be neccesary, as `AuthorizedName`s should only appear within a `Name` */ - name: Name; + // name: Name; + /** Human-readable name */ + displayName: string; /** Human-readable authority */ - taxonConceptAuthority?: string; + authority: string; /** The URI of the respective `dwcFP:TaxonConcept` if it exists */ taxonConceptURI?: string; diff --git a/example/cli.ts b/example/cli.ts index e69de29..f685397 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -0,0 +1,64 @@ +import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; +import { SparqlEndpoint, SynonymGroup, Treatment } from "../mod.ts"; + +const sparqlEndpoint = new SparqlEndpoint( + "https://treatment.ld.plazi.org/sparql", +); +const taxonName = Deno.args.length > 0 + ? Deno.args.join(" ") + : "https://www.catalogueoflife.org/data/taxon/3CP83"; +const synoGroup = new SynonymGroup(sparqlEndpoint, taxonName); + +console.log(Colors.blue(`Synonym Group For ${taxonName}`)); +for await (const name of synoGroup) { + console.log( + Colors.underline(name.displayName) + + colorizeIfPresent(name.taxonNameURI, "yellow"), + ); + for (const trt of name.treatments.treats) { + console.log(Colors.blue(" ● ") + await treatmentToString(trt)); + } + for (const trt of name.treatments.cite) { + console.log(Colors.gray(" ● ") + await treatmentToString(trt)); + } + // TODO justification + for (const authorizedName of name.authorizedNames) { + console.log( + " " + + Colors.underline( + authorizedName.displayName + " " + + Colors.italic(authorizedName.authority), + ) + + colorizeIfPresent(authorizedName.taxonConceptURI, "yellow") + + colorizeIfPresent(authorizedName.colURI, "cyan"), + ); + for (const trt of authorizedName.treatments.def) { + console.log(Colors.green(" ● ") + await treatmentToString(trt)); + } + for (const trt of authorizedName.treatments.aug) { + console.log(Colors.blue(" ● ") + await treatmentToString(trt)); + } + for (const trt of authorizedName.treatments.dpr) { + console.log(Colors.red(" ● ") + await treatmentToString(trt)); + } + for (const trt of authorizedName.treatments.cite) { + console.log(Colors.gray(" ● ") + await treatmentToString(trt)); + } + // TODO justification + } +} + +function colorizeIfPresent( + text: string | undefined, + color: "gray" | "yellow" | "green" | "cyan", +) { + if (text) return " " + Colors[color](text); + else return ""; +} + +async function treatmentToString(trt: Treatment) { + const details = await trt.details; + return `${details.creators} ${details.date} “${ + Colors.italic(details.title || Colors.dim("No Title")) + }” ${Colors.magenta(trt.url)}`; +} From ed57af60b7429f29134c984ac2882d43f67a9a04 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:14:24 +0200 Subject: [PATCH 03/71] working on queuing --- SynonymGroup.ts | 94 ++++++++++++++++++++++++++++++++++++------------- example/cli.ts | 5 +-- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 3559a91..fae09e2 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,7 +1,9 @@ import { JustificationSet, SparqlEndpoint } from "./mod.ts"; enum NameStatus { - inProgress, + makingName, + madeName, + findingSynonyms, done, } @@ -12,11 +14,6 @@ export class SynonymGroup implements AsyncIterable { * @readonly */ isFinished = false; - /** Indicates whether the SynonymGroup has been aborted. - * - * @readonly - */ - isAborted = false; /** Used internally to watch for new names found */ private monitor = new EventTarget(); @@ -43,10 +40,21 @@ export class SynonymGroup implements AsyncIterable { this.monitor.dispatchEvent(new CustomEvent("updated")); } + // An URI of a name can have on of the following states: + // 1) we have found it as a synonym but have not gotten the details + // -> in this.freshSynonymQueue + // 2) we have loaded the details and made a `Name` from it. All authorizedNames of the `Name` become at least 3).* + // 3) we are looking for the (treatment-linked-) synonyms of the name. When we get them, they all become at least 1).* + // 4) we are done with this URI. + // * if the name already has a higher state, it is not downgraded. + /** contains TN, TC, CoL uris of synonyms whose `Name` we have not yet constructed */ - private queue = new Set(); + private freshSynonymQueue: string[] = []; + + /** contains TN, TC, CoL uris of synonyms whose (treatment-linked-) synonyms we have not yet looked for */ + private expandSynonymQueue: string[] = []; - /** contains TN, TC, CoL uris of synonyms whose `Name` is being constructed or has been constructed. */ + /** contains TN, TC, CoL uris of synonyms which are in-flight somehow */ private expanded = new Map(); /** Used internally to deduplicate treatments, maps from URI to Object */ @@ -66,17 +74,34 @@ export class SynonymGroup implements AsyncIterable { ) { this.sparqlEndpoint = sparqlEndpoint; - // TODO use queue - if (taxonName.startsWith("https://www.catalogueoflife.org")) { - this.getNameFromCol(taxonName).then((name) => { - this.pushName(name); - this.finish(); - }); + // TODO handle "Genus species"-style input + + if (taxonName.startsWith("http")) { + this.freshSynonymQueue.push(taxonName); } - // TODO + this.driveQueue(); } + private async driveQueue() { + while ( + this.freshSynonymQueue.length > 0 || this.expandSynonymQueue.length > 0 + ) { + if (this.freshSynonymQueue.length > 0) { + const taxonName = this.freshSynonymQueue.pop()!; + if (taxonName.startsWith("https://www.catalogueoflife.org")) { + await this.getNameFromCol(taxonName).then((name) => { + this.pushName(name); + }); + } + } + // TODO handle expansion + console.log("Not expanding:", this.expandSynonymQueue.pop()) + } + this.finish(); + } + + /** colUri must have been freshly popped from freshSynonymQueue */ private async getNameFromCol(colUri: string): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon @@ -141,6 +166,9 @@ GROUP BY ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. + if (this.expanded.get(colUri)) throw "Called on previously expanded name"; + this.expanded.set(colUri, NameStatus.makingName); + if (this.controller.signal?.aborted) return Promise.reject(); /// ?tn ?tc !rank !genus ?species ?infrasp !fullName !authority ?tcAuth @@ -171,11 +199,22 @@ LIMIT 500`; const authorizedNames = [colName]; const taxonNameURI = json.results.bindings[0].tn?.value; + if (taxonNameURI) { + this.expanded.set(taxonNameURI, NameStatus.madeName); + this.expandSynonymQueue.push(taxonNameURI); + } for (const t of json.results.bindings) { if (t.tc) { if (t.tcAuth?.value.split(" / ").includes(colName.authority)) { - // TODO tc is same as colName, get tretments and merge together + colName.authority = t.tcAuth?.value; + colName.taxonConceptURI = t.tc.value; + colName.treatments = { + def: this.makeTreatmentSet(t.defs?.value.split("|")), + aug: this.makeTreatmentSet(t.augs?.value.split("|")), + dpr: this.makeTreatmentSet(t.dprs?.value.split("|")), + cite: this.makeTreatmentSet(t.cites?.value.split("|")), + }; } else if (t.tcAuth?.value) { authorizedNames.push({ displayName, @@ -188,27 +227,32 @@ LIMIT 500`; cite: this.makeTreatmentSet(t.cites?.value.split("|")), }, }); - // TODO get treatments } + this.expanded.set(t.tc.value, NameStatus.madeName); + this.expandSynonymQueue.push(t.tc.value); } } - // TODO treatments - // TODO handle queue/expandedNames/etc + // TODO: handle col-data "acceptedName" and stuff + this.expanded.set(colUri, NameStatus.done); return { displayName, taxonNameURI, authorizedNames, - justification: new JustificationSet("//TODO"), + justification: new JustificationSet(["//TODO"]), treatments: { - treats: this.makeTreatmentSet(json.results.bindings[0].tntreats?.value.split("|")), - cite: this.makeTreatmentSet(json.results.bindings[0].tncites?.value.split("|")), + treats: this.makeTreatmentSet( + json.results.bindings[0].tntreats?.value.split("|"), + ), + cite: this.makeTreatmentSet( + json.results.bindings[0].tncites?.value.split("|"), + ), }, }; } - private makeTreatmentSet (urls?: string[]): Set { + private makeTreatmentSet(urls?: string[]): Set { if (!urls) return new Set(); return new Set( urls.filter((url) => !!url).map((url) => { @@ -221,7 +265,7 @@ LIMIT 500`; return this.treatments.get(url) as Treatment; }), ); - }; + } private async getTreatmentDetails( treatmentUri: string, @@ -356,7 +400,7 @@ SELECT DISTINCT ?url ?description WHERE { new Promise>( (resolve, reject) => { const callback = () => { - if (this.isAborted) { + if (this.controller.signal.aborted) { reject(new Error("SynyonymGroup has been aborted")); } else if (returnedSoFar < this.names.length) { resolve({ value: this.names[returnedSoFar++] }); diff --git a/example/cli.ts b/example/cli.ts index f685397..9fbaa26 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -6,13 +6,14 @@ const sparqlEndpoint = new SparqlEndpoint( ); const taxonName = Deno.args.length > 0 ? Deno.args.join(" ") - : "https://www.catalogueoflife.org/data/taxon/3CP83"; + : "https://www.catalogueoflife.org/data/taxon/4P523"; const synoGroup = new SynonymGroup(sparqlEndpoint, taxonName); console.log(Colors.blue(`Synonym Group For ${taxonName}`)); for await (const name of synoGroup) { console.log( - Colors.underline(name.displayName) + + "\n" + + Colors.underline(name.displayName) + colorizeIfPresent(name.taxonNameURI, "yellow"), ); for (const trt of name.treatments.treats) { From d58f9e6e187c770f6843df730529e999ce2dc3c6 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 12:58:15 +0100 Subject: [PATCH 04/71] mostly working for coL and tc --- SynonymGroup.ts | 410 ++++++++++++++++++++++++++++++++++++++++++------ example/cli.ts | 68 ++++++-- 2 files changed, 412 insertions(+), 66 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index fae09e2..579bcf5 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -52,10 +52,12 @@ export class SynonymGroup implements AsyncIterable { private freshSynonymQueue: string[] = []; /** contains TN, TC, CoL uris of synonyms whose (treatment-linked-) synonyms we have not yet looked for */ - private expandSynonymQueue: string[] = []; + // private expandSynonymQueue: string[] = []; + + private runningPromises: Promise[] = []; /** contains TN, TC, CoL uris of synonyms which are in-flight somehow */ - private expanded = new Map(); + private expanded = new Set(); // new Map(); /** Used internally to deduplicate treatments, maps from URI to Object */ private treatments = new Map(); @@ -77,39 +79,31 @@ export class SynonymGroup implements AsyncIterable { // TODO handle "Genus species"-style input if (taxonName.startsWith("http")) { - this.freshSynonymQueue.push(taxonName); + this.getName(taxonName).then(() => this.finish()); } - - this.driveQueue(); } - private async driveQueue() { - while ( - this.freshSynonymQueue.length > 0 || this.expandSynonymQueue.length > 0 - ) { - if (this.freshSynonymQueue.length > 0) { - const taxonName = this.freshSynonymQueue.pop()!; - if (taxonName.startsWith("https://www.catalogueoflife.org")) { - await this.getNameFromCol(taxonName).then((name) => { - this.pushName(name); - }); - } - } - // TODO handle expansion - console.log("Not expanding:", this.expandSynonymQueue.pop()) + private async getName(taxonName: string): Promise { + if (this.expanded.has(taxonName)) { + console.log("Skipping known", taxonName); + } else if (taxonName.startsWith("https://www.catalogueoflife.org")) { + await this.getNameFromCol(taxonName); + } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { + await this.getNameFromTC(taxonName); + } else { + console.log("// TODO handle", taxonName); } - this.finish(); } /** colUri must have been freshly popped from freshSynonymQueue */ - private async getNameFromCol(colUri: string): Promise { + private async getNameFromCol(colUri: string): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` PREFIX dwc: PREFIX dwcFP: PREFIX cito: -PREFIX treat: +PREFIX trt: SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) @@ -148,16 +142,16 @@ SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority FILTER NOT EXISTS { ?tn dwc:species ?species . } } - OPTIONAL { ?trtn treat:treatsTaxonName ?tn . } - OPTIONAL { ?citetn treat:citesTaxonName ?tn . } + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } OPTIONAL { - ?tc treat:hasTaxonName ?tn ; + ?tc trt:hasTaxonName ?tn ; dwc:scientificNameAuthorship ?tcauth ; a dwcFP:TaxonConcept . - OPTIONAL { ?aug treat:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def treat:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr treat:deprecates ?tc . } + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } OPTIONAL { ?cite cito:cites ?tc . } } } @@ -166,8 +160,7 @@ GROUP BY ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. - if (this.expanded.get(colUri)) throw "Called on previously expanded name"; - this.expanded.set(colUri, NameStatus.makingName); + if (this.expanded.has(colUri)) throw "Called on previously expanded name"; if (this.controller.signal?.aborted) return Promise.reject(); @@ -178,6 +171,8 @@ LIMIT 500`; "Starting Points", ); + const treatmentPromises: Promise[] = []; + const displayName: string = json.results.bindings[0].fullName!.value .replace( json.results.bindings[0].authority!.value, @@ -200,56 +195,289 @@ LIMIT 500`; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { - this.expanded.set(taxonNameURI, NameStatus.madeName); - this.expandSynonymQueue.push(taxonNameURI); + if (this.expanded.has(taxonNameURI)) { + console.log("Abbruch: already known", taxonNameURI); + return; + } + this.expanded.add(taxonNameURI); //, NameStatus.madeName); } for (const t of json.results.bindings) { - if (t.tc) { + if (t.tc && t.tcAuth?.value) { + if (this.expanded.has(t.tc.value)) { + console.log("Abbruch: already known", t.tc.value); + return; + } + const def = this.makeTreatmentSet(t.defs?.value.split("|")); + const aug = this.makeTreatmentSet(t.augs?.value.split("|")); + const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); + const cite = this.makeTreatmentSet(t.cites?.value.split("|")); if (t.tcAuth?.value.split(" / ").includes(colName.authority)) { colName.authority = t.tcAuth?.value; colName.taxonConceptURI = t.tc.value; colName.treatments = { - def: this.makeTreatmentSet(t.defs?.value.split("|")), - aug: this.makeTreatmentSet(t.augs?.value.split("|")), - dpr: this.makeTreatmentSet(t.dprs?.value.split("|")), - cite: this.makeTreatmentSet(t.cites?.value.split("|")), + def, + aug, + dpr, + cite, }; - } else if (t.tcAuth?.value) { + } else { authorizedNames.push({ displayName, authority: t.tcAuth.value, taxonConceptURI: t.tc.value, treatments: { - def: this.makeTreatmentSet(t.defs?.value.split("|")), - aug: this.makeTreatmentSet(t.augs?.value.split("|")), - dpr: this.makeTreatmentSet(t.dprs?.value.split("|")), - cite: this.makeTreatmentSet(t.cites?.value.split("|")), + def, + aug, + dpr, + cite, }, }); } - this.expanded.set(t.tc.value, NameStatus.madeName); - this.expandSynonymQueue.push(t.tc.value); + // this.expanded.set(t.tc.value, NameStatus.madeName); + this.expanded.add(t.tc.value); + + def.forEach((t) => treatmentPromises.push(t.details)); + aug.forEach((t) => treatmentPromises.push(t.details)); + dpr.forEach((t) => treatmentPromises.push(t.details)); } } // TODO: handle col-data "acceptedName" and stuff - this.expanded.set(colUri, NameStatus.done); + this.expanded.add(colUri); //, NameStatus.done); - return { + const treats = this.makeTreatmentSet( + json.results.bindings[0].tntreats?.value.split("|"), + ); + treats.forEach((t) => treatmentPromises.push(t.details)); + + this.pushName({ displayName, taxonNameURI, authorizedNames, justification: new JustificationSet(["//TODO"]), treatments: { - treats: this.makeTreatmentSet( - json.results.bindings[0].tntreats?.value.split("|"), + treats, + cite: this.makeTreatmentSet( + json.results.bindings[0].tncites?.value.split("|"), ), + }, + }); + + let newSynonyms = new Set(); + (await Promise.all(treatmentPromises)).map((d) => { + newSynonyms = newSynonyms + .union(d.treats.aug) + .union(d.treats.def) + .union(d.treats.dpr) + .union(d.treats.treattn) + .difference(this.expanded); + }); + + await Promise.allSettled( + [...newSynonyms].map((n) => this.getName(n)), + ); + } + + private async getNameFromTC(tcUri: string): Promise { + // Note: this query assumes that there is no sub-species taxa with missing dwc:species + // Note: the handling assumes that at most one taxon-name matches this colTaxon + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + <${tcUri}> trt:hasTaxonName ?tn . + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + + OPTIONAL { + ?col dwc:taxonRank ?rank . + ?col dwc:scientificNameAuthorship ?colAuth . + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + OPTIONAL { + ?col dwc:specificEpithet ?species . + OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } + } + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } +} +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +LIMIT 500`; + // For unclear reasons, the query breaks if the limit is removed. + + if (this.expanded.has(tcUri)) { + console.log("Abbruch: already known", tcUri); + return; + } + + if (this.controller.signal?.aborted) return Promise.reject(); + + /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + "Starting Points", + ); + + const treatmentPromises: Promise[] = []; + + const displayName: string = json.results.bindings[0].name!.value + .replace( + json.results.bindings[0].authority!.value, + "", + ).trim(); + + const colName: AuthorizedName | undefined = + json.results.bindings[0].col?.value + ? { + displayName, + authority: json.results.bindings[0].authority!.value, + colURI: json.results.bindings[0].col.value, + treatments: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + cite: new Set(), + }, + } + : undefined; + + if (colName) { + if (this.expanded.has(colName.colURI!)) { + console.log("Abbruch: already known", colName.colURI!); + return; + } + this.expanded.add(colName.colURI!); + } + + const authorizedNames = colName ? [colName] : []; + + const taxonNameURI = json.results.bindings[0].tn?.value; + if (taxonNameURI) { + if (this.expanded.has(taxonNameURI)) { + console.log("Abbruch: already known", taxonNameURI); + return; + } + this.expanded.add(taxonNameURI); //, NameStatus.madeName); + } + + for (const t of json.results.bindings) { + if (t.tc && t.tcAuth?.value) { + if (this.expanded.has(t.tc.value)) { + console.log("Abbruch: already known", t.tc.value); + return; + } + const def = this.makeTreatmentSet(t.defs?.value.split("|")); + const aug = this.makeTreatmentSet(t.augs?.value.split("|")); + const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); + const cite = this.makeTreatmentSet(t.cites?.value.split("|")); + if ( + colName && t.tcAuth?.value.split(" / ").includes(colName.authority) + ) { + colName.authority = t.tcAuth?.value; + colName.taxonConceptURI = t.tc.value; + colName.treatments = { + def, + aug, + dpr, + cite, + }; + } else { + authorizedNames.push({ + displayName, + authority: t.tcAuth.value, + taxonConceptURI: t.tc.value, + treatments: { + def, + aug, + dpr, + cite, + }, + }); + } + // this.expanded.set(t.tc.value, NameStatus.madeName); + this.expanded.add(t.tc.value); + + def.forEach((t) => treatmentPromises.push(t.details)); + aug.forEach((t) => treatmentPromises.push(t.details)); + dpr.forEach((t) => treatmentPromises.push(t.details)); + } + } + + // TODO: handle col-data "acceptedName" and stuff + this.expanded.add(tcUri); //, NameStatus.done); + + const treats = this.makeTreatmentSet( + json.results.bindings[0].tntreats?.value.split("|"), + ); + treats.forEach((t) => treatmentPromises.push(t.details)); + + this.pushName({ + displayName, + taxonNameURI, + authorizedNames, + justification: new JustificationSet(["//TODO"]), + treatments: { + treats, cite: this.makeTreatmentSet( json.results.bindings[0].tncites?.value.split("|"), ), }, - }; + }); + + let newSynonyms = new Set(); + (await Promise.all(treatmentPromises)).map((d) => { + newSynonyms = newSynonyms + .union(d.treats.aug) + .union(d.treats.def) + .union(d.treats.dpr) + .union(d.treats.treattn) + .difference(this.expanded); + }); + + await Promise.allSettled( + [...newSynonyms].map((n) => this.getName(n)), + ); } private makeTreatmentSet(urls?: string[]): Set { @@ -257,10 +485,12 @@ LIMIT 500`; return new Set( urls.filter((url) => !!url).map((url) => { if (!this.treatments.has(url)) { + const details = this.getTreatmentDetails(url); this.treatments.set(url, { url, - details: this.getTreatmentDetails(url), + details, }); + this.runningPromises.push(details); } return this.treatments.get(url) as Treatment; }), @@ -273,6 +503,8 @@ LIMIT 500`; const query = ` PREFIX dc: PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: PREFIX trt: SELECT DISTINCT ?date ?title ?mc @@ -295,11 +527,23 @@ SELECT DISTINCT (group_concat(DISTINCT ?gbifSpecimenId;separator=" / ") as ?gbifSpecimenIds) (group_concat(DISTINCT ?creator;separator="; ") as ?creators) (group_concat(DISTINCT ?httpUri;separator="|") as ?httpUris) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trttn;separator="|") as ?trttns) + (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) WHERE { BIND (<${treatmentUri}> as ?treatment) ?treatment dc:creator ?creator . OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . } OPTIONAL { ?treatment dc:title ?title } + OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . } + OPTIONAL { ?treatment trt:definesTaxonConcept ?def . } + OPTIONAL { ?treatment trt:deprecates ?dpr . } + OPTIONAL { ?treatment cito:cites ?cite . ?cite a dwcFP:TaxonConcept . } + OPTIONAL { ?treatment trt:treatsTaxonName ?trttn . } + OPTIONAL { ?treatment trt:citesTaxonName ?citetn . } OPTIONAL { ?treatment dwc:basisOfRecord ?mc . ?mc dwc:catalogNumber ?catalogNumber . @@ -324,7 +568,18 @@ WHERE { } GROUP BY ?date ?title ?mc`; if (this.controller.signal.aborted) { - return { materialCitations: [], figureCitations: [] }; + return { + materialCitations: [], + figureCitations: [], + treats: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + citetc: new Set(), + treattn: new Set(), + citetn: new Set(), + }, + }; } try { const json = await this.sparqlEndpoint.getSparqlResultSet( @@ -385,10 +640,53 @@ SELECT DISTINCT ?url ?description WHERE { title: json.results.bindings[0]?.title?.value, materialCitations, figureCitations, + treats: { + def: new Set( + json.results.bindings[0]?.defs?.value + ? json.results.bindings[0].defs.value.split("|") + : undefined, + ), + aug: new Set( + json.results.bindings[0]?.augs?.value + ? json.results.bindings[0].augs.value.split("|") + : undefined, + ), + dpr: new Set( + json.results.bindings[0]?.dprs?.value + ? json.results.bindings[0].dprs.value.split("|") + : undefined, + ), + citetc: new Set( + json.results.bindings[0]?.cites?.value + ? json.results.bindings[0].cites.value.split("|") + : undefined, + ), + treattn: new Set( + json.results.bindings[0]?.trttns?.value + ? json.results.bindings[0].trttns.value.split("|") + : undefined, + ), + citetn: new Set( + json.results.bindings[0]?.citetns?.value + ? json.results.bindings[0].citetns.value.split("|") + : undefined, + ), + }, }; } catch (error) { console.warn("SPARQL Error: " + error); - return { materialCitations: [], figureCitations: [] }; + return { + materialCitations: [], + figureCitations: [], + treats: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + citetc: new Set(), + treattn: new Set(), + citetn: new Set(), + }, + }; } } @@ -501,6 +799,14 @@ export type TreatmentDetails = { date?: number; creators?: string; title?: string; + treats: { + def: Set; + aug: Set; + dpr: Set; + citetc: Set; + treattn: Set; + citetn: Set; + }; }; /** A cited material */ diff --git a/example/cli.ts b/example/cli.ts index 9fbaa26..d75fc45 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -9,6 +9,13 @@ const taxonName = Deno.args.length > 0 : "https://www.catalogueoflife.org/data/taxon/4P523"; const synoGroup = new SynonymGroup(sparqlEndpoint, taxonName); +const trtColor = { + "def": Colors.green, + "aug": Colors.blue, + "dpr": Colors.red, + "cite": Colors.gray, +}; + console.log(Colors.blue(`Synonym Group For ${taxonName}`)); for await (const name of synoGroup) { console.log( @@ -16,12 +23,9 @@ for await (const name of synoGroup) { Colors.underline(name.displayName) + colorizeIfPresent(name.taxonNameURI, "yellow"), ); - for (const trt of name.treatments.treats) { - console.log(Colors.blue(" ● ") + await treatmentToString(trt)); - } - for (const trt of name.treatments.cite) { - console.log(Colors.gray(" ● ") + await treatmentToString(trt)); - } + for (const trt of name.treatments.treats) await logTreatment(trt, "aug"); + for (const trt of name.treatments.cite) await logTreatment(trt, "cite"); + // TODO justification for (const authorizedName of name.authorizedNames) { console.log( @@ -34,16 +38,16 @@ for await (const name of synoGroup) { colorizeIfPresent(authorizedName.colURI, "cyan"), ); for (const trt of authorizedName.treatments.def) { - console.log(Colors.green(" ● ") + await treatmentToString(trt)); + await logTreatment(trt, "def"); } for (const trt of authorizedName.treatments.aug) { - console.log(Colors.blue(" ● ") + await treatmentToString(trt)); + await logTreatment(trt, "aug"); } for (const trt of authorizedName.treatments.dpr) { - console.log(Colors.red(" ● ") + await treatmentToString(trt)); + await logTreatment(trt, "dpr"); } for (const trt of authorizedName.treatments.cite) { - console.log(Colors.gray(" ● ") + await treatmentToString(trt)); + await logTreatment(trt, "cite"); } // TODO justification } @@ -57,9 +61,45 @@ function colorizeIfPresent( else return ""; } -async function treatmentToString(trt: Treatment) { +async function logTreatment( + trt: Treatment, + type: "def" | "aug" | "dpr" | "cite", +) { const details = await trt.details; - return `${details.creators} ${details.date} “${ - Colors.italic(details.title || Colors.dim("No Title")) - }” ${Colors.magenta(trt.url)}`; + console.log( + ` ${trtColor[type]("●")} ${details.creators} ${details.date} “${ + Colors.italic(details.title || Colors.dim("No Title")) + }” ${Colors.magenta(trt.url)}`, + ); + if (type !== "def" && details.treats.def.size > 0) { + console.log( + ` → ${trtColor.def("●")} ${ + Colors.magenta( + [...details.treats.def.values()].join(", "), + ) + }`, + ); + } + if ( + type !== "aug" && + (details.treats.aug.size > 0 || details.treats.treattn.size > 0) + ) { + console.log( + ` → ${trtColor.aug("●")} ${ + Colors.magenta( + [...details.treats.aug.values(), ...details.treats.treattn.values()] + .join(", "), + ) + }`, + ); + } + if (type !== "dpr" && details.treats.dpr.size > 0) { + console.log( + ` → ${trtColor.dpr("●")} ${ + Colors.magenta( + [...details.treats.dpr.values()].join(", "), + ) + }`, + ); + } } From cef7cd988a3b9001e3668da3db9dd4d80b745cf1 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:18:32 +0100 Subject: [PATCH 05/71] small cleanup --- SynonymGroup.ts | 41 +++++++++++++++-------------------------- example/cli.ts | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 579bcf5..f47dda5 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -40,27 +40,17 @@ export class SynonymGroup implements AsyncIterable { this.monitor.dispatchEvent(new CustomEvent("updated")); } - // An URI of a name can have on of the following states: - // 1) we have found it as a synonym but have not gotten the details - // -> in this.freshSynonymQueue - // 2) we have loaded the details and made a `Name` from it. All authorizedNames of the `Name` become at least 3).* - // 3) we are looking for the (treatment-linked-) synonyms of the name. When we get them, they all become at least 1).* - // 4) we are done with this URI. - // * if the name already has a higher state, it is not downgraded. - - /** contains TN, TC, CoL uris of synonyms whose `Name` we have not yet constructed */ - private freshSynonymQueue: string[] = []; - - /** contains TN, TC, CoL uris of synonyms whose (treatment-linked-) synonyms we have not yet looked for */ - // private expandSynonymQueue: string[] = []; - - private runningPromises: Promise[] = []; - - /** contains TN, TC, CoL uris of synonyms which are in-flight somehow */ + /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */ private expanded = new Set(); // new Map(); - /** Used internally to deduplicate treatments, maps from URI to Object */ - private treatments = new Map(); + /** + * Used internally to deduplicate treatments, maps from URI to Object. + * + * Contains full list of treatments _if_ .isFinished and not .isAborted + * + * @readonly + */ + treatments = new Map(); /** * Constructs a SynonymGroup @@ -196,7 +186,7 @@ LIMIT 500`; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { if (this.expanded.has(taxonNameURI)) { - console.log("Abbruch: already known", taxonNameURI); + // console.log("Abbruch: already known", taxonNameURI); return; } this.expanded.add(taxonNameURI); //, NameStatus.madeName); @@ -205,7 +195,7 @@ LIMIT 500`; for (const t of json.results.bindings) { if (t.tc && t.tcAuth?.value) { if (this.expanded.has(t.tc.value)) { - console.log("Abbruch: already known", t.tc.value); + // console.log("Abbruch: already known", t.tc.value); return; } const def = this.makeTreatmentSet(t.defs?.value.split("|")); @@ -346,7 +336,7 @@ LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. if (this.expanded.has(tcUri)) { - console.log("Abbruch: already known", tcUri); + // console.log("Abbruch: already known", tcUri); return; } @@ -384,7 +374,7 @@ LIMIT 500`; if (colName) { if (this.expanded.has(colName.colURI!)) { - console.log("Abbruch: already known", colName.colURI!); + // console.log("Abbruch: already known", colName.colURI!); return; } this.expanded.add(colName.colURI!); @@ -395,7 +385,7 @@ LIMIT 500`; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { if (this.expanded.has(taxonNameURI)) { - console.log("Abbruch: already known", taxonNameURI); + // console.log("Abbruch: already known", taxonNameURI); return; } this.expanded.add(taxonNameURI); //, NameStatus.madeName); @@ -404,7 +394,7 @@ LIMIT 500`; for (const t of json.results.bindings) { if (t.tc && t.tcAuth?.value) { if (this.expanded.has(t.tc.value)) { - console.log("Abbruch: already known", t.tc.value); + // console.log("Abbruch: already known", t.tc.value); return; } const def = this.makeTreatmentSet(t.defs?.value.split("|")); @@ -490,7 +480,6 @@ LIMIT 500`; url, details, }); - this.runningPromises.push(details); } return this.treatments.get(url) as Treatment; }), diff --git a/example/cli.ts b/example/cli.ts index d75fc45..159f795 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -6,7 +6,7 @@ const sparqlEndpoint = new SparqlEndpoint( ); const taxonName = Deno.args.length > 0 ? Deno.args.join(" ") - : "https://www.catalogueoflife.org/data/taxon/4P523"; + : "https://www.catalogueoflife.org/data/taxon/3WD9M"; // "https://www.catalogueoflife.org/data/taxon/4P523"; const synoGroup = new SynonymGroup(sparqlEndpoint, taxonName); const trtColor = { @@ -17,6 +17,10 @@ const trtColor = { }; console.log(Colors.blue(`Synonym Group For ${taxonName}`)); + +let authorizedNamesCount = 0; +const timeStart = performance.now(); + for await (const name of synoGroup) { console.log( "\n" + @@ -28,6 +32,7 @@ for await (const name of synoGroup) { // TODO justification for (const authorizedName of name.authorizedNames) { + authorizedNamesCount++; console.log( " " + Colors.underline( @@ -53,6 +58,17 @@ for await (const name of synoGroup) { } } +const timeEnd = performance.now(); + +console.log( + "\n" + + Colors.bgYellow( + `Found ${synoGroup.names.length} names (${authorizedNamesCount} authorized names) and ${synoGroup.treatments.size} treatments. This took ${ + timeEnd - timeStart + } milliseconds.`, + ), +); + function colorizeIfPresent( text: string | undefined, color: "gray" | "yellow" | "green" | "cyan", From 18c96a668f2a56c70352ff5e68e0fbb12f5ca164 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:29:39 +0100 Subject: [PATCH 06/71] deduplicated code --- SynonymGroup.ts | 129 ++++-------------------------------------------- 1 file changed, 10 insertions(+), 119 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index f47dda5..87c8b52 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,11 +1,4 @@ -import { JustificationSet, SparqlEndpoint } from "./mod.ts"; - -enum NameStatus { - makingName, - madeName, - findingSynonyms, - done, -} +import { JustificationSet, SparqlEndpoint, SparqlJson } from "./mod.ts"; /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -94,7 +87,7 @@ PREFIX dwc: PREFIX dwcFP: PREFIX cito: PREFIX trt: -SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) @@ -105,7 +98,7 @@ SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority BIND(<${colUri}> as ?col) ?col dwc:taxonRank ?rank . ?col dwc:scientificNameAuthorship ?authority . - ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . OPTIONAL { ?col dwc:specificEpithet ?species . @@ -146,7 +139,7 @@ SELECT DISTINCT ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority } } } -GROUP BY ?tn ?tc ?rank ?genus ?species ?infrasp ?fullName ?authority +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. @@ -154,119 +147,14 @@ LIMIT 500`; if (this.controller.signal?.aborted) return Promise.reject(); - /// ?tn ?tc !rank !genus ?species ?infrasp !fullName !authority ?tcAuth + /// ?tn ?tc !rank !genus ?species ?infrasp !name !authority ?tcAuth const json = await this.sparqlEndpoint.getSparqlResultSet( query, { signal: this.controller.signal }, "Starting Points", ); - const treatmentPromises: Promise[] = []; - - const displayName: string = json.results.bindings[0].fullName!.value - .replace( - json.results.bindings[0].authority!.value, - "", - ).trim(); - - const colName: AuthorizedName = { - displayName, - authority: json.results.bindings[0].authority!.value, - colURI: colUri, - treatments: { - def: new Set(), - aug: new Set(), - dpr: new Set(), - cite: new Set(), - }, - }; - - const authorizedNames = [colName]; - - const taxonNameURI = json.results.bindings[0].tn?.value; - if (taxonNameURI) { - if (this.expanded.has(taxonNameURI)) { - // console.log("Abbruch: already known", taxonNameURI); - return; - } - this.expanded.add(taxonNameURI); //, NameStatus.madeName); - } - - for (const t of json.results.bindings) { - if (t.tc && t.tcAuth?.value) { - if (this.expanded.has(t.tc.value)) { - // console.log("Abbruch: already known", t.tc.value); - return; - } - const def = this.makeTreatmentSet(t.defs?.value.split("|")); - const aug = this.makeTreatmentSet(t.augs?.value.split("|")); - const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); - const cite = this.makeTreatmentSet(t.cites?.value.split("|")); - if (t.tcAuth?.value.split(" / ").includes(colName.authority)) { - colName.authority = t.tcAuth?.value; - colName.taxonConceptURI = t.tc.value; - colName.treatments = { - def, - aug, - dpr, - cite, - }; - } else { - authorizedNames.push({ - displayName, - authority: t.tcAuth.value, - taxonConceptURI: t.tc.value, - treatments: { - def, - aug, - dpr, - cite, - }, - }); - } - // this.expanded.set(t.tc.value, NameStatus.madeName); - this.expanded.add(t.tc.value); - - def.forEach((t) => treatmentPromises.push(t.details)); - aug.forEach((t) => treatmentPromises.push(t.details)); - dpr.forEach((t) => treatmentPromises.push(t.details)); - } - } - - // TODO: handle col-data "acceptedName" and stuff - this.expanded.add(colUri); //, NameStatus.done); - - const treats = this.makeTreatmentSet( - json.results.bindings[0].tntreats?.value.split("|"), - ); - treats.forEach((t) => treatmentPromises.push(t.details)); - - this.pushName({ - displayName, - taxonNameURI, - authorizedNames, - justification: new JustificationSet(["//TODO"]), - treatments: { - treats, - cite: this.makeTreatmentSet( - json.results.bindings[0].tncites?.value.split("|"), - ), - }, - }); - - let newSynonyms = new Set(); - (await Promise.all(treatmentPromises)).map((d) => { - newSynonyms = newSynonyms - .union(d.treats.aug) - .union(d.treats.def) - .union(d.treats.dpr) - .union(d.treats.treattn) - .difference(this.expanded); - }); - - await Promise.allSettled( - [...newSynonyms].map((n) => this.getName(n)), - ); + await this.handleName(json); } private async getNameFromTC(tcUri: string): Promise { @@ -349,6 +237,10 @@ LIMIT 500`; "Starting Points", ); + await this.handleName(json); + } + + private async handleName(json: SparqlJson): Promise { const treatmentPromises: Promise[] = []; const displayName: string = json.results.bindings[0].name!.value @@ -435,7 +327,6 @@ LIMIT 500`; } // TODO: handle col-data "acceptedName" and stuff - this.expanded.add(tcUri); //, NameStatus.done); const treats = this.makeTreatmentSet( json.results.bindings[0].tntreats?.value.split("|"), From aaaa4691b99354c2abe1a31656e40e3186269caa Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:37:44 +0100 Subject: [PATCH 07/71] added support for TNuris --- SynonymGroup.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 87c8b52..42bead5 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -73,12 +73,13 @@ export class SynonymGroup implements AsyncIterable { await this.getNameFromCol(taxonName); } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { await this.getNameFromTC(taxonName); + } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { + await this.getNameFromTN(taxonName); } else { console.log("// TODO handle", taxonName); } } - /** colUri must have been freshly popped from freshSynonymQueue */ private async getNameFromCol(colUri: string): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon @@ -143,8 +144,6 @@ GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. - if (this.expanded.has(colUri)) throw "Called on previously expanded name"; - if (this.controller.signal?.aborted) return Promise.reject(); /// ?tn ?tc !rank !genus ?species ?infrasp !name !authority ?tcAuth @@ -223,14 +222,87 @@ GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority LIMIT 500`; // For unclear reasons, the query breaks if the limit is removed. - if (this.expanded.has(tcUri)) { - // console.log("Abbruch: already known", tcUri); - return; + if (this.controller.signal?.aborted) return Promise.reject(); + + /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + "Starting Points", + ); + + await this.handleName(json); + } + + private async getNameFromTN(tnUri: string): Promise { + // Note: this query assumes that there is no sub-species taxa with missing dwc:species + // Note: the handling assumes that at most one taxon-name matches this colTaxon + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + BIND(<${tnUri}> as ?tn) + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + + OPTIONAL { + ?col dwc:taxonRank ?rank . + ?col dwc:scientificNameAuthorship ?colAuth . + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + + OPTIONAL { + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } + } +} +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +LIMIT 500`; + // For unclear reasons, the query breaks if the limit is removed. if (this.controller.signal?.aborted) return Promise.reject(); - /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth const json = await this.sparqlEndpoint.getSparqlResultSet( query, { signal: this.controller.signal }, From bd30d55844b73f97249a7e3842772b63b0261146 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:39:26 +0100 Subject: [PATCH 08/71] small tweak --- SynonymGroup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 42bead5..a35db6d 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -180,16 +180,16 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn a dwcFP:TaxonName . ?tn dwc:rank ?rank . ?tn dwc:genus ?genus . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } OPTIONAL { ?col dwc:taxonRank ?rank . ?col dwc:scientificNameAuthorship ?colAuth . ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . - OPTIONAL { - ?col dwc:specificEpithet ?species . - OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } - } { ?col dwc:specificEpithet ?species . From b253e7daa40172b2184d1aed85490535d4bc432f Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:06:24 +0100 Subject: [PATCH 09/71] justifications! --- SynonymGroup.ts | 102 +++++++++++++++++++++++++++++++++--------------- example/cli.ts | 23 ++++++++++- mod.ts | 3 +- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index a35db6d..aa147a5 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,4 +1,4 @@ -import { JustificationSet, SparqlEndpoint, SparqlJson } from "./mod.ts"; +import { SparqlEndpoint, SparqlJson } from "./mod.ts"; /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -62,25 +62,31 @@ export class SynonymGroup implements AsyncIterable { // TODO handle "Genus species"-style input if (taxonName.startsWith("http")) { - this.getName(taxonName).then(() => this.finish()); + this.getName(taxonName, { searchTerm: true }).then(() => this.finish()); } } - private async getName(taxonName: string): Promise { + private async getName( + taxonName: string, + justification: Justification, + ): Promise { if (this.expanded.has(taxonName)) { console.log("Skipping known", taxonName); } else if (taxonName.startsWith("https://www.catalogueoflife.org")) { - await this.getNameFromCol(taxonName); + await this.getNameFromCol(taxonName, justification); } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { - await this.getNameFromTC(taxonName); + await this.getNameFromTC(taxonName, justification); } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { - await this.getNameFromTN(taxonName); + await this.getNameFromTN(taxonName, justification); } else { console.log("// TODO handle", taxonName); } } - private async getNameFromCol(colUri: string): Promise { + private async getNameFromCol( + colUri: string, + justification: Justification, + ): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` @@ -153,10 +159,13 @@ LIMIT 500`; "Starting Points", ); - await this.handleName(json); + await this.handleName(json, justification); } - private async getNameFromTC(tcUri: string): Promise { + private async getNameFromTC( + tcUri: string, + justification: Justification, + ): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` @@ -231,10 +240,13 @@ LIMIT 500`; "Starting Points", ); - await this.handleName(json); + await this.handleName(json, justification); } - private async getNameFromTN(tnUri: string): Promise { + private async getNameFromTN( + tnUri: string, + justification: Justification, + ): Promise { // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` @@ -309,11 +321,14 @@ LIMIT 500`; "Starting Points", ); - await this.handleName(json); + await this.handleName(json, justification); } - private async handleName(json: SparqlJson): Promise { - const treatmentPromises: Promise[] = []; + private async handleName( + json: SparqlJson, + justification: Justification, + ): Promise { + const treatmentPromises: Treatment[] = []; const displayName: string = json.results.bindings[0].name!.value .replace( @@ -392,9 +407,9 @@ LIMIT 500`; // this.expanded.set(t.tc.value, NameStatus.madeName); this.expanded.add(t.tc.value); - def.forEach((t) => treatmentPromises.push(t.details)); - aug.forEach((t) => treatmentPromises.push(t.details)); - dpr.forEach((t) => treatmentPromises.push(t.details)); + def.forEach((t) => treatmentPromises.push(t)); + aug.forEach((t) => treatmentPromises.push(t)); + dpr.forEach((t) => treatmentPromises.push(t)); } } @@ -403,33 +418,49 @@ LIMIT 500`; const treats = this.makeTreatmentSet( json.results.bindings[0].tntreats?.value.split("|"), ); - treats.forEach((t) => treatmentPromises.push(t.details)); + treats.forEach((t) => treatmentPromises.push(t)); - this.pushName({ + const name: Name = { displayName, taxonNameURI, authorizedNames, - justification: new JustificationSet(["//TODO"]), + justification, treatments: { treats, cite: this.makeTreatmentSet( json.results.bindings[0].tncites?.value.split("|"), ), }, - }); - - let newSynonyms = new Set(); - (await Promise.all(treatmentPromises)).map((d) => { - newSynonyms = newSynonyms - .union(d.treats.aug) - .union(d.treats.def) - .union(d.treats.dpr) - .union(d.treats.treattn) - .difference(this.expanded); + }; + this.pushName(name); + + /** Map */ + const newSynonyms = new Map(); + (await Promise.all( + treatmentPromises.map((treat) => + treat.details.then((d): [Treatment, TreatmentDetails] => { + return [treat, d]; + }) + ), + )).map(([treat, d]) => { + d.treats.aug.difference(this.expanded).forEach((s) => + newSynonyms.set(s, treat) + ); + d.treats.def.difference(this.expanded).forEach((s) => + newSynonyms.set(s, treat) + ); + d.treats.dpr.difference(this.expanded).forEach((s) => + newSynonyms.set(s, treat) + ); + d.treats.treattn.difference(this.expanded).forEach((s) => + newSynonyms.set(s, treat) + ); }); await Promise.allSettled( - [...newSynonyms].map((n) => this.getName(n)), + [...newSynonyms].map(([n, treatment]) => + this.getName(n, { searchTerm: false, parent: name, treatment }) + ), ); } @@ -697,7 +728,7 @@ export type Name = { authorizedNames: AuthorizedName[]; /** How this name was found */ - justification: JustificationSet; + justification: Justification; /** treatments directly associated with .taxonNameUri */ treatments: { @@ -706,6 +737,13 @@ export type Name = { }; }; +/** Why a given Name was found (ther migth be other possible justifications) */ +export type Justification = { searchTerm: true } | { + searchTerm: false; + parent: Name; + treatment: Treatment; +}; + /** * Corresponds to a taxon-concept or a CoL-Taxon */ diff --git a/example/cli.ts b/example/cli.ts index 159f795..b756f37 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,5 +1,10 @@ import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; -import { SparqlEndpoint, SynonymGroup, Treatment } from "../mod.ts"; +import { + Justification, + SparqlEndpoint, + SynonymGroup, + Treatment, +} from "../mod.ts"; const sparqlEndpoint = new SparqlEndpoint( "https://treatment.ld.plazi.org/sparql", @@ -27,6 +32,7 @@ for await (const name of synoGroup) { Colors.underline(name.displayName) + colorizeIfPresent(name.taxonNameURI, "yellow"), ); + await logJustification(name.justification); for (const trt of name.treatments.treats) await logTreatment(trt, "aug"); for (const trt of name.treatments.cite) await logTreatment(trt, "cite"); @@ -119,3 +125,18 @@ async function logTreatment( ); } } + +async function logJustification(justification: Justification) { + if (justification.searchTerm) { + console.log(Colors.dim(` (This is the search term)`)); + } else { + const details = await justification.treatment.details; + console.log( + Colors.dim( + ` (Justification: Synonym of ${justification.parent.displayName} according to ${details.creators} ${details.date} “${ + Colors.italic(details.title || Colors.dim("No Title")) + }” ${Colors.magenta(justification.treatment.url)})`, + ), + ); + } +} diff --git a/mod.ts b/mod.ts index 593c6a8..b2ef36b 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,2 @@ export * from "./SparqlEndpoint.ts"; -export * from "./JustificationSet.ts"; -export * from "./SynonymGroup.ts" \ No newline at end of file +export * from "./SynonymGroup.ts"; From 7507c727af37f1d536348867a1a76d3a82860df6 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:11:39 +0100 Subject: [PATCH 10/71] doc cleanup --- JustificationSet.ts | 123 -------------------------------------------- SynonymGroup.ts | 22 ++++++++ 2 files changed, 22 insertions(+), 123 deletions(-) delete mode 100644 JustificationSet.ts diff --git a/JustificationSet.ts b/JustificationSet.ts deleted file mode 100644 index 4076aac..0000000 --- a/JustificationSet.ts +++ /dev/null @@ -1,123 +0,0 @@ -// @ts-ignore: Import unneccesary for typings, will collate .d.ts files -import type { JustifiedSynonym, Treatment } from "./mod.ts"; - -/** //TODO */ -export type Justification = { - /** //TODO */ - toString: () => string; - /** //TODO */ - treatment?: Treatment; - /** //TODO */ - precedingSynonym?: JustifiedSynonym; // eslint-disable-line no-use-before-define -} - -/** //TODO */ -export class JustificationSet implements AsyncIterable { - /** @internal */ - private monitor = new EventTarget(); - /** @internal */ - contents: Justification[] = []; - /** @internal */ - isFinished = false; - /** @internal */ - isAborted = false; - /** @internal */ - entries = ((Array.from(this.contents.values()).map((v) => [v, v])) as [ - Justification, - Justification, - ][]).values; - /** @internal */ - constructor(iterable?: Iterable) { - if (iterable) { - for (const el of iterable) { - this.add(el); - } - } - return this; - } - /** @internal */ - get size() { - return new Promise((resolve, reject) => { - if (this.isAborted) { - reject(new Error("JustificationSet has been aborted")); - } else if (this.isFinished) { - resolve(this.contents.length); - } else { - const listener = () => { - if (this.isFinished) { - this.monitor.removeEventListener("updated", listener); - resolve(this.contents.length); - } - }; - this.monitor.addEventListener("updated", listener); - } - }); - } - - /** @internal */ - add(value: Justification) { - if ( - this.contents.findIndex((c) => c.toString() === value.toString()) === -1 - ) { - this.contents.push(value); - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - return this; - } - - /** @internal */ - finish(): void { - //console.info("%cJustificationSet finished", "color: #69F0AE;"); - this.isFinished = true; - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - - /** @internal */ - forEachCurrent(cb: (val: Justification) => void): void { - this.contents.forEach(cb); - } - - /** @internal */ - first(): Promise { - return new Promise((resolve) => { - if (this.contents[0]) { - resolve(this.contents[0]); - } else { - this.monitor.addEventListener("update", () => { - resolve(this.contents[0]); - }); - } - }); - } - - /** //TODO */ - [Symbol.asyncIterator](): AsyncIterator { - // this.monitor.addEventListener("updated", () => console.log("ARA")); - let returnedSoFar = 0; - return { - next: () => { - return new Promise>( - (resolve, reject) => { - const _ = () => { - if (this.isAborted) { - reject(new Error("JustificationSet has been aborted")); - } else if (returnedSoFar < this.contents.length) { - resolve({ value: this.contents[returnedSoFar++] }); - } else if (this.isFinished) { - resolve({ done: true, value: true }); - } else { - const listener = () => { - console.log("ahgfd"); - this.monitor.removeEventListener("updated", listener); - _(); - }; - this.monitor.addEventListener("updated", listener); - } - }; - _(); - }, - ); - }, - }; - } -} diff --git a/SynonymGroup.ts b/SynonymGroup.ts index aa147a5..92a7908 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -24,10 +24,21 @@ export class SynonymGroup implements AsyncIterable { * @readonly */ names: Name[] = []; + /** + * Add a new Name to this.names. + * + * Note: does not deduplicate on its own + * + * @internal */ private pushName(name: Name) { this.names.push(name); this.monitor.dispatchEvent(new CustomEvent("updated")); } + + /** + * Call when all synonyms are found + * + * @internal */ private finish() { this.isFinished = true; this.monitor.dispatchEvent(new CustomEvent("updated")); @@ -66,6 +77,7 @@ export class SynonymGroup implements AsyncIterable { } } + /** @internal */ private async getName( taxonName: string, justification: Justification, @@ -83,6 +95,7 @@ export class SynonymGroup implements AsyncIterable { } } + /** @internal */ private async getNameFromCol( colUri: string, justification: Justification, @@ -162,6 +175,7 @@ LIMIT 500`; await this.handleName(json, justification); } + /** @internal */ private async getNameFromTC( tcUri: string, justification: Justification, @@ -243,6 +257,8 @@ LIMIT 500`; await this.handleName(json, justification); } + + /** @internal */ private async getNameFromTN( tnUri: string, justification: Justification, @@ -324,6 +340,10 @@ LIMIT 500`; await this.handleName(json, justification); } + /** + * Note this makes some assumptions on which variables are present in the bindings + * + * @internal */ private async handleName( json: SparqlJson, justification: Justification, @@ -464,6 +484,7 @@ LIMIT 500`; ); } + /** @internal */ private makeTreatmentSet(urls?: string[]): Set { if (!urls) return new Set(); return new Set( @@ -480,6 +501,7 @@ LIMIT 500`; ); } + /** @internal */ private async getTreatmentDetails( treatmentUri: string, ): Promise { From 229f01028ac254d942c858dd099eb9b03508d2c3 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:44:59 +0100 Subject: [PATCH 11/71] latin names --- SynonymGroup.ts | 58 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 92a7908..28b86aa 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -26,9 +26,9 @@ export class SynonymGroup implements AsyncIterable { names: Name[] = []; /** * Add a new Name to this.names. - * + * * Note: does not deduplicate on its own - * + * * @internal */ private pushName(name: Name) { this.names.push(name); @@ -37,7 +37,7 @@ export class SynonymGroup implements AsyncIterable { /** * Call when all synonyms are found - * + * * @internal */ private finish() { this.isFinished = true; @@ -70,10 +70,17 @@ export class SynonymGroup implements AsyncIterable { ) { this.sparqlEndpoint = sparqlEndpoint; - // TODO handle "Genus species"-style input - if (taxonName.startsWith("http")) { this.getName(taxonName, { searchTerm: true }).then(() => this.finish()); + } else { + const name = [ + ...taxonName.split(" ").filter((n) => !!n), + undefined, + undefined, + ] as [string, string | undefined, string | undefined]; + this.getNameFromLatin(name, { searchTerm: true }).then(() => + this.finish() + ); } } @@ -91,10 +98,46 @@ export class SynonymGroup implements AsyncIterable { } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { await this.getNameFromTN(taxonName, justification); } else { - console.log("// TODO handle", taxonName); + throw `Cannot handle name-uri <${taxonName}> !`; } } + /** @internal */ + private async getNameFromLatin( + [genus, species, infrasp]: [string, string | undefined, string | undefined], + justification: Justification, + ): Promise { + const query = ` + PREFIX dwc: +SELECT DISTINCT ?uri WHERE { + ?uri dwc:genus|dwc:genericName "${genus}" . + ${ + species + ? `?uri dwc:species|dwc:specificEpithet "${species}" .` + : "FILTER NOT EXISTS { ?uri dwc:species|dwc:specificEpithet ?species . }" + } + ${ + infrasp + ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet "${infrasp}" .` + : "FILTER NOT EXISTS { ?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }" + } +} +LIMIT 500`; + + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + "Starting Points", + ); + + const names = json.results.bindings + .map((n) => n.uri?.value) + .filter((n) => n && !this.expanded.has(n)) as string[]; + + await Promise.allSettled(names.map((n) => this.getName(n, justification))); + } + /** @internal */ private async getNameFromCol( colUri: string, @@ -257,7 +300,6 @@ LIMIT 500`; await this.handleName(json, justification); } - /** @internal */ private async getNameFromTN( tnUri: string, @@ -342,7 +384,7 @@ LIMIT 500`; /** * Note this makes some assumptions on which variables are present in the bindings - * + * * @internal */ private async handleName( json: SparqlJson, From ce97f55765c6ee7c61fd8539f87b142d7c398d13 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:45:10 +0100 Subject: [PATCH 12/71] recursife justification display --- example/cli.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/example/cli.ts b/example/cli.ts index b756f37..ef5b485 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,6 +1,7 @@ import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; import { Justification, + Name, SparqlEndpoint, SynonymGroup, Treatment, @@ -32,11 +33,10 @@ for await (const name of synoGroup) { Colors.underline(name.displayName) + colorizeIfPresent(name.taxonNameURI, "yellow"), ); - await logJustification(name.justification); + await logJustification(name); for (const trt of name.treatments.treats) await logTreatment(trt, "aug"); for (const trt of name.treatments.cite) await logTreatment(trt, "cite"); - // TODO justification for (const authorizedName of name.authorizedNames) { authorizedNamesCount++; console.log( @@ -60,7 +60,6 @@ for await (const name of synoGroup) { for (const trt of authorizedName.treatments.cite) { await logTreatment(trt, "cite"); } - // TODO justification } } @@ -126,17 +125,23 @@ async function logTreatment( } } -async function logJustification(justification: Justification) { - if (justification.searchTerm) { - console.log(Colors.dim(` (This is the search term)`)); +async function logJustification(name: Name) { + const just = await justify(name); + console.log(Colors.dim(` (This ${just})`)); +} + +async function justify(name: Name): Promise { + if (name.justification.searchTerm) { + return "is the search term."; } else { - const details = await justification.treatment.details; - console.log( - Colors.dim( - ` (Justification: Synonym of ${justification.parent.displayName} according to ${details.creators} ${details.date} “${ + const details = await name.justification.treatment.details; + const parent = await justify(name.justification.parent); + return `is, according to ${ + Colors.italic( + `${details.creators} ${details.date} “${ Colors.italic(details.title || Colors.dim("No Title")) - }” ${Colors.magenta(justification.treatment.url)})`, - ), - ); + }” ${Colors.magenta(name.justification.treatment.url)}`, + ) + }, a synonym of ${name.justification.parent.displayName} \n which ${parent}`; } } From c61f3c089a0fb96042b96a3313cdcd7403becf8b Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:51:36 +0100 Subject: [PATCH 13/71] vernacular names --- SynonymGroup.ts | 32 +++++++++++++++++++++++++++++++- example/cli.ts | 9 +++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 28b86aa..7823264 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -493,6 +493,9 @@ LIMIT 500`; json.results.bindings[0].tncites?.value.split("|"), ), }, + vernacularNames: taxonNameURI + ? this.getVernacular(taxonNameURI) + : Promise.resolve(new Map()), }; this.pushName(name); @@ -526,6 +529,28 @@ LIMIT 500`; ); } + /** @internal */ + private async getVernacular(uri: string): Promise { + const result: vernacularNames = new Map(); + const query = + `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`; + const bindings = + (await this.sparqlEndpoint.getSparqlResultSet(query)).results.bindings; + for (const b of bindings) { + if (b.n?.value) { + if (b.n["xml:lang"]) { + if (result.has(b.n["xml:lang"])) { + result.get(b.n["xml:lang"])!.push(b.n.value); + } else result.set(b.n["xml:lang"], [b.n.value]); + } else { + if (result.has("??")) result.get("??")!.push(b.n.value); + else result.set("??", [b.n.value]); + } + } + } + return result; + } + /** @internal */ private makeTreatmentSet(urls?: string[]): Set { if (!urls) return new Set(); @@ -778,7 +803,7 @@ export type Name = { displayName: string; /** //TODO Promise? */ - // vernacularNames: Promise; + vernacularNames: Promise; // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank. // * //TODO Promise? */ // trees: Promise<{ @@ -801,6 +826,11 @@ export type Name = { }; }; +/** + * A map from language tags (IETF) to an array of vernacular names. + */ +export type vernacularNames = Map; + /** Why a given Name was found (ther migth be other possible justifications) */ export type Justification = { searchTerm: true } | { searchTerm: false; diff --git a/example/cli.ts b/example/cli.ts index ef5b485..a120387 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -33,6 +33,11 @@ for await (const name of synoGroup) { Colors.underline(name.displayName) + colorizeIfPresent(name.taxonNameURI, "yellow"), ); + const vernacular = await name.vernacularNames; + if (vernacular.size > 0) { + console.log(" “" + [...vernacular.values()].join("”, “") + "”"); + } + await logJustification(name); for (const trt of name.treatments.treats) await logTreatment(trt, "aug"); for (const trt of name.treatments.cite) await logTreatment(trt, "cite"); @@ -126,8 +131,8 @@ async function logTreatment( } async function logJustification(name: Name) { - const just = await justify(name); - console.log(Colors.dim(` (This ${just})`)); + const just = await justify(name); + console.log(Colors.dim(` (This ${just})`)); } async function justify(name: Name): Promise { From f8e0181c03ad81e851dc223c4c249301285c7583 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:29:57 +0100 Subject: [PATCH 14/71] foloow CoL-acceptedName-links --- SynonymGroup.ts | 136 +++++++++++++++++++++++++++++++++++++++++------- example/cli.ts | 36 +++++++++---- 2 files changed, 143 insertions(+), 29 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 7823264..2c62b3e 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -47,6 +47,12 @@ export class SynonymGroup implements AsyncIterable { /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */ private expanded = new Set(); // new Map(); + /** contains CoL uris where we don't need to check for Col "acceptedName" links + * + * col -> accepted col + */ + private acceptedCol = new Map(); + /** * Used internally to deduplicate treatments, maps from URI to Object. * @@ -56,6 +62,11 @@ export class SynonymGroup implements AsyncIterable { */ treatments = new Map(); + /** + * Whether to show taxa deprecated by CoL that would not have been found otherwise + */ + ignoreDeprecatedCoL: boolean; + /** * Constructs a SynonymGroup * @@ -66,9 +77,11 @@ export class SynonymGroup implements AsyncIterable { constructor( sparqlEndpoint: SparqlEndpoint, taxonName: string, + ignoreDeprecatedCoL = true, ignoreRank = false, ) { this.sparqlEndpoint = sparqlEndpoint; + this.ignoreDeprecatedCoL = ignoreDeprecatedCoL; if (taxonName.startsWith("http")) { this.getName(taxonName, { searchTerm: true }).then(() => this.finish()); @@ -160,7 +173,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { BIND(<${colUri}> as ?col) ?col dwc:taxonRank ?rank . - ?col dwc:scientificNameAuthorship ?authority . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . OPTIONAL { @@ -215,7 +228,7 @@ LIMIT 500`; "Starting Points", ); - await this.handleName(json, justification); + return this.handleName(json, justification); } /** @internal */ @@ -253,7 +266,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority OPTIONAL { ?col dwc:taxonRank ?rank . - ?col dwc:scientificNameAuthorship ?colAuth . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . @@ -331,7 +344,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority OPTIONAL { ?col dwc:taxonRank ?rank . - ?col dwc:scientificNameAuthorship ?colAuth . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . @@ -379,7 +392,7 @@ LIMIT 500`; "Starting Points", ); - await this.handleName(json, justification); + return this.handleName(json, justification); } /** @@ -414,10 +427,7 @@ LIMIT 500`; : undefined; if (colName) { - if (this.expanded.has(colName.colURI!)) { - // console.log("Abbruch: already known", colName.colURI!); - return; - } + if (this.expanded.has(colName.colURI!)) return; this.expanded.add(colName.colURI!); } @@ -425,10 +435,7 @@ LIMIT 500`; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { - if (this.expanded.has(taxonNameURI)) { - // console.log("Abbruch: already known", taxonNameURI); - return; - } + if (this.expanded.has(taxonNameURI)) return; this.expanded.add(taxonNameURI); //, NameStatus.madeName); } @@ -497,6 +504,16 @@ LIMIT 500`; ? this.getVernacular(taxonNameURI) : Promise.resolve(new Map()), }; + + let colPromises: Promise[] = []; + + if (colName) { + [colName.acceptedColURI, colPromises] = await this.getAcceptedCol( + colName.colURI!, + name, + ); + } + this.pushName(name); /** Map */ @@ -523,12 +540,83 @@ LIMIT 500`; }); await Promise.allSettled( - [...newSynonyms].map(([n, treatment]) => - this.getName(n, { searchTerm: false, parent: name, treatment }) - ), + [ + ...colPromises, + ...[...newSynonyms].map(([n, treatment]) => + this.getName(n, { searchTerm: false, parent: name, treatment }) + ), + ], ); } + /** @internal */ + private async getAcceptedCol( + colUri: string, + parent: Name, + ): Promise<[string, Promise[]]> { + const query = ` +PREFIX dwc: +SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator="|") AS ?dprs) WHERE { + BIND(<${colUri}> AS ?col) + { + ?col dwc:acceptedName ?current . + ?dpr dwc:acceptedName ?current . + ?current dwc:taxonomicStatus ?current_status . + } UNION { + ?col dwc:taxonomicStatus ?current_status . + OPTIONAL { ?dpr dwc:acceptedName ?col . } + FILTER NOT EXISTS { ?col dwc:acceptedName ?current . } + BIND(?col AS ?current) + } +} +GROUP BY ?current ?current_status`; + + if (this.acceptedCol.has(colUri)) { + return [this.acceptedCol.get(colUri)!, []]; + } + + const json = await this.sparqlEndpoint.getSparqlResultSet(query, { + signal: this.controller.signal, + }); + + const promises: Promise[] = []; + + for (const b of json.results.bindings) { + for (const dpr of b.dprs!.value.split("|")) { + if (dpr) { + if (!this.acceptedCol.has(b.current!.value)) { + this.acceptedCol.set(b.current!.value, b.current!.value); + promises.push( + this.getNameFromCol(b.current!.value, { + searchTerm: false, + parent, + }), + ); + } + + this.acceptedCol.set(dpr, b.current!.value); + if (!this.ignoreDeprecatedCoL) { + promises.push( + this.getNameFromCol(dpr, { searchTerm: false, parent }), + ); + } + } + } + } + + if (json.results.bindings.length === 0) { + // the provided colUri is not in CoL + // promises === [] + if (!this.acceptedCol.has(colUri)) { + this.acceptedCol.set(colUri, "INVALID COL"); + } + return [this.acceptedCol.get(colUri)!, promises]; + } + + if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri); + return [this.acceptedCol.get(colUri)!, promises]; + } + /** @internal */ private async getVernacular(uri: string): Promise { const result: vernacularNames = new Map(); @@ -802,10 +890,11 @@ export type Name = { /** Human-readable name */ displayName: string; - /** //TODO Promise? */ + /** vernacular names */ vernacularNames: Promise; + // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank. - // * //TODO Promise? */ + // * //TODO */ // trees: Promise<{ // col?: Tree; // tb?: Tree; @@ -835,7 +924,8 @@ export type vernacularNames = Map; export type Justification = { searchTerm: true } | { searchTerm: false; parent: Name; - treatment: Treatment; + /** if missing, indicates synonymy according to CoL */ + treatment?: Treatment; }; /** @@ -852,8 +942,16 @@ export type AuthorizedName = { /** The URI of the respective `dwcFP:TaxonConcept` if it exists */ taxonConceptURI?: string; + /** The URI of the respective CoL-taxon if it exists */ colURI?: string; + /** The URI of the corresponding accepted CoL-taxon if it exists. + * + * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon. + * + * May be the string "INVALID COL" if the colURI is not valid. + */ + acceptedColURI?: string; // TODO: sensible? // /** these are CoL-taxa linked in the rdf, which differ lexically */ diff --git a/example/cli.ts b/example/cli.ts index a120387..073509b 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,11 +1,7 @@ import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; -import { - Justification, - Name, - SparqlEndpoint, - SynonymGroup, - Treatment, -} from "../mod.ts"; +import { Name, SparqlEndpoint, SynonymGroup, Treatment } from "../mod.ts"; + +const HIDE_COL_ONLY_SYNONYMS = true; const sparqlEndpoint = new SparqlEndpoint( "https://treatment.ld.plazi.org/sparql", @@ -13,7 +9,11 @@ const sparqlEndpoint = new SparqlEndpoint( const taxonName = Deno.args.length > 0 ? Deno.args.join(" ") : "https://www.catalogueoflife.org/data/taxon/3WD9M"; // "https://www.catalogueoflife.org/data/taxon/4P523"; -const synoGroup = new SynonymGroup(sparqlEndpoint, taxonName); +const synoGroup = new SynonymGroup( + sparqlEndpoint, + taxonName, + HIDE_COL_ONLY_SYNONYMS, +); const trtColor = { "def": Colors.green, @@ -53,6 +53,19 @@ for await (const name of synoGroup) { colorizeIfPresent(authorizedName.taxonConceptURI, "yellow") + colorizeIfPresent(authorizedName.colURI, "cyan"), ); + if (authorizedName.colURI) { + if (authorizedName.acceptedColURI !== authorizedName.colURI) { + console.log( + ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ + trtColor.aug("●") + } ${Colors.cyan(authorizedName.acceptedColURI!)}`, + ); + } else { + console.log( + ` ${trtColor.aug("●")} Catalogue of Life`, + ); + } + } for (const trt of authorizedName.treatments.def) { await logTreatment(trt, "def"); } @@ -138,7 +151,7 @@ async function logJustification(name: Name) { async function justify(name: Name): Promise { if (name.justification.searchTerm) { return "is the search term."; - } else { + } else if (name.justification.treatment) { const details = await name.justification.treatment.details; const parent = await justify(name.justification.parent); return `is, according to ${ @@ -147,6 +160,9 @@ async function justify(name: Name): Promise { Colors.italic(details.title || Colors.dim("No Title")) }” ${Colors.magenta(name.justification.treatment.url)}`, ) - }, a synonym of ${name.justification.parent.displayName} \n which ${parent}`; + },\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + } else { + const parent = await justify(name.justification.parent); + return `is, according to the Catalogue of Life,\n a synonym of ${name.justification.parent.displayName} which ${parent}`; } } From 9676a3f2e19f66d3bcac2247608d8c4b355608d1 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:48:39 +0100 Subject: [PATCH 15/71] fixed kingdom-alignment --- SynonymGroup.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 2c62b3e..d0761e2 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -176,6 +176,8 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . + ?col dwc:parent* ?p . + ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . OPTIONAL { ?col dwc:specificEpithet ?species . OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } @@ -185,7 +187,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn a dwcFP:TaxonName . ?tn dwc:rank ?rank . ?tn dwc:genus ?genus . - + ?tn dwc:kingdom ?kingdom . { ?col dwc:specificEpithet ?species . ?tn dwc:species ?species . @@ -258,6 +260,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn a dwcFP:TaxonName . ?tn dwc:rank ?rank . + ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:species ?species . @@ -269,6 +272,9 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . + ?col dwc:parent* ?p . + ?p dwc:rank "kingdom" ; + dwc:taxonName ?kingdom . { ?col dwc:specificEpithet ?species . @@ -337,6 +343,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn a dwcFP:TaxonName . ?tn dwc:rank ?rank . ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . OPTIONAL { ?tn dwc:species ?species . OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } @@ -347,6 +354,8 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . + ?col dwc:parent* ?p . + ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . { ?col dwc:specificEpithet ?species . From 1e17c3bd61c16e83642970a5e37adfd9c40b3230 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:54:22 +0100 Subject: [PATCH 16/71] updated readme (removed index.html as it is currently outdated) --- README.md | 18 ++++++++++++++---- index.html | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28e41ed..64fe9a8 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,21 @@ A js module to get potential synonyms of a taxon name, the justifications for such synonymity and treatments about these taxon names or the respective taxa. -See `index.html` for an example of a webpage using the library. Go to -[http://plazi.github.io/synolib/](http://plazi.github.io/synolib/) to open the example page in the browser -and execute the script. +For a command line example using the library see: `example/cli.ts`. -For a simple command line example using the library see: `example/cli.ts`. +You can try it locally using Deno with + +```sh +deno run --allow-net ./example/cli.ts Ludwigia adscendens +# or +deno run --allow-net ./example/cli.ts http://taxon-name.plazi.org/id/Plantae/Ludwigia_adscendens +# or +deno run --allow-net ./example/cli.ts http://taxon-concept.plazi.org/id/Plantae/Ludwigia_adscendens_Linnaeus_1767 +# or +deno run --allow-net ./example/cli.ts https://www.catalogueoflife.org/data/taxon/3WD9M +``` + +(replace the argument with whatever name interests you) ## building diff --git a/index.html b/index.html index c220139..5445f17 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,8 @@ + BROKEN // TODO // +

You should see a list of synonyms here

+ + + \ No newline at end of file diff --git a/npm-package/build.mjs b/npm-package/build.mjs new file mode 100644 index 0000000..2e55e7c --- /dev/null +++ b/npm-package/build.mjs @@ -0,0 +1,13 @@ +import * as esbuild from "esbuild"; + +const BUILD = false; + +await esbuild.build({ + entryPoints: ["../mod.ts"], + sourcemap: true, + bundle: true, + format: "esm", + lineLimit: 120, + minify: BUILD ? true : false, + outfile: "./build/mod.js", +}); diff --git a/npm-package/build/mod.js b/npm-package/build/mod.js new file mode 100644 index 0000000..696e08f --- /dev/null +++ b/npm-package/build/mod.js @@ -0,0 +1,860 @@ +// ../SparqlEndpoint.ts +async function sleep(ms) { + const p = new Promise((resolve) => { + setTimeout(resolve, ms); + }); + return await p; +} +var SparqlEndpoint = class { + /** Create a new SparqlEndpoint with the given URI */ + constructor(sparqlEnpointUri) { + this.sparqlEnpointUri = sparqlEnpointUri; + } + /** @ignore */ + // reasons: string[] = []; + /** + * Run a query against the sparql endpoint + * + * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time. + * Retries are logged to the console (`console.warn`) + * + * @throws In case of non-ok response status codes or if fetch failed 10 times. + * @param query The sparql query to run against the endpoint + * @param fetchOptions Additional options for the `fetch` request + * @param _reason (Currently ignored, used internally for debugging purposes) + * @returns Results of the query + */ + async getSparqlResultSet(query, fetchOptions = {}, _reason = "") { + fetchOptions.headers = fetchOptions.headers || {}; + fetchOptions.headers["Accept"] = "application/sparql-results+json"; + let retryCount = 0; + const sendRequest = async () => { + try { + const response = await fetch( + this.sparqlEnpointUri + "?query=" + encodeURIComponent(query), + fetchOptions + ); + if (!response.ok) { + throw new Error("Response not ok. Status " + response.status); + } + return await response.json(); + } catch (error) { + if (fetchOptions.signal?.aborted) { + throw error; + } else if (retryCount < 10) { + const wait = 50 * (1 << retryCount++); + console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); + await sleep(wait); + return await sendRequest(); + } + console.warn("!! Fetch Error:", query, "\n---\n", error); + throw error; + } + }; + return await sendRequest(); + } +}; + +// ../SynonymGroup.ts +var SynonymGroup = class { + /** Indicates whether the SynonymGroup has found all synonyms. + * + * @readonly + */ + isFinished = false; + /** Used internally to watch for new names found */ + monitor = new EventTarget(); + /** Used internally to abort in-flight network requests when SynonymGroup is aborted */ + controller = new AbortController(); + /** The SparqlEndpoint used */ + sparqlEndpoint; + /** + * List of names found so-far. + * + * Contains full list of synonyms _if_ .isFinished and not .isAborted + * + * @readonly + */ + names = []; + /** + * Add a new Name to this.names. + * + * Note: does not deduplicate on its own + * + * @internal */ + pushName(name) { + this.names.push(name); + this.monitor.dispatchEvent(new CustomEvent("updated")); + } + /** + * Call when all synonyms are found + * + * @internal */ + finish() { + this.isFinished = true; + this.monitor.dispatchEvent(new CustomEvent("updated")); + } + /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */ + expanded = /* @__PURE__ */ new Set(); + // new Map(); + /** contains CoL uris where we don't need to check for Col "acceptedName" links + * + * col -> accepted col + */ + acceptedCol = /* @__PURE__ */ new Map(); + /** + * Used internally to deduplicate treatments, maps from URI to Object. + * + * Contains full list of treatments _if_ .isFinished and not .isAborted + * + * @readonly + */ + treatments = /* @__PURE__ */ new Map(); + /** + * Whether to show taxa deprecated by CoL that would not have been found otherwise. + * This significantly increases the number of results in some cases. + */ + ignoreDeprecatedCoL; + /** + * if set to true, subTaxa of the search term are also considered as starting points. + * + * Not that "weird" ranks like subGenus are always included when searching for a genus by latin name. + */ + startWithSubTaxa; + /** + * Constructs a SynonymGroup + * + * @param sparqlEndpoint SPARQL-Endpoint to query + * @param taxonName either a string of the form "Genus species infraspecific" (species & infraspecific names optional), or an URI of a http://filteredpush.org/ontologies/oa/dwcFP#TaxonConcept or ...#TaxonName or a CoL taxon URI + * @param [ignoreDeprecatedCoL=true] Whether to show taxa deprecated by CoL that would not have been found otherwise + * @param [startWithSubTaxa=false] if set to true, subTaxa of the search term are also considered as starting points. + */ + constructor(sparqlEndpoint, taxonName, ignoreDeprecatedCoL = true, startWithSubTaxa = false) { + this.sparqlEndpoint = sparqlEndpoint; + this.ignoreDeprecatedCoL = ignoreDeprecatedCoL; + this.startWithSubTaxa = startWithSubTaxa; + if (taxonName.startsWith("http")) { + this.getName(taxonName, { searchTerm: true, subTaxon: false }).finally( + () => this.finish() + ); + } else { + const name = [ + ...taxonName.split(" ").filter((n) => !!n), + void 0, + void 0 + ]; + this.getNameFromLatin(name, { searchTerm: true, subTaxon: false }).finally( + () => this.finish() + ); + } + } + /** @internal */ + async getName(taxonName, justification) { + if (this.expanded.has(taxonName)) { + console.log("Skipping known", taxonName); + return; + } + if (taxonName.startsWith("https://www.catalogueoflife.org")) { + await this.getNameFromCol(taxonName, justification); + } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { + await this.getNameFromTC(taxonName, justification); + } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { + await this.getNameFromTN(taxonName, justification); + } else { + throw `Cannot handle name-uri <${taxonName}> !`; + } + if (this.startWithSubTaxa && justification.searchTerm && !justification.subTaxon) { + await this.getSubtaxa(taxonName); + } + } + /** @internal */ + async getSubtaxa(url) { + const query = url.startsWith("http://taxon-concept.plazi.org") ? ` +PREFIX trt: +SELECT DISTINCT ?sub WHERE { + BIND(<${url}> as ?url) + ?sub trt:hasParentName*/^trt:hasTaxonName ?url . +} +LIMIT 5000` : ` +PREFIX dwc: +PREFIX trt: +SELECT DISTINCT ?sub WHERE { + BIND(<${url}> as ?url) + ?sub (dwc:parent|trt:hasParentName)* ?url . +} +LIMIT 5000`; + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `Subtaxa ${url}` + ); + const names = json.results.bindings.map((n) => n.sub?.value).filter((n) => n && !this.expanded.has(n)); + await Promise.allSettled( + names.map((n) => this.getName(n, { searchTerm: true, subTaxon: true })) + ); + } + /** @internal */ + async getNameFromLatin([genus, species, infrasp], justification) { + const query = ` + PREFIX dwc: +SELECT DISTINCT ?uri WHERE { + ?uri dwc:genus|dwc:genericName "${genus}" . + ${species ? `?uri dwc:species|dwc:specificEpithet "${species}" .` : "FILTER NOT EXISTS { ?uri dwc:species|dwc:specific\ +Epithet ?species . }"} + ${infrasp ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet "${infrasp}" .` : "FILTER NOT EXISTS { ?uri dwc:\ +subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }"} +} +LIMIT 500`; + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `NameFromLatin ${genus} ${species} ${infrasp}` + ); + const names = json.results.bindings.map((n) => n.uri?.value).filter((n) => n && !this.expanded.has(n)); + await Promise.allSettled(names.map((n) => this.getName(n, justification))); + } + /** @internal */ + async getNameFromCol(colUri, justification) { + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + BIND(<${colUri}> as ?col) + ?col dwc:taxonRank ?rank . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) + ?col dwc:scientificName ?name . # Note: contains authority + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + OPTIONAL { + ?col dwc:specificEpithet ?species . + OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } + } + + OPTIONAL { + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + + OPTIONAL { + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } + } + } +} +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +LIMIT 500`; + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `NameFromCol ${colUri}` + ); + return this.handleName(json, justification); + } + /** @internal */ + async getNameFromTC(tcUri, justification) { + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + <${tcUri}> trt:hasTaxonName ?tn . + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:kingdom ?kingdom . + ?tn dwc:genus ?genus . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + + OPTIONAL { + ?col dwc:taxonRank ?rank . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), \ +COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } +} +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +LIMIT 500`; + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `NameFromTC ${tcUri}` + ); + await this.handleName(json, justification); + } + /** @internal */ + async getNameFromTN(tnUri, justification) { + const query = ` +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { + BIND(<${tnUri}> as ?tn) + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + + OPTIONAL { + ?col dwc:taxonRank ?rank . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subspecies|dwc:variety ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), \ +COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } + OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + + OPTIONAL { + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } + OPTIONAL { ?def trt:definesTaxonConcept ?tc . } + OPTIONAL { ?dpr trt:deprecates ?tc . } + OPTIONAL { ?cite cito:cites ?tc . } + } +} +GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +LIMIT 500`; + if (this.controller.signal?.aborted) return Promise.reject(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `NameFromTN ${tnUri}` + ); + return this.handleName(json, justification); + } + /** + * Note this makes some assumptions on which variables are present in the bindings + * + * @internal */ + async handleName(json, justification) { + const treatmentPromises = []; + const displayName = json.results.bindings[0].name.value.replace( + json.results.bindings[0].authority.value, + "" + ).trim(); + const colName = json.results.bindings[0].col?.value ? { + displayName, + authority: json.results.bindings[0].authority.value, + colURI: json.results.bindings[0].col.value, + treatments: { + def: /* @__PURE__ */ new Set(), + aug: /* @__PURE__ */ new Set(), + dpr: /* @__PURE__ */ new Set(), + cite: /* @__PURE__ */ new Set() + } + } : void 0; + if (colName) { + if (this.expanded.has(colName.colURI)) return; + this.expanded.add(colName.colURI); + } + const authorizedNames = colName ? [colName] : []; + const taxonNameURI = json.results.bindings[0].tn?.value; + if (taxonNameURI) { + if (this.expanded.has(taxonNameURI)) return; + this.expanded.add(taxonNameURI); + } + for (const t of json.results.bindings) { + if (t.tc && t.tcAuth?.value) { + if (this.expanded.has(t.tc.value)) { + return; + } + const def = this.makeTreatmentSet(t.defs?.value.split("|")); + const aug = this.makeTreatmentSet(t.augs?.value.split("|")); + const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); + const cite = this.makeTreatmentSet(t.cites?.value.split("|")); + if (colName && t.tcAuth?.value.split(" / ").includes(colName.authority)) { + colName.authority = t.tcAuth?.value; + colName.taxonConceptURI = t.tc.value; + colName.treatments = { + def, + aug, + dpr, + cite + }; + } else { + authorizedNames.push({ + displayName, + authority: t.tcAuth.value, + taxonConceptURI: t.tc.value, + treatments: { + def, + aug, + dpr, + cite + } + }); + } + this.expanded.add(t.tc.value); + def.forEach((t2) => treatmentPromises.push(t2)); + aug.forEach((t2) => treatmentPromises.push(t2)); + dpr.forEach((t2) => treatmentPromises.push(t2)); + } + } + const treats = this.makeTreatmentSet( + json.results.bindings[0].tntreats?.value.split("|") + ); + treats.forEach((t) => treatmentPromises.push(t)); + const name = { + displayName, + taxonNameURI, + authorizedNames, + justification, + treatments: { + treats, + cite: this.makeTreatmentSet( + json.results.bindings[0].tncites?.value.split("|") + ) + }, + vernacularNames: taxonNameURI ? this.getVernacular(taxonNameURI) : Promise.resolve(/* @__PURE__ */ new Map()) + }; + let colPromises = []; + if (colName) { + [colName.acceptedColURI, colPromises] = await this.getAcceptedCol( + colName.colURI, + name + ); + } + this.pushName(name); + const newSynonyms = /* @__PURE__ */ new Map(); + (await Promise.all( + treatmentPromises.map( + (treat) => treat.details.then((d) => { + return [treat, d]; + }) + ) + )).map(([treat, d]) => { + d.treats.aug.difference(this.expanded).forEach( + (s) => newSynonyms.set(s, treat) + ); + d.treats.def.difference(this.expanded).forEach( + (s) => newSynonyms.set(s, treat) + ); + d.treats.dpr.difference(this.expanded).forEach( + (s) => newSynonyms.set(s, treat) + ); + d.treats.treattn.difference(this.expanded).forEach( + (s) => newSynonyms.set(s, treat) + ); + }); + await Promise.allSettled( + [ + ...colPromises, + ...[...newSynonyms].map( + ([n, treatment]) => this.getName(n, { searchTerm: false, parent: name, treatment }) + ) + ] + ); + } + /** @internal */ + async getAcceptedCol(colUri, parent) { + const query = ` +PREFIX dwc: +SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator="|") AS ?dprs) WHERE { + BIND(<${colUri}> AS ?col) + { + ?col dwc:acceptedName ?current . + ?dpr dwc:acceptedName ?current . + ?current dwc:taxonomicStatus ?current_status . + } UNION { + ?col dwc:taxonomicStatus ?current_status . + OPTIONAL { ?dpr dwc:acceptedName ?col . } + FILTER NOT EXISTS { ?col dwc:acceptedName ?current . } + BIND(?col AS ?current) + } +} +GROUP BY ?current ?current_status`; + if (this.acceptedCol.has(colUri)) { + return [this.acceptedCol.get(colUri), []]; + } + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `AcceptedCol ${colUri}` + ); + const promises = []; + for (const b of json.results.bindings) { + for (const dpr of b.dprs.value.split("|")) { + if (dpr) { + if (!this.acceptedCol.has(b.current.value)) { + this.acceptedCol.set(b.current.value, b.current.value); + promises.push( + this.getNameFromCol(b.current.value, { + searchTerm: false, + parent + }) + ); + } + this.acceptedCol.set(dpr, b.current.value); + if (!this.ignoreDeprecatedCoL) { + promises.push( + this.getNameFromCol(dpr, { searchTerm: false, parent }) + ); + } + } + } + } + if (json.results.bindings.length === 0) { + if (!this.acceptedCol.has(colUri)) { + this.acceptedCol.set(colUri, "INVALID COL"); + } + return [this.acceptedCol.get(colUri), promises]; + } + if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri); + return [this.acceptedCol.get(colUri), promises]; + } + /** @internal */ + async getVernacular(uri) { + const result = /* @__PURE__ */ new Map(); + const query = `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`; + const bindings = (await this.sparqlEndpoint.getSparqlResultSet(query, { + signal: this.controller.signal + }, `Vernacular ${uri}`)).results.bindings; + for (const b of bindings) { + if (b.n?.value) { + if (b.n["xml:lang"]) { + if (result.has(b.n["xml:lang"])) { + result.get(b.n["xml:lang"]).push(b.n.value); + } else result.set(b.n["xml:lang"], [b.n.value]); + } else { + if (result.has("??")) result.get("??").push(b.n.value); + else result.set("??", [b.n.value]); + } + } + } + return result; + } + /** @internal */ + makeTreatmentSet(urls) { + if (!urls) return /* @__PURE__ */ new Set(); + return new Set( + urls.filter((url) => !!url).map((url) => { + if (!this.treatments.has(url)) { + const details = this.getTreatmentDetails(url); + this.treatments.set(url, { + url, + details + }); + } + return this.treatments.get(url); + }) + ); + } + /** @internal */ + async getTreatmentDetails(treatmentUri) { + const query = ` +PREFIX dc: +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT + ?date ?title ?mc + (group_concat(DISTINCT ?catalogNumber;separator=" / ") as ?catalogNumbers) + (group_concat(DISTINCT ?collectionCode;separator=" / ") as ?collectionCodes) + (group_concat(DISTINCT ?typeStatus;separator=" / ") as ?typeStatuss) + (group_concat(DISTINCT ?countryCode;separator=" / ") as ?countryCodes) + (group_concat(DISTINCT ?stateProvince;separator=" / ") as ?stateProvinces) + (group_concat(DISTINCT ?municipality;separator=" / ") as ?municipalitys) + (group_concat(DISTINCT ?county;separator=" / ") as ?countys) + (group_concat(DISTINCT ?locality;separator=" / ") as ?localitys) + (group_concat(DISTINCT ?verbatimLocality;separator=" / ") as ?verbatimLocalitys) + (group_concat(DISTINCT ?recordedBy;separator=" / ") as ?recordedBys) + (group_concat(DISTINCT ?eventDate;separator=" / ") as ?eventDates) + (group_concat(DISTINCT ?samplingProtocol;separator=" / ") as ?samplingProtocols) + (group_concat(DISTINCT ?decimalLatitude;separator=" / ") as ?decimalLatitudes) + (group_concat(DISTINCT ?decimalLongitude;separator=" / ") as ?decimalLongitudes) + (group_concat(DISTINCT ?verbatimElevation;separator=" / ") as ?verbatimElevations) + (group_concat(DISTINCT ?gbifOccurrenceId;separator=" / ") as ?gbifOccurrenceIds) + (group_concat(DISTINCT ?gbifSpecimenId;separator=" / ") as ?gbifSpecimenIds) + (group_concat(DISTINCT ?creator;separator="; ") as ?creators) + (group_concat(DISTINCT ?httpUri;separator="|") as ?httpUris) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trttn;separator="|") as ?trttns) + (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) +WHERE { + BIND (<${treatmentUri}> as ?treatment) + ?treatment dc:creator ?creator . + OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . } + OPTIONAL { ?treatment dc:title ?title } + OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . } + OPTIONAL { ?treatment trt:definesTaxonConcept ?def . } + OPTIONAL { ?treatment trt:deprecates ?dpr . } + OPTIONAL { ?treatment cito:cites ?cite . ?cite a dwcFP:TaxonConcept . } + OPTIONAL { ?treatment trt:treatsTaxonName ?trttn . } + OPTIONAL { ?treatment trt:citesTaxonName ?citetn . } + OPTIONAL { + ?treatment dwc:basisOfRecord ?mc . + ?mc dwc:catalogNumber ?catalogNumber . + OPTIONAL { ?mc dwc:collectionCode ?collectionCode . } + OPTIONAL { ?mc dwc:typeStatus ?typeStatus . } + OPTIONAL { ?mc dwc:countryCode ?countryCode . } + OPTIONAL { ?mc dwc:stateProvince ?stateProvince . } + OPTIONAL { ?mc dwc:municipality ?municipality . } + OPTIONAL { ?mc dwc:county ?county . } + OPTIONAL { ?mc dwc:locality ?locality . } + OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . } + OPTIONAL { ?mc dwc:recordedBy ?recordedBy . } + OPTIONAL { ?mc dwc:eventDate ?eventDate . } + OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . } + OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . } + OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . } + OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . } + OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . } + OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . } + OPTIONAL { ?mc trt:httpUri ?httpUri . } + } +} +GROUP BY ?date ?title ?mc`; + if (this.controller.signal.aborted) { + return { + materialCitations: [], + figureCitations: [], + treats: { + def: /* @__PURE__ */ new Set(), + aug: /* @__PURE__ */ new Set(), + dpr: /* @__PURE__ */ new Set(), + citetc: /* @__PURE__ */ new Set(), + treattn: /* @__PURE__ */ new Set(), + citetn: /* @__PURE__ */ new Set() + } + }; + } + try { + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `TreatmentDetails ${treatmentUri}` + ); + const materialCitations = json.results.bindings.filter((t) => t.mc && t.catalogNumbers?.value).map((t) => { + const httpUri = t.httpUris?.value?.split("|"); + return { + "catalogNumber": t.catalogNumbers.value, + "collectionCode": t.collectionCodes?.value || void 0, + "typeStatus": t.typeStatuss?.value || void 0, + "countryCode": t.countryCodes?.value || void 0, + "stateProvince": t.stateProvinces?.value || void 0, + "municipality": t.municipalitys?.value || void 0, + "county": t.countys?.value || void 0, + "locality": t.localitys?.value || void 0, + "verbatimLocality": t.verbatimLocalitys?.value || void 0, + "recordedBy": t.recordedBys?.value || void 0, + "eventDate": t.eventDates?.value || void 0, + "samplingProtocol": t.samplingProtocols?.value || void 0, + "decimalLatitude": t.decimalLatitudes?.value || void 0, + "decimalLongitude": t.decimalLongitudes?.value || void 0, + "verbatimElevation": t.verbatimElevations?.value || void 0, + "gbifOccurrenceId": t.gbifOccurrenceIds?.value || void 0, + "gbifSpecimenId": t.gbifSpecimenIds?.value || void 0, + httpUri: httpUri?.length ? httpUri : void 0 + }; + }); + const figureQuery = ` +PREFIX cito: +PREFIX fabio: +PREFIX dc: +SELECT DISTINCT ?url ?description WHERE { + <${treatmentUri}> cito:cites ?cites . + ?cites a fabio:Figure ; + fabio:hasRepresentation ?url . + OPTIONAL { ?cites dc:description ?description . } +} `; + const figures = (await this.sparqlEndpoint.getSparqlResultSet( + figureQuery, + { signal: this.controller.signal }, + `TreatmentDetails/Figures ${treatmentUri}` + )).results.bindings; + const figureCitations = figures.filter((f) => f.url?.value).map( + (f) => { + return { url: f.url.value, description: f.description?.value }; + } + ); + return { + creators: json.results.bindings[0]?.creators?.value, + date: json.results.bindings[0]?.date?.value ? parseInt(json.results.bindings[0].date.value, 10) : void 0, + title: json.results.bindings[0]?.title?.value, + materialCitations, + figureCitations, + treats: { + def: new Set( + json.results.bindings[0]?.defs?.value ? json.results.bindings[0].defs.value.split("|") : void 0 + ), + aug: new Set( + json.results.bindings[0]?.augs?.value ? json.results.bindings[0].augs.value.split("|") : void 0 + ), + dpr: new Set( + json.results.bindings[0]?.dprs?.value ? json.results.bindings[0].dprs.value.split("|") : void 0 + ), + citetc: new Set( + json.results.bindings[0]?.cites?.value ? json.results.bindings[0].cites.value.split("|") : void 0 + ), + treattn: new Set( + json.results.bindings[0]?.trttns?.value ? json.results.bindings[0].trttns.value.split("|") : void 0 + ), + citetn: new Set( + json.results.bindings[0]?.citetns?.value ? json.results.bindings[0].citetns.value.split("|") : void 0 + ) + } + }; + } catch (error) { + console.warn("SPARQL Error: " + error); + return { + materialCitations: [], + figureCitations: [], + treats: { + def: /* @__PURE__ */ new Set(), + aug: /* @__PURE__ */ new Set(), + dpr: /* @__PURE__ */ new Set(), + citetc: /* @__PURE__ */ new Set(), + treattn: /* @__PURE__ */ new Set(), + citetn: /* @__PURE__ */ new Set() + } + }; + } + } + /** Allows iterating over the synonyms while they are found */ + [Symbol.asyncIterator]() { + let returnedSoFar = 0; + return { + next: () => new Promise( + (resolve, reject) => { + const callback = () => { + if (this.controller.signal.aborted) { + reject(new Error("SynyonymGroup has been aborted")); + } else if (returnedSoFar < this.names.length) { + resolve({ value: this.names[returnedSoFar++] }); + } else if (this.isFinished) { + resolve({ done: true, value: true }); + } else { + const listener = () => { + this.monitor.removeEventListener("updated", listener); + callback(); + }; + this.monitor.addEventListener("updated", listener); + } + }; + callback(); + } + ) + }; + } +}; +export { + SparqlEndpoint, + SynonymGroup +}; +//# sourceMappingURL=mod.js.map diff --git a/npm-package/build/mod.js.map b/npm-package/build/mod.js.map new file mode 100644 index 0000000..806852a --- /dev/null +++ b/npm-package/build/mod.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../SparqlEndpoint.ts", "../../SynonymGroup.ts"], + "sourcesContent": ["async function sleep(ms: number): Promise {\n const p = new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n return await p;\n}\n\n/** Describes the format of the JSON return by SPARQL endpoints */\nexport type SparqlJson = {\n head: {\n vars: string[];\n };\n results: {\n bindings: {\n [key: string]:\n | { type: string; value: string; \"xml:lang\"?: string }\n | undefined;\n }[];\n };\n};\n\n/**\n * Represents a remote sparql endpoint and provides a uniform way to run queries.\n */\nexport class SparqlEndpoint {\n /** Create a new SparqlEndpoint with the given URI */\n constructor(private sparqlEnpointUri: string) {}\n\n /** @ignore */\n // reasons: string[] = [];\n\n /**\n * Run a query against the sparql endpoint\n *\n * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time.\n * Retries are logged to the console (`console.warn`)\n *\n * @throws In case of non-ok response status codes or if fetch failed 10 times.\n * @param query The sparql query to run against the endpoint\n * @param fetchOptions Additional options for the `fetch` request\n * @param _reason (Currently ignored, used internally for debugging purposes)\n * @returns Results of the query\n */\n async getSparqlResultSet(\n query: string,\n fetchOptions: RequestInit = {},\n _reason = \"\",\n ): Promise {\n // this.reasons.push(_reason);\n\n fetchOptions.headers = fetchOptions.headers || {};\n (fetchOptions.headers as Record)[\"Accept\"] =\n \"application/sparql-results+json\";\n let retryCount = 0;\n const sendRequest = async (): Promise => {\n try {\n // console.info(`SPARQL ${_reason} (${retryCount + 1})`);\n const response = await fetch(\n this.sparqlEnpointUri + \"?query=\" + encodeURIComponent(query),\n fetchOptions,\n );\n if (!response.ok) {\n throw new Error(\"Response not ok. Status \" + response.status);\n }\n return await response.json();\n } catch (error) {\n if (fetchOptions.signal?.aborted) {\n throw error;\n } else if (retryCount < 10) {\n const wait = 50 * (1 << retryCount++);\n console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`);\n await sleep(wait);\n return await sendRequest();\n }\n console.warn(\"!! Fetch Error:\", query, \"\\n---\\n\", error);\n throw error;\n }\n };\n return await sendRequest();\n }\n}\n", "import { SparqlEndpoint, SparqlJson } from \"./mod.ts\";\n\n/** Finds all synonyms of a taxon */\nexport class SynonymGroup implements AsyncIterable {\n /** Indicates whether the SynonymGroup has found all synonyms.\n *\n * @readonly\n */\n isFinished = false;\n /** Used internally to watch for new names found */\n private monitor = new EventTarget();\n\n /** Used internally to abort in-flight network requests when SynonymGroup is aborted */\n private controller = new AbortController();\n\n /** The SparqlEndpoint used */\n private sparqlEndpoint: SparqlEndpoint;\n\n /**\n * List of names found so-far.\n *\n * Contains full list of synonyms _if_ .isFinished and not .isAborted\n *\n * @readonly\n */\n names: Name[] = [];\n /**\n * Add a new Name to this.names.\n *\n * Note: does not deduplicate on its own\n *\n * @internal */\n private pushName(name: Name) {\n this.names.push(name);\n this.monitor.dispatchEvent(new CustomEvent(\"updated\"));\n }\n\n /**\n * Call when all synonyms are found\n *\n * @internal */\n private finish() {\n this.isFinished = true;\n this.monitor.dispatchEvent(new CustomEvent(\"updated\"));\n }\n\n /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */\n private expanded = new Set(); // new Map();\n\n /** contains CoL uris where we don't need to check for Col \"acceptedName\" links\n *\n * col -> accepted col\n */\n private acceptedCol = new Map();\n\n /**\n * Used internally to deduplicate treatments, maps from URI to Object.\n *\n * Contains full list of treatments _if_ .isFinished and not .isAborted\n *\n * @readonly\n */\n treatments = new Map();\n\n /**\n * Whether to show taxa deprecated by CoL that would not have been found otherwise.\n * This significantly increases the number of results in some cases.\n */\n ignoreDeprecatedCoL: boolean;\n\n /**\n * if set to true, subTaxa of the search term are also considered as starting points.\n *\n * Not that \"weird\" ranks like subGenus are always included when searching for a genus by latin name.\n */\n startWithSubTaxa: boolean;\n\n /**\n * Constructs a SynonymGroup\n *\n * @param sparqlEndpoint SPARQL-Endpoint to query\n * @param taxonName either a string of the form \"Genus species infraspecific\" (species & infraspecific names optional), or an URI of a http://filteredpush.org/ontologies/oa/dwcFP#TaxonConcept or ...#TaxonName or a CoL taxon URI\n * @param [ignoreDeprecatedCoL=true] Whether to show taxa deprecated by CoL that would not have been found otherwise\n * @param [startWithSubTaxa=false] if set to true, subTaxa of the search term are also considered as starting points.\n */\n constructor(\n sparqlEndpoint: SparqlEndpoint,\n taxonName: string,\n ignoreDeprecatedCoL = true,\n startWithSubTaxa = false,\n ) {\n this.sparqlEndpoint = sparqlEndpoint;\n this.ignoreDeprecatedCoL = ignoreDeprecatedCoL;\n this.startWithSubTaxa = startWithSubTaxa;\n\n if (taxonName.startsWith(\"http\")) {\n this.getName(taxonName, { searchTerm: true, subTaxon: false }).finally(\n () => this.finish(),\n );\n } else {\n const name = [\n ...taxonName.split(\" \").filter((n) => !!n),\n undefined,\n undefined,\n ] as [string, string | undefined, string | undefined];\n this.getNameFromLatin(name, { searchTerm: true, subTaxon: false })\n .finally(\n () => this.finish(),\n );\n }\n }\n\n /** @internal */\n private async getName(\n taxonName: string,\n justification: Justification,\n ): Promise {\n if (this.expanded.has(taxonName)) {\n console.log(\"Skipping known\", taxonName);\n return;\n }\n\n if (taxonName.startsWith(\"https://www.catalogueoflife.org\")) {\n await this.getNameFromCol(taxonName, justification);\n } else if (taxonName.startsWith(\"http://taxon-concept.plazi.org\")) {\n await this.getNameFromTC(taxonName, justification);\n } else if (taxonName.startsWith(\"http://taxon-name.plazi.org\")) {\n await this.getNameFromTN(taxonName, justification);\n } else {\n throw `Cannot handle name-uri <${taxonName}> !`;\n }\n\n if (\n this.startWithSubTaxa && justification.searchTerm &&\n !justification.subTaxon\n ) {\n await this.getSubtaxa(taxonName);\n }\n }\n\n /** @internal */\n private async getSubtaxa(url: string): Promise {\n const query = url.startsWith(\"http://taxon-concept.plazi.org\")\n ? `\nPREFIX trt: \nSELECT DISTINCT ?sub WHERE {\n BIND(<${url}> as ?url)\n ?sub trt:hasParentName*/^trt:hasTaxonName ?url .\n}\nLIMIT 5000`\n : `\nPREFIX dwc: \nPREFIX trt: \nSELECT DISTINCT ?sub WHERE {\n BIND(<${url}> as ?url)\n ?sub (dwc:parent|trt:hasParentName)* ?url .\n}\nLIMIT 5000`;\n\n if (this.controller.signal?.aborted) return Promise.reject();\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `Subtaxa ${url}`,\n );\n\n const names = json.results.bindings\n .map((n) => n.sub?.value)\n .filter((n) => n && !this.expanded.has(n)) as string[];\n\n await Promise.allSettled(\n names.map((n) => this.getName(n, { searchTerm: true, subTaxon: true })),\n );\n }\n\n /** @internal */\n private async getNameFromLatin(\n [genus, species, infrasp]: [string, string | undefined, string | undefined],\n justification: Justification,\n ): Promise {\n const query = `\n PREFIX dwc: \nSELECT DISTINCT ?uri WHERE {\n ?uri dwc:genus|dwc:genericName \"${genus}\" .\n ${\n species\n ? `?uri dwc:species|dwc:specificEpithet \"${species}\" .`\n : \"FILTER NOT EXISTS { ?uri dwc:species|dwc:specificEpithet ?species . }\"\n }\n ${\n infrasp\n ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet \"${infrasp}\" .`\n : \"FILTER NOT EXISTS { ?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }\"\n }\n}\nLIMIT 500`;\n\n if (this.controller.signal?.aborted) return Promise.reject();\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromLatin ${genus} ${species} ${infrasp}`,\n );\n\n const names = json.results.bindings\n .map((n) => n.uri?.value)\n .filter((n) => n && !this.expanded.has(n)) as string[];\n\n await Promise.allSettled(names.map((n) => this.getName(n, justification)));\n }\n\n /** @internal */\n private async getNameFromCol(\n colUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n BIND(<${colUri}> as ?col)\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, \"\") as ?authority)\n ?col dwc:scientificName ?name . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n OPTIONAL {\n ?col dwc:specificEpithet ?species .\n OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . }\n }\n\n OPTIONAL {\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:genus ?genus .\n ?tn dwc:kingdom ?kingdom .\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL {\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n }\n }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n /// ?tn ?tc !rank !genus ?species ?infrasp !name !authority ?tcAuth\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromCol ${colUri}`,\n );\n\n return this.handleName(json, justification);\n }\n\n /** @internal */\n private async getNameFromTC(\n tcUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n <${tcUri}> trt:hasTaxonName ?tn .\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:kingdom ?kingdom .\n ?tn dwc:genus ?genus .\n OPTIONAL {\n ?tn dwc:species ?species .\n OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n \n OPTIONAL {\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . }\n ?col dwc:scientificName ?fullName . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n }\n \n BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(\" (\",?subgenus,\")\"), \"\"), COALESCE(CONCAT(\" \",?species), \"\"), COALESCE(CONCAT(\" \", ?infrasp), \"\"))) as ?name)\n BIND(COALESCE(?colAuth, \"\") as ?authority)\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromTC ${tcUri}`,\n );\n\n await this.handleName(json, justification);\n }\n\n /** @internal */\n private async getNameFromTN(\n tnUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n BIND(<${tnUri}> as ?tn)\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:genus ?genus .\n ?tn dwc:kingdom ?kingdom .\n OPTIONAL {\n ?tn dwc:species ?species .\n OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n \n OPTIONAL {\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . }\n ?col dwc:scientificName ?fullName . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n }\n \n BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(\" (\",?subgenus,\")\"), \"\"), COALESCE(CONCAT(\" \",?species), \"\"), COALESCE(CONCAT(\" \", ?infrasp), \"\"))) as ?name)\n BIND(COALESCE(?colAuth, \"\") as ?authority)\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL {\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromTN ${tnUri}`,\n );\n\n return this.handleName(json, justification);\n }\n\n /**\n * Note this makes some assumptions on which variables are present in the bindings\n *\n * @internal */\n private async handleName(\n json: SparqlJson,\n justification: Justification,\n ): Promise {\n const treatmentPromises: Treatment[] = [];\n\n const displayName: string = json.results.bindings[0].name!.value\n .replace(\n json.results.bindings[0].authority!.value,\n \"\",\n ).trim();\n\n const colName: AuthorizedName | undefined =\n json.results.bindings[0].col?.value\n ? {\n displayName,\n authority: json.results.bindings[0].authority!.value,\n colURI: json.results.bindings[0].col.value,\n treatments: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n cite: new Set(),\n },\n }\n : undefined;\n\n if (colName) {\n if (this.expanded.has(colName.colURI!)) return;\n this.expanded.add(colName.colURI!);\n }\n\n const authorizedNames = colName ? [colName] : [];\n\n const taxonNameURI = json.results.bindings[0].tn?.value;\n if (taxonNameURI) {\n if (this.expanded.has(taxonNameURI)) return;\n this.expanded.add(taxonNameURI); //, NameStatus.madeName);\n }\n\n for (const t of json.results.bindings) {\n if (t.tc && t.tcAuth?.value) {\n if (this.expanded.has(t.tc.value)) {\n // console.log(\"Abbruch: already known\", t.tc.value);\n return;\n }\n const def = this.makeTreatmentSet(t.defs?.value.split(\"|\"));\n const aug = this.makeTreatmentSet(t.augs?.value.split(\"|\"));\n const dpr = this.makeTreatmentSet(t.dprs?.value.split(\"|\"));\n const cite = this.makeTreatmentSet(t.cites?.value.split(\"|\"));\n if (\n colName && t.tcAuth?.value.split(\" / \").includes(colName.authority)\n ) {\n colName.authority = t.tcAuth?.value;\n colName.taxonConceptURI = t.tc.value;\n colName.treatments = {\n def,\n aug,\n dpr,\n cite,\n };\n } else {\n authorizedNames.push({\n displayName,\n authority: t.tcAuth.value,\n taxonConceptURI: t.tc.value,\n treatments: {\n def,\n aug,\n dpr,\n cite,\n },\n });\n }\n // this.expanded.set(t.tc.value, NameStatus.madeName);\n this.expanded.add(t.tc.value);\n\n def.forEach((t) => treatmentPromises.push(t));\n aug.forEach((t) => treatmentPromises.push(t));\n dpr.forEach((t) => treatmentPromises.push(t));\n }\n }\n\n // TODO: handle col-data \"acceptedName\" and stuff\n\n const treats = this.makeTreatmentSet(\n json.results.bindings[0].tntreats?.value.split(\"|\"),\n );\n treats.forEach((t) => treatmentPromises.push(t));\n\n const name: Name = {\n displayName,\n taxonNameURI,\n authorizedNames,\n justification,\n treatments: {\n treats,\n cite: this.makeTreatmentSet(\n json.results.bindings[0].tncites?.value.split(\"|\"),\n ),\n },\n vernacularNames: taxonNameURI\n ? this.getVernacular(taxonNameURI)\n : Promise.resolve(new Map()),\n };\n\n let colPromises: Promise[] = [];\n\n if (colName) {\n [colName.acceptedColURI, colPromises] = await this.getAcceptedCol(\n colName.colURI!,\n name,\n );\n }\n\n this.pushName(name);\n\n /** Map */\n const newSynonyms = new Map();\n (await Promise.all(\n treatmentPromises.map((treat) =>\n treat.details.then((d): [Treatment, TreatmentDetails] => {\n return [treat, d];\n })\n ),\n )).map(([treat, d]) => {\n d.treats.aug.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.def.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.dpr.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.treattn.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n });\n\n await Promise.allSettled(\n [\n ...colPromises,\n ...[...newSynonyms].map(([n, treatment]) =>\n this.getName(n, { searchTerm: false, parent: name, treatment })\n ),\n ],\n );\n }\n\n /** @internal */\n private async getAcceptedCol(\n colUri: string,\n parent: Name,\n ): Promise<[string, Promise[]]> {\n const query = `\nPREFIX dwc: \nSELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator=\"|\") AS ?dprs) WHERE {\n BIND(<${colUri}> AS ?col)\n {\n ?col dwc:acceptedName ?current .\n ?dpr dwc:acceptedName ?current .\n ?current dwc:taxonomicStatus ?current_status .\n } UNION {\n ?col dwc:taxonomicStatus ?current_status .\n OPTIONAL { ?dpr dwc:acceptedName ?col . }\n FILTER NOT EXISTS { ?col dwc:acceptedName ?current . }\n BIND(?col AS ?current)\n }\n}\nGROUP BY ?current ?current_status`;\n\n if (this.acceptedCol.has(colUri)) {\n return [this.acceptedCol.get(colUri)!, []];\n }\n\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `AcceptedCol ${colUri}`,\n );\n\n const promises: Promise[] = [];\n\n for (const b of json.results.bindings) {\n for (const dpr of b.dprs!.value.split(\"|\")) {\n if (dpr) {\n if (!this.acceptedCol.has(b.current!.value)) {\n this.acceptedCol.set(b.current!.value, b.current!.value);\n promises.push(\n this.getNameFromCol(b.current!.value, {\n searchTerm: false,\n parent,\n }),\n );\n }\n\n this.acceptedCol.set(dpr, b.current!.value);\n if (!this.ignoreDeprecatedCoL) {\n promises.push(\n this.getNameFromCol(dpr, { searchTerm: false, parent }),\n );\n }\n }\n }\n }\n\n if (json.results.bindings.length === 0) {\n // the provided colUri is not in CoL\n // promises === []\n if (!this.acceptedCol.has(colUri)) {\n this.acceptedCol.set(colUri, \"INVALID COL\");\n }\n return [this.acceptedCol.get(colUri)!, promises];\n }\n\n if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri);\n return [this.acceptedCol.get(colUri)!, promises];\n }\n\n /** @internal */\n private async getVernacular(uri: string): Promise {\n const result: vernacularNames = new Map();\n const query =\n `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`;\n const bindings = (await this.sparqlEndpoint.getSparqlResultSet(query, {\n signal: this.controller.signal,\n }, `Vernacular ${uri}`)).results.bindings;\n for (const b of bindings) {\n if (b.n?.value) {\n if (b.n[\"xml:lang\"]) {\n if (result.has(b.n[\"xml:lang\"])) {\n result.get(b.n[\"xml:lang\"])!.push(b.n.value);\n } else result.set(b.n[\"xml:lang\"], [b.n.value]);\n } else {\n if (result.has(\"??\")) result.get(\"??\")!.push(b.n.value);\n else result.set(\"??\", [b.n.value]);\n }\n }\n }\n return result;\n }\n\n /** @internal */\n private makeTreatmentSet(urls?: string[]): Set {\n if (!urls) return new Set();\n return new Set(\n urls.filter((url) => !!url).map((url) => {\n if (!this.treatments.has(url)) {\n const details = this.getTreatmentDetails(url);\n this.treatments.set(url, {\n url,\n details,\n });\n }\n return this.treatments.get(url) as Treatment;\n }),\n );\n }\n\n /** @internal */\n private async getTreatmentDetails(\n treatmentUri: string,\n ): Promise {\n const query = `\nPREFIX dc: \nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT\n ?date ?title ?mc\n (group_concat(DISTINCT ?catalogNumber;separator=\" / \") as ?catalogNumbers)\n (group_concat(DISTINCT ?collectionCode;separator=\" / \") as ?collectionCodes)\n (group_concat(DISTINCT ?typeStatus;separator=\" / \") as ?typeStatuss)\n (group_concat(DISTINCT ?countryCode;separator=\" / \") as ?countryCodes)\n (group_concat(DISTINCT ?stateProvince;separator=\" / \") as ?stateProvinces)\n (group_concat(DISTINCT ?municipality;separator=\" / \") as ?municipalitys)\n (group_concat(DISTINCT ?county;separator=\" / \") as ?countys)\n (group_concat(DISTINCT ?locality;separator=\" / \") as ?localitys)\n (group_concat(DISTINCT ?verbatimLocality;separator=\" / \") as ?verbatimLocalitys)\n (group_concat(DISTINCT ?recordedBy;separator=\" / \") as ?recordedBys)\n (group_concat(DISTINCT ?eventDate;separator=\" / \") as ?eventDates)\n (group_concat(DISTINCT ?samplingProtocol;separator=\" / \") as ?samplingProtocols)\n (group_concat(DISTINCT ?decimalLatitude;separator=\" / \") as ?decimalLatitudes)\n (group_concat(DISTINCT ?decimalLongitude;separator=\" / \") as ?decimalLongitudes)\n (group_concat(DISTINCT ?verbatimElevation;separator=\" / \") as ?verbatimElevations)\n (group_concat(DISTINCT ?gbifOccurrenceId;separator=\" / \") as ?gbifOccurrenceIds)\n (group_concat(DISTINCT ?gbifSpecimenId;separator=\" / \") as ?gbifSpecimenIds)\n (group_concat(DISTINCT ?creator;separator=\"; \") as ?creators)\n (group_concat(DISTINCT ?httpUri;separator=\"|\") as ?httpUris)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trttn;separator=\"|\") as ?trttns)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?citetns)\nWHERE {\n BIND (<${treatmentUri}> as ?treatment)\n ?treatment dc:creator ?creator .\n OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . }\n OPTIONAL { ?treatment dc:title ?title }\n OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . }\n OPTIONAL { ?treatment trt:definesTaxonConcept ?def . }\n OPTIONAL { ?treatment trt:deprecates ?dpr . }\n OPTIONAL { ?treatment cito:cites ?cite . ?cite a dwcFP:TaxonConcept . }\n OPTIONAL { ?treatment trt:treatsTaxonName ?trttn . }\n OPTIONAL { ?treatment trt:citesTaxonName ?citetn . }\n OPTIONAL {\n ?treatment dwc:basisOfRecord ?mc .\n ?mc dwc:catalogNumber ?catalogNumber .\n OPTIONAL { ?mc dwc:collectionCode ?collectionCode . }\n OPTIONAL { ?mc dwc:typeStatus ?typeStatus . }\n OPTIONAL { ?mc dwc:countryCode ?countryCode . }\n OPTIONAL { ?mc dwc:stateProvince ?stateProvince . }\n OPTIONAL { ?mc dwc:municipality ?municipality . }\n OPTIONAL { ?mc dwc:county ?county . }\n OPTIONAL { ?mc dwc:locality ?locality . }\n OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . }\n OPTIONAL { ?mc dwc:recordedBy ?recordedBy . }\n OPTIONAL { ?mc dwc:eventDate ?eventDate . }\n OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . }\n OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . }\n OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . }\n OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . }\n OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . }\n OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . }\n OPTIONAL { ?mc trt:httpUri ?httpUri . }\n }\n}\nGROUP BY ?date ?title ?mc`;\n if (this.controller.signal.aborted) {\n return {\n materialCitations: [],\n figureCitations: [],\n treats: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n citetc: new Set(),\n treattn: new Set(),\n citetn: new Set(),\n },\n };\n }\n try {\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `TreatmentDetails ${treatmentUri}`,\n );\n const materialCitations: MaterialCitation[] = json.results.bindings\n .filter((t) => t.mc && t.catalogNumbers?.value)\n .map((t) => {\n const httpUri = t.httpUris?.value?.split(\"|\");\n return {\n \"catalogNumber\": t.catalogNumbers!.value,\n \"collectionCode\": t.collectionCodes?.value || undefined,\n \"typeStatus\": t.typeStatuss?.value || undefined,\n \"countryCode\": t.countryCodes?.value || undefined,\n \"stateProvince\": t.stateProvinces?.value || undefined,\n \"municipality\": t.municipalitys?.value || undefined,\n \"county\": t.countys?.value || undefined,\n \"locality\": t.localitys?.value || undefined,\n \"verbatimLocality\": t.verbatimLocalitys?.value || undefined,\n \"recordedBy\": t.recordedBys?.value || undefined,\n \"eventDate\": t.eventDates?.value || undefined,\n \"samplingProtocol\": t.samplingProtocols?.value || undefined,\n \"decimalLatitude\": t.decimalLatitudes?.value || undefined,\n \"decimalLongitude\": t.decimalLongitudes?.value || undefined,\n \"verbatimElevation\": t.verbatimElevations?.value || undefined,\n \"gbifOccurrenceId\": t.gbifOccurrenceIds?.value || undefined,\n \"gbifSpecimenId\": t.gbifSpecimenIds?.value || undefined,\n httpUri: httpUri?.length ? httpUri : undefined,\n };\n });\n const figureQuery = `\nPREFIX cito: \nPREFIX fabio: \nPREFIX dc: \nSELECT DISTINCT ?url ?description WHERE {\n <${treatmentUri}> cito:cites ?cites .\n ?cites a fabio:Figure ;\n fabio:hasRepresentation ?url .\n OPTIONAL { ?cites dc:description ?description . }\n} `;\n const figures = (await this.sparqlEndpoint.getSparqlResultSet(\n figureQuery,\n { signal: this.controller.signal },\n `TreatmentDetails/Figures ${treatmentUri}`,\n )).results.bindings;\n const figureCitations = figures.filter((f) => f.url?.value).map(\n (f) => {\n return { url: f.url!.value, description: f.description?.value };\n },\n );\n return {\n creators: json.results.bindings[0]?.creators?.value,\n date: json.results.bindings[0]?.date?.value\n ? parseInt(json.results.bindings[0].date.value, 10)\n : undefined,\n title: json.results.bindings[0]?.title?.value,\n materialCitations,\n figureCitations,\n treats: {\n def: new Set(\n json.results.bindings[0]?.defs?.value\n ? json.results.bindings[0].defs.value.split(\"|\")\n : undefined,\n ),\n aug: new Set(\n json.results.bindings[0]?.augs?.value\n ? json.results.bindings[0].augs.value.split(\"|\")\n : undefined,\n ),\n dpr: new Set(\n json.results.bindings[0]?.dprs?.value\n ? json.results.bindings[0].dprs.value.split(\"|\")\n : undefined,\n ),\n citetc: new Set(\n json.results.bindings[0]?.cites?.value\n ? json.results.bindings[0].cites.value.split(\"|\")\n : undefined,\n ),\n treattn: new Set(\n json.results.bindings[0]?.trttns?.value\n ? json.results.bindings[0].trttns.value.split(\"|\")\n : undefined,\n ),\n citetn: new Set(\n json.results.bindings[0]?.citetns?.value\n ? json.results.bindings[0].citetns.value.split(\"|\")\n : undefined,\n ),\n },\n };\n } catch (error) {\n console.warn(\"SPARQL Error: \" + error);\n return {\n materialCitations: [],\n figureCitations: [],\n treats: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n citetc: new Set(),\n treattn: new Set(),\n citetn: new Set(),\n },\n };\n }\n }\n\n /** Allows iterating over the synonyms while they are found */\n [Symbol.asyncIterator](): AsyncIterator {\n let returnedSoFar = 0;\n return {\n next: () =>\n new Promise>(\n (resolve, reject) => {\n const callback = () => {\n if (this.controller.signal.aborted) {\n reject(new Error(\"SynyonymGroup has been aborted\"));\n } else if (returnedSoFar < this.names.length) {\n resolve({ value: this.names[returnedSoFar++] });\n } else if (this.isFinished) {\n resolve({ done: true, value: true });\n } else {\n const listener = () => {\n this.monitor.removeEventListener(\"updated\", listener);\n callback();\n };\n this.monitor.addEventListener(\"updated\", listener);\n }\n };\n callback();\n },\n ),\n };\n }\n}\n\n/** The central object.\n *\n * Each `Name` exists because of a taxon-name, taxon-concept or col-taxon in the data.\n * Each `Name` is uniquely determined by its human-readable latin name (for taxa ranking below genus, this is a multi-part name \u2014 binomial or trinomial) and kingdom.\n */\nexport type Name = {\n /** taxonomic kingdom */\n // kingdom: string;\n /** Human-readable name */\n displayName: string;\n\n /** vernacular names */\n vernacularNames: Promise;\n\n // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank.\n // * //TODO */\n // trees: Promise<{\n // col?: Tree;\n // tb?: Tree;\n // }>;\n\n /** The URI of the respective `dwcFP:TaxonName` if it exists */\n taxonNameURI?: string;\n /** All `AuthorizedName`s with this name */\n authorizedNames: AuthorizedName[];\n\n /** How this name was found */\n justification: Justification;\n\n /** treatments directly associated with .taxonNameUri */\n treatments: {\n treats: Set;\n cite: Set;\n };\n};\n\n/**\n * A map from language tags (IETF) to an array of vernacular names.\n */\nexport type vernacularNames = Map;\n\n/** Why a given Name was found (ther migth be other possible justifications) */\nexport type Justification = {\n searchTerm: true;\n /** indicates that this is a subTaxon of the parent */\n subTaxon: boolean;\n} | {\n searchTerm: false;\n parent: Name;\n /** if missing, indicates synonymy according to CoL or subTaxon */\n treatment?: Treatment;\n};\n\n/**\n * Corresponds to a taxon-concept or a CoL-Taxon\n */\nexport type AuthorizedName = {\n // TODO: neccesary?\n /** this may not be neccesary, as `AuthorizedName`s should only appear within a `Name` */\n // name: Name;\n /** Human-readable name */\n displayName: string;\n /** Human-readable authority */\n authority: string;\n\n /** The URI of the respective `dwcFP:TaxonConcept` if it exists */\n taxonConceptURI?: string;\n\n /** The URI of the respective CoL-taxon if it exists */\n colURI?: string;\n /** The URI of the corresponding accepted CoL-taxon if it exists.\n *\n * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon.\n *\n * May be the string \"INVALID COL\" if the colURI is not valid.\n */\n acceptedColURI?: string;\n\n // TODO: sensible?\n // /** these are CoL-taxa linked in the rdf, which differ lexically */\n // seeAlsoCol: string[];\n\n /** treatments directly associated with .taxonConceptURI */\n treatments: {\n def: Set;\n aug: Set;\n dpr: Set;\n cite: Set;\n };\n};\n\n/** A plazi-treatment */\nexport type Treatment = {\n url: string;\n\n /** Details are behind a promise becuase they are loaded with a separate query. */\n details: Promise;\n};\n\n/** Details of a treatment */\nexport type TreatmentDetails = {\n materialCitations: MaterialCitation[];\n figureCitations: FigureCitation[];\n date?: number;\n creators?: string;\n title?: string;\n treats: {\n def: Set;\n aug: Set;\n dpr: Set;\n citetc: Set;\n treattn: Set;\n citetn: Set;\n };\n};\n\n/** A cited material */\nexport type MaterialCitation = {\n \"catalogNumber\": string;\n \"collectionCode\"?: string;\n \"typeStatus\"?: string;\n \"countryCode\"?: string;\n \"stateProvince\"?: string;\n \"municipality\"?: string;\n \"county\"?: string;\n \"locality\"?: string;\n \"verbatimLocality\"?: string;\n \"recordedBy\"?: string;\n \"eventDate\"?: string;\n \"samplingProtocol\"?: string;\n \"decimalLatitude\"?: string;\n \"decimalLongitude\"?: string;\n \"verbatimElevation\"?: string;\n \"gbifOccurrenceId\"?: string;\n \"gbifSpecimenId\"?: string;\n \"httpUri\"?: string[];\n};\n\n/** A cited figure */\nexport type FigureCitation = {\n url: string;\n description?: string;\n};\n"], + "mappings": "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", + "names": ["t"] +} diff --git a/npm-package/package-lock.json b/npm-package/package-lock.json index c7968cf..215d7ba 100644 --- a/npm-package/package-lock.json +++ b/npm-package/package-lock.json @@ -9,20 +9,444 @@ "version": "2.2.0", "license": "MIT", "devDependencies": { - "typescript": "^4.9.5" + "esbuild": "0.24.0", + "typescript": "5.6.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } } } diff --git a/npm-package/package.json b/npm-package/package.json index 4cc298a..d67accd 100644 --- a/npm-package/package.json +++ b/npm-package/package.json @@ -2,9 +2,9 @@ "name": "@factsmission/synogroup", "version": "2.2.0", "description": "", - "main": "index.js", + "main": "build/mod.mjs", "scripts": { - "make-package": "deno bundle ../SynonymGroup.ts index.js && ./node_modules/typescript/bin/tsc && mv JustificationSet.d.ts index.d.ts && cat SynonymGroup.d.ts >> index.d.ts && rm SynonymGroup.d.ts", + "build": "node build.mjs && rm -rf ./types; tsc", "publish-package": "echo 'see readme'" }, "repository": { @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/factsmission/synogroup#readme", "devDependencies": { - "typescript": "^4.9.5" + "esbuild": "0.24.0", + "typescript": "5.6.3" } } diff --git a/npm-package/readme.md b/npm-package/readme.md index 1860896..edce317 100644 --- a/npm-package/readme.md +++ b/npm-package/readme.md @@ -1,8 +1,10 @@ # SynoGroup NPM Package -This folder contains all the neccesary tools to generate and publish a NPM package containing the synogroup library. +This folder contains all the neccesary tools to generate and publish a NPM +package containing the synogroup library. -(If you’re reading this on npmjs.com, read the actual readme in the parent repository linked to the left for more Information about Synogroup itself) +(If you’re reading this on npmjs.com, read the actual readme in the parent +repository linked to the left for more Information about Synogroup itself) ## How to @@ -11,24 +13,26 @@ This folder contains all the neccesary tools to generate and publish a NPM packa npm install # install tsc for the declaration file npm version patch # or ensure that the version number differs from the last published version otherwise # npm run publish-package # generates and publishes npm package -npm run make-package +npm run build ``` -**Note that the generated types are currently slightly broken, manually remove `import`s from `index.d.ts` before publishing** + +**Note that the generated types are currently possibly broken, please check! +manually remove `import`s from `index.d.ts` before publishing** + ```bash -npm publish --access public +# npm publish --access public ``` - ## Testing (-ish) ```bash -npm run make-package +npm run build ``` Generates the package code (index.js & index.d.ts) without publishing. ## Prerequiites -1. Deno -2. You need to be logged in to NPM with an account that can publish "@factsmission/synogroup" -3. \ No newline at end of file +2. You need to be logged in to NPM with an account that can publish + "@factsmission/synogroup" +3. diff --git a/npm-package/tsconfig.json b/npm-package/tsconfig.json index e7da24a..9a2b07d 100644 --- a/npm-package/tsconfig.json +++ b/npm-package/tsconfig.json @@ -1,12 +1,12 @@ { "include": [ - "../SynonymGroup.ts", - "../JustificationSet.ts" + "../mod.ts" ], "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, "removeComments": false, + "declarationDir": "./types", "outDir": ".", "lib": [ "esnext", @@ -14,7 +14,10 @@ "dom.iterable", "scripthost" ], + "isolatedModules": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, "target": "esnext", - "module": "esnext", + "module": "esnext" } -} \ No newline at end of file +} From be615784396529b6f5059466ff89ded23373db04 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:30:27 +0000 Subject: [PATCH 21/71] query params --- example/index.html | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/example/index.html b/example/index.html index 5d3d81b..8233454 100644 --- a/example/index.html +++ b/example/index.html @@ -13,15 +13,17 @@ -

Synonym Group for "https://www.catalogueoflife.org/data/taxon/3WD9M"

+

SynoLib

+ \ No newline at end of file diff --git a/example/index.ts b/example/index.ts new file mode 100644 index 0000000..e3afe68 --- /dev/null +++ b/example/index.ts @@ -0,0 +1,203 @@ +/// +import { + type Name, + SparqlEndpoint, + SynonymGroup, + type Treatment, +} from "../mod.ts"; + +const params = new URLSearchParams(document.location.search); +const HIDE_COL_ONLY_SYNONYMS = !params.has("show_col"); +const START_WITH_SUBTAXA = params.has("subtaxa"); +const ENDPOINT_URL = params.get("server") || + "https://treatment.ld.plazi.org/sparql"; +const NAME = params.get("q") || + "https://www.catalogueoflife.org/data/taxon/3WD9M"; + +const root = document.getElementById("root") as HTMLDivElement; + +class SynoTreatment extends HTMLElement { + constructor(trt: Treatment) { + super(); + + const li = document.createElement("li"); + li.innerText = trt.url; + this.append(li); + trt.details.then((details) => + li.innerText = `${details.creators} ${details.date} “${ + details.title || "No Title" + }” ${trt.url}` + ); + } +} +customElements.define("syno-treatment", SynoTreatment); + +class SynoName extends HTMLElement { + constructor(name: Name) { + super(); + + const title = document.createElement("h2"); + title.innerText = name.displayName; + this.append(title); + + if (name.taxonNameURI) { + const name_uri = document.createElement("code"); + name_uri.classList.add("taxon", "uri"); + name_uri.innerText = name.taxonNameURI.replace("http://", ""); + name_uri.title = name.taxonNameURI; + title.append(" ", name_uri); + } + + const justification = document.createElement("abbr"); + justification.classList.add("justification"); + justification.innerText = "...?"; + justify(name).then((just) => justification.title = `This ${just}`); + title.append(" ", justification); + + const vernacular = document.createElement("code"); + vernacular.classList.add("vernacular"); + name.vernacularNames.then((names) => { + if (names.size > 0) { + vernacular.innerText = "“" + [...names.values()].join("”, “") + "”"; + } + }); + this.append(vernacular); + + if (name.treatments.treats.size > 0 || name.treatments.cite.size > 0) { + const treatments = document.createElement("ul"); + this.append(treatments); + for (const trt of name.treatments.treats) { + const li = new SynoTreatment(trt); + li.classList.add("aug"); + treatments.append(li); + } + for (const trt of name.treatments.cite) { + const li = new SynoTreatment(trt); + li.classList.add("cite"); + treatments.append(li); + } + } + + for (const authorizedName of name.authorizedNames) { + const authName = document.createElement("h3"); + authName.innerText = authorizedName.displayName + " " + + authorizedName.authority; + this.append(authName); + + const treatments = document.createElement("ul"); + this.append(treatments); + + if (authorizedName.taxonConceptURI) { + const name_uri = document.createElement("code"); + name_uri.classList.add("taxon", "uri"); + name_uri.innerText = authorizedName.taxonConceptURI.replace( + "http://", + "", + ); + name_uri.title = authorizedName.taxonConceptURI; + authName.append(" ", name_uri); + } + if (authorizedName.colURI) { + const col_uri = document.createElement("code"); + col_uri.classList.add("col", "uri"); + const id = authorizedName.colURI.replace( + "https://www.catalogueoflife.org/data/taxon/", + "", + ); + col_uri.innerText = id; + col_uri.id = id; + col_uri.title = authorizedName.colURI; + authName.append(" ", col_uri); + + const li = document.createElement("li"); + li.classList.add("treatment"); + li.innerText = "Catalogue of Life"; + treatments.append(li); + + if (authorizedName.acceptedColURI !== authorizedName.colURI) { + li.classList.add("dpr"); + const col_uri = document.createElement("a"); + col_uri.classList.add("col", "uri"); + const id = authorizedName.acceptedColURI!.replace( + "https://www.catalogueoflife.org/data/taxon/", + "", + ); + col_uri.innerText = id; + col_uri.href = `#${id}`; + col_uri.title = authorizedName.acceptedColURI!; + li.append(" → "); + li.append(col_uri); + } else { + li.classList.add("aug"); + } + } + + for (const trt of authorizedName.treatments.def) { + const li = new SynoTreatment(trt); + li.classList.add("def"); + treatments.append(li); + } + for (const trt of authorizedName.treatments.aug) { + const li = new SynoTreatment(trt); + li.classList.add("aug"); + treatments.append(li); + } + for (const trt of authorizedName.treatments.dpr) { + const li = new SynoTreatment(trt); + li.classList.add("dpr"); + treatments.append(li); + } + for (const trt of authorizedName.treatments.cite) { + const li = new SynoTreatment(trt); + li.classList.add("cite"); + treatments.append(li); + } + } + } +} +customElements.define("syno-name", SynoName); + +async function justify(name: Name): Promise { + if (name.justification.searchTerm) { + if (name.justification.subTaxon) { + return "is a sub-taxon of the search term."; + } else return "is the search term."; + } else if (name.justification.treatment) { + const details = await name.justification.treatment.details; + const parent = await justify(name.justification.parent); + return `is, according to ${details.creators} ${details.date},\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + // return `is, according to ${details.creators} ${details.date} “${details.title||"No Title"}” ${name.justification.treatment.url},\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + } else { + const parent = await justify(name.justification.parent); + return `is, according to the Catalogue of Life,\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + } +} + +const indicator = document.createElement("div"); +root.insertAdjacentElement("beforebegin", indicator); +indicator.append(`Finding Synonyms for ${NAME} `); +indicator.append(document.createElement("progress")); + +const timeStart = performance.now(); + +const sparqlEndpoint = new SparqlEndpoint(ENDPOINT_URL); +const synoGroup = new SynonymGroup( + sparqlEndpoint, + NAME, + HIDE_COL_ONLY_SYNONYMS, + START_WITH_SUBTAXA, +); + +for await (const name of synoGroup) { + const element = new SynoName(name); + root.append(element); +} + +const timeEnd = performance.now(); + +indicator.innerHTML = ""; +indicator.innerText = + `Found ${synoGroup.names.length} names with ${synoGroup.treatments.size} treatments. This took ${ + timeEnd - timeStart + } milliseconds.`; +if (synoGroup.names.length === 0) root.append(":["); diff --git a/npm-package/build.mjs b/npm-package/build.mjs deleted file mode 100644 index 2e55e7c..0000000 --- a/npm-package/build.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import * as esbuild from "esbuild"; - -const BUILD = false; - -await esbuild.build({ - entryPoints: ["../mod.ts"], - sourcemap: true, - bundle: true, - format: "esm", - lineLimit: 120, - minify: BUILD ? true : false, - outfile: "./build/mod.js", -}); diff --git a/npm-package/build/mod.js b/npm-package/build/mod.js deleted file mode 100644 index 696e08f..0000000 --- a/npm-package/build/mod.js +++ /dev/null @@ -1,860 +0,0 @@ -// ../SparqlEndpoint.ts -async function sleep(ms) { - const p = new Promise((resolve) => { - setTimeout(resolve, ms); - }); - return await p; -} -var SparqlEndpoint = class { - /** Create a new SparqlEndpoint with the given URI */ - constructor(sparqlEnpointUri) { - this.sparqlEnpointUri = sparqlEnpointUri; - } - /** @ignore */ - // reasons: string[] = []; - /** - * Run a query against the sparql endpoint - * - * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time. - * Retries are logged to the console (`console.warn`) - * - * @throws In case of non-ok response status codes or if fetch failed 10 times. - * @param query The sparql query to run against the endpoint - * @param fetchOptions Additional options for the `fetch` request - * @param _reason (Currently ignored, used internally for debugging purposes) - * @returns Results of the query - */ - async getSparqlResultSet(query, fetchOptions = {}, _reason = "") { - fetchOptions.headers = fetchOptions.headers || {}; - fetchOptions.headers["Accept"] = "application/sparql-results+json"; - let retryCount = 0; - const sendRequest = async () => { - try { - const response = await fetch( - this.sparqlEnpointUri + "?query=" + encodeURIComponent(query), - fetchOptions - ); - if (!response.ok) { - throw new Error("Response not ok. Status " + response.status); - } - return await response.json(); - } catch (error) { - if (fetchOptions.signal?.aborted) { - throw error; - } else if (retryCount < 10) { - const wait = 50 * (1 << retryCount++); - console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); - await sleep(wait); - return await sendRequest(); - } - console.warn("!! Fetch Error:", query, "\n---\n", error); - throw error; - } - }; - return await sendRequest(); - } -}; - -// ../SynonymGroup.ts -var SynonymGroup = class { - /** Indicates whether the SynonymGroup has found all synonyms. - * - * @readonly - */ - isFinished = false; - /** Used internally to watch for new names found */ - monitor = new EventTarget(); - /** Used internally to abort in-flight network requests when SynonymGroup is aborted */ - controller = new AbortController(); - /** The SparqlEndpoint used */ - sparqlEndpoint; - /** - * List of names found so-far. - * - * Contains full list of synonyms _if_ .isFinished and not .isAborted - * - * @readonly - */ - names = []; - /** - * Add a new Name to this.names. - * - * Note: does not deduplicate on its own - * - * @internal */ - pushName(name) { - this.names.push(name); - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - /** - * Call when all synonyms are found - * - * @internal */ - finish() { - this.isFinished = true; - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */ - expanded = /* @__PURE__ */ new Set(); - // new Map(); - /** contains CoL uris where we don't need to check for Col "acceptedName" links - * - * col -> accepted col - */ - acceptedCol = /* @__PURE__ */ new Map(); - /** - * Used internally to deduplicate treatments, maps from URI to Object. - * - * Contains full list of treatments _if_ .isFinished and not .isAborted - * - * @readonly - */ - treatments = /* @__PURE__ */ new Map(); - /** - * Whether to show taxa deprecated by CoL that would not have been found otherwise. - * This significantly increases the number of results in some cases. - */ - ignoreDeprecatedCoL; - /** - * if set to true, subTaxa of the search term are also considered as starting points. - * - * Not that "weird" ranks like subGenus are always included when searching for a genus by latin name. - */ - startWithSubTaxa; - /** - * Constructs a SynonymGroup - * - * @param sparqlEndpoint SPARQL-Endpoint to query - * @param taxonName either a string of the form "Genus species infraspecific" (species & infraspecific names optional), or an URI of a http://filteredpush.org/ontologies/oa/dwcFP#TaxonConcept or ...#TaxonName or a CoL taxon URI - * @param [ignoreDeprecatedCoL=true] Whether to show taxa deprecated by CoL that would not have been found otherwise - * @param [startWithSubTaxa=false] if set to true, subTaxa of the search term are also considered as starting points. - */ - constructor(sparqlEndpoint, taxonName, ignoreDeprecatedCoL = true, startWithSubTaxa = false) { - this.sparqlEndpoint = sparqlEndpoint; - this.ignoreDeprecatedCoL = ignoreDeprecatedCoL; - this.startWithSubTaxa = startWithSubTaxa; - if (taxonName.startsWith("http")) { - this.getName(taxonName, { searchTerm: true, subTaxon: false }).finally( - () => this.finish() - ); - } else { - const name = [ - ...taxonName.split(" ").filter((n) => !!n), - void 0, - void 0 - ]; - this.getNameFromLatin(name, { searchTerm: true, subTaxon: false }).finally( - () => this.finish() - ); - } - } - /** @internal */ - async getName(taxonName, justification) { - if (this.expanded.has(taxonName)) { - console.log("Skipping known", taxonName); - return; - } - if (taxonName.startsWith("https://www.catalogueoflife.org")) { - await this.getNameFromCol(taxonName, justification); - } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { - await this.getNameFromTC(taxonName, justification); - } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { - await this.getNameFromTN(taxonName, justification); - } else { - throw `Cannot handle name-uri <${taxonName}> !`; - } - if (this.startWithSubTaxa && justification.searchTerm && !justification.subTaxon) { - await this.getSubtaxa(taxonName); - } - } - /** @internal */ - async getSubtaxa(url) { - const query = url.startsWith("http://taxon-concept.plazi.org") ? ` -PREFIX trt: -SELECT DISTINCT ?sub WHERE { - BIND(<${url}> as ?url) - ?sub trt:hasParentName*/^trt:hasTaxonName ?url . -} -LIMIT 5000` : ` -PREFIX dwc: -PREFIX trt: -SELECT DISTINCT ?sub WHERE { - BIND(<${url}> as ?url) - ?sub (dwc:parent|trt:hasParentName)* ?url . -} -LIMIT 5000`; - if (this.controller.signal?.aborted) return Promise.reject(); - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `Subtaxa ${url}` - ); - const names = json.results.bindings.map((n) => n.sub?.value).filter((n) => n && !this.expanded.has(n)); - await Promise.allSettled( - names.map((n) => this.getName(n, { searchTerm: true, subTaxon: true })) - ); - } - /** @internal */ - async getNameFromLatin([genus, species, infrasp], justification) { - const query = ` - PREFIX dwc: -SELECT DISTINCT ?uri WHERE { - ?uri dwc:genus|dwc:genericName "${genus}" . - ${species ? `?uri dwc:species|dwc:specificEpithet "${species}" .` : "FILTER NOT EXISTS { ?uri dwc:species|dwc:specific\ -Epithet ?species . }"} - ${infrasp ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet "${infrasp}" .` : "FILTER NOT EXISTS { ?uri dwc:\ -subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }"} -} -LIMIT 500`; - if (this.controller.signal?.aborted) return Promise.reject(); - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromLatin ${genus} ${species} ${infrasp}` - ); - const names = json.results.bindings.map((n) => n.uri?.value).filter((n) => n && !this.expanded.has(n)); - await Promise.allSettled(names.map((n) => this.getName(n, justification))); - } - /** @internal */ - async getNameFromCol(colUri, justification) { - const query = ` -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - BIND(<${colUri}> as ?col) - ?col dwc:taxonRank ?rank . - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) - ?col dwc:scientificName ?name . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - OPTIONAL { - ?col dwc:specificEpithet ?species . - OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } - } - - OPTIONAL { - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . - ?tn dwc:genus ?genus . - ?tn dwc:kingdom ?kingdom . - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } - - OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } - } - } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - if (this.controller.signal?.aborted) return Promise.reject(); - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromCol ${colUri}` - ); - return this.handleName(json, justification); - } - /** @internal */ - async getNameFromTC(tcUri, justification) { - const query = ` -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - <${tcUri}> trt:hasTaxonName ?tn . - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . - ?tn dwc:kingdom ?kingdom . - ?tn dwc:genus ?genus . - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } - } - - OPTIONAL { - ?col dwc:taxonRank ?rank . - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), \ -COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) - - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } - - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - if (this.controller.signal?.aborted) return Promise.reject(); - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromTC ${tcUri}` - ); - await this.handleName(json, justification); - } - /** @internal */ - async getNameFromTN(tnUri, justification) { - const query = ` -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - BIND(<${tnUri}> as ?tn) - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . - ?tn dwc:genus ?genus . - ?tn dwc:kingdom ?kingdom . - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } - } - - OPTIONAL { - ?col dwc:taxonRank ?rank . - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), \ -COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) - - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } - - OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } - } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - if (this.controller.signal?.aborted) return Promise.reject(); - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromTN ${tnUri}` - ); - return this.handleName(json, justification); - } - /** - * Note this makes some assumptions on which variables are present in the bindings - * - * @internal */ - async handleName(json, justification) { - const treatmentPromises = []; - const displayName = json.results.bindings[0].name.value.replace( - json.results.bindings[0].authority.value, - "" - ).trim(); - const colName = json.results.bindings[0].col?.value ? { - displayName, - authority: json.results.bindings[0].authority.value, - colURI: json.results.bindings[0].col.value, - treatments: { - def: /* @__PURE__ */ new Set(), - aug: /* @__PURE__ */ new Set(), - dpr: /* @__PURE__ */ new Set(), - cite: /* @__PURE__ */ new Set() - } - } : void 0; - if (colName) { - if (this.expanded.has(colName.colURI)) return; - this.expanded.add(colName.colURI); - } - const authorizedNames = colName ? [colName] : []; - const taxonNameURI = json.results.bindings[0].tn?.value; - if (taxonNameURI) { - if (this.expanded.has(taxonNameURI)) return; - this.expanded.add(taxonNameURI); - } - for (const t of json.results.bindings) { - if (t.tc && t.tcAuth?.value) { - if (this.expanded.has(t.tc.value)) { - return; - } - const def = this.makeTreatmentSet(t.defs?.value.split("|")); - const aug = this.makeTreatmentSet(t.augs?.value.split("|")); - const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); - const cite = this.makeTreatmentSet(t.cites?.value.split("|")); - if (colName && t.tcAuth?.value.split(" / ").includes(colName.authority)) { - colName.authority = t.tcAuth?.value; - colName.taxonConceptURI = t.tc.value; - colName.treatments = { - def, - aug, - dpr, - cite - }; - } else { - authorizedNames.push({ - displayName, - authority: t.tcAuth.value, - taxonConceptURI: t.tc.value, - treatments: { - def, - aug, - dpr, - cite - } - }); - } - this.expanded.add(t.tc.value); - def.forEach((t2) => treatmentPromises.push(t2)); - aug.forEach((t2) => treatmentPromises.push(t2)); - dpr.forEach((t2) => treatmentPromises.push(t2)); - } - } - const treats = this.makeTreatmentSet( - json.results.bindings[0].tntreats?.value.split("|") - ); - treats.forEach((t) => treatmentPromises.push(t)); - const name = { - displayName, - taxonNameURI, - authorizedNames, - justification, - treatments: { - treats, - cite: this.makeTreatmentSet( - json.results.bindings[0].tncites?.value.split("|") - ) - }, - vernacularNames: taxonNameURI ? this.getVernacular(taxonNameURI) : Promise.resolve(/* @__PURE__ */ new Map()) - }; - let colPromises = []; - if (colName) { - [colName.acceptedColURI, colPromises] = await this.getAcceptedCol( - colName.colURI, - name - ); - } - this.pushName(name); - const newSynonyms = /* @__PURE__ */ new Map(); - (await Promise.all( - treatmentPromises.map( - (treat) => treat.details.then((d) => { - return [treat, d]; - }) - ) - )).map(([treat, d]) => { - d.treats.aug.difference(this.expanded).forEach( - (s) => newSynonyms.set(s, treat) - ); - d.treats.def.difference(this.expanded).forEach( - (s) => newSynonyms.set(s, treat) - ); - d.treats.dpr.difference(this.expanded).forEach( - (s) => newSynonyms.set(s, treat) - ); - d.treats.treattn.difference(this.expanded).forEach( - (s) => newSynonyms.set(s, treat) - ); - }); - await Promise.allSettled( - [ - ...colPromises, - ...[...newSynonyms].map( - ([n, treatment]) => this.getName(n, { searchTerm: false, parent: name, treatment }) - ) - ] - ); - } - /** @internal */ - async getAcceptedCol(colUri, parent) { - const query = ` -PREFIX dwc: -SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator="|") AS ?dprs) WHERE { - BIND(<${colUri}> AS ?col) - { - ?col dwc:acceptedName ?current . - ?dpr dwc:acceptedName ?current . - ?current dwc:taxonomicStatus ?current_status . - } UNION { - ?col dwc:taxonomicStatus ?current_status . - OPTIONAL { ?dpr dwc:acceptedName ?col . } - FILTER NOT EXISTS { ?col dwc:acceptedName ?current . } - BIND(?col AS ?current) - } -} -GROUP BY ?current ?current_status`; - if (this.acceptedCol.has(colUri)) { - return [this.acceptedCol.get(colUri), []]; - } - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `AcceptedCol ${colUri}` - ); - const promises = []; - for (const b of json.results.bindings) { - for (const dpr of b.dprs.value.split("|")) { - if (dpr) { - if (!this.acceptedCol.has(b.current.value)) { - this.acceptedCol.set(b.current.value, b.current.value); - promises.push( - this.getNameFromCol(b.current.value, { - searchTerm: false, - parent - }) - ); - } - this.acceptedCol.set(dpr, b.current.value); - if (!this.ignoreDeprecatedCoL) { - promises.push( - this.getNameFromCol(dpr, { searchTerm: false, parent }) - ); - } - } - } - } - if (json.results.bindings.length === 0) { - if (!this.acceptedCol.has(colUri)) { - this.acceptedCol.set(colUri, "INVALID COL"); - } - return [this.acceptedCol.get(colUri), promises]; - } - if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri); - return [this.acceptedCol.get(colUri), promises]; - } - /** @internal */ - async getVernacular(uri) { - const result = /* @__PURE__ */ new Map(); - const query = `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`; - const bindings = (await this.sparqlEndpoint.getSparqlResultSet(query, { - signal: this.controller.signal - }, `Vernacular ${uri}`)).results.bindings; - for (const b of bindings) { - if (b.n?.value) { - if (b.n["xml:lang"]) { - if (result.has(b.n["xml:lang"])) { - result.get(b.n["xml:lang"]).push(b.n.value); - } else result.set(b.n["xml:lang"], [b.n.value]); - } else { - if (result.has("??")) result.get("??").push(b.n.value); - else result.set("??", [b.n.value]); - } - } - } - return result; - } - /** @internal */ - makeTreatmentSet(urls) { - if (!urls) return /* @__PURE__ */ new Set(); - return new Set( - urls.filter((url) => !!url).map((url) => { - if (!this.treatments.has(url)) { - const details = this.getTreatmentDetails(url); - this.treatments.set(url, { - url, - details - }); - } - return this.treatments.get(url); - }) - ); - } - /** @internal */ - async getTreatmentDetails(treatmentUri) { - const query = ` -PREFIX dc: -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT - ?date ?title ?mc - (group_concat(DISTINCT ?catalogNumber;separator=" / ") as ?catalogNumbers) - (group_concat(DISTINCT ?collectionCode;separator=" / ") as ?collectionCodes) - (group_concat(DISTINCT ?typeStatus;separator=" / ") as ?typeStatuss) - (group_concat(DISTINCT ?countryCode;separator=" / ") as ?countryCodes) - (group_concat(DISTINCT ?stateProvince;separator=" / ") as ?stateProvinces) - (group_concat(DISTINCT ?municipality;separator=" / ") as ?municipalitys) - (group_concat(DISTINCT ?county;separator=" / ") as ?countys) - (group_concat(DISTINCT ?locality;separator=" / ") as ?localitys) - (group_concat(DISTINCT ?verbatimLocality;separator=" / ") as ?verbatimLocalitys) - (group_concat(DISTINCT ?recordedBy;separator=" / ") as ?recordedBys) - (group_concat(DISTINCT ?eventDate;separator=" / ") as ?eventDates) - (group_concat(DISTINCT ?samplingProtocol;separator=" / ") as ?samplingProtocols) - (group_concat(DISTINCT ?decimalLatitude;separator=" / ") as ?decimalLatitudes) - (group_concat(DISTINCT ?decimalLongitude;separator=" / ") as ?decimalLongitudes) - (group_concat(DISTINCT ?verbatimElevation;separator=" / ") as ?verbatimElevations) - (group_concat(DISTINCT ?gbifOccurrenceId;separator=" / ") as ?gbifOccurrenceIds) - (group_concat(DISTINCT ?gbifSpecimenId;separator=" / ") as ?gbifSpecimenIds) - (group_concat(DISTINCT ?creator;separator="; ") as ?creators) - (group_concat(DISTINCT ?httpUri;separator="|") as ?httpUris) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trttn;separator="|") as ?trttns) - (group_concat(DISTINCT ?citetn;separator="|") as ?citetns) -WHERE { - BIND (<${treatmentUri}> as ?treatment) - ?treatment dc:creator ?creator . - OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . } - OPTIONAL { ?treatment dc:title ?title } - OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . } - OPTIONAL { ?treatment trt:definesTaxonConcept ?def . } - OPTIONAL { ?treatment trt:deprecates ?dpr . } - OPTIONAL { ?treatment cito:cites ?cite . ?cite a dwcFP:TaxonConcept . } - OPTIONAL { ?treatment trt:treatsTaxonName ?trttn . } - OPTIONAL { ?treatment trt:citesTaxonName ?citetn . } - OPTIONAL { - ?treatment dwc:basisOfRecord ?mc . - ?mc dwc:catalogNumber ?catalogNumber . - OPTIONAL { ?mc dwc:collectionCode ?collectionCode . } - OPTIONAL { ?mc dwc:typeStatus ?typeStatus . } - OPTIONAL { ?mc dwc:countryCode ?countryCode . } - OPTIONAL { ?mc dwc:stateProvince ?stateProvince . } - OPTIONAL { ?mc dwc:municipality ?municipality . } - OPTIONAL { ?mc dwc:county ?county . } - OPTIONAL { ?mc dwc:locality ?locality . } - OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . } - OPTIONAL { ?mc dwc:recordedBy ?recordedBy . } - OPTIONAL { ?mc dwc:eventDate ?eventDate . } - OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . } - OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . } - OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . } - OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . } - OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . } - OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . } - OPTIONAL { ?mc trt:httpUri ?httpUri . } - } -} -GROUP BY ?date ?title ?mc`; - if (this.controller.signal.aborted) { - return { - materialCitations: [], - figureCitations: [], - treats: { - def: /* @__PURE__ */ new Set(), - aug: /* @__PURE__ */ new Set(), - dpr: /* @__PURE__ */ new Set(), - citetc: /* @__PURE__ */ new Set(), - treattn: /* @__PURE__ */ new Set(), - citetn: /* @__PURE__ */ new Set() - } - }; - } - try { - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `TreatmentDetails ${treatmentUri}` - ); - const materialCitations = json.results.bindings.filter((t) => t.mc && t.catalogNumbers?.value).map((t) => { - const httpUri = t.httpUris?.value?.split("|"); - return { - "catalogNumber": t.catalogNumbers.value, - "collectionCode": t.collectionCodes?.value || void 0, - "typeStatus": t.typeStatuss?.value || void 0, - "countryCode": t.countryCodes?.value || void 0, - "stateProvince": t.stateProvinces?.value || void 0, - "municipality": t.municipalitys?.value || void 0, - "county": t.countys?.value || void 0, - "locality": t.localitys?.value || void 0, - "verbatimLocality": t.verbatimLocalitys?.value || void 0, - "recordedBy": t.recordedBys?.value || void 0, - "eventDate": t.eventDates?.value || void 0, - "samplingProtocol": t.samplingProtocols?.value || void 0, - "decimalLatitude": t.decimalLatitudes?.value || void 0, - "decimalLongitude": t.decimalLongitudes?.value || void 0, - "verbatimElevation": t.verbatimElevations?.value || void 0, - "gbifOccurrenceId": t.gbifOccurrenceIds?.value || void 0, - "gbifSpecimenId": t.gbifSpecimenIds?.value || void 0, - httpUri: httpUri?.length ? httpUri : void 0 - }; - }); - const figureQuery = ` -PREFIX cito: -PREFIX fabio: -PREFIX dc: -SELECT DISTINCT ?url ?description WHERE { - <${treatmentUri}> cito:cites ?cites . - ?cites a fabio:Figure ; - fabio:hasRepresentation ?url . - OPTIONAL { ?cites dc:description ?description . } -} `; - const figures = (await this.sparqlEndpoint.getSparqlResultSet( - figureQuery, - { signal: this.controller.signal }, - `TreatmentDetails/Figures ${treatmentUri}` - )).results.bindings; - const figureCitations = figures.filter((f) => f.url?.value).map( - (f) => { - return { url: f.url.value, description: f.description?.value }; - } - ); - return { - creators: json.results.bindings[0]?.creators?.value, - date: json.results.bindings[0]?.date?.value ? parseInt(json.results.bindings[0].date.value, 10) : void 0, - title: json.results.bindings[0]?.title?.value, - materialCitations, - figureCitations, - treats: { - def: new Set( - json.results.bindings[0]?.defs?.value ? json.results.bindings[0].defs.value.split("|") : void 0 - ), - aug: new Set( - json.results.bindings[0]?.augs?.value ? json.results.bindings[0].augs.value.split("|") : void 0 - ), - dpr: new Set( - json.results.bindings[0]?.dprs?.value ? json.results.bindings[0].dprs.value.split("|") : void 0 - ), - citetc: new Set( - json.results.bindings[0]?.cites?.value ? json.results.bindings[0].cites.value.split("|") : void 0 - ), - treattn: new Set( - json.results.bindings[0]?.trttns?.value ? json.results.bindings[0].trttns.value.split("|") : void 0 - ), - citetn: new Set( - json.results.bindings[0]?.citetns?.value ? json.results.bindings[0].citetns.value.split("|") : void 0 - ) - } - }; - } catch (error) { - console.warn("SPARQL Error: " + error); - return { - materialCitations: [], - figureCitations: [], - treats: { - def: /* @__PURE__ */ new Set(), - aug: /* @__PURE__ */ new Set(), - dpr: /* @__PURE__ */ new Set(), - citetc: /* @__PURE__ */ new Set(), - treattn: /* @__PURE__ */ new Set(), - citetn: /* @__PURE__ */ new Set() - } - }; - } - } - /** Allows iterating over the synonyms while they are found */ - [Symbol.asyncIterator]() { - let returnedSoFar = 0; - return { - next: () => new Promise( - (resolve, reject) => { - const callback = () => { - if (this.controller.signal.aborted) { - reject(new Error("SynyonymGroup has been aborted")); - } else if (returnedSoFar < this.names.length) { - resolve({ value: this.names[returnedSoFar++] }); - } else if (this.isFinished) { - resolve({ done: true, value: true }); - } else { - const listener = () => { - this.monitor.removeEventListener("updated", listener); - callback(); - }; - this.monitor.addEventListener("updated", listener); - } - }; - callback(); - } - ) - }; - } -}; -export { - SparqlEndpoint, - SynonymGroup -}; -//# sourceMappingURL=mod.js.map diff --git a/npm-package/build/mod.js.map b/npm-package/build/mod.js.map deleted file mode 100644 index 806852a..0000000 --- a/npm-package/build/mod.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["../../SparqlEndpoint.ts", "../../SynonymGroup.ts"], - "sourcesContent": ["async function sleep(ms: number): Promise {\n const p = new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n return await p;\n}\n\n/** Describes the format of the JSON return by SPARQL endpoints */\nexport type SparqlJson = {\n head: {\n vars: string[];\n };\n results: {\n bindings: {\n [key: string]:\n | { type: string; value: string; \"xml:lang\"?: string }\n | undefined;\n }[];\n };\n};\n\n/**\n * Represents a remote sparql endpoint and provides a uniform way to run queries.\n */\nexport class SparqlEndpoint {\n /** Create a new SparqlEndpoint with the given URI */\n constructor(private sparqlEnpointUri: string) {}\n\n /** @ignore */\n // reasons: string[] = [];\n\n /**\n * Run a query against the sparql endpoint\n *\n * It automatically retries up to 10 times on fetch errors, waiting 50ms on the first retry and doupling the wait each time.\n * Retries are logged to the console (`console.warn`)\n *\n * @throws In case of non-ok response status codes or if fetch failed 10 times.\n * @param query The sparql query to run against the endpoint\n * @param fetchOptions Additional options for the `fetch` request\n * @param _reason (Currently ignored, used internally for debugging purposes)\n * @returns Results of the query\n */\n async getSparqlResultSet(\n query: string,\n fetchOptions: RequestInit = {},\n _reason = \"\",\n ): Promise {\n // this.reasons.push(_reason);\n\n fetchOptions.headers = fetchOptions.headers || {};\n (fetchOptions.headers as Record)[\"Accept\"] =\n \"application/sparql-results+json\";\n let retryCount = 0;\n const sendRequest = async (): Promise => {\n try {\n // console.info(`SPARQL ${_reason} (${retryCount + 1})`);\n const response = await fetch(\n this.sparqlEnpointUri + \"?query=\" + encodeURIComponent(query),\n fetchOptions,\n );\n if (!response.ok) {\n throw new Error(\"Response not ok. Status \" + response.status);\n }\n return await response.json();\n } catch (error) {\n if (fetchOptions.signal?.aborted) {\n throw error;\n } else if (retryCount < 10) {\n const wait = 50 * (1 << retryCount++);\n console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`);\n await sleep(wait);\n return await sendRequest();\n }\n console.warn(\"!! Fetch Error:\", query, \"\\n---\\n\", error);\n throw error;\n }\n };\n return await sendRequest();\n }\n}\n", "import { SparqlEndpoint, SparqlJson } from \"./mod.ts\";\n\n/** Finds all synonyms of a taxon */\nexport class SynonymGroup implements AsyncIterable {\n /** Indicates whether the SynonymGroup has found all synonyms.\n *\n * @readonly\n */\n isFinished = false;\n /** Used internally to watch for new names found */\n private monitor = new EventTarget();\n\n /** Used internally to abort in-flight network requests when SynonymGroup is aborted */\n private controller = new AbortController();\n\n /** The SparqlEndpoint used */\n private sparqlEndpoint: SparqlEndpoint;\n\n /**\n * List of names found so-far.\n *\n * Contains full list of synonyms _if_ .isFinished and not .isAborted\n *\n * @readonly\n */\n names: Name[] = [];\n /**\n * Add a new Name to this.names.\n *\n * Note: does not deduplicate on its own\n *\n * @internal */\n private pushName(name: Name) {\n this.names.push(name);\n this.monitor.dispatchEvent(new CustomEvent(\"updated\"));\n }\n\n /**\n * Call when all synonyms are found\n *\n * @internal */\n private finish() {\n this.isFinished = true;\n this.monitor.dispatchEvent(new CustomEvent(\"updated\"));\n }\n\n /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */\n private expanded = new Set(); // new Map();\n\n /** contains CoL uris where we don't need to check for Col \"acceptedName\" links\n *\n * col -> accepted col\n */\n private acceptedCol = new Map();\n\n /**\n * Used internally to deduplicate treatments, maps from URI to Object.\n *\n * Contains full list of treatments _if_ .isFinished and not .isAborted\n *\n * @readonly\n */\n treatments = new Map();\n\n /**\n * Whether to show taxa deprecated by CoL that would not have been found otherwise.\n * This significantly increases the number of results in some cases.\n */\n ignoreDeprecatedCoL: boolean;\n\n /**\n * if set to true, subTaxa of the search term are also considered as starting points.\n *\n * Not that \"weird\" ranks like subGenus are always included when searching for a genus by latin name.\n */\n startWithSubTaxa: boolean;\n\n /**\n * Constructs a SynonymGroup\n *\n * @param sparqlEndpoint SPARQL-Endpoint to query\n * @param taxonName either a string of the form \"Genus species infraspecific\" (species & infraspecific names optional), or an URI of a http://filteredpush.org/ontologies/oa/dwcFP#TaxonConcept or ...#TaxonName or a CoL taxon URI\n * @param [ignoreDeprecatedCoL=true] Whether to show taxa deprecated by CoL that would not have been found otherwise\n * @param [startWithSubTaxa=false] if set to true, subTaxa of the search term are also considered as starting points.\n */\n constructor(\n sparqlEndpoint: SparqlEndpoint,\n taxonName: string,\n ignoreDeprecatedCoL = true,\n startWithSubTaxa = false,\n ) {\n this.sparqlEndpoint = sparqlEndpoint;\n this.ignoreDeprecatedCoL = ignoreDeprecatedCoL;\n this.startWithSubTaxa = startWithSubTaxa;\n\n if (taxonName.startsWith(\"http\")) {\n this.getName(taxonName, { searchTerm: true, subTaxon: false }).finally(\n () => this.finish(),\n );\n } else {\n const name = [\n ...taxonName.split(\" \").filter((n) => !!n),\n undefined,\n undefined,\n ] as [string, string | undefined, string | undefined];\n this.getNameFromLatin(name, { searchTerm: true, subTaxon: false })\n .finally(\n () => this.finish(),\n );\n }\n }\n\n /** @internal */\n private async getName(\n taxonName: string,\n justification: Justification,\n ): Promise {\n if (this.expanded.has(taxonName)) {\n console.log(\"Skipping known\", taxonName);\n return;\n }\n\n if (taxonName.startsWith(\"https://www.catalogueoflife.org\")) {\n await this.getNameFromCol(taxonName, justification);\n } else if (taxonName.startsWith(\"http://taxon-concept.plazi.org\")) {\n await this.getNameFromTC(taxonName, justification);\n } else if (taxonName.startsWith(\"http://taxon-name.plazi.org\")) {\n await this.getNameFromTN(taxonName, justification);\n } else {\n throw `Cannot handle name-uri <${taxonName}> !`;\n }\n\n if (\n this.startWithSubTaxa && justification.searchTerm &&\n !justification.subTaxon\n ) {\n await this.getSubtaxa(taxonName);\n }\n }\n\n /** @internal */\n private async getSubtaxa(url: string): Promise {\n const query = url.startsWith(\"http://taxon-concept.plazi.org\")\n ? `\nPREFIX trt: \nSELECT DISTINCT ?sub WHERE {\n BIND(<${url}> as ?url)\n ?sub trt:hasParentName*/^trt:hasTaxonName ?url .\n}\nLIMIT 5000`\n : `\nPREFIX dwc: \nPREFIX trt: \nSELECT DISTINCT ?sub WHERE {\n BIND(<${url}> as ?url)\n ?sub (dwc:parent|trt:hasParentName)* ?url .\n}\nLIMIT 5000`;\n\n if (this.controller.signal?.aborted) return Promise.reject();\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `Subtaxa ${url}`,\n );\n\n const names = json.results.bindings\n .map((n) => n.sub?.value)\n .filter((n) => n && !this.expanded.has(n)) as string[];\n\n await Promise.allSettled(\n names.map((n) => this.getName(n, { searchTerm: true, subTaxon: true })),\n );\n }\n\n /** @internal */\n private async getNameFromLatin(\n [genus, species, infrasp]: [string, string | undefined, string | undefined],\n justification: Justification,\n ): Promise {\n const query = `\n PREFIX dwc: \nSELECT DISTINCT ?uri WHERE {\n ?uri dwc:genus|dwc:genericName \"${genus}\" .\n ${\n species\n ? `?uri dwc:species|dwc:specificEpithet \"${species}\" .`\n : \"FILTER NOT EXISTS { ?uri dwc:species|dwc:specificEpithet ?species . }\"\n }\n ${\n infrasp\n ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet \"${infrasp}\" .`\n : \"FILTER NOT EXISTS { ?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }\"\n }\n}\nLIMIT 500`;\n\n if (this.controller.signal?.aborted) return Promise.reject();\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromLatin ${genus} ${species} ${infrasp}`,\n );\n\n const names = json.results.bindings\n .map((n) => n.uri?.value)\n .filter((n) => n && !this.expanded.has(n)) as string[];\n\n await Promise.allSettled(names.map((n) => this.getName(n, justification)));\n }\n\n /** @internal */\n private async getNameFromCol(\n colUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n BIND(<${colUri}> as ?col)\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, \"\") as ?authority)\n ?col dwc:scientificName ?name . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n OPTIONAL {\n ?col dwc:specificEpithet ?species .\n OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . }\n }\n\n OPTIONAL {\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:genus ?genus .\n ?tn dwc:kingdom ?kingdom .\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL {\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n }\n }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n /// ?tn ?tc !rank !genus ?species ?infrasp !name !authority ?tcAuth\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromCol ${colUri}`,\n );\n\n return this.handleName(json, justification);\n }\n\n /** @internal */\n private async getNameFromTC(\n tcUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n <${tcUri}> trt:hasTaxonName ?tn .\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:kingdom ?kingdom .\n ?tn dwc:genus ?genus .\n OPTIONAL {\n ?tn dwc:species ?species .\n OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n \n OPTIONAL {\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . }\n ?col dwc:scientificName ?fullName . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n }\n \n BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(\" (\",?subgenus,\")\"), \"\"), COALESCE(CONCAT(\" \",?species), \"\"), COALESCE(CONCAT(\" \", ?infrasp), \"\"))) as ?name)\n BIND(COALESCE(?colAuth, \"\") as ?authority)\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromTC ${tcUri}`,\n );\n\n await this.handleName(json, justification);\n }\n\n /** @internal */\n private async getNameFromTN(\n tnUri: string,\n justification: Justification,\n ): Promise {\n // Note: this query assumes that there is no sub-species taxa with missing dwc:species\n // Note: the handling assumes that at most one taxon-name matches this colTaxon\n const query = `\nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\n (group_concat(DISTINCT ?tcauth;separator=\" / \") AS ?tcAuth)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trtn;separator=\"|\") as ?tntreats)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?tncites) WHERE {\n BIND(<${tnUri}> as ?tn)\n ?tn a dwcFP:TaxonName .\n ?tn dwc:rank ?rank .\n ?tn dwc:genus ?genus .\n ?tn dwc:kingdom ?kingdom .\n OPTIONAL {\n ?tn dwc:species ?species .\n OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n \n OPTIONAL {\n ?col dwc:taxonRank ?rank .\n OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . }\n ?col dwc:scientificName ?fullName . # Note: contains authority\n ?col dwc:genericName ?genus .\n # TODO # ?col dwc:parent* ?p . ?p dwc:rank \"kingdom\" ; dwc:taxonName ?kingdom .\n\n {\n ?col dwc:specificEpithet ?species .\n ?tn dwc:species ?species .\n {\n ?col dwc:infraspecificEpithet ?infrasp .\n ?tn dwc:subspecies|dwc:variety ?infrasp .\n } UNION {\n FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . }\n FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . }\n }\n } UNION {\n FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . }\n FILTER NOT EXISTS { ?tn dwc:species ?species . }\n }\n }\n \n BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(\" (\",?subgenus,\")\"), \"\"), COALESCE(CONCAT(\" \",?species), \"\"), COALESCE(CONCAT(\" \", ?infrasp), \"\"))) as ?name)\n BIND(COALESCE(?colAuth, \"\") as ?authority)\n\n OPTIONAL { ?trtn trt:treatsTaxonName ?tn . }\n OPTIONAL { ?citetn trt:citesTaxonName ?tn . }\n\n OPTIONAL {\n ?tc trt:hasTaxonName ?tn ;\n dwc:scientificNameAuthorship ?tcauth ;\n a dwcFP:TaxonConcept .\n OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . }\n OPTIONAL { ?def trt:definesTaxonConcept ?tc . }\n OPTIONAL { ?dpr trt:deprecates ?tc . }\n OPTIONAL { ?cite cito:cites ?tc . }\n }\n}\nGROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority\nLIMIT 500`;\n // For unclear reasons, the query breaks if the limit is removed.\n\n if (this.controller.signal?.aborted) return Promise.reject();\n\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `NameFromTN ${tnUri}`,\n );\n\n return this.handleName(json, justification);\n }\n\n /**\n * Note this makes some assumptions on which variables are present in the bindings\n *\n * @internal */\n private async handleName(\n json: SparqlJson,\n justification: Justification,\n ): Promise {\n const treatmentPromises: Treatment[] = [];\n\n const displayName: string = json.results.bindings[0].name!.value\n .replace(\n json.results.bindings[0].authority!.value,\n \"\",\n ).trim();\n\n const colName: AuthorizedName | undefined =\n json.results.bindings[0].col?.value\n ? {\n displayName,\n authority: json.results.bindings[0].authority!.value,\n colURI: json.results.bindings[0].col.value,\n treatments: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n cite: new Set(),\n },\n }\n : undefined;\n\n if (colName) {\n if (this.expanded.has(colName.colURI!)) return;\n this.expanded.add(colName.colURI!);\n }\n\n const authorizedNames = colName ? [colName] : [];\n\n const taxonNameURI = json.results.bindings[0].tn?.value;\n if (taxonNameURI) {\n if (this.expanded.has(taxonNameURI)) return;\n this.expanded.add(taxonNameURI); //, NameStatus.madeName);\n }\n\n for (const t of json.results.bindings) {\n if (t.tc && t.tcAuth?.value) {\n if (this.expanded.has(t.tc.value)) {\n // console.log(\"Abbruch: already known\", t.tc.value);\n return;\n }\n const def = this.makeTreatmentSet(t.defs?.value.split(\"|\"));\n const aug = this.makeTreatmentSet(t.augs?.value.split(\"|\"));\n const dpr = this.makeTreatmentSet(t.dprs?.value.split(\"|\"));\n const cite = this.makeTreatmentSet(t.cites?.value.split(\"|\"));\n if (\n colName && t.tcAuth?.value.split(\" / \").includes(colName.authority)\n ) {\n colName.authority = t.tcAuth?.value;\n colName.taxonConceptURI = t.tc.value;\n colName.treatments = {\n def,\n aug,\n dpr,\n cite,\n };\n } else {\n authorizedNames.push({\n displayName,\n authority: t.tcAuth.value,\n taxonConceptURI: t.tc.value,\n treatments: {\n def,\n aug,\n dpr,\n cite,\n },\n });\n }\n // this.expanded.set(t.tc.value, NameStatus.madeName);\n this.expanded.add(t.tc.value);\n\n def.forEach((t) => treatmentPromises.push(t));\n aug.forEach((t) => treatmentPromises.push(t));\n dpr.forEach((t) => treatmentPromises.push(t));\n }\n }\n\n // TODO: handle col-data \"acceptedName\" and stuff\n\n const treats = this.makeTreatmentSet(\n json.results.bindings[0].tntreats?.value.split(\"|\"),\n );\n treats.forEach((t) => treatmentPromises.push(t));\n\n const name: Name = {\n displayName,\n taxonNameURI,\n authorizedNames,\n justification,\n treatments: {\n treats,\n cite: this.makeTreatmentSet(\n json.results.bindings[0].tncites?.value.split(\"|\"),\n ),\n },\n vernacularNames: taxonNameURI\n ? this.getVernacular(taxonNameURI)\n : Promise.resolve(new Map()),\n };\n\n let colPromises: Promise[] = [];\n\n if (colName) {\n [colName.acceptedColURI, colPromises] = await this.getAcceptedCol(\n colName.colURI!,\n name,\n );\n }\n\n this.pushName(name);\n\n /** Map */\n const newSynonyms = new Map();\n (await Promise.all(\n treatmentPromises.map((treat) =>\n treat.details.then((d): [Treatment, TreatmentDetails] => {\n return [treat, d];\n })\n ),\n )).map(([treat, d]) => {\n d.treats.aug.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.def.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.dpr.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n d.treats.treattn.difference(this.expanded).forEach((s) =>\n newSynonyms.set(s, treat)\n );\n });\n\n await Promise.allSettled(\n [\n ...colPromises,\n ...[...newSynonyms].map(([n, treatment]) =>\n this.getName(n, { searchTerm: false, parent: name, treatment })\n ),\n ],\n );\n }\n\n /** @internal */\n private async getAcceptedCol(\n colUri: string,\n parent: Name,\n ): Promise<[string, Promise[]]> {\n const query = `\nPREFIX dwc: \nSELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator=\"|\") AS ?dprs) WHERE {\n BIND(<${colUri}> AS ?col)\n {\n ?col dwc:acceptedName ?current .\n ?dpr dwc:acceptedName ?current .\n ?current dwc:taxonomicStatus ?current_status .\n } UNION {\n ?col dwc:taxonomicStatus ?current_status .\n OPTIONAL { ?dpr dwc:acceptedName ?col . }\n FILTER NOT EXISTS { ?col dwc:acceptedName ?current . }\n BIND(?col AS ?current)\n }\n}\nGROUP BY ?current ?current_status`;\n\n if (this.acceptedCol.has(colUri)) {\n return [this.acceptedCol.get(colUri)!, []];\n }\n\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `AcceptedCol ${colUri}`,\n );\n\n const promises: Promise[] = [];\n\n for (const b of json.results.bindings) {\n for (const dpr of b.dprs!.value.split(\"|\")) {\n if (dpr) {\n if (!this.acceptedCol.has(b.current!.value)) {\n this.acceptedCol.set(b.current!.value, b.current!.value);\n promises.push(\n this.getNameFromCol(b.current!.value, {\n searchTerm: false,\n parent,\n }),\n );\n }\n\n this.acceptedCol.set(dpr, b.current!.value);\n if (!this.ignoreDeprecatedCoL) {\n promises.push(\n this.getNameFromCol(dpr, { searchTerm: false, parent }),\n );\n }\n }\n }\n }\n\n if (json.results.bindings.length === 0) {\n // the provided colUri is not in CoL\n // promises === []\n if (!this.acceptedCol.has(colUri)) {\n this.acceptedCol.set(colUri, \"INVALID COL\");\n }\n return [this.acceptedCol.get(colUri)!, promises];\n }\n\n if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri);\n return [this.acceptedCol.get(colUri)!, promises];\n }\n\n /** @internal */\n private async getVernacular(uri: string): Promise {\n const result: vernacularNames = new Map();\n const query =\n `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`;\n const bindings = (await this.sparqlEndpoint.getSparqlResultSet(query, {\n signal: this.controller.signal,\n }, `Vernacular ${uri}`)).results.bindings;\n for (const b of bindings) {\n if (b.n?.value) {\n if (b.n[\"xml:lang\"]) {\n if (result.has(b.n[\"xml:lang\"])) {\n result.get(b.n[\"xml:lang\"])!.push(b.n.value);\n } else result.set(b.n[\"xml:lang\"], [b.n.value]);\n } else {\n if (result.has(\"??\")) result.get(\"??\")!.push(b.n.value);\n else result.set(\"??\", [b.n.value]);\n }\n }\n }\n return result;\n }\n\n /** @internal */\n private makeTreatmentSet(urls?: string[]): Set {\n if (!urls) return new Set();\n return new Set(\n urls.filter((url) => !!url).map((url) => {\n if (!this.treatments.has(url)) {\n const details = this.getTreatmentDetails(url);\n this.treatments.set(url, {\n url,\n details,\n });\n }\n return this.treatments.get(url) as Treatment;\n }),\n );\n }\n\n /** @internal */\n private async getTreatmentDetails(\n treatmentUri: string,\n ): Promise {\n const query = `\nPREFIX dc: \nPREFIX dwc: \nPREFIX dwcFP: \nPREFIX cito: \nPREFIX trt: \nSELECT DISTINCT\n ?date ?title ?mc\n (group_concat(DISTINCT ?catalogNumber;separator=\" / \") as ?catalogNumbers)\n (group_concat(DISTINCT ?collectionCode;separator=\" / \") as ?collectionCodes)\n (group_concat(DISTINCT ?typeStatus;separator=\" / \") as ?typeStatuss)\n (group_concat(DISTINCT ?countryCode;separator=\" / \") as ?countryCodes)\n (group_concat(DISTINCT ?stateProvince;separator=\" / \") as ?stateProvinces)\n (group_concat(DISTINCT ?municipality;separator=\" / \") as ?municipalitys)\n (group_concat(DISTINCT ?county;separator=\" / \") as ?countys)\n (group_concat(DISTINCT ?locality;separator=\" / \") as ?localitys)\n (group_concat(DISTINCT ?verbatimLocality;separator=\" / \") as ?verbatimLocalitys)\n (group_concat(DISTINCT ?recordedBy;separator=\" / \") as ?recordedBys)\n (group_concat(DISTINCT ?eventDate;separator=\" / \") as ?eventDates)\n (group_concat(DISTINCT ?samplingProtocol;separator=\" / \") as ?samplingProtocols)\n (group_concat(DISTINCT ?decimalLatitude;separator=\" / \") as ?decimalLatitudes)\n (group_concat(DISTINCT ?decimalLongitude;separator=\" / \") as ?decimalLongitudes)\n (group_concat(DISTINCT ?verbatimElevation;separator=\" / \") as ?verbatimElevations)\n (group_concat(DISTINCT ?gbifOccurrenceId;separator=\" / \") as ?gbifOccurrenceIds)\n (group_concat(DISTINCT ?gbifSpecimenId;separator=\" / \") as ?gbifSpecimenIds)\n (group_concat(DISTINCT ?creator;separator=\"; \") as ?creators)\n (group_concat(DISTINCT ?httpUri;separator=\"|\") as ?httpUris)\n (group_concat(DISTINCT ?aug;separator=\"|\") as ?augs)\n (group_concat(DISTINCT ?def;separator=\"|\") as ?defs)\n (group_concat(DISTINCT ?dpr;separator=\"|\") as ?dprs)\n (group_concat(DISTINCT ?cite;separator=\"|\") as ?cites)\n (group_concat(DISTINCT ?trttn;separator=\"|\") as ?trttns)\n (group_concat(DISTINCT ?citetn;separator=\"|\") as ?citetns)\nWHERE {\n BIND (<${treatmentUri}> as ?treatment)\n ?treatment dc:creator ?creator .\n OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . }\n OPTIONAL { ?treatment dc:title ?title }\n OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . }\n OPTIONAL { ?treatment trt:definesTaxonConcept ?def . }\n OPTIONAL { ?treatment trt:deprecates ?dpr . }\n OPTIONAL { ?treatment cito:cites ?cite . ?cite a dwcFP:TaxonConcept . }\n OPTIONAL { ?treatment trt:treatsTaxonName ?trttn . }\n OPTIONAL { ?treatment trt:citesTaxonName ?citetn . }\n OPTIONAL {\n ?treatment dwc:basisOfRecord ?mc .\n ?mc dwc:catalogNumber ?catalogNumber .\n OPTIONAL { ?mc dwc:collectionCode ?collectionCode . }\n OPTIONAL { ?mc dwc:typeStatus ?typeStatus . }\n OPTIONAL { ?mc dwc:countryCode ?countryCode . }\n OPTIONAL { ?mc dwc:stateProvince ?stateProvince . }\n OPTIONAL { ?mc dwc:municipality ?municipality . }\n OPTIONAL { ?mc dwc:county ?county . }\n OPTIONAL { ?mc dwc:locality ?locality . }\n OPTIONAL { ?mc dwc:verbatimLocality ?verbatimLocality . }\n OPTIONAL { ?mc dwc:recordedBy ?recordedBy . }\n OPTIONAL { ?mc dwc:eventDate ?eventDate . }\n OPTIONAL { ?mc dwc:samplingProtocol ?samplingProtocol . }\n OPTIONAL { ?mc dwc:decimalLatitude ?decimalLatitude . }\n OPTIONAL { ?mc dwc:decimalLongitude ?decimalLongitude . }\n OPTIONAL { ?mc dwc:verbatimElevation ?verbatimElevation . }\n OPTIONAL { ?mc trt:gbifOccurrenceId ?gbifOccurrenceId . }\n OPTIONAL { ?mc trt:gbifSpecimenId ?gbifSpecimenId . }\n OPTIONAL { ?mc trt:httpUri ?httpUri . }\n }\n}\nGROUP BY ?date ?title ?mc`;\n if (this.controller.signal.aborted) {\n return {\n materialCitations: [],\n figureCitations: [],\n treats: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n citetc: new Set(),\n treattn: new Set(),\n citetn: new Set(),\n },\n };\n }\n try {\n const json = await this.sparqlEndpoint.getSparqlResultSet(\n query,\n { signal: this.controller.signal },\n `TreatmentDetails ${treatmentUri}`,\n );\n const materialCitations: MaterialCitation[] = json.results.bindings\n .filter((t) => t.mc && t.catalogNumbers?.value)\n .map((t) => {\n const httpUri = t.httpUris?.value?.split(\"|\");\n return {\n \"catalogNumber\": t.catalogNumbers!.value,\n \"collectionCode\": t.collectionCodes?.value || undefined,\n \"typeStatus\": t.typeStatuss?.value || undefined,\n \"countryCode\": t.countryCodes?.value || undefined,\n \"stateProvince\": t.stateProvinces?.value || undefined,\n \"municipality\": t.municipalitys?.value || undefined,\n \"county\": t.countys?.value || undefined,\n \"locality\": t.localitys?.value || undefined,\n \"verbatimLocality\": t.verbatimLocalitys?.value || undefined,\n \"recordedBy\": t.recordedBys?.value || undefined,\n \"eventDate\": t.eventDates?.value || undefined,\n \"samplingProtocol\": t.samplingProtocols?.value || undefined,\n \"decimalLatitude\": t.decimalLatitudes?.value || undefined,\n \"decimalLongitude\": t.decimalLongitudes?.value || undefined,\n \"verbatimElevation\": t.verbatimElevations?.value || undefined,\n \"gbifOccurrenceId\": t.gbifOccurrenceIds?.value || undefined,\n \"gbifSpecimenId\": t.gbifSpecimenIds?.value || undefined,\n httpUri: httpUri?.length ? httpUri : undefined,\n };\n });\n const figureQuery = `\nPREFIX cito: \nPREFIX fabio: \nPREFIX dc: \nSELECT DISTINCT ?url ?description WHERE {\n <${treatmentUri}> cito:cites ?cites .\n ?cites a fabio:Figure ;\n fabio:hasRepresentation ?url .\n OPTIONAL { ?cites dc:description ?description . }\n} `;\n const figures = (await this.sparqlEndpoint.getSparqlResultSet(\n figureQuery,\n { signal: this.controller.signal },\n `TreatmentDetails/Figures ${treatmentUri}`,\n )).results.bindings;\n const figureCitations = figures.filter((f) => f.url?.value).map(\n (f) => {\n return { url: f.url!.value, description: f.description?.value };\n },\n );\n return {\n creators: json.results.bindings[0]?.creators?.value,\n date: json.results.bindings[0]?.date?.value\n ? parseInt(json.results.bindings[0].date.value, 10)\n : undefined,\n title: json.results.bindings[0]?.title?.value,\n materialCitations,\n figureCitations,\n treats: {\n def: new Set(\n json.results.bindings[0]?.defs?.value\n ? json.results.bindings[0].defs.value.split(\"|\")\n : undefined,\n ),\n aug: new Set(\n json.results.bindings[0]?.augs?.value\n ? json.results.bindings[0].augs.value.split(\"|\")\n : undefined,\n ),\n dpr: new Set(\n json.results.bindings[0]?.dprs?.value\n ? json.results.bindings[0].dprs.value.split(\"|\")\n : undefined,\n ),\n citetc: new Set(\n json.results.bindings[0]?.cites?.value\n ? json.results.bindings[0].cites.value.split(\"|\")\n : undefined,\n ),\n treattn: new Set(\n json.results.bindings[0]?.trttns?.value\n ? json.results.bindings[0].trttns.value.split(\"|\")\n : undefined,\n ),\n citetn: new Set(\n json.results.bindings[0]?.citetns?.value\n ? json.results.bindings[0].citetns.value.split(\"|\")\n : undefined,\n ),\n },\n };\n } catch (error) {\n console.warn(\"SPARQL Error: \" + error);\n return {\n materialCitations: [],\n figureCitations: [],\n treats: {\n def: new Set(),\n aug: new Set(),\n dpr: new Set(),\n citetc: new Set(),\n treattn: new Set(),\n citetn: new Set(),\n },\n };\n }\n }\n\n /** Allows iterating over the synonyms while they are found */\n [Symbol.asyncIterator](): AsyncIterator {\n let returnedSoFar = 0;\n return {\n next: () =>\n new Promise>(\n (resolve, reject) => {\n const callback = () => {\n if (this.controller.signal.aborted) {\n reject(new Error(\"SynyonymGroup has been aborted\"));\n } else if (returnedSoFar < this.names.length) {\n resolve({ value: this.names[returnedSoFar++] });\n } else if (this.isFinished) {\n resolve({ done: true, value: true });\n } else {\n const listener = () => {\n this.monitor.removeEventListener(\"updated\", listener);\n callback();\n };\n this.monitor.addEventListener(\"updated\", listener);\n }\n };\n callback();\n },\n ),\n };\n }\n}\n\n/** The central object.\n *\n * Each `Name` exists because of a taxon-name, taxon-concept or col-taxon in the data.\n * Each `Name` is uniquely determined by its human-readable latin name (for taxa ranking below genus, this is a multi-part name \u2014 binomial or trinomial) and kingdom.\n */\nexport type Name = {\n /** taxonomic kingdom */\n // kingdom: string;\n /** Human-readable name */\n displayName: string;\n\n /** vernacular names */\n vernacularNames: Promise;\n\n // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank.\n // * //TODO */\n // trees: Promise<{\n // col?: Tree;\n // tb?: Tree;\n // }>;\n\n /** The URI of the respective `dwcFP:TaxonName` if it exists */\n taxonNameURI?: string;\n /** All `AuthorizedName`s with this name */\n authorizedNames: AuthorizedName[];\n\n /** How this name was found */\n justification: Justification;\n\n /** treatments directly associated with .taxonNameUri */\n treatments: {\n treats: Set;\n cite: Set;\n };\n};\n\n/**\n * A map from language tags (IETF) to an array of vernacular names.\n */\nexport type vernacularNames = Map;\n\n/** Why a given Name was found (ther migth be other possible justifications) */\nexport type Justification = {\n searchTerm: true;\n /** indicates that this is a subTaxon of the parent */\n subTaxon: boolean;\n} | {\n searchTerm: false;\n parent: Name;\n /** if missing, indicates synonymy according to CoL or subTaxon */\n treatment?: Treatment;\n};\n\n/**\n * Corresponds to a taxon-concept or a CoL-Taxon\n */\nexport type AuthorizedName = {\n // TODO: neccesary?\n /** this may not be neccesary, as `AuthorizedName`s should only appear within a `Name` */\n // name: Name;\n /** Human-readable name */\n displayName: string;\n /** Human-readable authority */\n authority: string;\n\n /** The URI of the respective `dwcFP:TaxonConcept` if it exists */\n taxonConceptURI?: string;\n\n /** The URI of the respective CoL-taxon if it exists */\n colURI?: string;\n /** The URI of the corresponding accepted CoL-taxon if it exists.\n *\n * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon.\n *\n * May be the string \"INVALID COL\" if the colURI is not valid.\n */\n acceptedColURI?: string;\n\n // TODO: sensible?\n // /** these are CoL-taxa linked in the rdf, which differ lexically */\n // seeAlsoCol: string[];\n\n /** treatments directly associated with .taxonConceptURI */\n treatments: {\n def: Set;\n aug: Set;\n dpr: Set;\n cite: Set;\n };\n};\n\n/** A plazi-treatment */\nexport type Treatment = {\n url: string;\n\n /** Details are behind a promise becuase they are loaded with a separate query. */\n details: Promise;\n};\n\n/** Details of a treatment */\nexport type TreatmentDetails = {\n materialCitations: MaterialCitation[];\n figureCitations: FigureCitation[];\n date?: number;\n creators?: string;\n title?: string;\n treats: {\n def: Set;\n aug: Set;\n dpr: Set;\n citetc: Set;\n treattn: Set;\n citetn: Set;\n };\n};\n\n/** A cited material */\nexport type MaterialCitation = {\n \"catalogNumber\": string;\n \"collectionCode\"?: string;\n \"typeStatus\"?: string;\n \"countryCode\"?: string;\n \"stateProvince\"?: string;\n \"municipality\"?: string;\n \"county\"?: string;\n \"locality\"?: string;\n \"verbatimLocality\"?: string;\n \"recordedBy\"?: string;\n \"eventDate\"?: string;\n \"samplingProtocol\"?: string;\n \"decimalLatitude\"?: string;\n \"decimalLongitude\"?: string;\n \"verbatimElevation\"?: string;\n \"gbifOccurrenceId\"?: string;\n \"gbifSpecimenId\"?: string;\n \"httpUri\"?: string[];\n};\n\n/** A cited figure */\nexport type FigureCitation = {\n url: string;\n description?: string;\n};\n"], - "mappings": "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", - "names": ["t"] -} diff --git a/npm-package/readme.md b/npm-package/readme.md deleted file mode 100644 index edce317..0000000 --- a/npm-package/readme.md +++ /dev/null @@ -1,38 +0,0 @@ -# SynoGroup NPM Package - -This folder contains all the neccesary tools to generate and publish a NPM -package containing the synogroup library. - -(If you’re reading this on npmjs.com, read the actual readme in the parent -repository linked to the left for more Information about Synogroup itself) - -## How to - -```bash -# (from within this folder) -npm install # install tsc for the declaration file -npm version patch # or ensure that the version number differs from the last published version otherwise -# npm run publish-package # generates and publishes npm package -npm run build -``` - -**Note that the generated types are currently possibly broken, please check! -manually remove `import`s from `index.d.ts` before publishing** - -```bash -# npm publish --access public -``` - -## Testing (-ish) - -```bash -npm run build -``` - -Generates the package code (index.js & index.d.ts) without publishing. - -## Prerequiites - -2. You need to be logged in to NPM with an account that can publish - "@factsmission/synogroup" -3. diff --git a/npm-package/package-lock.json b/package-lock.json similarity index 100% rename from npm-package/package-lock.json rename to package-lock.json diff --git a/npm-package/package.json b/package.json similarity index 78% rename from npm-package/package.json rename to package.json index d67accd..b05bd3e 100644 --- a/npm-package/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "", "main": "build/mod.mjs", "scripts": { - "build": "node build.mjs && rm -rf ./types; tsc", + "build": "node build.mjs build", + "build___unused": "node build.mjs build && rm -rf ./types; tsc", + "example": "node build.mjs example", "publish-package": "echo 'see readme'" }, "repository": { diff --git a/npm-package/tsconfig.json b/tsconfig.json similarity index 100% rename from npm-package/tsconfig.json rename to tsconfig.json From 4c22ecd537d9413f300a617b6072e69d56ad226c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:08:01 +0000 Subject: [PATCH 23/71] updated README.md to explain example --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f2f535d..ba811cc 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,15 @@ npm run example serve (Both require `npm install` first) +The example page uses query parameters for options: +- `q=TAXON` for the search term (Latin name, CoL-URI, taxon-name-URI or taxon-concept-URI) +- `show_col=` to include many more CoL taxa +- `subtaxa=` to include subtaxa of the search term +- `server=URL` to configure the sparql endpoint + +e.g. http://localhost:8000/?q=Sadayoshia%20miyakei&show_col= + + ## Building for npm/web To build the library for use in web projects, use From f2bb1d7981e43f5a20b2e47f6055c09b5972191f Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:26:19 +0000 Subject: [PATCH 24/71] looks real nice --- example/index.css | 71 +++++++++++++--- example/index.ts | 213 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 238 insertions(+), 46 deletions(-) diff --git a/example/index.css b/example/index.css index 0d7ffe4..926b553 100644 --- a/example/index.css +++ b/example/index.css @@ -1,30 +1,49 @@ +* { + box-sizing: border-box; +} + #root:empty::before { content: "You should see a list of synonyms here"; } +svg { + height: 1rem; + vertical-align: sub; + margin: 0; +} + syno-name { display: block; margin: 1rem 0; } -syno-treatment { - &.def { list-style: "DEF "; } - &.aug { list-style: "(•) "; } - &.dpr { list-style: "(×) "; } - &.cite { list-style: "(•) "; color: gray; } -} - .uri:not(:empty) { font-size: 0.8rem; - padding: 0.2rem; + line-height: 1rem; + padding: 0 0.2rem; margin: 0.2rem; border-radius: 0.2rem; background: #ededed; font-family: monospace; font-weight: normal; - &.taxon { color: #4e69ae; } - &.col { color: #177669; } + &.taxon { + color: #4e69ae; + } + + &.treatment { + color: #8894af; + } + + &.col { + color: #177669; + } + + svg { + height: 0.8em; + vertical-align: baseline; + margin: 0 -0.1em 0 0.2em; + } } .justification { @@ -35,6 +54,36 @@ syno-treatment { background: #ededed; } -h2, h3, ul { +h2, +h3, +ul { margin: 0; +} + + +syno-treatment, +.treatmentline { + display: block; + + &>svg { + height: 1rem; + vertical-align: sub; + margin: 0 0.2rem 0 -1.2rem; + } +} + +.blue { + color: #1e88e5; +} + +.green { + color: #388e3c; +} + +.red { + color: #e53935; +} + +.gray { + color: #666666; } \ No newline at end of file diff --git a/example/index.ts b/example/index.ts index e3afe68..dd07148 100644 --- a/example/index.ts +++ b/example/index.ts @@ -16,18 +16,158 @@ const NAME = params.get("q") || const root = document.getElementById("root") as HTMLDivElement; +enum SynoStatus { + Def = "def", + Aug = "aug", + Dpr = "dpr", + Cite = "cite", +} + +const icons = { + def: + ``, + aug: + ``, + dpr: + ``, + cite: + ``, + unknown: + ``, + + link: + ``, + east: + ``, + west: + ``, + line: + ``, + empty: ``, +}; + class SynoTreatment extends HTMLElement { - constructor(trt: Treatment) { + constructor(trt: Treatment, status: SynoStatus) { super(); - const li = document.createElement("li"); - li.innerText = trt.url; - this.append(li); - trt.details.then((details) => - li.innerText = `${details.creators} ${details.date} “${ - details.title || "No Title" - }” ${trt.url}` - ); + this.innerHTML = icons[status] ?? icons.unknown; + + const creators = document.createElement("span"); + this.append(creators); + + const date = document.createElement("span"); + this.append(" ", date); + + const title = document.createElement("em"); + this.append(" ", title); + + const url = document.createElement("a"); + url.classList.add("treatment", "uri"); + url.href = trt.url; + url.innerText = trt.url.replace("http://treatment.plazi.org/id/", ""); + url.innerHTML += icons.link; + this.append(" ", url); + + const names = document.createElement("div"); + this.append(names); + + trt.details.then((details) => { + if (details.creators) creators.innerText = details.creators; + else { + creators.classList.add("missing"); + creators.innerText = "No Authors"; + } + + if (details.date) date.innerText = "" + details.date; + else { + date.classList.add("missing"); + date.innerText = "No Date"; + } + + if (details.title) title.innerText = "“" + details.title + "”"; + else { + title.classList.add("missing"); + title.innerText = "No Title"; + } + + if ( + status !== SynoStatus.Def && details.treats.def.size > 0 && + status !== SynoStatus.Cite + ) { + const line = document.createElement("div"); + line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + line.innerHTML += icons.def; + names.append(line); + + details.treats.def.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + line.append(url); + }); + } + if ( + status !== SynoStatus.Aug && + (details.treats.aug.size > 0 || details.treats.treattn.size > 0) && + status !== SynoStatus.Cite + ) { + const line = document.createElement("div"); + line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + line.innerHTML += icons.aug; + names.append(line); + + details.treats.aug.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + line.append(url); + }); + details.treats.treattn.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-name.plazi.org/id/", ""); + line.append(url); + }); + } + if ( + status !== SynoStatus.Dpr && details.treats.dpr.size > 0 && + status !== SynoStatus.Cite + ) { + const line = document.createElement("div"); + line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.west; + line.innerHTML += icons.dpr; + names.append(line); + + details.treats.dpr.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + line.append(url); + }); + } + if ( + status !== SynoStatus.Dpr && + (details.treats.citetc.size > 0 || details.treats.citetn.size > 0) && + status !== SynoStatus.Cite + ) { + const line = document.createElement("div"); + line.innerHTML = icons.empty + icons.cite; + names.append(line); + + details.treats.citetc.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + line.append(url); + }); + details.treats.citetn.forEach((n) => { + const url = document.createElement("code"); + url.classList.add("taxon", "uri"); + url.innerText = n.replace("http://taxon-name.plazi.org/id/", ""); + line.append(url); + }); + } + }); } } customElements.define("syno-treatment", SynoTreatment); @@ -43,16 +183,16 @@ class SynoName extends HTMLElement { if (name.taxonNameURI) { const name_uri = document.createElement("code"); name_uri.classList.add("taxon", "uri"); - name_uri.innerText = name.taxonNameURI.replace("http://", ""); + name_uri.innerText = name.taxonNameURI.replace("http://taxon-name.plazi.org/id/", ""); name_uri.title = name.taxonNameURI; - title.append(" ", name_uri); + title.append(name_uri); } const justification = document.createElement("abbr"); justification.classList.add("justification"); justification.innerText = "...?"; justify(name).then((just) => justification.title = `This ${just}`); - title.append(" ", justification); + title.append(justification); const vernacular = document.createElement("code"); vernacular.classList.add("vernacular"); @@ -67,13 +207,11 @@ class SynoName extends HTMLElement { const treatments = document.createElement("ul"); this.append(treatments); for (const trt of name.treatments.treats) { - const li = new SynoTreatment(trt); - li.classList.add("aug"); + const li = new SynoTreatment(trt, SynoStatus.Aug); treatments.append(li); } for (const trt of name.treatments.cite) { - const li = new SynoTreatment(trt); - li.classList.add("cite"); + const li = new SynoTreatment(trt, SynoStatus.Cite); treatments.append(li); } } @@ -91,11 +229,11 @@ class SynoName extends HTMLElement { const name_uri = document.createElement("code"); name_uri.classList.add("taxon", "uri"); name_uri.innerText = authorizedName.taxonConceptURI.replace( - "http://", + "http://taxon-concept.plazi.org/id/", "", ); name_uri.title = authorizedName.taxonConceptURI; - authName.append(" ", name_uri); + authName.append(name_uri); } if (authorizedName.colURI) { const col_uri = document.createElement("code"); @@ -107,15 +245,27 @@ class SynoName extends HTMLElement { col_uri.innerText = id; col_uri.id = id; col_uri.title = authorizedName.colURI; - authName.append(" ", col_uri); + authName.append(col_uri); - const li = document.createElement("li"); - li.classList.add("treatment"); - li.innerText = "Catalogue of Life"; + const li = document.createElement("div"); + li.classList.add("treatmentline"); + li.innerHTML = authorizedName.acceptedColURI !== authorizedName.colURI + ? icons.dpr + : icons.aug; treatments.append(li); + const creators = document.createElement("span"); + creators.innerText = "Catalogue of Life"; + li.append(creators); + + const names = document.createElement("div"); + li.append(names); + if (authorizedName.acceptedColURI !== authorizedName.colURI) { - li.classList.add("dpr"); + const line = document.createElement("div"); + line.innerHTML = icons.east + icons.aug; + names.append(line); + const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); const id = authorizedName.acceptedColURI!.replace( @@ -125,31 +275,24 @@ class SynoName extends HTMLElement { col_uri.innerText = id; col_uri.href = `#${id}`; col_uri.title = authorizedName.acceptedColURI!; - li.append(" → "); - li.append(col_uri); - } else { - li.classList.add("aug"); + line.append(col_uri); } } for (const trt of authorizedName.treatments.def) { - const li = new SynoTreatment(trt); - li.classList.add("def"); + const li = new SynoTreatment(trt, SynoStatus.Def); treatments.append(li); } for (const trt of authorizedName.treatments.aug) { - const li = new SynoTreatment(trt); - li.classList.add("aug"); + const li = new SynoTreatment(trt, SynoStatus.Aug); treatments.append(li); } for (const trt of authorizedName.treatments.dpr) { - const li = new SynoTreatment(trt); - li.classList.add("dpr"); + const li = new SynoTreatment(trt, SynoStatus.Dpr); treatments.append(li); } for (const trt of authorizedName.treatments.cite) { - const li = new SynoTreatment(trt); - li.classList.add("cite"); + const li = new SynoTreatment(trt, SynoStatus.Cite); treatments.append(li); } } From 6d34a0bae8d5e774cf518756bad018a136147ba4 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:50:55 +0000 Subject: [PATCH 25/71] tweaks --- example/index.css | 4 ++++ example/index.ts | 26 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/example/index.css b/example/index.css index 926b553..279381c 100644 --- a/example/index.css +++ b/example/index.css @@ -72,6 +72,10 @@ syno-treatment, } } +.indent { + margin-left: 1.4rem; +} + .blue { color: #1e88e5; } diff --git a/example/index.ts b/example/index.ts index dd07148..856f17e 100644 --- a/example/index.ts +++ b/example/index.ts @@ -53,12 +53,15 @@ class SynoTreatment extends HTMLElement { this.innerHTML = icons[status] ?? icons.unknown; const creators = document.createElement("span"); + creators.innerText = "…"; this.append(creators); const date = document.createElement("span"); + date.innerText = "…"; this.append(" ", date); - const title = document.createElement("em"); + const title = document.createElement("i"); + title.innerText = "…"; this.append(" ", title); const url = document.createElement("a"); @@ -69,6 +72,7 @@ class SynoTreatment extends HTMLElement { this.append(" ", url); const names = document.createElement("div"); + names.classList.add("indent"); this.append(names); trt.details.then((details) => { @@ -177,13 +181,18 @@ class SynoName extends HTMLElement { super(); const title = document.createElement("h2"); - title.innerText = name.displayName; + const name_title = document.createElement("i"); + name_title.innerText = name.displayName; + title.append(name_title); this.append(title); if (name.taxonNameURI) { const name_uri = document.createElement("code"); name_uri.classList.add("taxon", "uri"); - name_uri.innerText = name.taxonNameURI.replace("http://taxon-name.plazi.org/id/", ""); + name_uri.innerText = name.taxonNameURI.replace( + "http://taxon-name.plazi.org/id/", + "", + ); name_uri.title = name.taxonNameURI; title.append(name_uri); } @@ -218,8 +227,11 @@ class SynoName extends HTMLElement { for (const authorizedName of name.authorizedNames) { const authName = document.createElement("h3"); - authName.innerText = authorizedName.displayName + " " + - authorizedName.authority; + const name_title = document.createElement("i"); + name_title.innerText = authorizedName.displayName; + name_title.classList.add("gray"); + authName.append(name_title); + authName.append(" ", authorizedName.authority); this.append(authName); const treatments = document.createElement("ul"); @@ -341,6 +353,6 @@ const timeEnd = performance.now(); indicator.innerHTML = ""; indicator.innerText = `Found ${synoGroup.names.length} names with ${synoGroup.treatments.size} treatments. This took ${ - timeEnd - timeStart - } milliseconds.`; + (timeEnd - timeStart) / 1000 + } seconds.`; if (synoGroup.names.length === 0) root.append(":["); From 8877e5e42d2141835aaee9fdc68ae9d5ae97965c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:50:51 +0000 Subject: [PATCH 26/71] fixed build --- build.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.mjs b/build.mjs index 67f3571..f6ebd99 100644 --- a/build.mjs +++ b/build.mjs @@ -4,7 +4,7 @@ const SERVE = process.argv.includes("serve"); const BUILD = process.argv.includes("build"); const EXAMPLE = process.argv.includes("example"); -let ctx = await esbuild.context({ +const config = { entryPoints: EXAMPLE ? ["./example/index.ts"] : ["./mod.ts"], outfile: EXAMPLE ? "./example/index.js" : "./build/mod.js", sourcemap: true, @@ -18,9 +18,10 @@ let ctx = await esbuild.context({ "new EventSource('/esbuild').addEventListener('change', () => location.reload());", } : undefined, -}); +}; if (SERVE) { + let ctx = await esbuild.context(config); await ctx.watch(); const { host, port } = await ctx.serve({ @@ -29,5 +30,5 @@ if (SERVE) { console.log(`Listening at ${host}:${port}`); } else { - await ctx.rebuild(); + await esbuild.build(config); } From d8220869b64c81df056f8bb617e272a5149c7880 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:13:14 +0000 Subject: [PATCH 27/71] order treatments by date --- .devcontainer/Dockerfile | 2 +- SynonymGroup.ts | 126 +++++++++++++++++++++++++++++++-------- example/index.ts | 59 +++++++++++------- 3 files changed, 139 insertions(+), 48 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0d78850..63940d2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,3 +1,3 @@ -FROM denoland/deno:bin-1.41.3 AS deno +FROM denoland/deno:bin-1.45.5 AS deno FROM mcr.microsoft.com/devcontainers/typescript-node:20 COPY --from=deno /deno /usr/local/bin/deno \ No newline at end of file diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 6296d54..99411ff 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -217,6 +217,7 @@ LIMIT 500`; // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` +PREFIX dc: PREFIX dwc: PREFIX dwcFP: PREFIX cito: @@ -260,17 +261,41 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority FILTER NOT EXISTS { ?tn dwc:species ?species . } } - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } OPTIONAL { ?tc trt:hasTaxonName ?tn ; dwc:scientificNameAuthorship ?tcauth ; a dwcFP:TaxonConcept . - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } } } } @@ -298,6 +323,7 @@ LIMIT 500`; // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` +PREFIX dc: PREFIX dwc: PREFIX dwcFP: PREFIX cito: @@ -350,13 +376,37 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) BIND(COALESCE(?colAuth, "") as ?authority) - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } } GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority LIMIT 500`; @@ -382,6 +432,7 @@ LIMIT 500`; // Note: this query assumes that there is no sub-species taxa with missing dwc:species // Note: the handling assumes that at most one taxon-name matches this colTaxon const query = ` +PREFIX dc: PREFIX dwc: PREFIX dwcFP: PREFIX cito: @@ -430,17 +481,41 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) BIND(COALESCE(?colAuth, "") as ?authority) - OPTIONAL { ?trtn trt:treatsTaxonName ?tn . } - OPTIONAL { ?citetn trt:citesTaxonName ?tn . } + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } OPTIONAL { ?tc trt:hasTaxonName ?tn ; dwc:scientificNameAuthorship ?tcauth ; a dwcFP:TaxonConcept . - OPTIONAL { ?aug trt:augmentsTaxonConcept ?tc . } - OPTIONAL { ?def trt:definesTaxonConcept ?tc . } - OPTIONAL { ?dpr trt:deprecates ?tc . } - OPTIONAL { ?cite cito:cites ?tc . } + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } } } GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority @@ -705,15 +780,20 @@ GROUP BY ?current ?current_status`; return result; } - /** @internal */ + /** @internal + * + * the supplied "urls" must be of the form "URL>DATE" + */ private makeTreatmentSet(urls?: string[]): Set { if (!urls) return new Set(); return new Set( - urls.filter((url) => !!url).map((url) => { + urls.filter((url) => !!url).map((url_d) => { + const [url, date] = url_d.split(">"); if (!this.treatments.has(url)) { const details = this.getTreatmentDetails(url); this.treatments.set(url, { url, + date: date ? parseInt(date, 10) : undefined, details, }); } @@ -762,7 +842,6 @@ SELECT DISTINCT WHERE { BIND (<${treatmentUri}> as ?treatment) ?treatment dc:creator ?creator . - OPTIONAL { ?treatment trt:publishedIn/dc:date ?date . } OPTIONAL { ?treatment dc:title ?title } OPTIONAL { ?treatment trt:augmentsTaxonConcept ?aug . } OPTIONAL { ?treatment trt:definesTaxonConcept ?def . } @@ -860,9 +939,6 @@ SELECT DISTINCT ?url ?description WHERE { ); return { creators: json.results.bindings[0]?.creators?.value, - date: json.results.bindings[0]?.date?.value - ? parseInt(json.results.bindings[0].date.value, 10) - : undefined, title: json.results.bindings[0]?.title?.value, materialCitations, figureCitations, @@ -1042,6 +1118,7 @@ export type AuthorizedName = { /** A plazi-treatment */ export type Treatment = { url: string; + date?: number; /** Details are behind a promise becuase they are loaded with a separate query. */ details: Promise; @@ -1051,7 +1128,6 @@ export type Treatment = { export type TreatmentDetails = { materialCitations: MaterialCitation[]; figureCitations: FigureCitation[]; - date?: number; creators?: string; title?: string; treats: { diff --git a/example/index.ts b/example/index.ts index 856f17e..e74f22d 100644 --- a/example/index.ts +++ b/example/index.ts @@ -9,6 +9,7 @@ import { const params = new URLSearchParams(document.location.search); const HIDE_COL_ONLY_SYNONYMS = !params.has("show_col"); const START_WITH_SUBTAXA = params.has("subtaxa"); +const SORT_TREATMENTS_BY_TYPE = params.has("sort_treatments_by_type"); const ENDPOINT_URL = params.get("server") || "https://treatment.ld.plazi.org/sparql"; const NAME = params.get("q") || @@ -52,13 +53,17 @@ class SynoTreatment extends HTMLElement { this.innerHTML = icons[status] ?? icons.unknown; + const date = document.createElement("span"); + if (trt.date) date.innerText = "" + trt.date; + else { + date.classList.add("missing"); + date.innerText = "No Date"; + } + this.append(date); + const creators = document.createElement("span"); creators.innerText = "…"; - this.append(creators); - - const date = document.createElement("span"); - date.innerText = "…"; - this.append(" ", date); + this.append(": ", creators); const title = document.createElement("i"); title.innerText = "…"; @@ -82,12 +87,6 @@ class SynoTreatment extends HTMLElement { creators.innerText = "No Authors"; } - if (details.date) date.innerText = "" + details.date; - else { - date.classList.add("missing"); - date.innerText = "No Date"; - } - if (details.title) title.innerText = "“" + details.title + "”"; else { title.classList.add("missing"); @@ -99,7 +98,8 @@ class SynoTreatment extends HTMLElement { status !== SynoStatus.Cite ) { const line = document.createElement("div"); - line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + line.innerHTML = icons.east; line.innerHTML += icons.def; names.append(line); @@ -116,7 +116,8 @@ class SynoTreatment extends HTMLElement { status !== SynoStatus.Cite ) { const line = document.createElement("div"); - line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; + line.innerHTML = icons.east; line.innerHTML += icons.aug; names.append(line); @@ -138,7 +139,8 @@ class SynoTreatment extends HTMLElement { status !== SynoStatus.Cite ) { const line = document.createElement("div"); - line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.west; + // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.west; + line.innerHTML = icons.west; line.innerHTML += icons.dpr; names.append(line); @@ -271,6 +273,7 @@ class SynoName extends HTMLElement { li.append(creators); const names = document.createElement("div"); + names.classList.add("indent"); li.append(names); if (authorizedName.acceptedColURI !== authorizedName.colURI) { @@ -291,20 +294,32 @@ class SynoName extends HTMLElement { } } + const treatments_array: { trt: Treatment; status: SynoStatus }[] = []; + for (const trt of authorizedName.treatments.def) { - const li = new SynoTreatment(trt, SynoStatus.Def); - treatments.append(li); + treatments_array.push({ trt, status: SynoStatus.Def }); } for (const trt of authorizedName.treatments.aug) { - const li = new SynoTreatment(trt, SynoStatus.Aug); - treatments.append(li); + treatments_array.push({ trt, status: SynoStatus.Aug }); } for (const trt of authorizedName.treatments.dpr) { - const li = new SynoTreatment(trt, SynoStatus.Dpr); - treatments.append(li); + treatments_array.push({ trt, status: SynoStatus.Dpr }); } for (const trt of authorizedName.treatments.cite) { - const li = new SynoTreatment(trt, SynoStatus.Cite); + treatments_array.push({ trt, status: SynoStatus.Cite }); + } + + if (!SORT_TREATMENTS_BY_TYPE) { + treatments_array.sort((a, b) => { + if (a.trt.date && b.trt.date) return a.trt.date - b.trt.date; + if (a.trt.date) return 1; + if (b.trt.date) return -1; + return 0; + }); + } + + for (const { trt, status } of treatments_array) { + const li = new SynoTreatment(trt, status); treatments.append(li); } } @@ -320,7 +335,7 @@ async function justify(name: Name): Promise { } else if (name.justification.treatment) { const details = await name.justification.treatment.details; const parent = await justify(name.justification.parent); - return `is, according to ${details.creators} ${details.date},\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + return `is, according to ${details.creators} ${name.justification.treatment.date},\n a synonym of ${name.justification.parent.displayName} which ${parent}`; // return `is, according to ${details.creators} ${details.date} “${details.title||"No Title"}” ${name.justification.treatment.url},\n a synonym of ${name.justification.parent.displayName} which ${parent}`; } else { const parent = await justify(name.justification.parent); From 04f0a1c6a8c29a4f33236e047e9611327b87288c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:54:03 +0000 Subject: [PATCH 28/71] expand treatment details on click --- example/index.css | 24 ++++++++++++++++++++++++ example/index.ts | 48 +++++++++++++++++++++++++++-------------------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/example/index.css b/example/index.css index 279381c..dc314a8 100644 --- a/example/index.css +++ b/example/index.css @@ -64,12 +64,36 @@ ul { syno-treatment, .treatmentline { display: block; + position: relative; &>svg { height: 1rem; vertical-align: sub; margin: 0 0.2rem 0 -1.2rem; } + + .details { + display: grid; + } + + .hidden { + height: 0; + overflow: hidden; + transition: height 1s; + } + + &.expanded { + /* position: absolute; */ + background: #f0f9fc; + z-index: 10; + border-radius: 0.2rem; + padding: 0.2rem; + margin: -0.2rem; + + .hidden { + height: 100%; + } + } } .indent { diff --git a/example/index.ts b/example/index.ts index e74f22d..4fac66e 100644 --- a/example/index.ts +++ b/example/index.ts @@ -22,6 +22,7 @@ enum SynoStatus { Aug = "aug", Dpr = "dpr", Cite = "cite", + Full = "full", } const icons = { @@ -51,7 +52,16 @@ class SynoTreatment extends HTMLElement { constructor(trt: Treatment, status: SynoStatus) { super(); - this.innerHTML = icons[status] ?? icons.unknown; + if (status === SynoStatus.Full) this.classList.add("expanded"); + else { + this.innerHTML = icons[status] ?? icons.unknown; + this.addEventListener("click", () => { + // const expanded = new SynoTreatment(trt, SynoStatus.Full); + // this.prepend(expanded); + // expanded.addEventListener("click", () => expanded.remove()); + this.classList.toggle("expanded"); + }); + } const date = document.createElement("span"); if (trt.date) date.innerText = "" + trt.date; @@ -77,7 +87,7 @@ class SynoTreatment extends HTMLElement { this.append(" ", url); const names = document.createElement("div"); - names.classList.add("indent"); + names.classList.add("indent", "details"); this.append(names); trt.details.then((details) => { @@ -93,14 +103,14 @@ class SynoTreatment extends HTMLElement { title.innerText = "No Title"; } - if ( - status !== SynoStatus.Def && details.treats.def.size > 0 && - status !== SynoStatus.Cite - ) { + if (details.treats.def.size > 0) { const line = document.createElement("div"); // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; line.innerHTML = icons.east; line.innerHTML += icons.def; + if (status === SynoStatus.Def || status === SynoStatus.Cite) { + line.classList.add("hidden"); + } names.append(line); details.treats.def.forEach((n) => { @@ -110,15 +120,14 @@ class SynoTreatment extends HTMLElement { line.append(url); }); } - if ( - status !== SynoStatus.Aug && - (details.treats.aug.size > 0 || details.treats.treattn.size > 0) && - status !== SynoStatus.Cite - ) { + if (details.treats.aug.size > 0 || details.treats.treattn.size > 0) { const line = document.createElement("div"); // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.east; line.innerHTML = icons.east; line.innerHTML += icons.aug; + if (status === SynoStatus.Aug || status === SynoStatus.Cite) { + line.classList.add("hidden"); + } names.append(line); details.treats.aug.forEach((n) => { @@ -134,14 +143,14 @@ class SynoTreatment extends HTMLElement { line.append(url); }); } - if ( - status !== SynoStatus.Dpr && details.treats.dpr.size > 0 && - status !== SynoStatus.Cite - ) { + if (details.treats.dpr.size > 0) { const line = document.createElement("div"); // line.innerHTML = status === SynoStatus.Cite ? icons.line : icons.west; line.innerHTML = icons.west; line.innerHTML += icons.dpr; + if (status === SynoStatus.Dpr || status === SynoStatus.Cite) { + line.classList.add("hidden"); + } names.append(line); details.treats.dpr.forEach((n) => { @@ -151,13 +160,12 @@ class SynoTreatment extends HTMLElement { line.append(url); }); } - if ( - status !== SynoStatus.Dpr && - (details.treats.citetc.size > 0 || details.treats.citetn.size > 0) && - status !== SynoStatus.Cite - ) { + if (details.treats.citetc.size > 0 || details.treats.citetn.size > 0) { const line = document.createElement("div"); line.innerHTML = icons.empty + icons.cite; + // if (status === SynoStatus.Dpr || status === SynoStatus.Cite) { + line.classList.add("hidden"); + // } names.append(line); details.treats.citetc.forEach((n) => { From 38700eddca57cc52a1a7ad0e22423a6329917db4 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:22:49 +0100 Subject: [PATCH 29/71] move to build example with deno --- build.mjs => build_example.ts | 15 +- deno.jsonc | 18 ++ deno.lock | 114 +++++++++ index.html | 83 ------- package-lock.json | 453 ---------------------------------- package.json | 26 -- tsconfig.json | 23 -- 7 files changed, 139 insertions(+), 593 deletions(-) rename build.mjs => build_example.ts (55%) create mode 100644 deno.jsonc create mode 100644 deno.lock delete mode 100644 index.html delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 tsconfig.json diff --git a/build.mjs b/build_example.ts similarity index 55% rename from build.mjs rename to build_example.ts index f6ebd99..7e7a72f 100644 --- a/build.mjs +++ b/build_example.ts @@ -1,12 +1,11 @@ import * as esbuild from "esbuild"; -const SERVE = process.argv.includes("serve"); -const BUILD = process.argv.includes("build"); -const EXAMPLE = process.argv.includes("example"); +const SERVE = Deno.args.includes("serve"); +const BUILD = Deno.args.includes("build"); -const config = { - entryPoints: EXAMPLE ? ["./example/index.ts"] : ["./mod.ts"], - outfile: EXAMPLE ? "./example/index.js" : "./build/mod.js", +const config: esbuild.BuildOptions = { + entryPoints: ["./example/index.ts"], + outfile: "./example/index.js", sourcemap: true, bundle: true, format: "esm", @@ -21,11 +20,11 @@ const config = { }; if (SERVE) { - let ctx = await esbuild.context(config); + const ctx = await esbuild.context(config); await ctx.watch(); const { host, port } = await ctx.serve({ - servedir: EXAMPLE ? "./example" : "./build", + servedir: "./example", }); console.log(`Listening at ${host}:${port}`); diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..a413d9a --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": [ + "dom", + "dom.iterable", + "dom.asynciterable", + "deno.ns" + ] + }, + "imports": { + "esbuild": "npm:esbuild@^0.24.0" + }, + "tasks": { + "example_serve": "deno run --allow-read --allow-env --allow-run build_example.ts serve", + "example_build": "deno run --allow-read --allow-env --allow-run build_example.ts build" + } + } \ No newline at end of file diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..2883292 --- /dev/null +++ b/deno.lock @@ -0,0 +1,114 @@ +{ + "version": "4", + "specifiers": { + "npm:esbuild@0.24": "0.24.0" + }, + "npm": { + "@esbuild/aix-ppc64@0.24.0": { + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==" + }, + "@esbuild/android-arm64@0.24.0": { + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==" + }, + "@esbuild/android-arm@0.24.0": { + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==" + }, + "@esbuild/android-x64@0.24.0": { + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==" + }, + "@esbuild/darwin-arm64@0.24.0": { + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==" + }, + "@esbuild/darwin-x64@0.24.0": { + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==" + }, + "@esbuild/freebsd-arm64@0.24.0": { + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==" + }, + "@esbuild/freebsd-x64@0.24.0": { + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==" + }, + "@esbuild/linux-arm64@0.24.0": { + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==" + }, + "@esbuild/linux-arm@0.24.0": { + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==" + }, + "@esbuild/linux-ia32@0.24.0": { + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==" + }, + "@esbuild/linux-loong64@0.24.0": { + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==" + }, + "@esbuild/linux-mips64el@0.24.0": { + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==" + }, + "@esbuild/linux-ppc64@0.24.0": { + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==" + }, + "@esbuild/linux-riscv64@0.24.0": { + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==" + }, + "@esbuild/linux-s390x@0.24.0": { + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==" + }, + "@esbuild/linux-x64@0.24.0": { + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==" + }, + "@esbuild/netbsd-x64@0.24.0": { + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==" + }, + "@esbuild/openbsd-arm64@0.24.0": { + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==" + }, + "@esbuild/openbsd-x64@0.24.0": { + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==" + }, + "@esbuild/sunos-x64@0.24.0": { + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==" + }, + "@esbuild/win32-arm64@0.24.0": { + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==" + }, + "@esbuild/win32-ia32@0.24.0": { + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==" + }, + "@esbuild/win32-x64@0.24.0": { + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==" + }, + "esbuild@0.24.0": { + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + } + }, + "workspace": { + "dependencies": [ + "npm:esbuild@0.24" + ] + } +} diff --git a/index.html b/index.html deleted file mode 100644 index 5445f17..0000000 --- a/index.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - BROKEN // TODO // - -

You should see a list of synonyms here

- - - - - - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 215d7ba..0000000 --- a/package-lock.json +++ /dev/null @@ -1,453 +0,0 @@ -{ - "name": "@factsmission/synogroup", - "version": "2.2.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@factsmission/synogroup", - "version": "2.2.0", - "license": "MIT", - "devDependencies": { - "esbuild": "0.24.0", - "typescript": "5.6.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index b05bd3e..0000000 --- a/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@factsmission/synogroup", - "version": "2.2.0", - "description": "", - "main": "build/mod.mjs", - "scripts": { - "build": "node build.mjs build", - "build___unused": "node build.mjs build && rm -rf ./types; tsc", - "example": "node build.mjs example", - "publish-package": "echo 'see readme'" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/factsmission/synogroup.git" - }, - "author": "", - "license": "MIT", - "bugs": { - "url": "https://github.com/factsmission/synogroup/issues" - }, - "homepage": "https://github.com/factsmission/synogroup#readme", - "devDependencies": { - "esbuild": "0.24.0", - "typescript": "5.6.3" - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 9a2b07d..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "include": [ - "../mod.ts" - ], - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "removeComments": false, - "declarationDir": "./types", - "outDir": ".", - "lib": [ - "esnext", - "dom", - "dom.iterable", - "scripthost" - ], - "isolatedModules": true, - "allowImportingTsExtensions": true, - "esModuleInterop": true, - "target": "esnext", - "module": "esnext" - } -} From d9edaa02440a8df224590b3be0dc85732b8c91ef Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:23:17 +0100 Subject: [PATCH 30/71] deno fmt --- .devcontainer/devcontainer.json | 43 +++++----- .github/workflows/pages.yml | 14 +-- .vscode/launch.json | 1 - DESIGN.md | 2 +- README.md | 15 ++-- deno.jsonc | 34 ++++---- example/index.css | 145 ++++++++++++++++---------------- example/index.html | 18 ++-- 8 files changed, 136 insertions(+), 136 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1be6322..8895aa6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,27 +1,26 @@ { - "name": "Deno", - "build": { - "dockerfile": "Dockerfile" - }, + "name": "Deno", + "build": { + "dockerfile": "Dockerfile" + }, - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "justjavac.vscode-deno-extensionpack" - ] - } - } - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} \ No newline at end of file + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "justjavac.vscode-deno-extensionpack" + ] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index fd228da..8ebddbd 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,8 +3,8 @@ name: GitHub Pages on: push: branches: - - main # Set a branch to deploy - - synolib2 # publish on change in either branch + - main # Set a branch to deploy + - synolib2 # publish on change in either branch permissions: contents: write @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-22.04 steps: - + # main / old: - - name: 'Checkout main branch' + - name: "Checkout main branch" uses: actions/checkout@v3 with: ref: main @@ -32,15 +32,15 @@ jobs: - run: cp SynonymGroup.ts build/ # synolib2 / new: - - name: 'Checkout synolib2 branch' + - name: "Checkout synolib2 branch" uses: actions/checkout@v3 with: ref: synolib2 - path: 'synolib2' + path: "synolib2" - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: "20.x" - name: Draw the rest of the owl run: | cd synolib2 diff --git a/.vscode/launch.json b/.vscode/launch.json index 675ce19..c18226a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -46,6 +46,5 @@ ], "attachSimplePort": 9229 } - ] } diff --git a/DESIGN.md b/DESIGN.md index 4cd6135..fb7993f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -54,4 +54,4 @@ it added a `N` to the results.\ This "justification" is also proved as metadata of a `N`. [^1]: I.e. ignoring differences in punctuation, diacritics, capitalization and -such. + such. diff --git a/README.md b/README.md index ba811cc..cc040bd 100644 --- a/README.md +++ b/README.md @@ -25,32 +25,37 @@ deno run --allow-net ./example/cli.ts https://www.catalogueoflife.org/data/taxon ### Web -An example running in the browser is located in `example/index.html` and `example/index.ts`. +An example running in the browser is located in `example/index.html` and +`example/index.ts`. To build the example, use + ```sh npm run example ``` + or for a live-reloading server use + ```sh npm run example serve ``` (Both require `npm install` first) - The example page uses query parameters for options: -- `q=TAXON` for the search term (Latin name, CoL-URI, taxon-name-URI or taxon-concept-URI) + +- `q=TAXON` for the search term (Latin name, CoL-URI, taxon-name-URI or + taxon-concept-URI) - `show_col=` to include many more CoL taxa - `subtaxa=` to include subtaxa of the search term - `server=URL` to configure the sparql endpoint e.g. http://localhost:8000/?q=Sadayoshia%20miyakei&show_col= - ## Building for npm/web To build the library for use in web projects, use + ```sh npm run build ``` @@ -59,4 +64,4 @@ This will place the built library in `./build/mod.js`. Note that this does not (yet) generate `.d.ts` typings. -(Requires `npm install` first) \ No newline at end of file +(Requires `npm install` first) diff --git a/deno.jsonc b/deno.jsonc index a413d9a..0941f01 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,18 +1,18 @@ { - "compilerOptions": { - "target": "esnext", - "lib": [ - "dom", - "dom.iterable", - "dom.asynciterable", - "deno.ns" - ] - }, - "imports": { - "esbuild": "npm:esbuild@^0.24.0" - }, - "tasks": { - "example_serve": "deno run --allow-read --allow-env --allow-run build_example.ts serve", - "example_build": "deno run --allow-read --allow-env --allow-run build_example.ts build" - } - } \ No newline at end of file + "compilerOptions": { + "target": "esnext", + "lib": [ + "dom", + "dom.iterable", + "dom.asynciterable", + "deno.ns" + ] + }, + "imports": { + "esbuild": "npm:esbuild@^0.24.0" + }, + "tasks": { + "example_serve": "deno run --allow-read --allow-env --allow-run build_example.ts serve", + "example_build": "deno run --allow-read --allow-env --allow-run build_example.ts build" + } +} diff --git a/example/index.css b/example/index.css index dc314a8..add898e 100644 --- a/example/index.css +++ b/example/index.css @@ -1,117 +1,116 @@ * { - box-sizing: border-box; + box-sizing: border-box; } #root:empty::before { - content: "You should see a list of synonyms here"; + content: "You should see a list of synonyms here"; } svg { - height: 1rem; - vertical-align: sub; - margin: 0; + height: 1rem; + vertical-align: sub; + margin: 0; } syno-name { - display: block; - margin: 1rem 0; + display: block; + margin: 1rem 0; } .uri:not(:empty) { - font-size: 0.8rem; - line-height: 1rem; - padding: 0 0.2rem; - margin: 0.2rem; - border-radius: 0.2rem; - background: #ededed; - font-family: monospace; - font-weight: normal; - - &.taxon { - color: #4e69ae; - } - - &.treatment { - color: #8894af; - } - - &.col { - color: #177669; - } - - svg { - height: 0.8em; - vertical-align: baseline; - margin: 0 -0.1em 0 0.2em; - } + font-size: 0.8rem; + line-height: 1rem; + padding: 0 0.2rem; + margin: 0.2rem; + border-radius: 0.2rem; + background: #ededed; + font-family: monospace; + font-weight: normal; + + &.taxon { + color: #4e69ae; + } + + &.treatment { + color: #8894af; + } + + &.col { + color: #177669; + } + + svg { + height: 0.8em; + vertical-align: baseline; + margin: 0 -0.1em 0 0.2em; + } } .justification { - font-size: 0.8rem; - padding: 0.2rem; - margin: 0.2rem; - border-radius: 0.2rem; - background: #ededed; + font-size: 0.8rem; + padding: 0.2rem; + margin: 0.2rem; + border-radius: 0.2rem; + background: #ededed; } h2, h3, ul { - margin: 0; + margin: 0; } - syno-treatment, .treatmentline { - display: block; - position: relative; - - &>svg { - height: 1rem; - vertical-align: sub; - margin: 0 0.2rem 0 -1.2rem; - } + display: block; + position: relative; - .details { - display: grid; - } + & > svg { + height: 1rem; + vertical-align: sub; + margin: 0 0.2rem 0 -1.2rem; + } + + .details { + display: grid; + } + + .hidden { + height: 0; + overflow: hidden; + transition: height 1s; + } + + &.expanded { + /* position: absolute; */ + background: #f0f9fc; + z-index: 10; + border-radius: 0.2rem; + padding: 0.2rem; + margin: -0.2rem; .hidden { - height: 0; - overflow: hidden; - transition: height 1s; - } - - &.expanded { - /* position: absolute; */ - background: #f0f9fc; - z-index: 10; - border-radius: 0.2rem; - padding: 0.2rem; - margin: -0.2rem; - - .hidden { - height: 100%; - } + height: 100%; } + } } .indent { - margin-left: 1.4rem; + margin-left: 1.4rem; } .blue { - color: #1e88e5; + color: #1e88e5; } .green { - color: #388e3c; + color: #388e3c; } .red { - color: #e53935; + color: #e53935; } .gray { - color: #666666; -} \ No newline at end of file + color: #666666; +} diff --git a/example/index.html b/example/index.html index d6b7570..2a454c9 100644 --- a/example/index.html +++ b/example/index.html @@ -1,18 +1,16 @@ - - - + + SynoLib - - - + + + - +

SynoLib

- - - \ No newline at end of file + + From 5b282611c6c9a96e93bbb03fea15e86686ccd791 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:27:20 +0100 Subject: [PATCH 31/71] fix build and lint --- .github/workflows/pages.yml | 11 +++-------- README.md | 19 +++++-------------- SynonymGroup.ts | 4 ++-- deno.jsonc | 8 +++++++- example/cli.ts | 11 ++++++++--- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8ebddbd..3ebd50e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,9 +21,9 @@ jobs: with: ref: main - name: Setup Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: - deno-version: v1.x + deno-version: v2.x - name: prepare run: | mkdir build @@ -37,15 +37,10 @@ jobs: with: ref: synolib2 path: "synolib2" - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: "20.x" - name: Draw the rest of the owl run: | cd synolib2 - npm ci - npm run example build + deno task example_build mkdir ../build/next cp ./example/index.* ../build/next/ cd .. diff --git a/README.md b/README.md index cc040bd..3ed5a80 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,15 @@ An example running in the browser is located in `example/index.html` and To build the example, use ```sh -npm run example +deno task example_build ``` or for a live-reloading server use ```sh -npm run example serve +deno task example_serve ``` -(Both require `npm install` first) - The example page uses query parameters for options: - `q=TAXON` for the search term (Latin name, CoL-URI, taxon-name-URI or @@ -54,14 +52,7 @@ e.g. http://localhost:8000/?q=Sadayoshia%20miyakei&show_col= ## Building for npm/web -To build the library for use in web projects, use - -```sh -npm run build -``` - -This will place the built library in `./build/mod.js`. - -Note that this does not (yet) generate `.d.ts` typings. +The library is to be published as-is (in typescript) to jsr.io. -(Requires `npm install` first) +It can be used from there in with other deno or node/npm projects. There is no +building step neccesary on our side. diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 99411ff..4c1cc0e 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,4 +1,4 @@ -import { SparqlEndpoint, SparqlJson } from "./mod.ts"; +import type { SparqlEndpoint, SparqlJson } from "./mod.ts"; /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -60,7 +60,7 @@ export class SynonymGroup implements AsyncIterable { * * @readonly */ - treatments = new Map(); + treatments: Map = new Map(); /** * Whether to show taxa deprecated by CoL that would not have been found otherwise. diff --git a/deno.jsonc b/deno.jsonc index 0941f01..1013d43 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,12 @@ { + "name": "@plazi/synolib", + "version": "3.0.0", + "exports": "./mod.ts", + "publish": { + "include": ["./*.ts", "README.md", "LICENSE"], + "exclude": ["./build_example.ts"] + }, "compilerOptions": { - "target": "esnext", "lib": [ "dom", "dom.iterable", diff --git a/example/cli.ts b/example/cli.ts index e211e34..666f4c0 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,5 +1,10 @@ import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; -import { Name, SparqlEndpoint, SynonymGroup, Treatment } from "../mod.ts"; +import { + type Name, + SparqlEndpoint, + SynonymGroup, + type Treatment, +} from "../mod.ts"; const HIDE_COL_ONLY_SYNONYMS = true; const START_WITH_SUBTAXA = false; @@ -115,7 +120,7 @@ async function logTreatment( ) { const details = await trt.details; console.log( - ` ${trtColor[type]("●")} ${details.creators} ${details.date} “${ + ` ${trtColor[type]("●")} ${details.creators} ${trt.date} “${ Colors.italic(details.title || Colors.dim("No Title")) }” ${Colors.magenta(trt.url)}`, ); @@ -167,7 +172,7 @@ async function justify(name: Name): Promise { const parent = await justify(name.justification.parent); return `is, according to ${ Colors.italic( - `${details.creators} ${details.date} “${ + `${details.creators} ${name.justification.treatment.date} “${ Colors.italic(details.title || Colors.dim("No Title")) }” ${Colors.magenta(name.justification.treatment.url)}`, ) From 36d464d51190910268cbbccc559329e7de70edb5 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:55:20 +0100 Subject: [PATCH 32/71] fixed handling of multiple col taxa for one name --- SynonymGroup.ts | 84 +++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 4c1cc0e..8ed75d2 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -549,27 +549,9 @@ LIMIT 500`; "", ).trim(); - const colName: AuthorizedName | undefined = - json.results.bindings[0].col?.value - ? { - displayName, - authority: json.results.bindings[0].authority!.value, - colURI: json.results.bindings[0].col.value, - treatments: { - def: new Set(), - aug: new Set(), - dpr: new Set(), - cite: new Set(), - }, - } - : undefined; - - if (colName) { - if (this.expanded.has(colName.colURI!)) return; - this.expanded.add(colName.colURI!); - } - - const authorizedNames = colName ? [colName] : []; + // there can be multiple CoL-taxa with same latin name, e.g. Leontopodium alpinum has 3T6ZY and 3T6ZX. + const authorizedCoLNames: AuthorizedName[] = []; + const authorizedTCNames: AuthorizedName[] = []; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { @@ -578,18 +560,39 @@ LIMIT 500`; } for (const t of json.results.bindings) { - if (t.tc && t.tcAuth?.value) { - if (this.expanded.has(t.tc.value)) { - // console.log("Abbruch: already known", t.tc.value); - return; + if (t.col) { + const colURI = t.col.value; + console.log(colURI); + if (!authorizedCoLNames.find((e) => e.colURI === colURI)) { + if (this.expanded.has(colURI)) { + console.log("Abbruch: already known", colURI); + return; + } + this.expanded.add(colURI); + authorizedCoLNames.push({ + displayName, + authority: t.authority!.value, + colURI: t.col.value, + treatments: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + cite: new Set(), + }, + }); } + } + + if (t.tc && t.tcAuth && t.tcAuth.value) { const def = this.makeTreatmentSet(t.defs?.value.split("|")); const aug = this.makeTreatmentSet(t.augs?.value.split("|")); const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); const cite = this.makeTreatmentSet(t.cites?.value.split("|")); - if ( - colName && t.tcAuth?.value.split(" / ").includes(colName.authority) - ) { + + const colName = authorizedCoLNames.find((e) => + t.tcAuth!.value.split(" / ").includes(e.authority) + ); + if (colName) { colName.authority = t.tcAuth?.value; colName.taxonConceptURI = t.tc.value; colName.treatments = { @@ -598,8 +601,11 @@ LIMIT 500`; dpr, cite, }; + } else if (this.expanded.has(t.tc.value)) { + console.log("Abbruch: already known", t.tc.value); + return; } else { - authorizedNames.push({ + authorizedTCNames.push({ displayName, authority: t.tcAuth.value, taxonConceptURI: t.tc.value, @@ -630,7 +636,7 @@ LIMIT 500`; const name: Name = { displayName, taxonNameURI, - authorizedNames, + authorizedNames: [...authorizedCoLNames, ...authorizedTCNames], justification, treatments: { treats, @@ -643,14 +649,18 @@ LIMIT 500`; : Promise.resolve(new Map()), }; - let colPromises: Promise[] = []; + const colPromises: Promise[] = []; - if (colName) { - [colName.acceptedColURI, colPromises] = await this.getAcceptedCol( - colName.colURI!, - name, - ); - } + await Promise.all( + authorizedCoLNames.map(async (n) => { + const [acceptedColURI, promises] = await this.getAcceptedCol( + n.colURI!, + name, + ); + n.acceptedColURI = acceptedColURI; + colPromises.push(...promises); + }), + ); this.pushName(name); From c80dc9449361b5e7beb3ea10d10d2c8f87f24450 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:31:02 +0100 Subject: [PATCH 33/71] improved handling of other infraspecific names --- SynonymGroup.ts | 38 +++++++++++++++++++++----------------- build_example.ts | 2 ++ deno.jsonc | 1 + deno.lock | 24 ++++++++++++++++++++++++ example/index.css | 17 ++++++++++++++++- example/index.ts | 15 +++++++++++---- 6 files changed, 75 insertions(+), 22 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 8ed75d2..db08cb9 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -189,8 +189,8 @@ SELECT DISTINCT ?uri WHERE { } ${ infrasp - ? `?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet "${infrasp}" .` - : "FILTER NOT EXISTS { ?uri dwc:subspecies|dwc:variety|dwc:infraspecificEpithet ?infrasp . }" + ? `?uri dwc:subSpecies|dwc:variety|dwc:form|dwc:infraspecificEpithet "${infrasp}" .` + : "FILTER NOT EXISTS { ?uri dwc:subSpecies|dwc:variety|dwc:form|dwc:infraspecificEpithet ?infrasp . }" } } LIMIT 500`; @@ -242,8 +242,8 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority } OPTIONAL { - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . + ?tn dwc:rank ?trank . + FILTER(LCASE(?rank) = LCASE(?trank)) ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . { @@ -251,10 +251,10 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn dwc:species ?species . { ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } UNION { FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } } } UNION { FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } @@ -347,11 +347,12 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } } OPTIONAL { - ?col dwc:taxonRank ?rank . + ?col dwc:taxonRank ?crank . + FILTER(LCASE(?rank) = LCASE(?crank)) OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . @@ -362,10 +363,10 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn dwc:species ?species . { ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } UNION { FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } } } UNION { FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } @@ -452,11 +453,12 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn dwc:kingdom ?kingdom . OPTIONAL { ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subspecies|dwc:variety ?infrasp . } + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } } OPTIONAL { - ?col dwc:taxonRank ?rank . + ?col dwc:taxonRank ?crank . + FILTER(LCASE(?rank) = LCASE(?crank)) OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } ?col dwc:scientificName ?fullName . # Note: contains authority ?col dwc:genericName ?genus . @@ -467,10 +469,10 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority ?tn dwc:species ?species . { ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subspecies|dwc:variety ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } UNION { FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subspecies|dwc:variety ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } } } UNION { FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } @@ -562,10 +564,9 @@ LIMIT 500`; for (const t of json.results.bindings) { if (t.col) { const colURI = t.col.value; - console.log(colURI); if (!authorizedCoLNames.find((e) => e.colURI === colURI)) { if (this.expanded.has(colURI)) { - console.log("Abbruch: already known", colURI); + console.log("Skipping known", colURI); return; } this.expanded.add(colURI); @@ -602,7 +603,7 @@ LIMIT 500`; cite, }; } else if (this.expanded.has(t.tc.value)) { - console.log("Abbruch: already known", t.tc.value); + console.log("Skipping known", t.tc.value); return; } else { authorizedTCNames.push({ @@ -635,6 +636,7 @@ LIMIT 500`; const name: Name = { displayName, + rank: json.results.bindings[0].rank!.value, taxonNameURI, authorizedNames: [...authorizedCoLNames, ...authorizedTCNames], justification, @@ -1044,6 +1046,8 @@ export type Name = { // kingdom: string; /** Human-readable name */ displayName: string; + /** taxonomic rank */ + rank: string; /** vernacular names */ vernacularNames: Promise; diff --git a/build_example.ts b/build_example.ts index 7e7a72f..e9b618a 100644 --- a/build_example.ts +++ b/build_example.ts @@ -1,4 +1,5 @@ import * as esbuild from "esbuild"; +import { denoPlugins } from "@luca/esbuild-deno-loader"; const SERVE = Deno.args.includes("serve"); const BUILD = Deno.args.includes("build"); @@ -9,6 +10,7 @@ const config: esbuild.BuildOptions = { sourcemap: true, bundle: true, format: "esm", + plugins: [...denoPlugins()], lineLimit: 120, minify: BUILD ? true : false, banner: SERVE diff --git a/deno.jsonc b/deno.jsonc index 1013d43..773338b 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -15,6 +15,7 @@ ] }, "imports": { + "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0", "esbuild": "npm:esbuild@^0.24.0" }, "tasks": { diff --git a/deno.lock b/deno.lock index 2883292..90070a3 100644 --- a/deno.lock +++ b/deno.lock @@ -1,8 +1,31 @@ { "version": "4", "specifiers": { + "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0", + "jsr:@std/bytes@^1.0.2": "1.0.2", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/path@^1.0.6": "1.0.7", "npm:esbuild@0.24": "0.24.0" }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.0": { + "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path" + ] + }, + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/path@1.0.7": { + "integrity": "76a689e07f0e15dcc6002ec39d0866797e7156629212b28f27179b8a5c3b33a1" + } + }, "npm": { "@esbuild/aix-ppc64@0.24.0": { "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==" @@ -108,6 +131,7 @@ }, "workspace": { "dependencies": [ + "jsr:@luca/esbuild-deno-loader@0.11", "npm:esbuild@0.24" ] } diff --git a/example/index.css b/example/index.css index add898e..b61bbaa 100644 --- a/example/index.css +++ b/example/index.css @@ -2,6 +2,10 @@ box-sizing: border-box; } +:root { + font-family: Inter, Arial, Helvetica, sans-serif; +} + #root:empty::before { content: "You should see a list of synonyms here"; } @@ -17,11 +21,22 @@ syno-name { margin: 1rem 0; } +.rank { + font-size: 0.8rem; + line-height: 1rem; + padding: 0 0.2rem; + margin: 0.2rem 0; + border-radius: 0.2rem; + background: #ededed; + font-weight: normal; + color: #686868; +} + .uri:not(:empty) { font-size: 0.8rem; line-height: 1rem; padding: 0 0.2rem; - margin: 0.2rem; + margin: 0.2rem 0; border-radius: 0.2rem; background: #ededed; font-family: monospace; diff --git a/example/index.ts b/example/index.ts index 4fac66e..aece473 100644 --- a/example/index.ts +++ b/example/index.ts @@ -5,6 +5,7 @@ import { SynonymGroup, type Treatment, } from "../mod.ts"; +import { distinct } from "jsr:@std/collections/distinct"; const params = new URLSearchParams(document.location.search); const HIDE_COL_ONLY_SYNONYMS = !params.has("show_col"); @@ -196,6 +197,11 @@ class SynoName extends HTMLElement { title.append(name_title); this.append(title); + const rank_badge = document.createElement("span"); + rank_badge.classList.add("rank"); + rank_badge.innerText = name.rank; + title.append(" ", rank_badge); + if (name.taxonNameURI) { const name_uri = document.createElement("code"); name_uri.classList.add("taxon", "uri"); @@ -204,20 +210,21 @@ class SynoName extends HTMLElement { "", ); name_uri.title = name.taxonNameURI; - title.append(name_uri); + title.append(" ", name_uri); } const justification = document.createElement("abbr"); justification.classList.add("justification"); justification.innerText = "...?"; justify(name).then((just) => justification.title = `This ${just}`); - title.append(justification); + title.append(" ", justification); - const vernacular = document.createElement("code"); + const vernacular = document.createElement("div"); vernacular.classList.add("vernacular"); name.vernacularNames.then((names) => { if (names.size > 0) { - vernacular.innerText = "“" + [...names.values()].join("”, “") + "”"; + vernacular.innerText = "“" + + distinct([...names.values()].flat()).join("”, “") + "”"; } }); this.append(vernacular); From d9c1d17ea0a513db3ef1c5ed76b731997120186a Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:39:26 +0100 Subject: [PATCH 34/71] fixed duplicate check --- SynonymGroup.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index db08cb9..967c821 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -569,7 +569,6 @@ LIMIT 500`; console.log("Skipping known", colURI); return; } - this.expanded.add(colURI); authorizedCoLNames.push({ displayName, authority: t.authority!.value, @@ -618,8 +617,6 @@ LIMIT 500`; }, }); } - // this.expanded.set(t.tc.value, NameStatus.madeName); - this.expanded.add(t.tc.value); def.forEach((t) => treatmentPromises.push(t)); aug.forEach((t) => treatmentPromises.push(t)); @@ -627,8 +624,6 @@ LIMIT 500`; } } - // TODO: handle col-data "acceptedName" and stuff - const treats = this.makeTreatmentSet( json.results.bindings[0].tntreats?.value.split("|"), ); @@ -651,6 +646,11 @@ LIMIT 500`; : Promise.resolve(new Map()), }; + for (const authName of name.authorizedNames) { + if (authName.colURI) this.expanded.add(authName.colURI); + if (authName.taxonConceptURI) this.expanded.add(authName.taxonConceptURI); + } + const colPromises: Promise[] = []; await Promise.all( From c1ff9ef32f13d6cbccbe39f7c709a005f507abf7 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:39:59 +0100 Subject: [PATCH 35/71] various small improvements i think after this the example is done and work should begin on synospecies proper --- SynonymGroup.ts | 13 ++++--- example/index.css | 20 ++++------ example/index.html | 25 +++++++++++++ example/index.ts | 91 ++++++++++++++++++++++++++++++---------------- 4 files changed, 101 insertions(+), 48 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 967c821..92c254c 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -8,7 +8,7 @@ export class SynonymGroup implements AsyncIterable { */ isFinished = false; /** Used internally to watch for new names found */ - private monitor = new EventTarget(); + private monitor: EventTarget = new EventTarget(); /** Used internally to abort in-flight network requests when SynonymGroup is aborted */ private controller = new AbortController(); @@ -94,9 +94,12 @@ export class SynonymGroup implements AsyncIterable { this.startWithSubTaxa = startWithSubTaxa; if (taxonName.startsWith("http")) { - this.getName(taxonName, { searchTerm: true, subTaxon: false }).finally( - () => this.finish(), - ); + this.getName(taxonName, { searchTerm: true, subTaxon: false }) + .catch((e) => { + console.log("SynoGroup Failure: ", e); + this.controller.abort("SynoGroup Failed"); + }) + .finally(() => this.finish()); } else { const name = [ ...taxonName.split(" ").filter((n) => !!n), @@ -602,7 +605,7 @@ LIMIT 500`; cite, }; } else if (this.expanded.has(t.tc.value)) { - console.log("Skipping known", t.tc.value); + // console.log("Skipping known", t.tc.value); return; } else { authorizedTCNames.push({ diff --git a/example/index.css b/example/index.css index b61bbaa..a7066fc 100644 --- a/example/index.css +++ b/example/index.css @@ -21,7 +21,8 @@ syno-name { margin: 1rem 0; } -.rank { +.rank, +.justification { font-size: 0.8rem; line-height: 1rem; padding: 0 0.2rem; @@ -61,14 +62,6 @@ syno-name { } } -.justification { - font-size: 0.8rem; - padding: 0.2rem; - margin: 0.2rem; - border-radius: 0.2rem; - background: #ededed; -} - h2, h3, ul { @@ -87,13 +80,14 @@ syno-treatment, } .details { - display: grid; + /* display: grid; */ } .hidden { height: 0; + max-height: 0; overflow: hidden; - transition: height 1s; + transition: all 200ms; } &.expanded { @@ -105,7 +99,9 @@ syno-treatment, margin: -0.2rem; .hidden { - height: 100%; + height: auto; + max-height: 100px; + overflow: auto; } } } diff --git a/example/index.html b/example/index.html index 2a454c9..e717096 100644 --- a/example/index.html +++ b/example/index.html @@ -9,6 +9,31 @@

SynoLib

+

+ Use url parameters to configure: (e.g. /?q=Sadayoshia&subtaxa=&show_col) +

    +
  • + q=TAXON for the search term (Latin name, CoL-URI, + taxon-name-URI or taxon-concept-URI) +
  • +
  • + show_col= to include many more CoL taxa +
  • +
  • + subtaxa= to include subtaxa of the search term +
  • +
  • + server=URL to configure the sparql endpoint +
  • +
+
+ Click on a treatment to show all cited taxa. Click on a taxon identifier + beneath a treatment to scroll to that name — if it is (already) in the + list. +

+
diff --git a/example/index.ts b/example/index.ts index aece473..72b7cb8 100644 --- a/example/index.ts +++ b/example/index.ts @@ -72,17 +72,13 @@ class SynoTreatment extends HTMLElement { } this.append(date); - const creators = document.createElement("span"); - creators.innerText = "…"; - this.append(": ", creators); - - const title = document.createElement("i"); - title.innerText = "…"; - this.append(" ", title); + const spinner = document.createElement("progress"); + this.append(": ", spinner); const url = document.createElement("a"); url.classList.add("treatment", "uri"); url.href = trt.url; + url.target = "_blank"; url.innerText = trt.url.replace("http://treatment.plazi.org/id/", ""); url.innerHTML += icons.link; this.append(" ", url); @@ -92,6 +88,10 @@ class SynoTreatment extends HTMLElement { this.append(names); trt.details.then((details) => { + const creators = document.createElement("span"); + const title = document.createElement("i"); + spinner.replaceWith(creators, " ", title); + if (details.creators) creators.innerText = details.creators; else { creators.classList.add("missing"); @@ -115,9 +115,12 @@ class SynoTreatment extends HTMLElement { names.append(line); details.treats.def.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); } @@ -132,15 +135,21 @@ class SynoTreatment extends HTMLElement { names.append(line); details.treats.aug.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); details.treats.treattn.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-name.plazi.org/id/", ""); + const short = n.replace("http://taxon-name.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); } @@ -155,9 +164,12 @@ class SynoTreatment extends HTMLElement { names.append(line); details.treats.dpr.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); } @@ -170,15 +182,21 @@ class SynoTreatment extends HTMLElement { names.append(line); details.treats.citetc.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-concept.plazi.org/id/", ""); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); details.treats.citetn.forEach((n) => { - const url = document.createElement("code"); + const url = document.createElement("a"); url.classList.add("taxon", "uri"); - url.innerText = n.replace("http://taxon-name.plazi.org/id/", ""); + const short = n.replace("http://taxon-name.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; line.append(url); }); } @@ -203,13 +221,17 @@ class SynoName extends HTMLElement { title.append(" ", rank_badge); if (name.taxonNameURI) { - const name_uri = document.createElement("code"); + const name_uri = document.createElement("a"); name_uri.classList.add("taxon", "uri"); - name_uri.innerText = name.taxonNameURI.replace( + const short = name.taxonNameURI.replace( "http://taxon-name.plazi.org/id/", "", ); - name_uri.title = name.taxonNameURI; + name_uri.innerText = short; + name_uri.id = short; + name_uri.href = name.taxonNameURI; + name_uri.target = "_blank"; + name_uri.innerHTML += icons.link; title.append(" ", name_uri); } @@ -255,17 +277,21 @@ class SynoName extends HTMLElement { this.append(treatments); if (authorizedName.taxonConceptURI) { - const name_uri = document.createElement("code"); + const name_uri = document.createElement("a"); name_uri.classList.add("taxon", "uri"); - name_uri.innerText = authorizedName.taxonConceptURI.replace( + const short = authorizedName.taxonConceptURI.replace( "http://taxon-concept.plazi.org/id/", "", ); - name_uri.title = authorizedName.taxonConceptURI; - authName.append(name_uri); + name_uri.innerText = short; + name_uri.id = short; + name_uri.href = authorizedName.taxonConceptURI; + name_uri.target = "_blank"; + name_uri.innerHTML += icons.link; + authName.append(" ", name_uri); } if (authorizedName.colURI) { - const col_uri = document.createElement("code"); + const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); const id = authorizedName.colURI.replace( "https://www.catalogueoflife.org/data/taxon/", @@ -273,8 +299,10 @@ class SynoName extends HTMLElement { ); col_uri.innerText = id; col_uri.id = id; - col_uri.title = authorizedName.colURI; - authName.append(col_uri); + col_uri.href = authorizedName.colURI; + col_uri.target = "_blank"; + col_uri.innerHTML += icons.link; + authName.append(" ", col_uri); const li = document.createElement("div"); li.classList.add("treatmentline"); @@ -304,7 +332,7 @@ class SynoName extends HTMLElement { ); col_uri.innerText = id; col_uri.href = `#${id}`; - col_uri.title = authorizedName.acceptedColURI!; + col_uri.title = "show name"; line.append(col_uri); } } @@ -361,7 +389,8 @@ async function justify(name: Name): Promise { const indicator = document.createElement("div"); root.insertAdjacentElement("beforebegin", indicator); indicator.append(`Finding Synonyms for ${NAME} `); -indicator.append(document.createElement("progress")); +const progress = document.createElement("progress"); +indicator.append(progress); const timeStart = performance.now(); From e89be67c8e15e6714a09efc25379c76221e4f7a6 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:42:23 +0100 Subject: [PATCH 36/71] fixed github action --- .github/workflows/pages.yml | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3ebd50e..e2c4a6b 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -14,36 +14,15 @@ jobs: runs-on: ubuntu-22.04 steps: - - # main / old: - - name: "Checkout main branch" - uses: actions/checkout@v3 - with: - ref: main - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: prepare - run: | - mkdir build - cp index.html build/ - - run: deno bundle SynonymGroup.ts build/synonym-group.js - - run: cp SynonymGroup.ts build/ - - # synolib2 / new: - name: "Checkout synolib2 branch" uses: actions/checkout@v3 with: ref: synolib2 - path: "synolib2" - name: Draw the rest of the owl run: | - cd synolib2 deno task example_build - mkdir ../build/next - cp ./example/index.* ../build/next/ - cd .. + mkdir ./build + cp ./example/index.* ./build/ - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@4.1.5 From fb76109cee9136aa48deb2eceabc42840b74c8f7 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:06:00 +0100 Subject: [PATCH 37/71] actually fix github action --- .github/workflows/pages.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e2c4a6b..eda72f9 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -18,6 +18,10 @@ jobs: uses: actions/checkout@v3 with: ref: synolib2 + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x - name: Draw the rest of the owl run: | deno task example_build From d9618407ebc864e491f37510108bdd8eb377670e Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:44:22 +0100 Subject: [PATCH 38/71] handle col-taxa without authority --- SynonymGroup.ts | 39 +++++++++++++++++++++++++++++- example/index.ts | 63 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 92c254c..3d0a8ce 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -554,6 +554,9 @@ LIMIT 500`; "", ).trim(); + // Case where the CoL-taxon has no authority. There should only be one of these. + let unathorizedCol: string | undefined; + // there can be multiple CoL-taxa with same latin name, e.g. Leontopodium alpinum has 3T6ZY and 3T6ZX. const authorizedCoLNames: AuthorizedName[] = []; const authorizedTCNames: AuthorizedName[] = []; @@ -567,7 +570,16 @@ LIMIT 500`; for (const t of json.results.bindings) { if (t.col) { const colURI = t.col.value; - if (!authorizedCoLNames.find((e) => e.colURI === colURI)) { + if (!t.authority?.value) { + if (this.expanded.has(colURI)) { + console.log("Skipping known", colURI); + return; + } + if (unathorizedCol && unathorizedCol !== colURI) { + console.log("Duplicate unathorized COL:", unathorizedCol, colURI); + } + unathorizedCol = colURI; + } else if (!authorizedCoLNames.find((e) => e.colURI === colURI)) { if (this.expanded.has(colURI)) { console.log("Skipping known", colURI); return; @@ -637,6 +649,7 @@ LIMIT 500`; rank: json.results.bindings[0].rank!.value, taxonNameURI, authorizedNames: [...authorizedCoLNames, ...authorizedTCNames], + colURI: unathorizedCol, justification, treatments: { treats, @@ -656,6 +669,15 @@ LIMIT 500`; const colPromises: Promise[] = []; + if (unathorizedCol) { + const [acceptedColURI, promises] = await this.getAcceptedCol( + unathorizedCol, + name, + ); + name.acceptedColURI = acceptedColURI; + colPromises.push(...promises); + } + await Promise.all( authorizedCoLNames.map(async (n) => { const [acceptedColURI, promises] = await this.getAcceptedCol( @@ -1064,6 +1086,21 @@ export type Name = { /** The URI of the respective `dwcFP:TaxonName` if it exists */ taxonNameURI?: string; + + /** + * The URI of the respective CoL-taxon if it exists + * + * Not that this is only for CoL-taxa which do not have an authority. + */ + colURI?: string; + /** The URI of the corresponding accepted CoL-taxon if it exists. + * + * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon. + * + * May be the string "INVALID COL" if the colURI is not valid. + */ + acceptedColURI?: string; + /** All `AuthorizedName`s with this name */ authorizedNames: AuthorizedName[]; diff --git a/example/index.ts b/example/index.ts index 72b7cb8..42d0faa 100644 --- a/example/index.ts +++ b/example/index.ts @@ -235,12 +235,6 @@ class SynoName extends HTMLElement { title.append(" ", name_uri); } - const justification = document.createElement("abbr"); - justification.classList.add("justification"); - justification.innerText = "...?"; - justify(name).then((just) => justification.title = `This ${just}`); - title.append(" ", justification); - const vernacular = document.createElement("div"); vernacular.classList.add("vernacular"); name.vernacularNames.then((names) => { @@ -251,9 +245,56 @@ class SynoName extends HTMLElement { }); this.append(vernacular); + const treatments = document.createElement("ul"); + this.append(treatments); + + if (name.colURI) { + const col_uri = document.createElement("a"); + col_uri.classList.add("col", "uri"); + const id = name.colURI.replace( + "https://www.catalogueoflife.org/data/taxon/", + "", + ); + col_uri.innerText = id; + col_uri.id = id; + col_uri.href = name.colURI; + col_uri.target = "_blank"; + col_uri.innerHTML += icons.link; + title.append(" ", col_uri); + + const li = document.createElement("div"); + li.classList.add("treatmentline"); + li.innerHTML = name.acceptedColURI !== name.colURI + ? icons.dpr + : icons.aug; + treatments.append(li); + + const creators = document.createElement("span"); + creators.innerText = "Catalogue of Life"; + li.append(creators); + + const names = document.createElement("div"); + names.classList.add("indent"); + li.append(names); + + if (name.acceptedColURI !== name.colURI) { + const line = document.createElement("div"); + line.innerHTML = icons.east + icons.aug; + names.append(line); + + const col_uri = document.createElement("a"); + col_uri.classList.add("col", "uri"); + const id = name.acceptedColURI!.replace( + "https://www.catalogueoflife.org/data/taxon/", + "", + ); + col_uri.innerText = id; + col_uri.href = `#${id}`; + col_uri.title = "show name"; + line.append(col_uri); + } + } if (name.treatments.treats.size > 0 || name.treatments.cite.size > 0) { - const treatments = document.createElement("ul"); - this.append(treatments); for (const trt of name.treatments.treats) { const li = new SynoTreatment(trt, SynoStatus.Aug); treatments.append(li); @@ -264,6 +305,12 @@ class SynoName extends HTMLElement { } } + const justification = document.createElement("abbr"); + justification.classList.add("justification"); + justification.innerText = "...?"; + justify(name).then((just) => justification.title = `This ${just}`); + title.append(" ", justification); + for (const authorizedName of name.authorizedNames) { const authName = document.createElement("h3"); const name_title = document.createElement("i"); From 986931a0365c7439519200b6fee1108b1cdb693c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:34:35 +0100 Subject: [PATCH 39/71] resolve ids to names if the are synonyms --- SynonymGroup.ts | 40 ++++++++++++++ example/index.css | 11 +++- example/index.ts | 135 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 162 insertions(+), 24 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 3d0a8ce..3a2f43f 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -113,6 +113,46 @@ export class SynonymGroup implements AsyncIterable { } } + /** + * Finds the given name (identified by taxon-name, taxon-concept or CoL uri) among the list of synonyms. + * + * Will reject when the SynonymGroup finishes but the name was not found — this means that this was not a synonym. + */ + findName(uri: string): Promise { + let name: Name | AuthorizedName | undefined; + for (const n of this.names) { + if (n.taxonNameURI === uri || n.colURI === uri) { + name = n; + break; + } + const an = n.authorizedNames.find((an) => + an.taxonConceptURI === uri || an.colURI === uri + ); + if (an) { + name = an; + break; + } + } + if (name) return Promise.resolve(name); + return new Promise((resolve, reject) => { + this.monitor.addEventListener("updated", () => { + if (this.names.length === 0 || this.isFinished) reject(); + const n = this.names.at(-1)!; + if (n.taxonNameURI === uri || n.colURI === uri) { + resolve(n); + return; + } + const an = n.authorizedNames.find((an) => + an.taxonConceptURI === uri || an.colURI === uri + ); + if (an) { + resolve(an); + return; + } + }); + }); + } + /** @internal */ private async getName( taxonName: string, diff --git a/example/index.css b/example/index.css index a7066fc..30b6a53 100644 --- a/example/index.css +++ b/example/index.css @@ -33,6 +33,15 @@ syno-name { color: #686868; } +.taxon, .col { + color: #444; + font-size: 0.8rem; + + &:not(:last-child):not(.uri)::after { + content: ","; + } +} + .uri:not(:empty) { font-size: 0.8rem; line-height: 1rem; @@ -100,7 +109,7 @@ syno-treatment, .hidden { height: auto; - max-height: 100px; + max-height: 300px; overflow: auto; } } diff --git a/example/index.ts b/example/index.ts index 42d0faa..12e8bea 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,5 +1,6 @@ /// import { + type AuthorizedName, type Name, SparqlEndpoint, SynonymGroup, @@ -49,6 +50,22 @@ const icons = { empty: ``, }; +const indicator = document.createElement("div"); +root.insertAdjacentElement("beforebegin", indicator); +indicator.append(`Finding Synonyms for ${NAME} `); +const progress = document.createElement("progress"); +indicator.append(progress); + +const timeStart = performance.now(); + +const sparqlEndpoint = new SparqlEndpoint(ENDPOINT_URL); +const synoGroup = new SynonymGroup( + sparqlEndpoint, + NAME, + HIDE_COL_ONLY_SYNONYMS, + START_WITH_SUBTAXA, +); + class SynoTreatment extends HTMLElement { constructor(trt: Treatment, status: SynoStatus) { super(); @@ -121,7 +138,16 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); } if (details.treats.aug.size > 0 || details.treats.treattn.size > 0) { @@ -141,7 +167,16 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); details.treats.treattn.forEach((n) => { const url = document.createElement("a"); @@ -150,7 +185,16 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); } if (details.treats.dpr.size > 0) { @@ -170,7 +214,16 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); } if (details.treats.citetc.size > 0 || details.treats.citetn.size > 0) { @@ -188,7 +241,16 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); details.treats.citetn.forEach((n) => { const url = document.createElement("a"); @@ -197,9 +259,35 @@ class SynoTreatment extends HTMLElement { url.innerText = short; url.href = "#" + short; url.title = "show name"; - line.append(url); + line.append(" ", url); + synoGroup.findName(n).then((nn) => { + url.classList.remove("uri"); + if ((nn as AuthorizedName).authority) { + url.innerText = nn.displayName + " " + + (nn as AuthorizedName).authority; + } else url.innerText = nn.displayName; + }, () => { + url.removeAttribute("href"); + }); }); } + if (details.materialCitations.length > 0) { + const line = document.createElement("div"); + line.innerHTML = icons.empty + icons.cite + " Material Citations:
"; + line.classList.add("hidden"); + names.append(line); + line.innerText += details.materialCitations.map((c) => + JSON.stringify(c) + ).join("\n"); + } + if (details.figureCitations.length > 0) { + const line = document.createElement("div"); + line.innerHTML = icons.empty + icons.cite + " Figures:
"; + line.classList.add("hidden"); + names.append(line); + line.innerText += details.figureCitations.map((c) => JSON.stringify(c)) + .join("\n"); + } }); } } @@ -292,6 +380,14 @@ class SynoName extends HTMLElement { col_uri.href = `#${id}`; col_uri.title = "show name"; line.append(col_uri); + synoGroup.findName(name.acceptedColURI!).then((n) => { + if ((n as AuthorizedName).authority) { + col_uri.innerText = n.displayName + " " + + (n as AuthorizedName).authority; + } else col_uri.innerText = n.displayName; + }, () => { + col_uri.removeAttribute("href"); + }); } } if (name.treatments.treats.size > 0 || name.treatments.cite.size > 0) { @@ -380,7 +476,16 @@ class SynoName extends HTMLElement { col_uri.innerText = id; col_uri.href = `#${id}`; col_uri.title = "show name"; - line.append(col_uri); + line.append(" ", col_uri); + synoGroup.findName(authorizedName.acceptedColURI!).then((n) => { + col_uri.classList.remove("uri"); + if ((n as AuthorizedName).authority) { + col_uri.innerText = n.displayName + " " + + (n as AuthorizedName).authority; + } else col_uri.innerText = n.displayName; + }, () => { + col_uri.removeAttribute("href"); + }); } } @@ -433,22 +538,6 @@ async function justify(name: Name): Promise { } } -const indicator = document.createElement("div"); -root.insertAdjacentElement("beforebegin", indicator); -indicator.append(`Finding Synonyms for ${NAME} `); -const progress = document.createElement("progress"); -indicator.append(progress); - -const timeStart = performance.now(); - -const sparqlEndpoint = new SparqlEndpoint(ENDPOINT_URL); -const synoGroup = new SynonymGroup( - sparqlEndpoint, - NAME, - HIDE_COL_ONLY_SYNONYMS, - START_WITH_SUBTAXA, -); - for await (const name of synoGroup) { const element = new SynoName(name); root.append(element); From 35c8c3ba604e20625e0660acee2ca287b57c4a42 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:56:50 +0100 Subject: [PATCH 40/71] actually require tn to ba a tn --- SynonymGroup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 3a2f43f..607f29b 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -285,7 +285,8 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority } OPTIONAL { - ?tn dwc:rank ?trank . + ?tn dwc:rank ?trank ; + a dwcFP:TaxonName . FILTER(LCASE(?rank) = LCASE(?trank)) ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . From d932950cca4ce8d34a91b53dc72d1b7cbb5c222e Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:46:23 +0100 Subject: [PATCH 41/71] moved queries to own file --- Queries.ts | 275 +++++++++++++++++++++++++++++++++++++ SynonymGroup.ts | 356 ++++-------------------------------------------- 2 files changed, 299 insertions(+), 332 deletions(-) create mode 100644 Queries.ts diff --git a/Queries.ts b/Queries.ts new file mode 100644 index 0000000..8108207 --- /dev/null +++ b/Queries.ts @@ -0,0 +1,275 @@ +/** + * Common to all of the `getNameFrom_`-queries. + * + * As its own variable to ensure consistency in the resturned bindings. + */ +const preamble = `PREFIX dc: +PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: +PREFIX trt: +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority + (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) + (group_concat(DISTINCT ?aug;separator="|") as ?augs) + (group_concat(DISTINCT ?def;separator="|") as ?defs) + (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) + (group_concat(DISTINCT ?cite;separator="|") as ?cites) + (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) + (group_concat(DISTINCT ?citetn;separator="|") as ?tncites)`; + +/** + * Common to all of the `getNameFrom_`-queries. + * + * As its own variable to ensure consistency in the resturned bindings. + */ +const postamble = + `GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority`; + +// For unclear reasons, the queries breaks if the limit is removed. + +/** + * Note: this query assumes that there is no sub-species taxa with missing dwc:species + * + * Note: the handling assumes that at most one taxon-name matches this colTaxon + */ +export const getNameFromCol = (colUri: string) => + `${preamble} WHERE { +BIND(<${colUri}> as ?col) + ?col dwc:taxonRank ?rank . + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) + ?col dwc:scientificName ?name . + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + OPTIONAL { + ?col dwc:specificEpithet ?species . + OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } + } + + OPTIONAL { + ?tn dwc:rank ?trank ; + a dwcFP:TaxonName . + FILTER(LCASE(?rank) = LCASE(?trank)) + ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } + + OPTIONAL { + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } + } + } +} +${postamble} +LIMIT 500`; + +/** + * Note: this query assumes that there is no sub-species taxa with missing dwc:species + * + * Note: the handling assumes that at most one taxon-name matches this colTaxon + */ +export const getNameFromTC = (tcUri: string) => + `${preamble} WHERE { + <${tcUri}> trt:hasTaxonName ?tn . + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:kingdom ?kingdom . + ?tn dwc:genus ?genus . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + } + + OPTIONAL { + ?col dwc:taxonRank ?crank . + FILTER(LCASE(?rank) = LCASE(?crank)) + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } + + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } +} +${postamble} +LIMIT 500`; + +/** + * Note: this query assumes that there is no sub-species taxa with missing dwc:species + * + * Note: the handling assumes that at most one taxon-name matches this colTaxon + */ +export const getNameFromTN = (tnUri: string) => + `${preamble} WHERE { + BIND(<${tnUri}> as ?tn) + ?tn a dwcFP:TaxonName . + ?tn dwc:rank ?rank . + ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . + OPTIONAL { + ?tn dwc:species ?species . + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + } + + OPTIONAL { + ?col dwc:taxonRank ?crank . + FILTER(LCASE(?rank) = LCASE(?crank)) + OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } + ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:genericName ?genus . + # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + + { + ?col dwc:specificEpithet ?species . + ?tn dwc:species ?species . + { + ?col dwc:infraspecificEpithet ?infrasp . + ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . + } UNION { + FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } + FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + } + } UNION { + FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } + FILTER NOT EXISTS { ?tn dwc:species ?species . } + } + } + + BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) + BIND(COALESCE(?colAuth, "") as ?authority) + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn . + OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } + BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn . + OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } + BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + } + + OPTIONAL { + ?tc trt:hasTaxonName ?tn ; + dwc:scientificNameAuthorship ?tcauth ; + a dwcFP:TaxonConcept . + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc . + OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } + BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc . + OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } + BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc . + OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } + BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc . + OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } + BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + } + } +} +${postamble} +LIMIT 500`; diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 607f29b..714b4f5 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,4 +1,5 @@ import type { SparqlEndpoint, SparqlJson } from "./mod.ts"; +import * as Queries from "./Queries.ts"; /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -163,16 +164,34 @@ export class SynonymGroup implements AsyncIterable { return; } + if (this.controller.signal?.aborted) return Promise.reject(); + + let json: SparqlJson | undefined; + if (taxonName.startsWith("https://www.catalogueoflife.org")) { - await this.getNameFromCol(taxonName, justification); + json = await this.sparqlEndpoint.getSparqlResultSet( + Queries.getNameFromCol(taxonName), + { signal: this.controller.signal }, + `NameFromCol ${taxonName}`, + ); } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { - await this.getNameFromTC(taxonName, justification); + json = await this.sparqlEndpoint.getSparqlResultSet( + Queries.getNameFromTC(taxonName), + { signal: this.controller.signal }, + `NameFromTC ${taxonName}`, + ); } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { - await this.getNameFromTN(taxonName, justification); + json = await this.sparqlEndpoint.getSparqlResultSet( + Queries.getNameFromTN(taxonName), + { signal: this.controller.signal }, + `NameFromTN ${taxonName}`, + ); } else { throw `Cannot handle name-uri <${taxonName}> !`; } + await this.handleName(json!, justification); + if ( this.startWithSubTaxa && justification.searchTerm && !justification.subTaxon @@ -252,333 +271,6 @@ LIMIT 500`; await Promise.allSettled(names.map((n) => this.getName(n, justification))); } - /** @internal */ - private async getNameFromCol( - colUri: string, - justification: Justification, - ): Promise { - // Note: this query assumes that there is no sub-species taxa with missing dwc:species - // Note: the handling assumes that at most one taxon-name matches this colTaxon - const query = ` -PREFIX dc: -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - BIND(<${colUri}> as ?col) - ?col dwc:taxonRank ?rank . - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) - ?col dwc:scientificName ?name . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - OPTIONAL { - ?col dwc:specificEpithet ?species . - OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } - } - - OPTIONAL { - ?tn dwc:rank ?trank ; - a dwcFP:TaxonName . - FILTER(LCASE(?rank) = LCASE(?trank)) - ?tn dwc:genus ?genus . - ?tn dwc:kingdom ?kingdom . - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - - OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) - } - OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) - } - - OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) - } - OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) - } - OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) - } - OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) - } - } - } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - // For unclear reasons, the query breaks if the limit is removed. - - if (this.controller.signal?.aborted) return Promise.reject(); - - /// ?tn ?tc !rank !genus ?species ?infrasp !name !authority ?tcAuth - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromCol ${colUri}`, - ); - - return this.handleName(json, justification); - } - - /** @internal */ - private async getNameFromTC( - tcUri: string, - justification: Justification, - ): Promise { - // Note: this query assumes that there is no sub-species taxa with missing dwc:species - // Note: the handling assumes that at most one taxon-name matches this colTaxon - const query = ` -PREFIX dc: -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - <${tcUri}> trt:hasTaxonName ?tn . - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . - ?tn dwc:kingdom ?kingdom . - ?tn dwc:genus ?genus . - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - - OPTIONAL { - ?col dwc:taxonRank ?crank . - FILTER(LCASE(?rank) = LCASE(?crank)) - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) - - OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) - } - OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) - } - - OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) - } - OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) - } - OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) - } - OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) - } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - // For unclear reasons, the query breaks if the limit is removed. - - if (this.controller.signal?.aborted) return Promise.reject(); - - /// ?tn ?tc ?col !rank !genus ?species ?infrasp !name !authority ?tcAuth - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromTC ${tcUri}`, - ); - - await this.handleName(json, justification); - } - - /** @internal */ - private async getNameFromTN( - tnUri: string, - justification: Justification, - ): Promise { - // Note: this query assumes that there is no sub-species taxa with missing dwc:species - // Note: the handling assumes that at most one taxon-name matches this colTaxon - const query = ` -PREFIX dc: -PREFIX dwc: -PREFIX dwcFP: -PREFIX cito: -PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority - (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) - (group_concat(DISTINCT ?aug;separator="|") as ?augs) - (group_concat(DISTINCT ?def;separator="|") as ?defs) - (group_concat(DISTINCT ?dpr;separator="|") as ?dprs) - (group_concat(DISTINCT ?cite;separator="|") as ?cites) - (group_concat(DISTINCT ?trtn;separator="|") as ?tntreats) - (group_concat(DISTINCT ?citetn;separator="|") as ?tncites) WHERE { - BIND(<${tnUri}> as ?tn) - ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . - ?tn dwc:genus ?genus . - ?tn dwc:kingdom ?kingdom . - OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - - OPTIONAL { - ?col dwc:taxonRank ?crank . - FILTER(LCASE(?rank) = LCASE(?crank)) - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority - ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } - } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) - - OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) - } - OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) - } - - OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . - OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) - } - OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) - } - OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) - } - OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) - } - } -} -GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority -LIMIT 500`; - // For unclear reasons, the query breaks if the limit is removed. - - if (this.controller.signal?.aborted) return Promise.reject(); - - const json = await this.sparqlEndpoint.getSparqlResultSet( - query, - { signal: this.controller.signal }, - `NameFromTN ${tnUri}`, - ); - - return this.handleName(json, justification); - } - /** * Note this makes some assumptions on which variables are present in the bindings * @@ -805,7 +497,7 @@ GROUP BY ?current ?current_status`; if (!this.acceptedCol.has(b.current!.value)) { this.acceptedCol.set(b.current!.value, b.current!.value); promises.push( - this.getNameFromCol(b.current!.value, { + this.getName(b.current!.value, { searchTerm: false, parent, }), @@ -815,7 +507,7 @@ GROUP BY ?current ?current_status`; this.acceptedCol.set(dpr, b.current!.value); if (!this.ignoreDeprecatedCoL) { promises.push( - this.getNameFromCol(dpr, { searchTerm: false, parent }), + this.getName(dpr, { searchTerm: false, parent }), ); } } From 24af399b7fd37e7654748d6b478415e3a414c01c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:49:16 +0100 Subject: [PATCH 42/71] simplified treatments there are no treatments without a date --- Queries.ts | 100 +++++++++++++++++++++-------------------------------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/Queries.ts b/Queries.ts index 8108207..6c7f0c7 100644 --- a/Queries.ts +++ b/Queries.ts @@ -67,39 +67,32 @@ BIND(<${colUri}> as ?col) } OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) } OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + ?citetnt trt:citesTaxonName ?tn ; trt:publishedIn/dc:date ?citetndate . + BIND(CONCAT(STR(?citetnt), ">", ?citetndate) AS ?citetn) } OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . + ?tc trt:hasTaxonName ?tn ; dwc:scientificNameAuthorship ?tcauth ; a dwcFP:TaxonConcept . + OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) } OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) } OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) } OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?citedate) AS ?cite) } } } @@ -156,35 +149,29 @@ export const getNameFromTC = (tcUri: string) => BIND(COALESCE(?colAuth, "") as ?authority) OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) } OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + ?citetnt trt:citesTaxonName ?tn ; trt:publishedIn/dc:date ?citetndate . + BIND(CONCAT(STR(?citetnt), ">", ?citetndate) AS ?citetn) } OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) } OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) } OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) } OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?citedate) AS ?cite) } } ${postamble} @@ -235,39 +222,32 @@ export const getNameFromTN = (tnUri: string) => BIND(COALESCE(?colAuth, "") as ?authority) OPTIONAL { - ?trtnt trt:treatsTaxonName ?tn . - OPTIONAL { ?trtnt trt:publishedIn/dc:date ?trtndate . } - BIND(CONCAT(STR(?trtnt), ">", COALESCE(?trtndate, "")) AS ?trtn) + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) } OPTIONAL { - ?citetnt trt:citesTaxonName ?tn . - OPTIONAL { ?citetnt trt:publishedIn/dc:date ?citetndate . } - BIND(CONCAT(STR(?citetnt), ">", COALESCE(?citetndate, "")) AS ?citetn) + ?citetnt trt:citesTaxonName ?tn ; trt:publishedIn/dc:date ?citetndate . + BIND(CONCAT(STR(?citetnt), ">", ?citetndate) AS ?citetn) } OPTIONAL { - ?tc trt:hasTaxonName ?tn ; - dwc:scientificNameAuthorship ?tcauth ; - a dwcFP:TaxonConcept . + ?tc trt:hasTaxonName ?tn ; dwc:scientificNameAuthorship ?tcauth ; a dwcFP:TaxonConcept . + OPTIONAL { - ?augt trt:augmentsTaxonConcept ?tc . - OPTIONAL { ?augt trt:publishedIn/dc:date ?augdate . } - BIND(CONCAT(STR(?augt), ">", COALESCE(?augdate, "")) AS ?aug) + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) } OPTIONAL { - ?deft trt:definesTaxonConcept ?tc . - OPTIONAL { ?deft trt:publishedIn/dc:date ?defdate . } - BIND(CONCAT(STR(?deft), ">", COALESCE(?defdate, "")) AS ?def) + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) } OPTIONAL { - ?dprt trt:deprecates ?tc . - OPTIONAL { ?dprt trt:publishedIn/dc:date ?dprdate . } - BIND(CONCAT(STR(?dprt), ">", COALESCE(?dprdate, "")) AS ?dpr) + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) } OPTIONAL { - ?citet cito:cites ?tc . - OPTIONAL { ?citet trt:publishedIn/dc:date ?citedate . } - BIND(CONCAT(STR(?citet), ">", COALESCE(?citedate, "")) AS ?cite) + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?citedate) AS ?cite) } } } From 936a99c16276230b6ebe9d62b6f1cd2aa2094857 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:27:34 +0100 Subject: [PATCH 43/71] better figure support --- SparqlEndpoint.ts | 4 +- SynonymGroup.ts | 2 +- example/index.css | 85 ++++++++++++++++++++++++++++++++++--- example/index.html | 2 +- example/index.ts | 103 ++++++++++++++++++++++++++++----------------- 5 files changed, 148 insertions(+), 48 deletions(-) diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts index 023ae52..b3ba699 100644 --- a/SparqlEndpoint.ts +++ b/SparqlEndpoint.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file no-unused-labels async function sleep(ms: number): Promise { const p = new Promise((resolve) => { setTimeout(resolve, ms); @@ -47,6 +48,7 @@ export class SparqlEndpoint { _reason = "", ): Promise { // this.reasons.push(_reason); + // DEBUG: console.info(`SPARQL ${_reason}:\n${query}`); fetchOptions.headers = fetchOptions.headers || {}; (fetchOptions.headers as Record)["Accept"] = @@ -54,7 +56,7 @@ export class SparqlEndpoint { let retryCount = 0; const sendRequest = async (): Promise => { try { - // console.info(`SPARQL ${_reason} (${retryCount + 1})`); + // DEBUG: console.info(`SPARQL ${_reason} (${retryCount + 1})`); const response = await fetch( this.sparqlEnpointUri + "?query=" + encodeURIComponent(query), fetchOptions, diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 714b4f5..0519f6b 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -283,7 +283,7 @@ LIMIT 500`; const displayName: string = json.results.bindings[0].name!.value .replace( - json.results.bindings[0].authority!.value, + json.results.bindings[0].authority?.value ?? "", "", ).trim(); diff --git a/example/index.css b/example/index.css index 30b6a53..81e8e5b 100644 --- a/example/index.css +++ b/example/index.css @@ -6,10 +6,20 @@ font-family: Inter, Arial, Helvetica, sans-serif; } +body { + margin: 0 auto; + max-width: 80rem; +} + #root:empty::before { content: "You should see a list of synonyms here"; } +img { + max-width: 100%; + max-height: 100%; +} + svg { height: 1rem; vertical-align: sub; @@ -33,7 +43,8 @@ syno-name { color: #686868; } -.taxon, .col { +.taxon, +.col { color: #444; font-size: 0.8rem; @@ -81,22 +92,32 @@ syno-treatment, .treatmentline { display: block; position: relative; + margin: 0; + transition: all 200ms; + clear: both; - & > svg { + &>svg { height: 1rem; vertical-align: sub; margin: 0 0.2rem 0 -1.2rem; } + >.icon.button { + float: right; + } + .details { /* display: grid; */ } .hidden { - height: 0; max-height: 0; overflow: hidden; transition: all 200ms; + + & img { + display: none; + } } &.expanded { @@ -105,16 +126,66 @@ syno-treatment, z-index: 10; border-radius: 0.2rem; padding: 0.2rem; - margin: -0.2rem; + margin: 0.2rem -0.2rem; .hidden { - height: auto; - max-height: 300px; + max-height: 200rem; overflow: auto; + + & img { + display: unset; + } } } } +.figures { + display: grid; + grid-template-columns: repeat( auto-fill, minmax(12rem, 1fr) ); + grid-template-rows: masonry; + margin: 0.2rem 0 0.2rem 2.2rem; + gap: 1rem; + + & figure { + margin: 0; + } + + & figcaption { + font-size: 0.4rem; + } +} + + +.icon.button { + border-radius: 1rem; + border: none; + background: none; + width: 1rem; + height: 1rem; + display: inline-block; + padding: 0; + position: relative; + + &>svg { + height: 1rem; + margin: 0; + } + + &::before { + content: ""; + position: absolute; + top: -1rem; + bottom: -1rem; + left: -1rem; + right: -1rem; + border-radius: 100%; + } + + &:hover::before { + background: #ededed8c; + } +} + .indent { margin-left: 1.4rem; } @@ -133,4 +204,4 @@ syno-treatment, .gray { color: #666666; -} +} \ No newline at end of file diff --git a/example/index.html b/example/index.html index e717096..c65e1c5 100644 --- a/example/index.html +++ b/example/index.html @@ -29,7 +29,7 @@

SynoLib


- Click on a treatment to show all cited taxa. Click on a taxon identifier + Expand a treatment with the button on the right to show all cited taxa, figures and materials. Click on a taxon identifier beneath a treatment to scroll to that name — if it is (already) in the list.

diff --git a/example/index.ts b/example/index.ts index 12e8bea..ee1f6c3 100644 --- a/example/index.ts +++ b/example/index.ts @@ -24,30 +24,38 @@ enum SynoStatus { Aug = "aug", Dpr = "dpr", Cite = "cite", - Full = "full", } const icons = { def: - ``, + ``, aug: - ``, + ``, dpr: - ``, + ``, cite: - ``, + ``, unknown: - ``, + ``, + + col_aug: + ``, + col_dpr: + ``, link: - ``, + ``, + + expand: + ``, + collapse: + ``, + east: - ``, + ``, west: - ``, - line: - ``, - empty: ``, + ``, + empty: ``, }; const indicator = document.createElement("div"); @@ -70,16 +78,18 @@ class SynoTreatment extends HTMLElement { constructor(trt: Treatment, status: SynoStatus) { super(); - if (status === SynoStatus.Full) this.classList.add("expanded"); - else { - this.innerHTML = icons[status] ?? icons.unknown; - this.addEventListener("click", () => { - // const expanded = new SynoTreatment(trt, SynoStatus.Full); - // this.prepend(expanded); - // expanded.addEventListener("click", () => expanded.remove()); - this.classList.toggle("expanded"); - }); - } + this.innerHTML = icons[status] ?? icons.unknown; + + const button = document.createElement("button"); + button.classList.add("icon", "button"); + button.innerHTML = icons.expand; + button.addEventListener("click", () => { + if (this.classList.toggle("expanded")) { + button.innerHTML = icons.collapse; + } else { + button.innerHTML = icons.expand; + } + }); const date = document.createElement("span"); if (trt.date) date.innerText = "" + trt.date; @@ -100,6 +110,8 @@ class SynoTreatment extends HTMLElement { url.innerHTML += icons.link; this.append(" ", url); + this.append(button); + const names = document.createElement("div"); names.classList.add("indent", "details"); this.append(names); @@ -271,22 +283,37 @@ class SynoTreatment extends HTMLElement { }); }); } - if (details.materialCitations.length > 0) { + if (details.figureCitations.length > 0) { const line = document.createElement("div"); - line.innerHTML = icons.empty + icons.cite + " Material Citations:
"; - line.classList.add("hidden"); + line.classList.add("figures", "hidden"); names.append(line); - line.innerText += details.materialCitations.map((c) => - JSON.stringify(c) - ).join("\n"); + for (const figure of details.figureCitations) { + const el = document.createElement("figure"); + line.append(el); + const img = document.createElement("img"); + img.src = figure.url; + img.loading = "lazy"; + img.alt = figure.description ?? "Cited Figure without caption"; + el.append(img); + const caption = document.createElement("figcaption"); + caption.innerText = figure.description ?? ""; + el.append(caption); + } } - if (details.figureCitations.length > 0) { + if (details.materialCitations.length > 0) { const line = document.createElement("div"); - line.innerHTML = icons.empty + icons.cite + " Figures:
"; + line.innerHTML = icons.empty + icons.cite + + " Material Citations:
-"; line.classList.add("hidden"); names.append(line); - line.innerText += details.figureCitations.map((c) => JSON.stringify(c)) - .join("\n"); + line.innerText += details.materialCitations.map((c) => + JSON.stringify(c) + .replaceAll("{", "") + .replaceAll("}", "") + .replaceAll('":', ": ") + .replaceAll(",", ", ") + .replaceAll('"', "") + ).join("\n -"); } }); } @@ -353,8 +380,8 @@ class SynoName extends HTMLElement { const li = document.createElement("div"); li.classList.add("treatmentline"); li.innerHTML = name.acceptedColURI !== name.colURI - ? icons.dpr - : icons.aug; + ? icons.col_dpr + : icons.col_aug; treatments.append(li); const creators = document.createElement("span"); @@ -367,7 +394,7 @@ class SynoName extends HTMLElement { if (name.acceptedColURI !== name.colURI) { const line = document.createElement("div"); - line.innerHTML = icons.east + icons.aug; + line.innerHTML = icons.east + icons.col_aug; names.append(line); const col_uri = document.createElement("a"); @@ -450,8 +477,8 @@ class SynoName extends HTMLElement { const li = document.createElement("div"); li.classList.add("treatmentline"); li.innerHTML = authorizedName.acceptedColURI !== authorizedName.colURI - ? icons.dpr - : icons.aug; + ? icons.col_dpr + : icons.col_aug; treatments.append(li); const creators = document.createElement("span"); @@ -464,7 +491,7 @@ class SynoName extends HTMLElement { if (authorizedName.acceptedColURI !== authorizedName.colURI) { const line = document.createElement("div"); - line.innerHTML = icons.east + icons.aug; + line.innerHTML = icons.east + icons.col_aug; names.append(line); const col_uri = document.createElement("a"); From 9663f681ca1149060f7b5a6eb0c391b135757963 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:06:18 +0100 Subject: [PATCH 44/71] fixed queries to be usable with lindas --- Queries.ts | 84 +++++++++++++++++++------------------------------ SynonymGroup.ts | 44 +++++++++++++++++++++----- 2 files changed, 70 insertions(+), 58 deletions(-) diff --git a/Queries.ts b/Queries.ts index 6c7f0c7..d52d3af 100644 --- a/Queries.ts +++ b/Queries.ts @@ -8,7 +8,7 @@ PREFIX dwc: PREFIX dwcFP: PREFIX cito: PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority +SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) @@ -23,7 +23,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority * As its own variable to ensure consistency in the resturned bindings. */ const postamble = - `GROUP BY ?tn ?tc ?col ?rank ?genus ?species ?infrasp ?name ?authority`; + `GROUP BY ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority`; // For unclear reasons, the queries breaks if the limit is removed. @@ -36,7 +36,6 @@ export const getNameFromCol = (colUri: string) => `${preamble} WHERE { BIND(<${colUri}> as ?col) ?col dwc:taxonRank ?rank . - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } BIND(COALESCE(?colAuth, "") as ?authority) ?col dwc:scientificName ?name . ?col dwc:genericName ?genus . # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . @@ -44,6 +43,7 @@ BIND(<${colUri}> as ?col) ?col dwc:specificEpithet ?species . OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } } + OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } OPTIONAL { ?tn dwc:rank ?trank ; @@ -113,40 +113,31 @@ export const getNameFromTC = (tcUri: string) => a dwcFP:TaxonConcept . ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . + ?tn dwc:rank ?tnrank . ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . + OPTIONAL { ?tn dwc:subGenus ?subgenus . } OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + ?tn dwc:species ?tnspecies . + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } } + BIND(LCASE(?tnrank) AS ?rank) + BIND(COALESCE(?tnspecies, "") AS ?species) + BIND(COALESCE(?tninfrasp, "") AS ?infrasp) + OPTIONAL { - ?col dwc:taxonRank ?crank . - FILTER(LCASE(?rank) = LCASE(?crank)) - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:taxonRank ?rank . + ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } + OPTIONAL { ?col dwc:specificEpithet ?colspecies . } + FILTER(?species = COALESCE(?colspecies, "")) + OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } + FILTER(?infrasp = COALESCE(?colinfrasp, "")) + OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) OPTIONAL { ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . @@ -186,40 +177,31 @@ export const getNameFromTN = (tnUri: string) => `${preamble} WHERE { BIND(<${tnUri}> as ?tn) ?tn a dwcFP:TaxonName . - ?tn dwc:rank ?rank . + ?tn dwc:rank ?tnrank . ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . + OPTIONAL { ?tn dwc:subGenus ?subgenus . } OPTIONAL { - ?tn dwc:species ?species . - OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } + ?tn dwc:species ?tnspecies . + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } } + BIND(LCASE(?tnrank) AS ?rank) + BIND(COALESCE(?tnspecies, "") AS ?species) + BIND(COALESCE(?tninfrasp, "") AS ?infrasp) + OPTIONAL { - ?col dwc:taxonRank ?crank . - FILTER(LCASE(?rank) = LCASE(?crank)) - OPTIONAL { ?col dwc:scientificNameAuthorship ?colAuth . } - ?col dwc:scientificName ?fullName . # Note: contains authority + ?col dwc:taxonRank ?rank . + ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } + OPTIONAL { ?col dwc:specificEpithet ?colspecies . } + FILTER(?species = COALESCE(?colspecies, "")) + OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } + FILTER(?infrasp = COALESCE(?colinfrasp, "")) + OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } } - - BIND(COALESCE(?fullName, CONCAT(?genus, COALESCE(CONCAT(" (",?subgenus,")"), ""), COALESCE(CONCAT(" ",?species), ""), COALESCE(CONCAT(" ", ?infrasp), ""))) as ?name) - BIND(COALESCE(?colAuth, "") as ?authority) OPTIONAL { ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 0519f6b..21db4b6 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -281,11 +281,41 @@ LIMIT 500`; ): Promise { const treatmentPromises: Treatment[] = []; - const displayName: string = json.results.bindings[0].name!.value - .replace( - json.results.bindings[0].authority?.value ?? "", - "", - ).trim(); + const abbreviateRank = (rank: string) => { + switch (rank) { + case "variety": + return "var."; + case "subspecies": + return "subsp."; + case "form": + return "f."; + default: + return rank; + } + }; + + const displayName: string = (json.results.bindings[0].name + ? ( + json.results.bindings[0].authority + ? json.results.bindings[0].name.value + .replace( + json.results.bindings[0].authority.value, + "", + ) + : json.results.bindings[0].name.value + ) + : json.results.bindings[0].genus!.value + + (json.results.bindings[0].subgenus?.value + ? ` (${json.results.bindings[0].subgenus.value})` + : "") + + (json.results.bindings[0].species?.value + ? ` ${json.results.bindings[0].species.value}` + : "") + + (json.results.bindings[0].infrasp?.value + ? ` ${abbreviateRank(json.results.bindings[0].rank!.value)} ${ + json.results.bindings[0].infrasp.value + }` + : "")).trim(); // Case where the CoL-taxon has no authority. There should only be one of these. let unathorizedCol: string | undefined; @@ -469,11 +499,11 @@ SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator= { ?col dwc:acceptedName ?current . ?dpr dwc:acceptedName ?current . - ?current dwc:taxonomicStatus ?current_status . + OPTIONAL { ?current dwc:taxonomicStatus ?current_status . } } UNION { ?col dwc:taxonomicStatus ?current_status . OPTIONAL { ?dpr dwc:acceptedName ?col . } - FILTER NOT EXISTS { ?col dwc:acceptedName ?current . } + FILTER NOT EXISTS { ?col dwc:acceptedName ?_ . } BIND(?col AS ?current) } } From 46d1d35d269de7fe857c04ea7f772c9d8f84cd89 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:14:13 +0100 Subject: [PATCH 45/71] handle kingdoms --- Queries.ts | 34 ++++++++++++++-------------------- SparqlEndpoint.ts | 1 - SynonymGroup.ts | 7 +++++-- example/index.css | 8 -------- example/index.ts | 5 ++++- 5 files changed, 23 insertions(+), 32 deletions(-) diff --git a/Queries.ts b/Queries.ts index d52d3af..b81381a 100644 --- a/Queries.ts +++ b/Queries.ts @@ -8,7 +8,7 @@ PREFIX dwc: PREFIX dwcFP: PREFIX cito: PREFIX trt: -SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority +SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) @@ -23,7 +23,7 @@ SELECT DISTINCT ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?aut * As its own variable to ensure consistency in the resturned bindings. */ const postamble = - `GROUP BY ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority`; + `GROUP BY ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority`; // For unclear reasons, the queries breaks if the limit is removed. @@ -38,7 +38,8 @@ BIND(<${colUri}> as ?col) ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } + BIND(COALESCE(?colkingdom, "") AS ?kingdom) OPTIONAL { ?col dwc:specificEpithet ?species . OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } @@ -49,22 +50,13 @@ BIND(<${colUri}> as ?col) ?tn dwc:rank ?trank ; a dwcFP:TaxonName . FILTER(LCASE(?rank) = LCASE(?trank)) - ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . - { - ?col dwc:specificEpithet ?species . - ?tn dwc:species ?species . - { - ?col dwc:infraspecificEpithet ?infrasp . - ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . - } UNION { - FILTER NOT EXISTS { ?col dwc:infraspecificEpithet ?infrasp . } - FILTER NOT EXISTS { ?tn dwc:subSpecies|dwc:variety|dwc:form ?infrasp . } - } - } UNION { - FILTER NOT EXISTS { ?col dwc:specificEpithet ?species . } - FILTER NOT EXISTS { ?tn dwc:species ?species . } - } + ?tn dwc:genus ?genus . + + OPTIONAL { ?tn dwc:species ?tnspecies . } + FILTER(?species = COALESCE(?tnspecies, "")) + OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } + FILTER(?infrasp = COALESCE(?tninfrasp, "")) OPTIONAL { ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . @@ -130,7 +122,8 @@ export const getNameFromTC = (tcUri: string) => ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } + FILTER(?kingdom = COALESCE(?colkingdom, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) @@ -194,7 +187,8 @@ export const getNameFromTN = (tnUri: string) => ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . - # TODO # ?col dwc:parent* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?kingdom . + OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } + FILTER(?kingdom = COALESCE(?colkingdom, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts index b3ba699..39627d1 100644 --- a/SparqlEndpoint.ts +++ b/SparqlEndpoint.ts @@ -1,4 +1,3 @@ -// deno-lint-ignore-file no-unused-labels async function sleep(ms: number): Promise { const p = new Promise((resolve) => { setTimeout(resolve, ms); diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 21db4b6..23bf2be 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -408,6 +408,7 @@ LIMIT 500`; treats.forEach((t) => treatmentPromises.push(t)); const name: Name = { + kingdom: json.results.bindings[0].kingdom!.value, displayName, rank: json.results.bindings[0].rank!.value, taxonNameURI, @@ -830,8 +831,10 @@ SELECT DISTINCT ?url ?description WHERE { * Each `Name` is uniquely determined by its human-readable latin name (for taxa ranking below genus, this is a multi-part name — binomial or trinomial) and kingdom. */ export type Name = { - /** taxonomic kingdom */ - // kingdom: string; + /** taxonomic kingdom + * + * may be empty for some CoL-taxa with missing ancestors */ + kingdom: string; /** Human-readable name */ displayName: string; /** taxonomic rank */ diff --git a/example/index.css b/example/index.css index 81e8e5b..5a53c40 100644 --- a/example/index.css +++ b/example/index.css @@ -114,10 +114,6 @@ syno-treatment, max-height: 0; overflow: hidden; transition: all 200ms; - - & img { - display: none; - } } &.expanded { @@ -131,10 +127,6 @@ syno-treatment, .hidden { max-height: 200rem; overflow: auto; - - & img { - display: unset; - } } } } diff --git a/example/index.ts b/example/index.ts index ee1f6c3..4a5aee4 100644 --- a/example/index.ts +++ b/example/index.ts @@ -333,7 +333,10 @@ class SynoName extends HTMLElement { const rank_badge = document.createElement("span"); rank_badge.classList.add("rank"); rank_badge.innerText = name.rank; - title.append(" ", rank_badge); + const kingdom_badge = document.createElement("span"); + kingdom_badge.classList.add("rank"); + kingdom_badge.innerText = name.kingdom || "Missing Kingdom"; + title.append(" ", kingdom_badge, " ", rank_badge); if (name.taxonNameURI) { const name_uri = document.createElement("a"); From 57a06848d467f9ab065c7568acde4ec1fb99d463 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:32:55 +0100 Subject: [PATCH 46/71] sungenus and fmt --- Queries.ts | 16 +++++++++++++--- SynonymGroup.ts | 2 +- example/index.css | 11 +++++------ example/index.html | 6 +++--- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Queries.ts b/Queries.ts index b81381a..7661784 100644 --- a/Queries.ts +++ b/Queries.ts @@ -40,6 +40,8 @@ BIND(<${colUri}> as ?col) ?col dwc:genericName ?genus . OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } BIND(COALESCE(?colkingdom, "") AS ?kingdom) + OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } + BIND(COALESCE(?colsubgenus, "") AS ?subgenus) OPTIONAL { ?col dwc:specificEpithet ?species . OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } @@ -52,7 +54,9 @@ BIND(<${colUri}> as ?col) FILTER(LCASE(?rank) = LCASE(?trank)) ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . - + + OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } + FILTER(?subgenus = COALESCE(?tnsubgenus, "")) OPTIONAL { ?tn dwc:species ?tnspecies . } FILTER(?species = COALESCE(?tnspecies, "")) OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } @@ -108,13 +112,14 @@ export const getNameFromTC = (tcUri: string) => ?tn dwc:rank ?tnrank . ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . - OPTIONAL { ?tn dwc:subGenus ?subgenus . } + OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:species ?tnspecies . OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } } BIND(LCASE(?tnrank) AS ?rank) + BIND(COALESCE(?tnsubgenus, "") AS ?subgenus) BIND(COALESCE(?tnspecies, "") AS ?species) BIND(COALESCE(?tninfrasp, "") AS ?infrasp) @@ -125,6 +130,8 @@ export const getNameFromTC = (tcUri: string) => OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } FILTER(?kingdom = COALESCE(?colkingdom, "")) + OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } + FILTER(?subgenus = COALESCE(?colsubgenus, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } @@ -173,13 +180,14 @@ export const getNameFromTN = (tnUri: string) => ?tn dwc:rank ?tnrank . ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . - OPTIONAL { ?tn dwc:subGenus ?subgenus . } + OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:species ?tnspecies . OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } } BIND(LCASE(?tnrank) AS ?rank) + BIND(COALESCE(?tnsubgenus, "") AS ?subgenus) BIND(COALESCE(?tnspecies, "") AS ?species) BIND(COALESCE(?tninfrasp, "") AS ?infrasp) @@ -190,6 +198,8 @@ export const getNameFromTN = (tnUri: string) => OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } FILTER(?kingdom = COALESCE(?colkingdom, "")) + OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } + FILTER(?subgenus = COALESCE(?colsubgenus, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 23bf2be..5ebb1f7 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -832,7 +832,7 @@ SELECT DISTINCT ?url ?description WHERE { */ export type Name = { /** taxonomic kingdom - * + * * may be empty for some CoL-taxa with missing ancestors */ kingdom: string; /** Human-readable name */ diff --git a/example/index.css b/example/index.css index 5a53c40..6fedae9 100644 --- a/example/index.css +++ b/example/index.css @@ -96,13 +96,13 @@ syno-treatment, transition: all 200ms; clear: both; - &>svg { + & > svg { height: 1rem; vertical-align: sub; margin: 0 0.2rem 0 -1.2rem; } - >.icon.button { + > .icon.button { float: right; } @@ -133,7 +133,7 @@ syno-treatment, .figures { display: grid; - grid-template-columns: repeat( auto-fill, minmax(12rem, 1fr) ); + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); grid-template-rows: masonry; margin: 0.2rem 0 0.2rem 2.2rem; gap: 1rem; @@ -147,7 +147,6 @@ syno-treatment, } } - .icon.button { border-radius: 1rem; border: none; @@ -158,7 +157,7 @@ syno-treatment, padding: 0; position: relative; - &>svg { + & > svg { height: 1rem; margin: 0; } @@ -196,4 +195,4 @@ syno-treatment, .gray { color: #666666; -} \ No newline at end of file +} diff --git a/example/index.html b/example/index.html index c65e1c5..0643f32 100644 --- a/example/index.html +++ b/example/index.html @@ -29,9 +29,9 @@

SynoLib


- Expand a treatment with the button on the right to show all cited taxa, figures and materials. Click on a taxon identifier - beneath a treatment to scroll to that name — if it is (already) in the - list. + Expand a treatment with the button on the right to show all cited taxa, + figures and materials. Click on a taxon identifier beneath a treatment to + scroll to that name — if it is (already) in the list.

From 1c1aad9a87b88695e36d1511bf8110e32f49dfec Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:35:50 +0100 Subject: [PATCH 47/71] v3.0.1 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 773338b..9a3c278 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.0.0", + "version": "3.1.0", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE"], From df61eb088f55eb8b039dfff218feb30c4f7ed6c7 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:17:09 +0100 Subject: [PATCH 48/71] fix getNameFromCoL query --- Queries.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Queries.ts b/Queries.ts index 7661784..575e6e6 100644 --- a/Queries.ts +++ b/Queries.ts @@ -39,15 +39,18 @@ BIND(<${colUri}> as ?col) ?col dwc:scientificName ?name . ?col dwc:genericName ?genus . OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } - BIND(COALESCE(?colkingdom, "") AS ?kingdom) OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } - BIND(COALESCE(?colsubgenus, "") AS ?subgenus) OPTIONAL { - ?col dwc:specificEpithet ?species . - OPTIONAL { ?col dwc:infraspecificEpithet ?infrasp . } + ?col dwc:specificEpithet ?colspecies . + OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } } OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } + BIND(COALESCE(?colkingdom, "") AS ?kingdom) + BIND(COALESCE(?colsubgenus, "") AS ?subgenus) + BIND(COALESCE(?colspecies, "") AS ?species) + BIND(COALESCE(?colinfrasp, "") AS ?infrasp) + OPTIONAL { ?tn dwc:rank ?trank ; a dwcFP:TaxonName . From 2b610f9103225190dff550b620ef9d04ea747b33 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:19:51 +0100 Subject: [PATCH 49/71] v3.1.1 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 9a3c278..21eb719 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.1.0", + "version": "3.1.1", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE"], From 81e71cfcf78673f85ea8b71093c7d2b8cd3f0043 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:25:08 +0100 Subject: [PATCH 50/71] added comment --- SynonymGroup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 5ebb1f7..a78363d 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -453,6 +453,7 @@ LIMIT 500`; }), ); + // TODO: make "acceptedCol" be a promise so we can move this above the this.getAcceptedCol-awaits, to show names sooner. this.pushName(name); /** Map */ From 2189046f88da10602d28c034523585537321108d Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:27:59 +0100 Subject: [PATCH 51/71] improved cli --- README.md | 36 ++++++++++++++++- SparqlEndpoint.ts | 2 +- deno.lock | 7 ++++ example/cli.ts | 99 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 127 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3ed5a80..8a3d0c5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ such synonymity and treatments about these taxon names or the respective taxa. For a command line example using the library see: `example/cli.ts`. You can try it locally using Deno with - ```sh deno run --allow-net ./example/cli.ts Ludwigia adscendens # or @@ -23,6 +22,41 @@ deno run --allow-net ./example/cli.ts https://www.catalogueoflife.org/data/taxon (replace the argument with whatever name interests you) +Params: +``` +deno run --allow-net ./example/cli.ts [--json] [--ignore-deprecated-col] [--subtaxa] [--server=] +``` + +#### JSON-Lines output +The example CLI also outputs information about the names in JSON-Lines format to *stderr* if given the `--json` flag. + +(Output to stdout is the same, human-readable output. Having JSON on stderr is mainly to avoid picking up other log messages from synolib, such as "Skipping known" and "failed fetch. Retrying") + +Example usage: +```sh +deno run --allow-net ./example/cli.ts --json --ignore-deprecated-col Sadayoshia acamar 2>&1 >/dev/null | jq +``` + +Format: Each line represents a name or authorized name, using the following fields: +```ts +type name = { + "name": string, + "taxonNameURI"?: string (url), + "colURI"?: string (url), + "acceptedColURI"?: string (url), + "treatments": { "treatment": string (url), "year": number, "kind": "aug"|"cite" }[], +} +type authorizedName = { + "name": string, + "taxonConceptURI"?: string (url), + "colURI"?: string (url), + "acceptedColURI"?: string (url), + "treatments": { "treatment": string (url), "year": number, "kind": "def"|"aug"|"dpr"|"cite" }[], +} +``` + +The array of treatments is sorted bythe year. + ### Web An example running in the browser is located in `example/index.html` and diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts index 39627d1..ee25288 100644 --- a/SparqlEndpoint.ts +++ b/SparqlEndpoint.ts @@ -69,7 +69,7 @@ export class SparqlEndpoint { throw error; } else if (retryCount < 10) { const wait = 50 * (1 << retryCount++); - console.warn(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); + console.info(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); await sleep(wait); return await sendRequest(); } diff --git a/deno.lock b/deno.lock index 90070a3..7149fd3 100644 --- a/deno.lock +++ b/deno.lock @@ -3,6 +3,7 @@ "specifiers": { "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0", "jsr:@std/bytes@^1.0.2": "1.0.2", + "jsr:@std/cli@*": "1.0.9", "jsr:@std/encoding@^1.0.5": "1.0.5", "jsr:@std/path@^1.0.6": "1.0.7", "npm:esbuild@0.24": "0.24.0" @@ -19,6 +20,9 @@ "@std/bytes@1.0.2": { "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" }, + "@std/cli@1.0.9": { + "integrity": "557e5865af000efbf3f737dcfea5b8ab86453594f4a9cd8d08c9fa83d8e3f3bc" + }, "@std/encoding@1.0.5": { "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" }, @@ -129,6 +133,9 @@ ] } }, + "remote": { + "https://deno.land/std@0.214.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb" + }, "workspace": { "dependencies": [ "jsr:@luca/esbuild-deno-loader@0.11", diff --git a/example/cli.ts b/example/cli.ts index 666f4c0..442f4b4 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,4 +1,6 @@ import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; +import { parseArgs } from "jsr:@std/cli"; + import { type Name, SparqlEndpoint, @@ -6,20 +8,35 @@ import { type Treatment, } from "../mod.ts"; -const HIDE_COL_ONLY_SYNONYMS = true; -const START_WITH_SUBTAXA = false; -const ENDPOINT_URL = "https://treatment.ld.plazi.org/sparql"; -// const ENDPOINT_URL = "https://lindas-cached.cluster.ldbar.ch/query"; // is missing some CoL-data +const args = parseArgs(Deno.args, { + boolean: ["json", "ignore-deprecated-col", "subtaxa"], + string: ["server", "q"], + negatable: ["ignore-deprecated-col"], + default: { + server: "https://treatment.ld.plazi.org/sparql", + }, +}); + +const taxonName = args.q || args._.join(" ") || + "https://www.catalogueoflife.org/data/taxon/3WD9M"; -const sparqlEndpoint = new SparqlEndpoint(ENDPOINT_URL); -const taxonName = Deno.args.length > 0 - ? Deno.args.join(" ") - : "https://www.catalogueoflife.org/data/taxon/3WD9M"; // "https://www.catalogueoflife.org/data/taxon/4P523"; +const encoder = new TextEncoder(); +function outputStderr(msg: string) { + const data = encoder.encode(msg + "\n"); + Deno.stderr.writeSync(data); +} + +console.log( + Colors.blue(`Synonym Group For ${taxonName}`) + + Colors.dim(` (Server: ${args.server})`), +); + +const sparqlEndpoint = new SparqlEndpoint(args.server); const synoGroup = new SynonymGroup( sparqlEndpoint, taxonName, - HIDE_COL_ONLY_SYNONYMS, - START_WITH_SUBTAXA, + args["ignore-deprecated-col"], + args.subtaxa, ); const trtColor = { @@ -29,10 +46,6 @@ const trtColor = { "cite": Colors.gray, }; -console.log(ENDPOINT_URL); - -console.log(Colors.blue(`Synonym Group For ${taxonName}`)); - let authorizedNamesCount = 0; const timeStart = performance.now(); @@ -40,14 +53,46 @@ for await (const name of synoGroup) { console.log( "\n" + Colors.underline(name.displayName) + - colorizeIfPresent(name.taxonNameURI, "yellow"), + colorizeIfPresent(name.taxonNameURI, "yellow") + + colorizeIfPresent(name.colURI, "yellow"), ); const vernacular = await name.vernacularNames; if (vernacular.size > 0) { console.log(" “" + [...vernacular.values()].join("”, “") + "”"); } + if (args.json) { + outputStderr(JSON.stringify({ + name: name.displayName, + taxonNameURI: name.taxonNameURI, + colURI: name.colURI, + acceptedColURI: name.acceptedColURI, + treatments: [ + ...name.treatments.treats.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "aug" }; + }), + ...name.treatments.cite.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "cite" }; + }), + ].sort((a, b) => a.year - b.year), + })); + } + await logJustification(name); + + if (name.colURI) { + if (name.acceptedColURI !== name.colURI) { + console.log( + ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ + trtColor.aug("●") + } ${Colors.cyan(name.acceptedColURI!)}`, + ); + } else { + console.log( + ` ${trtColor.aug("●")} Catalogue of Life`, + ); + } + } for (const trt of name.treatments.treats) await logTreatment(trt, "aug"); for (const trt of name.treatments.cite) await logTreatment(trt, "cite"); @@ -62,6 +107,30 @@ for await (const name of synoGroup) { colorizeIfPresent(authorizedName.taxonConceptURI, "yellow") + colorizeIfPresent(authorizedName.colURI, "cyan"), ); + + if (args.json) { + outputStderr(JSON.stringify({ + name: authorizedName.displayName + " " + authorizedName.authority, + taxonNameURI: authorizedName.taxonConceptURI, + colURI: authorizedName.colURI, + acceptedColURI: authorizedName.acceptedColURI, + treatments: [ + ...authorizedName.treatments.def.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "def" }; + }), + ...authorizedName.treatments.aug.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "aug" }; + }), + ...authorizedName.treatments.dpr.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "dpr" }; + }), + ...authorizedName.treatments.cite.values().map((trt) => { + return { treatment: trt.url, year: trt.date ?? 0, kind: "cite" }; + }), + ].sort((a, b) => a.year - b.year), + })); + } + if (authorizedName.colURI) { if (authorizedName.acceptedColURI !== authorizedName.colURI) { console.log( From a5d04adc27c7a0591062fd3450ff182abd662fc8 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:49:52 +0100 Subject: [PATCH 52/71] deduplicate authNames if multiple col-authorities found --- SynonymGroup.ts | 54 ++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index a78363d..85c2e97 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -330,6 +330,8 @@ LIMIT 500`; this.expanded.add(taxonNameURI); //, NameStatus.madeName); } + const expandedHere = new Set(); + for (const t of json.results.bindings) { if (t.col) { const colURI = t.col.value; @@ -347,17 +349,20 @@ LIMIT 500`; console.log("Skipping known", colURI); return; } - authorizedCoLNames.push({ - displayName, - authority: t.authority!.value, - colURI: t.col.value, - treatments: { - def: new Set(), - aug: new Set(), - dpr: new Set(), - cite: new Set(), - }, - }); + if (!expandedHere.has(colURI)) { + expandedHere.add(colURI); + authorizedCoLNames.push({ + displayName, + authority: t.authority!.value, + colURI: t.col.value, + treatments: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + cite: new Set(), + }, + }); + } } } @@ -380,20 +385,23 @@ LIMIT 500`; cite, }; } else if (this.expanded.has(t.tc.value)) { - // console.log("Skipping known", t.tc.value); + console.log("Skipping known", t.tc.value); return; } else { - authorizedTCNames.push({ - displayName, - authority: t.tcAuth.value, - taxonConceptURI: t.tc.value, - treatments: { - def, - aug, - dpr, - cite, - }, - }); + if (!expandedHere.has(t.tc.value)) { + expandedHere.add(t.tc.value); + authorizedTCNames.push({ + displayName, + authority: t.tcAuth.value, + taxonConceptURI: t.tc.value, + treatments: { + def, + aug, + dpr, + cite, + }, + }); + } } def.forEach((t) => treatmentPromises.push(t)); From b2b8bd782d9ec99feb5a4843b40d44e847a079bd Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:52:24 +0100 Subject: [PATCH 53/71] unify authoritiy names if they are obviously equivalent e.g. abbreviated, missing year or missing diacritica --- README.md | 17 ++++-- SynonymGroup.ts | 96 ++++++++++++++++++++-------------- UnifyAuthorities.ts | 75 ++++++++++++++++++++++++++ deno.lock | 11 ++++ example/cli.ts | 14 ++++- tests/UnifyAuthorities_test.ts | 58 ++++++++++++++++++++ 6 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 UnifyAuthorities.ts create mode 100644 tests/UnifyAuthorities_test.ts diff --git a/README.md b/README.md index 8a3d0c5..28c4350 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ such synonymity and treatments about these taxon names or the respective taxa. For a command line example using the library see: `example/cli.ts`. You can try it locally using Deno with + ```sh deno run --allow-net ./example/cli.ts Ludwigia adscendens # or @@ -23,21 +24,29 @@ deno run --allow-net ./example/cli.ts https://www.catalogueoflife.org/data/taxon (replace the argument with whatever name interests you) Params: + ``` deno run --allow-net ./example/cli.ts [--json] [--ignore-deprecated-col] [--subtaxa] [--server=] ``` #### JSON-Lines output -The example CLI also outputs information about the names in JSON-Lines format to *stderr* if given the `--json` flag. -(Output to stdout is the same, human-readable output. Having JSON on stderr is mainly to avoid picking up other log messages from synolib, such as "Skipping known" and "failed fetch. Retrying") +The example CLI also outputs information about the names in JSON-Lines format to +_stderr_ if given the `--json` flag. + +(Output to stdout is the same, human-readable output. Having JSON on stderr is +mainly to avoid picking up other log messages from synolib, such as "Skipping +known" and "failed fetch. Retrying") Example usage: + ```sh deno run --allow-net ./example/cli.ts --json --ignore-deprecated-col Sadayoshia acamar 2>&1 >/dev/null | jq ``` -Format: Each line represents a name or authorized name, using the following fields: +Format: Each line represents a name or authorized name, using the following +fields: + ```ts type name = { "name": string, @@ -48,7 +57,7 @@ type name = { } type authorizedName = { "name": string, - "taxonConceptURI"?: string (url), + "taxonConceptURIs": string[], // urls -- possibly empty "colURI"?: string (url), "acceptedColURI"?: string (url), "treatments": { "treatment": string (url), "year": number, "kind": "def"|"aug"|"dpr"|"cite" }[], diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 85c2e97..d7ff381 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,5 +1,6 @@ import type { SparqlEndpoint, SparqlJson } from "./mod.ts"; import * as Queries from "./Queries.ts"; +import { unifyAuthorithy } from "./UnifyAuthorities.ts"; /** Finds all synonyms of a taxon */ export class SynonymGroup implements AsyncIterable { @@ -127,7 +128,7 @@ export class SynonymGroup implements AsyncIterable { break; } const an = n.authorizedNames.find((an) => - an.taxonConceptURI === uri || an.colURI === uri + an.colURI === uri || an.taxonConceptURIs.includes(uri) ); if (an) { name = an; @@ -144,7 +145,7 @@ export class SynonymGroup implements AsyncIterable { return; } const an = n.authorizedNames.find((an) => - an.taxonConceptURI === uri || an.colURI === uri + an.colURI === uri || an.taxonConceptURIs.includes(uri) ); if (an) { resolve(an); @@ -321,8 +322,7 @@ LIMIT 500`; let unathorizedCol: string | undefined; // there can be multiple CoL-taxa with same latin name, e.g. Leontopodium alpinum has 3T6ZY and 3T6ZX. - const authorizedCoLNames: AuthorizedName[] = []; - const authorizedTCNames: AuthorizedName[] = []; + const authorizedNames: AuthorizedName[] = []; const taxonNameURI = json.results.bindings[0].tn?.value; if (taxonNameURI) { @@ -344,17 +344,22 @@ LIMIT 500`; console.log("Duplicate unathorized COL:", unathorizedCol, colURI); } unathorizedCol = colURI; - } else if (!authorizedCoLNames.find((e) => e.colURI === colURI)) { + } else if (!authorizedNames.find((e) => e.colURI === colURI)) { if (this.expanded.has(colURI)) { console.log("Skipping known", colURI); return; } if (!expandedHere.has(colURI)) { expandedHere.add(colURI); - authorizedCoLNames.push({ + // TODO: handle unification of names + // might not be neccessary, assuming all CoL-taxa are non-unifiable and + // they are always handled first + authorizedNames.push({ displayName, authority: t.authority!.value, + authorities: [t.authority!.value], colURI: t.col.value, + taxonConceptURIs: [], treatments: { def: new Set(), aug: new Set(), @@ -367,33 +372,48 @@ LIMIT 500`; } if (t.tc && t.tcAuth && t.tcAuth.value) { - const def = this.makeTreatmentSet(t.defs?.value.split("|")); - const aug = this.makeTreatmentSet(t.augs?.value.split("|")); - const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); - const cite = this.makeTreatmentSet(t.cites?.value.split("|")); - - const colName = authorizedCoLNames.find((e) => - t.tcAuth!.value.split(" / ").includes(e.authority) - ); - if (colName) { - colName.authority = t.tcAuth?.value; - colName.taxonConceptURI = t.tc.value; - colName.treatments = { - def, - aug, - dpr, - cite, - }; - } else if (this.expanded.has(t.tc.value)) { + if (this.expanded.has(t.tc.value)) { console.log("Skipping known", t.tc.value); return; - } else { - if (!expandedHere.has(t.tc.value)) { - expandedHere.add(t.tc.value); - authorizedTCNames.push({ + } else if (!expandedHere.has(t.tc.value)) { + expandedHere.add(t.tc.value); + + const def = this.makeTreatmentSet(t.defs?.value.split("|")); + const aug = this.makeTreatmentSet(t.augs?.value.split("|")); + const dpr = this.makeTreatmentSet(t.dprs?.value.split("|")); + const cite = this.makeTreatmentSet(t.cites?.value.split("|")); + + def.forEach((t) => treatmentPromises.push(t)); + aug.forEach((t) => treatmentPromises.push(t)); + dpr.forEach((t) => treatmentPromises.push(t)); + + const prevName = authorizedNames.find((e) => + unifyAuthorithy(e.authority, t.tcAuth!.value) !== null + // t.tcAuth!.value.split(" / ").some((auth) => + // unifyAuthorithy(e.authority, auth) !== null + // ) + ); + if (prevName) { + // TODO: I feel like this could be made much more efficient -- we are unifying repeatedly + const best = t.tcAuth!.value; // .split(" / ").find((auth) => + // unifyAuthorithy(prevName.authority, auth) !== null + // )!; + + prevName.authority = unifyAuthorithy(prevName.authority, best)!; + prevName.authorities.push(...t.tcAuth.value.split(" / ")); + prevName.taxonConceptURIs.push(t.tc.value); + prevName.treatments = { + def: prevName.treatments.def.union(def), + aug: prevName.treatments.aug.union(aug), + dpr: prevName.treatments.dpr.union(dpr), + cite: prevName.treatments.cite.union(cite), + }; + } else { + authorizedNames.push({ displayName, authority: t.tcAuth.value, - taxonConceptURI: t.tc.value, + authorities: t.tcAuth.value.split(" / "), + taxonConceptURIs: [t.tc.value], treatments: { def, aug, @@ -403,10 +423,6 @@ LIMIT 500`; }); } } - - def.forEach((t) => treatmentPromises.push(t)); - aug.forEach((t) => treatmentPromises.push(t)); - dpr.forEach((t) => treatmentPromises.push(t)); } } @@ -420,7 +436,7 @@ LIMIT 500`; displayName, rank: json.results.bindings[0].rank!.value, taxonNameURI, - authorizedNames: [...authorizedCoLNames, ...authorizedTCNames], + authorizedNames: authorizedNames, colURI: unathorizedCol, justification, treatments: { @@ -436,7 +452,7 @@ LIMIT 500`; for (const authName of name.authorizedNames) { if (authName.colURI) this.expanded.add(authName.colURI); - if (authName.taxonConceptURI) this.expanded.add(authName.taxonConceptURI); + for (const tc of authName.taxonConceptURIs) this.expanded.add(tc); } const colPromises: Promise[] = []; @@ -451,7 +467,7 @@ LIMIT 500`; } await Promise.all( - authorizedCoLNames.map(async (n) => { + authorizedNames.map(async (n) => { const [acceptedColURI, promises] = await this.getAcceptedCol( n.colURI!, name, @@ -917,9 +933,13 @@ export type AuthorizedName = { displayName: string; /** Human-readable authority */ authority: string; + /** + * Human-readable authorities as given in the Data. + */ + authorities: string[]; - /** The URI of the respective `dwcFP:TaxonConcept` if it exists */ - taxonConceptURI?: string; + /** The URIs of the respective `dwcFP:TaxonConcept` if it exists */ + taxonConceptURIs: string[]; /** The URI of the respective CoL-taxon if it exists */ colURI?: string; diff --git a/UnifyAuthorities.ts b/UnifyAuthorities.ts new file mode 100644 index 0000000..03735bb --- /dev/null +++ b/UnifyAuthorities.ts @@ -0,0 +1,75 @@ +export function unifyAuthorithy(a: string, b: string): string | null { + const as = a.split(/\s*[,]\s*/); + const bs = b.split(/\s*[,]\s*/); + const yearA = (as.length > 0 && /\d{4}/.test(as.at(-1)!)) ? as.pop()! : null; + const yearB = (bs.length > 0 && /\d{4}/.test(bs.at(-1)!)) ? bs.pop()! : null; + const etalA = as.length > 0 && /\s*et\.?\s*al\.?/.test(as.at(-1)!); + const etalB = bs.length > 0 && /\s*et\.?\s*al\.?/.test(bs.at(-1)!); + if (etalA) { + as[as.length - 1] = as[as.length - 1].replace(/\s*et\.?\s*al\.?/, ""); + } + if (etalB) { + bs[bs.length - 1] = bs[bs.length - 1].replace(/\s*et\.?\s*al\.?/, ""); + } + + if (!etalA && !etalB && as.length != bs.length) return null; + + const result: string[] = []; + let i = 0; + for (; i < as.length && i < bs.length; i++) { + const r = unifySingleName(as[i], bs[i]); + if (r !== null) result.push(r); + else return null; + } + for (let j = i; j < as.length; j++) { + if (as[j]) result.push(as[j]); + } + for (let j = i; j < bs.length; j++) { + if (bs[j]) result.push(bs[j]); + } + + if (yearA && yearB) { + if (yearA === yearB) result.push(yearA); + else return null; + } else if (yearA) { + result.push(yearA); + } else if (yearB) { + result.push(yearB); + } + + return result.join(", "); +} + +function unifySingleName(a: string, b: string) { + let prefixA = a.replaceAll("-", " "); + let prefixB = b.replaceAll("-", " "); + if (prefixA.endsWith(".") || prefixB.endsWith(".")) { + // might be abbreviation + // normalize to get compatible string lengths + const longA = prefixA.normalize("NFKC"); + const longB = prefixB.normalize("NFKC"); + const indexA = longA.lastIndexOf("."); + const indexB = longB.lastIndexOf("."); + const index = indexA !== -1 + ? (indexB !== -1 ? Math.min(indexA, indexB) : indexA) + : indexB; + prefixA = longA.substring(0, index); + prefixB = longB.substring(0, index); + } + + if (isEquivalent(prefixA, prefixB)) { + // normalize such that accents are represented with combining characters + // so that the version with accents is longer + const normA = a.normalize("NFD"); + const normB = b.normalize("NFD"); + return normA.length >= normB.length ? a : b; + } + return null; +} + +function isEquivalent(a: string, b: string): boolean { + return a.localeCompare(b, "en", { + sensitivity: "base", // a = ä, A = a, a ≠ b + usage: "search", + }) === 0; +} diff --git a/deno.lock b/deno.lock index 7149fd3..1b105c0 100644 --- a/deno.lock +++ b/deno.lock @@ -2,9 +2,11 @@ "version": "4", "specifiers": { "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0", + "jsr:@std/assert@*": "1.0.9", "jsr:@std/bytes@^1.0.2": "1.0.2", "jsr:@std/cli@*": "1.0.9", "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/path@^1.0.6": "1.0.7", "npm:esbuild@0.24": "0.24.0" }, @@ -17,6 +19,12 @@ "jsr:@std/path" ] }, + "@std/assert@1.0.9": { + "integrity": "a9f0c611a869cc791b26f523eec54c7e187aab7932c2c8e8bea0622d13680dcd", + "dependencies": [ + "jsr:@std/internal" + ] + }, "@std/bytes@1.0.2": { "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" }, @@ -26,6 +34,9 @@ "@std/encoding@1.0.5": { "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + }, "@std/path@1.0.7": { "integrity": "76a689e07f0e15dcc6002ec39d0866797e7156629212b28f27179b8a5c3b33a1" } diff --git a/example/cli.ts b/example/cli.ts index 442f4b4..981b7db 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -104,14 +104,24 @@ for await (const name of synoGroup) { authorizedName.displayName + " " + Colors.italic(authorizedName.authority), ) + - colorizeIfPresent(authorizedName.taxonConceptURI, "yellow") + + colorizeIfPresent(authorizedName.taxonConceptURIs.join(), "yellow") + colorizeIfPresent(authorizedName.colURI, "cyan"), ); + const auths = authorizedName.authorities.filter((auth) => + auth != authorizedName.authority + ); + if (auths.length > 1) { + console.log( + Colors.dim( + ` (Authority also given as: “${auths.join("”, “")}”)`, + ), + ); + } if (args.json) { outputStderr(JSON.stringify({ name: authorizedName.displayName + " " + authorizedName.authority, - taxonNameURI: authorizedName.taxonConceptURI, + taxonNameURIs: authorizedName.taxonConceptURIs, colURI: authorizedName.colURI, acceptedColURI: authorizedName.acceptedColURI, treatments: [ diff --git a/tests/UnifyAuthorities_test.ts b/tests/UnifyAuthorities_test.ts new file mode 100644 index 0000000..fd6a633 --- /dev/null +++ b/tests/UnifyAuthorities_test.ts @@ -0,0 +1,58 @@ +import { unifyAuthorithy } from "../UnifyAuthorities.ts"; +import { assertEquals } from "jsr:@std/assert"; + +const unify_tests: [string, string, string][] = [ + ["Bolívar, 1893", "Bolivar, 1893", "Bolívar, 1893"], + ["García-Aldrete, 2009", "Garcia Aldrete, 2009", "García-Aldrete, 2009"], + ["Kulczyński, 1901", "Kulczynski, 1901", "Kulczyński, 1901"], + ["(Kulczyński, 1903)", "(Kulczynski, 1903)", "(Kulczyński, 1903)"], + ["Mello-Leitão, 1942", "Mello-Leitao, 1942", "Mello-Leitão, 1942"], + // ["Quatrefages, 1842", "de Quatrefages, 1842", "Quatrefages, 1842"], + // ["(Linnaeus, 1753) Linnaeus, 1763", "(L.) L.", "(Linnaeus, 1753) Linnaeus, 1763"], + // ["(Linnaeus, 1753)", "(L.)", "(Linnaeus, 1753)"], + ["Linnaeus", "L.", "Linnaeus"], + [ + "Bakker et al., 1988", + "Bakker, Williams & Currie, 1988", + "Bakker, Williams & Currie, 1988", + ], + + // TODO + // debatable if these should unify -- how to handle cases like "Gorgosaurus" where the same authority is given with different years: how would a hypothetical third authority without year unify? + ["(Cass.) Greuter, 1791", "(Cass.) Greuter", "(Cass.) Greuter, 1791"], + // ["(Linnaeus) Linnaeus", "(L., 1753) L., 1763", "(Linnaeus, 1753) Linnaeus, 1763"], + ["Osborn, 1905", "Osborn", "Osborn, 1905"], + [ + "Bakker et al., 1988", + "Bakker, Williams & Currie", + "Bakker, Williams & Currie, 1988", + ], + [ + "Bakker et al.", + "Bakker, Williams & Currie, 1988", + "Bakker, Williams & Currie, 1988", + ], +]; + +const incompatible_tests: [string, string][] = [ + ["(Bolívar, 1893)", "Bolivar, 1893"], + ["Simon, 1890", "(Simon, 1890)"], +]; + +for (const test of unify_tests) { + Deno.test({ + name: `Unify '${test[0]}','${test[1]}' → '${test[2]}'`, + fn() { + assertEquals(unifyAuthorithy(test[0], test[1]), test[2]); + }, + }); +} + +for (const test of incompatible_tests) { + Deno.test({ + name: `No-Unify '${test[0]}','${test[1]}'`, + fn() { + assertEquals(unifyAuthorithy(test[0], test[1]), null); + }, + }); +} From aa6f0bfb9ac8582969936efbd18e9c8de056ddb0 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:54:10 +0100 Subject: [PATCH 54/71] v3.2.0 --- deno.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 21eb719..5de2be6 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,9 +1,9 @@ { "name": "@plazi/synolib", - "version": "3.1.1", + "version": "3.2.0", "exports": "./mod.ts", "publish": { - "include": ["./*.ts", "README.md", "LICENSE"], + "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], "exclude": ["./build_example.ts"] }, "compilerOptions": { From c258f9497bae1b8a0549aa41ba24f556ae10a399 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:05:16 +0100 Subject: [PATCH 55/71] v3.2.1 small fix to cli script --- deno.jsonc | 2 +- deno.lock | 7 ++++--- example/cli.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 5de2be6..cc428e8 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.2.0", + "version": "3.2.1", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], diff --git a/deno.lock b/deno.lock index 1b105c0..15f5acb 100644 --- a/deno.lock +++ b/deno.lock @@ -6,6 +6,7 @@ "jsr:@std/bytes@^1.0.2": "1.0.2", "jsr:@std/cli@*": "1.0.9", "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/fmt@0.214.0": "0.214.0", "jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/path@^1.0.6": "1.0.7", "npm:esbuild@0.24": "0.24.0" @@ -34,6 +35,9 @@ "@std/encoding@1.0.5": { "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" }, + "@std/fmt@0.214.0": { + "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" + }, "@std/internal@1.0.5": { "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" }, @@ -144,9 +148,6 @@ ] } }, - "remote": { - "https://deno.land/std@0.214.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb" - }, "workspace": { "dependencies": [ "jsr:@luca/esbuild-deno-loader@0.11", diff --git a/example/cli.ts b/example/cli.ts index 981b7db..ec94ff4 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -1,4 +1,4 @@ -import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; +import * as Colors from "jsr:@std/fmt@0.214.0/colors"; import { parseArgs } from "jsr:@std/cli"; import { From f9d0b187079d7ec231bb43fc1e2b2e1642c7811e Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:38:18 +0100 Subject: [PATCH 56/71] fixed stray "INVALID COL" appearing --- SynonymGroup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index d7ff381..5d64fde 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -467,7 +467,7 @@ LIMIT 500`; } await Promise.all( - authorizedNames.map(async (n) => { + authorizedNames.filter((n) => n.colURI).map(async (n) => { const [acceptedColURI, promises] = await this.getAcceptedCol( n.colURI!, name, From 9d1efbb85afccb711de5d16a63bfb27709a94e71 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:28:33 +0100 Subject: [PATCH 57/71] v3.2.2 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index cc428e8..4faaaf5 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.2.1", + "version": "3.2.2", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From ce1046312c1e881dadf436258169f7b28970e0e9 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:35:37 +0100 Subject: [PATCH 58/71] acceptedCOL is a promise now --- SynonymGroup.ts | 42 ++++++++++++++++++++++-------------------- example/cli.ts | 10 ++++++---- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 5d64fde..03f38c6 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -456,28 +456,28 @@ LIMIT 500`; } const colPromises: Promise[] = []; + const authorizedColPromises: Promise[] = authorizedNames + .filter((n) => n.colURI) + .map((n) => { + n.acceptedColURI = this.getAcceptedCol(n.colURI!, name).then( + ([acceptedColURI, promises]) => { + colPromises.push(...promises); + return acceptedColURI; + }, + ); + return n.acceptedColURI; + }); if (unathorizedCol) { - const [acceptedColURI, promises] = await this.getAcceptedCol( - unathorizedCol, - name, + name.acceptedColURI = this.getAcceptedCol(unathorizedCol, name).then( + ([acceptedColURI, promises]) => { + colPromises.push(...promises); + return acceptedColURI; + }, ); - name.acceptedColURI = acceptedColURI; - colPromises.push(...promises); + authorizedColPromises.push(name.acceptedColURI); } - await Promise.all( - authorizedNames.filter((n) => n.colURI).map(async (n) => { - const [acceptedColURI, promises] = await this.getAcceptedCol( - n.colURI!, - name, - ); - n.acceptedColURI = acceptedColURI; - colPromises.push(...promises); - }), - ); - - // TODO: make "acceptedCol" be a promise so we can move this above the this.getAcceptedCol-awaits, to show names sooner. this.pushName(name); /** Map */ @@ -505,12 +505,14 @@ LIMIT 500`; await Promise.allSettled( [ - ...colPromises, + ...authorizedColPromises, ...[...newSynonyms].map(([n, treatment]) => this.getName(n, { searchTerm: false, parent: name, treatment }) ), ], ); + // nedd to await after awaiting all authorizedColPromises as those will push colPromises + await Promise.allSettled(colPromises); } /** @internal */ @@ -890,7 +892,7 @@ export type Name = { * * May be the string "INVALID COL" if the colURI is not valid. */ - acceptedColURI?: string; + acceptedColURI?: Promise; /** All `AuthorizedName`s with this name */ authorizedNames: AuthorizedName[]; @@ -949,7 +951,7 @@ export type AuthorizedName = { * * May be the string "INVALID COL" if the colURI is not valid. */ - acceptedColURI?: string; + acceptedColURI?: Promise; // TODO: sensible? // /** these are CoL-taxa linked in the rdf, which differ lexically */ diff --git a/example/cli.ts b/example/cli.ts index ec94ff4..584b74a 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -81,11 +81,12 @@ for await (const name of synoGroup) { await logJustification(name); if (name.colURI) { - if (name.acceptedColURI !== name.colURI) { + const acceptedColURI = await name.acceptedColURI; + if (acceptedColURI !== name.colURI) { console.log( ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ trtColor.aug("●") - } ${Colors.cyan(name.acceptedColURI!)}`, + } ${Colors.cyan(acceptedColURI!)}`, ); } else { console.log( @@ -142,11 +143,12 @@ for await (const name of synoGroup) { } if (authorizedName.colURI) { - if (authorizedName.acceptedColURI !== authorizedName.colURI) { + const acceptedColURI = await authorizedName.acceptedColURI; + if (acceptedColURI !== authorizedName.colURI) { console.log( ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ trtColor.aug("●") - } ${Colors.cyan(authorizedName.acceptedColURI!)}`, + } ${Colors.cyan(acceptedColURI!)}`, ); } else { console.log( From aa40d7501e50b60330db0fd46f9533a11edfaf06 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:36:09 +0100 Subject: [PATCH 59/71] v3.3.0 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 4faaaf5..27403e7 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.2.2", + "version": "3.3.0", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From ffe68cba0eeed017ba9610d88fc90df1ec4738bc Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:37:25 +0100 Subject: [PATCH 60/71] handle sections --- Queries.ts | 15 ++++++++------- SynonymGroup.ts | 5 ++++- example/cli.ts | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Queries.ts b/Queries.ts index 575e6e6..8a2693c 100644 --- a/Queries.ts +++ b/Queries.ts @@ -8,7 +8,7 @@ PREFIX dwc: PREFIX dwcFP: PREFIX cito: PREFIX trt: -SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority +SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) @@ -23,9 +23,7 @@ SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ? * As its own variable to ensure consistency in the resturned bindings. */ const postamble = - `GROUP BY ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?species ?infrasp ?name ?authority`; - -// For unclear reasons, the queries breaks if the limit is removed. + `GROUP BY ?kingdom ?tn ?tc ?col ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority`; /** * Note: this query assumes that there is no sub-species taxa with missing dwc:species @@ -59,7 +57,8 @@ BIND(<${colUri}> as ?col) ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } - FILTER(?subgenus = COALESCE(?tnsubgenus, "")) + FILTER(?subgenus = COALESCE(?tnsubgenus, COALESCE(?section, ""))) + OPTIONAL { ?tn dwc:section ?section . } OPTIONAL { ?tn dwc:species ?tnspecies . } FILTER(?species = COALESCE(?tnspecies, "")) OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } @@ -116,6 +115,7 @@ export const getNameFromTC = (tcUri: string) => ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } + OPTIONAL { ?tn dwc:section ?section . } OPTIONAL { ?tn dwc:species ?tnspecies . OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } @@ -134,7 +134,7 @@ export const getNameFromTC = (tcUri: string) => FILTER(?kingdom = COALESCE(?colkingdom, "")) OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } - FILTER(?subgenus = COALESCE(?colsubgenus, "")) + FILTER(COALESCE(?tnsubgenus, COALESCE(?section, "")) = COALESCE(?colsubgenus, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } @@ -184,6 +184,7 @@ export const getNameFromTN = (tnUri: string) => ?tn dwc:genus ?genus . ?tn dwc:kingdom ?kingdom . OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } + OPTIONAL { ?tn dwc:section ?section . } OPTIONAL { ?tn dwc:species ?tnspecies . OPTIONAL { ?tn dwc:subSpecies|dwc:variety|dwc:form ?tninfrasp . } @@ -202,7 +203,7 @@ export const getNameFromTN = (tnUri: string) => FILTER(?kingdom = COALESCE(?colkingdom, "")) OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } - FILTER(?subgenus = COALESCE(?colsubgenus, "")) + FILTER(COALESCE(?tnsubgenus, COALESCE(?section, "")) = COALESCE(?colsubgenus, "")) OPTIONAL { ?col dwc:specificEpithet ?colspecies . } FILTER(?species = COALESCE(?colspecies, "")) OPTIONAL { ?col dwc:infraspecificEpithet ?colinfrasp . } diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 03f38c6..fdf0b40 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -73,7 +73,7 @@ export class SynonymGroup implements AsyncIterable { /** * if set to true, subTaxa of the search term are also considered as starting points. * - * Not that "weird" ranks like subGenus are always included when searching for a genus by latin name. + * Note that "intermediate" ranks like subGenus and section are always included when searching for a genus by latin name. */ startWithSubTaxa: boolean; @@ -306,6 +306,9 @@ LIMIT 500`; : json.results.bindings[0].name.value ) : json.results.bindings[0].genus!.value + + (json.results.bindings[0].section?.value + ? ` sect. ${json.results.bindings[0].section.value}` + : "") + (json.results.bindings[0].subgenus?.value ? ` (${json.results.bindings[0].subgenus.value})` : "") + diff --git a/example/cli.ts b/example/cli.ts index 584b74a..ca6f647 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -52,7 +52,7 @@ const timeStart = performance.now(); for await (const name of synoGroup) { console.log( "\n" + - Colors.underline(name.displayName) + + Colors.bold(Colors.underline(name.displayName)) + colorizeIfPresent(name.taxonNameURI, "yellow") + colorizeIfPresent(name.colURI, "yellow"), ); From 519fc39fd213bf7f3ea1d542643807a2189eb5ac Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:38:10 +0100 Subject: [PATCH 61/71] v3.3.1 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 27403e7..420b55d 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.3.0", + "version": "3.3.1", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From bfe62f8a8cd043d50687065778159e6ce2e3c62b Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:20:31 +0100 Subject: [PATCH 62/71] col is no longer a promise --- Queries.ts | 25 +++++++++- SynonymGroup.ts | 121 ++++++++++++++++++++++------------------------- example/cli.ts | 28 +++++------ example/index.ts | 40 ++++++++-------- 4 files changed, 115 insertions(+), 99 deletions(-) diff --git a/Queries.ts b/Queries.ts index 8a2693c..f11cb00 100644 --- a/Queries.ts +++ b/Queries.ts @@ -8,7 +8,7 @@ PREFIX dwc: PREFIX dwcFP: PREFIX cito: PREFIX trt: -SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority +SELECT DISTINCT ?kingdom ?tn ?tc ?col ?acceptedcol ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority (group_concat(DISTINCT ?tcauth;separator=" / ") AS ?tcAuth) (group_concat(DISTINCT ?aug;separator="|") as ?augs) (group_concat(DISTINCT ?def;separator="|") as ?defs) @@ -23,7 +23,7 @@ SELECT DISTINCT ?kingdom ?tn ?tc ?col ?rank ?genus ?section ?subgenus ?species ? * As its own variable to ensure consistency in the resturned bindings. */ const postamble = - `GROUP BY ?kingdom ?tn ?tc ?col ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority`; + `GROUP BY ?kingdom ?tn ?tc ?col ?acceptedcol ?rank ?genus ?section ?subgenus ?species ?infrasp ?name ?authority`; /** * Note: this query assumes that there is no sub-species taxa with missing dwc:species @@ -36,6 +36,13 @@ BIND(<${colUri}> as ?col) ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . ?col dwc:genericName ?genus . + { + ?col dwc:acceptedName ?acceptedcol . + } UNION { + ?col dwc:taxonomicStatus ?col_status . # TODO: unused + FILTER NOT EXISTS { ?col dwc:acceptedName ?_ . } + BIND(?col AS ?acceptedcol) + } OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } OPTIONAL { @@ -130,6 +137,13 @@ export const getNameFromTC = (tcUri: string) => ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . + { + ?col dwc:acceptedName ?acceptedcol . + } UNION { + ?col dwc:taxonomicStatus ?col_status . # TODO: unused + FILTER NOT EXISTS { ?col dwc:acceptedName ?_ . } + BIND(?col AS ?acceptedcol) + } OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } FILTER(?kingdom = COALESCE(?colkingdom, "")) @@ -199,6 +213,13 @@ export const getNameFromTN = (tnUri: string) => ?col dwc:taxonRank ?rank . ?col dwc:scientificName ?name . # Note: contains authority ?col dwc:genericName ?genus . + { + ?col dwc:acceptedName ?acceptedcol . + } UNION { + ?col dwc:taxonomicStatus ?col_status . # TODO: unused + FILTER NOT EXISTS { ?col dwc:acceptedName ?_ . } + BIND(?col AS ?acceptedcol) + } OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } FILTER(?kingdom = COALESCE(?colkingdom, "")) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index fdf0b40..c146db7 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -123,12 +123,12 @@ export class SynonymGroup implements AsyncIterable { findName(uri: string): Promise { let name: Name | AuthorizedName | undefined; for (const n of this.names) { - if (n.taxonNameURI === uri || n.colURI === uri) { + if (n.taxonNameURI === uri || n.col?.colURI === uri) { name = n; break; } const an = n.authorizedNames.find((an) => - an.colURI === uri || an.taxonConceptURIs.includes(uri) + an.col?.colURI === uri || an.taxonConceptURIs.includes(uri) ); if (an) { name = an; @@ -140,12 +140,12 @@ export class SynonymGroup implements AsyncIterable { this.monitor.addEventListener("updated", () => { if (this.names.length === 0 || this.isFinished) reject(); const n = this.names.at(-1)!; - if (n.taxonNameURI === uri || n.colURI === uri) { + if (n.taxonNameURI === uri || n.col?.colURI === uri) { resolve(n); return; } const an = n.authorizedNames.find((an) => - an.colURI === uri || an.taxonConceptURIs.includes(uri) + an.col?.colURI === uri || an.taxonConceptURIs.includes(uri) ); if (an) { resolve(an); @@ -322,7 +322,7 @@ LIMIT 500`; : "")).trim(); // Case where the CoL-taxon has no authority. There should only be one of these. - let unathorizedCol: string | undefined; + let unathorizedCol: { colURI: string; acceptedURI: string } | undefined; // there can be multiple CoL-taxa with same latin name, e.g. Leontopodium alpinum has 3T6ZY and 3T6ZX. const authorizedNames: AuthorizedName[] = []; @@ -343,11 +343,14 @@ LIMIT 500`; console.log("Skipping known", colURI); return; } - if (unathorizedCol && unathorizedCol !== colURI) { + if (unathorizedCol && unathorizedCol.colURI !== colURI) { console.log("Duplicate unathorized COL:", unathorizedCol, colURI); } - unathorizedCol = colURI; - } else if (!authorizedNames.find((e) => e.colURI === colURI)) { + unathorizedCol = { + colURI, + acceptedURI: t.acceptedcol?.value ?? "INVALID COL", + }; + } else if (!authorizedNames.find((e) => e.col?.colURI === colURI)) { if (this.expanded.has(colURI)) { console.log("Skipping known", colURI); return; @@ -361,7 +364,10 @@ LIMIT 500`; displayName, authority: t.authority!.value, authorities: [t.authority!.value], - colURI: t.col.value, + col: { + colURI: t.col.value, + acceptedURI: t.acceptedcol?.value ?? "INAVLID COL", + }, taxonConceptURIs: [], treatments: { def: new Set(), @@ -440,7 +446,7 @@ LIMIT 500`; rank: json.results.bindings[0].rank!.value, taxonNameURI, authorizedNames: authorizedNames, - colURI: unathorizedCol, + col: unathorizedCol, justification, treatments: { treats, @@ -454,33 +460,10 @@ LIMIT 500`; }; for (const authName of name.authorizedNames) { - if (authName.colURI) this.expanded.add(authName.colURI); + if (authName.col) this.expanded.add(authName.col.colURI); for (const tc of authName.taxonConceptURIs) this.expanded.add(tc); } - const colPromises: Promise[] = []; - const authorizedColPromises: Promise[] = authorizedNames - .filter((n) => n.colURI) - .map((n) => { - n.acceptedColURI = this.getAcceptedCol(n.colURI!, name).then( - ([acceptedColURI, promises]) => { - colPromises.push(...promises); - return acceptedColURI; - }, - ); - return n.acceptedColURI; - }); - - if (unathorizedCol) { - name.acceptedColURI = this.getAcceptedCol(unathorizedCol, name).then( - ([acceptedColURI, promises]) => { - colPromises.push(...promises); - return acceptedColURI; - }, - ); - authorizedColPromises.push(name.acceptedColURI); - } - this.pushName(name); /** Map */ @@ -506,23 +489,27 @@ LIMIT 500`; ); }); + if (unathorizedCol) { + await this.findColSynonyms(unathorizedCol.colURI, name); + } + await Promise.allSettled( [ - ...authorizedColPromises, + ...authorizedNames + .filter((n) => n.col) + .map((n) => this.findColSynonyms(n.col!.colURI, name)), ...[...newSynonyms].map(([n, treatment]) => this.getName(n, { searchTerm: false, parent: name, treatment }) ), ], ); - // nedd to await after awaiting all authorizedColPromises as those will push colPromises - await Promise.allSettled(colPromises); } /** @internal */ - private async getAcceptedCol( + private async findColSynonyms( colUri: string, parent: Name, - ): Promise<[string, Promise[]]> { + ): Promise { const query = ` PREFIX dwc: SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator="|") AS ?dprs) WHERE { @@ -541,7 +528,8 @@ SELECT DISTINCT ?current ?current_status (GROUP_CONCAT(DISTINCT ?dpr; separator= GROUP BY ?current ?current_status`; if (this.acceptedCol.has(colUri)) { - return [this.acceptedCol.get(colUri)!, []]; + // we have already found this group of synonyms + return []; } const json = await this.sparqlEndpoint.getSparqlResultSet( @@ -581,11 +569,11 @@ GROUP BY ?current ?current_status`; if (!this.acceptedCol.has(colUri)) { this.acceptedCol.set(colUri, "INVALID COL"); } - return [this.acceptedCol.get(colUri)!, promises]; + return Promise.all(promises); } if (!this.acceptedCol.has(colUri)) this.acceptedCol.set(colUri, colUri); - return [this.acceptedCol.get(colUri)!, promises]; + return Promise.all(promises); } /** @internal */ @@ -883,19 +871,21 @@ export type Name = { /** The URI of the respective `dwcFP:TaxonName` if it exists */ taxonNameURI?: string; - /** - * The URI of the respective CoL-taxon if it exists - * - * Not that this is only for CoL-taxa which do not have an authority. - */ - colURI?: string; - /** The URI of the corresponding accepted CoL-taxon if it exists. - * - * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon. - * - * May be the string "INVALID COL" if the colURI is not valid. - */ - acceptedColURI?: Promise; + /** Catalogue of Life-Data */ + col?: { + /** The URI of the respective CoL-taxon if it exists + * + * Note that this is only for CoL-taxa which do not have an authority. + */ + colURI: string; + /** The URI of the corresponding accepted CoL-taxon if it exists. + * + * The same as URI if it is the accepted CoL-Taxon. + * + * May be the string "INVALID COL" if the colURI is not valid. + */ + acceptedURI: string; + }; /** All `AuthorizedName`s with this name */ authorizedNames: AuthorizedName[]; @@ -946,15 +936,18 @@ export type AuthorizedName = { /** The URIs of the respective `dwcFP:TaxonConcept` if it exists */ taxonConceptURIs: string[]; - /** The URI of the respective CoL-taxon if it exists */ - colURI?: string; - /** The URI of the corresponding accepted CoL-taxon if it exists. - * - * Always present if colURI is present, they are the same if it is the accepted CoL-Taxon. - * - * May be the string "INVALID COL" if the colURI is not valid. - */ - acceptedColURI?: Promise; + /** Catalogue of Life-Data */ + col?: { + /** The URI of the respective CoL-taxon if it exists */ + colURI: string; + /** The URI of the corresponding accepted CoL-taxon if it exists. + * + * The same as URI if it is the accepted CoL-Taxon. + * + * May be the string "INVALID COL" if the colURI is not valid. + */ + acceptedURI: string; + }; // TODO: sensible? // /** these are CoL-taxa linked in the rdf, which differ lexically */ diff --git a/example/cli.ts b/example/cli.ts index ca6f647..e499e0f 100644 --- a/example/cli.ts +++ b/example/cli.ts @@ -54,7 +54,7 @@ for await (const name of synoGroup) { "\n" + Colors.bold(Colors.underline(name.displayName)) + colorizeIfPresent(name.taxonNameURI, "yellow") + - colorizeIfPresent(name.colURI, "yellow"), + colorizeIfPresent(name.col?.colURI, "yellow"), ); const vernacular = await name.vernacularNames; if (vernacular.size > 0) { @@ -65,8 +65,8 @@ for await (const name of synoGroup) { outputStderr(JSON.stringify({ name: name.displayName, taxonNameURI: name.taxonNameURI, - colURI: name.colURI, - acceptedColURI: name.acceptedColURI, + colURI: name.col?.colURI, + acceptedColURI: name.col?.acceptedURI, treatments: [ ...name.treatments.treats.values().map((trt) => { return { treatment: trt.url, year: trt.date ?? 0, kind: "aug" }; @@ -80,13 +80,13 @@ for await (const name of synoGroup) { await logJustification(name); - if (name.colURI) { - const acceptedColURI = await name.acceptedColURI; - if (acceptedColURI !== name.colURI) { + if (name.col) { + const acceptedColURI = name.col.acceptedURI; + if (acceptedColURI !== name.col.colURI) { console.log( ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ trtColor.aug("●") - } ${Colors.cyan(acceptedColURI!)}`, + } ${Colors.cyan(acceptedColURI)}`, ); } else { console.log( @@ -106,7 +106,7 @@ for await (const name of synoGroup) { Colors.italic(authorizedName.authority), ) + colorizeIfPresent(authorizedName.taxonConceptURIs.join(), "yellow") + - colorizeIfPresent(authorizedName.colURI, "cyan"), + colorizeIfPresent(authorizedName.col?.colURI, "cyan"), ); const auths = authorizedName.authorities.filter((auth) => auth != authorizedName.authority @@ -123,8 +123,8 @@ for await (const name of synoGroup) { outputStderr(JSON.stringify({ name: authorizedName.displayName + " " + authorizedName.authority, taxonNameURIs: authorizedName.taxonConceptURIs, - colURI: authorizedName.colURI, - acceptedColURI: authorizedName.acceptedColURI, + colURI: authorizedName.col?.colURI, + acceptedColURI: authorizedName.col?.acceptedURI, treatments: [ ...authorizedName.treatments.def.values().map((trt) => { return { treatment: trt.url, year: trt.date ?? 0, kind: "def" }; @@ -142,13 +142,13 @@ for await (const name of synoGroup) { })); } - if (authorizedName.colURI) { - const acceptedColURI = await authorizedName.acceptedColURI; - if (acceptedColURI !== authorizedName.colURI) { + if (authorizedName.col) { + const acceptedColURI = authorizedName.col.acceptedURI; + if (acceptedColURI !== authorizedName.col.colURI) { console.log( ` ${trtColor.dpr("●")} Catalogue of Life\n → ${ trtColor.aug("●") - } ${Colors.cyan(acceptedColURI!)}`, + } ${Colors.cyan(acceptedColURI)}`, ); } else { console.log( diff --git a/example/index.ts b/example/index.ts index 4a5aee4..ab389c9 100644 --- a/example/index.ts +++ b/example/index.ts @@ -366,23 +366,23 @@ class SynoName extends HTMLElement { const treatments = document.createElement("ul"); this.append(treatments); - if (name.colURI) { + if (name.col) { const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); - const id = name.colURI.replace( + const id = name.col.colURI.replace( "https://www.catalogueoflife.org/data/taxon/", "", ); col_uri.innerText = id; col_uri.id = id; - col_uri.href = name.colURI; + col_uri.href = name.col.colURI; col_uri.target = "_blank"; col_uri.innerHTML += icons.link; title.append(" ", col_uri); const li = document.createElement("div"); li.classList.add("treatmentline"); - li.innerHTML = name.acceptedColURI !== name.colURI + li.innerHTML = name.col.acceptedURI !== name.col.colURI ? icons.col_dpr : icons.col_aug; treatments.append(li); @@ -395,14 +395,14 @@ class SynoName extends HTMLElement { names.classList.add("indent"); li.append(names); - if (name.acceptedColURI !== name.colURI) { + if (name.col.acceptedURI !== name.col.colURI) { const line = document.createElement("div"); line.innerHTML = icons.east + icons.col_aug; names.append(line); const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); - const id = name.acceptedColURI!.replace( + const id = name.col.acceptedURI.replace( "https://www.catalogueoflife.org/data/taxon/", "", ); @@ -410,7 +410,7 @@ class SynoName extends HTMLElement { col_uri.href = `#${id}`; col_uri.title = "show name"; line.append(col_uri); - synoGroup.findName(name.acceptedColURI!).then((n) => { + synoGroup.findName(name.col.acceptedURI).then((n) => { if ((n as AuthorizedName).authority) { col_uri.innerText = n.displayName + " " + (n as AuthorizedName).authority; @@ -449,39 +449,41 @@ class SynoName extends HTMLElement { const treatments = document.createElement("ul"); this.append(treatments); - if (authorizedName.taxonConceptURI) { + if (authorizedName.taxonConceptURIs) { const name_uri = document.createElement("a"); name_uri.classList.add("taxon", "uri"); - const short = authorizedName.taxonConceptURI.replace( + // TODO handle other URIs + const short = authorizedName.taxonConceptURIs[0].replace( "http://taxon-concept.plazi.org/id/", "", ); name_uri.innerText = short; name_uri.id = short; - name_uri.href = authorizedName.taxonConceptURI; + name_uri.href = authorizedName.taxonConceptURIs[0]; name_uri.target = "_blank"; name_uri.innerHTML += icons.link; authName.append(" ", name_uri); } - if (authorizedName.colURI) { + if (authorizedName.col) { const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); - const id = authorizedName.colURI.replace( + const id = authorizedName.col.colURI.replace( "https://www.catalogueoflife.org/data/taxon/", "", ); col_uri.innerText = id; col_uri.id = id; - col_uri.href = authorizedName.colURI; + col_uri.href = authorizedName.col.colURI; col_uri.target = "_blank"; col_uri.innerHTML += icons.link; authName.append(" ", col_uri); const li = document.createElement("div"); li.classList.add("treatmentline"); - li.innerHTML = authorizedName.acceptedColURI !== authorizedName.colURI - ? icons.col_dpr - : icons.col_aug; + li.innerHTML = + authorizedName.col.acceptedURI !== authorizedName.col.colURI + ? icons.col_dpr + : icons.col_aug; treatments.append(li); const creators = document.createElement("span"); @@ -492,14 +494,14 @@ class SynoName extends HTMLElement { names.classList.add("indent"); li.append(names); - if (authorizedName.acceptedColURI !== authorizedName.colURI) { + if (authorizedName.col.acceptedURI !== authorizedName.col.colURI) { const line = document.createElement("div"); line.innerHTML = icons.east + icons.col_aug; names.append(line); const col_uri = document.createElement("a"); col_uri.classList.add("col", "uri"); - const id = authorizedName.acceptedColURI!.replace( + const id = authorizedName.col.acceptedURI.replace( "https://www.catalogueoflife.org/data/taxon/", "", ); @@ -507,7 +509,7 @@ class SynoName extends HTMLElement { col_uri.href = `#${id}`; col_uri.title = "show name"; line.append(" ", col_uri); - synoGroup.findName(authorizedName.acceptedColURI!).then((n) => { + synoGroup.findName(authorizedName.col.acceptedURI).then((n) => { col_uri.classList.remove("uri"); if ((n as AuthorizedName).authority) { col_uri.innerText = n.displayName + " " + From 967aa1fa02916285ec4fe749e6c533b7946fb92c Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:23:02 +0100 Subject: [PATCH 63/71] added automatic publish --- .github/workflows/publish.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7cafe8f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: Publish + +on: + push: + branches: + - synolib2 + +# runs on every push, does not publish unless deno.jsonc version number was updated + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # The OIDC ID token is used for authentication with JSR. + steps: + - uses: actions/checkout@v4 + - run: npx jsr publish \ No newline at end of file From d7ba2458d3d0a2842035afa9f1cb0497eefa605b Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:24:06 +0100 Subject: [PATCH 64/71] v3.4.0 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 420b55d..bcf0cbd 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.3.1", + "version": "3.4.0", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From 7203ee6d6721890626544f61044a8b1cff49e962 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:45:18 +0100 Subject: [PATCH 65/71] quick fix --- example/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/index.ts b/example/index.ts index ab389c9..0e32a2f 100644 --- a/example/index.ts +++ b/example/index.ts @@ -449,7 +449,7 @@ class SynoName extends HTMLElement { const treatments = document.createElement("ul"); this.append(treatments); - if (authorizedName.taxonConceptURIs) { + if (authorizedName.taxonConceptURIs[0]) { const name_uri = document.createElement("a"); name_uri.classList.add("taxon", "uri"); // TODO handle other URIs From 0f5ddfb79fe0aec788176775ab5476a4e9d0c838 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:08:09 +0100 Subject: [PATCH 66/71] handle taxonnames without dwc:kingdom --- Queries.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Queries.ts b/Queries.ts index f11cb00..22085cc 100644 --- a/Queries.ts +++ b/Queries.ts @@ -60,8 +60,8 @@ BIND(<${colUri}> as ?col) ?tn dwc:rank ?trank ; a dwcFP:TaxonName . FILTER(LCASE(?rank) = LCASE(?trank)) - ?tn dwc:kingdom ?kingdom . ?tn dwc:genus ?genus . + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } FILTER(?subgenus = COALESCE(?tnsubgenus, COALESCE(?section, ""))) @@ -119,7 +119,7 @@ export const getNameFromTC = (tcUri: string) => ?tn a dwcFP:TaxonName . ?tn dwc:rank ?tnrank . - ?tn dwc:kingdom ?kingdom . + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:section ?section . } @@ -196,7 +196,7 @@ export const getNameFromTN = (tnUri: string) => ?tn a dwcFP:TaxonName . ?tn dwc:rank ?tnrank . ?tn dwc:genus ?genus . - ?tn dwc:kingdom ?kingdom . + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:section ?section . } OPTIONAL { From c9512920b0646cb36e7b9e27b576ae3a5643ee1b Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:10:49 +0100 Subject: [PATCH 67/71] v3.4.1 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index bcf0cbd..a5121cb 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.4.0", + "version": "3.4.1", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From 85713a918cf6bfc7700d8dd6465ca90f5e5adf17 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:34:34 +0100 Subject: [PATCH 68/71] fixed queries for indirect kingdom --- Queries.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Queries.ts b/Queries.ts index 22085cc..1205ad6 100644 --- a/Queries.ts +++ b/Queries.ts @@ -61,7 +61,7 @@ BIND(<${colUri}> as ?col) a dwcFP:TaxonName . FILTER(LCASE(?rank) = LCASE(?trank)) ?tn dwc:genus ?genus . - { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?k . ?k dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } FILTER(?subgenus = COALESCE(?tnsubgenus, COALESCE(?section, ""))) @@ -119,7 +119,7 @@ export const getNameFromTC = (tcUri: string) => ?tn a dwcFP:TaxonName . ?tn dwc:rank ?tnrank . - { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?k . ?k dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } ?tn dwc:genus ?genus . OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:section ?section . } @@ -196,7 +196,7 @@ export const getNameFromTN = (tnUri: string) => ?tn a dwcFP:TaxonName . ?tn dwc:rank ?tnrank . ?tn dwc:genus ?genus . - { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?p . ?p dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } + { ?tn dwc:kingdom ?kingdom . } UNION { ?tn trt:hasParentName* ?k . ?k dwc:rank "kingdom" ; dwc:kingdom ?kingdom . } OPTIONAL { ?tn dwc:subGenus ?tnsubgenus . } OPTIONAL { ?tn dwc:section ?section . } OPTIONAL { From 140d2442b57fec67edac6643904fee23a513d59b Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:41:01 +0100 Subject: [PATCH 69/71] v3.4.2 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index a5121cb..8a8b3e7 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@plazi/synolib", - "version": "3.4.1", + "version": "3.4.2", "exports": "./mod.ts", "publish": { "include": ["./*.ts", "README.md", "LICENSE", "./example/cli.ts"], From 4b7fddf4e5d44a964b2091cc50efdaf4933afb8d Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:46:00 +0100 Subject: [PATCH 70/71] moved fetchOptions to central place --- SynonymGroup.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/SynonymGroup.ts b/SynonymGroup.ts index c146db7..8e41026 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -18,6 +18,8 @@ export class SynonymGroup implements AsyncIterable { /** The SparqlEndpoint used */ private sparqlEndpoint: SparqlEndpoint; + private fetchOptions: RequestInit = { signal: this.controller.signal }; + /** * List of names found so-far. * @@ -172,19 +174,19 @@ export class SynonymGroup implements AsyncIterable { if (taxonName.startsWith("https://www.catalogueoflife.org")) { json = await this.sparqlEndpoint.getSparqlResultSet( Queries.getNameFromCol(taxonName), - { signal: this.controller.signal }, + this.fetchOptions, `NameFromCol ${taxonName}`, ); } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { json = await this.sparqlEndpoint.getSparqlResultSet( Queries.getNameFromTC(taxonName), - { signal: this.controller.signal }, + this.fetchOptions, `NameFromTC ${taxonName}`, ); } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { json = await this.sparqlEndpoint.getSparqlResultSet( Queries.getNameFromTN(taxonName), - { signal: this.controller.signal }, + this.fetchOptions, `NameFromTN ${taxonName}`, ); } else { @@ -223,7 +225,7 @@ LIMIT 5000`; if (this.controller.signal?.aborted) return Promise.reject(); const json = await this.sparqlEndpoint.getSparqlResultSet( query, - { signal: this.controller.signal }, + this.fetchOptions, `Subtaxa ${url}`, ); @@ -261,7 +263,7 @@ LIMIT 500`; if (this.controller.signal?.aborted) return Promise.reject(); const json = await this.sparqlEndpoint.getSparqlResultSet( query, - { signal: this.controller.signal }, + this.fetchOptions, `NameFromLatin ${genus} ${species} ${infrasp}`, ); @@ -534,7 +536,7 @@ GROUP BY ?current ?current_status`; const json = await this.sparqlEndpoint.getSparqlResultSet( query, - { signal: this.controller.signal }, + this.fetchOptions, `AcceptedCol ${colUri}`, ); @@ -581,9 +583,11 @@ GROUP BY ?current ?current_status`; const result: vernacularNames = new Map(); const query = `SELECT DISTINCT ?n WHERE { <${uri}> ?n . }`; - const bindings = (await this.sparqlEndpoint.getSparqlResultSet(query, { - signal: this.controller.signal, - }, `Vernacular ${uri}`)).results.bindings; + const bindings = (await this.sparqlEndpoint.getSparqlResultSet( + query, + this.fetchOptions, + `Vernacular ${uri}`, + )).results.bindings; for (const b of bindings) { if (b.n?.value) { if (b.n["xml:lang"]) { @@ -708,7 +712,7 @@ GROUP BY ?date ?title ?mc`; try { const json = await this.sparqlEndpoint.getSparqlResultSet( query, - { signal: this.controller.signal }, + this.fetchOptions, `TreatmentDetails ${treatmentUri}`, ); const materialCitations: MaterialCitation[] = json.results.bindings @@ -748,7 +752,7 @@ SELECT DISTINCT ?url ?description WHERE { } `; const figures = (await this.sparqlEndpoint.getSparqlResultSet( figureQuery, - { signal: this.controller.signal }, + this.fetchOptions, `TreatmentDetails/Figures ${treatmentUri}`, )).results.bindings; const figureCitations = figures.filter((f) => f.url?.value).map( From 5828cfcf3ca688359763cc345413373275335768 Mon Sep 17 00:00:00 2001 From: nleanba <25827850+nleanba@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:14:24 +0100 Subject: [PATCH 71/71] force use of cached responses client-side This should improve performance and reduce server load a bit, altough it is dependent on the browser occasionally flushing the cache --- SparqlEndpoint.ts | 1 + SynonymGroup.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/SparqlEndpoint.ts b/SparqlEndpoint.ts index ee25288..04b53b3 100644 --- a/SparqlEndpoint.ts +++ b/SparqlEndpoint.ts @@ -71,6 +71,7 @@ export class SparqlEndpoint { const wait = 50 * (1 << retryCount++); console.info(`!! Fetch Error. Retrying in ${wait}ms (${retryCount})`); await sleep(wait); + fetchOptions.cache = "no-cache"; // do not attempt to use cache on retry (this will populate the chache on success though) return await sendRequest(); } console.warn("!! Fetch Error:", query, "\n---\n", error); diff --git a/SynonymGroup.ts b/SynonymGroup.ts index 8e41026..829f46b 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -18,7 +18,10 @@ export class SynonymGroup implements AsyncIterable { /** The SparqlEndpoint used */ private sparqlEndpoint: SparqlEndpoint; - private fetchOptions: RequestInit = { signal: this.controller.signal }; + private fetchOptions: RequestInit = { + signal: this.controller.signal, + cache: "force-cache", + }; /** * List of names found so-far.