From 2497e5913f0977b2e0bc4ee7a6234a669dbed013 Mon Sep 17 00:00:00 2001 From: kempersc Date: Mon, 1 Dec 2025 00:37:24 +0100 Subject: [PATCH] enrich entries --- .../nde/enriched/entries/0478_Q110907423.yaml | 12 + .../nde/enriched/entries/0896_Q110891813.yaml | 30 + .../entries/1657_heemkring_glatbeke.yaml | 43 +- .../1659_historische_werkgroep_kynhout.yaml | 29 +- .../1660_heemkundekring_de_plaets.yaml | 23 +- .../1667_historische_kring_wierden.yaml | 33 +- exa-mcp-server-source | 2 +- .../data/heritage_custodian_ontology.mmd | 1642 +++++++++++++++++ .../schemas/20251121/linkml/manifest.json | 22 +- .../linkml/modules/classes/Appellation.yaml | 2 +- .../classes/ArchiveOrganizationType.yaml | 2 +- .../modules/classes/BioCustodianType.yaml | 2 +- .../modules/classes/CallForApplication.yaml | 2 +- .../classes/CollectionManagementSystem.yaml | 5 +- .../modules/classes/CustodianArchive.yaml | 6 +- .../modules/classes/DigitalPlatform.yaml | 7 +- .../classes/EducationProviderType.yaml | 2 +- .../linkml/modules/classes/GalleryType.yaml | 2 +- .../modules/classes/GeoSpatialPlace.yaml | 57 +- .../linkml/modules/classes/GiftShop.yaml | 10 +- .../modules/classes/HeritageSocietyType.yaml | 2 +- .../linkml/modules/classes/LibraryType.yaml | 2 +- .../linkml/modules/classes/MuseumType.yaml | 2 +- .../classes/OfficialInstitutionType.yaml | 2 +- .../linkml/modules/classes/Project.yaml | 5 +- .../classes/ReconstructionActivity.yaml | 8 +- .../modules/classes/RegistrationInfo.yaml | 1 + .../classes/ResearchOrganizationType.yaml | 2 +- .../linkml/modules/classes/Settlement.yaml | 4 +- .../linkml/modules/classes/Storage.yaml | 11 +- .../linkml/modules/classes/Subregion.yaml | 2 +- .../linkml/modules/slots/altitude.yaml | 24 + .../modules/slots/documentation_url.yaml | 22 + .../linkml/modules/slots/managed_by.yaml | 22 + .../modules/slots/storage_location.yaml | 24 + frontend/src/App.tsx | 32 +- frontend/src/components/layout/HomeLayout.css | 67 - frontend/src/components/layout/HomeLayout.tsx | 44 - frontend/src/components/layout/Layout.css | 69 +- frontend/src/components/layout/Layout.tsx | 14 +- frontend/src/components/layout/Navigation.css | 40 +- frontend/src/components/layout/Navigation.tsx | 40 +- frontend/src/contexts/LanguageContext.tsx | 174 ++ frontend/src/pages/ProjectPlanPage.tsx | 31 +- frontend/src/pages/Visualize.css | 106 ++ frontend/src/pages/Visualize.tsx | 415 ++--- .../linkml/modules/classes/Appellation.yaml | 2 +- .../classes/ArchiveOrganizationType.yaml | 2 +- .../modules/classes/BioCustodianType.yaml | 2 +- .../modules/classes/CallForApplication.yaml | 2 +- .../classes/CollectionManagementSystem.yaml | 5 +- .../modules/classes/CustodianArchive.yaml | 6 +- .../modules/classes/DigitalPlatform.yaml | 7 +- .../classes/EducationProviderType.yaml | 2 +- .../linkml/modules/classes/GalleryType.yaml | 2 +- .../modules/classes/GeoSpatialPlace.yaml | 57 +- .../linkml/modules/classes/GiftShop.yaml | 10 +- .../modules/classes/HeritageSocietyType.yaml | 2 +- .../linkml/modules/classes/LibraryType.yaml | 2 +- .../linkml/modules/classes/MuseumType.yaml | 2 +- .../classes/OfficialInstitutionType.yaml | 2 +- .../linkml/modules/classes/Project.yaml | 5 +- .../classes/ReconstructionActivity.yaml | 8 +- .../modules/classes/RegistrationInfo.yaml | 154 +- .../classes/ResearchOrganizationType.yaml | 2 +- .../linkml/modules/classes/Settlement.yaml | 4 +- .../linkml/modules/classes/Storage.yaml | 11 +- .../linkml/modules/classes/Subregion.yaml | 2 +- .../linkml/modules/slots/altitude.yaml | 24 + .../modules/slots/documentation_url.yaml | 22 + .../linkml/modules/slots/managed_by.yaml | 22 + .../modules/slots/storage_location.yaml | 24 + scripts/enrich_nde_entries_ghcid.py | 689 +++++++ 73 files changed, 3297 insertions(+), 873 deletions(-) create mode 100644 frontend/public/data/heritage_custodian_ontology.mmd create mode 100644 frontend/public/schemas/20251121/linkml/modules/slots/altitude.yaml create mode 100644 frontend/public/schemas/20251121/linkml/modules/slots/documentation_url.yaml create mode 100644 frontend/public/schemas/20251121/linkml/modules/slots/managed_by.yaml create mode 100644 frontend/public/schemas/20251121/linkml/modules/slots/storage_location.yaml delete mode 100644 frontend/src/components/layout/HomeLayout.css delete mode 100644 frontend/src/components/layout/HomeLayout.tsx create mode 100644 frontend/src/contexts/LanguageContext.tsx create mode 100644 schemas/20251121/linkml/modules/slots/altitude.yaml create mode 100644 schemas/20251121/linkml/modules/slots/documentation_url.yaml create mode 100644 schemas/20251121/linkml/modules/slots/managed_by.yaml create mode 100644 schemas/20251121/linkml/modules/slots/storage_location.yaml create mode 100644 scripts/enrich_nde_entries_ghcid.py diff --git a/data/nde/enriched/entries/0478_Q110907423.yaml b/data/nde/enriched/entries/0478_Q110907423.yaml index 5b7c173fb1..cc3c35aefd 100644 --- a/data/nde/enriched/entries/0478_Q110907423.yaml +++ b/data/nde/enriched/entries/0478_Q110907423.yaml @@ -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 diff --git a/data/nde/enriched/entries/0896_Q110891813.yaml b/data/nde/enriched/entries/0896_Q110891813.yaml index 9d9ab528f8..5164825d63 100644 --- a/data/nde/enriched/entries/0896_Q110891813.yaml +++ b/data/nde/enriched/entries/0896_Q110891813.yaml @@ -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' diff --git a/data/nde/enriched/entries/1657_heemkring_glatbeke.yaml b/data/nde/enriched/entries/1657_heemkring_glatbeke.yaml index def2bb5585..07362136f7 100644 --- a/data/nde/enriched/entries/1657_heemkring_glatbeke.yaml +++ b/data/nde/enriched/entries/1657_heemkring_glatbeke.yaml @@ -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. diff --git a/data/nde/enriched/entries/1659_historische_werkgroep_kynhout.yaml b/data/nde/enriched/entries/1659_historische_werkgroep_kynhout.yaml index c2474a5c09..0400575891 100644 --- a/data/nde/enriched/entries/1659_historische_werkgroep_kynhout.yaml +++ b/data/nde/enriched/entries/1659_historische_werkgroep_kynhout.yaml @@ -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 diff --git a/data/nde/enriched/entries/1660_heemkundekring_de_plaets.yaml b/data/nde/enriched/entries/1660_heemkundekring_de_plaets.yaml index dc1cd43b0b..1621323f03 100644 --- a/data/nde/enriched/entries/1660_heemkundekring_de_plaets.yaml +++ b/data/nde/enriched/entries/1660_heemkundekring_de_plaets.yaml @@ -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 diff --git a/data/nde/enriched/entries/1667_historische_kring_wierden.yaml b/data/nde/enriched/entries/1667_historische_kring_wierden.yaml index aeb4ddf9fc..0beb2d0b14 100644 --- a/data/nde/enriched/entries/1667_historische_kring_wierden.yaml +++ b/data/nde/enriched/entries/1667_historische_kring_wierden.yaml @@ -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 diff --git a/exa-mcp-server-source b/exa-mcp-server-source index 07aedc21cc..4aeb0543f9 160000 --- a/exa-mcp-server-source +++ b/exa-mcp-server-source @@ -1 +1 @@ -Subproject commit 07aedc21cc3d3e626c702fae7631f4f3bfe3a1ac +Subproject commit 4aeb0543f9becb95a320dd60c547b86f7134cbe3 diff --git a/frontend/public/data/heritage_custodian_ontology.mmd b/frontend/public/data/heritage_custodian_ontology.mmd new file mode 100644 index 0000000000..c0014ee5c9 --- /dev/null +++ b/frontend/public/data/heritage_custodian_ontology.mmd @@ -0,0 +1,1642 @@ +```mermaid +erDiagram +CustodianAppellation { + string appellation_value + string appellation_language + AppellationTypeEnum appellation_type +} +CustodianName { + string emic_name + string name_language + string standardized_name + uriorcurie endorsement_source + string name_authority + date valid_from + date valid_to +} +TimeSpan { + datetime begin_of_the_begin + datetime end_of_the_begin + datetime begin_of_the_end + datetime end_of_the_end +} +ReconstructionAgent { + uriorcurie id + string agent_name + AgentTypeEnum agent_type + string affiliation + string contact +} +CustodianObservation { + date observation_date + string observation_source + string observation_context +} +CustodianLegalStatus { + string refers_to_custodian + date dissolution_date + string reconstruction_method +} +ConfidenceMeasure { + float confidence_value + string confidence_method +} +Custodian { + uriorcurie hc_id + uriorcurie preferred_label + uriorcurie legal_status + uriorcurie place_designation + uriorcurieList digital_platform + uriorcurieList has_collection + uriorcurieList organizational_structure + uriorcurieList organizational_change_events + uriorcurieList encompassing_body + uriorcurieList identifiers + datetime created + datetime modified +} +CustodianType { + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + stringList type_label + string type_description + stringList applicable_countries + datetime created + datetime modified +} +ArchiveOrganizationType { + string archive_scope + stringList record_types + stringList preservation_standards + string finding_aids_format + string access_policy + uri appraisal_policy + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +MuseumType { + stringList collection_focus + string exhibition_program + stringList visitor_facilities + string cataloging_standard + boolean conservation_lab + boolean research_department + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +LibraryType { + string lending_policy + string catalog_system + stringList special_collections + boolean membership_required + boolean interlibrary_loan + string cataloging_standard + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +GalleryType { + boolean commercial_operation + stringList artist_representation + string exhibition_focus + boolean sales_activity + string exhibition_model + string commission_rate + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +ResearchOrganizationType { + stringList research_focus + boolean publication_output + uri data_repository + stringList research_infrastructure + uri academic_affiliation + stringList research_projects + uriorcurie type_id + CustodianPrimaryTypeEnum primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +OfficialInstitutionType { + string administrative_level + stringList heritage_mandate + boolean regulatory_authority + stringList funding_programs + string oversight_jurisdiction + string policy_authority + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +BioCustodianType { + stringList specimen_types + string collection_size + boolean living_collections + stringList research_programs + stringList public_education + string conservation_breeding + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +EducationProviderType { + stringList education_level + stringList academic_programs + string collection_access + stringList teaching_collections + stringList student_services + string accreditation + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +HeritageSocietyType { + string society_focus + string membership_size + stringList publication_activities + stringList collecting_scope + stringList volunteer_programs + stringList community_engagement + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +FeatureCustodianType { + stringList feature_types + string site_portfolio + string visitor_services + string conservation_activities + string access_management + string stewardship_model + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +IntangibleHeritageGroupType { + stringList ich_domain + string transmission_methods + string practitioner_community + string performance_repertoire + string cultural_context + string safeguarding_measures + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +PersonalCollectionType { + stringList collection_focus + string collection_size + string acquisition_history + string access_restrictions + string preservation_approach + string legacy_planning + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +HolySacredSiteType { + string religious_tradition + stringList collection_types + string religious_function + string access_policy + string stewardship_responsibility + string secularization_status + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +DigitalPlatformType { + stringList platform_category + string digital_collections + string technology_stack + stringList data_standards + string user_services + string sustainability_model + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +NonProfitType { + string organizational_mission + string program_activities + string geographic_scope + stringList beneficiary_groups + string partnership_model + string impact_measurement + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +TasteScentHeritageType { + string heritage_practice + string sensory_heritage_domain + stringList preservation_methods + stringList traditional_products + string knowledge_transmission + string community_significance + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +CommercialOrganizationType { + string business_model + string collection_purpose + string corporate_integration + string public_access + stringList heritage_holdings + stringList commercial_activities + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +MixedCustodianType { + stringList constituent_types + string functional_integration + string mixed_governance_structure + stringList service_portfolio + string facility_design + stringList user_communities + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +UnspecifiedType { + string classification_status + stringList evidence_gaps + stringList type_hypotheses + stringList research_attempts + string review_status + stringList data_quality_flags + uriorcurie type_id + string primary_type + string wikidata_entity + string type_label + string type_description + string applicable_countries + datetime created + datetime modified +} +CustodianPlace { + string place_name + string place_language + PlaceSpecificityEnum place_specificity + string place_note + date valid_from + date valid_to +} +AuxiliaryPlace { + uriorcurie auxiliary_place_id + string place_name + AuxiliaryPlaceTypeEnum auxiliary_place_type + string place_description + string street_address + string postal_code + string city + float latitude + float longitude + integer geonames_id + date valid_from + date valid_to +} +ReconstructionActivity { + uriorcurie id + ReconstructionActivityTypeEnum activity_type + string method + string justification +} +OrganizationalStructure { + string id + string unit_name + OrganizationalUnitTypeEnum unit_type + integer staff_count + string contact_point + date valid_from + date valid_to +} +OrganizationBranch { + uriorcurie branch_id + string branch_name + OrganizationBranchTypeEnum branch_type + string branch_description + uriorcurie is_branch_of + string branch_head + integer staff_count + string contact_point + date valid_from + date valid_to +} +AuxiliaryDigitalPlatform { + uriorcurie auxiliary_platform_id + string platform_name + AuxiliaryDigitalPlatformTypeEnum auxiliary_platform_type + uri platform_url + string platform_purpose + string platform_description + uri api_documentation + stringList technology_stack + uriorcurieList provides_access_to + string related_project + string funding_source + boolean iiif_support + boolean linked_data + date valid_from + date valid_to + string archival_status + uri archived_at + string preservation_event_type + string fixity_info + boolean cms_detected +} +CustodianCollection { + string id + string collection_name + string collection_description + stringList collection_type + string collection_scope + string extent + string access_rights + stringList digital_surrogates + string digitization_status + string preservation_level + stringList custody_history + date valid_from + date valid_to +} +LegalResponsibilityCollection { + string legal_responsibility_basis + date legal_responsibility_start_date + date legal_responsibility_end_date + uriorcurie id + string collection_name + string collection_description + stringList collection_type + string collection_scope + string extent + string access_rights + stringList digital_surrogates + string digitization_status + string preservation_level + stringList custody_history + string refers_to_custodian + date valid_from + date valid_to +} +GeoSpatialPlace { + string geospatial_id + string latitude + string longitude + float altitude + string geometry_wkt + GeometryTypeEnum geometry_type + string coordinate_reference_system + string geonames_id + string osm_id + string cadastral_id + float accuracy_meters + string geospatial_source + string bounding_box + string spatial_resolution + string feature_class + string feature_code + date valid_from_geo + date valid_to_geo +} +OrganizationalChangeEvent { + uriorcurie id + OrganizationalChangeEventTypeEnum event_type + date event_date + string event_description + string change_rationale + string staff_impact + uri documentation_source + date valid_from + date valid_to +} +PersonObservation { + uriorcurie id + string person_name + StaffRoleTypeEnum staff_role + string role_title + date role_start_date + date role_end_date + string contact_email + stringList expertise_areas + datetime created + datetime modified +} +CustodianIdentifier { + string identifier_scheme + string identifier_value + string defined_by_standard + string allocated_by + string identifier_format_used + string canonical_value + string also_identifies_name + string allocation_date +} +LanguageCode { + string language_code +} +SourceDocument { + uriorcurie source_uri + SourceDocumentTypeEnum source_type + date source_date + string source_creator +} +LegalEntityType { + uriorcurie id + string code + string label + string definition + uriorcurieList ontology_mapping +} +LegalForm { + uriorcurie id + string elf_code + string local_name + string transliterated_name + string abbreviation + date valid_from + date valid_to +} +LegalName { + uriorcurie id + string full_name + string name_without_type + string alphabetical_name + string display_name + string language + string script + string temporal_validity +} +RegistrationNumber { + uriorcurie id + string number + string type + string temporal_validity +} +RegistrationAuthority { + uriorcurie id + string name + string name_local + string abbreviation + uri registry_url + uri api_url + uri sparql_endpoint + string data_license + RegistrationAuthorityGovernanceEnum governance_type + integer established_year + string standards_maintained + string allocation_agencies + uri website + string description + string wikidata_id +} +GovernanceStructure { + uriorcurie id + string structure_type + stringList organizational_units + string governance_body + string description +} +LegalStatus { + uriorcurie id + string status_code + string status_name + string description + string temporal_validity +} +Country { + string alpha_2 + string alpha_3 +} +Subregion { + string iso_3166_2_code + string country + string subdivision_name +} +Settlement { + string geonames_id + string settlement_name + string country + string subregion + string latitude + string longitude +} +DataLicensePolicy { + uriorcurie id + string policy_name + OpennessStanceEnum openness_stance + stringList open_data_principles + uri policy_url + date policy_effective_date + stringList advocacy_activities + string description +} +DataLicense { + uriorcurie id + string name + string abbreviation + DataLicenseTypeEnum license_type + DataOpennessLevelEnum openness_level + uri license_url + uri deed_url + string version + boolean allows_commercial_use + boolean requires_attribution + boolean requires_sharealike + boolean allows_derivatives + string steward_organization + string spdx_identifier + string description +} +ServiceLicense { + string service_name + uri service_url + string license + string license_notes +} +Project { + uriorcurie project_id + string project_name + string project_short_name + string project_description + ProjectStatusEnum project_status + uri project_url + date start_date + date end_date + stringList funding_source + string funding_amount + stringList objectives + stringList deliverables + uriorcurie organizing_body + uriorcurieList participating_custodians + uriorcurieList related_projects + uri documentation_url + string contact_email + stringList keywords + uriorcurieList project_identifiers +} +Jurisdiction { + string jurisdiction_id + string jurisdiction_type + string country + string subregion + string settlement + string supranational_code + string gleif_jurisdiction_code + LegalSystemTypeEnum legal_system_type + string description +} +EncompassingBody { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + stringList service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + stringList area_served +} +UmbrellaOrganisation { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + stringList service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + stringList area_served +} +NetworkOrganisation { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + string service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + stringList area_served +} +Consortium { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + string service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + stringList area_served +} +Cooperative { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + stringList service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + string data_license_policy + stringList area_served +} +SocialMovement { + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + uriorcurieList member_custodians + string governance_authority + string service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + string data_license_policy + stringList area_served +} +FundingOrganisation { + uriorcurieList implements_agenda + uriorcurieList issued_calls + stringList funding_focus + stringList funding_schemes + string total_annual_budget + string funding_source + uriorcurie id + string organization_name + EncompassingBodyTypeEnum organization_type + string description + string organization_legal_form + date founding_date + date dissolution_date + string member_custodians + string governance_authority + string service_offerings + string membership_criteria + uriorcurieList external_identifiers + uri website + string area_served +} +FeaturePlace { + FeatureTypeEnum feature_type + string feature_name + string feature_language + string feature_description + string feature_note + date valid_from + date valid_to +} +DigitalPlatform { + uriorcurie platform_id + string platform_name + uri homepage_web_address + uriList collection_web_addresses + uriList inventory_web_addresses + uri api_endpoint + uri sparql_endpoint + uri oai_pmh_endpoint + stringList programming_languages + string repository_software + boolean iiif_support + boolean linked_data + stringList metadata_standards + string access_restrictions + string preservation_level + string storage_location + date fixity_check_date +} +CollectionManagementSystem { + uriorcurie cms_id + string cms_product_name + string cms_product_version + string cms_category + boolean open_source + string license + string vendor_name + uri vendor_url + uri documentation_url + stringList programming_languages + uri repository_url + stringList supported_metadata_standards + boolean iiif_compatible + boolean linked_data_export + boolean api_available + date deployment_date +} +TradeRegister { + string register_id + string register_name + string register_name_local + string register_abbreviation + string register_type + string jurisdiction + string maintained_by + string gleif_ra_code + uri website + uri api_endpoint + string identifier_format + string description +} +StandardsOrganization { + uriorcurie id + string name + string abbreviation + EncompassingBodyTypeEnum organization_type + stringList member_countries + integer founded_year + string headquarters_country + uri website + string description + string standards_maintained + string wikidata_id +} +Standard { + uriorcurie id + string name + string abbreviation + string iso_standard_number + string country_scope + StandardScopeTypeEnum scope_type + IdentifierDomainEnum identifier_domain + uri website + string lookup_url_template + integer first_published_year + string current_version + string description + StandardTypeEnum standard_type + GovernanceModelEnum governance_model + string data_license + uriorcurieList applicable_schema_types + string wikidata_id + stringList glamorcubesfixphdnt_types + string category +} +IdentifierFormat { + uriorcurie id + string format_name + string pattern + string example + boolean is_canonical + boolean is_uri_format + string transformation_to_canonical +} +AllocationAgency { + uriorcurie id + string name + string name_local + string abbreviation + string country_scope + AllocationDomainEnumList allocation_domain + string allocation_prefix + date allocation_start_date + date allocation_end_date + string is_active + uri website + string contact_email + uri allocation_policy_url + string description +} +CustodianArchive { + string id + string archive_name + string archive_description + string accession_number + date accession_date + date accumulation_date_start + date accumulation_date_end + string creating_agency + ArchiveProcessingStatusEnum processing_status + string processing_priority + string estimated_extent + string assigned_processor + date processing_started_date + date processing_completed_date + date transfer_to_collection_date + uriorcurie successor_collection + string access_restrictions + string appraisal_notes + string arrangement_notes + date valid_from + date valid_to +} +ArticlesOfAssociation { + string id + string document_title + string document_description + string document_type + date execution_date + date effective_date + string notary_name + string notary_office + string notarial_deed_number + integer version_number + boolean is_current_version + string purpose_clause + string registered_office_clause + string governance_clauses + stringList amendment_history + string language + uri document_url + string document_format + RecordsLifecycleStageEnum current_archival_stage + boolean requires_articles_at_registration + date valid_from + date valid_to +} +ContributingAgency { + uriorcurie id + string contributor_code + string name + string name_local + string abbreviation + string authority_file_name + string authority_file_abbreviation + uri authority_file_url + AuthorityRecordFormatEnum record_format + AuthorityEntityTypeEnumList entity_types_covered + date contribution_start_date + string is_active + boolean governance_representative + uri website + string description + ConsortiumGovernanceRoleEnum governance_role +} +SocialMediaProfile { + uriorcurie social_media_profile_id + SocialMediaPlatformTypeEnum platform_type + string platform_name + string account_name + string account_id + uri profile_url + string profile_description + boolean is_primary_digital_presence + boolean verified + integer follower_count + integer following_count + integer post_count + float engagement_rate + datetime metrics_observed_date + uri profile_image_url + uri cover_image_url + date created_date + date valid_from + date valid_to + string account_status + string language +} +InternetOfThings { + uriorcurie device_id + string device_name + DigitalPresenceTypeEnum device_type + string device_model + string device_manufacturer + integer device_count + string coverage_area + string purpose + string technical_specifications + stringList connectivity_type + string power_source + uri publishes_to + uri api_endpoint + string data_format + string update_frequency + date installation_date + date decommission_date + string operational_status + string maintenance_schedule +} +FundingRequirement { + uriorcurie requirement_id + FundingRequirementTypeEnum requirement_type + string requirement_text + string requirement_value + string requirement_unit + boolean is_mandatory + uriorcurie applies_to_call + uriorcurie observed_in + string source_section + date valid_from + date valid_to + uriorcurie supersedes + float extraction_confidence + string extraction_notes +} +CallForApplication { + uriorcurie call_id + string call_title + string call_short_name + string call_description + CallForApplicationStatusEnum call_status + uri call_url + date application_opening_date + date application_deadline + date results_expected_date + string total_budget + string typical_grant_range + stringList eligible_applicants + stringList eligible_countries + stringList thematic_areas + stringList heritage_types + string funding_rate + boolean co_funding_required + boolean partnership_required + integer minimum_partners + uriorcurie issuing_organisation + string parent_programme + integer programme_year + uriorcurieList call_identifiers + uriorcurieList related_calls + string contact_email + stringList info_session_dates + stringList keywords + uriorcurieList web_observations +} +WebObservation { + uriorcurie observation_id + uri source_url + datetime retrieved_on + string retrieved_by + string retrieval_method + string content_hash + integer http_status_code + string content_type + string page_title + datetime last_modified + string etag + float extraction_confidence + string extraction_notes + uriorcurieList observed_entities + uriorcurie previous_observation + boolean content_changed + uri archived_at +} +FundingAgenda { + uriorcurie agenda_id + string agenda_title + string agenda_short_name + string agenda_description + uri agenda_url + uri agenda_document_url + uriorcurie governing_body + uriorcurieList implementing_organisations + stringList strategic_objectives + string heritage_relevance + string total_investment + stringList geographic_scope + uriorcurieList related_agendas + stringList keywords + string language +} +ThematicRoute { + uriorcurie route_id + string route_title + string route_description + stringList route_keywords + string route_relevance_to_heritage +} +WebPortal { + uriorcurie portal_id + string portal_name + WebPortalTypeEnum portal_type + uri portal_url + string portal_description + stringList geographic_scope + stringList thematic_scope + uriorcurieList portal_data_sources + uriorcurieList exposes_collections + uriorcurie operated_by + uriorcurieList aggregates_from + uriorcurieList aggregated_by + stringList metadata_standards + uri api_endpoint + uri sparql_endpoint + uri oai_pmh_endpoint + stringList portal_language + date launch_date + string portal_status + uriorcurie successor_portal + integer record_count + integer participating_institutions + uriorcurieList identifiers +} +PrimaryDigitalPresenceAssertion { + uriorcurie assertion_id + uriorcurie about_digital_presence + DigitalPresenceTypeEnum digital_presence_type + boolean assertion_value + string assertion_rationale + datetime assertion_date + string asserted_by + float confidence_score + uriorcurie superseded_by + uriorcurie supersedes +} +GiftShop { + uriorcurie shop_id + string shop_name + GiftShopTypeEnum shop_type + string shop_description + ProductCategoryEnumList product_categories + string price_currency + string price_range + stringList accepts_payment_methods + string opening_hours + string annual_revenue + float visitor_conversion_rate + integer staff_count + float square_meters + string managed_by + stringList supplier_relationships + date valid_from + date valid_to +} +Storage { + uriorcurie storage_id + string storage_name + StorageTypeEnum storage_type + string storage_description + string capacity_description + float capacity_linear_meters + float capacity_cubic_meters + integer capacity_items + float current_utilization_percent + StorageStandardEnumList standards_applied + string managed_by + date valid_from + date valid_to +} +StorageCondition { + uriorcurie condition_id + date observation_date + StorageObserverTypeEnum observer_type + string observer_name + string observer_affiliation + boolean is_official_assessment + StorageConditionStatusEnum overall_status + string observation_notes + uriorcurieList evidence_documentation + string measurement_data + string compliance_status + boolean remediation_required + string remediation_notes + date follow_up_date + float confidence_score +} +StorageConditionCategoryAssessment { + string assessment_category + StorageConditionStatusEnum category_status + string category_measurement + string category_notes +} +StorageConditionPolicy { + uriorcurie policy_id + string policy_name + string policy_description + float temperature_target + float temperature_min + float temperature_max + float temperature_tolerance + float humidity_target + float humidity_min + float humidity_max + float humidity_tolerance + float light_max_lux + boolean uv_filtered_required + float air_changes_per_hour + float particulate_max + boolean pest_management_required + string fire_suppression_type + boolean flood_protection_required + string security_level + string access_restrictions + StorageStandardEnumList standards_compliance + date policy_effective_from + date policy_effective_to + string policy_approved_by + date policy_review_date + string notes +} +CustodianAdministration { + string id + string administration_name + string administration_description + stringList record_type + string creating_function + date active_since + string estimated_volume + string growth_rate + string retention_schedule + integer retention_period_years + date expected_transfer_date + string data_sensitivity + boolean gdpr_relevant + string business_criticality + string backup_status + string access_control + date valid_from + date valid_to +} +Budget { + string id + string budget_name + string budget_description + stringList budget_type + date fiscal_year_start + date fiscal_year_end + decimal total_budget_amount + string budget_currency + decimal operating_budget + decimal capital_budget + decimal acquisition_budget + decimal personnel_budget + decimal preservation_budget + decimal digitization_budget + decimal external_funding + decimal internal_funding + decimal endowment_draw + date approval_date + string approved_by + string budget_status + integer revision_number + date revision_date + uriorcurieList documented_by + date valid_from + date valid_to +} +WebClaim { + uriorcurie claim_id + string claim_type + string claim_value + uri source_url + datetime retrieved_on + string xpath + string html_file + string xpath_match_score + string xpath_matched_text + datetime extraction_timestamp + string extraction_method + string claim_notes +} + +CustodianAppellation ||--|o CustodianName : "variant_of_name" +CustodianName ||--}o CustodianAppellation : "alternative_names" +CustodianName ||--|o TimeSpan : "name_validity_period" +CustodianName ||--|o CustodianName : "supersedes" +CustodianName ||--|o CustodianName : "superseded_by" +CustodianName ||--}| CustodianObservation : "was_derived_from" +CustodianName ||--|o ReconstructionActivity : "was_generated_by" +CustodianName ||--|| Custodian : "refers_to_custodian" +CustodianObservation ||--|| CustodianAppellation : "observed_name" +CustodianObservation ||--}o CustodianAppellation : "alternative_observed_names" +CustodianObservation ||--|| SourceDocument : "source" +CustodianObservation ||--|o LanguageCode : "language" +CustodianObservation ||--|o CustodianLegalStatus : "derived_from_entity" +CustodianObservation ||--|o ConfidenceMeasure : "confidence_score" +CustodianLegalStatus ||--|| Custodian : "refers_to_custodian" +CustodianLegalStatus ||--|| LegalEntityType : "legal_entity_type" +CustodianLegalStatus ||--|| LegalName : "legal_name" +CustodianLegalStatus ||--|o LegalForm : "legal_form" +CustodianLegalStatus ||--}o RegistrationNumber : "registration_numbers" +CustodianLegalStatus ||--|o RegistrationAuthority : "registration_authority" +CustodianLegalStatus ||--|o TradeRegister : "primary_register" +CustodianLegalStatus ||--|o Jurisdiction : "legal_jurisdiction" +CustodianLegalStatus ||--|o TimeSpan : "temporal_extent" +CustodianLegalStatus ||--|o CustodianLegalStatus : "parent_custodian" +CustodianLegalStatus ||--|| LegalStatus : "legal_status" +CustodianLegalStatus ||--|o GovernanceStructure : "governance_structure" +CustodianLegalStatus ||--}o ArticlesOfAssociation : "has_articles_of_association" +CustodianLegalStatus ||--}| CustodianObservation : "was_derived_from" +CustodianLegalStatus ||--|| ReconstructionActivity : "was_generated_by" +CustodianLegalStatus ||--|o CustodianLegalStatus : "was_revision_of" +CustodianLegalStatus ||--}o CustodianIdentifier : "identifiers" +CustodianLegalStatus ||--}o LegalResponsibilityCollection : "collections_under_responsibility" +Custodian ||--|o CustodianType : "custodian_type" +Custodian ||--}o CustodianArchive : "has_operational_archive" +Custodian ||--}o CustodianAdministration : "has_administration" +Custodian ||--}o Budget : "has_budget" +Custodian ||--}o SocialMediaProfile : "social_media_profiles" +Custodian ||--|o DataLicensePolicy : "data_license_policy" +Custodian ||--}o Project : "participated_in_projects" +Custodian ||--}o GiftShop : "gift_shop" +Custodian ||--}o Storage : "storage_facilities" +CustodianType ||--|o CustodianType : "broader_type" +CustodianType ||--}o CustodianType : "narrower_types" +CustodianType ||--}o CustodianType : "related_types" +ArchiveOrganizationType ||--|| CustodianType : "inherits" +ArchiveOrganizationType ||--|o ArchiveOrganizationType : "broader_type" +ArchiveOrganizationType ||--}o CustodianType : "narrower_types" +ArchiveOrganizationType ||--}o CustodianType : "related_types" +MuseumType ||--|| CustodianType : "inherits" +MuseumType ||--|o MuseumType : "broader_type" +MuseumType ||--}o CustodianType : "narrower_types" +MuseumType ||--}o CustodianType : "related_types" +LibraryType ||--|| CustodianType : "inherits" +LibraryType ||--|o LibraryType : "broader_type" +LibraryType ||--}o CustodianType : "narrower_types" +LibraryType ||--}o CustodianType : "related_types" +GalleryType ||--|| CustodianType : "inherits" +GalleryType ||--|o GalleryType : "broader_type" +GalleryType ||--}o CustodianType : "narrower_types" +GalleryType ||--}o CustodianType : "related_types" +ResearchOrganizationType ||--|| CustodianType : "inherits" +ResearchOrganizationType ||--|o ResearchOrganizationType : "broader_type" +ResearchOrganizationType ||--}o CustodianType : "narrower_types" +ResearchOrganizationType ||--}o CustodianType : "related_types" +OfficialInstitutionType ||--|| CustodianType : "inherits" +OfficialInstitutionType ||--|o CustodianType : "broader_type" +OfficialInstitutionType ||--}o CustodianType : "narrower_types" +OfficialInstitutionType ||--}o CustodianType : "related_types" +BioCustodianType ||--|| CustodianType : "inherits" +BioCustodianType ||--|o CustodianType : "broader_type" +BioCustodianType ||--}o CustodianType : "narrower_types" +BioCustodianType ||--}o CustodianType : "related_types" +EducationProviderType ||--|| CustodianType : "inherits" +EducationProviderType ||--|o CustodianType : "broader_type" +EducationProviderType ||--}o CustodianType : "narrower_types" +EducationProviderType ||--}o CustodianType : "related_types" +HeritageSocietyType ||--|| CustodianType : "inherits" +HeritageSocietyType ||--|o CustodianType : "broader_type" +HeritageSocietyType ||--}o CustodianType : "narrower_types" +HeritageSocietyType ||--}o CustodianType : "related_types" +FeatureCustodianType ||--|| CustodianType : "inherits" +FeatureCustodianType ||--|o CustodianType : "broader_type" +FeatureCustodianType ||--}o CustodianType : "narrower_types" +FeatureCustodianType ||--}o CustodianType : "related_types" +IntangibleHeritageGroupType ||--|| CustodianType : "inherits" +IntangibleHeritageGroupType ||--|o CustodianType : "broader_type" +IntangibleHeritageGroupType ||--}o CustodianType : "narrower_types" +IntangibleHeritageGroupType ||--}o CustodianType : "related_types" +PersonalCollectionType ||--|| CustodianType : "inherits" +PersonalCollectionType ||--|o CustodianType : "broader_type" +PersonalCollectionType ||--}o CustodianType : "narrower_types" +PersonalCollectionType ||--}o CustodianType : "related_types" +HolySacredSiteType ||--|| CustodianType : "inherits" +HolySacredSiteType ||--|o CustodianType : "broader_type" +HolySacredSiteType ||--}o CustodianType : "narrower_types" +HolySacredSiteType ||--}o CustodianType : "related_types" +DigitalPlatformType ||--|| CustodianType : "inherits" +DigitalPlatformType ||--|o CustodianType : "broader_type" +DigitalPlatformType ||--}o CustodianType : "narrower_types" +DigitalPlatformType ||--}o CustodianType : "related_types" +NonProfitType ||--|| CustodianType : "inherits" +NonProfitType ||--|o CustodianType : "broader_type" +NonProfitType ||--}o CustodianType : "narrower_types" +NonProfitType ||--}o CustodianType : "related_types" +TasteScentHeritageType ||--|| CustodianType : "inherits" +TasteScentHeritageType ||--|o CustodianType : "broader_type" +TasteScentHeritageType ||--}o CustodianType : "narrower_types" +TasteScentHeritageType ||--}o CustodianType : "related_types" +CommercialOrganizationType ||--|| CustodianType : "inherits" +CommercialOrganizationType ||--|o CustodianType : "broader_type" +CommercialOrganizationType ||--}o CustodianType : "narrower_types" +CommercialOrganizationType ||--}o CustodianType : "related_types" +MixedCustodianType ||--|| CustodianType : "inherits" +MixedCustodianType ||--|o CustodianType : "broader_type" +MixedCustodianType ||--}o CustodianType : "narrower_types" +MixedCustodianType ||--}o CustodianType : "related_types" +UnspecifiedType ||--|| CustodianType : "inherits" +UnspecifiedType ||--|o CustodianType : "broader_type" +UnspecifiedType ||--}o CustodianType : "narrower_types" +UnspecifiedType ||--}o CustodianType : "related_types" +CustodianPlace ||--|o Country : "country" +CustodianPlace ||--|o Subregion : "subregion" +CustodianPlace ||--|o Settlement : "settlement" +CustodianPlace ||--|o FeaturePlace : "has_feature_type" +CustodianPlace ||--}o GeoSpatialPlace : "has_geospatial_location" +CustodianPlace ||--}o AuxiliaryPlace : "auxiliary_places" +CustodianPlace ||--}| CustodianObservation : "was_derived_from" +CustodianPlace ||--|o ReconstructionActivity : "was_generated_by" +CustodianPlace ||--|| Custodian : "refers_to_custodian" +AuxiliaryPlace ||--|o Country : "country" +AuxiliaryPlace ||--|o Subregion : "subregion" +AuxiliaryPlace ||--|o Settlement : "settlement" +AuxiliaryPlace ||--}o GeoSpatialPlace : "has_geospatial_location" +AuxiliaryPlace ||--|o FeaturePlace : "has_feature_type" +AuxiliaryPlace ||--}o OrganizationBranch : "hosts_branch" +AuxiliaryPlace ||--|| CustodianPlace : "is_auxiliary_of_place" +AuxiliaryPlace ||--|o TimeSpan : "temporal_extent" +AuxiliaryPlace ||--}o CustodianObservation : "was_derived_from" +AuxiliaryPlace ||--|o ReconstructionActivity : "was_generated_by" +AuxiliaryPlace ||--|| Custodian : "refers_to_custodian" +ReconstructionActivity ||--|o ReconstructionAgent : "responsible_agent" +ReconstructionActivity ||--|o TimeSpan : "temporal_extent" +ReconstructionActivity ||--}| CustodianObservation : "used" +ReconstructionActivity ||--|o ConfidenceMeasure : "confidence_score" +OrganizationalStructure ||--|o OrganizationalStructure : "parent_unit" +OrganizationalStructure ||--}o PersonObservation : "staff_members" +OrganizationalStructure ||--}o CustodianCollection : "managed_collections" +OrganizationalStructure ||--}o AuxiliaryPlace : "located_at" +OrganizationalStructure ||--|| Custodian : "refers_to_custodian" +OrganizationBranch ||--}o AuxiliaryPlace : "located_at" +OrganizationBranch ||--}o OrganizationalStructure : "has_operational_unit" +OrganizationBranch ||--}o OrganizationBranch : "has_sub_branch" +OrganizationBranch ||--|o TimeSpan : "temporal_extent" +OrganizationBranch ||--}o CustodianObservation : "was_derived_from" +OrganizationBranch ||--|o ReconstructionActivity : "was_generated_by" +OrganizationBranch ||--|| Custodian : "refers_to_custodian" +AuxiliaryDigitalPlatform ||--|| DigitalPlatform : "is_auxiliary_of_platform" +AuxiliaryDigitalPlatform ||--|o TimeSpan : "temporal_extent" +AuxiliaryDigitalPlatform ||--}o CollectionManagementSystem : "powered_by_cms" +AuxiliaryDigitalPlatform ||--}o CustodianObservation : "was_derived_from" +AuxiliaryDigitalPlatform ||--|o ReconstructionActivity : "was_generated_by" +AuxiliaryDigitalPlatform ||--|| Custodian : "refers_to_custodian" +CustodianCollection ||--|o TimeSpan : "temporal_coverage" +CustodianCollection ||--}o CollectionManagementSystem : "managed_by_cms" +CustodianCollection ||--|o OrganizationalStructure : "managing_unit" +CustodianCollection ||--|| Custodian : "refers_to_custodian" +CustodianCollection ||--}| CustodianObservation : "was_derived_from" +LegalResponsibilityCollection ||--|| CustodianCollection : "inherits" +LegalResponsibilityCollection ||--|| CustodianLegalStatus : "responsible_legal_entity" +LegalResponsibilityCollection ||--|o TimeSpan : "temporal_coverage" +LegalResponsibilityCollection ||--}o CollectionManagementSystem : "managed_by_cms" +LegalResponsibilityCollection ||--|o OrganizationalStructure : "managing_unit" +LegalResponsibilityCollection ||--|| Custodian : "refers_to_custodian" +LegalResponsibilityCollection ||--}| CustodianObservation : "was_derived_from" +OrganizationalChangeEvent ||--}o OrganizationalStructure : "affected_units" +OrganizationalChangeEvent ||--}o OrganizationalStructure : "resulting_units" +OrganizationalChangeEvent ||--|| Custodian : "parent_custodian" +OrganizationalChangeEvent ||--|o CustodianPlace : "event_location" +OrganizationalChangeEvent ||--|o CustodianPlace : "from_location" +OrganizationalChangeEvent ||--|o CustodianPlace : "to_location" +OrganizationalChangeEvent ||--}o GeoSpatialPlace : "affected_territory" +PersonObservation ||--|o OrganizationalStructure : "unit_affiliation" +PersonObservation ||--|o SourceDocument : "observation_source" +PersonObservation ||--|o OrganizationalChangeEvent : "affected_by_event" +CustodianIdentifier ||--|o Custodian : "identifies_custodian" +CustodianIdentifier ||--|o Standard : "defined_by_standard" +CustodianIdentifier ||--|o AllocationAgency : "allocated_by" +CustodianIdentifier ||--|o IdentifierFormat : "identifier_format_used" +CustodianIdentifier ||--|o CustodianName : "also_identifies_name" +LegalForm ||--|| Country : "country_code" +LegalForm ||--|| LegalEntityType : "legal_entity_type" +LegalForm ||--|o LegalForm : "parent_form" +LegalName ||--|o TimeSpan : "temporal_validity" +RegistrationNumber ||--|o TradeRegister : "trade_register" +RegistrationNumber ||--|| TimeSpan : "temporal_validity" +RegistrationAuthority ||--|| Country : "country" +RegistrationAuthority ||--|o RegistrationAuthority : "predecessor" +RegistrationAuthority ||--}o Standard : "standards_maintained" +RegistrationAuthority ||--}o AllocationAgency : "allocation_agencies" +LegalStatus ||--|| TimeSpan : "temporal_validity" +LegalStatus ||--|o Jurisdiction : "jurisdiction" +Subregion ||--|| Country : "country" +Settlement ||--|| Country : "country" +Settlement ||--|o Subregion : "subregion" +DataLicensePolicy ||--|| DataLicense : "default_license" +DataLicensePolicy ||--}o ServiceLicense : "service_specific_licenses" +ServiceLicense ||--|| DataLicense : "license" +Jurisdiction ||--|o Country : "country" +Jurisdiction ||--|o Subregion : "subregion" +Jurisdiction ||--|o Settlement : "settlement" +EncompassingBody ||--|o DataLicensePolicy : "data_license_policy" +EncompassingBody ||--}o Project : "projects" +EncompassingBody ||--|o Jurisdiction : "legal_jurisdiction" +UmbrellaOrganisation ||--|| EncompassingBody : "inherits" +UmbrellaOrganisation ||--|o DataLicensePolicy : "data_license_policy" +UmbrellaOrganisation ||--}o Project : "projects" +UmbrellaOrganisation ||--|| Jurisdiction : "legal_jurisdiction" +NetworkOrganisation ||--|| EncompassingBody : "inherits" +NetworkOrganisation ||--|o DataLicensePolicy : "data_license_policy" +NetworkOrganisation ||--}o Project : "projects" +NetworkOrganisation ||--|o Jurisdiction : "legal_jurisdiction" +Consortium ||--|| EncompassingBody : "inherits" +Consortium ||--|o DataLicensePolicy : "data_license_policy" +Consortium ||--}o Project : "projects" +Consortium ||--|o Jurisdiction : "legal_jurisdiction" +Cooperative ||--|| EncompassingBody : "inherits" +Cooperative ||--|o DataLicensePolicy : "data_license_policy" +Cooperative ||--}o Project : "projects" +Cooperative ||--|o Jurisdiction : "legal_jurisdiction" +SocialMovement ||--|| EncompassingBody : "inherits" +SocialMovement ||--|| DataLicensePolicy : "data_license_policy" +SocialMovement ||--}o Project : "projects" +SocialMovement ||--|o Jurisdiction : "legal_jurisdiction" +FundingOrganisation ||--|| EncompassingBody : "inherits" +FundingOrganisation ||--|o TimeSpan : "programme_period" +FundingOrganisation ||--|o DataLicensePolicy : "data_license_policy" +FundingOrganisation ||--}o Project : "projects" +FundingOrganisation ||--|o Jurisdiction : "legal_jurisdiction" +FeaturePlace ||--|| CustodianPlace : "classifies_place" +FeaturePlace ||--}| CustodianObservation : "was_derived_from" +FeaturePlace ||--|o ReconstructionActivity : "was_generated_by" +DigitalPlatform ||--}| DigitalPlatformType : "platform_type" +DigitalPlatform ||--}o CollectionManagementSystem : "powered_by_cms" +DigitalPlatform ||--}o AuxiliaryDigitalPlatform : "auxiliary_platforms" +DigitalPlatform ||--|o TimeSpan : "temporal_extent" +DigitalPlatform ||--}o CustodianObservation : "was_derived_from" +DigitalPlatform ||--|o ReconstructionActivity : "was_generated_by" +DigitalPlatform ||--|| Custodian : "refers_to_custodian" +CollectionManagementSystem ||--}o DigitalPlatform : "powers_platform" +CollectionManagementSystem ||--}o CustodianCollection : "manages_collection" +CollectionManagementSystem ||--}o Custodian : "used_by_custodian" +CollectionManagementSystem ||--|o TimeSpan : "temporal_extent" +CollectionManagementSystem ||--}o CustodianObservation : "was_derived_from" +CollectionManagementSystem ||--|o ReconstructionActivity : "was_generated_by" +CollectionManagementSystem ||--|| Custodian : "refers_to_custodian" +TradeRegister ||--|| Jurisdiction : "jurisdiction" +TradeRegister ||--|| RegistrationAuthority : "maintained_by" +StandardsOrganization ||--}o Standard : "standards_maintained" +Standard ||--|| StandardsOrganization : "defined_by" +Standard ||--|o RegistrationAuthority : "registration_authority" +Standard ||--}o Country : "country_scope" +Standard ||--}o IdentifierFormat : "formats" +Standard ||--|o IdentifierFormat : "canonical_format" +Standard ||--}o ContributingAgency : "contributing_agencies" +Standard ||--|o StandardsOrganization : "governance_council" +AllocationAgency ||--}| Country : "country_scope" +AllocationAgency ||--}o Subregion : "subregion_scope" +AllocationAgency ||--}| Standard : "allocates_for" +AllocationAgency ||--|o RegistrationAuthority : "parent_registration_authority" +CustodianArchive ||--}o Storage : "storage_location" +CustodianArchive ||--}o CollectionManagementSystem : "tracked_in_cms" +CustodianArchive ||--|o OrganizationalStructure : "managing_unit" +CustodianArchive ||--|| Custodian : "refers_to_custodian" +CustodianArchive ||--}o CustodianObservation : "was_derived_from" +CustodianArchive ||--|o ReconstructionActivity : "was_generated_by" +ArticlesOfAssociation ||--|o ArticlesOfAssociation : "supersedes" +ArticlesOfAssociation ||--|o ArticlesOfAssociation : "superseded_by" +ArticlesOfAssociation ||--|o CustodianArchive : "archived_in" +ArticlesOfAssociation ||--|o CustodianCollection : "collected_in" +ArticlesOfAssociation ||--|| CustodianLegalStatus : "refers_to_legal_status" +ArticlesOfAssociation ||--|| Custodian : "refers_to_custodian" +ArticlesOfAssociation ||--|o LegalForm : "legal_form" +ArticlesOfAssociation ||--|o Jurisdiction : "jurisdiction" +ArticlesOfAssociation ||--}o CustodianObservation : "was_derived_from" +ArticlesOfAssociation ||--|o ReconstructionActivity : "was_generated_by" +ContributingAgency ||--|| Country : "country" +ContributingAgency ||--}| Standard : "contributes_to" +ContributingAgency ||--|o AllocationAgency : "also_allocation_agency" +ContributingAgency ||--}o StandardsOrganization : "member_of" +SocialMediaProfile ||--}o PrimaryDigitalPresenceAssertion : "primary_presence_assertions" +SocialMediaProfile ||--|o DigitalPlatform : "associated_digital_platform" +SocialMediaProfile ||--|o AuxiliaryDigitalPlatform : "associated_auxiliary_platform" +SocialMediaProfile ||--|o TimeSpan : "temporal_extent" +SocialMediaProfile ||--}o CustodianObservation : "was_derived_from" +SocialMediaProfile ||--|o ReconstructionActivity : "was_generated_by" +SocialMediaProfile ||--|| Custodian : "refers_to_custodian" +InternetOfThings ||--|o CustodianPlace : "installed_at_place" +InternetOfThings ||--|o TimeSpan : "temporal_extent" +InternetOfThings ||--}o CustodianObservation : "was_derived_from" +InternetOfThings ||--|o ReconstructionActivity : "was_generated_by" +InternetOfThings ||--|| Custodian : "refers_to_custodian" +CallForApplication ||--}o FundingRequirement : "requirements" +WebObservation ||--}o WebClaim : "claims" +FundingAgenda ||--|o TimeSpan : "validity_period" +FundingAgenda ||--}o ThematicRoute : "thematic_routes" +WebPortal ||--|o TimeSpan : "temporal_extent" +WebPortal ||--}| CustodianObservation : "was_derived_from" +WebPortal ||--|| ReconstructionActivity : "was_generated_by" +PrimaryDigitalPresenceAssertion ||--|o TimeSpan : "temporal_extent" +PrimaryDigitalPresenceAssertion ||--}o WebObservation : "based_on_observations" +GiftShop ||--}o AuxiliaryPlace : "physical_location" +GiftShop ||--}o AuxiliaryDigitalPlatform : "online_shop" +GiftShop ||--|o TimeSpan : "temporal_extent" +GiftShop ||--}o CustodianObservation : "was_derived_from" +GiftShop ||--|o ReconstructionActivity : "was_generated_by" +GiftShop ||--|| Custodian : "refers_to_custodian" +Storage ||--|o AuxiliaryPlace : "storage_location" +Storage ||--}o CustodianCollection : "stores_collections" +Storage ||--|o StorageConditionPolicy : "condition_policy" +Storage ||--}o StorageCondition : "storage_conditions" +Storage ||--|o TimeSpan : "temporal_extent" +Storage ||--|| Custodian : "refers_to_custodian" +StorageCondition ||--|| Storage : "refers_to_storage" +StorageCondition ||--|o TimeSpan : "observation_period" +StorageCondition ||--}o StorageConditionCategoryAssessment : "category_assessments" +StorageCondition ||--|o StorageCondition : "supersedes" +CustodianAdministration ||--|o OrganizationalStructure : "managing_unit" +CustodianAdministration ||--|o DigitalPlatform : "primary_system" +CustodianAdministration ||--}o DigitalPlatform : "secondary_systems" +CustodianAdministration ||--|| Custodian : "refers_to_custodian" +CustodianAdministration ||--}o CustodianObservation : "was_derived_from" +CustodianAdministration ||--|o ReconstructionActivity : "was_generated_by" +Budget ||--|o OrganizationalStructure : "managing_unit" +Budget ||--|| Custodian : "refers_to_custodian" +Budget ||--}o CustodianObservation : "was_derived_from" +Budget ||--|o ReconstructionActivity : "was_generated_by" + +``` diff --git a/frontend/public/schemas/20251121/linkml/manifest.json b/frontend/public/schemas/20251121/linkml/manifest.json index dbf39cc9c1..6f85124263 100644 --- a/frontend/public/schemas/20251121/linkml/manifest.json +++ b/frontend/public/schemas/20251121/linkml/manifest.json @@ -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", diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/Appellation.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/Appellation.yaml index c73fb6ce97..b07859f0a0 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/Appellation.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/Appellation.yaml @@ -9,7 +9,7 @@ imports: - linkml:types - ../metadata - ../enums/AppellationTypeEnum - - CustodianName + - ./CustodianName classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml index b6b0af205d..399dc4ab16 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml @@ -4,7 +4,7 @@ title: Archive Organization Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/access_policy classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml index 6486827f75..50ced80131 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml @@ -3,7 +3,7 @@ name: BioCustodianType title: Biological and Zoological Custodian Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/collection_size classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/CallForApplication.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/CallForApplication.yaml index c69f662ed6..7996f9e12d 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/CallForApplication.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/CallForApplication.yaml @@ -34,7 +34,7 @@ imports: - linkml:types - ../enums/CallForApplicationStatusEnum - ../enums/FundingRequirementTypeEnum - - FundingRequirement + - ./FundingRequirement - ../slots/contact_email - ../slots/keywords diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml index acc184ca26..7b916d8bc2 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml index 53ea2ed5dd..1c34a3fe0d 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml index 8e3952c8e7..67b1c33b9a 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml index d8aa3fa9b8..0b24164a14 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml @@ -37,7 +37,7 @@ see_also: - https://www.wikidata.org/wiki/Q132560468 # university archive imports: - - CustodianType + - ./CustodianType prefixes: hc: https://nde.nl/ontology/hc/ diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/GalleryType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/GalleryType.yaml index b230d33cf1..7c4c91a56f 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/GalleryType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/GalleryType.yaml @@ -4,7 +4,7 @@ title: Gallery Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: GalleryType: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml index 334964a75f..018b12923b 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/GiftShop.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/GiftShop.yaml index 35e5f1f150..00e9501cec 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/GiftShop.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/GiftShop.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml index 6a7bbb2159..1ca1d8378b 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml @@ -39,7 +39,7 @@ see_also: - https://www.wikidata.org/wiki/Q15755503 # archaeological society imports: - - CustodianType + - ./CustodianType prefixes: hc: https://nde.nl/ontology/hc/ diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/LibraryType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/LibraryType.yaml index 1b16cae02a..43585ea5ab 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/LibraryType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/LibraryType.yaml @@ -4,7 +4,7 @@ title: Library Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/cataloging_standard classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/MuseumType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/MuseumType.yaml index a22629e0a8..8e98dd396d 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/MuseumType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/MuseumType.yaml @@ -4,7 +4,7 @@ title: Museum Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/collection_focus - ../slots/cataloging_standard diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml index 553de9979c..6681507ab9 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml @@ -4,7 +4,7 @@ title: Official Institution Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: OfficialInstitutionType: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/Project.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/Project.yaml index 2f32fc42bc..9133f95377 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/Project.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/Project.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml index 3d86c9732f..cddc711df8 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml @@ -9,10 +9,10 @@ imports: - linkml:types - ../metadata - ../enums/ReconstructionActivityTypeEnum - - ReconstructionAgent - - TimeSpan - - CustodianObservation - - ConfidenceMeasure + - ./ReconstructionAgent + - ./TimeSpan + - ./CustodianObservation + - ./ConfidenceMeasure classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml index 168763825d..bdfefa72d9 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml @@ -34,6 +34,7 @@ imports: - ../metadata - ./TimeSpan - ./Jurisdiction + - ./RegistrationAuthority - ../slots/jurisdiction - ../slots/description - ../slots/website diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml index 680a7a3405..14df14273f 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml @@ -4,7 +4,7 @@ title: Research Organization Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: ResearchOrganizationType: diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/Settlement.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/Settlement.yaml index 2a7123fdbb..4a3c448aca 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/Settlement.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/Settlement.yaml @@ -14,8 +14,8 @@ title: Settlement Class imports: - linkml:types - - Country - - Subregion + - ./Country + - ./Subregion - ../slots/country - ../slots/subregion - ../slots/geonames_id diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/Storage.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/Storage.yaml index 19330a3f9f..aa2e47c01e 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/Storage.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/Storage.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/classes/Subregion.yaml b/frontend/public/schemas/20251121/linkml/modules/classes/Subregion.yaml index 7e985526b6..497d82ee75 100644 --- a/frontend/public/schemas/20251121/linkml/modules/classes/Subregion.yaml +++ b/frontend/public/schemas/20251121/linkml/modules/classes/Subregion.yaml @@ -15,7 +15,7 @@ title: Subregion Class imports: - linkml:types - - Country + - ./Country - ../slots/country classes: diff --git a/frontend/public/schemas/20251121/linkml/modules/slots/altitude.yaml b/frontend/public/schemas/20251121/linkml/modules/slots/altitude.yaml new file mode 100644 index 0000000000..5498792f0f --- /dev/null +++ b/frontend/public/schemas/20251121/linkml/modules/slots/altitude.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/slots/documentation_url.yaml b/frontend/public/schemas/20251121/linkml/modules/slots/documentation_url.yaml new file mode 100644 index 0000000000..cddac2c5c5 --- /dev/null +++ b/frontend/public/schemas/20251121/linkml/modules/slots/documentation_url.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/slots/managed_by.yaml b/frontend/public/schemas/20251121/linkml/modules/slots/managed_by.yaml new file mode 100644 index 0000000000..922c43bf4a --- /dev/null +++ b/frontend/public/schemas/20251121/linkml/modules/slots/managed_by.yaml @@ -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 diff --git a/frontend/public/schemas/20251121/linkml/modules/slots/storage_location.yaml b/frontend/public/schemas/20251121/linkml/modules/slots/storage_location.yaml new file mode 100644 index 0000000000..1b746afc5c --- /dev/null +++ b/frontend/public/schemas/20251121/linkml/modules/slots/storage_location.yaml @@ -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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 20925e4b46..ddb4e05274 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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: , }, - // Home page with navigation at bottom - { - path: '/', - element: ( - - - - ), - children: [ - { - index: true, - element: , - }, - ], - }, - // Other pages with navigation at top { path: '/', element: ( @@ -55,6 +40,11 @@ const router = createBrowserRouter([ ), children: [ + { + // Home page shows the Project Plan + index: true, + element: , + }, { path: 'visualize', element: , @@ -98,9 +88,11 @@ const router = createBrowserRouter([ function App() { return ( - - - + + + + + ); } diff --git a/frontend/src/components/layout/HomeLayout.css b/frontend/src/components/layout/HomeLayout.css deleted file mode 100644 index 6ecad672a5..0000000000 --- a/frontend/src/components/layout/HomeLayout.css +++ /dev/null @@ -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; -} diff --git a/frontend/src/components/layout/HomeLayout.tsx b/frontend/src/components/layout/HomeLayout.tsx deleted file mode 100644 index 0bada2731d..0000000000 --- a/frontend/src/components/layout/HomeLayout.tsx +++ /dev/null @@ -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 ( -
-
- - - {/* Navigation appears at the bottom after all content */} -
- - -
-
-
- ); -} diff --git a/frontend/src/components/layout/Layout.css b/frontend/src/components/layout/Layout.css index b4c3a6bdb6..0b839723a0 100644 --- a/frontend/src/components/layout/Layout.css +++ b/frontend/src/components/layout/Layout.css @@ -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; diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx index ee0c8767a5..5610a568e6 100644 --- a/frontend/src/components/layout/Layout.tsx +++ b/frontend/src/components/layout/Layout.tsx @@ -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() {
- {/* Footer only shows on non-fullscreen pages, or at very bottom of scroll */} + {/* Minimal footer - only shows on pages with scrollable content */} {!isFullscreenPage && ( diff --git a/frontend/src/components/layout/Navigation.css b/frontend/src/components/layout/Navigation.css index 00436a2de9..06c3e244c7 100644 --- a/frontend/src/components/layout/Navigation.css +++ b/frontend/src/components/layout/Navigation.css @@ -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 { diff --git a/frontend/src/components/layout/Navigation.tsx b/frontend/src/components/layout/Navigation.tsx index ad15ad4a1a..e82b766c68 100644 --- a/frontend/src/components/layout/Navigation.tsx +++ b/frontend/src/components/layout/Navigation.tsx @@ -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(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 (
)} diff --git a/frontend/src/contexts/LanguageContext.tsx b/frontend/src/contexts/LanguageContext.tsx new file mode 100644 index 0000000000..45d3a79955 --- /dev/null +++ b/frontend/src/contexts/LanguageContext.tsx @@ -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(undefined); + +interface LanguageProviderProps { + children: ReactNode; +} + +export function LanguageProvider({ children }: LanguageProviderProps) { + // Default to Dutch as primary language + const [language, setLanguage] = useState(() => { + // 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 ( + + {children} + + ); +} + +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; + const item = section[subKey]; + if (!item) return subKey; + return language === 'en' ? item.en : item.nl; +} diff --git a/frontend/src/pages/ProjectPlanPage.tsx b/frontend/src/pages/ProjectPlanPage.tsx index 604cd68bfa..bd54a9fe94 100644 --- a/frontend/src/pages/ProjectPlanPage.tsx +++ b/frontend/src/pages/ProjectPlanPage.tsx @@ -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(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [language, setLanguage] = useState('nl'); + const { language } = useLanguage(); // Use global language context const [activeTab, setActiveTab] = useState(0); const timelineRef = useRef(null); const networkRef = useRef(null); @@ -504,10 +503,6 @@ export default function ProjectPlanPage() { } }, [projectData, activeTab, drawTimeline, drawNetwork]); - const handleLanguageChange = (_: React.MouseEvent, newLang: Language | null) => { - if (newLang) setLanguage(newLang); - }; - if (loading) { return ( @@ -550,15 +545,6 @@ export default function ProjectPlanPage() { {getText(projectData.plan_description, projectData.plan_description_en, language).substring(0, 200)}... - - NL - EN - {/* Summary Cards */} @@ -834,17 +820,6 @@ export default function ProjectPlanPage() { )} - {/* Footer */} - - - {language === 'nl' ? 'Laatst bijgewerkt: ' : 'Last updated: '}{projectData.modified_date} - {' | '} - {language === 'nl' ? 'Opdrachtgever: ' : 'Commissioner: '} - - {projectData.commissioning_organization.agent_name} - - - diff --git a/frontend/src/pages/Visualize.css b/frontend/src/pages/Visualize.css index d84df77e46..697cb3a8c3 100644 --- a/frontend/src/pages/Visualize.css +++ b/frontend/src/pages/Visualize.css @@ -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; diff --git a/frontend/src/pages/Visualize.tsx b/frontend/src/pages/Visualize.tsx index aae27be0d7..4ec9f3f852 100644 --- a/frontend/src/pages/Visualize.tsx +++ b/frontend/src/pages/Visualize.tsx @@ -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(persistedState.current?.fileName || ''); const [currentCategory, setCurrentCategory] = useState<'rdf' | 'uml' | null>(persistedState.current?.currentCategory || null); - const [schemasSectionExpanded, setSchemasSectionExpanded] = useState(false); const [loadSectionExpanded, setLoadSectionExpanded] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); const [customInput, setCustomInput] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(true); @@ -177,136 +168,9 @@ export function Visualize() { const [umlError, setUmlError] = useState(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) => { @@ -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() { - {/* Search Bar */} -
- - setSearchQuery(e.target.value)} - className="search-input" - /> - {searchQuery && ( - - )} -
- {/* Load Data Section */}
+ {/* Generate Section */} +
+

+ + Generate Visualizations +

- {schemasSectionExpanded && ( -
- {/* RDF Schemas */} - {rdfSchemas.length > 0 && ( -
-

RDF Schemas

- {rdfSchemas.map((schema, index) => ( - - ))} -
- )} - - {/* UML Schemas */} - {umlSchemas.length > 0 && ( -
-

UML Diagrams

- {umlSchemas.map((schema, index) => ( - - ))} -
- )} - - {filteredSchemas.length === 0 && ( -

No schemas found for "{searchQuery}"

- )} +
+ {/* UML Diagram Generator */} +
+

UML Diagram

+

+ Generate from LinkML schema (Heritage Custodian Ontology) +

+
- )} + + {/* RDF Overview Generator */} +
+

RDF Overview

+

+ Generate RDF graph of all heritage custodians +

+ +

+ Available after converting enriched entries to instances +

+
+
{/* Current File */} diff --git a/schemas/20251121/linkml/modules/classes/Appellation.yaml b/schemas/20251121/linkml/modules/classes/Appellation.yaml index c73fb6ce97..b07859f0a0 100644 --- a/schemas/20251121/linkml/modules/classes/Appellation.yaml +++ b/schemas/20251121/linkml/modules/classes/Appellation.yaml @@ -9,7 +9,7 @@ imports: - linkml:types - ../metadata - ../enums/AppellationTypeEnum - - CustodianName + - ./CustodianName classes: diff --git a/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml b/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml index b6b0af205d..399dc4ab16 100644 --- a/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml +++ b/schemas/20251121/linkml/modules/classes/ArchiveOrganizationType.yaml @@ -4,7 +4,7 @@ title: Archive Organization Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/access_policy classes: diff --git a/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml b/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml index 6486827f75..50ced80131 100644 --- a/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml +++ b/schemas/20251121/linkml/modules/classes/BioCustodianType.yaml @@ -3,7 +3,7 @@ name: BioCustodianType title: Biological and Zoological Custodian Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/collection_size classes: diff --git a/schemas/20251121/linkml/modules/classes/CallForApplication.yaml b/schemas/20251121/linkml/modules/classes/CallForApplication.yaml index c69f662ed6..7996f9e12d 100644 --- a/schemas/20251121/linkml/modules/classes/CallForApplication.yaml +++ b/schemas/20251121/linkml/modules/classes/CallForApplication.yaml @@ -34,7 +34,7 @@ imports: - linkml:types - ../enums/CallForApplicationStatusEnum - ../enums/FundingRequirementTypeEnum - - FundingRequirement + - ./FundingRequirement - ../slots/contact_email - ../slots/keywords diff --git a/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml b/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml index acc184ca26..7b916d8bc2 100644 --- a/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml +++ b/schemas/20251121/linkml/modules/classes/CollectionManagementSystem.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml b/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml index 53ea2ed5dd..1c34a3fe0d 100644 --- a/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml +++ b/schemas/20251121/linkml/modules/classes/CustodianArchive.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml b/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml index 8e3952c8e7..67b1c33b9a 100644 --- a/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml +++ b/schemas/20251121/linkml/modules/classes/DigitalPlatform.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml b/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml index d8aa3fa9b8..0b24164a14 100644 --- a/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml +++ b/schemas/20251121/linkml/modules/classes/EducationProviderType.yaml @@ -37,7 +37,7 @@ see_also: - https://www.wikidata.org/wiki/Q132560468 # university archive imports: - - CustodianType + - ./CustodianType prefixes: hc: https://nde.nl/ontology/hc/ diff --git a/schemas/20251121/linkml/modules/classes/GalleryType.yaml b/schemas/20251121/linkml/modules/classes/GalleryType.yaml index b230d33cf1..7c4c91a56f 100644 --- a/schemas/20251121/linkml/modules/classes/GalleryType.yaml +++ b/schemas/20251121/linkml/modules/classes/GalleryType.yaml @@ -4,7 +4,7 @@ title: Gallery Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: GalleryType: diff --git a/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml b/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml index 334964a75f..018b12923b 100644 --- a/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml +++ b/schemas/20251121/linkml/modules/classes/GeoSpatialPlace.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/GiftShop.yaml b/schemas/20251121/linkml/modules/classes/GiftShop.yaml index 35e5f1f150..00e9501cec 100644 --- a/schemas/20251121/linkml/modules/classes/GiftShop.yaml +++ b/schemas/20251121/linkml/modules/classes/GiftShop.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml b/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml index 6a7bbb2159..1ca1d8378b 100644 --- a/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml +++ b/schemas/20251121/linkml/modules/classes/HeritageSocietyType.yaml @@ -39,7 +39,7 @@ see_also: - https://www.wikidata.org/wiki/Q15755503 # archaeological society imports: - - CustodianType + - ./CustodianType prefixes: hc: https://nde.nl/ontology/hc/ diff --git a/schemas/20251121/linkml/modules/classes/LibraryType.yaml b/schemas/20251121/linkml/modules/classes/LibraryType.yaml index 1b16cae02a..43585ea5ab 100644 --- a/schemas/20251121/linkml/modules/classes/LibraryType.yaml +++ b/schemas/20251121/linkml/modules/classes/LibraryType.yaml @@ -4,7 +4,7 @@ title: Library Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/cataloging_standard classes: diff --git a/schemas/20251121/linkml/modules/classes/MuseumType.yaml b/schemas/20251121/linkml/modules/classes/MuseumType.yaml index a22629e0a8..8e98dd396d 100644 --- a/schemas/20251121/linkml/modules/classes/MuseumType.yaml +++ b/schemas/20251121/linkml/modules/classes/MuseumType.yaml @@ -4,7 +4,7 @@ title: Museum Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType - ../slots/collection_focus - ../slots/cataloging_standard diff --git a/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml b/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml index 553de9979c..6681507ab9 100644 --- a/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml +++ b/schemas/20251121/linkml/modules/classes/OfficialInstitutionType.yaml @@ -4,7 +4,7 @@ title: Official Institution Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: OfficialInstitutionType: diff --git a/schemas/20251121/linkml/modules/classes/Project.yaml b/schemas/20251121/linkml/modules/classes/Project.yaml index 2f32fc42bc..9133f95377 100644 --- a/schemas/20251121/linkml/modules/classes/Project.yaml +++ b/schemas/20251121/linkml/modules/classes/Project.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml b/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml index 3d86c9732f..cddc711df8 100644 --- a/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml +++ b/schemas/20251121/linkml/modules/classes/ReconstructionActivity.yaml @@ -9,10 +9,10 @@ imports: - linkml:types - ../metadata - ../enums/ReconstructionActivityTypeEnum - - ReconstructionAgent - - TimeSpan - - CustodianObservation - - ConfidenceMeasure + - ./ReconstructionAgent + - ./TimeSpan + - ./CustodianObservation + - ./ConfidenceMeasure classes: diff --git a/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml b/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml index 168763825d..4d4c2beaff 100644 --- a/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml +++ b/schemas/20251121/linkml/modules/classes/RegistrationInfo.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml b/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml index 680a7a3405..14df14273f 100644 --- a/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml +++ b/schemas/20251121/linkml/modules/classes/ResearchOrganizationType.yaml @@ -4,7 +4,7 @@ title: Research Organization Type Classification imports: - linkml:types - - CustodianType + - ./CustodianType classes: ResearchOrganizationType: diff --git a/schemas/20251121/linkml/modules/classes/Settlement.yaml b/schemas/20251121/linkml/modules/classes/Settlement.yaml index 2a7123fdbb..4a3c448aca 100644 --- a/schemas/20251121/linkml/modules/classes/Settlement.yaml +++ b/schemas/20251121/linkml/modules/classes/Settlement.yaml @@ -14,8 +14,8 @@ title: Settlement Class imports: - linkml:types - - Country - - Subregion + - ./Country + - ./Subregion - ../slots/country - ../slots/subregion - ../slots/geonames_id diff --git a/schemas/20251121/linkml/modules/classes/Storage.yaml b/schemas/20251121/linkml/modules/classes/Storage.yaml index 19330a3f9f..aa2e47c01e 100644 --- a/schemas/20251121/linkml/modules/classes/Storage.yaml +++ b/schemas/20251121/linkml/modules/classes/Storage.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/classes/Subregion.yaml b/schemas/20251121/linkml/modules/classes/Subregion.yaml index 7e985526b6..497d82ee75 100644 --- a/schemas/20251121/linkml/modules/classes/Subregion.yaml +++ b/schemas/20251121/linkml/modules/classes/Subregion.yaml @@ -15,7 +15,7 @@ title: Subregion Class imports: - linkml:types - - Country + - ./Country - ../slots/country classes: diff --git a/schemas/20251121/linkml/modules/slots/altitude.yaml b/schemas/20251121/linkml/modules/slots/altitude.yaml new file mode 100644 index 0000000000..5498792f0f --- /dev/null +++ b/schemas/20251121/linkml/modules/slots/altitude.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/slots/documentation_url.yaml b/schemas/20251121/linkml/modules/slots/documentation_url.yaml new file mode 100644 index 0000000000..cddac2c5c5 --- /dev/null +++ b/schemas/20251121/linkml/modules/slots/documentation_url.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/slots/managed_by.yaml b/schemas/20251121/linkml/modules/slots/managed_by.yaml new file mode 100644 index 0000000000..922c43bf4a --- /dev/null +++ b/schemas/20251121/linkml/modules/slots/managed_by.yaml @@ -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 diff --git a/schemas/20251121/linkml/modules/slots/storage_location.yaml b/schemas/20251121/linkml/modules/slots/storage_location.yaml new file mode 100644 index 0000000000..1b746afc5c --- /dev/null +++ b/schemas/20251121/linkml/modules/slots/storage_location.yaml @@ -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 diff --git a/scripts/enrich_nde_entries_ghcid.py b/scripts/enrich_nde_entries_ghcid.py new file mode 100644 index 0000000000..c6c5283350 --- /dev/null +++ b/scripts/enrich_nde_entries_ghcid.py @@ -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()