|
| 1 | +// eslint-disable-next-line @typescript-eslint/no-unused-vars |
| 2 | +import { FunctionalComponent, h } from '@stencil/core'; |
| 3 | +import { GenericIdentifierType } from '../utils/GenericIdentifierType'; |
| 4 | +import { FoldableItem } from '../utils/FoldableItem'; |
| 5 | +import { FoldableAction } from '../utils/FoldableAction'; |
| 6 | + |
| 7 | +/** |
| 8 | + * This class specifies a custom renderer for Research Organization Registry (ROR) identifiers. |
| 9 | + * It fetches organization details from the ROR API v2 and displays them. |
| 10 | + * @extends GenericIdentifierType |
| 11 | + */ |
| 12 | +export class RORType extends GenericIdentifierType { |
| 13 | + private rorData: Record<string, any>; |
| 14 | + private label: string; |
| 15 | + private acronym: string; |
| 16 | + |
| 17 | + getSettingsKey(): string { |
| 18 | + return 'RORType'; |
| 19 | + } |
| 20 | + |
| 21 | + /** |
| 22 | + * Checks if the provided value is a valid ROR ID format |
| 23 | + * ROR IDs typically have the format: https://ror.org/XXXXXXXXX where X is an alphanumeric character |
| 24 | + * @returns {boolean} Whether the value has the correct ROR ID format |
| 25 | + */ |
| 26 | + hasCorrectFormat(): boolean { |
| 27 | + const regex = new RegExp('^https?://ror.org/[0-9a-z]{9}$', 'i'); |
| 28 | + return regex.test(this.value); |
| 29 | + } |
| 30 | + |
| 31 | + /** |
| 32 | + * Extracts the ROR ID from the full URL |
| 33 | + * @returns {string} The ROR ID |
| 34 | + */ |
| 35 | + private getRorId(): string { |
| 36 | + return this.value.split('/').pop(); |
| 37 | + } |
| 38 | + |
| 39 | + /** |
| 40 | + * Fetches organization data from the ROR API v2 |
| 41 | + * @returns {Promise<void>} |
| 42 | + */ |
| 43 | + async init(): Promise<void> { |
| 44 | + try { |
| 45 | + // Extract the ROR ID from the URL |
| 46 | + const rorId = this.getRorId(); |
| 47 | + |
| 48 | + // Fetch data from ROR API v2 |
| 49 | + const response = await fetch(`https://api.ror.org/v2/organizations/${rorId}`); |
| 50 | + if (!response.ok) { |
| 51 | + throw new Error(`Failed to fetch ROR data: ${response.status}`); |
| 52 | + } |
| 53 | + |
| 54 | + this.rorData = await response.json(); |
| 55 | + |
| 56 | + if (!this.rorData) return; |
| 57 | + |
| 58 | + // Initialize name, acronym, and labels |
| 59 | + if (!this.rorData.names || this.rorData.names.length === 0) { |
| 60 | + this.label = 'Unknown'; |
| 61 | + this.items.push(new FoldableItem(0, 'Name', 'Unknown', 'No names available for this organization')); |
| 62 | + return; |
| 63 | + } else { |
| 64 | + for (const name of this.rorData.names) { |
| 65 | + const types = name.types || []; |
| 66 | + if (types.includes('acronym')) { |
| 67 | + this.acronym = name.value; |
| 68 | + this.items.push(new FoldableItem(20, 'Acronym', name.value, 'Short form of the organization name')); |
| 69 | + } else if (types.includes('ror_display')) { |
| 70 | + this.label = name.value; |
| 71 | + this.items.push(new FoldableItem(1, 'Display Name', name.value, 'Name used for display purposes')); |
| 72 | + } else if (types.includes('alias')) { |
| 73 | + this.items.push(new FoldableItem(5, 'Alias', name.value, 'Alternative name for the organization')); |
| 74 | + } else if (types.includes('label')) { |
| 75 | + this.items.push(new FoldableItem(15, 'Label', name.value, 'Name in another language or script')); |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // Add ROR ID |
| 81 | + this.items.push(new FoldableItem(20, 'ROR ID', this.rorData.id, 'Unique identifier for the organization in the ROR registry', null, null, false)); |
| 82 | + this.actions.push(new FoldableAction(10, 'View on ROR', this.rorData.id, 'primary')); |
| 83 | + |
| 84 | + // Add status of the organization |
| 85 | + this.items.push(new FoldableItem(30, 'Status', this.rorData.status || 'Unknown', 'Current status of the organization in the ROR registry')); |
| 86 | + |
| 87 | + // Add types of the organization |
| 88 | + if (!this.rorData.types || this.rorData.types.length === 0) { |
| 89 | + this.items.push(new FoldableItem(25, 'Type', 'Unknown', 'Type of organization')); |
| 90 | + } else { |
| 91 | + for (const type of this.rorData.types) { |
| 92 | + this.items.push(new FoldableItem(25, 'Type', type, 'Type of organization')); |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + // Add external references |
| 97 | + if (this.rorData.links && this.rorData.links.length > 0) { |
| 98 | + for (const link of this.rorData.links) { |
| 99 | + if (link.type && link.type.isEmpty()) { |
| 100 | + this.items.push(new FoldableItem(35, `Link to ${link.type}`, link.value, 'External link related to the organization')); |
| 101 | + } else { |
| 102 | + this.items.push(new FoldableItem(35, `Link`, link.value, 'External link related to the organization')); |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // Add external identifiers |
| 108 | + if (this.rorData.external_ids && this.rorData.external_ids.length > 0) { |
| 109 | + for (const external of this.rorData.external_ids) { |
| 110 | + const type = external.type; |
| 111 | + const value = external.preferred || external.all[0]; |
| 112 | + this.items.push(new FoldableItem(40, `External ID: ${type}`, value, `Identifier from another system: ${type}`)); |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + // Add related organizations with tooltips for relationship types |
| 117 | + if (this.rorData.relationships && this.rorData.relationships.length > 0) { |
| 118 | + const relationshipTypes = { |
| 119 | + parent: { |
| 120 | + title: 'Parent Organization', |
| 121 | + tooltip: 'Organization that this organization is part of', |
| 122 | + }, |
| 123 | + child: { |
| 124 | + title: 'Child Organization', |
| 125 | + tooltip: 'Organization that is part of this organization', |
| 126 | + }, |
| 127 | + related: { |
| 128 | + title: 'Related Organization', |
| 129 | + tooltip: 'Organization that is related to this organization', |
| 130 | + }, |
| 131 | + predecessor: { |
| 132 | + title: 'Predecessor Organization', |
| 133 | + tooltip: 'Organization that preceded this organization', |
| 134 | + }, |
| 135 | + successor: { |
| 136 | + title: 'Successor Organization', |
| 137 | + tooltip: 'Organization that succeeded this organization', |
| 138 | + }, |
| 139 | + }; |
| 140 | + |
| 141 | + for (const rel of this.rorData.relationships) { |
| 142 | + const relationType = relationshipTypes[rel.type] || { title: rel.type, tooltip: `${rel.type} organization` }; |
| 143 | + |
| 144 | + this.items.push(new FoldableItem(90, relationType.title, `https://ror.org/${rel.id}`, relationType.tooltip)); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + // Add locations with country code and coordinates |
| 149 | + if (this.rorData.locations && this.rorData.locations.length > 0) { |
| 150 | + for (const location of this.rorData.locations) { |
| 151 | + const details = location.geonames_details; |
| 152 | + if (details.country_code) { |
| 153 | + this.items.push(new FoldableItem(50, 'Country', details.country_code, 'Country where the organization is located')); |
| 154 | + } |
| 155 | + if (details.lat && details.lng) { |
| 156 | + this.items.push(new FoldableItem(55, 'Coordinates', `${details.lat}, ${details.lng}`, 'Geographic coordinates of the organization')); |
| 157 | + const osmUrl = `https://www.openstreetmap.org/?mlat=${details.lat}&mlon=${details.lng}&zoom=15`; |
| 158 | + this.actions.push(new FoldableAction(20, 'View on OpenStreetMap', osmUrl, 'secondary')); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + } catch (error) { |
| 163 | + console.error('Error fetching ROR data:', error); |
| 164 | + // Add an error item |
| 165 | + this.items.push(new FoldableItem(0, 'Error', `Failed to fetch data from ROR API: ${error.message}`)); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * Renders a preview of the ROR organization |
| 171 | + * @returns {FunctionalComponent} The preview component |
| 172 | + */ |
| 173 | + renderPreview(): FunctionalComponent { |
| 174 | + // If data is not yet loaded, show the ROR ID |
| 175 | + if (!this.rorData) { |
| 176 | + return <span class="font-mono text-sm">Loading ROR: {this.value}...</span>; |
| 177 | + } |
| 178 | + |
| 179 | + // If data is loaded, show organization name and ID |
| 180 | + return ( |
| 181 | + <span class={'inline-flex flex-nowrap items-center align-top font-mono'}> |
| 182 | + <svg |
| 183 | + xmlns="http://www.w3.org/2000/svg" |
| 184 | + // xmlns:xlink="http://www.w3.org/1999/xlink" |
| 185 | + // xmlns:serif="http://www.serif.com/" |
| 186 | + // width="100%" |
| 187 | + // height="100%" |
| 188 | + viewBox="0 0 164 118" |
| 189 | + version="1.1" |
| 190 | + // xml:space="preserve" |
| 191 | + class={'mr-1 h-5 flex-none items-center p-0.5'} |
| 192 | + style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: '2' }} |
| 193 | + > |
| 194 | + <g transform="matrix(0.994301,0,0,0.989352,0,0)"> |
| 195 | + <rect x="0" y="0" width="164.94" height="119.27" style={{ fill: 'white' }} /> |
| 196 | + </g> |
| 197 | + <g transform="matrix(1,0,0,1,-0.945,-0.815)"> |
| 198 | + <path d="M68.65,4.16L56.52,22.74L44.38,4.16L68.65,4.16Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} /> |
| 199 | + <path d="M119.41,4.16L107.28,22.74L95.14,4.16L119.41,4.16Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} /> |
| 200 | + <path d="M44.38,115.47L56.52,96.88L68.65,115.47L44.38,115.47Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} /> |
| 201 | + <path d="M95.14,115.47L107.28,96.88L119.41,115.47L95.14,115.47Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} /> |
| 202 | + <path |
| 203 | + d="M145.53,63.71C149.83,62.91 153.1,61 155.33,57.99C157.57,54.98 158.68,51.32 158.68,47.03C158.68,43.47 158.06,40.51 156.83,38.13C155.6,35.75 153.93,33.86 151.84,32.45C149.75,31.05 147.31,30.04 144.53,29.44C141.75,28.84 138.81,28.54 135.72,28.54L112.16,28.54L112.16,47.37C111.97,46.82 111.77,46.28 111.55,45.74C109.92,41.79 107.64,38.42 104.71,35.64C101.78,32.86 98.32,30.72 94.3,29.23C90.29,27.74 85.9,26.99 81.14,26.99C76.38,26.99 72,27.74 67.98,29.23C63.97,30.72 60.5,32.86 57.57,35.64C54.95,38.13 52.85,41.1 51.27,44.54C51.04,42.07 50.46,39.93 49.53,38.13C48.3,35.75 46.63,33.86 44.54,32.45C42.45,31.05 40.01,30.04 37.23,29.44C34.45,28.84 31.51,28.54 28.42,28.54L4.87,28.54L4.87,89.42L18.28,89.42L18.28,65.08L24.9,65.08L37.63,89.42L53.71,89.42L38.24,63.71C42.54,62.91 45.81,61 48.04,57.99C48.14,57.85 48.23,57.7 48.33,57.56C48.31,58.03 48.3,58.5 48.3,58.98C48.3,63.85 49.12,68.27 50.75,72.22C52.38,76.17 54.66,79.54 57.59,82.32C60.51,85.1 63.98,87.24 68,88.73C72.01,90.22 76.4,90.97 81.16,90.97C85.92,90.97 90.3,90.22 94.32,88.73C98.33,87.24 101.8,85.1 104.73,82.32C107.65,79.54 109.93,76.17 111.57,72.22C111.79,71.69 111.99,71.14 112.18,70.59L112.18,89.42L125.59,89.42L125.59,65.08L132.21,65.08L144.94,89.42L161.02,89.42L145.53,63.71ZM36.39,50.81C35.67,51.73 34.77,52.4 33.68,52.83C32.59,53.26 31.37,53.52 30.03,53.6C28.68,53.69 27.41,53.73 26.2,53.73L18.29,53.73L18.29,39.89L27.06,39.89C28.26,39.89 29.5,39.98 30.76,40.15C32.02,40.32 33.14,40.65 34.11,41.14C35.08,41.63 35.89,42.33 36.52,43.25C37.15,44.17 37.47,45.4 37.47,46.95C37.47,48.6 37.11,49.89 36.39,50.81ZM98.74,66.85C97.85,69.23 96.58,71.29 94.91,73.04C93.25,74.79 91.26,76.15 88.93,77.13C86.61,78.11 84.01,78.59 81.15,78.59C78.28,78.59 75.69,78.1 73.37,77.13C71.05,76.16 69.06,74.79 67.39,73.04C65.73,71.29 64.45,69.23 63.56,66.85C62.67,64.47 62.23,61.85 62.23,58.98C62.23,56.17 62.67,53.56 63.56,51.15C64.45,48.74 65.72,46.67 67.39,44.92C69.05,43.17 71.04,41.81 73.37,40.83C75.69,39.86 78.28,39.37 81.15,39.37C84.02,39.37 86.61,39.86 88.93,40.83C91.25,41.8 93.24,43.17 94.91,44.92C96.57,46.67 97.85,48.75 98.74,51.15C99.63,53.56 100.07,56.17 100.07,58.98C100.07,61.85 99.63,64.47 98.74,66.85ZM143.68,50.81C142.96,51.73 142.06,52.4 140.97,52.83C139.88,53.26 138.66,53.52 137.32,53.6C135.97,53.69 134.7,53.73 133.49,53.73L125.58,53.73L125.58,39.89L134.35,39.89C135.55,39.89 136.79,39.98 138.05,40.15C139.31,40.32 140.43,40.65 141.4,41.14C142.37,41.63 143.18,42.33 143.81,43.25C144.44,44.17 144.76,45.4 144.76,46.95C144.76,48.6 144.4,49.89 143.68,50.81Z" |
| 204 | + style={{ fill: 'rgb(32,40,38)', fillRule: 'nonzero' }} |
| 205 | + /> |
| 206 | + </g> |
| 207 | + </svg> |
| 208 | + <span class={'flex-none items-center px-1'}> |
| 209 | + {this.label} |
| 210 | + {this.acronym ? ' (' + this.acronym + ')' : ''} |
| 211 | + </span> |
| 212 | + </span> |
| 213 | + ); |
| 214 | + } |
| 215 | + |
| 216 | + /** |
| 217 | + * Returns the data fetched from ROR |
| 218 | + * @returns {unknown} The data fetched from ROR API |
| 219 | + */ |
| 220 | + get data(): unknown { |
| 221 | + return this.rorData; |
| 222 | + } |
| 223 | +} |
0 commit comments