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