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/.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..eda72f9 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,41 +14,19 @@ 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@v1 - with: - deno-version: v1.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' + - name: "Checkout synolib2 branch" uses: actions/checkout@v3 with: ref: synolib2 - path: 'synolib2' - - name: Use Node.js - uses: actions/setup-node@v4 + - name: Setup Deno + uses: denoland/setup-deno@v2 with: - node-version: '20.x' + deno-version: v2.x - name: Draw the rest of the owl run: | - cd synolib2 - npm ci - npm run example build - mkdir ../build/next - cp ./example/index.* ../build/next/ - cd .. + deno task example_build + mkdir ./build + cp ./example/index.* ./build/ - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@4.1.5 diff --git a/.gitignore b/.gitignore index 03e5fd5..71e6791 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -synonym-group.js - -npm-package/node_modules -npm-package/index.js -npm-package/index.d.ts \ No newline at end of file +docs/ +build/ +example/index.js +example/index.js.map +node_modules/ \ No newline at end of file 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 new file mode 100644 index 0000000..fb7993f --- /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 deleted file mode 100644 index a3260c8..0000000 --- a/JustificationSet.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-ignore: Import unneccesary for typings, will collate .d.ts files -import type { JustifiedSynonym, Treatment } from "./SynonymGroup.ts"; - -interface Justification { - toString: () => string; - 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 { - private monitor = new EventTarget(); - contents: anyJustification[] = []; - isFinished = false; - isAborted = false; - entries = ((Array.from(this.contents.values()).map((v) => [v, v])) as [ - anyJustification, - anyJustification, - ][]).values; - - constructor(iterable?: Iterable) { - if (iterable) { - for (const el of iterable) { - this.add(el); - } - } - return this; - } - - 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); - } - }); - } - - add(value: anyJustification) { - if ( - this.contents.findIndex((c) => c.toString() === value.toString()) === -1 - ) { - this.contents.push(value); - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - return this; - } - - finish() { - //console.info("%cJustificationSet finished", "color: #69F0AE;"); - this.isFinished = true; - this.monitor.dispatchEvent(new CustomEvent("updated")); - } - - forEachCurrent(cb: (val: anyJustification) => void) { - this.contents.forEach(cb); - } - - first() { - return new Promise((resolve) => { - if (this.contents[0]) { - resolve(this.contents[0]); - } else { - this.monitor.addEventListener("update", () => { - resolve(this.contents[0]); - }); - } - }); - } - - [Symbol.toStringTag] = ""; - [Symbol.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/Queries.ts b/Queries.ts new file mode 100644 index 0000000..575e6e6 --- /dev/null +++ b/Queries.ts @@ -0,0 +1,244 @@ +/** + * 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 ?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) + (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 ?kingdom ?tn ?tc ?col ?rank ?genus ?subgenus ?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 . + ?col dwc:scientificName ?name . + ?col dwc:genericName ?genus . + OPTIONAL { ?col (dwc:parent|dwc:acceptedName)* ?p . ?p dwc:rank "kingdom" ; dwc:taxonName ?colkingdom . } + OPTIONAL { ?col dwc:infragenericEpithet ?colsubgenus . } + OPTIONAL { + ?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 . + 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 . } + FILTER(?infrasp = COALESCE(?tninfrasp, "")) + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) + } + OPTIONAL { + ?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 . + + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?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 ?tnrank . + ?tn dwc:kingdom ?kingdom . + ?tn dwc:genus ?genus . + 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) + + OPTIONAL { + ?col dwc:taxonRank ?rank . + ?col dwc:scientificName ?name . # Note: contains authority + ?col dwc:genericName ?genus . + 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 . } + FILTER(?infrasp = COALESCE(?colinfrasp, "")) + OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } + } + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) + } + OPTIONAL { + ?citetnt trt:citesTaxonName ?tn ; trt:publishedIn/dc:date ?citetndate . + BIND(CONCAT(STR(?citetnt), ">", ?citetndate) AS ?citetn) + } + + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?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 ?tnrank . + ?tn dwc:genus ?genus . + ?tn dwc:kingdom ?kingdom . + 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) + + OPTIONAL { + ?col dwc:taxonRank ?rank . + ?col dwc:scientificName ?name . # Note: contains authority + ?col dwc:genericName ?genus . + 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 . } + FILTER(?infrasp = COALESCE(?colinfrasp, "")) + OPTIONAL { ?col dwc:scientificNameAuthorship ?authority . } + } + + OPTIONAL { + ?trtnt trt:treatsTaxonName ?tn ; trt:publishedIn/dc:date ?trtndate . + BIND(CONCAT(STR(?trtnt), ">", ?trtndate) AS ?trtn) + } + OPTIONAL { + ?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 . + + OPTIONAL { + ?augt trt:augmentsTaxonConcept ?tc ; trt:publishedIn/dc:date ?augdate . + BIND(CONCAT(STR(?augt), ">", ?augdate) AS ?aug) + } + OPTIONAL { + ?deft trt:definesTaxonConcept ?tc ; trt:publishedIn/dc:date ?defdate . + BIND(CONCAT(STR(?deft), ">", ?defdate) AS ?def) + } + OPTIONAL { + ?dprt trt:deprecates ?tc ; trt:publishedIn/dc:date ?dprdate . + BIND(CONCAT(STR(?dprt), ">", ?dprdate) AS ?dpr) + } + OPTIONAL { + ?citet cito:cites ?tc ; trt:publishedIn/dc:date ?citedate . + BIND(CONCAT(STR(?citet), ">", ?citedate) AS ?cite) + } + } +} +${postamble} +LIMIT 500`; diff --git a/README.md b/README.md index 92bc389..3ed5a80 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,56 @@ 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. +## Examples -For a simple command line example using the library see: `main.ts`. +### Command-Line -## building +For a command line example using the library see: `example/cli.ts`. -Only for in-browser usage the code needs to be bundled +You can try it locally using Deno with - deno bundle SynonymGroup.ts synonym-group.js +```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) + +### Web + +An example running in the browser is located in `example/index.html` and +`example/index.ts`. + +To build the example, use + +```sh +deno task example_build +``` + +or for a live-reloading server use + +```sh +deno task example_serve +``` + +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 + +The library is to be published as-is (in typescript) to jsr.io. + +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/SparqlEndpoint.ts b/SparqlEndpoint.ts new file mode 100644 index 0000000..39627d1 --- /dev/null +++ b/SparqlEndpoint.ts @@ -0,0 +1,82 @@ +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 } + | undefined; + }[]; + }; +}; + +/** + * 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) {} + + /** @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: string, + fetchOptions: RequestInit = {}, + _reason = "", + ): Promise { + // this.reasons.push(_reason); + // DEBUG: console.info(`SPARQL ${_reason}:\n${query}`); + + fetchOptions.headers = fetchOptions.headers || {}; + (fetchOptions.headers as Record)["Accept"] = + "application/sparql-results+json"; + let retryCount = 0; + const sendRequest = async (): Promise => { + try { + // DEBUG: 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..5ebb1f7 100644 --- a/SynonymGroup.ts +++ b/SynonymGroup.ts @@ -1,192 +1,617 @@ -// @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 type { SparqlEndpoint, SparqlJson } from "./mod.ts"; +import * as Queries from "./Queries.ts"; -export type MaterialCitation = { - "catalogNumber": string; - "collectionCode"?: string; - "typeStatus"?: string; - "countryCode"?: string; - "stateProvince"?: string; - "municipality"?: string; - "county"?: string; - "locality"?: string; - "verbatimLocality"?: string; - "recordedBy"?: string; - "eventDate"?: string; - "samplingProtocol"?: string; - "decimalLatitude"?: string; - "decimalLongitude"?: string; - "verbatimElevation"?: string; - "gbifOccurrenceId"?: string; - "gbifSpecimenId"?: string; - "httpUri"?: string[]; -}; - -export type FigureCitation = { - url: string; - description?: string; -}; +/** Finds all synonyms of a taxon */ +export class SynonymGroup implements AsyncIterable { + /** Indicates whether the SynonymGroup has found all synonyms. + * + * @readonly + */ + isFinished = false; + /** Used internally to watch for new names found */ + private monitor: EventTarget = new EventTarget(); -export type TreatmentDetails = { - materialCitations: MaterialCitation[]; - figureCitations: FigureCitation[]; - date?: number; - creators?: string; - title?: string; -}; + /** Used internally to abort in-flight network requests when SynonymGroup is aborted */ + private controller = new AbortController(); -export type Treatment = { - url: string; - details: Promise; -}; + /** The SparqlEndpoint used */ + private sparqlEndpoint: SparqlEndpoint; -/** - * 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; -}; + /** + * List of names found so-far. + * + * Contains full list of synonyms _if_ .isFinished and not .isAborted + * + * @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")); + } -/** - * A map from language tags (IETF) to an array of vernacular names. - */ -export type vernacularNames = Record; + /** + * Call when all synonyms are found + * + * @internal */ + private finish() { + this.isFinished = true; + this.monitor.dispatchEvent(new CustomEvent("updated")); + } -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; -}; + /** contains TN, TC, CoL uris of synonyms which are in-flight somehow or are done already */ + private expanded = new Set(); // new Map(); -async function sleep(ms: number): Promise { - const p = new Promise((resolve) => { - setTimeout(resolve, ms); - }); - return await p; -} + /** contains CoL uris where we don't need to check for Col "acceptedName" links + * + * col -> accepted col + */ + private acceptedCol = new Map(); -type SparqlJson = { - head: { - vars: string[]; - }; - results: { - bindings: { - [key: string]: { type: string; value: string; "xml:lang"?: string }; - }[]; - }; -}; + /** + * Used internally to deduplicate treatments, maps from URI to Object. + * + * Contains full list of treatments _if_ .isFinished and not .isAborted + * + * @readonly + */ + treatments: Map = new Map(); -/** - * Represents a remote sparql endpoint and provides a uniform way to run queries. - */ -export class SparqlEndpoint { - constructor(private sparqlEnpointUri: string) { - } + /** + * 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: boolean; /** - * Run a query against the sparql endpoint + * if set to true, subTaxa of the search term are also considered as starting points. * - * 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`) + * Not that "weird" ranks like subGenus are always included when searching for a genus by latin name. + */ + startWithSubTaxa: boolean; + + /** + * Constructs a SynonymGroup * - * @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 + * @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. */ - async getSparqlResultSet( - query: string, - fetchOptions: RequestInit = {}, - _reason = "", + constructor( + sparqlEndpoint: SparqlEndpoint, + taxonName: string, + ignoreDeprecatedCoL = true, + startWithSubTaxa = false, ) { - 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, + this.sparqlEndpoint = sparqlEndpoint; + this.ignoreDeprecatedCoL = ignoreDeprecatedCoL; + this.startWithSubTaxa = startWithSubTaxa; + + if (taxonName.startsWith("http")) { + 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), + undefined, + undefined, + ] as [string, string | undefined, string | undefined]; + this.getNameFromLatin(name, { searchTerm: true, subTaxon: false }) + .finally( + () => this.finish(), + ); + } + } + + /** + * 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 (!response.ok) { - throw new Error("Response not ok. Status " + response.status); + if (an) { + resolve(an); + return; + } + }); + }); + } + + /** @internal */ + private async getName( + taxonName: string, + justification: Justification, + ): Promise { + if (this.expanded.has(taxonName)) { + console.log("Skipping known", taxonName); + return; + } + + if (this.controller.signal?.aborted) return Promise.reject(); + + let json: SparqlJson | undefined; + + if (taxonName.startsWith("https://www.catalogueoflife.org")) { + json = await this.sparqlEndpoint.getSparqlResultSet( + Queries.getNameFromCol(taxonName), + { signal: this.controller.signal }, + `NameFromCol ${taxonName}`, + ); + } else if (taxonName.startsWith("http://taxon-concept.plazi.org")) { + json = await this.sparqlEndpoint.getSparqlResultSet( + Queries.getNameFromTC(taxonName), + { signal: this.controller.signal }, + `NameFromTC ${taxonName}`, + ); + } else if (taxonName.startsWith("http://taxon-name.plazi.org")) { + 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 + ) { + await this.getSubtaxa(taxonName); + } + } + + /** @internal */ + private async getSubtaxa(url: string): Promise { + 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)) as string[]; + + await Promise.allSettled( + names.map((n) => this.getName(n, { searchTerm: true, subTaxon: true })), + ); + } + + /** @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:form|dwc:infraspecificEpithet "${infrasp}" .` + : "FILTER NOT EXISTS { ?uri dwc:subSpecies|dwc:variety|dwc:form|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)) as string[]; + + await Promise.allSettled(names.map((n) => this.getName(n, justification))); + } + + /** + * Note this makes some assumptions on which variables are present in the bindings + * + * @internal */ + private async handleName( + json: SparqlJson, + justification: Justification, + ): Promise { + const treatmentPromises: Treatment[] = []; + + 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; + + // 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) { + if (this.expanded.has(taxonNameURI)) return; + this.expanded.add(taxonNameURI); //, NameStatus.madeName); + } + + for (const t of json.results.bindings) { + if (t.col) { + const colURI = t.col.value; + 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; + } + authorizedCoLNames.push({ + displayName, + authority: t.authority!.value, + colURI: t.col.value, + treatments: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + cite: new Set(), + }, + }); } - 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(); + } + + 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)) { + // 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, + }, + }); } - console.warn("!! Fetch Error:", query, "\n---\n", error); - throw error; + + def.forEach((t) => treatmentPromises.push(t)); + aug.forEach((t) => treatmentPromises.push(t)); + dpr.forEach((t) => treatmentPromises.push(t)); } + } + + const treats = this.makeTreatmentSet( + json.results.bindings[0].tntreats?.value.split("|"), + ); + 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, + authorizedNames: [...authorizedCoLNames, ...authorizedTCNames], + colURI: unathorizedCol, + justification, + treatments: { + treats, + cite: this.makeTreatmentSet( + json.results.bindings[0].tncites?.value.split("|"), + ), + }, + vernacularNames: taxonNameURI + ? this.getVernacular(taxonNameURI) + : Promise.resolve(new Map()), }; - return await sendRequest(); + + 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[] = []; + + 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( + n.colURI!, + name, + ); + n.acceptedColURI = acceptedColURI; + colPromises.push(...promises); + }), + ); + + 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( + [ + ...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 . + OPTIONAL { ?current dwc:taxonomicStatus ?current_status . } + } UNION { + ?col dwc:taxonomicStatus ?current_status . + OPTIONAL { ?dpr dwc:acceptedName ?col . } + FILTER NOT EXISTS { ?col dwc:acceptedName ?_ . } + BIND(?col AS ?current) } } +GROUP BY ?current ?current_status`; -export default class SynonymGroup implements AsyncIterable { - justifiedArray: JustifiedSynonym[] = []; - monitor = new EventTarget(); - isFinished = false; - isAborted = false; + if (this.acceptedCol.has(colUri)) { + return [this.acceptedCol.get(colUri)!, []]; + } - /** Maps from url to object */ - treatments: Map = new Map(); - taxonNames: Map = new Map(); + const json = await this.sparqlEndpoint.getSparqlResultSet( + query, + { signal: this.controller.signal }, + `AcceptedCol ${colUri}`, + ); - private controller = new AbortController(); + const promises: Promise[] = []; - 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; + 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.getName(b.current!.value, { + searchTerm: false, + parent, + }), + ); + } + + this.acceptedCol.set(dpr, b.current!.value); + if (!this.ignoreDeprecatedCoL) { + promises.push( + this.getName(dpr, { searchTerm: false, parent }), + ); + } + } } - this.monitor.dispatchEvent(new CustomEvent("updated")); - }; + } + + 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(); + 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; + } - const fetchInit = { signal: this.controller.signal }; + /** @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_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, + }); + } + return this.treatments.get(url) as Treatment; + }), + ); + } - async function getTreatmentDetails( - treatmentUri: string, - ): Promise { - const query = ` + /** @internal */ + private async getTreatmentDetails( + treatmentUri: string, + ): Promise { + const query = ` PREFIX dc: PREFIX dwc: +PREFIX dwcFP: +PREFIX cito: PREFIX trt: SELECT DISTINCT ?date ?title ?mc @@ -209,555 +634,356 @@ 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 { -<${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 . } -} + BIND (<${treatmentUri}> as ?treatment) + ?treatment dc:creator ?creator . + 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 (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: + if (this.controller.signal.aborted) { + 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( + query, + { signal: this.controller.signal }, + `TreatmentDetails ${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 . + 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 []; - }); + 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 }; }, - /** 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 []; - }); + ); + return { + creators: json.results.bindings[0]?.creators?.value, + 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, + ), }, - /** 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 []; - }); + }; + } catch (error) { + console.warn("SPARQL Error: " + error); + return { + materialCitations: [], + figureCitations: [], + treats: { + def: new Set(), + aug: new Set(), + dpr: new Set(), + citetc: new Set(), + treattn: new Set(), + citetn: new Set(), }, - ]; - - 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]() { + /** Allows iterating over the synonyms while they are found */ + [Symbol.asyncIterator](): AsyncIterator { let returnedSoFar = 0; return { - next: () => { - return new Promise>( + next: () => + new Promise>( (resolve, reject) => { - const _ = () => { - if (this.isAborted) { + const callback = () => { + if (this.controller.signal.aborted) { reject(new Error("SynyonymGroup has been aborted")); - } else if (returnedSoFar < this.justifiedArray.length) { - resolve({ value: this.justifiedArray[returnedSoFar++] }); + } 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(); }, - ); - }, + ), }; } } + +// TODO: CoL taxa without authority -- associate them with the Name directly +// eg. 5KTTT is "Quercus robur subsp. robur" w/o authority + +/** 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 + * + * may be empty for some CoL-taxa with missing ancestors */ + kingdom: string; + /** Human-readable name */ + displayName: string; + /** taxonomic rank */ + rank: string; + + /** vernacular names */ + vernacularNames: Promise; + + // /** Contains the family tree / upper taxons accorindg to CoL / treatmentbank. + // * //TODO */ + // trees: Promise<{ + // col?: Tree; + // tb?: Tree; + // }>; + + /** 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[]; + + /** How this name was found */ + justification: Justification; + + /** treatments directly associated with .taxonNameUri */ + treatments: { + treats: Set; + cite: Set; + }; +}; + +/** + * 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; + /** indicates that this is a subTaxon of the parent */ + subTaxon: boolean; +} | { + searchTerm: false; + parent: Name; + /** if missing, indicates synonymy according to CoL or subTaxon */ + treatment?: Treatment; +}; + +/** + * 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 name */ + displayName: string; + /** Human-readable authority */ + authority: 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; + /** 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 */ + // seeAlsoCol: string[]; + + /** treatments directly associated with .taxonConceptURI */ + treatments: { + def: Set; + aug: Set; + dpr: Set; + cite: Set; + }; +}; + +/** 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; +}; + +/** Details of a treatment */ +export type TreatmentDetails = { + materialCitations: MaterialCitation[]; + figureCitations: FigureCitation[]; + creators?: string; + title?: string; + treats: { + def: Set; + aug: Set; + dpr: Set; + citetc: Set; + treattn: Set; + citetn: Set; + }; +}; + +/** A cited material */ +export type MaterialCitation = { + "catalogNumber": string; + "collectionCode"?: string; + "typeStatus"?: string; + "countryCode"?: string; + "stateProvince"?: string; + "municipality"?: string; + "county"?: string; + "locality"?: string; + "verbatimLocality"?: string; + "recordedBy"?: string; + "eventDate"?: string; + "samplingProtocol"?: string; + "decimalLatitude"?: string; + "decimalLongitude"?: string; + "verbatimElevation"?: string; + "gbifOccurrenceId"?: string; + "gbifSpecimenId"?: string; + "httpUri"?: string[]; +}; + +/** A cited figure */ +export type FigureCitation = { + url: string; + description?: string; +}; diff --git a/build_example.ts b/build_example.ts new file mode 100644 index 0000000..e9b618a --- /dev/null +++ b/build_example.ts @@ -0,0 +1,35 @@ +import * as esbuild from "esbuild"; +import { denoPlugins } from "@luca/esbuild-deno-loader"; + +const SERVE = Deno.args.includes("serve"); +const BUILD = Deno.args.includes("build"); + +const config: esbuild.BuildOptions = { + entryPoints: ["./example/index.ts"], + outfile: "./example/index.js", + sourcemap: true, + bundle: true, + format: "esm", + plugins: [...denoPlugins()], + lineLimit: 120, + minify: BUILD ? true : false, + banner: SERVE + ? { + js: + "new EventSource('/esbuild').addEventListener('change', () => location.reload());", + } + : undefined, +}; + +if (SERVE) { + const ctx = await esbuild.context(config); + await ctx.watch(); + + const { host, port } = await ctx.serve({ + servedir: "./example", + }); + + console.log(`Listening at ${host}:${port}`); +} else { + await esbuild.build(config); +} diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..21eb719 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,25 @@ +{ + "name": "@plazi/synolib", + "version": "3.1.1", + "exports": "./mod.ts", + "publish": { + "include": ["./*.ts", "README.md", "LICENSE"], + "exclude": ["./build_example.ts"] + }, + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "dom.asynciterable", + "deno.ns" + ] + }, + "imports": { + "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0", + "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/deno.lock b/deno.lock new file mode 100644 index 0000000..90070a3 --- /dev/null +++ b/deno.lock @@ -0,0 +1,138 @@ +{ + "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==" + }, + "@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": [ + "jsr:@luca/esbuild-deno-loader@0.11", + "npm:esbuild@0.24" + ] + } +} diff --git a/example/cli.ts b/example/cli.ts new file mode 100644 index 0000000..666f4c0 --- /dev/null +++ b/example/cli.ts @@ -0,0 +1,184 @@ +import * as Colors from "https://deno.land/std@0.214.0/fmt/colors.ts"; +import { + type Name, + SparqlEndpoint, + SynonymGroup, + 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 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 synoGroup = new SynonymGroup( + sparqlEndpoint, + taxonName, + HIDE_COL_ONLY_SYNONYMS, + START_WITH_SUBTAXA, +); + +const trtColor = { + "def": Colors.green, + "aug": Colors.blue, + "dpr": Colors.red, + "cite": Colors.gray, +}; + +console.log(ENDPOINT_URL); + +console.log(Colors.blue(`Synonym Group For ${taxonName}`)); + +let authorizedNamesCount = 0; +const timeStart = performance.now(); + +for await (const name of synoGroup) { + console.log( + "\n" + + 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"); + + for (const authorizedName of name.authorizedNames) { + authorizedNamesCount++; + console.log( + " " + + Colors.underline( + authorizedName.displayName + " " + + Colors.italic(authorizedName.authority), + ) + + 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"); + } + for (const trt of authorizedName.treatments.aug) { + await logTreatment(trt, "aug"); + } + for (const trt of authorizedName.treatments.dpr) { + await logTreatment(trt, "dpr"); + } + for (const trt of authorizedName.treatments.cite) { + await logTreatment(trt, "cite"); + } + } +} + +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.`, + ), +); +// console.log( +// `Ran ${sparqlEndpoint.reasons.length} queries:\n ${ +// sparqlEndpoint.reasons.sort().join("\n ") +// }`, +// ); + +function colorizeIfPresent( + text: string | undefined, + color: "gray" | "yellow" | "green" | "cyan", +) { + if (text) return " " + Colors[color](text); + else return ""; +} + +async function logTreatment( + trt: Treatment, + type: "def" | "aug" | "dpr" | "cite", +) { + const details = await trt.details; + console.log( + ` ${trtColor[type]("●")} ${details.creators} ${trt.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(", "), + ) + }`, + ); + } +} + +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) { + 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 ${ + Colors.italic( + `${details.creators} ${name.justification.treatment.date} “${ + Colors.italic(details.title || Colors.dim("No Title")) + }” ${Colors.magenta(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}`; + } +} diff --git a/example/index.css b/example/index.css new file mode 100644 index 0000000..6fedae9 --- /dev/null +++ b/example/index.css @@ -0,0 +1,198 @@ +* { + box-sizing: border-box; +} + +:root { + 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; + margin: 0; +} + +syno-name { + display: block; + margin: 1rem 0; +} + +.rank, +.justification { + 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; +} + +.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; + padding: 0 0.2rem; + margin: 0.2rem 0; + 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; + } +} + +h2, +h3, +ul { + margin: 0; +} + +syno-treatment, +.treatmentline { + display: block; + position: relative; + margin: 0; + transition: all 200ms; + clear: both; + + & > svg { + height: 1rem; + vertical-align: sub; + margin: 0 0.2rem 0 -1.2rem; + } + + > .icon.button { + float: right; + } + + .details { + /* display: grid; */ + } + + .hidden { + max-height: 0; + overflow: hidden; + transition: all 200ms; + } + + &.expanded { + /* position: absolute; */ + background: #f0f9fc; + z-index: 10; + border-radius: 0.2rem; + padding: 0.2rem; + margin: 0.2rem -0.2rem; + + .hidden { + max-height: 200rem; + overflow: auto; + } + } +} + +.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; +} + +.blue { + color: #1e88e5; +} + +.green { + color: #388e3c; +} + +.red { + color: #e53935; +} + +.gray { + color: #666666; +} diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..0643f32 --- /dev/null +++ b/example/index.html @@ -0,0 +1,41 @@ + + + + + SynoLib + + + + + +

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 +
  • +
+
+ 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 new file mode 100644 index 0000000..4a5aee4 --- /dev/null +++ b/example/index.ts @@ -0,0 +1,583 @@ +/// +import { + type AuthorizedName, + type Name, + SparqlEndpoint, + 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"); +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") || + "https://www.catalogueoflife.org/data/taxon/3WD9M"; + +const root = document.getElementById("root") as HTMLDivElement; + +enum SynoStatus { + Def = "def", + Aug = "aug", + Dpr = "dpr", + Cite = "cite", +} + +const icons = { + def: + ``, + aug: + ``, + dpr: + ``, + cite: + ``, + unknown: + ``, + + col_aug: + ``, + col_dpr: + ``, + + link: + ``, + + expand: + ``, + collapse: + ``, + + east: + ``, + west: + ``, + 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(); + + 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; + else { + date.classList.add("missing"); + date.innerText = "No Date"; + } + this.append(date); + + 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); + + this.append(button); + + const names = document.createElement("div"); + names.classList.add("indent", "details"); + 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"); + creators.innerText = "No Authors"; + } + + if (details.title) title.innerText = "“" + details.title + "”"; + else { + title.classList.add("missing"); + title.innerText = "No Title"; + } + + 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) => { + const url = document.createElement("a"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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) { + 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) => { + const url = document.createElement("a"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-name.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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) { + 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) => { + const url = document.createElement("a"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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) { + 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) => { + const url = document.createElement("a"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-concept.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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"); + url.classList.add("taxon", "uri"); + const short = n.replace("http://taxon-name.plazi.org/id/", ""); + url.innerText = short; + url.href = "#" + short; + url.title = "show name"; + 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.figureCitations.length > 0) { + const line = document.createElement("div"); + line.classList.add("figures", "hidden"); + names.append(line); + 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.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) + .replaceAll("{", "") + .replaceAll("}", "") + .replaceAll('":', ": ") + .replaceAll(",", ", ") + .replaceAll('"', "") + ).join("\n -"); + } + }); + } +} +customElements.define("syno-treatment", SynoTreatment); + +class SynoName extends HTMLElement { + constructor(name: Name) { + super(); + + const title = document.createElement("h2"); + const name_title = document.createElement("i"); + name_title.innerText = name.displayName; + title.append(name_title); + this.append(title); + + const rank_badge = document.createElement("span"); + rank_badge.classList.add("rank"); + rank_badge.innerText = name.rank; + 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"); + name_uri.classList.add("taxon", "uri"); + const short = name.taxonNameURI.replace( + "http://taxon-name.plazi.org/id/", + "", + ); + 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); + } + + const vernacular = document.createElement("div"); + vernacular.classList.add("vernacular"); + name.vernacularNames.then((names) => { + if (names.size > 0) { + vernacular.innerText = "“" + + distinct([...names.values()].flat()).join("”, “") + "”"; + } + }); + 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.col_dpr + : icons.col_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.col_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); + 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) { + for (const trt of name.treatments.treats) { + const li = new SynoTreatment(trt, SynoStatus.Aug); + treatments.append(li); + } + for (const trt of name.treatments.cite) { + const li = new SynoTreatment(trt, SynoStatus.Cite); + treatments.append(li); + } + } + + 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"); + 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"); + this.append(treatments); + + if (authorizedName.taxonConceptURI) { + const name_uri = document.createElement("a"); + name_uri.classList.add("taxon", "uri"); + const short = authorizedName.taxonConceptURI.replace( + "http://taxon-concept.plazi.org/id/", + "", + ); + 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("a"); + 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.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"); + li.innerHTML = authorizedName.acceptedColURI !== authorizedName.colURI + ? icons.col_dpr + : icons.col_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 (authorizedName.acceptedColURI !== authorizedName.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( + "https://www.catalogueoflife.org/data/taxon/", + "", + ); + col_uri.innerText = id; + col_uri.href = `#${id}`; + col_uri.title = "show name"; + 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"); + }); + } + } + + const treatments_array: { trt: Treatment; status: SynoStatus }[] = []; + + for (const trt of authorizedName.treatments.def) { + treatments_array.push({ trt, status: SynoStatus.Def }); + } + for (const trt of authorizedName.treatments.aug) { + treatments_array.push({ trt, status: SynoStatus.Aug }); + } + for (const trt of authorizedName.treatments.dpr) { + treatments_array.push({ trt, status: SynoStatus.Dpr }); + } + for (const trt of authorizedName.treatments.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); + } + } + } +} +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} ${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); + return `is, according to the Catalogue of Life,\n a synonym of ${name.justification.parent.displayName} which ${parent}`; + } +} + +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) / 1000 + } seconds.`; +if (synoGroup.names.length === 0) root.append(":["); diff --git a/index.html b/index.html deleted file mode 100644 index c220139..0000000 --- a/index.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - -

You should see a list of synonyms here

- - - - - - - \ No newline at end of file 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..b2ef36b --- /dev/null +++ b/mod.ts @@ -0,0 +1,2 @@ +export * from "./SparqlEndpoint.ts"; +export * from "./SynonymGroup.ts"; diff --git a/npm-package/package-lock.json b/npm-package/package-lock.json deleted file mode 100644 index c7968cf..0000000 --- a/npm-package/package-lock.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@factsmission/synogroup", - "version": "2.2.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@factsmission/synogroup", - "version": "2.2.0", - "license": "MIT", - "devDependencies": { - "typescript": "^4.9.5" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - } - } -} diff --git a/npm-package/package.json b/npm-package/package.json deleted file mode 100644 index 4cc298a..0000000 --- a/npm-package/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@factsmission/synogroup", - "version": "2.2.0", - "description": "", - "main": "index.js", - "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", - "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": { - "typescript": "^4.9.5" - } -} diff --git a/npm-package/readme.md b/npm-package/readme.md deleted file mode 100644 index 1860896..0000000 --- a/npm-package/readme.md +++ /dev/null @@ -1,34 +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 make-package -``` -**Note that the generated types are currently slightly broken, manually remove `import`s from `index.d.ts` before publishing** -```bash -npm publish --access public -``` - - -## Testing (-ish) - -```bash -npm run make-package -``` - -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 diff --git a/npm-package/tsconfig.json b/npm-package/tsconfig.json deleted file mode 100644 index e7da24a..0000000 --- a/npm-package/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "include": [ - "../SynonymGroup.ts", - "../JustificationSet.ts" - ], - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "removeComments": false, - "outDir": ".", - "lib": [ - "esnext", - "dom", - "dom.iterable", - "scripthost" - ], - "target": "esnext", - "module": "esnext", - } -} \ No newline at end of file