enrich entries
This commit is contained in:
parent
f3c149b1bb
commit
2497e5913f
73 changed files with 3297 additions and 873 deletions
|
|
@ -190,3 +190,15 @@ contact_info:
|
|||
email: pietvangorp@casema.nl
|
||||
location: Oosteind (gemeente Oosterhout), Noord-Brabant
|
||||
source: https://www.brabantserfgoed.nl/page/4508/heemkundegroep-ulendonc
|
||||
locations:
|
||||
- city: Oosteind
|
||||
street_address: Maalderijstraat 22
|
||||
postal_code: '4909 AR'
|
||||
municipality: Oosterhout
|
||||
region: Noord-Brabant
|
||||
country: NL
|
||||
latitude: 51.6430903
|
||||
longitude: 4.8979482
|
||||
location_source: user_provided_google_maps_link
|
||||
location_timestamp: '2025-12-01T06:20:00+00:00'
|
||||
notes: Address location provided via Google Maps short link
|
||||
|
|
|
|||
|
|
@ -193,3 +193,33 @@ social_media:
|
|||
facebook_page: https://www.facebook.com/historischemuurreclameszwolle/
|
||||
enrichment_timestamp: '2025-11-30T18:39:58.119162+00:00'
|
||||
enrichment_method: user_provided
|
||||
google_maps_enrichment:
|
||||
search_status: found_via_user_link
|
||||
user_provided_link: https://maps.app.goo.gl/Ds4Uzih4bhTcAEcQ6
|
||||
place_name: Ervenconsulent
|
||||
place_type: Consultant
|
||||
address: Aan de Stadsmuur 79-83, 8011 VD Zwolle
|
||||
plus_code: G37W+P9 Zwolle
|
||||
coordinates:
|
||||
latitude: 52.5143008
|
||||
longitude: 6.0959301
|
||||
google_maps_url: https://www.google.com/maps/place/Ervenconsulent/@52.5143008,6.0959301,17z
|
||||
phone: 038 421 3257
|
||||
website: http://www.hetoversticht.nl/erfadvies
|
||||
enrichment_timestamp: '2025-12-01T06:30:00+00:00'
|
||||
notes: >
|
||||
User confirmed that Ervenconsulent at Aan de Stadsmuur 79-83 is also the center
|
||||
for Historische Muurreclames Zwolle. The organization operates from this location,
|
||||
which is part of Het Oversticht heritage advisory organization.
|
||||
locations:
|
||||
- city: Zwolle
|
||||
street_address: Aan de Stadsmuur 79-83
|
||||
postal_code: '8011 VD'
|
||||
region: Overijssel
|
||||
country: NL
|
||||
latitude: 52.5143008
|
||||
longitude: 6.0959301
|
||||
notes: >
|
||||
Location confirmed by user. Historische Muurreclames Zwolle operates from the
|
||||
Ervenconsulent / Het Oversticht building at Aan de Stadsmuur 79-83.
|
||||
location_timestamp: '2025-12-01T06:30:00+00:00'
|
||||
|
|
|
|||
|
|
@ -48,12 +48,41 @@ notes: |-
|
|||
google_maps_enrichment:
|
||||
search_attempted: true
|
||||
search_query: "Heemkring Glatbeke Opglabbeek Belgium"
|
||||
result: not_found
|
||||
result: found_via_user_link
|
||||
user_provided_link: https://maps.app.goo.gl/7vvyQr8ByZ7d8PnG9
|
||||
place_name: Troempeelke - UiTbalie
|
||||
place_type: Cultural center / UiTbalie
|
||||
address: Gildenstraat 10, 3660 Oudsbergen, Belgium
|
||||
plus_code: 2HVM+JG Oudsbergen, Belgium
|
||||
coordinates:
|
||||
latitude: 51.0440122
|
||||
longitude: 5.5838605
|
||||
google_maps_url: https://www.google.com/maps/place/Troempeelke+-+UiTbalie/@51.0440122,5.5838605,17z
|
||||
rating: 4.2
|
||||
total_reviews: 6
|
||||
phone: +32 89 81 09 10
|
||||
business_status: Permanently closed
|
||||
business_status_note: >
|
||||
Google Maps shows "Permanently closed" for Troempeelke - UiTbalie. This was the
|
||||
cultural center / UiTbalie (tourism office) in Opglabbeek where Heemkring Glatbeke
|
||||
may have been located. The heemkring organization may still operate without a
|
||||
physical public location.
|
||||
notes: |
|
||||
No Google Maps listing found for Heemkring Glatbeke.
|
||||
Related heemkringen in the area:
|
||||
- Geschied- en Heemkundige Kring Groot-Bree Vzw (Local history museum)
|
||||
- Heemkring Heidebloemke Vzw (Historical society in Genk)
|
||||
The heemkring may not have a physical location or Google Maps presence.
|
||||
User provided Google Maps link pointing to Troempeelke - UiTbalie, a cultural
|
||||
center in Opglabbeek/Oudsbergen. This appears to be the former location where
|
||||
Heemkring Glatbeke was based or held meetings.
|
||||
enrichment_timestamp: '2025-11-30T21:55:00+00:00'
|
||||
source: Google Maps search
|
||||
updated_timestamp: '2025-12-01T06:20:00+00:00'
|
||||
source: Google Maps via user-provided link
|
||||
locations:
|
||||
- city: Opglabbeek
|
||||
street_address: Gildenstraat 10
|
||||
postal_code: '3660'
|
||||
municipality: Oudsbergen
|
||||
region: Limburg
|
||||
country: BE
|
||||
latitude: 51.0440122
|
||||
longitude: 5.5838605
|
||||
location_note: >
|
||||
Location via Troempeelke - UiTbalie cultural center (now permanently closed).
|
||||
Opglabbeek merged into Oudsbergen municipality in 2019.
|
||||
|
|
|
|||
|
|
@ -48,10 +48,29 @@ notes: |-
|
|||
google_maps_enrichment:
|
||||
search_attempted: true
|
||||
search_query: "Historische Werkgroep Kynhout De Knipe Friesland"
|
||||
result: not_found
|
||||
result: found_via_user_link
|
||||
user_provided_link: https://maps.app.goo.gl/ZBGEvEY94QPuMmyk9
|
||||
place_name: Dominee Veenweg 30
|
||||
place_type: Building (residential address)
|
||||
address: Dominee Veenweg 30, 8456 HS De Knipe
|
||||
coordinates:
|
||||
latitude: 52.9657872
|
||||
longitude: 5.9947032
|
||||
google_maps_url: https://www.google.com/maps/place/Dominee+Veenweg+30/@52.9657872,5.9947032,17z
|
||||
notes: |
|
||||
No Google Maps listing found for Historische Werkgroep Kynhout.
|
||||
The organization likely doesn't have a physical location or Google Maps presence.
|
||||
De Knipe is correctly identified as a village in Friesland.
|
||||
User provided Google Maps link pointing to a residential address in De Knipe.
|
||||
This is likely the contact address or meeting location for the historical
|
||||
working group, rather than a public museum or cultural center.
|
||||
enrichment_timestamp: '2025-11-30T22:05:00+00:00'
|
||||
source: Google Maps search
|
||||
updated_timestamp: '2025-12-01T06:20:00+00:00'
|
||||
source: Google Maps via user-provided link
|
||||
locations:
|
||||
- city: De Knipe
|
||||
street_address: Dominee Veenweg 30
|
||||
postal_code: '8456 HS'
|
||||
municipality: Heerenveen
|
||||
region: Friesland
|
||||
country: NL
|
||||
latitude: 52.9657872
|
||||
longitude: 5.9947032
|
||||
location_note: Contact/meeting address for the historical working group
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ google_maps_enrichment:
|
|||
rating: null
|
||||
total_ratings: 0
|
||||
address: Clarissenhoeve 52, 5258 PK Berlicum
|
||||
alternate_address: Koesteeg 37, 5258 TN Berlicum
|
||||
coordinates:
|
||||
latitude: 51.6851289
|
||||
longitude: 5.4367943
|
||||
google_maps_url: https://www.google.com/maps/place/Koesteeg+37/@51.6851289,5.4367943,17z
|
||||
user_provided_link: https://maps.app.goo.gl/7vp195ZWR9jaLBDE9
|
||||
phone: "073 503 8368"
|
||||
website: https://www.deplaets.nl/
|
||||
plus_code: M9HW+R2 Berlicum
|
||||
|
|
@ -59,4 +65,19 @@ google_maps_enrichment:
|
|||
hours_status: Closed · Opens 9 am Tue
|
||||
wheelchair_accessible: true
|
||||
enrichment_timestamp: '2025-11-30T22:35:00+00:00'
|
||||
source: Google Maps search
|
||||
updated_timestamp: '2025-12-01T06:20:00+00:00'
|
||||
source: Google Maps search + user-provided link
|
||||
notes: >
|
||||
User-provided link points to Koesteeg 37, Berlicum. The official Google Maps
|
||||
listing shows Clarissenhoeve 52. Both addresses are in Berlicum - Koesteeg 37
|
||||
may be a secondary location or meeting place.
|
||||
locations:
|
||||
- city: Berlicum
|
||||
street_address: Koesteeg 37
|
||||
postal_code: '5258 TN'
|
||||
municipality: Sint-Michielsgestel
|
||||
region: Noord-Brabant
|
||||
country: NL
|
||||
latitude: 51.6851289
|
||||
longitude: 5.4367943
|
||||
location_note: User-provided location (Koesteeg 37); official listing at Clarissenhoeve 52
|
||||
|
|
|
|||
|
|
@ -60,10 +60,41 @@ google_maps_enrichment:
|
|||
rating: 4.5
|
||||
total_ratings: 10
|
||||
address: Appelhofdwarsstraat 2, 7641 BX Wierden
|
||||
coordinates:
|
||||
latitude: 52.3580015
|
||||
longitude: 6.5932438
|
||||
google_maps_url: https://www.google.com/maps/place/Historische+Kring+Wierden/@52.3580015,6.5932438,17z
|
||||
user_provided_link: https://maps.app.goo.gl/zmxXujGdjutEfxcAA
|
||||
phone: "0546 572 651"
|
||||
website: https://historischekringwierden.nl/
|
||||
plus_code: 9H5V+67 Wierden
|
||||
business_status: OPERATIONAL
|
||||
hours_status: Closed · Opens 2 pm Wed
|
||||
review_summary:
|
||||
5_stars: 6
|
||||
4_stars: 3
|
||||
3_stars: 1
|
||||
2_stars: 0
|
||||
1_stars: 0
|
||||
sample_reviews:
|
||||
- reviewer: Henk Hollegien
|
||||
rating: 5
|
||||
date: 7 years ago
|
||||
text: Enthusiastic volunteers and wonderful exhibitions on a wide variety of subjects, at least 3 per year.
|
||||
- reviewer: Sander Veldkamp
|
||||
rating: 5
|
||||
date: 2 years ago
|
||||
text: Interesting exhibition about the Twente Canal and the adjacent associations.
|
||||
enrichment_timestamp: '2025-11-30T22:25:00+00:00'
|
||||
source: Google Maps search
|
||||
updated_timestamp: '2025-12-01T06:20:00+00:00'
|
||||
source: Google Maps search + user-provided link
|
||||
locations:
|
||||
- city: Wierden
|
||||
street_address: Appelhofdwarsstraat 2
|
||||
postal_code: '7641 BX'
|
||||
municipality: Wierden
|
||||
region: Overijssel
|
||||
country: NL
|
||||
latitude: 52.3580015
|
||||
longitude: 6.5932438
|
||||
location_note: Located in "gebouw Van Buuren Stee" building
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 07aedc21cc3d3e626c702fae7631f4f3bfe3a1ac
|
||||
Subproject commit 4aeb0543f9becb95a320dd60c547b86f7134cbe3
|
||||
1642
frontend/public/data/heritage_custodian_ontology.mmd
Normal file
1642
frontend/public/data/heritage_custodian_ontology.mmd
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"generated": "2025-11-30T22:26:27.880Z",
|
||||
"generated": "2025-11-30T23:33:22.231Z",
|
||||
"version": "1.0.0",
|
||||
"categories": [
|
||||
{
|
||||
|
|
@ -669,6 +669,11 @@
|
|||
"path": "modules/slots/alternative_observed_names.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "altitude",
|
||||
"path": "modules/slots/altitude.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "api_endpoint",
|
||||
"path": "modules/slots/api_endpoint.yaml",
|
||||
|
|
@ -879,6 +884,11 @@
|
|||
"path": "modules/slots/documentation_source.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "documentation_url",
|
||||
"path": "modules/slots/documentation_url.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "emic_name",
|
||||
"path": "modules/slots/emic_name.yaml",
|
||||
|
|
@ -1164,6 +1174,11 @@
|
|||
"path": "modules/slots/longitude.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "managed_by",
|
||||
"path": "modules/slots/managed_by.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "managed_collections",
|
||||
"path": "modules/slots/managed_collections.yaml",
|
||||
|
|
@ -1464,6 +1479,11 @@
|
|||
"path": "modules/slots/started_at_time.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "storage_location",
|
||||
"path": "modules/slots/storage_location.yaml",
|
||||
"category": "slots"
|
||||
},
|
||||
{
|
||||
"name": "street_address",
|
||||
"path": "modules/slots/street_address.yaml",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ imports:
|
|||
- linkml:types
|
||||
- ../metadata
|
||||
- ../enums/AppellationTypeEnum
|
||||
- CustodianName
|
||||
- ./CustodianName
|
||||
|
||||
classes:
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Archive Organization Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/access_policy
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name: BioCustodianType
|
|||
title: Biological and Zoological Custodian Type Classification
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/collection_size
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ imports:
|
|||
- linkml:types
|
||||
- ../enums/CallForApplicationStatusEnum
|
||||
- ../enums/FundingRequirementTypeEnum
|
||||
- FundingRequirement
|
||||
- ./FundingRequirement
|
||||
- ../slots/contact_email
|
||||
- ../slots/keywords
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ imports:
|
|||
- ./CustodianObservation
|
||||
- ./ReconstructionActivity
|
||||
- ./TimeSpan
|
||||
- ../slots/documentation_url
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -701,9 +702,7 @@ slots:
|
|||
description: Vendor website URL
|
||||
range: uri
|
||||
|
||||
documentation_url:
|
||||
description: Documentation URL
|
||||
range: uri
|
||||
# NOTE: documentation_url imported from global slot ../slots/documentation_url.yaml
|
||||
|
||||
repository_url:
|
||||
description: Source code repository URL
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ imports:
|
|||
- ./Storage
|
||||
- ../enums/ArchiveProcessingStatusEnum
|
||||
- ../slots/access_restrictions
|
||||
- ../slots/storage_location
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -680,9 +681,8 @@ slots:
|
|||
description: Estimated physical/digital extent
|
||||
range: string
|
||||
|
||||
storage_location:
|
||||
description: Physical storage location(s)
|
||||
range: Storage
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
tracked_in_cms:
|
||||
description: CMS tracking this accession
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ imports:
|
|||
- ../slots/oai_pmh_endpoint
|
||||
- ../slots/platform_type
|
||||
- ../slots/platform_name
|
||||
- ../slots/storage_location
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -776,10 +777,8 @@ slots:
|
|||
|
||||
# NOTE: preservation_level imported from global slot ../slots/preservation_level.yaml
|
||||
|
||||
storage_location:
|
||||
slot_uri: premis:storedAt
|
||||
description: Primary storage location for digital content
|
||||
range: string
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
fixity_check_date:
|
||||
slot_uri: premis:fixity
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ see_also:
|
|||
- https://www.wikidata.org/wiki/Q132560468 # university archive
|
||||
|
||||
imports:
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
prefixes:
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Gallery Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
GalleryType:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ prefixes:
|
|||
imports:
|
||||
- linkml:types
|
||||
- ../metadata
|
||||
- ../slots/geonames_id
|
||||
- ../slots/latitude
|
||||
- ../slots/longitude
|
||||
- ../slots/altitude
|
||||
|
||||
types:
|
||||
WktLiteral:
|
||||
|
|
@ -43,39 +47,9 @@ slots:
|
|||
range: uriorcurie
|
||||
description: "Unique identifier for this geospatial place"
|
||||
|
||||
latitude:
|
||||
range: float
|
||||
slot_uri: wgs84:lat
|
||||
description: >-
|
||||
WGS84 latitude coordinate (decimal degrees).
|
||||
Positive = North, Negative = South.
|
||||
minimum_value: -90.0
|
||||
maximum_value: 90.0
|
||||
examples:
|
||||
- value: 52.3600
|
||||
description: "Amsterdam latitude"
|
||||
|
||||
longitude:
|
||||
range: float
|
||||
slot_uri: wgs84:long
|
||||
description: >-
|
||||
WGS84 longitude coordinate (decimal degrees).
|
||||
Positive = East, Negative = West.
|
||||
minimum_value: -180.0
|
||||
maximum_value: 180.0
|
||||
examples:
|
||||
- value: 4.8852
|
||||
description: "Amsterdam longitude"
|
||||
|
||||
altitude:
|
||||
range: float
|
||||
slot_uri: wgs84:alt
|
||||
description: >-
|
||||
Altitude above sea level (meters).
|
||||
Optional - use for elevated or underground locations.
|
||||
examples:
|
||||
- value: -2.0
|
||||
description: "Amsterdam (below sea level)"
|
||||
# NOTE: latitude imported from global slot ../slots/latitude.yaml
|
||||
# NOTE: longitude imported from global slot ../slots/longitude.yaml
|
||||
# NOTE: altitude imported from global slot ../slots/altitude.yaml
|
||||
|
||||
geometry_wkt:
|
||||
range: string
|
||||
|
|
@ -119,22 +93,7 @@ slots:
|
|||
- value: "EPSG:28992"
|
||||
description: "Dutch Rijksdriehoeksstelsel"
|
||||
|
||||
geonames_id:
|
||||
range: integer
|
||||
slot_uri: geonames:geonameId
|
||||
description: >-
|
||||
GeoNames numeric identifier.
|
||||
Resolves to https://www.geonames.org/{id}/
|
||||
|
||||
Use for:
|
||||
- Linking to GeoNames knowledge base
|
||||
- Disambiguating place names (41 "Springfield"s in USA)
|
||||
- Accessing hierarchical administrative data
|
||||
examples:
|
||||
- value: 2759794
|
||||
description: "Amsterdam (GeoNames ID)"
|
||||
- value: 6930126
|
||||
description: "Rijksmuseum building"
|
||||
# NOTE: geonames_id imported from global slot ../slots/geonames_id.yaml
|
||||
|
||||
osm_id:
|
||||
range: string
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ imports:
|
|||
- ./TimeSpan
|
||||
- ../enums/GiftShopTypeEnum
|
||||
- ../enums/ProductCategoryEnum
|
||||
- ../slots/staff_count
|
||||
- ../slots/managed_by
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -747,17 +749,13 @@ slots:
|
|||
description: Visitor to purchase conversion rate
|
||||
range: float
|
||||
|
||||
staff_count:
|
||||
description: Number of shop staff
|
||||
range: integer
|
||||
# NOTE: staff_count imported from global slot ../slots/staff_count.yaml
|
||||
|
||||
square_meters:
|
||||
description: Retail floor space
|
||||
range: float
|
||||
|
||||
managed_by:
|
||||
description: Management structure
|
||||
range: string
|
||||
# NOTE: managed_by imported from global slot ../slots/managed_by.yaml
|
||||
|
||||
supplier_relationships:
|
||||
description: Key supplier relationships
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ see_also:
|
|||
- https://www.wikidata.org/wiki/Q15755503 # archaeological society
|
||||
|
||||
imports:
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
prefixes:
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Library Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/cataloging_standard
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Museum Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/collection_focus
|
||||
- ../slots/cataloging_standard
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Official Institution Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
OfficialInstitutionType:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ imports:
|
|||
- ../slots/funding_source
|
||||
- ../slots/contact_email
|
||||
- ../slots/keywords
|
||||
- ../slots/documentation_url
|
||||
|
||||
default_prefix: hc
|
||||
|
||||
|
|
@ -86,9 +87,7 @@ slots:
|
|||
range: uriorcurie
|
||||
multivalued: true
|
||||
description: Related or predecessor/successor projects
|
||||
documentation_url:
|
||||
range: uri
|
||||
description: URL to project documentation
|
||||
# NOTE: documentation_url imported from global slot ../slots/documentation_url.yaml
|
||||
|
||||
# NOTE: contact_email imported from global slot ../slots/contact_email.yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ imports:
|
|||
- linkml:types
|
||||
- ../metadata
|
||||
- ../enums/ReconstructionActivityTypeEnum
|
||||
- ReconstructionAgent
|
||||
- TimeSpan
|
||||
- CustodianObservation
|
||||
- ConfidenceMeasure
|
||||
- ./ReconstructionAgent
|
||||
- ./TimeSpan
|
||||
- ./CustodianObservation
|
||||
- ./ConfidenceMeasure
|
||||
|
||||
classes:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ imports:
|
|||
- ../metadata
|
||||
- ./TimeSpan
|
||||
- ./Jurisdiction
|
||||
- ./RegistrationAuthority
|
||||
- ../slots/jurisdiction
|
||||
- ../slots/description
|
||||
- ../slots/website
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Research Organization Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
ResearchOrganizationType:
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ title: Settlement Class
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- Country
|
||||
- Subregion
|
||||
- ./Country
|
||||
- ./Subregion
|
||||
- ../slots/country
|
||||
- ../slots/subregion
|
||||
- ../slots/geonames_id
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ imports:
|
|||
- ./StorageConditionPolicy
|
||||
- ../enums/StorageTypeEnum
|
||||
- ../enums/StorageStandardEnum
|
||||
- ../slots/storage_location
|
||||
- ../slots/managed_by
|
||||
|
||||
classes:
|
||||
Storage:
|
||||
|
|
@ -451,9 +453,8 @@ slots:
|
|||
description: Description of storage facility
|
||||
range: string
|
||||
|
||||
storage_location:
|
||||
description: Physical location (AuxiliaryPlace)
|
||||
range: AuxiliaryPlace
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
capacity_description:
|
||||
description: Qualitative capacity description
|
||||
|
|
@ -494,6 +495,4 @@ slots:
|
|||
range: StorageCondition
|
||||
multivalued: true
|
||||
|
||||
managed_by:
|
||||
description: Managing organizational unit
|
||||
range: string
|
||||
# NOTE: managed_by imported from global slot ../slots/managed_by.yaml
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ title: Subregion Class
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- Country
|
||||
- ./Country
|
||||
- ../slots/country
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# Global Slot: altitude
|
||||
# Altitude above sea level in meters
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/altitude
|
||||
name: altitude-slot
|
||||
title: Altitude Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
wgs84: http://www.w3.org/2003/01/geo/wgs84_pos#
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
altitude:
|
||||
slot_uri: wgs84:alt
|
||||
range: float
|
||||
description: >-
|
||||
Altitude above sea level (meters).
|
||||
Optional - use for elevated or underground locations.
|
||||
exact_mappings:
|
||||
- wgs84:alt
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Global Slot: documentation_url
|
||||
# URL to documentation for a project, system, or resource
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/documentation_url
|
||||
name: documentation-url-slot
|
||||
title: Documentation URL Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
schema: http://schema.org/
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
documentation_url:
|
||||
slot_uri: schema:documentation
|
||||
description: URL to documentation for this entity
|
||||
range: uri
|
||||
exact_mappings:
|
||||
- schema:documentation
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Global Slot: managed_by
|
||||
# Identifies the entity or organizational unit managing something
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/managed_by
|
||||
name: managed-by-slot
|
||||
title: Managed By Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
org: http://www.w3.org/ns/org#
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
managed_by:
|
||||
slot_uri: org:linkedTo
|
||||
description: Entity or organizational unit managing this resource
|
||||
range: string
|
||||
exact_mappings:
|
||||
- org:linkedTo
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Global Slot: storage_location
|
||||
# Physical or logical location where materials are stored
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/storage_location
|
||||
name: storage-location-slot
|
||||
title: Storage Location Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
premis: http://www.loc.gov/standards/premis/rdf/v3/
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
storage_location:
|
||||
slot_uri: premis:storedAt
|
||||
description: >-
|
||||
Physical or logical location where materials are stored.
|
||||
Range varies by context - can be AuxiliaryPlace, Storage, or string.
|
||||
range: uriorcurie
|
||||
exact_mappings:
|
||||
- premis:storedAt
|
||||
|
|
@ -10,9 +10,9 @@ import {
|
|||
Navigate,
|
||||
} from 'react-router-dom';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||
import { Layout } from './components/layout/Layout';
|
||||
import { HomeLayout } from './components/layout/HomeLayout';
|
||||
import { Visualize } from './pages/Visualize';
|
||||
import { Database } from './pages/Database';
|
||||
import { Settings } from './pages/Settings';
|
||||
|
|
@ -26,27 +26,12 @@ import ProjectPlanPage from './pages/ProjectPlanPage';
|
|||
import './App.css';
|
||||
|
||||
// Create router configuration with protected routes
|
||||
// All pages use the standard Layout with navigation at the top
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginPage />,
|
||||
},
|
||||
// Home page with navigation at bottom
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<HomeLayout />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <ProjectPlanPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Other pages with navigation at top
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
|
|
@ -55,6 +40,11 @@ const router = createBrowserRouter([
|
|||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
// Home page shows the Project Plan
|
||||
index: true,
|
||||
element: <ProjectPlanPage />,
|
||||
},
|
||||
{
|
||||
path: 'visualize',
|
||||
element: <Visualize />,
|
||||
|
|
@ -98,9 +88,11 @@ const router = createBrowserRouter([
|
|||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
/**
|
||||
* Home Layout Styles
|
||||
*
|
||||
* Navigation appears at the very bottom of the page.
|
||||
*/
|
||||
|
||||
.home-layout {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.home-layout-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Bottom section containing navigation and footer */
|
||||
.home-layout-bottom {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Override navigation styles for bottom placement */
|
||||
.home-layout .navigation {
|
||||
position: relative;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
/* Footer at the very bottom */
|
||||
.home-layout-footer {
|
||||
background: #172a59;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
border-top: 3px solid #0a3dfa;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.home-layout-footer .footer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.home-layout-footer .footer-copyright {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.home-layout-footer .footer-copyright a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.home-layout-footer .footer-copyright a:hover {
|
||||
color: #fa5200;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/**
|
||||
* Home Layout Component - Navigation at bottom
|
||||
*
|
||||
* This layout places the navigation bar at the very bottom of the page,
|
||||
* so users only see it when scrolling all the way down.
|
||||
*
|
||||
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
|
||||
*/
|
||||
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Navigation } from './Navigation';
|
||||
import './HomeLayout.css';
|
||||
|
||||
export function HomeLayout() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<div className="home-layout">
|
||||
<div className="home-layout-content">
|
||||
<Outlet />
|
||||
|
||||
{/* Navigation appears at the bottom after all content */}
|
||||
<div className="home-layout-bottom">
|
||||
<Navigation />
|
||||
<footer className="home-layout-footer">
|
||||
<div className="footer-content">
|
||||
<div className="footer-copyright">
|
||||
© {currentYear}{' '}
|
||||
<a href="https://netwerkdigitaalerfgoed.nl" target="_blank" rel="noopener noreferrer">
|
||||
Netwerk Digitaal Erfgoed
|
||||
</a>
|
||||
{' & '}
|
||||
<a href="https://textpast.com" target="_blank" rel="noopener noreferrer">
|
||||
TextPast
|
||||
</a>
|
||||
. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -25,74 +25,17 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
/* Footer Styles - now inside scrollable area */
|
||||
/* Footer Styles - minimal, at the very bottom */
|
||||
.layout-footer {
|
||||
background: #172a59; /* NDE dark blue */
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
border-top: 3px solid #0a3dfa; /* NDE primary blue accent */
|
||||
background: transparent;
|
||||
color: rgba(23, 42, 89, 0.5); /* Subtle NDE dark blue */
|
||||
padding: 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
margin-top: auto; /* Push to bottom of flex container */
|
||||
flex-shrink: 0; /* Don't shrink */
|
||||
}
|
||||
|
||||
/* Minimal footer for full-screen pages */
|
||||
.layout-footer.footer-minimal {
|
||||
display: none; /* Hide completely on fullscreen pages */
|
||||
}
|
||||
|
||||
.layout-footer.footer-minimal .footer-content {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-footer.footer-minimal .footer-copyright {
|
||||
color: rgba(100, 100, 100, 0.7);
|
||||
font-size: 0.75rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-footer.footer-minimal .footer-copyright a {
|
||||
color: rgba(100, 100, 100, 0.8);
|
||||
}
|
||||
|
||||
.layout-footer.footer-minimal .footer-copyright a:hover {
|
||||
color: #0a3dfa;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.footer-copyright a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-copyright a:hover {
|
||||
color: #fa5200; /* NDE orange on hover */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive footer */
|
||||
@media (max-width: 600px) {
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Auth Loading State */
|
||||
.auth-loading {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Outlet, useLocation } from 'react-router-dom';
|
|||
import { Navigation } from './Navigation';
|
||||
import './Layout.css';
|
||||
|
||||
// Pages that should hide the footer completely (it appears at the bottom when scrolling)
|
||||
// Pages that should hide the footer completely
|
||||
const FULLSCREEN_PAGES = ['/visualize', '/map', '/database', '/query-builder', '/linkml', '/ontology', '/stats'];
|
||||
|
||||
export function Layout() {
|
||||
|
|
@ -23,20 +23,12 @@ export function Layout() {
|
|||
<Navigation />
|
||||
<div className="layout-content">
|
||||
<Outlet />
|
||||
{/* Footer only shows on non-fullscreen pages, or at very bottom of scroll */}
|
||||
{/* Minimal footer - only shows on pages with scrollable content */}
|
||||
{!isFullscreenPage && (
|
||||
<footer className="layout-footer">
|
||||
<div className="footer-content">
|
||||
<div className="footer-copyright">
|
||||
© {currentYear}{' '}
|
||||
<a href="https://netwerkdigitaalerfgoed.nl" target="_blank" rel="noopener noreferrer">
|
||||
Netwerk Digitaal Erfgoed
|
||||
</a>
|
||||
{' & '}
|
||||
<a href="https://textpast.com" target="_blank" rel="noopener noreferrer">
|
||||
TextPast
|
||||
</a>
|
||||
. All rights reserved.
|
||||
© {currentYear} Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -75,10 +75,48 @@
|
|||
gap: 2rem; /* Larger gap between links like NDE */
|
||||
}
|
||||
|
||||
/* Language Toggle */
|
||||
.nav-lang-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(23, 42, 89, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 0.35rem 0.6rem;
|
||||
cursor: pointer;
|
||||
font-family: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #172a59;
|
||||
transition: all 0.2s ease;
|
||||
margin-left: auto;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-lang-toggle:hover {
|
||||
border-color: #0a3dfa;
|
||||
background: rgba(10, 61, 250, 0.05);
|
||||
}
|
||||
|
||||
.lang-active {
|
||||
color: #0a3dfa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lang-inactive {
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.lang-separator {
|
||||
color: rgba(23, 42, 89, 0.3);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* User Account Dropdown */
|
||||
.nav-user {
|
||||
position: relative;
|
||||
margin-left: 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-user-btn {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
/**
|
||||
* Navigation Component
|
||||
* Styled following Netwerk Digitaal Erfgoed (NDE) house style
|
||||
* With bilingual support (NL/EN)
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useLanguage, translations } from '../../contexts/LanguageContext';
|
||||
import './Navigation.css';
|
||||
|
||||
export function Navigation() {
|
||||
const location = useLocation();
|
||||
const { user, logout } = useAuth();
|
||||
const { language, toggleLanguage } = useLanguage();
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -29,6 +32,11 @@ export function Navigation() {
|
|||
return location.pathname === path;
|
||||
};
|
||||
|
||||
// Get translated nav text
|
||||
const t = (key: keyof typeof translations.nav) => {
|
||||
return language === 'en' ? translations.nav[key].en : translations.nav[key].nl;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navigation">
|
||||
<div className="nav-container">
|
||||
|
|
@ -47,58 +55,70 @@ export function Navigation() {
|
|||
to="/"
|
||||
className={`nav-link ${isActive('/') ? 'active' : ''}`}
|
||||
>
|
||||
Home
|
||||
{t('home')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/visualize"
|
||||
className={`nav-link ${isActive('/visualize') ? 'active' : ''}`}
|
||||
>
|
||||
Visualize
|
||||
{t('visualize')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/database"
|
||||
className={`nav-link ${isActive('/database') ? 'active' : ''}`}
|
||||
>
|
||||
Database
|
||||
{t('database')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/query-builder"
|
||||
className={`nav-link ${isActive('/query-builder') ? 'active' : ''}`}
|
||||
>
|
||||
Query Builder
|
||||
{t('queryBuilder')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/linkml"
|
||||
className={`nav-link ${isActive('/linkml') ? 'active' : ''}`}
|
||||
>
|
||||
LinkML
|
||||
{t('linkml')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/ontology"
|
||||
className={`nav-link ${isActive('/ontology') ? 'active' : ''}`}
|
||||
>
|
||||
Ontology
|
||||
{t('ontology')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/map"
|
||||
className={`nav-link ${isActive('/map') ? 'active' : ''}`}
|
||||
>
|
||||
Map
|
||||
{t('map')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/stats"
|
||||
className={`nav-link ${isActive('/stats') ? 'active' : ''}`}
|
||||
>
|
||||
Stats
|
||||
{t('stats')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/settings"
|
||||
className={`nav-link ${isActive('/settings') ? 'active' : ''}`}
|
||||
>
|
||||
Settings
|
||||
{t('settings')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<button
|
||||
className="nav-lang-toggle"
|
||||
onClick={toggleLanguage}
|
||||
aria-label={language === 'nl' ? 'Switch to English' : 'Schakel naar Nederlands'}
|
||||
title={language === 'nl' ? 'Switch to English' : 'Schakel naar Nederlands'}
|
||||
>
|
||||
<span className={language === 'nl' ? 'lang-active' : 'lang-inactive'}>NL</span>
|
||||
<span className="lang-separator">|</span>
|
||||
<span className={language === 'en' ? 'lang-active' : 'lang-inactive'}>EN</span>
|
||||
</button>
|
||||
|
||||
{/* User Account Dropdown */}
|
||||
{user && (
|
||||
<div className="nav-user" ref={userMenuRef}>
|
||||
|
|
@ -118,7 +138,7 @@ export function Navigation() {
|
|||
<span className="nav-user-role">{user.role}</span>
|
||||
</div>
|
||||
<button onClick={logout} className="nav-user-logout">
|
||||
Sign Out
|
||||
{t('signOut')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
174
frontend/src/contexts/LanguageContext.tsx
Normal file
174
frontend/src/contexts/LanguageContext.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Language Context for bilingual support (NL/EN)
|
||||
*
|
||||
* Provides a global language state that can be used across all pages.
|
||||
* LinkML and ontology descriptions should remain in their original language.
|
||||
*
|
||||
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||||
|
||||
export type Language = 'nl' | 'en';
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
toggleLanguage: () => void;
|
||||
t: (nl: string, en?: string) => string;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
interface LanguageProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LanguageProvider({ children }: LanguageProviderProps) {
|
||||
// Default to Dutch as primary language
|
||||
const [language, setLanguage] = useState<Language>(() => {
|
||||
// Check localStorage for saved preference
|
||||
const saved = localStorage.getItem('glam-language');
|
||||
return (saved === 'en' || saved === 'nl') ? saved : 'nl';
|
||||
});
|
||||
|
||||
const handleSetLanguage = useCallback((lang: Language) => {
|
||||
setLanguage(lang);
|
||||
localStorage.setItem('glam-language', lang);
|
||||
}, []);
|
||||
|
||||
const toggleLanguage = useCallback(() => {
|
||||
const newLang = language === 'nl' ? 'en' : 'nl';
|
||||
handleSetLanguage(newLang);
|
||||
}, [language, handleSetLanguage]);
|
||||
|
||||
// Translation helper - returns English text if available and language is EN, otherwise Dutch
|
||||
const t = useCallback((nl: string, en?: string): string => {
|
||||
return language === 'en' && en ? en : nl;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage, toggleLanguage, t }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
const context = useContext(LanguageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Common translations for UI elements
|
||||
export const translations = {
|
||||
// Navigation
|
||||
nav: {
|
||||
home: { nl: 'Home', en: 'Home' },
|
||||
visualize: { nl: 'Visualiseren', en: 'Visualize' },
|
||||
database: { nl: 'Database', en: 'Database' },
|
||||
queryBuilder: { nl: 'Query Builder', en: 'Query Builder' },
|
||||
linkml: { nl: 'LinkML', en: 'LinkML' },
|
||||
ontology: { nl: 'Ontologie', en: 'Ontology' },
|
||||
map: { nl: 'Kaart', en: 'Map' },
|
||||
stats: { nl: 'Statistieken', en: 'Stats' },
|
||||
settings: { nl: 'Instellingen', en: 'Settings' },
|
||||
signOut: { nl: 'Uitloggen', en: 'Sign Out' },
|
||||
},
|
||||
// Common UI
|
||||
common: {
|
||||
loading: { nl: 'Laden...', en: 'Loading...' },
|
||||
error: { nl: 'Fout', en: 'Error' },
|
||||
search: { nl: 'Zoeken', en: 'Search' },
|
||||
filter: { nl: 'Filteren', en: 'Filter' },
|
||||
save: { nl: 'Opslaan', en: 'Save' },
|
||||
cancel: { nl: 'Annuleren', en: 'Cancel' },
|
||||
close: { nl: 'Sluiten', en: 'Close' },
|
||||
back: { nl: 'Terug', en: 'Back' },
|
||||
next: { nl: 'Volgende', en: 'Next' },
|
||||
previous: { nl: 'Vorige', en: 'Previous' },
|
||||
results: { nl: 'resultaten', en: 'results' },
|
||||
noResults: { nl: 'Geen resultaten gevonden', en: 'No results found' },
|
||||
total: { nl: 'Totaal', en: 'Total' },
|
||||
hours: { nl: 'uren', en: 'hours' },
|
||||
week: { nl: 'week', en: 'week' },
|
||||
status: { nl: 'Status', en: 'Status' },
|
||||
},
|
||||
// Project Plan specific
|
||||
projectPlan: {
|
||||
title: { nl: 'Projectplan', en: 'Project Plan' },
|
||||
timeline: { nl: 'Tijdlijn', en: 'Timeline' },
|
||||
workPackages: { nl: 'Werkpakketten', en: 'Work Packages' },
|
||||
ontologies: { nl: 'Ontologieën', en: 'Ontologies' },
|
||||
outOfScope: { nl: 'Buiten scope', en: 'Out of Scope' },
|
||||
totalHours: { nl: 'Totaal uren', en: 'Total Hours' },
|
||||
deliverables: { nl: 'Deliverables', en: 'Deliverables' },
|
||||
ontologyAlignments: { nl: 'Ontologie verbindingen', en: 'Ontology Alignments' },
|
||||
hoursPerWP: { nl: 'Uren per werkpakket', en: 'Hours per Work Package' },
|
||||
lastUpdated: { nl: 'Laatst bijgewerkt', en: 'Last updated' },
|
||||
commissioner: { nl: 'Opdrachtgever', en: 'Commissioner' },
|
||||
notIncluded: { nl: 'Niet inbegrepen in dit project', en: 'Not Included in This Project' },
|
||||
futureScope: { nl: 'Toekomstig', en: 'Future' },
|
||||
rationale: { nl: 'Reden', en: 'Rationale' },
|
||||
ontologyNetwork: { nl: 'Ontologie Afstemming Netwerk', en: 'Ontology Alignment Network' },
|
||||
networkDescription: {
|
||||
nl: 'Visualisatie van de verbindingen tussen de Bronhouder Ontologie en bestaande ontologieën.',
|
||||
en: 'Visualization of connections between the Heritage Custodian Ontology and existing ontologies.'
|
||||
},
|
||||
extension: { nl: 'Uitbreiding', en: 'Extension' },
|
||||
integration: { nl: 'Integratie', en: 'Integration' },
|
||||
mapping: { nl: 'Mapping', en: 'Mapping' },
|
||||
},
|
||||
// Visualize page
|
||||
visualize: {
|
||||
title: { nl: 'Schema Visualisatie', en: 'Schema Visualization' },
|
||||
selectDiagram: { nl: 'Selecteer diagram', en: 'Select diagram' },
|
||||
zoom: { nl: 'Zoom', en: 'Zoom' },
|
||||
reset: { nl: 'Reset', en: 'Reset' },
|
||||
export: { nl: 'Exporteren', en: 'Export' },
|
||||
},
|
||||
// Database page
|
||||
database: {
|
||||
title: { nl: 'Database Verkenner', en: 'Database Explorer' },
|
||||
tables: { nl: 'Tabellen', en: 'Tables' },
|
||||
records: { nl: 'Records', en: 'Records' },
|
||||
columns: { nl: 'Kolommen', en: 'Columns' },
|
||||
},
|
||||
// Map page
|
||||
map: {
|
||||
title: { nl: 'Erfgoedinstellingen Kaart', en: 'Heritage Institutions Map' },
|
||||
institutions: { nl: 'Instellingen', en: 'Institutions' },
|
||||
province: { nl: 'Provincie', en: 'Province' },
|
||||
city: { nl: 'Plaats', en: 'City' },
|
||||
type: { nl: 'Type', en: 'Type' },
|
||||
},
|
||||
// Stats page
|
||||
stats: {
|
||||
title: { nl: 'Statistieken', en: 'Statistics' },
|
||||
byProvince: { nl: 'Per provincie', en: 'By Province' },
|
||||
byType: { nl: 'Per type', en: 'By Type' },
|
||||
overview: { nl: 'Overzicht', en: 'Overview' },
|
||||
},
|
||||
// Settings page
|
||||
settings: {
|
||||
title: { nl: 'Instellingen', en: 'Settings' },
|
||||
language: { nl: 'Taal', en: 'Language' },
|
||||
theme: { nl: 'Thema', en: 'Theme' },
|
||||
darkMode: { nl: 'Donkere modus', en: 'Dark Mode' },
|
||||
notifications: { nl: 'Notificaties', en: 'Notifications' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Helper to get translation from the translations object
|
||||
export function getTranslation(
|
||||
key: keyof typeof translations,
|
||||
subKey: string,
|
||||
language: Language
|
||||
): string {
|
||||
const section = translations[key] as Record<string, { nl: string; en: string }>;
|
||||
const item = section[subKey];
|
||||
if (!item) return subKey;
|
||||
return language === 'en' ? item.en : item.nl;
|
||||
}
|
||||
|
|
@ -21,8 +21,6 @@ import {
|
|||
LinearProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
|
|
@ -52,6 +50,7 @@ import {
|
|||
} from '@mui/icons-material';
|
||||
import * as d3 from 'd3';
|
||||
import yaml from 'js-yaml';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import './ProjectPlanPage.css';
|
||||
|
||||
// NDE-inspired theme
|
||||
|
|
@ -194,9 +193,9 @@ interface ProjectPlan {
|
|||
modified_date: string;
|
||||
}
|
||||
|
||||
// Helper to get bilingual text (using Language type from context)
|
||||
type Language = 'nl' | 'en';
|
||||
|
||||
// Helper to get bilingual text
|
||||
const getText = (nl: string, en: string | undefined, lang: Language): string => {
|
||||
return lang === 'en' && en ? en : nl;
|
||||
};
|
||||
|
|
@ -235,7 +234,7 @@ export default function ProjectPlanPage() {
|
|||
const [projectData, setProjectData] = useState<ProjectPlan | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [language, setLanguage] = useState<Language>('nl');
|
||||
const { language } = useLanguage(); // Use global language context
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const timelineRef = useRef<SVGSVGElement>(null);
|
||||
const networkRef = useRef<SVGSVGElement>(null);
|
||||
|
|
@ -504,10 +503,6 @@ export default function ProjectPlanPage() {
|
|||
}
|
||||
}, [projectData, activeTab, drawTimeline, drawNetwork]);
|
||||
|
||||
const handleLanguageChange = (_: React.MouseEvent<HTMLElement>, newLang: Language | null) => {
|
||||
if (newLang) setLanguage(newLang);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ThemeProvider theme={ndeTheme}>
|
||||
|
|
@ -550,15 +545,6 @@ export default function ProjectPlanPage() {
|
|||
{getText(projectData.plan_description, projectData.plan_description_en, language).substring(0, 200)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={language}
|
||||
exclusive
|
||||
onChange={handleLanguageChange}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="nl">NL</ToggleButton>
|
||||
<ToggleButton value="en">EN</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
{/* Summary Cards */}
|
||||
|
|
@ -834,17 +820,6 @@ export default function ProjectPlanPage() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<Box mt={4} textAlign="center">
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{language === 'nl' ? 'Laatst bijgewerkt: ' : 'Last updated: '}{projectData.modified_date}
|
||||
{' | '}
|
||||
{language === 'nl' ? 'Opdrachtgever: ' : 'Commissioner: '}
|
||||
<a href={projectData.commissioning_organization.agent_url} target="_blank" rel="noopener noreferrer">
|
||||
{projectData.commissioning_organization.agent_name}
|
||||
</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -255,6 +255,112 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Generate Section */
|
||||
.generate-section {
|
||||
padding: 1rem 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.generate-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #172a59;
|
||||
}
|
||||
|
||||
.generate-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.generate-group {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.generate-group h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #172a59;
|
||||
}
|
||||
|
||||
.generate-desc {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.generate-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: linear-gradient(135deg, #4a7dff 0%, #2c5ce6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.generate-button:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #2c5ce6 0%, #1a4cbb 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(74, 125, 255, 0.3);
|
||||
}
|
||||
|
||||
.generate-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.generate-button--disabled {
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.generate-button--disabled:hover {
|
||||
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.generate-hint {
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-size: 0.6875rem;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Spinning animation for loading state */
|
||||
@keyframes spinning {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spinning 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Available Schemas Section */
|
||||
.schemas-section {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
|
|
|||
|
|
@ -14,20 +14,13 @@ import { parseUMLDiagramWithDetails } from '@/components/uml/UMLParser';
|
|||
import type { GraphNode, GraphLink } from '@/types/rdf';
|
||||
import type { UMLDiagram, DagreDirection, DagreRanker } from '@/components/uml/UMLVisualization';
|
||||
import {
|
||||
Search, Menu, X, Download, Image, FileCode, Code, ChevronDown, Upload, FileText
|
||||
Menu, X, Download, Image, FileCode, Code, ChevronDown, Upload, FileText, RefreshCw, Database
|
||||
} from 'lucide-react';
|
||||
import './Visualize.css';
|
||||
|
||||
// File type definitions
|
||||
type SchemaFormat = 'turtle' | 'n-triples' | 'jsonld' | 'mermaid' | 'erdiagram' | 'plantuml' | 'graphviz';
|
||||
|
||||
interface SchemaFile {
|
||||
name: string;
|
||||
path: string;
|
||||
format: SchemaFormat;
|
||||
category: 'rdf' | 'uml';
|
||||
}
|
||||
|
||||
// Detect format from file extension
|
||||
function detectFormat(filename: string): { format: SchemaFormat; category: 'rdf' | 'uml' } {
|
||||
const ext = filename.toLowerCase().split('.').pop() || '';
|
||||
|
|
@ -125,9 +118,7 @@ export function Visualize() {
|
|||
// File and format state
|
||||
const [fileName, setFileName] = useState<string>(persistedState.current?.fileName || '');
|
||||
const [currentCategory, setCurrentCategory] = useState<'rdf' | 'uml' | null>(persistedState.current?.currentCategory || null);
|
||||
const [schemasSectionExpanded, setSchemasSectionExpanded] = useState<boolean>(false);
|
||||
const [loadSectionExpanded, setLoadSectionExpanded] = useState<boolean>(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [customInput, setCustomInput] = useState<string>('');
|
||||
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true);
|
||||
|
||||
|
|
@ -177,136 +168,9 @@ export function Visualize() {
|
|||
const [umlError, setUmlError] = useState<string | null>(null);
|
||||
const isLoading = dbLoading || parserLoading || umlLoading;
|
||||
|
||||
// Available schema files - combined RDF and UML
|
||||
const availableSchemas: SchemaFile[] = [
|
||||
// RDF Schemas
|
||||
{
|
||||
name: 'Custodian with Ontology Mappings (Latest)',
|
||||
path: '/schemas/20251121/rdf/custodian_with_ontology_mappings_20251126_193334.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
{
|
||||
name: 'Custodian with Auxiliary Classes',
|
||||
path: '/schemas/20251121/rdf/custodian_with_auxiliary_classes_20251126_101032.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
{
|
||||
name: 'Custodian with Digital Platform',
|
||||
path: '/schemas/20251121/rdf/custodian_with_digital_platform_20251125_115124.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
{
|
||||
name: 'Custodian Name Modular',
|
||||
path: '/schemas/20251121/rdf/01_custodian_name_modular_20251124_002122.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
{
|
||||
name: 'Custodian Multi-Aspect',
|
||||
path: '/schemas/20251121/rdf/custodian_multi_aspect_20251122_155319.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
{
|
||||
name: 'Encompassing Body',
|
||||
path: '/schemas/20251121/rdf/EncompassingBody_20251123_232811.owl.ttl',
|
||||
format: 'turtle',
|
||||
category: 'rdf'
|
||||
},
|
||||
|
||||
// UML Diagrams - Mermaid Class
|
||||
{
|
||||
name: 'Custodian Multi-Aspect (Mermaid Class)',
|
||||
path: '/schemas/20251121/uml/mermaid/custodian_multi_aspect_20251122_155319.mmd',
|
||||
format: 'mermaid',
|
||||
category: 'uml'
|
||||
},
|
||||
{
|
||||
name: 'Custodian Name Modular (Mermaid YuML)',
|
||||
path: '/schemas/20251121/uml/mermaid/01_custodian_name_modular_20251122_182317_yuml.mmd',
|
||||
format: 'mermaid',
|
||||
category: 'uml'
|
||||
},
|
||||
|
||||
// UML Diagrams - ER
|
||||
{
|
||||
name: 'Custodian ER Diagram',
|
||||
path: '/schemas/20251121/uml/erdiagram/custodian_multi_aspect_20251122_171249.mmd',
|
||||
format: 'erdiagram',
|
||||
category: 'uml'
|
||||
},
|
||||
{
|
||||
name: 'Custodian Name Modular (ER)',
|
||||
path: '/schemas/20251121/uml/mermaid/01_custodian_name_modular_20251122_205118_er.mmd',
|
||||
format: 'erdiagram',
|
||||
category: 'uml'
|
||||
},
|
||||
{
|
||||
name: 'Custodian with Ontology Mappings (ER)',
|
||||
path: '/schemas/20251121/uml/mermaid/custodian_with_ontology_mappings_20251126_204859_er.mmd',
|
||||
format: 'erdiagram',
|
||||
category: 'uml'
|
||||
},
|
||||
{
|
||||
name: 'Custodian with Inheritance (ER)',
|
||||
path: '/schemas/20251121/uml/mermaid/custodian_with_inheritance_20251127_211317.mmd',
|
||||
format: 'erdiagram',
|
||||
category: 'uml'
|
||||
},
|
||||
|
||||
// UML Diagrams - PlantUML
|
||||
{
|
||||
name: 'Custodian Multi-Aspect (PlantUML)',
|
||||
path: '/schemas/20251121/uml/plantuml/custodian_multi_aspect_20251122_155319.puml',
|
||||
format: 'plantuml',
|
||||
category: 'uml'
|
||||
},
|
||||
|
||||
// UML Diagrams - GraphViz
|
||||
{
|
||||
name: 'Custodian Multi-Aspect (GraphViz)',
|
||||
path: '/schemas/20251121/uml/graphviz/custodian_multi_aspect_20251122_155319.dot',
|
||||
format: 'graphviz',
|
||||
category: 'uml'
|
||||
},
|
||||
];
|
||||
|
||||
// Filter schemas based on search
|
||||
const filteredSchemas = availableSchemas.filter(schema =>
|
||||
schema.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
// Group schemas by category
|
||||
const rdfSchemas = filteredSchemas.filter(s => s.category === 'rdf');
|
||||
const umlSchemas = filteredSchemas.filter(s => s.category === 'uml');
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (exportDropdownRef.current && !exportDropdownRef.current.contains(event.target as Node)) {
|
||||
setExportDropdownOpen(false);
|
||||
}
|
||||
if (layoutDropdownRef.current && !layoutDropdownRef.current.contains(event.target as Node)) {
|
||||
setLayoutDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Persist state to sessionStorage whenever relevant state changes
|
||||
useEffect(() => {
|
||||
saveStateToSession({
|
||||
fileName,
|
||||
currentCategory,
|
||||
umlFileContent,
|
||||
umlDiagram,
|
||||
});
|
||||
}, [fileName, currentCategory, umlFileContent, umlDiagram]);
|
||||
// Generator state
|
||||
const [generatingUml, setGeneratingUml] = useState(false);
|
||||
const [_generatingRdf, setGeneratingRdf] = useState(false);
|
||||
|
||||
// Clear current visualization
|
||||
const clearVisualization = useCallback(() => {
|
||||
|
|
@ -316,22 +180,6 @@ export function Visualize() {
|
|||
// Reset RDF graph data would require loadGraphData with empty result
|
||||
}, []);
|
||||
|
||||
// Load RDF content
|
||||
const loadRdfContent = useCallback(
|
||||
async (content: string, format: 'turtle' | 'n-triples' | 'jsonld') => {
|
||||
clearVisualization();
|
||||
setCurrentCategory('rdf');
|
||||
|
||||
const mimeType = format === 'turtle' ? 'text/turtle' :
|
||||
format === 'n-triples' ? 'application/n-triples' :
|
||||
'application/ld+json';
|
||||
|
||||
const result = await parse(content, mimeType);
|
||||
loadGraphData(result);
|
||||
},
|
||||
[parse, loadGraphData, clearVisualization]
|
||||
);
|
||||
|
||||
// Load UML content
|
||||
const loadUmlContent = useCallback(
|
||||
(content: string, name: string) => {
|
||||
|
|
@ -372,6 +220,88 @@ export function Visualize() {
|
|||
[clearVisualization]
|
||||
);
|
||||
|
||||
// Generate UML from LinkML schema (fetches pre-generated file)
|
||||
const handleGenerateUml = useCallback(async () => {
|
||||
setGeneratingUml(true);
|
||||
setUmlError(null);
|
||||
|
||||
try {
|
||||
// Fetch the pre-generated Mermaid file from public folder
|
||||
const response = await fetch('/data/heritage_custodian_ontology.mmd');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load UML diagram: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const mermaidContent = await response.text();
|
||||
|
||||
if (mermaidContent) {
|
||||
setFileName('Heritage Custodian Ontology (Generated)');
|
||||
loadUmlContent(mermaidContent, 'Heritage Custodian Ontology');
|
||||
} else {
|
||||
throw new Error('No Mermaid diagram content found');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading UML:', err);
|
||||
setUmlError(err instanceof Error ? err.message : 'Failed to load UML diagram');
|
||||
} finally {
|
||||
setGeneratingUml(false);
|
||||
}
|
||||
}, [loadUmlContent]);
|
||||
|
||||
// Generate RDF overview (placeholder for future implementation)
|
||||
const handleGenerateRdf = useCallback(async () => {
|
||||
setGeneratingRdf(true);
|
||||
|
||||
try {
|
||||
// Placeholder - will be implemented after converting enriched entries to instances
|
||||
setUmlError('RDF generation coming soon! This feature will be available after converting enriched entries to Heritage Custodian Ontology instances.');
|
||||
} finally {
|
||||
setGeneratingRdf(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (exportDropdownRef.current && !exportDropdownRef.current.contains(event.target as Node)) {
|
||||
setExportDropdownOpen(false);
|
||||
}
|
||||
if (layoutDropdownRef.current && !layoutDropdownRef.current.contains(event.target as Node)) {
|
||||
setLayoutDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Persist state to sessionStorage whenever relevant state changes
|
||||
useEffect(() => {
|
||||
saveStateToSession({
|
||||
fileName,
|
||||
currentCategory,
|
||||
umlFileContent,
|
||||
umlDiagram,
|
||||
});
|
||||
}, [fileName, currentCategory, umlFileContent, umlDiagram]);
|
||||
|
||||
// Load RDF content
|
||||
const loadRdfContent = useCallback(
|
||||
async (content: string, format: 'turtle' | 'n-triples' | 'jsonld') => {
|
||||
clearVisualization();
|
||||
setCurrentCategory('rdf');
|
||||
|
||||
const mimeType = format === 'turtle' ? 'text/turtle' :
|
||||
format === 'n-triples' ? 'application/n-triples' :
|
||||
'application/ld+json';
|
||||
|
||||
const result = await parse(content, mimeType);
|
||||
loadGraphData(result);
|
||||
},
|
||||
[parse, loadGraphData, clearVisualization]
|
||||
);
|
||||
|
||||
// Handle file upload (unified)
|
||||
const handleFileUpload = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -405,33 +335,6 @@ export function Visualize() {
|
|||
[loadRdfContent, loadUmlContent]
|
||||
);
|
||||
|
||||
// Handle loading predefined schema file
|
||||
const handleLoadSchemaFile = useCallback(
|
||||
async (schema: SchemaFile) => {
|
||||
setFileName(schema.name);
|
||||
|
||||
try {
|
||||
const response = await fetch(schema.path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const content = await response.text();
|
||||
|
||||
if (schema.category === 'rdf') {
|
||||
await loadRdfContent(content, schema.format as 'turtle' | 'n-triples' | 'jsonld');
|
||||
} else {
|
||||
loadUmlContent(content, schema.name);
|
||||
}
|
||||
|
||||
setSchemasSectionExpanded(false);
|
||||
} catch (err) {
|
||||
console.error('Failed to load schema file:', err);
|
||||
}
|
||||
},
|
||||
[loadRdfContent, loadUmlContent]
|
||||
);
|
||||
|
||||
// Handle custom input (paste)
|
||||
const handleLoadCustomInput = useCallback(async () => {
|
||||
if (!customInput.trim()) return;
|
||||
|
|
@ -563,27 +466,6 @@ export function Visualize() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="search-section">
|
||||
<Search size={16} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search schemas..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="search-clear"
|
||||
onClick={() => setSearchQuery('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Load Data Section */}
|
||||
<div className="load-section">
|
||||
<button
|
||||
|
|
@ -639,82 +521,59 @@ export function Visualize() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Available Schemas */}
|
||||
<div className="schemas-section">
|
||||
<button
|
||||
className="schemas-header"
|
||||
onClick={() => setSchemasSectionExpanded(!schemasSectionExpanded)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
<span>Available Schemas</span>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
width="16"
|
||||
height="16"
|
||||
className={`chevron ${schemasSectionExpanded ? 'expanded' : ''}`}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Generate Section */}
|
||||
<div className="generate-section">
|
||||
<h3 className="generate-header">
|
||||
<RefreshCw size={16} />
|
||||
<span>Generate Visualizations</span>
|
||||
</h3>
|
||||
|
||||
{schemasSectionExpanded && (
|
||||
<div className="schemas-list">
|
||||
{/* RDF Schemas */}
|
||||
{rdfSchemas.length > 0 && (
|
||||
<div className="schema-group">
|
||||
<h4>RDF Schemas</h4>
|
||||
{rdfSchemas.map((schema, index) => (
|
||||
<button
|
||||
key={`rdf-${index}`}
|
||||
className={`schema-button ${fileName === schema.name ? 'active' : ''}`}
|
||||
onClick={() => handleLoadSchemaFile(schema)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
{schema.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UML Schemas */}
|
||||
{umlSchemas.length > 0 && (
|
||||
<div className="schema-group">
|
||||
<h4>UML Diagrams</h4>
|
||||
{umlSchemas.map((schema, index) => (
|
||||
<button
|
||||
key={`uml-${index}`}
|
||||
className={`schema-button ${fileName === schema.name ? 'active' : ''}`}
|
||||
onClick={() => handleLoadSchemaFile(schema)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="9" y1="9" x2="15" y2="9" />
|
||||
<line x1="9" y1="15" x2="15" y2="15" />
|
||||
</svg>
|
||||
{schema.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredSchemas.length === 0 && (
|
||||
<p className="no-results">No schemas found for "{searchQuery}"</p>
|
||||
)}
|
||||
<div className="generate-content">
|
||||
{/* UML Diagram Generator */}
|
||||
<div className="generate-group">
|
||||
<h4>UML Diagram</h4>
|
||||
<p className="generate-desc">
|
||||
Generate from LinkML schema (Heritage Custodian Ontology)
|
||||
</p>
|
||||
<button
|
||||
className="generate-button"
|
||||
onClick={handleGenerateUml}
|
||||
disabled={generatingUml || isLoading}
|
||||
>
|
||||
{generatingUml ? (
|
||||
<>
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={16} />
|
||||
Generate UML
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RDF Overview Generator */}
|
||||
<div className="generate-group">
|
||||
<h4>RDF Overview</h4>
|
||||
<p className="generate-desc">
|
||||
Generate RDF graph of all heritage custodians
|
||||
</p>
|
||||
<button
|
||||
className="generate-button generate-button--disabled"
|
||||
onClick={handleGenerateRdf}
|
||||
disabled={true}
|
||||
title="Coming soon after converting enriched entries to ontology instances"
|
||||
>
|
||||
<Database size={16} />
|
||||
Coming Soon
|
||||
</button>
|
||||
<p className="generate-hint">
|
||||
Available after converting enriched entries to instances
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current File */}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ imports:
|
|||
- linkml:types
|
||||
- ../metadata
|
||||
- ../enums/AppellationTypeEnum
|
||||
- CustodianName
|
||||
- ./CustodianName
|
||||
|
||||
classes:
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Archive Organization Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/access_policy
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name: BioCustodianType
|
|||
title: Biological and Zoological Custodian Type Classification
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/collection_size
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ imports:
|
|||
- linkml:types
|
||||
- ../enums/CallForApplicationStatusEnum
|
||||
- ../enums/FundingRequirementTypeEnum
|
||||
- FundingRequirement
|
||||
- ./FundingRequirement
|
||||
- ../slots/contact_email
|
||||
- ../slots/keywords
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ imports:
|
|||
- ./CustodianObservation
|
||||
- ./ReconstructionActivity
|
||||
- ./TimeSpan
|
||||
- ../slots/documentation_url
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -701,9 +702,7 @@ slots:
|
|||
description: Vendor website URL
|
||||
range: uri
|
||||
|
||||
documentation_url:
|
||||
description: Documentation URL
|
||||
range: uri
|
||||
# NOTE: documentation_url imported from global slot ../slots/documentation_url.yaml
|
||||
|
||||
repository_url:
|
||||
description: Source code repository URL
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ imports:
|
|||
- ./Storage
|
||||
- ../enums/ArchiveProcessingStatusEnum
|
||||
- ../slots/access_restrictions
|
||||
- ../slots/storage_location
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -680,9 +681,8 @@ slots:
|
|||
description: Estimated physical/digital extent
|
||||
range: string
|
||||
|
||||
storage_location:
|
||||
description: Physical storage location(s)
|
||||
range: Storage
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
tracked_in_cms:
|
||||
description: CMS tracking this accession
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ imports:
|
|||
- ../slots/oai_pmh_endpoint
|
||||
- ../slots/platform_type
|
||||
- ../slots/platform_name
|
||||
- ../slots/storage_location
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -776,10 +777,8 @@ slots:
|
|||
|
||||
# NOTE: preservation_level imported from global slot ../slots/preservation_level.yaml
|
||||
|
||||
storage_location:
|
||||
slot_uri: premis:storedAt
|
||||
description: Primary storage location for digital content
|
||||
range: string
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
fixity_check_date:
|
||||
slot_uri: premis:fixity
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ see_also:
|
|||
- https://www.wikidata.org/wiki/Q132560468 # university archive
|
||||
|
||||
imports:
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
prefixes:
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Gallery Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
GalleryType:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ prefixes:
|
|||
imports:
|
||||
- linkml:types
|
||||
- ../metadata
|
||||
- ../slots/geonames_id
|
||||
- ../slots/latitude
|
||||
- ../slots/longitude
|
||||
- ../slots/altitude
|
||||
|
||||
types:
|
||||
WktLiteral:
|
||||
|
|
@ -43,39 +47,9 @@ slots:
|
|||
range: uriorcurie
|
||||
description: "Unique identifier for this geospatial place"
|
||||
|
||||
latitude:
|
||||
range: float
|
||||
slot_uri: wgs84:lat
|
||||
description: >-
|
||||
WGS84 latitude coordinate (decimal degrees).
|
||||
Positive = North, Negative = South.
|
||||
minimum_value: -90.0
|
||||
maximum_value: 90.0
|
||||
examples:
|
||||
- value: 52.3600
|
||||
description: "Amsterdam latitude"
|
||||
|
||||
longitude:
|
||||
range: float
|
||||
slot_uri: wgs84:long
|
||||
description: >-
|
||||
WGS84 longitude coordinate (decimal degrees).
|
||||
Positive = East, Negative = West.
|
||||
minimum_value: -180.0
|
||||
maximum_value: 180.0
|
||||
examples:
|
||||
- value: 4.8852
|
||||
description: "Amsterdam longitude"
|
||||
|
||||
altitude:
|
||||
range: float
|
||||
slot_uri: wgs84:alt
|
||||
description: >-
|
||||
Altitude above sea level (meters).
|
||||
Optional - use for elevated or underground locations.
|
||||
examples:
|
||||
- value: -2.0
|
||||
description: "Amsterdam (below sea level)"
|
||||
# NOTE: latitude imported from global slot ../slots/latitude.yaml
|
||||
# NOTE: longitude imported from global slot ../slots/longitude.yaml
|
||||
# NOTE: altitude imported from global slot ../slots/altitude.yaml
|
||||
|
||||
geometry_wkt:
|
||||
range: string
|
||||
|
|
@ -119,22 +93,7 @@ slots:
|
|||
- value: "EPSG:28992"
|
||||
description: "Dutch Rijksdriehoeksstelsel"
|
||||
|
||||
geonames_id:
|
||||
range: integer
|
||||
slot_uri: geonames:geonameId
|
||||
description: >-
|
||||
GeoNames numeric identifier.
|
||||
Resolves to https://www.geonames.org/{id}/
|
||||
|
||||
Use for:
|
||||
- Linking to GeoNames knowledge base
|
||||
- Disambiguating place names (41 "Springfield"s in USA)
|
||||
- Accessing hierarchical administrative data
|
||||
examples:
|
||||
- value: 2759794
|
||||
description: "Amsterdam (GeoNames ID)"
|
||||
- value: 6930126
|
||||
description: "Rijksmuseum building"
|
||||
# NOTE: geonames_id imported from global slot ../slots/geonames_id.yaml
|
||||
|
||||
osm_id:
|
||||
range: string
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ imports:
|
|||
- ./TimeSpan
|
||||
- ../enums/GiftShopTypeEnum
|
||||
- ../enums/ProductCategoryEnum
|
||||
- ../slots/staff_count
|
||||
- ../slots/managed_by
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
|
|
@ -747,17 +749,13 @@ slots:
|
|||
description: Visitor to purchase conversion rate
|
||||
range: float
|
||||
|
||||
staff_count:
|
||||
description: Number of shop staff
|
||||
range: integer
|
||||
# NOTE: staff_count imported from global slot ../slots/staff_count.yaml
|
||||
|
||||
square_meters:
|
||||
description: Retail floor space
|
||||
range: float
|
||||
|
||||
managed_by:
|
||||
description: Management structure
|
||||
range: string
|
||||
# NOTE: managed_by imported from global slot ../slots/managed_by.yaml
|
||||
|
||||
supplier_relationships:
|
||||
description: Key supplier relationships
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ see_also:
|
|||
- https://www.wikidata.org/wiki/Q15755503 # archaeological society
|
||||
|
||||
imports:
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
prefixes:
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Library Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/cataloging_standard
|
||||
|
||||
classes:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Museum Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
- ../slots/collection_focus
|
||||
- ../slots/cataloging_standard
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Official Institution Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
OfficialInstitutionType:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ imports:
|
|||
- ../slots/funding_source
|
||||
- ../slots/contact_email
|
||||
- ../slots/keywords
|
||||
- ../slots/documentation_url
|
||||
|
||||
default_prefix: hc
|
||||
|
||||
|
|
@ -86,9 +87,7 @@ slots:
|
|||
range: uriorcurie
|
||||
multivalued: true
|
||||
description: Related or predecessor/successor projects
|
||||
documentation_url:
|
||||
range: uri
|
||||
description: URL to project documentation
|
||||
# NOTE: documentation_url imported from global slot ../slots/documentation_url.yaml
|
||||
|
||||
# NOTE: contact_email imported from global slot ../slots/contact_email.yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ imports:
|
|||
- linkml:types
|
||||
- ../metadata
|
||||
- ../enums/ReconstructionActivityTypeEnum
|
||||
- ReconstructionAgent
|
||||
- TimeSpan
|
||||
- CustodianObservation
|
||||
- ConfidenceMeasure
|
||||
- ./ReconstructionAgent
|
||||
- ./TimeSpan
|
||||
- ./CustodianObservation
|
||||
- ./ConfidenceMeasure
|
||||
|
||||
classes:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ imports:
|
|||
- ../metadata
|
||||
- ./TimeSpan
|
||||
- ./Jurisdiction
|
||||
- ./RegistrationAuthority
|
||||
- ../slots/jurisdiction
|
||||
- ../slots/description
|
||||
- ../slots/website
|
||||
|
|
@ -135,157 +136,8 @@ classes:
|
|||
range: TimeSpan
|
||||
required: true
|
||||
|
||||
RegistrationAuthority:
|
||||
class_uri: gleif-base:RegistrationAuthority
|
||||
description: >-
|
||||
Authority that maintains official registrations of organizations.
|
||||
|
||||
**Ontology Alignment:**
|
||||
|
||||
- gleif-base:RegistrationAuthority - "An organization that is responsible for
|
||||
maintaining a registry and provides registration services."
|
||||
|
||||
A RegistrationAuthority is the **organization** that maintains one or more
|
||||
trade registers, distinct from the TradeRegister itself (the database/system).
|
||||
|
||||
**Key Distinction:**
|
||||
- RegistrationAuthority: The organization (e.g., "Kamer van Koophandel", "Companies House")
|
||||
- TradeRegister: The register/database (e.g., "Handelsregister", "Companies Register")
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Netherlands: Kamer van Koophandel (KvK) - GLEIF RA000439
|
||||
- UK: Companies House - GLEIF RA000585
|
||||
- Germany: Amtsgericht München (local court) - GLEIF RA000385
|
||||
- Japan: Legal Affairs Bureau (法務局) - GLEIF RA000429
|
||||
- Ireland: Companies Registration Office (CRO) - GLEIF RA000421
|
||||
|
||||
**GLEIF Integration:**
|
||||
|
||||
GLEIF maintains the Registration Authorities List (RAL) with 1,050+ authorities.
|
||||
Each authority has a unique RA code (format: RA followed by 6 digits).
|
||||
|
||||
Reference: https://www.gleif.org/en/about-lei/code-lists/registration-authorities-list
|
||||
|
||||
See also:
|
||||
- TradeRegister: Registers maintained by this authority
|
||||
- Jurisdiction: Geographic/legal scope of the authority
|
||||
- RegistrationNumber: Numbers issued through this authority's registers
|
||||
|
||||
exact_mappings:
|
||||
- gleif-base:RegistrationAuthority
|
||||
close_mappings:
|
||||
- org:Organization
|
||||
- schema:GovernmentOrganization
|
||||
related_mappings:
|
||||
- rov:hasRegisteredOrganization
|
||||
|
||||
attributes:
|
||||
id:
|
||||
identifier: true
|
||||
slot_uri: schema:identifier
|
||||
description: Unique identifier for the registration authority
|
||||
range: uriorcurie
|
||||
required: true
|
||||
|
||||
name:
|
||||
slot_uri: gleif-base:hasNameTranslatedEnglish
|
||||
description: >-
|
||||
Official name of the registration authority in English.
|
||||
|
||||
gleif-base:hasNameTranslatedEnglish - "The name used to refer to a person
|
||||
or organization, translated into English."
|
||||
|
||||
Examples:
|
||||
- "Chamber of Commerce" (Netherlands)
|
||||
- "Companies House" (UK)
|
||||
- "Legal Affairs Bureau" (Japan)
|
||||
range: string
|
||||
required: true
|
||||
|
||||
name_local:
|
||||
slot_uri: gleif-base:hasNameLegalLocal
|
||||
description: >-
|
||||
Official name in local language.
|
||||
|
||||
gleif-base:hasNameLegalLocal - "The name used to refer to an person or
|
||||
organization in legal communications in local alphabet"
|
||||
|
||||
Examples:
|
||||
- "Kamer van Koophandel" (Dutch)
|
||||
- "法務局" (Japanese)
|
||||
- "Amtsgericht" (German)
|
||||
range: string
|
||||
|
||||
abbreviation:
|
||||
slot_uri: gleif-base:hasAbbreviationLocal
|
||||
description: >-
|
||||
Common abbreviation.
|
||||
|
||||
gleif-base:hasAbbreviationLocal - "An abbreviation using a language local
|
||||
to the entity identified"
|
||||
|
||||
Examples: "KvK", "CH", "CRO"
|
||||
range: string
|
||||
|
||||
jurisdiction:
|
||||
slot_uri: gleif-base:hasCoverageArea
|
||||
description: >-
|
||||
Geographic/legal jurisdiction of the authority.
|
||||
|
||||
gleif-base:hasCoverageArea - "Indicates a geographic region in which some
|
||||
service is provided, or to which some policy applies"
|
||||
|
||||
Links to Jurisdiction class.
|
||||
range: Jurisdiction
|
||||
required: true
|
||||
inlined: true
|
||||
|
||||
gleif_ra_code:
|
||||
slot_uri: schema:identifier
|
||||
description: >-
|
||||
GLEIF Registration Authority code.
|
||||
|
||||
Format: "RA" followed by 6 digits
|
||||
|
||||
Examples:
|
||||
- RA000439: Netherlands KvK
|
||||
- RA000585: UK Companies House
|
||||
- RA000385: Germany Amtsgericht München
|
||||
|
||||
Reference: https://www.gleif.org/en/about-lei/code-lists/registration-authorities-list
|
||||
range: string
|
||||
pattern: "^RA[0-9]{6}$"
|
||||
|
||||
registers:
|
||||
slot_uri: gleif-base:isManagedBy
|
||||
description: >-
|
||||
Trade registers maintained by this authority.
|
||||
|
||||
Inverse of TradeRegister.maintained_by.
|
||||
|
||||
Examples:
|
||||
- KvK maintains: Handelsregister
|
||||
- Companies House maintains: Companies Register, LLP Register
|
||||
range: TradeRegister
|
||||
multivalued: true
|
||||
inlined: false
|
||||
|
||||
website:
|
||||
slot_uri: gleif-base:hasWebsite
|
||||
description: >-
|
||||
Official website of the registration authority.
|
||||
|
||||
gleif-base:hasWebsite - "A website associated with something"
|
||||
range: uri
|
||||
|
||||
registration_types:
|
||||
slot_uri: schema:knowsAbout
|
||||
description: >-
|
||||
Types of entities this authority can register.
|
||||
Examples: ["companies", "charities", "foundations"]
|
||||
range: string
|
||||
multivalued: true
|
||||
# NOTE: RegistrationAuthority class imported from ./RegistrationAuthority.yaml
|
||||
# Contains the full definition of the authority class with GLEIF RA codes.
|
||||
|
||||
GovernanceStructure:
|
||||
class_uri: org:hasUnit
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Research Organization Type Classification
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- CustodianType
|
||||
- ./CustodianType
|
||||
|
||||
classes:
|
||||
ResearchOrganizationType:
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ title: Settlement Class
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- Country
|
||||
- Subregion
|
||||
- ./Country
|
||||
- ./Subregion
|
||||
- ../slots/country
|
||||
- ../slots/subregion
|
||||
- ../slots/geonames_id
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ imports:
|
|||
- ./StorageConditionPolicy
|
||||
- ../enums/StorageTypeEnum
|
||||
- ../enums/StorageStandardEnum
|
||||
- ../slots/storage_location
|
||||
- ../slots/managed_by
|
||||
|
||||
classes:
|
||||
Storage:
|
||||
|
|
@ -451,9 +453,8 @@ slots:
|
|||
description: Description of storage facility
|
||||
range: string
|
||||
|
||||
storage_location:
|
||||
description: Physical location (AuxiliaryPlace)
|
||||
range: AuxiliaryPlace
|
||||
# NOTE: storage_location imported from global slot ../slots/storage_location.yaml
|
||||
# Use slot_usage in class to customize range
|
||||
|
||||
capacity_description:
|
||||
description: Qualitative capacity description
|
||||
|
|
@ -494,6 +495,4 @@ slots:
|
|||
range: StorageCondition
|
||||
multivalued: true
|
||||
|
||||
managed_by:
|
||||
description: Managing organizational unit
|
||||
range: string
|
||||
# NOTE: managed_by imported from global slot ../slots/managed_by.yaml
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ title: Subregion Class
|
|||
|
||||
imports:
|
||||
- linkml:types
|
||||
- Country
|
||||
- ./Country
|
||||
- ../slots/country
|
||||
|
||||
classes:
|
||||
|
|
|
|||
24
schemas/20251121/linkml/modules/slots/altitude.yaml
Normal file
24
schemas/20251121/linkml/modules/slots/altitude.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Global Slot: altitude
|
||||
# Altitude above sea level in meters
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/altitude
|
||||
name: altitude-slot
|
||||
title: Altitude Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
wgs84: http://www.w3.org/2003/01/geo/wgs84_pos#
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
altitude:
|
||||
slot_uri: wgs84:alt
|
||||
range: float
|
||||
description: >-
|
||||
Altitude above sea level (meters).
|
||||
Optional - use for elevated or underground locations.
|
||||
exact_mappings:
|
||||
- wgs84:alt
|
||||
22
schemas/20251121/linkml/modules/slots/documentation_url.yaml
Normal file
22
schemas/20251121/linkml/modules/slots/documentation_url.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Global Slot: documentation_url
|
||||
# URL to documentation for a project, system, or resource
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/documentation_url
|
||||
name: documentation-url-slot
|
||||
title: Documentation URL Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
schema: http://schema.org/
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
documentation_url:
|
||||
slot_uri: schema:documentation
|
||||
description: URL to documentation for this entity
|
||||
range: uri
|
||||
exact_mappings:
|
||||
- schema:documentation
|
||||
22
schemas/20251121/linkml/modules/slots/managed_by.yaml
Normal file
22
schemas/20251121/linkml/modules/slots/managed_by.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Global Slot: managed_by
|
||||
# Identifies the entity or organizational unit managing something
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/managed_by
|
||||
name: managed-by-slot
|
||||
title: Managed By Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
org: http://www.w3.org/ns/org#
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
managed_by:
|
||||
slot_uri: org:linkedTo
|
||||
description: Entity or organizational unit managing this resource
|
||||
range: string
|
||||
exact_mappings:
|
||||
- org:linkedTo
|
||||
24
schemas/20251121/linkml/modules/slots/storage_location.yaml
Normal file
24
schemas/20251121/linkml/modules/slots/storage_location.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Global Slot: storage_location
|
||||
# Physical or logical location where materials are stored
|
||||
|
||||
id: https://nde.nl/ontology/hc/slot/storage_location
|
||||
name: storage-location-slot
|
||||
title: Storage Location Slot
|
||||
|
||||
prefixes:
|
||||
linkml: https://w3id.org/linkml/
|
||||
hc: https://nde.nl/ontology/hc/
|
||||
premis: http://www.loc.gov/standards/premis/rdf/v3/
|
||||
|
||||
imports:
|
||||
- linkml:types
|
||||
|
||||
slots:
|
||||
storage_location:
|
||||
slot_uri: premis:storedAt
|
||||
description: >-
|
||||
Physical or logical location where materials are stored.
|
||||
Range varies by context - can be AuxiliaryPlace, Storage, or string.
|
||||
range: uriorcurie
|
||||
exact_mappings:
|
||||
- premis:storedAt
|
||||
689
scripts/enrich_nde_entries_ghcid.py
Normal file
689
scripts/enrich_nde_entries_ghcid.py
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enrich NDE Heritage Institution Entries with GHCID Persistent Identifiers.
|
||||
|
||||
This script:
|
||||
1. Loads all YAML files from data/nde/enriched/entries/
|
||||
2. Extracts location data (city, region, coordinates)
|
||||
3. Generates base GHCIDs using NL-REGION-CITY-TYPE-ABBREV format
|
||||
4. Detects collisions and applies First Batch rule (all get name suffixes)
|
||||
5. Generates all 4 identifier formats:
|
||||
- Human-readable GHCID string
|
||||
- UUID v5 (SHA-1, RFC 4122 compliant) - PRIMARY
|
||||
- UUID v8 (SHA-256, SOTA cryptographic strength) - Future-proof
|
||||
- Numeric (64-bit integer for database PKs)
|
||||
6. Adds GHCID fields to each entry
|
||||
7. Generates collision statistics report
|
||||
|
||||
## GHCID Format
|
||||
|
||||
Base: NL-{Region}-{City}-{Type}-{Abbreviation}
|
||||
With collision suffix: NL-{Region}-{City}-{Type}-{Abbreviation}-{name_suffix}
|
||||
|
||||
## Collision Resolution (First Batch Rule)
|
||||
|
||||
Since this is a batch import (all entries processed together), when multiple
|
||||
institutions generate the same base GHCID:
|
||||
- ALL colliding institutions receive native language name suffixes
|
||||
- Name suffix: snake_case of institution name
|
||||
|
||||
Example:
|
||||
- Two societies with NL-OV-ZWO-S-HK both become:
|
||||
- NL-OV-ZWO-S-HK-historische_kring_zwolle
|
||||
- NL-OV-ZWO-S-HK-heemkundige_kring_zwolle
|
||||
|
||||
Usage:
|
||||
python scripts/enrich_nde_entries_ghcid.py [--dry-run]
|
||||
|
||||
Options:
|
||||
--dry-run Preview changes without writing to files
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
# Add src to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
from glam_extractor.identifiers.ghcid import (
|
||||
GHCIDComponents,
|
||||
GHCIDGenerator,
|
||||
InstitutionType,
|
||||
extract_abbreviation_from_name,
|
||||
normalize_city_name,
|
||||
)
|
||||
|
||||
|
||||
# Dutch province to ISO 3166-2 code mapping
|
||||
DUTCH_PROVINCE_CODES = {
|
||||
# Standard names
|
||||
"drenthe": "DR",
|
||||
"flevoland": "FL",
|
||||
"friesland": "FR",
|
||||
"fryslan": "FR",
|
||||
"fryslân": "FR",
|
||||
"gelderland": "GE",
|
||||
"groningen": "GR",
|
||||
"limburg": "LI",
|
||||
"noord-brabant": "NB",
|
||||
"north brabant": "NB",
|
||||
"noord brabant": "NB",
|
||||
"noord-holland": "NH",
|
||||
"north holland": "NH",
|
||||
"noord holland": "NH",
|
||||
"overijssel": "OV",
|
||||
"utrecht": "UT",
|
||||
"zeeland": "ZE",
|
||||
"zuid-holland": "ZH",
|
||||
"south holland": "ZH",
|
||||
"zuid holland": "ZH",
|
||||
}
|
||||
|
||||
# Institution type code mapping (from original entry 'type' field)
|
||||
TYPE_CODE_MAP = {
|
||||
"G": "G", # Gallery
|
||||
"L": "L", # Library
|
||||
"A": "A", # Archive
|
||||
"M": "M", # Museum
|
||||
"O": "O", # Official Institution
|
||||
"R": "R", # Research Center
|
||||
"C": "C", # Corporation
|
||||
"U": "U", # Unknown
|
||||
"B": "B", # Botanical/Zoo
|
||||
"E": "E", # Education Provider
|
||||
"S": "S", # Collecting Society
|
||||
"P": "P", # Personal Collection
|
||||
"F": "F", # Features (monuments, etc.)
|
||||
"I": "I", # Intangible Heritage Group
|
||||
"X": "X", # Mixed
|
||||
"H": "H", # Holy Sites
|
||||
"D": "D", # Digital Platform
|
||||
"N": "N", # NGO
|
||||
"T": "T", # Taste/Smell Heritage
|
||||
}
|
||||
|
||||
|
||||
def get_region_code(region_name: Optional[str]) -> str:
|
||||
"""
|
||||
Get ISO 3166-2 region code for a Dutch province.
|
||||
|
||||
Args:
|
||||
region_name: Province/region name (Dutch or English)
|
||||
|
||||
Returns:
|
||||
2-letter region code or "00" if not found
|
||||
"""
|
||||
if not region_name:
|
||||
return "00"
|
||||
|
||||
# Normalize: lowercase, remove accents
|
||||
normalized = unicodedata.normalize('NFD', region_name.lower())
|
||||
normalized = ''.join(c for c in normalized if unicodedata.category(c) != 'Mn')
|
||||
normalized = normalized.strip()
|
||||
|
||||
return DUTCH_PROVINCE_CODES.get(normalized, "00")
|
||||
|
||||
|
||||
def get_city_code(city_name: str) -> str:
|
||||
"""
|
||||
Generate 3-letter city code from city name.
|
||||
|
||||
Rules:
|
||||
1. Single word: first 3 letters uppercase
|
||||
2. City with article (de, het, den): first letter + first 2 of next word
|
||||
3. Multi-word: first letter of each word (up to 3)
|
||||
|
||||
Args:
|
||||
city_name: City name
|
||||
|
||||
Returns:
|
||||
3-letter uppercase city code
|
||||
"""
|
||||
if not city_name:
|
||||
return "XXX"
|
||||
|
||||
# Normalize: remove accents, handle special chars
|
||||
normalized = normalize_city_name(city_name)
|
||||
|
||||
# Split into words
|
||||
words = normalized.split()
|
||||
|
||||
if not words:
|
||||
return "XXX"
|
||||
|
||||
# Dutch articles and prepositions
|
||||
articles = {'de', 'het', 'den', "'s", 'op', 'aan', 'bij', 'ter'}
|
||||
|
||||
if len(words) == 1:
|
||||
# Single word: take first 3 letters
|
||||
code = words[0][:3].upper()
|
||||
elif words[0].lower() in articles and len(words) > 1:
|
||||
# City with article: first letter of article + first 2 of next word
|
||||
code = (words[0][0] + words[1][:2]).upper()
|
||||
else:
|
||||
# Multi-word: take first letter of each word (up to 3)
|
||||
code = ''.join(w[0] for w in words[:3]).upper()
|
||||
|
||||
# Ensure exactly 3 letters
|
||||
if len(code) < 3:
|
||||
code = code.ljust(3, 'X')
|
||||
elif len(code) > 3:
|
||||
code = code[:3]
|
||||
|
||||
# Ensure only A-Z characters
|
||||
code = re.sub(r'[^A-Z]', 'X', code)
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def generate_name_suffix(institution_name: str) -> str:
|
||||
"""
|
||||
Generate snake_case name suffix from institution name.
|
||||
|
||||
Used for collision resolution. Converts native language name to
|
||||
lowercase with underscores, removing diacritics and punctuation.
|
||||
|
||||
Args:
|
||||
institution_name: Full institution name
|
||||
|
||||
Returns:
|
||||
snake_case suffix (e.g., "historische_kring_zwolle")
|
||||
"""
|
||||
if not institution_name:
|
||||
return "unknown"
|
||||
|
||||
# Normalize: NFD decomposition to remove accents
|
||||
normalized = unicodedata.normalize('NFD', institution_name)
|
||||
ascii_name = ''.join(c for c in normalized if unicodedata.category(c) != 'Mn')
|
||||
|
||||
# Convert to lowercase
|
||||
lowercase = ascii_name.lower()
|
||||
|
||||
# Remove apostrophes, commas, and other punctuation
|
||||
no_punct = re.sub(r"[''`\",.:;!?()[\]{}]", '', lowercase)
|
||||
|
||||
# Replace spaces and hyphens with underscores
|
||||
underscored = re.sub(r'[\s\-/]+', '_', no_punct)
|
||||
|
||||
# Remove any remaining non-alphanumeric characters (except underscores)
|
||||
clean = re.sub(r'[^a-z0-9_]', '', underscored)
|
||||
|
||||
# Collapse multiple underscores
|
||||
final = re.sub(r'_+', '_', clean).strip('_')
|
||||
|
||||
# Truncate if too long (max 50 chars for name suffix)
|
||||
if len(final) > 50:
|
||||
final = final[:50].rstrip('_')
|
||||
|
||||
return final if final else "unknown"
|
||||
|
||||
|
||||
def extract_entry_data(entry: dict) -> dict:
|
||||
"""
|
||||
Extract relevant data from an entry for GHCID generation.
|
||||
|
||||
Looks in multiple sources for location data:
|
||||
1. locations[] array (if already enriched)
|
||||
2. original_entry.plaatsnaam_bezoekadres (NDE CSV city field)
|
||||
3. google_maps_enrichment.address / city
|
||||
4. museum_register_enrichment.province
|
||||
5. wikidata_enrichment.wikidata_claims.location
|
||||
|
||||
Args:
|
||||
entry: Entry dictionary from YAML
|
||||
|
||||
Returns:
|
||||
Dict with: name, type_code, city, region, wikidata_id
|
||||
"""
|
||||
# Get institution name
|
||||
name = None
|
||||
if 'original_entry' in entry:
|
||||
name = entry['original_entry'].get('organisatie')
|
||||
|
||||
if not name and 'wikidata_enrichment' in entry:
|
||||
name = entry['wikidata_enrichment'].get('wikidata_label_nl')
|
||||
if not name:
|
||||
name = entry['wikidata_enrichment'].get('wikidata_label_en')
|
||||
|
||||
if not name:
|
||||
name = "Unknown Institution"
|
||||
|
||||
# Get institution type
|
||||
type_codes = []
|
||||
if 'original_entry' in entry and 'type' in entry['original_entry']:
|
||||
types = entry['original_entry']['type']
|
||||
if isinstance(types, list):
|
||||
type_codes = types
|
||||
elif isinstance(types, str):
|
||||
type_codes = [types]
|
||||
|
||||
# Use first type, default to U (Unknown)
|
||||
type_code = type_codes[0] if type_codes else 'U'
|
||||
|
||||
# Get location - try multiple sources
|
||||
city = None
|
||||
region = None
|
||||
|
||||
# Source 1: locations[] array (already enriched)
|
||||
if 'locations' in entry and entry['locations']:
|
||||
loc = entry['locations'][0]
|
||||
city = loc.get('city')
|
||||
region = loc.get('region')
|
||||
|
||||
# Source 2: original_entry.plaatsnaam_bezoekadres (NDE CSV)
|
||||
if not city and 'original_entry' in entry:
|
||||
city = entry['original_entry'].get('plaatsnaam_bezoekadres')
|
||||
|
||||
# Source 3: google_maps_enrichment
|
||||
if not city and 'google_maps_enrichment' in entry:
|
||||
gm = entry['google_maps_enrichment']
|
||||
# Try to extract city from address
|
||||
address = gm.get('address', '')
|
||||
if address:
|
||||
# Dutch addresses: "Street Nr, Postcode City"
|
||||
# Try to extract city from last part
|
||||
parts = address.split(',')
|
||||
if len(parts) >= 2:
|
||||
last_part = parts[-1].strip()
|
||||
# Remove postcode (4 digits + 2 letters)
|
||||
import re
|
||||
city_match = re.sub(r'^\d{4}\s*[A-Z]{2}\s*', '', last_part)
|
||||
if city_match:
|
||||
city = city_match
|
||||
# Also try 'city' field if present
|
||||
if not city:
|
||||
city = gm.get('city')
|
||||
|
||||
# Source 4: museum_register_enrichment.province (for region)
|
||||
if not region and 'museum_register_enrichment' in entry:
|
||||
region = entry['museum_register_enrichment'].get('province')
|
||||
|
||||
# Source 5: wikidata_enrichment.wikidata_claims.location
|
||||
if not city and 'wikidata_enrichment' in entry:
|
||||
claims = entry['wikidata_enrichment'].get('wikidata_claims', {})
|
||||
if 'location' in claims:
|
||||
loc_data = claims['location']
|
||||
if isinstance(loc_data, dict):
|
||||
city = loc_data.get('label_en') or loc_data.get('label_nl')
|
||||
|
||||
# Source 6: Try wikidata description for city hint
|
||||
if not city and 'wikidata_enrichment' in entry:
|
||||
desc_nl = entry['wikidata_enrichment'].get('wikidata_description_nl', '')
|
||||
# Try to extract city from "museum in [City], Nederland"
|
||||
import re
|
||||
city_match = re.search(r'in\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?),?\s*(?:Nederland|Netherlands)', desc_nl)
|
||||
if city_match:
|
||||
city = city_match.group(1)
|
||||
|
||||
# Get Wikidata ID
|
||||
wikidata_id = None
|
||||
if 'wikidata_enrichment' in entry:
|
||||
wikidata_id = entry['wikidata_enrichment'].get('wikidata_entity_id')
|
||||
if not wikidata_id and 'original_entry' in entry:
|
||||
wikidata_id = entry['original_entry'].get('wikidata_id')
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'type_code': TYPE_CODE_MAP.get(type_code, 'U'),
|
||||
'city': city,
|
||||
'region': region,
|
||||
'wikidata_id': wikidata_id,
|
||||
}
|
||||
|
||||
|
||||
def generate_base_ghcid(data: dict) -> Tuple[str, GHCIDComponents]:
|
||||
"""
|
||||
Generate base GHCID (without name suffix) for an institution.
|
||||
|
||||
Args:
|
||||
data: Dict with name, type_code, city, region
|
||||
|
||||
Returns:
|
||||
Tuple of (base_ghcid_string, GHCIDComponents)
|
||||
"""
|
||||
# Get region code
|
||||
region_code = get_region_code(data['region'])
|
||||
|
||||
# Get city code
|
||||
city_code = get_city_code(data['city']) if data['city'] else "XXX"
|
||||
|
||||
# Get abbreviation from name
|
||||
abbreviation = extract_abbreviation_from_name(data['name'])
|
||||
if not abbreviation:
|
||||
abbreviation = "INST"
|
||||
|
||||
# Create components (without Wikidata QID - we'll use name suffix for collisions)
|
||||
components = GHCIDComponents(
|
||||
country_code="NL",
|
||||
region_code=region_code,
|
||||
city_locode=city_code,
|
||||
institution_type=data['type_code'],
|
||||
abbreviation=abbreviation,
|
||||
wikidata_qid=None, # Don't use QID for collision resolution
|
||||
)
|
||||
|
||||
return components.to_string(), components
|
||||
|
||||
|
||||
def process_entries(entries_dir: Path, dry_run: bool = False) -> dict:
|
||||
"""
|
||||
Process all entry files and generate GHCIDs.
|
||||
|
||||
Args:
|
||||
entries_dir: Path to entries directory
|
||||
dry_run: If True, don't write changes
|
||||
|
||||
Returns:
|
||||
Statistics dictionary
|
||||
"""
|
||||
stats = {
|
||||
'total': 0,
|
||||
'success': 0,
|
||||
'skipped_no_location': 0,
|
||||
'skipped_not_custodian': 0,
|
||||
'collisions': 0,
|
||||
'collision_groups': 0,
|
||||
'files_updated': 0,
|
||||
'errors': [],
|
||||
}
|
||||
|
||||
# Timestamp for this batch
|
||||
generation_timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Phase 1: Load all entries and generate base GHCIDs
|
||||
print("Phase 1: Loading entries and generating base GHCIDs...")
|
||||
entries_data = [] # List of (filepath, entry, extracted_data, base_ghcid, components)
|
||||
|
||||
yaml_files = sorted(entries_dir.glob("*.yaml"))
|
||||
stats['total'] = len(yaml_files)
|
||||
|
||||
for filepath in yaml_files:
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
entry = yaml.safe_load(f)
|
||||
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
# Check if NOT_CUSTODIAN (skip these)
|
||||
if entry.get('google_maps_status') == 'NOT_CUSTODIAN':
|
||||
stats['skipped_not_custodian'] += 1
|
||||
continue
|
||||
|
||||
# Extract data
|
||||
data = extract_entry_data(entry)
|
||||
|
||||
# Check if we have location data
|
||||
if not data['city']:
|
||||
stats['skipped_no_location'] += 1
|
||||
continue
|
||||
|
||||
# Generate base GHCID
|
||||
base_ghcid, components = generate_base_ghcid(data)
|
||||
|
||||
entries_data.append({
|
||||
'filepath': filepath,
|
||||
'entry': entry,
|
||||
'data': data,
|
||||
'base_ghcid': base_ghcid,
|
||||
'components': components,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
stats['errors'].append(f"{filepath.name}: {str(e)}")
|
||||
|
||||
print(f" Loaded {len(entries_data)} entries with location data")
|
||||
print(f" Skipped {stats['skipped_no_location']} entries without city")
|
||||
print(f" Skipped {stats['skipped_not_custodian']} NOT_CUSTODIAN entries")
|
||||
|
||||
# Phase 2: Detect collisions
|
||||
print("\nPhase 2: Detecting GHCID collisions...")
|
||||
collision_groups = defaultdict(list)
|
||||
|
||||
for ed in entries_data:
|
||||
collision_groups[ed['base_ghcid']].append(ed)
|
||||
|
||||
# Count collisions
|
||||
for base_ghcid, group in collision_groups.items():
|
||||
if len(group) > 1:
|
||||
stats['collision_groups'] += 1
|
||||
stats['collisions'] += len(group)
|
||||
|
||||
print(f" Found {stats['collision_groups']} collision groups ({stats['collisions']} entries)")
|
||||
|
||||
# Phase 3: Resolve collisions and generate final GHCIDs
|
||||
print("\nPhase 3: Resolving collisions and generating final GHCIDs...")
|
||||
|
||||
collision_report = []
|
||||
|
||||
for base_ghcid, group in collision_groups.items():
|
||||
if len(group) > 1:
|
||||
# COLLISION: Apply First Batch rule - ALL get name suffixes
|
||||
collision_report.append({
|
||||
'base_ghcid': base_ghcid,
|
||||
'count': len(group),
|
||||
'institutions': [ed['data']['name'] for ed in group],
|
||||
})
|
||||
|
||||
for ed in group:
|
||||
# Generate name suffix
|
||||
name_suffix = generate_name_suffix(ed['data']['name'])
|
||||
ed['final_ghcid'] = f"{base_ghcid}-{name_suffix}"
|
||||
ed['had_collision'] = True
|
||||
else:
|
||||
# No collision: use base GHCID
|
||||
ed = group[0]
|
||||
ed['final_ghcid'] = base_ghcid
|
||||
ed['had_collision'] = False
|
||||
|
||||
# Phase 4: Generate all identifier formats and update entries
|
||||
print("\nPhase 4: Generating identifier formats and updating entries...")
|
||||
|
||||
for ed in entries_data:
|
||||
final_ghcid = ed['final_ghcid']
|
||||
|
||||
# Create final components with the resolved GHCID string
|
||||
# We need to parse it back or generate UUIDs directly
|
||||
# For simplicity, hash the final GHCID string directly
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
# GHCID UUID v5 Namespace
|
||||
GHCID_NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
|
||||
|
||||
# Generate UUID v5 (SHA-1)
|
||||
ghcid_uuid = uuid.uuid5(GHCID_NAMESPACE, final_ghcid)
|
||||
|
||||
# Generate UUID v8 (SHA-256)
|
||||
hash_bytes = hashlib.sha256(final_ghcid.encode('utf-8')).digest()
|
||||
uuid_bytes = bytearray(hash_bytes[:16])
|
||||
uuid_bytes[6] = (uuid_bytes[6] & 0x0F) | 0x80 # Version 8
|
||||
uuid_bytes[8] = (uuid_bytes[8] & 0x3F) | 0x80 # Variant RFC 4122
|
||||
ghcid_uuid_sha256 = uuid.UUID(bytes=bytes(uuid_bytes))
|
||||
|
||||
# Generate numeric (64-bit)
|
||||
ghcid_numeric = int.from_bytes(hash_bytes[:8], byteorder='big', signed=False)
|
||||
|
||||
# Generate record ID (UUID v7 - time-ordered, non-deterministic)
|
||||
record_id = GHCIDComponents.generate_uuid_v7()
|
||||
|
||||
# Create GHCID block for entry
|
||||
ghcid_block = {
|
||||
'ghcid_current': final_ghcid,
|
||||
'ghcid_original': final_ghcid, # Same for first assignment
|
||||
'ghcid_uuid': str(ghcid_uuid),
|
||||
'ghcid_uuid_sha256': str(ghcid_uuid_sha256),
|
||||
'ghcid_numeric': ghcid_numeric,
|
||||
'record_id': str(record_id),
|
||||
'generation_timestamp': generation_timestamp,
|
||||
'ghcid_history': [
|
||||
{
|
||||
'ghcid': final_ghcid,
|
||||
'ghcid_numeric': ghcid_numeric,
|
||||
'valid_from': generation_timestamp,
|
||||
'valid_to': None,
|
||||
'reason': 'Initial GHCID assignment (NDE batch import December 2025)'
|
||||
+ (' - name suffix added to resolve collision' if ed.get('had_collision') else ''),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Add collision info if applicable
|
||||
if ed.get('had_collision'):
|
||||
ghcid_block['collision_resolved'] = True
|
||||
ghcid_block['base_ghcid_before_collision'] = ed['base_ghcid']
|
||||
|
||||
# Update entry
|
||||
entry = ed['entry']
|
||||
entry['ghcid'] = ghcid_block
|
||||
|
||||
# Also add to identifiers list
|
||||
if 'identifiers' not in entry:
|
||||
entry['identifiers'] = []
|
||||
|
||||
# Remove any existing GHCID identifiers
|
||||
entry['identifiers'] = [
|
||||
i for i in entry['identifiers']
|
||||
if i.get('identifier_scheme') not in ['GHCID', 'GHCID_NUMERIC', 'GHCID_UUID', 'GHCID_UUID_SHA256', 'RECORD_ID']
|
||||
]
|
||||
|
||||
# Add new GHCID identifiers
|
||||
entry['identifiers'].extend([
|
||||
{
|
||||
'identifier_scheme': 'GHCID',
|
||||
'identifier_value': final_ghcid,
|
||||
},
|
||||
{
|
||||
'identifier_scheme': 'GHCID_UUID',
|
||||
'identifier_value': str(ghcid_uuid),
|
||||
'identifier_url': f'urn:uuid:{ghcid_uuid}',
|
||||
},
|
||||
{
|
||||
'identifier_scheme': 'GHCID_UUID_SHA256',
|
||||
'identifier_value': str(ghcid_uuid_sha256),
|
||||
'identifier_url': f'urn:uuid:{ghcid_uuid_sha256}',
|
||||
},
|
||||
{
|
||||
'identifier_scheme': 'GHCID_NUMERIC',
|
||||
'identifier_value': str(ghcid_numeric),
|
||||
},
|
||||
{
|
||||
'identifier_scheme': 'RECORD_ID',
|
||||
'identifier_value': str(record_id),
|
||||
'identifier_url': f'urn:uuid:{record_id}',
|
||||
},
|
||||
])
|
||||
|
||||
ed['entry'] = entry
|
||||
stats['success'] += 1
|
||||
|
||||
# Phase 5: Write updated entries
|
||||
if not dry_run:
|
||||
print("\nPhase 5: Writing updated entry files...")
|
||||
|
||||
for ed in entries_data:
|
||||
filepath = ed['filepath']
|
||||
entry = ed['entry']
|
||||
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(entry, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||
stats['files_updated'] += 1
|
||||
except Exception as e:
|
||||
stats['errors'].append(f"Write error {filepath.name}: {str(e)}")
|
||||
|
||||
print(f" Updated {stats['files_updated']} files")
|
||||
else:
|
||||
print("\nPhase 5: DRY RUN - no files written")
|
||||
|
||||
# Phase 6: Generate collision report
|
||||
print("\nPhase 6: Generating collision report...")
|
||||
|
||||
if collision_report:
|
||||
report_path = entries_dir.parent / "ghcid_collision_report.json"
|
||||
|
||||
report = {
|
||||
'generation_timestamp': generation_timestamp,
|
||||
'total_entries': stats['total'],
|
||||
'entries_with_ghcid': stats['success'],
|
||||
'collision_groups': stats['collision_groups'],
|
||||
'entries_with_collisions': stats['collisions'],
|
||||
'collision_resolution_strategy': 'first_batch_all_get_name_suffix',
|
||||
'collisions': collision_report,
|
||||
}
|
||||
|
||||
if not dry_run:
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
||||
print(f" Collision report written to: {report_path}")
|
||||
else:
|
||||
print(f" Would write collision report to: {report_path}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution."""
|
||||
parser = argparse.ArgumentParser(description="Enrich NDE entries with GHCID identifiers")
|
||||
parser.add_argument('--dry-run', action='store_true', help="Preview changes without writing")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Paths
|
||||
project_root = Path(__file__).parent.parent
|
||||
entries_dir = project_root / "data" / "nde" / "enriched" / "entries"
|
||||
|
||||
print("="*70)
|
||||
print("NDE HERITAGE INSTITUTION GHCID ENRICHMENT")
|
||||
print("="*70)
|
||||
print(f"Entries directory: {entries_dir}")
|
||||
print(f"Dry run: {args.dry_run}")
|
||||
print()
|
||||
|
||||
if not entries_dir.exists():
|
||||
print(f"ERROR: Entries directory not found: {entries_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Process entries
|
||||
stats = process_entries(entries_dir, dry_run=args.dry_run)
|
||||
|
||||
# Print summary
|
||||
print()
|
||||
print("="*70)
|
||||
print("GHCID ENRICHMENT SUMMARY")
|
||||
print("="*70)
|
||||
print(f"Total entry files: {stats['total']}")
|
||||
print(f"Entries with GHCID generated: {stats['success']}")
|
||||
print(f"Skipped (no city): {stats['skipped_no_location']}")
|
||||
print(f"Skipped (NOT_CUSTODIAN): {stats['skipped_not_custodian']}")
|
||||
print(f"Collision groups: {stats['collision_groups']}")
|
||||
print(f"Entries with collisions: {stats['collisions']}")
|
||||
print(f"Files updated: {stats['files_updated']}")
|
||||
|
||||
if stats['errors']:
|
||||
print(f"\nErrors ({len(stats['errors'])}):")
|
||||
for err in stats['errors'][:10]:
|
||||
print(f" - {err}")
|
||||
if len(stats['errors']) > 10:
|
||||
print(f" ... and {len(stats['errors']) - 10} more")
|
||||
|
||||
print()
|
||||
print("="*70)
|
||||
if args.dry_run:
|
||||
print("DRY RUN COMPLETE - No files were modified")
|
||||
else:
|
||||
print("GHCID ENRICHMENT COMPLETE")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue