@@ -54,7 +54,7 @@
diff --git a/src/modules/otus/components/Panel/PanelNomenclature/PanelNomenclature.vue b/src/modules/otus/components/Panel/PanelNomenclature/PanelNomenclature.vue
index 89a6e80..8c3b367 100644
--- a/src/modules/otus/components/Panel/PanelNomenclature/PanelNomenclature.vue
+++ b/src/modules/otus/components/Panel/PanelNomenclature/PanelNomenclature.vue
@@ -1,13 +1,13 @@
-
-
+
+
diff --git a/src/modules/otus/components/Panel/PanelStats/PanelStats.vue b/src/modules/otus/components/Panel/PanelStats/PanelStats.vue
new file mode 100644
index 0000000..6e2921e
--- /dev/null
+++ b/src/modules/otus/components/Panel/PanelStats/PanelStats.vue
@@ -0,0 +1,53 @@
+
+
+
+ Stats
+
+
+
+
+ Taxa:
+
+ {{ store.catalog.stats.taxa }}
+
+
+
+
+
+ Names:
+
+ {{ store.catalog.stats.names }}
+
+
+
+
+
+
+
+
diff --git a/src/modules/otus/constants/overviewLayout.js b/src/modules/otus/constants/overviewLayout.js
index cf6e74b..49dc2fd 100644
--- a/src/modules/otus/constants/overviewLayout.js
+++ b/src/modules/otus/constants/overviewLayout.js
@@ -6,6 +6,7 @@ import PanelNomenclature from '../components/Panel/PanelNomenclature/PanelNomenc
import PanelMap from '../components/Panel/PanelMap/PanelMap.vue'
import PanelDescendants from '../components/Panel/PanelDescendants/Descendants.vue'
import PanelContent from '../components/Panel/PanelContent/PanelContent.vue'
+import PanelStats from '../components/Panel/PanelStats/PanelStats.vue'
export const overviewLayout = {
left: [
@@ -24,6 +25,7 @@ export const overviewLayout = {
right: [
{ component: PanelMap },
{ component: PanelDescendants },
- { component: PanelContent }
+ { component: PanelContent },
+ { component: PanelStats }
]
}
diff --git a/src/modules/otus/helpers/schema.js b/src/modules/otus/helpers/schema.js
index 2d3fcf0..c7b4273 100644
--- a/src/modules/otus/helpers/schema.js
+++ b/src/modules/otus/helpers/schema.js
@@ -37,24 +37,28 @@ export function defineTaxon({
taxonRank,
name,
scientificName,
- identifier
+ identifier,
+ commonNames,
+ alternateName
}) {
return removeEmptyProperties({
'@type': 'Taxon',
'@id': makeUrlPath(id),
- 'dct:conformsTo': {
- '@id': 'https://bioschemas.org/profiles/Taxon/0.8-DRAFT'
+ 'http://purl.org/dc/terms/conformsTo': {
+ '@id': 'https://bioschemas.org/profiles/Taxon/1.0-RELEASE'
},
additionalType: [
'dwc:Taxon',
'http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept'
],
+ 'dwc:vernacularName': defineCommonNames(commonNames),
name,
+ alternateName: alternateName.map((item) => item.replaceAll(/<\/?i>/g, '')),
childTaxon,
- parentTaxon,
scientificName: defineTaxonName(scientificName),
identifier,
- taxonRank
+ taxonRank,
+ parentTaxon: defineTaxonEntity(parentTaxon)
})
}
@@ -66,3 +70,18 @@ function defineTaxonName({ name, author, taxonRank }) {
taxonRank
})
}
+
+function defineTaxonEntity({ name, taxonRank }) {
+ return {
+ '@type': 'Taxon',
+ name,
+ taxonRank
+ }
+}
+
+function defineCommonNames(commonNames) {
+ return commonNames.map(({ name, language }) => ({
+ '@language': language,
+ '@value': name
+ }))
+}
diff --git a/src/modules/otus/services/TaxonWorks.js b/src/modules/otus/services/TaxonWorks.js
index aae3ceb..507cdc0 100644
--- a/src/modules/otus/services/TaxonWorks.js
+++ b/src/modules/otus/services/TaxonWorks.js
@@ -37,7 +37,7 @@ export default class TaxonWorks {
})
}
- static getOtuDescendants(otuId, params) {
+ static getTaxonomy(otuId, params) {
return makeAPIRequest.get(`/otus/${otuId}/inventory/taxonomy.json`, {
params
})
@@ -48,7 +48,15 @@ export default class TaxonWorks {
}
static getOtuDistribution(otuId) {
- return makeAPIRequest.get(`/otus/${otuId}/inventory/distribution`)
+ return makeAPIRequest.get(`/otus/${otuId}/inventory/distribution.json`)
+ }
+
+ static getOtuGeoJSONDistribution(otuId) {
+ return makeAPIRequest.get(`/otus/${otuId}/inventory/distribution.geojson`)
+ }
+
+ static getCachedMap(cachedId) {
+ return makeAPIRequest.get(`/cached_maps/${cachedId}`)
}
static getOtuContent(otuId) {
@@ -56,4 +64,8 @@ export default class TaxonWorks {
extend: ['depiction']
})
}
+
+ static getDwC(otuId) {
+ return makeAPIRequest.get(`/otus/${otuId}/inventory/dwc`)
+ }
}
diff --git a/src/modules/otus/store/actions/index.js b/src/modules/otus/store/actions/index.js
new file mode 100644
index 0000000..b2fbc8e
--- /dev/null
+++ b/src/modules/otus/store/actions/index.js
@@ -0,0 +1,3 @@
+export * from './loadCatalog'
+export * from './loadDistribution'
+export * from './loadTaxonomy'
diff --git a/src/modules/otus/store/actions/loadCatalog.js b/src/modules/otus/store/actions/loadCatalog.js
new file mode 100644
index 0000000..db851b9
--- /dev/null
+++ b/src/modules/otus/store/actions/loadCatalog.js
@@ -0,0 +1,25 @@
+import TaxonWorks from '../../services/TaxonWorks'
+
+function parseStats(obj) {
+ return Object.entries(obj)
+ .filter(([_, count]) => count)
+ .map((item) => item.join(': '))
+ .join('; ')
+}
+
+export const actionLoadCatalog = {
+ async loadCatalog(taxonId) {
+ const { data } = await TaxonWorks.getTaxonNameCitations(taxonId)
+
+ this.catalog = {
+ ...data,
+ stats: {
+ taxa: parseStats(data.stats.taxa),
+ names: parseStats(data.stats.names)
+ },
+ sources: data.sources.map(({ cached, url }) =>
+ cached.replace(url, `
${url}`)
+ )
+ }
+ }
+}
diff --git a/src/modules/otus/store/actions/loadDistribution.js b/src/modules/otus/store/actions/loadDistribution.js
new file mode 100644
index 0000000..7fedd28
--- /dev/null
+++ b/src/modules/otus/store/actions/loadDistribution.js
@@ -0,0 +1,51 @@
+import {
+ isRankGrpup,
+ removeDuplicateShapes,
+ makeGeoJSONFeature
+} from '../../utils'
+import TaxonWorks from '../../services/TaxonWorks'
+
+export const actionLoadDistribution = {
+ async loadDistribution({ otuId, rankString }) {
+ const isSpeciesGroup = isRankGrpup('SpeciesGroup', rankString)
+
+ const getAggregateShape = async (otuId) => {
+ TaxonWorks.getOtuDistribution(otuId)
+ .then(({ data }) => {
+ const geojson = JSON.parse(data.cached_map.geo_json)
+
+ this.distribution.currentShapeTypes = ['Aggregate']
+ this.distribution.geojson = {
+ features: [makeGeoJSONFeature(geojson, 'Aggregate')]
+ }
+ })
+ .catch((e) => {
+ this.distribution.errorMessage = e.response.data.error
+ this.distribution.currentShapeTypes = []
+ this.distribution.geojson = []
+ })
+ }
+
+ if (isSpeciesGroup) {
+ TaxonWorks.getOtuGeoJSONDistribution(otuId)
+ .then(({ data }) => {
+ if (data.request_too_large) {
+ this.distribution.geojson = null
+ this.distribution.errorMessage = data.message
+ } else {
+ const { features, shapeTypes } = removeDuplicateShapes(data)
+
+ this.distribution.currentShapeTypes = shapeTypes
+ this.distribution.geojson = {
+ features
+ }
+ }
+ })
+ .catch((e) => {
+ getAggregateShape(otuId)
+ })
+ } else {
+ getAggregateShape(otuId)
+ }
+ }
+}
diff --git a/src/modules/otus/store/actions/loadTaxonomy.js b/src/modules/otus/store/actions/loadTaxonomy.js
new file mode 100644
index 0000000..620832b
--- /dev/null
+++ b/src/modules/otus/store/actions/loadTaxonomy.js
@@ -0,0 +1,15 @@
+import TaxonWorks from '../../services/TaxonWorks'
+
+export const actionLoadTaxonomy = {
+ async loadTaxonomy(otuId) {
+ const { data } = await TaxonWorks.getTaxonomy(otuId, {
+ max_descendants_depth: 0,
+ extend: ['common_names']
+ })
+
+ this.taxonomy = {
+ commonNames: data.common_names,
+ synonyms: data.nomenclatural_synonyms
+ }
+ }
+}
diff --git a/src/modules/otus/store/store.js b/src/modules/otus/store/store.js
index b9b9277..75f6a5d 100644
--- a/src/modules/otus/store/store.js
+++ b/src/modules/otus/store/store.js
@@ -1,12 +1,31 @@
import { defineStore } from 'pinia'
import TaxonWorks from '../services/TaxonWorks'
+import {
+ actionLoadDistribution,
+ actionLoadCatalog,
+ actionLoadTaxonomy
+} from './actions'
export const useOtuStore = defineStore('otuStore', {
state: () => {
return {
otu: null,
taxon: null,
- images: null
+ images: null,
+ distribution: {
+ geojson: null,
+ errorMessage: null,
+ currentShapeTypes: []
+ },
+ catalog: {
+ sources: [],
+ stats: {},
+ timeline: []
+ },
+ taxonomy: {
+ commonNames: [],
+ synonyms: []
+ }
}
},
actions: {
@@ -22,11 +41,10 @@ export const useOtuStore = defineStore('otuStore', {
},
async loadInit(otuId) {
- const otu = (await TaxonWorks.getOtu(otuId)).data
- const taxon = (await TaxonWorks.summary(otu.taxon_name_id)).data
-
- this.otu = otu
- this.taxon = taxon
+ await this.loadOtu(otuId)
+ await this.loadTaxon(this.otu.taxon_name_id)
+ await this.loadCatalog(this.otu.taxon_name_id)
+ await this.loadTaxonomy(otuId)
},
async loadImages(otuId) {
@@ -36,6 +54,10 @@ export const useOtuStore = defineStore('otuStore', {
}
this.images = (await TaxonWorks.getOtuImages(otuId, params)).data
- }
+ },
+
+ ...actionLoadDistribution,
+ ...actionLoadCatalog,
+ ...actionLoadTaxonomy
}
})
diff --git a/src/modules/otus/utils/files.js b/src/modules/otus/utils/files.js
new file mode 100644
index 0000000..449b666
--- /dev/null
+++ b/src/modules/otus/utils/files.js
@@ -0,0 +1,29 @@
+function downloadTextFile(text, fileType, fileName) {
+ const blob = new Blob([text], { type: fileType })
+ const a = document.createElement('a')
+
+ a.download = fileName
+ a.href = URL.createObjectURL(blob)
+ a.dataset.downloadurl = [fileType, a.download, a.href].join(':')
+ a.style.display = 'none'
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ setTimeout(() => {
+ URL.revokeObjectURL(a.href)
+ }, 1500)
+}
+
+function blobToArrayBuffer(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader()
+
+ reader.addEventListener('loadend', (_) => {
+ resolve(reader.result)
+ })
+ reader.addEventListener('error', reject)
+ reader.readAsArrayBuffer(blob)
+ })
+}
+
+export { blobToArrayBuffer, downloadTextFile }
diff --git a/src/modules/otus/utils/index.js b/src/modules/otus/utils/index.js
new file mode 100644
index 0000000..9fe8130
--- /dev/null
+++ b/src/modules/otus/utils/index.js
@@ -0,0 +1,4 @@
+export * from './files'
+export * from './isRankGroup'
+export * from './makeGeoJSONFeature'
+export * from './removeDuplicateShapes'
diff --git a/src/modules/otus/utils/isRankGroup.js b/src/modules/otus/utils/isRankGroup.js
new file mode 100644
index 0000000..494b968
--- /dev/null
+++ b/src/modules/otus/utils/isRankGroup.js
@@ -0,0 +1,5 @@
+export function isRankGrpup(compareRank, rank) {
+ const rankGroup = rank.split('::').at(2)
+
+ return rankGroup === compareRank
+}
diff --git a/src/modules/otus/utils/makeGeoJSONFeature.js b/src/modules/otus/utils/makeGeoJSONFeature.js
new file mode 100644
index 0000000..177e79a
--- /dev/null
+++ b/src/modules/otus/utils/makeGeoJSONFeature.js
@@ -0,0 +1,13 @@
+export function makeGeoJSONFeature(geometry, type) {
+ return {
+ type: 'Feature',
+ geometry,
+ properties: {
+ base: [
+ {
+ type
+ }
+ ]
+ }
+ }
+}
diff --git a/src/modules/otus/utils/removeDuplicateShapes.js b/src/modules/otus/utils/removeDuplicateShapes.js
new file mode 100644
index 0000000..c90bd8f
--- /dev/null
+++ b/src/modules/otus/utils/removeDuplicateShapes.js
@@ -0,0 +1,40 @@
+export function removeDuplicateShapes(data) {
+ const features = []
+ const shapeTypes = []
+
+ data.features.forEach((feature) => {
+ const shapeId = feature.properties.shape.id
+ const shapeType = feature.properties.shape.type
+
+ if (!shapeTypes.includes(feature.properties.base.type)) {
+ shapeTypes.push(feature.properties.base.type)
+ }
+
+ const index = features.findIndex(
+ (item) =>
+ item.properties.shape.id === shapeId &&
+ item.properties.shape.type === shapeType
+ )
+
+ if (index > -1) {
+ const currentFeature = features[index]
+
+ currentFeature.properties.base.push(feature.properties.base)
+ currentFeature.properties.target.push(feature.properties.target)
+ } else {
+ const item = structuredClone(feature)
+
+ item.properties.base = [item.properties.base]
+ item.properties.target = [item.properties.target]
+
+ features.push(item)
+ }
+ })
+
+ shapeTypes.sort()
+
+ return {
+ shapeTypes,
+ features
+ }
+}
diff --git a/src/modules/otus/views/Index.vue b/src/modules/otus/views/Index.vue
index 4f1a825..43998c1 100644
--- a/src/modules/otus/views/Index.vue
+++ b/src/modules/otus/views/Index.vue
@@ -5,10 +5,10 @@
-
+
@@ -24,7 +24,7 @@
/>
-
+
{
if (!otu.value || otu.value.id !== Number(route.params.id)) {
await loadInitialData()
+ } else {
+ updateMetadata()
}
})
async function loadInitialData() {
store.$reset()
await store.loadInit(route.params.id)
+ updateMetadata()
+}
+function updateMetadata() {
useSchemaOrg([
defineTaxon({
id: route.fullPath,
@@ -123,7 +135,13 @@ async function loadInitialData() {
name: taxon.value.full_name,
author: taxon.value.author,
taxonRank: taxon.value.rank
- }
+ },
+ parentTaxon: {
+ name: taxon.value.parent.full_name,
+ taxonRank: taxon.value.parent.rank
+ },
+ commonNames: store.taxonomy.commonNames,
+ alternateName: store.taxonomy.synonyms
})
])
@@ -132,11 +150,11 @@ async function loadInitialData() {
})
}
-function loadOtu({ id }) {
+function loadOtu({ id, otu_valid_id }) {
router.push({
name: 'otus-id-overview',
params: {
- id
+ id: otu_valid_id || id
}
})
}
diff --git a/src/utils/loadConfiguration.js b/src/utils/loadConfiguration.js
index 97d76a5..8fd93b6 100644
--- a/src/utils/loadConfiguration.js
+++ b/src/utils/loadConfiguration.js
@@ -1,10 +1,13 @@
import fs from 'fs'
import yaml from 'js-yaml'
import glob from 'glob'
+import defaultConfig from '../constants/defaultConfig'
-export const loadConfiguration = appPath => {
+export const loadConfiguration = (appPath) => {
const filePaths = glob.sync(appPath + '/config/*.yml', {})
- const jsonConfig = filePaths.map(filepath => yaml.load(fs.readFileSync(filepath, 'utf8')))
+ const jsonConfig = filePaths.map((filepath) =>
+ yaml.load(fs.readFileSync(filepath, 'utf8'))
+ )
- return Object.assign({}, ...jsonConfig)
+ return Object.assign({}, defaultConfig, ...jsonConfig)
}
diff --git a/src/utils/url.js b/src/utils/url.js
new file mode 100644
index 0000000..ce665d3
--- /dev/null
+++ b/src/utils/url.js
@@ -0,0 +1,8 @@
+export function isValidUrl(string) {
+ try {
+ new URL(string)
+ return true
+ } catch (err) {
+ return false
+ }
+}
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index eab272f..15e08db 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -36,6 +36,7 @@ module.exports = {
map: {
georeference: withOpacity('--color-map-georeference'),
+ aggregate: withOpacity('--color-map-aggregate'),
asserted: withOpacity('--color-map-asserted'),
'type-material': withOpacity('--color-map-type-material'),
'collection-object': withOpacity('--color-map-collection-object')