diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4bb5eee..ebbe0d71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# October 2024
+# December 2024
## Templates
### Incidental Occurrence v3.0.0
#### [Schema v3.0.0](https://github.com/gaiaresources/abis-mapping/blob/main/abis_mapping/templates/incidental_occurrence_data_v3/schema.json) changes (by column order).
diff --git a/abis_mapping/templates/survey_metadata_v3/README.md b/abis_mapping/templates/survey_metadata_v3/README.md
new file mode 100644
index 00000000..bce67b50
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/README.md
@@ -0,0 +1,5 @@
+# Template Description
+TBC
+
+# Template Instructions
+See `instructions.pdf` for more details
diff --git a/abis_mapping/templates/survey_metadata_v3/examples/minimal.csv b/abis_mapping/templates/survey_metadata_v3/examples/minimal.csv
new file mode 100644
index 00000000..a3e93b7e
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/examples/minimal.csv
@@ -0,0 +1,2 @@
+surveyID,surveyName,surveyPurpose,surveyType,surveyStart,surveyEnd,targetTaxonomicScope,targetHabitatScope,spatialCoverageWKT,geodeticDatum,surveyOrgs,surveyMethodCitation,surveyMethodDescription,surveyMethodURL,keywords
+COL1,"Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",Summer sampling for peak insect diversity.,Wet pitfall trapping,21/01/2015,3/02/2015,Coleoptera | Insecta,Woodland,"POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",GDA2020,"NSW Department of Planning, Industry and Environment | CSIRO","Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9""","Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",https://doi.org/10.1002/9781118945568.ch11 | https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf ,ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study
diff --git a/abis_mapping/templates/survey_metadata_v3/examples/minimal.ttl b/abis_mapping/templates/survey_metadata_v3/examples/minimal.ttl
new file mode 100644
index 00000000..06ac4275
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/examples/minimal.ttl
@@ -0,0 +1,161 @@
+@prefix abis: .
+@prefix bdr: .
+@prefix geo: .
+@prefix prov: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix schema: .
+@prefix skos: .
+@prefix tern: .
+@prefix time: .
+@prefix xsd: .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Survey Collection - Survey Type - Wet pitfall trapping" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Survey Collection - Target Habitat Scope - Woodland" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Survey Collection - Target Taxonomic Scope - Coleoptera" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Survey Collection - Target Taxonomic Scope - Insecta" ;
+ tern:hasAttribute .
+
+ a abis:Project ;
+ schema:hasPart ;
+ schema:identifier "COL1" ;
+ schema:isPartOf ;
+ schema:name "Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer" .
+
+ a rdfs:Datatype ;
+ skos:prefLabel "surveyID source" ;
+ prov:qualifiedAttribution [ a prov:Attribution ;
+ prov:agent ;
+ prov:hadRole ] .
+
+ a rdfs:Datatype ;
+ skos:prefLabel "surveyID source" ;
+ prov:qualifiedAttribution [ a prov:Attribution ;
+ prov:agent ;
+ prov:hadRole ] .
+
+ a tern:Attribute ;
+ schema:isPartOf ;
+ tern:attribute ;
+ tern:hasSimpleValue "Wet pitfall trapping" ;
+ tern:hasValue .
+
+ a tern:Attribute ;
+ schema:isPartOf ;
+ tern:attribute ;
+ tern:hasSimpleValue "Woodland" ;
+ tern:hasValue .
+
+ a tern:Attribute ;
+ schema:isPartOf ;
+ tern:attribute ;
+ tern:hasSimpleValue "Coleoptera" ;
+ tern:hasValue .
+
+ a tern:Attribute ;
+ schema:isPartOf ;
+ tern:attribute ;
+ tern:hasSimpleValue "Insecta" ;
+ tern:hasValue .
+
+ a skos:Concept ;
+ skos:broader ;
+ skos:definition "A type of targetTaxonomicScope" ;
+ skos:inScheme ;
+ skos:prefLabel "Coleoptera" ;
+ schema:citation "https://linked.data.gov.au/dataset/bdr/00000000-0000-0000-0000-000000000000"^^xsd:anyURI .
+
+ a skos:Concept ;
+ skos:broader ;
+ skos:definition "A type of targetTaxonomicScope" ;
+ skos:inScheme ;
+ skos:prefLabel "Insecta" ;
+ schema:citation "https://linked.data.gov.au/dataset/bdr/00000000-0000-0000-0000-000000000000"^^xsd:anyURI .
+
+ a tern:IRI,
+ tern:Value ;
+ rdfs:label "Wet pitfall trapping" ;
+ rdf:value .
+
+ a tern:IRI,
+ tern:Value ;
+ rdfs:label "Woodland" ;
+ rdf:value .
+
+ a tern:IRI,
+ tern:Value ;
+ rdfs:label "Coleoptera" ;
+ rdf:value .
+
+ a tern:IRI,
+ tern:Value ;
+ rdfs:label "Insecta" ;
+ rdf:value .
+
+ a prov:Agent ;
+ schema:name "CSIRO" .
+
+ a prov:Agent ;
+ schema:name "NSW Department of Planning, Industry and Environment" .
+
+ a tern:Survey ;
+ bdr:purpose "Summer sampling for peak insect diversity." ;
+ bdr:target "Coleoptera",
+ "Insecta" ;
+ geo:hasGeometry _:N27fdbd9c9077333e51ac0d0600000000 ;
+ time:hasTime [ a time:TemporalEntity ;
+ time:hasBeginning [ a time:Instant ;
+ time:inXSDDate "2015-01-21"^^xsd:date ] ;
+ time:hasEnd [ a time:Instant ;
+ time:inXSDDate "2015-02-03"^^xsd:date ] ] ;
+ prov:hadPlan ;
+ schema:identifier "COL1"^^,
+ "COL1"^^ ;
+ schema:keywords "farmland",
+ "ground beetle",
+ "habitat",
+ "morphology",
+ "remnant vegetation",
+ "split-plot study",
+ "traits",
+ "woodland" ;
+ schema:name "Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer" .
+
+ a prov:Plan ;
+ schema:citation "Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9\"" ;
+ schema:description "Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location." ;
+ schema:isPartOf ;
+ schema:url "https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf"^^xsd:anyURI,
+ "https://doi.org/10.1002/9781118945568.ch11"^^xsd:anyURI .
+
+ a tern:Dataset .
+
+[] a rdf:Statement ;
+ geo:hasGeometry [ a geo:Geometry ;
+ geo:asWKT " POLYGON ((-33.826 146.363, -33.826 148.499, -34.411 148.499, -33.826 146.363))"^^geo:wktLiteral ] ;
+ rdf:object _:N27fdbd9c9077333e51ac0d0600000000 ;
+ rdf:predicate geo:hasGeometry ;
+ rdf:subject ;
+ rdfs:comment "supplied as" .
+
+_:N27fdbd9c9077333e51ac0d0600000000 a geo:Geometry ;
+ geo:asWKT " POLYGON ((-33.826 146.363, -33.826 148.499, -34.411 148.499, -33.826 146.363))"^^geo:wktLiteral .
+
diff --git a/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_chronological_order.csv b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_chronological_order.csv
new file mode 100644
index 00000000..b4e8d7c4
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_chronological_order.csv
@@ -0,0 +1,2 @@
+surveyID,surveyName,surveyPurpose,surveyType,surveyStart,surveyEnd,targetTaxonomicScope,targetHabitatScope,spatialCoverageWKT,geodeticDatum,surveyOrgs,surveyMethodCitation,surveyMethodDescription,surveyMethodURL,keywords
+COL1,"Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",Summer sampling for peak insect diversity.,Wet pitfall trapping,21/01/2015,3/02/2014,Coleoptera | Insecta,Woodland,"POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",GDA2020,"NSW Department of Planning, Industry and Environment | CSIRO","Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9""","Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",https://doi.org/10.1002/9781118945568.ch11 | https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf ,ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study
diff --git a/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_missing_datum.csv b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_missing_datum.csv
new file mode 100644
index 00000000..e70fd20d
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_missing_datum.csv
@@ -0,0 +1,2 @@
+surveyID,surveyName,surveyPurpose,surveyType,surveyStart,surveyEnd,targetTaxonomicScope,targetHabitatScope,spatialCoverageWKT,geodeticDatum,surveyOrgs,surveyMethodCitation,surveyMethodDescription,surveyMethodURL,keywords
+COL1,"Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",Summer sampling for peak insect diversity.,Wet pitfall trapping,21/01/2015,3/02/2015,Coleoptera | Insecta,Woodland,"POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",,"NSW Department of Planning, Industry and Environment | CSIRO","Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9""","Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",https://doi.org/10.1002/9781118945568.ch11 | https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf ,ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study
diff --git a/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_too_many_rows.csv b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_too_many_rows.csv
new file mode 100644
index 00000000..6dbb5c7f
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/examples/minimal_error_too_many_rows.csv
@@ -0,0 +1,3 @@
+surveyID,surveyName,surveyPurpose,surveyType,surveyStart,surveyEnd,targetTaxonomicScope,targetHabitatScope,spatialCoverageWKT,geodeticDatum,surveyOrgs,surveyMethodCitation,surveyMethodDescription,surveyMethodURL,keywords
+COL1,"Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",Summer sampling for peak insect diversity.,Wet pitfall trapping,21/01/2015,3/02/2015,Coleoptera | Insecta,Woodland,"POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",GDA2020,"NSW Department of Planning, Industry and Environment | CSIRO","Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9""","Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",https://doi.org/10.1002/9781118945568.ch11 | https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf ,ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study
+COL2,"Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",Summer sampling for peak insect diversity.,Wet pitfall trapping,21/01/2015,3/02/2015,Coleoptera | Insecta,Woodland,"POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",GDA2020,"NSW Department of Planning, Industry and Environment | CSIRO","Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9""","Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",https://doi.org/10.1002/9781118945568.ch11 | https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf ,ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study
diff --git a/abis_mapping/templates/survey_metadata_v3/mapping.py b/abis_mapping/templates/survey_metadata_v3/mapping.py
new file mode 100644
index 00000000..a9861be1
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/mapping.py
@@ -0,0 +1,903 @@
+"""Provides ABIS mapper for `survey_metadata.csv` template v3"""
+
+# Standard
+import dataclasses
+
+# Third-party
+import frictionless
+import frictionless.checks
+import rdflib
+
+# Local
+from abis_mapping import base
+from abis_mapping import plugins
+from abis_mapping import models
+from abis_mapping import utils
+
+# Typing
+from typing import Any
+
+
+# Constants / shortcuts
+a = rdflib.RDF.type
+PRINCIPAL_INVESTIGATOR = rdflib.URIRef("https://linked.data.gov.au/def/data-roles/principalInvestigator")
+CONCEPT_SURVEY_TYPE = utils.rdf.uri("concept/surveyType", utils.namespaces.EXAMPLE)
+CONCEPT_TARGET_HABITAT_SCOPE = rdflib.URIRef("https://linked.data.gov.au/def/nrm/ae2c88be-63d5-44d3-95ac-54b14c4a4b28")
+CONCEPT_TARGET_TAXONOMIC_SCOPE = rdflib.URIRef(
+ "https://linked.data.gov.au/def/nrm/7ea12fed-6b87-4c20-9ab4-600b32ce15ec",
+)
+
+
+# Dataclass used in mapping
+@dataclasses.dataclass
+class SurveyIDDatatype:
+ """Contains data items for a survey organisation"""
+
+ name: str
+ datatype: rdflib.URIRef
+ agent: rdflib.URIRef
+
+
+@dataclasses.dataclass
+class AttributeValue:
+ """Contains data items to enable producing attribute, value and collection nodes"""
+
+ raw: str
+ attribute: rdflib.URIRef
+ value: rdflib.URIRef
+ collection: rdflib.URIRef
+
+
+class SurveyMetadataMapper(base.mapper.ABISMapper):
+ """ABIS mapper for `survey_metadata.csv` v3"""
+
+ def apply_validation(self, data: base.types.ReadableType, **kwargs: Any) -> frictionless.Report:
+ """Applies Frictionless validation for the 'survey_metadata.csv' template
+
+ Args:
+ data (base.types.ReadableType): Raw data to be validated
+ **kwargs (Any): Additional keyword arguments.
+
+ Returns:
+ frictionless.Report: Validation report for the specified data.
+ """
+ # Construct Schema
+ schema = self.extra_fields_schema(
+ data=data,
+ full_schema=True,
+ )
+
+ # Construct Resource
+ resource = frictionless.Resource(
+ source=data,
+ format="csv", # TODO -> Hardcoded to csv for now
+ schema=schema,
+ encoding="utf-8",
+ )
+
+ # Validate
+ report: frictionless.Report = resource.validate(
+ checklist=frictionless.Checklist(
+ checks=[
+ # Enforces non-empty and maximum row count.
+ frictionless.checks.table_dimensions(max_rows=1, min_rows=1),
+ # Extra Custom Checks
+ plugins.tabular.IsTabular(),
+ plugins.chronological.ChronologicalOrder(
+ field_names=[
+ "surveyStart",
+ "surveyEnd",
+ ]
+ ),
+ plugins.mutual_inclusion.MutuallyInclusive(
+ field_names=[
+ "spatialCoverageWKT",
+ "geodeticDatum",
+ ]
+ ),
+ ],
+ ),
+ )
+
+ # Return validation report
+ return report
+
+ def apply_mapping_row(
+ self,
+ *,
+ row: frictionless.Row,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ extra_schema: frictionless.Schema,
+ base_iri: rdflib.Namespace,
+ **kwargs: Any,
+ ) -> None:
+ """Applies mapping for a row in the `survey_metadata.csv` template.
+
+ Args:
+ row (frictionless.Row): Row to be processed in the dataset.
+ dataset (rdflib.URIRef): Dataset IRI this row is a part of.
+ graph (rdflib.URIRef): Graph to map row into.
+ extra_schema (frictionless.Schema): Schema of extra fields.
+ base_iri (rdflib.Namespace): Base IRI to use for mapping.
+ """
+ # Set the row number to start from the data, excluding header
+ row_num = row.row_number - 1
+
+ # Create BDR project IRI
+ project = utils.rdf.uri(f"project/SSD-Survey-Project/{row_num}", base_iri)
+
+ # Create TERN survey IRI from surveyID field
+ survey_id: str | None = row["surveyID"]
+ survey = utils.iri_patterns.survey_iri(base_iri, survey_id)
+
+ # Create survey plan IRI
+ survey_plan = utils.iri_patterns.plan_iri(
+ base_iri,
+ "survey",
+ (survey_id or str(row_num)), # fallback to row number when surveyID not available.
+ )
+
+ # Conditionally create survey type attribute, value and collection IRIs
+ row_survey_type: str | None = row["surveyType"]
+ if row_survey_type:
+ survey_type_attribute = utils.iri_patterns.attribute_iri(base_iri, "surveyType", row_survey_type)
+ survey_type_value = utils.iri_patterns.attribute_value_iri(base_iri, "surveyType", row_survey_type)
+ survey_type_collection = utils.iri_patterns.attribute_collection_iri(
+ base_iri, "Survey", "surveyType", row_survey_type
+ )
+ else:
+ survey_type_attribute = None
+ survey_type_value = None
+ survey_type_collection = None
+
+ # Create target habitat scope attribute and value objects
+ target_habitat_objects: list[AttributeValue] = []
+ if target_habitats := row["targetHabitatScope"]:
+ for target_habitat in target_habitats:
+ target_habitat_objects.append(
+ AttributeValue(
+ raw=target_habitat,
+ attribute=utils.iri_patterns.attribute_iri(base_iri, "targetHabitatScope", target_habitat),
+ value=utils.iri_patterns.attribute_value_iri(base_iri, "targetHabitatScope", target_habitat),
+ collection=utils.iri_patterns.attribute_collection_iri(
+ base_iri, "Survey", "targetHabitatScope", target_habitat
+ ),
+ ),
+ )
+
+ # Create target taxonomic scope attribute and value IRIs (list input)
+ target_taxonomic_objects: list[AttributeValue] = []
+ if target_taxa := row["targetTaxonomicScope"]:
+ for target_taxon in target_taxa:
+ target_taxonomic_objects.append(
+ AttributeValue(
+ raw=target_taxon,
+ attribute=utils.iri_patterns.attribute_iri(base_iri, "targetTaxonomicScope", target_taxon),
+ value=utils.iri_patterns.attribute_value_iri(base_iri, "targetTaxonomicScope", target_taxon),
+ collection=utils.iri_patterns.attribute_collection_iri(
+ base_iri, "Survey", "targetTaxonomicScope", target_taxon
+ ),
+ )
+ )
+
+ # Create survey orgs iris
+ survey_org_objects: list[SurveyIDDatatype] = []
+ if survey_orgs := row["surveyOrgs"]:
+ for raw_org in survey_orgs:
+ survey_org_objects.append(
+ SurveyIDDatatype(
+ name=raw_org,
+ datatype=utils.iri_patterns.datatype_iri("surveyID", raw_org),
+ agent=utils.iri_patterns.agent_iri(raw_org),
+ )
+ )
+
+ # Add BDR project
+ self.add_project(
+ uri=project,
+ survey=survey,
+ dataset=dataset,
+ graph=graph,
+ row=row,
+ )
+
+ # Add BDR survey
+ self.add_survey(
+ uri=survey,
+ survey_plan=survey_plan,
+ survey_org_objects=survey_org_objects,
+ row=row,
+ graph=graph,
+ )
+
+ # Attach temporal coverage if present
+ self.add_temporal_coverage(
+ uri=survey,
+ row=row,
+ graph=graph,
+ )
+
+ # Add spatial coverage values
+ self.add_spatial_coverage(
+ uri=survey,
+ row=row,
+ graph=graph,
+ )
+
+ for so_obj in survey_org_objects:
+ # Add survey ID source datatype nodes
+ self.add_survey_id_source_datatypes(
+ uri=so_obj.datatype,
+ agent=so_obj.agent,
+ graph=graph,
+ )
+
+ # Add agent
+ self.add_agent(
+ uri=so_obj.agent,
+ name=so_obj.name,
+ graph=graph,
+ )
+
+ # Add plan
+ self.add_plan(
+ uri=survey_plan,
+ row=row,
+ dataset=dataset,
+ graph=graph,
+ )
+
+ # Add survey type attribute node
+ self.add_survey_type_attribute(
+ uri=survey_type_attribute,
+ survey_type_value=survey_type_value,
+ row_survey_type=row_survey_type,
+ dataset=dataset,
+ graph=graph,
+ )
+
+ # Add survey type value node
+ self.add_survey_type_value(
+ uri=survey_type_value,
+ row_survey_type=row_survey_type,
+ dataset=dataset,
+ graph=graph,
+ base_iri=base_iri,
+ )
+
+ # Add survey type collection node
+ self.add_survey_type_collection(
+ uri=survey_type_collection,
+ row_survey_type=row_survey_type,
+ survey_type_attribute=survey_type_attribute,
+ survey_plan=survey_plan,
+ dataset=dataset,
+ graph=graph,
+ )
+
+ # Iterate through target habitat objects
+ for th_obj in target_habitat_objects:
+ # Add target habitat scope attribute node
+ self.add_target_habitat_attribute(
+ uri=th_obj.attribute,
+ dataset=dataset,
+ target_habitat_value=th_obj.value,
+ raw_value=th_obj.raw,
+ graph=graph,
+ )
+
+ # Add target habitat scope value node
+ self.add_target_habitat_value(
+ uri=th_obj.value,
+ dataset=dataset,
+ raw_value=th_obj.raw,
+ graph=graph,
+ base_iri=base_iri,
+ )
+
+ # Add target habitat scope collection
+ self.add_target_habitat_collection(
+ uri=th_obj.collection,
+ raw_value=th_obj.raw,
+ target_habitat_attribute=th_obj.attribute,
+ survey_plan=survey_plan,
+ dataset=dataset,
+ graph=graph,
+ )
+
+ # Iterate through target taxonomic objects
+ for tt_obj in target_taxonomic_objects:
+ # Add target taxonomic scope attribute node
+ self.add_target_taxonomic_attribute(
+ uri=tt_obj.attribute,
+ dataset=dataset,
+ target_taxon_value=tt_obj.value,
+ raw_value=tt_obj.raw,
+ graph=graph,
+ )
+
+ # Add target taxonomic scope value node
+ self.add_target_taxonomic_value(
+ uri=tt_obj.value,
+ dataset=dataset,
+ raw_value=tt_obj.raw,
+ graph=graph,
+ base_iri=base_iri,
+ )
+
+ # Add target taxonomic scope collection node
+ self.add_target_taxonomic_scope_collection(
+ uri=tt_obj.collection,
+ raw_value=tt_obj.raw,
+ target_taxon_attribute=tt_obj.attribute,
+ survey_plan=survey_plan,
+ dataset=dataset,
+ graph=graph,
+ )
+
+ # Add extra columns JSON
+ self.add_extra_fields_json(
+ subject_uri=survey,
+ row=row,
+ graph=graph,
+ extra_schema=extra_schema,
+ )
+
+ def add_project(
+ self,
+ uri: rdflib.URIRef,
+ survey: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ row: frictionless.Row,
+ ) -> None:
+ """Adds the ABIS project to the graph
+
+ Args:
+ uri (rdflib.URIRef): URI to use for this node.
+ survey (rdflib.URIRef): BDR survey uri.
+ dataset (rdflib.URIRef): Dataset uri.
+ graph (rdflib.Graph): Graph to add to.
+ row (frictionless.Row): Row to be processed in dataset.
+ """
+ # Extract relevant values from row
+ project_id = row["surveyID"]
+ project_name = row["surveyName"]
+
+ # Add type and attach to dataset
+ graph.add((uri, a, utils.namespaces.ABIS.Project))
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+
+ # Add (required) project name, id (not required) and purpose (not required).
+ graph.add((uri, rdflib.SDO.name, rdflib.Literal(project_name)))
+ if project_id:
+ graph.add((uri, rdflib.SDO.identifier, rdflib.Literal(project_id)))
+
+ # Attach survey
+ graph.add((uri, rdflib.SDO.hasPart, survey))
+
+ def add_survey(
+ self,
+ uri: rdflib.URIRef,
+ survey_plan: rdflib.URIRef,
+ survey_org_objects: list[SurveyIDDatatype],
+ row: frictionless.Row,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds the tern:Survey to the graph.
+
+ Args:
+ uri (rdflib.URIRef): URI of the survey.
+ survey_plan (rdflib.URIRef): URI of survey plan
+ survey_org_objects (list[SurveyIDDatatype]): Data objects
+ describing the survey organisations
+ row (frictionless.Row): Data row provided in the data csv
+ graph (rdflib.Graph): The graph to be modified.
+ """
+ # Add type and dataset
+ graph.add((uri, a, utils.namespaces.TERN.Survey))
+
+ # Add survey name
+ graph.add((uri, rdflib.SDO.name, rdflib.Literal(row["surveyName"])))
+
+ # Add survey ID
+ if (survey_id := row["surveyID"]) is not None:
+ # Add survey id literals per organisation
+ for survey_org in survey_org_objects:
+ id_literal = rdflib.Literal(lexical_or_value=survey_id, datatype=survey_org.datatype)
+ graph.add((uri, rdflib.SDO.identifier, id_literal))
+
+ # Add survey id as type string if no organisation provided
+ if len(survey_org_objects) == 0:
+ id_literal = rdflib.Literal(survey_id)
+ graph.add((uri, rdflib.SDO.identifier, id_literal))
+
+ # Add taxonomic coverage
+ if taxonomic_coverage := row["targetTaxonomicScope"]:
+ for taxa in taxonomic_coverage:
+ graph.add((uri, utils.namespaces.BDR.target, rdflib.Literal(taxa)))
+
+ # Add purpose
+ if purpose := row["surveyPurpose"]:
+ graph.add((uri, utils.namespaces.BDR.purpose, rdflib.Literal(purpose)))
+
+ # Add plan
+ graph.add((uri, rdflib.PROV.hadPlan, survey_plan))
+
+ # Add keywords
+ if keywords := row["keywords"]:
+ for keyword in keywords:
+ graph.add((uri, rdflib.SDO.keywords, rdflib.Literal(keyword)))
+
+ def add_spatial_coverage(
+ self,
+ uri: rdflib.URIRef,
+ row: frictionless.Row,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds the spatial coverage fields to the graph.
+
+ Args:
+ uri (rdflib.URIRef): Base URI the spatial information will be attached
+ row (frictionless.Row): Data row provided in the data csv
+ graph (rdflib.Graph): Graph to be modified
+ """
+ # Extract relevant values
+ datum = row["geodeticDatum"]
+ sc_geometry = row["spatialCoverageWKT"]
+
+ if not (datum and sc_geometry):
+ return
+
+ # Construct geometry
+ geometry = models.spatial.Geometry(
+ raw=sc_geometry,
+ datum=datum,
+ )
+
+ # Add spatial coverage
+ geometry_node = rdflib.BNode()
+ graph.add((uri, utils.namespaces.GEO.hasGeometry, geometry_node))
+ graph.add((geometry_node, a, utils.namespaces.GEO.Geometry))
+ graph.add((geometry_node, utils.namespaces.GEO.asWKT, geometry.to_transformed_crs_rdf_literal()))
+
+ self.add_geometry_supplied_as(
+ subj=uri,
+ pred=utils.namespaces.GEO.hasGeometry,
+ obj=geometry_node,
+ geom=geometry,
+ graph=graph,
+ )
+
+ def add_temporal_coverage(
+ self,
+ uri: rdflib.URIRef,
+ row: frictionless.Row,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds the temporal coverage fields to the graph.
+
+ Args:
+ uri (rdflib.URIRef): Base URI the temporal information will be attached
+ row (frictionless.Row): Data row provided in the data csv
+ graph (rdflib.Graph): Graph to be modified
+ """
+ # Determine if any dates are present in the row
+ start_date: models.temporal.Timestamp = row["surveyStart"]
+ end_date: models.temporal.Timestamp = row["surveyEnd"]
+ if not start_date and not end_date:
+ return
+
+ # Create temporal coverage node
+ temporal_coverage = rdflib.BNode()
+ graph.add((temporal_coverage, a, rdflib.TIME.TemporalEntity))
+ if start_date:
+ begin = rdflib.BNode()
+ graph.add((temporal_coverage, rdflib.TIME.hasBeginning, begin))
+ graph.add((begin, a, rdflib.TIME.Instant))
+ graph.add((begin, start_date.rdf_in_xsd, start_date.to_rdf_literal()))
+ if end_date:
+ end = rdflib.BNode()
+ graph.add((temporal_coverage, rdflib.TIME.hasEnd, end))
+ graph.add((end, a, rdflib.TIME.Instant))
+ graph.add((end, end_date.rdf_in_xsd, end_date.to_rdf_literal()))
+
+ # Attach to survey node
+ graph.add((uri, rdflib.TIME.hasTime, temporal_coverage))
+
+ def add_survey_id_source_datatypes(
+ self,
+ uri: rdflib.URIRef,
+ agent: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds the source datatype nodes to graph.
+
+ Args:
+ uri (rdflib.URIRef): The reference uri.
+ agent (rdflib.URIRef): Agent uri.
+ graph (rdflib.Graph): Graph to be modified.
+ """
+ # Add type
+ graph.add((uri, a, rdflib.RDFS.Datatype))
+
+ # Add label
+ graph.add((uri, rdflib.SKOS.prefLabel, rdflib.Literal("surveyID source")))
+
+ # Add attribution
+ attribution = rdflib.BNode()
+ graph.add((attribution, a, rdflib.PROV.Attribution))
+ graph.add((attribution, rdflib.PROV.agent, agent))
+ graph.add((attribution, rdflib.PROV.hadRole, PRINCIPAL_INVESTIGATOR))
+ graph.add((uri, rdflib.PROV.qualifiedAttribution, attribution))
+
+ def add_agent(
+ self,
+ uri: rdflib.URIRef,
+ name: str,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds agent to graph.
+
+ Args:
+ uri (rdflib.URIRef): Agent reference
+ name (str): Original name supplied
+ graph (rdflib.Graph): Graph to be modified
+ """
+ # Add type
+ graph.add((uri, a, rdflib.PROV.Agent))
+
+ # Add name
+ graph.add((uri, rdflib.SDO.name, utils.rdf.uri_or_string_literal(name)))
+
+ def add_plan(
+ self,
+ uri: rdflib.URIRef,
+ row: frictionless.Row,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds plan to graph.
+
+ Args:
+ uri: Plan reference.
+ row: Raw data row.
+ dataset: URI for the dataset node.
+ graph: Graph to be modified.
+ """
+ # Add type
+ graph.add((uri, a, rdflib.PROV.Plan))
+
+ # add link to dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+
+ # Add citation(s)
+ if citations := row["surveyMethodCitation"]:
+ for citation in citations:
+ graph.add((uri, rdflib.SDO.citation, rdflib.Literal(citation)))
+
+ # Add description
+ if description := row["surveyMethodDescription"]:
+ graph.add((uri, rdflib.SDO.description, rdflib.Literal(description)))
+
+ # Add method url(s)
+ if method_urls := row["surveyMethodURL"]:
+ for method_url in method_urls:
+ graph.add((uri, rdflib.SDO.url, rdflib.Literal(method_url, datatype=rdflib.XSD.anyURI)))
+
+ def add_survey_type_attribute(
+ self,
+ uri: rdflib.URIRef | None,
+ survey_type_value: rdflib.URIRef | None,
+ row_survey_type: str | None,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds survey type attribute node.
+
+ Args:
+ uri: Attribute node for survey type
+ survey_type_value: Value node for Survey type
+ row_survey_type: Raw value from the template for surveyType
+ dataset: Dataset the data belongs.
+ graph: Graph to be modified.
+ """
+ # Non default field, return if not present
+ if uri is None:
+ return
+
+ # Add type
+ graph.add((uri, a, utils.namespaces.TERN.Attribute))
+
+ # Add dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+
+ # Add attribute
+ graph.add((uri, utils.namespaces.TERN.attribute, CONCEPT_SURVEY_TYPE))
+
+ # Add value
+ if row_survey_type:
+ graph.add((uri, utils.namespaces.TERN.hasSimpleValue, rdflib.Literal(row_survey_type)))
+ if survey_type_value:
+ graph.add((uri, utils.namespaces.TERN.hasValue, survey_type_value))
+
+ def add_survey_type_value(
+ self,
+ uri: rdflib.URIRef | None,
+ row_survey_type: str | None,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ base_iri: rdflib.Namespace,
+ ) -> None:
+ """Adds the survey type value node to graph.
+
+ Args:
+ uri: Survey type value iri.
+ row_survey_type: Raw value from the template for surveyType
+ dataset: Dataset raw data belongs.
+ graph: Graph to be modified.
+ base_iri: Namespace used to construct IRIs
+ """
+ # Return no value IRI
+ if uri is None:
+ return
+
+ # Add type
+ graph.add((uri, a, utils.namespaces.TERN.IRI))
+ graph.add((uri, a, utils.namespaces.TERN.Value))
+
+ if row_survey_type:
+ # Add label
+ graph.add((uri, rdflib.RDFS.label, rdflib.Literal(row_survey_type)))
+
+ # Retrieve vocab for field
+ vocab = self.fields()["surveyType"].get_flexible_vocab()
+
+ # Add value
+ term = vocab(graph=graph, source=dataset, base_iri=base_iri).get(row_survey_type)
+ graph.add((uri, rdflib.RDF.value, term))
+
+ def add_survey_type_collection(
+ self,
+ *,
+ uri: rdflib.URIRef | None,
+ row_survey_type: str | None,
+ survey_type_attribute: rdflib.URIRef | None,
+ survey_plan: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Add a survey type Collection to the graph
+
+ Args:
+ uri: The uri for the Collection.
+ row_survey_type: surveyType value from template.
+ survey_type_attribute: The uri for the attribute node.
+ survey_plan: The uri for the Survey Plan node that wil be a member of the Collection.
+ dataset: The uri for the dateset node.
+ graph: The graph.
+ """
+ # Check if collection node should be created
+ if uri is None:
+ return
+
+ # Add type
+ graph.add((uri, a, rdflib.SDO.Collection))
+ # Add identifier
+ if row_survey_type:
+ graph.add(
+ (
+ uri,
+ rdflib.SDO.name,
+ rdflib.Literal(f"Survey Collection - Survey Type - {row_survey_type}"),
+ )
+ )
+ # Add link to dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+ # Add link to attribute
+ if survey_type_attribute:
+ graph.add((uri, utils.namespaces.TERN.hasAttribute, survey_type_attribute))
+ # add link to the Survey Plan node
+ graph.add((uri, rdflib.SDO.member, survey_plan))
+
+ def add_target_habitat_attribute(
+ self,
+ uri: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ target_habitat_value: rdflib.URIRef,
+ raw_value: str,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Adds the target habitat scope attribute node.
+
+ Args:
+ uri (rdflib.URIRef): Subject of the node.
+ dataset (rdflib.URIRef): Dataset raw data belongs.
+ target_habitat_value (rdflib.URIRef): Corresponding value.
+ raw_value (str): Raw data.
+ graph (rdflib.Graph): Graph to be modified.
+ """
+ # Add type
+ graph.add((uri, a, utils.namespaces.TERN.Attribute))
+
+ # Add dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+
+ # Add attribute concept
+ graph.add((uri, utils.namespaces.TERN.attribute, CONCEPT_TARGET_HABITAT_SCOPE))
+
+ # Add value
+ graph.add((uri, utils.namespaces.TERN.hasSimpleValue, rdflib.Literal(raw_value)))
+ graph.add((uri, utils.namespaces.TERN.hasValue, target_habitat_value))
+
+ def add_target_habitat_value(
+ self,
+ uri: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ raw_value: str,
+ graph: rdflib.Graph,
+ base_iri: rdflib.Namespace,
+ ) -> None:
+ """Add the target habitat scope value node.
+
+ Args:
+ uri (rdflib.URIRef): Subject of the node.
+ dataset (rdflib.URIRef): Dataset raw data belongs.
+ raw_value (str): Raw data.
+ graph (rdflib.Graph): Graph to be modified.
+ base_iri (rdflib.Namespace): Namespace used to construct IRIs
+ """
+ # Add types
+ graph.add((uri, a, utils.namespaces.TERN.IRI))
+ graph.add((uri, a, utils.namespaces.TERN.Value))
+
+ # Add label
+ graph.add((uri, rdflib.RDFS.label, rdflib.Literal(raw_value)))
+
+ # Retrieve vocab for field
+ vocab = self.fields()["targetHabitatScope"].get_flexible_vocab()
+
+ # Add value
+ term = vocab(graph=graph, source=dataset, base_iri=base_iri).get(raw_value)
+ graph.add((uri, rdflib.RDF.value, term))
+
+ def add_target_habitat_collection(
+ self,
+ *,
+ uri: rdflib.URIRef,
+ raw_value: str,
+ target_habitat_attribute: rdflib.URIRef,
+ survey_plan: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Add a target habitat Collection to the graph
+
+ Args:
+ uri: The uri for the Collection.
+ raw_value: targetTaxonomicScope value from template.
+ target_habitat_attribute: The uri for the attribute node.
+ survey_plan: The uri for the Survey Plan node that wil be a member of the Collection.
+ dataset: The uri for the dateset node.
+ graph: The graph.
+ """
+ # Add type
+ graph.add((uri, a, rdflib.SDO.Collection))
+ # Add identifier
+ graph.add(
+ (
+ uri,
+ rdflib.SDO.name,
+ rdflib.Literal(f"Survey Collection - Target Habitat Scope - {raw_value}"),
+ )
+ )
+ # Add link to dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+ # Add link to attribute
+ graph.add((uri, utils.namespaces.TERN.hasAttribute, target_habitat_attribute))
+ # add link to the Survey Plan node
+ graph.add((uri, rdflib.SDO.member, survey_plan))
+
+ def add_target_taxonomic_attribute(
+ self,
+ uri: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ target_taxon_value: rdflib.URIRef,
+ raw_value: str,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Add the target taxonomic scope node.
+
+ Args:
+ uri (rdflib.URIRef): Subject of the node.
+ dataset (rdflib.URIRef): Dataset raw data belongs.
+ target_taxon_value (rdflib.URIRef): Corresponding
+ value node.
+ raw_value (str): Raw data provided.
+ graph (rdflib.Graph): Graph to be modified.
+ """
+ # Add type
+ graph.add((uri, a, utils.namespaces.TERN.Attribute))
+
+ # Add dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+
+ # Add attribute concept
+ graph.add((uri, utils.namespaces.TERN.attribute, CONCEPT_TARGET_TAXONOMIC_SCOPE))
+
+ # Add values
+ graph.add((uri, utils.namespaces.TERN.hasSimpleValue, rdflib.Literal(raw_value)))
+ graph.add((uri, utils.namespaces.TERN.hasValue, target_taxon_value))
+
+ def add_target_taxonomic_value(
+ self,
+ uri: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ raw_value: str,
+ graph: rdflib.Graph,
+ base_iri: rdflib.Namespace,
+ ) -> None:
+ """Adds the target toxonomic scope value node.
+
+ Args:
+ uri (rdflib.URIRef): Subject of the node.
+ dataset (rdflib.URIRef): Dataset raw data belongs.
+ raw_value (str): Raw data provided.
+ graph (rdflib.Graph): Graph to be modified.
+ base_iri (rdflib.Namespace): Namespace used to construct IRIs
+ """
+ # Add types
+ graph.add((uri, a, utils.namespaces.TERN.IRI))
+ graph.add((uri, a, utils.namespaces.TERN.Value))
+
+ # Add label
+ graph.add((uri, rdflib.RDFS.label, rdflib.Literal(raw_value)))
+
+ # Retrieve vocab for field
+ vocab = self.fields()["targetTaxonomicScope"].get_flexible_vocab()
+
+ # Add value
+ term = vocab(graph=graph, source=dataset, base_iri=base_iri).get(raw_value)
+ graph.add((uri, rdflib.RDF.value, term))
+
+ def add_target_taxonomic_scope_collection(
+ self,
+ *,
+ uri: rdflib.URIRef,
+ raw_value: str,
+ target_taxon_attribute: rdflib.URIRef,
+ survey_plan: rdflib.URIRef,
+ dataset: rdflib.URIRef,
+ graph: rdflib.Graph,
+ ) -> None:
+ """Add a target taxonomic scope Collection to the graph
+
+ Args:
+ uri: The uri for the Collection.
+ raw_value: targetTaxonomicScope value from template.
+ target_taxon_attribute: The uri for the attribute node.
+ survey_plan: The uri for the Survey Plan node that wil be a member of the Collection.
+ dataset: The uri for the dateset node.
+ graph: The graph.
+ """
+ # Add type
+ graph.add((uri, a, rdflib.SDO.Collection))
+ # Add identifier
+ graph.add(
+ (
+ uri,
+ rdflib.SDO.name,
+ rdflib.Literal(f"Survey Collection - Target Taxonomic Scope - {raw_value}"),
+ )
+ )
+ # Add link to dataset
+ graph.add((uri, rdflib.SDO.isPartOf, dataset))
+ # Add link to attribute
+ graph.add((uri, utils.namespaces.TERN.hasAttribute, target_taxon_attribute))
+ # add link to the Survey Plan node
+ graph.add((uri, rdflib.SDO.member, survey_plan))
+
+
+# Register Mapper
+base.mapper.register_mapper(SurveyMetadataMapper)
diff --git a/abis_mapping/templates/survey_metadata_v3/metadata.json b/abis_mapping/templates/survey_metadata_v3/metadata.json
new file mode 100644
index 00000000..83656bb1
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/metadata.json
@@ -0,0 +1,13 @@
+{
+ "name": "survey_metadata",
+ "label": "Systematic Survey Metadata Template",
+ "version": "3.0.0",
+ "description": "A template for systematic survey metadata",
+ "biodiversity_type": "Systematic Survey Metadata",
+ "spatial_type": "Point, line, polygon",
+ "file_type": "CSV",
+ "sampling_type": "systematic survey",
+ "template_url": "https://raw.githubusercontent.com/gaiaresources/abis-mapping/main/abis_mapping/templates/survey_metadata_v3/survey_metadata.csv",
+ "schema_url": "https://raw.githubusercontent.com/gaiaresources/abis-mapping/main/abis_mapping/templates/survey_metadata_v3/schema.json",
+ "template_lifecycle_status": "beta"
+}
diff --git a/abis_mapping/templates/survey_metadata_v3/schema.json b/abis_mapping/templates/survey_metadata_v3/schema.json
new file mode 100644
index 00000000..ae4682fb
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/schema.json
@@ -0,0 +1,194 @@
+{
+ "fields": [
+ {
+ "name": "surveyID",
+ "title": "Survey ID",
+ "description": "The identifier for the survey. Important if more there is more than one survey a the project.",
+ "example": "COL1",
+ "type": "string",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "surveyName",
+ "title": "Survey Name",
+ "description": "Brief title for the survey.",
+ "type": "string",
+ "format": "default",
+ "example": "Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits - Summer",
+ "constraints": {
+ "required": true
+ }
+ },
+ {
+ "name": "surveyPurpose",
+ "title": "Survey Purpose",
+ "description": "A description of the survey objective",
+ "type": "string",
+ "format": "default",
+ "example": "Summer sampling for peak insect diversity.",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "surveyType",
+ "title": "Survey Type",
+ "description": "Description of type of survey conducted",
+ "example": "Wet pitfall trapping",
+ "type": "string",
+ "format": "default",
+ "constraints": {
+ "required": false
+ },
+ "vocabularies": [
+ "SURVEY_TYPE"
+ ]
+ },
+ {
+ "name": "surveyStart",
+ "title": "Survey Start",
+ "description": "The date data collection commenced.",
+ "example": "21/09/2020",
+ "type": "timestamp",
+ "format": "default",
+ "constraints": {
+ "required": true
+ }
+ },
+ {
+ "name": "surveyEnd",
+ "title": "Survey End",
+ "description": "The date data collection was completed.",
+ "example": "23/09/2020",
+ "type": "timestamp",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "targetTaxonomicScope",
+ "title": "Target Taxonomic Scope",
+ "description": "The range of biological taxa covered by the survey. Multiple terms are allowed, separated by a vertical bar aka pipe |",
+ "example": "Coleoptera | Formicidae",
+ "type": "list",
+ "format": "default",
+ "constraints": {
+ "required": false
+ },
+ "vocabularies": [
+ "TARGET_TAXONOMIC_SCOPE"
+ ]
+ },
+ {
+ "name": "targetHabitatScope",
+ "title": "Target Habitat Scope",
+ "description": "The habitats targeted for sampling during the survey. Multiple terms are allowed, separated by a vertical bar aka pipe |",
+ "example": "Woodland",
+ "type": "list",
+ "format": "default",
+ "constraints": {
+ "required": false
+ },
+ "vocabularies": [
+ "TARGET_HABITAT_SCOPE"
+ ]
+ },
+ {
+ "name": "spatialCoverageWKT",
+ "title": "Spatial Coverage (WKT)",
+ "description": "Well Known Text (WKT) expression of the geographic coordinates that describe the survey's spatial extent. Ensure the coordinates are arranged in 'longitude latitude' order and do not include the CRS in the WKT expression (it comes from the geodeticDatum field).",
+ "example": "POLYGON ((146.363 -33.826, 148.499 -33.826, 148.499 -34.411, 146.363 -33.826))",
+ "type": "wkt",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "geodeticDatum",
+ "title": "Spatial Coverage (Geodetic Datum)",
+ "description": "The geodetic datum upon which the geographic coordinates in the Spatial coverage (WKT) are based.",
+ "example": "GDA2020",
+ "type": "string",
+ "format": "default",
+ "url": "https://dwc.tdwg.org/terms/#dwc:geodeticDatum",
+ "constraints": {
+ "required": false,
+ "enum": [
+ "AGD66",
+ "EPSG:4202",
+ "AGD84",
+ "EPSG:4203",
+ "GDA2020",
+ "EPSG:7844",
+ "GDA94",
+ "EPSG:4283",
+ "WGS84",
+ "EPSG:4326"
+ ]
+ },
+ "vocabularies": [
+ "GEODETIC_DATUM"
+ ]
+ },
+ {
+ "name": "surveyOrgs",
+ "title": "Survey Orgs",
+ "description": "Name of organisations or individuals for whom Survey is being conducted. Multiple terms are allowed, separated by a vertical bar aka pipe |",
+ "example": "NSW Department of Planning, Industry and Environment | CSIRO",
+ "type": "list",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "surveyMethodCitation",
+ "title": "Survey Method Citation",
+ "description": "A citation or reference to the survey methods used.",
+ "example": "Ng, K., Barton, P.S., Blanchard, W. et al. Disentangling the effects of farmland use, habitat edges, and vegetation structure on ground beetle morphological traits. Oecologia 188, 645–657 (2018). https://doi.org/10.1007/s00442-018-4180-9",
+ "type": "list",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "surveyMethodDescription",
+ "title": "Survey Method Description",
+ "description": "Free text description of the survey method used.",
+ "example": "Our experimental design consisted of four 400 m transects running from inside each woodland patch out into four adjoining farmland uses (crop, rested, woody debris application, revegetation plantings). To quantify potential edge efects on beetle species traits, we sampled beetles at five locations along each transect: 200 and 20 m inside woodlands, 200 and 20 m inside farmlands, and at the woodland–farmland edge (0 m). Each sampling location comprised a pair of wet invertebrate pitfall traps. separated by a drift fence (60 cm long x 10 cm high) to help direct arthropods into traps. We opened a total of 220 pairs of traps for 14 days during spring (Oct–Nov 2014), and repeated sampling during summer (January–February 2015). Beetle samples from each pitfall trap pair, and across the two time periods, were pooled to provide one sample per sampling location.",
+ "type": "string",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "surveyMethodURL",
+ "title": "Survey Method URL",
+ "description": "A DOI or link to the reference about the survey method, if available.",
+ "example": "https://biocollect.ala.org.au/document/download/2022-01/202201%20CBR%20Flora%20and%20Vegetation%20report_draftv1.pdf | https://doi.org/10.1002/9781118945568.ch11",
+ "type": "list",
+ "format": "uri",
+ "constraints": {
+ "required": false
+ }
+ },
+ {
+ "name": "keywords",
+ "title": "Keywords",
+ "description": "Terms, phrases or descriptors that highlight the key attributes of the study. Multiple terms are allowed, separated by a vertical bar aka pipe |",
+ "example": "ground beetle | habitat | morphology | traits | farmland | woodland | remnant vegetation | split-plot study",
+ "type": "list",
+ "format": "default",
+ "constraints": {
+ "required": false
+ }
+ }
+ ]
+}
diff --git a/abis_mapping/templates/survey_metadata_v3/survey_metadata.csv b/abis_mapping/templates/survey_metadata_v3/survey_metadata.csv
new file mode 100644
index 00000000..89fc5ae9
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/survey_metadata.csv
@@ -0,0 +1 @@
+surveyID,surveyName,surveyPurpose,surveyType,surveyStart,surveyEnd,targetTaxonomicScope,targetHabitatScope,spatialCoverageWKT,geodeticDatum,surveyOrgs,surveyMethodCitation,surveyMethodDescription,surveyMethodURL,keywords
diff --git a/abis_mapping/templates/survey_metadata_v3/templates/instructions.md b/abis_mapping/templates/survey_metadata_v3/templates/instructions.md
new file mode 100644
index 00000000..faa7b354
--- /dev/null
+++ b/abis_mapping/templates/survey_metadata_v3/templates/instructions.md
@@ -0,0 +1,181 @@
+{% extends "BASE_TEMPLATE base.md" %}
+{% block body %}
+# SYSTEMATIC SURVEY METADATA TEMPLATE INSTRUCTIONS
+
+## Intended Usage
+This Systematic Survey Metadata template should be used to record metadata relating to a Systematic Survey dataset.
+
+The Systematic Survey Metadata template **must be used in combination** with the
+Systematic Survey Occurrence template and, in some cases, the Systematic Survey Site template
+with or without the Systematic Survey Site Visit template.
+
+Templates have been provided to facilitate integration of your data into the Biodiversity
+Data Repository database. Not all types of data have been catered for in the available
+templates at this stage; therefore, if you are unable to find a suitable template, please
+contact to make us aware of your data needs.
+
+## Data Validation Requirements:
+For data validation, you will need your data file to:
+
+- be the correct **file format**,
+- have **fields that match the template downloaded** (do not remove, or
+ change the order of fields),
+- have extant values for **mandatory fields** (see Table 1), and
+- comply with all **data value constraints**; for example the geographic coordinates are
+ consistent with a [geodeticDatum](#geodeticDatum-vocabularies) type of the ***{{values.geodetic_datum_count}}*** available
+ options.
+- only **one row of metadata** should be included and only the first row of metadata will be accepted
+ (this symbolises one Survey per dataset submission).
+
+Additional fields may be added **after the templated fields** (noting that the data type
+is not assumed and values will be encoded as strings).
+
+### FILE FORMAT
+- The systematic survey metadata template is a [UTF-8](#appendix-iv-utf-8) encoded csv (not Microsoft
+Excel Spreadsheets). Be sure to save this file with your data as a .csv (UTF-8) as
+follows, otherwise it will not pass the in-browser csv validation step upon upload.
+
`[MS Excel: Save As > More options > Tools > Web options > Save this document as >
+Unicode (UTF-8)]`
+- **Do not include empty rows.**
+
+### FILE NAME
+
+When making a manual submission to the Biodiversity Data Repository,
+the file name must include the version number
+of this biodiversity data template (`v{{ metadata.version }}`).
+The following format is an example of a valid file name:
+
+`data_descripion-v{{ metadata.version }}-additional_description.csv`
+
+where:
+
+* `data_description`: A short description of the data (e.g. `survey_meta`, `test_data`).
+* `v{{ metadata.version }}`: The version number of this template.
+* `additional_description`: (Optional) Additional description of the data, if needed (e.g. `test_data`).
+* `.csv`: Ensure the file name ends with `.csv`.
+
+For example, `survey_meta-v{{ metadata.version }}-test_data.csv` or `test_data-v{{ metadata.version }}.csv`
+
+### FILE SIZE
+MS Excel imposes a limit of 1,048,576 rows on a spreadsheet, limiting a CSV file to the
+header row followed by 1,048,575 occurrences. Furthermore, MS Excel has a 32,767 character
+limit on individual cells in a spreadsheet. These limits may be overcome by using or
+editing CSV files with other software.
+
+Larger datasets may be more readily ingested using the API interface. Please contact
+ to make us aware of your data needs.
+
+## TEMPLATE FIELDS
+The template file contains the field names in the top row that form part of the core Survey
+data model. Table 1 will assist you in transferring your data to the template with the following
+information:
+
+- **Field name** in the template (and an external link to the Darwin Core standard for that
+field where available);
+- **Description** of the field;
+- **Required** i.e. whether the field is **mandatory,
+conditionally mandatory, or optional**;
+- **Datatype format** required for the data values for example text (string), number
+(integer, float), or date; and
+- **Example/s** of an entry for that field.
+- **Vocabulary links** within this document (for example pick list values) where relevant.
+The fields that have suggested values options for the fields in Table 1 are listed in
+Table 2 in alphabetical order of field name.
+
+### ADDITIONAL FIELDS
+Data that do not match the existing template fields may be added as additional columns in
+the CSV files after the templated fields.
+For example, `sampleSizeUnit`, `sampleSizeValue`.
+
+Table 1: Systematic Survey Metadata template fields with descriptions, conditions,
+datatype format, and examples.
+
+{{tables.fields}}
+
+## CHANGELOG
+
+No changes from Systematic Survey Metadata Template v2.0.0
+
+## APPENDICES
+### APPENDIX-I: Vocabulary List
+With the exception of `geodeticDatum`, data validation
+does not require adherence to the vocabularies for the various vocabularied fields.. These vocabularies are provided as a
+means of assistance in developing consistent language within the database. New terms can be added
+to more appropriately describe your data that goes beyond the current list.
+
+Table 2 provides some
+suggested values from existing sources such as: [Biodiversity Information Standard (TDWG)](https://dwc.tdwg.org/),
+[EPSG.io Coordinate systems worldwide](https://epsg.io/), the [Global Biodiversity Information
+System](https://rs.gbif.org/), and [Open Nomenclature in the biodiversity
+era](https://doi.org/10.1111/2041-210X.12594).
+
+Table 2: Suggested values for the controlled vocabulary fields in the template. Each term has
+a preferred label with a definition to aid understanding of its meaning. For some terms, alternative
+labels with similar semantics are provided.
Note: the value for `geodeticDatum`
+must come from one of five options in this table.
+
+{{tables.vocabularies}}
+
+### APPENDIX-II: Well Known Text (WKT)
+For general information on how WKT coordinate reference data is formatted is available [here](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry).
+The length of a WKT string or of its components is not prescribed; however, MS Excel *does* has a
+32,767 (32K) character limit on individual cells in a spreadsheet.
+
+It is possible to edit CSV files outside of Excel in order to include more than 32K characters.
+
+**Note:** Ensure the coordinates are arranged in `longitude latitude` order and do not include the CRS in the WKT expression (it comes from the geodeticDatum field).
+
+![Multipart geometries (2D) WKT](assets/multipart_geometries_2d_wkt.png)
+
*Source: Mwtoews - CC BY-SA 3.0 - Wikipedia *
+
+### APPENDIX-III: Timestamp
+Following date and date-time formats are acceptable within the timestamp:
+
+| TYPE | FORMAT |
+| --- |-------------------------------------------------------------------------------------------------------------------------------------|
+| **xsd:dateTimeStamp with timezone** | yyyy-mm-ddThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) OR
yyyy-mm-ddThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) OR
yyyy-mm-ddThh:mmTZD (eg 1997-07-16T19:20+01:00)|
+| **xsd:dateTime** | yyyy-mm-ddThh:mm:ss.s (eg 1997-07-16T19:20:30.45) OR
yyyy-mm-ddThh:mm:ss (eg 1997-07-16T19:20:30) OR
yyyy-mm-ddThh:mm (eg 1997-07-16T19:20) |
+| **xsd:Date** | dd/mm/yyyy OR
d/m/yyyy OR
yyyy-mm-dd OR
yyyy-m-d |
+| **xsd:gYearMonth** | mm/yyyy OR
m/yyyy OR
yyyy-mm |
+| **xsd:gYear** | yyyy |
+
+Where:
+ `yyyy`: four-digit year
+ `mm`: two-digit month (01=January, etc.)
+ `dd`: two-digit day of month (01 through 31)
+ `hh`: two digits of hour (00 through 23) (am/pm NOT allowed)
+ `mm`: two digits of minute (00 through 59)
+ `ss`: two digits of second (00 through 59)
+ `s`: one or more digits representing a decimal fraction of a second
+ `TZD`: time zone designator (Z or +hh:mm or -hh:mm)
+
+### APPENDIX-IV: UTF-8
+UTF-8 encoding is considered a best practice for handling character encoding, especially in
+the context of web development, data exchange, and modern software systems. UTF-8
+(Unicode Transformation Format, 8-bit) is a variable-width character encoding capable of
+encoding all possible characters (code points) in Unicode.
+Here are some reasons why UTF-8 is recommended:
+- **Universal Character Support:** UTF-8 can represent almost all characters from all writing
+ systems in use today. This includes characters from various languages, mathematical symbols,
+ and other special characters.
+- **Backward Compatibility:** UTF-8 is backward compatible with ASCII (American
+ Standard Code for Information Interchange). The first 128 characters in UTF-8 are
+ identical to ASCII, making it easy to work with systems that use ASCII.
+- **Efficiency:** UTF-8 is space-efficient for Latin-script characters (common in English
+ and many other languages). It uses one byte for ASCII characters and up to four
+ bytes for other characters. This variable-length encoding minimises storage and
+ bandwidth requirements.
+- **Web Standards:** UTF-8 is the dominant character encoding for web content. It is
+ widely supported by browsers, servers, and web-related technologies.
+- **Globalisation:** As software applications become more globalised, supporting a wide
+ range of languages and scripts becomes crucial. UTF-8 is well-suited for
+ internationalisation and multilingual support.
+- **Compatibility with Modern Systems:** UTF-8 is the default encoding for many
+ programming languages, databases, and operating systems. Choosing UTF-8 helps
+ ensure compatibility across different platforms and technologies.
+
+When working with text data, UTF-8 encoding is recommended to avoid issues related to character
+representation and ensure that a diverse set of characters and languages is supported.
+
+For assistance, please contact:
+{% endblock %}
diff --git a/abis_mapping/templates/survey_occurrence_data_v3/README.md b/abis_mapping/templates/survey_occurrence_data_v3/README.md
new file mode 100644
index 00000000..d2595b7c
--- /dev/null
+++ b/abis_mapping/templates/survey_occurrence_data_v3/README.md
@@ -0,0 +1,5 @@
+# Template Description
+A template to translate some Darwin Core fields
+
+# Template Instructions
+See `instructions.pdf` for more details
diff --git a/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.csv b/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.csv
new file mode 100644
index 00000000..49ddf57d
--- /dev/null
+++ b/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.csv
@@ -0,0 +1,17 @@
+providerRecordID,providerRecordIDSource,locality,decimalLatitude,decimalLongitude,geodeticDatum,coordinateUncertaintyInMeters,dataGeneralizations,eventDateStart,eventDateEnd,samplingProtocol,basisOfRecord,recordedBy,recordNumber,occurrenceStatus,habitat,establishmentMeans,organismRemarks,individualCount,organismQuantity,organismQuantityType,lifeStage,sex,reproductiveCondition,ownerRecordID,ownerRecordIDSource,collectionCode,catalogNumber,catalogNumberSource,otherCatalogNumbers,otherCatalogNumbersSource,preparations,preparedDate,associatedSequences,sequencingMethod,verbatimIdentification,dateIdentified,identifiedBy,identificationMethod,scientificName,identificationQualifier,identificationRemarks,acceptedNameUsage,kingdom,taxonRank,threatStatus,conservationAuthority,threatStatusCheckProtocol,threatStatusDateDetermined,threatStatusDeterminedBy,sensitivityCategory,sensitivityAuthority,surveyID,siteID,siteVisitID
+1,WAM,Cowaramup Bay Road,-33.8,115.21,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Calothamnus lateralis var. crassus,,Stream Environment and Water Pty Ltd,,Calothamnus lateralis var. crassus,,,,Plantae,,,,,,,,,,,
+2,WAM,Cowaramup Bay Road,-33.86,115.01,WGS84,,,26/09/2019,,,,,PE:12:8831,,,,,,,,,,,,,,,,,,,,,,Boronia anceps,,Stream Environment and Water Pty Ltd,,Boronia anceps,,,,Plantae,,,,,,,,,,,
+3,WAM,Cowaramup Bay Road,-33.86,115.01,WGS84,,,26/09/2019,,,,Test Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Boronia anceps,,Stream Environment and Water Pty Ltd,,Boronia anceps,,,,Plantae,,,,,,,,,,,
+4,WAM,Cowaramup Bay Road,-33.86,115.01,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Boronia anceps,,Stream Environment and Water Pty Ltd,,Boronia anceps,,,,Plantae,,,,,,,,,,,
+5,WAM,Cowaramup Bay Road,-33.86,114.99,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Banksia sessilis var. cordata,,Stream Environment and Water Pty Ltd,,Banksia sessilis var. cordata,,,,Plantae,,,,,,,,,,,
+6,WAM,Cowaramup Bay Road,-33.86,114.99,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Banksia sessilis var. cordata,,Stream Environment and Water Pty Ltd,,Banksia sessilis var. cordata,,,,Plantae,,,,,,,,,,,
+7,WAM,Cowaramup Bay Road,-33.86,114.99,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Banksia sessilis var. cordata,,Stream Environment and Water Pty Ltd,,Banksia sessilis var. cordata,,,,Plantae,,,,,,,,,,,
+8,WAM,Cowaramup Bay Road,-33.86,114.99,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Banksia sessilis var. cordata,,Stream Environment and Water Pty Ltd,,Banksia sessilis var. cordata,,,,Plantae,,,,,,,,,,,
+9,WAM,Cowaramup Bay Road,-33.86,115.02,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Caladenia excelsa,,Stream Environment and Water Pty Ltd,,Caladenia excelsa,,,,Plantae,,,,,,,,,,,
+10,WAM,Cowaramup Bay Road,-33.86,115.02,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Caladenia excelsa,,Stream Environment and Water Pty Ltd,,Caladenia excelsa,,,,Plantae,,,,,,,,,,,
+11,WAM,Cowaramup Bay Road,-33.86,115.02,WGS84,,,26/09/2019,,,,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,,,,,,,,,,Caladenia ?excelsa,,Stream Environment and Water Pty Ltd,,Caladenia excelsa,?,One unopened flower when recorded and one leaf only. ID not confirmed,,Plantae,,,,,,,,,,,
+12,WAM,,-33.8,115.21,WGS84,,,26/09/2019,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Caladenia excelsa,,,,Plantae,,,,,,,,,,,
+13,WAM,Cowaramup Bay Road,-33.86,115.02,WGS84,,,26/09/2019,,,PreservedSpecimen,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,C01,CC123,WAM,,,,,,,,,Stream Environment and Water Pty Ltd,,Caladenia excelsa,,,,Plantae,,,,,,,,,,,
+14,WAM,Cowaramup Bay Road,-33.86,115.02,WGS84,20,Coordinates rounded to the nearest 10 km for conservation concern,26/09/2019,,,HumanObservation,Stream Environment and Water Pty Ltd,,,,,,,,,,,,,,C01,CC456,WAM,,,,,,,Caladenia ?excelsa,,Stream Environment and Water Pty Ltd,,Caladenia excelsa,?,Could not confirm the ID due to damaged flower,,Plantae,,,,,,,,,,,
+8022FSJMJ079c5cf,WAM,Cowaramup Bay Road,-33.8,115.21,WGS84,50,Coordinates rounded to the nearest 10 km for conservation concern,26/09/2019,,human observation,PreservedSpecimen,Stream Environment and Water Pty Ltd,PE:12:8832,present,"Closed forest of Melaleuca lanceolata. White, grey or brown sand, sandy loam.",native,Dried out leaf tips,2,,,adult,male,No breeding evident,MR-456,Stream Environment and Water Pty Ltd,32237,ARACH,WAM,BHP2012-7521 | M12378,BHP,Wet (in ethanol or some other preservative),26/09/2019,https://www.ncbi.nlm.nih.gov/nuccore/MH040669.1 | https://www.ncbi.nlm.nih.gov/nuccore/MH040616.1,Sanger dideoxy sequencing,Caladenia ?excelsa,2019-09-27T12:34+08:00,Stream Environment and Water Pty Ltd,Visually identified in the field (sighting),Caladenia excelsa,species incerta,no flowers present,Caladenia excelsa Hopper & A.P.Br.,Plantae,species,VU,WA,Check against Threatened and Priority Fauna List WA available from https://www.dpaw.wa.gov.au/plants-and-animals/threatened-species-and-communities/threatened-animals. Last updated 13 June 2022,,WA-BIO,Category 1,Department of Biodiversity and Conservation,MR-R1,MR-S1,MR-R1-V1
+ABC123,WAM,Cowaramup Bay Road,-33.8,115.21,WGS84,30,Coordinates generalised,26/09/2019,,new sampling protocol,new basis of record,Stream Environment and Water Pty Ltd,PE:12:8833,new occurrence status,new habitat,new establishment means,Leaves brown,6,,,new life stage,new sex,new reproductiveCondition,MR-457,Stream Environment and Water Pty Ltd,32238,ARACH,WAM,BHP2012-7522 | M12379,BHP,new preparations,27/09/2019,https://www.ncbi.nlm.nih.gov/nuccore/MH040669.1 | https://www.ncbi.nlm.nih.gov/nuccore/MH040616.1,new sequencing method,Caladenia ?excelsa,2019-09-27T12:34+08:00,Stream Environment and Water Pty Ltd,new identification method,Caladenia excelsa,new identification qualifier,new remarks,Caladenia excelsa Hopper & A.P.Br.,new kingdom,new taxon rank,new threat status,WA,a random selection,,,Category 1,Department of Biodiversity and Conservation,MR-R1,MR-S1,
diff --git a/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.ttl b/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.ttl
new file mode 100644
index 00000000..7ff9007b
--- /dev/null
+++ b/abis_mapping/templates/survey_occurrence_data_v3/examples/margaret_river_flora/margaret_river_flora.ttl
@@ -0,0 +1,1991 @@
+@prefix abis: .
+@prefix dwc: .
+@prefix geo: .
+@prefix prov: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix schema: .
+@prefix skos: .
+@prefix sosa: .
+@prefix tern: .
+@prefix time: .
+@prefix xsd: .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Basis Of Record - HumanObservation" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ,
+ ;
+ schema:name "Occurrence Collection - Basis Of Record - PreservedSpecimen" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Basis Of Record - new basis of record" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ,
+ ;
+ schema:name "Occurrence Collection - Conservation Authority - WA" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Data Generalizations - Coordinates generalised" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ,
+ ;
+ schema:name "Occurrence Collection - Data Generalizations - Coordinates rounded to the nearest 10 km for conservation concern" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Habitat - Closed forest of Melaleuca lanceolata. White, grey or brown sand, sandy loam." ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Habitat - new habitat" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ,
+ ;
+ schema:name "Occurrence Collection - Identification Qualifier - ?" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Qualifier - new identification qualifier" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Qualifier - species incerta" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Remarks - Could not confirm the ID due to damaged flower" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Remarks - One unopened flower when recorded and one leaf only. ID not confirmed" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Remarks - new remarks" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Identification Remarks - no flowers present" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Preparations - Wet (in ethanol or some other preservative)" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Preparations - new preparations" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ,
+ ;
+ schema:name "Occurrence Collection - Sensitivity Category - Category 1" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Taxon Rank - new taxon rank" ;
+ tern:hasAttribute .
+
+ a schema:Collection ;
+ schema:isPartOf ;
+ schema:member ;
+ schema:name "Occurrence Collection - Taxon Rank - species" ;
+ tern:hasAttribute .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "1"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "10"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "11"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "12"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "13"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "14"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "2"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "3"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "4"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "5"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "6"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "7"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "8"^^ ;
+ schema:isPartOf .
+
+ a abis:BiodiversityRecord ;
+ schema:about ;
+ schema:identifier "9"^^ ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "acceptedNameUsage-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template dateIdentified used as proxy" ;
+ time:inXSDDateTimeStamp "2019-09-27T12:34:00+08:00"^^xsd:dateTimeStamp ] ;
+ sosa:hasFeatureOfInterest ;
+ sosa:hasResult ;
+ sosa:hasSimpleResult "Caladenia excelsa Hopper & A.P.Br." ;
+ sosa:observedProperty ;
+ sosa:usedProcedure ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "acceptedNameUsage-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template dateIdentified used as proxy" ;
+ time:inXSDDateTimeStamp "2019-09-27T12:34:00+08:00"^^xsd:dateTimeStamp ] ;
+ sosa:hasFeatureOfInterest ;
+ sosa:hasResult ;
+ sosa:hasSimpleResult "Caladenia excelsa Hopper & A.P.Br." ;
+ sosa:observedProperty ;
+ sosa:usedProcedure ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "Observation method unknown, 'human observation' used as proxy",
+ "establishmentMeans-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template eventDateStart used as proxy" ;
+ time:inXSDDate "2019-09-26"^^xsd:date ] ;
+ sosa:hasFeatureOfInterest ;
+ sosa:hasResult ;
+ sosa:hasSimpleResult "native" ;
+ sosa:observedProperty ;
+ sosa:usedProcedure ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "Observation method unknown, 'human observation' used as proxy",
+ "establishmentMeans-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template eventDateStart used as proxy" ;
+ time:inXSDDate "2019-09-26"^^xsd:date ] ;
+ sosa:hasFeatureOfInterest ;
+ sosa:hasResult ;
+ sosa:hasSimpleResult "new establishment means" ;
+ sosa:observedProperty ;
+ sosa:usedProcedure ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "Observation method unknown, 'human observation' used as proxy",
+ "individualCount-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template eventDateStart used as proxy" ;
+ time:inXSDDate "2019-09-26"^^xsd:date ] ;
+ sosa:hasFeatureOfInterest ;
+ sosa:hasResult ;
+ sosa:hasSimpleResult 2 ;
+ sosa:observedProperty ;
+ sosa:usedProcedure ;
+ schema:isPartOf .
+
+ a tern:Observation ;
+ rdfs:comment "Observation method unknown, 'human observation' used as proxy",
+ "individualCount-observation" ;
+ time:hasTime [ a time:Instant ;
+ rdfs:comment "Date unknown, template eventDateStart used as proxy" ;
+ time:inXSDDate "2019-09-26"^^xsd:date ] ;
+ sosa:hasFeatureOfInterest