@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix geo: <http://www.opengis.net/ont/geosparql#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix sdo: <https://schema.org/> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@base <http://www.opengis.net/def/geosparql/validator/> .


<http://www.opengis.net/def/geosparql/validator>
	a owl:Ontology ;
  	dcterms:title "GeoSPARQL RDF Shapes Validator"@en ;
	dcterms:description """This is a [Shapes Constraint Language (SHACL)](https://www.w3.org/TR/shacl/) file that specifies constraints for RDF data. It can be used to test the conformance of data to the GeoSPARQL standard.
	
As of GeoSPARQL 1.0, this validator is not normative, only informative, however this is likely to be the basis of future, normative, validators."""@en ;
	dcterms:publisher [
		a sdo:Organization ;
		sdo:name "Open Geospatial Consortium" ;
		sdo:url "https://www.ogc.org"^^xsd:anyURI ;
	] ;
	dcterms:creator "OGC GeoSPARQL Standards Working Group" ;
	dcterms:created "2021-06-13"^^xsd:date ;
	dcterms:modified "2021-08-12"^^xsd:date ;
    dcterms:license "https://www.ogc.org/license"^^xsd:anyURI ;
	dcterms:rights "(c) 2021 Open Geospatial Consortium" ; 	
	owl:versionInfo "OGC GeoSPARQL 1.0" ;
	owl:versionIRI <http://www.opengis.net/def/geosparql/validator/1.1> ;
.

#################################################
# Shape 1 (1a-1e)
#################################################

<S1-hasGeometry-hasSerialization>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:hasGeometry ;
	sh:property <S1-a-hasGeometry-hasSerialization-sub> , <S1-b-hasGeometry-hasSerialization-sub> , <S1-c-hasGeometry-hasSerialization-sub> , <S1-d-hasGeometry-hasSerialization-sub> , <S1-e-hasGeometry-hasSerialization-sub> ;
.
<S1-hasDefaultGeometry-hasSerialization>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:hasDefaultGeometry ;
	sh:property <S1-a-hasGeometry-hasSerialization-sub> , <S1-b-hasGeometry-hasSerialization-sub> , <S1-c-hasGeometry-hasSerialization-sub> , <S1-d-hasGeometry-hasSerialization-sub> , <S1-e-hasGeometry-hasSerialization-sub> ;
.
<S1-cefaultGeometry-hasSerialization>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:defaultGeometry ;
	sh:property <S1-a-hasGeometry-hasSerialization-sub> , <S1-b-hasGeometry-hasSerialization-sub> , <S1-c-hasGeometry-hasSerialization-sub> , <S1-d-hasGeometry-hasSerialization-sub> , <S1-e-hasGeometry-hasSerialization-sub> ;
.

<S1-a-hasGeometry-hasSerialization-sub>
	a sh:PropertyShape ;
	sh:path [ 
		sh:alternativePath (
			geo:hasSerialization
			geo:asGML
			geo:asWKT
			geo:asGeoJSON
			geo:asKML
			geo:asDGGS
		) 
	] ;
	sh:minCount 1 ;
	sh:message "Each node with an incoming geo:hasGeometry, or a specialization of it, should have minimum one outgoing relation that is either geo:hasSerialization, or a specialization of it."@en ;
	skos:example 
		"""
		# A valid example: incoming into the Geometry Blank Node is geo:hasGeometry, outgoing is geo:asWKT
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: incoming into the Geometry Blank Node is geo:hasGeometry, there is no outgoing geo:hasSerialization or a specialization of it
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [] ;
		.
		""" ,
		"""
		# A valid example: incoming into the Geometry Blank Node is geo:hasDefaultGeometry, outgoing is geo:asWKT
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasDefaultGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: incoming into the Geometry Blank Node is geo:hasDefaultGeometry, there is no outgoing geo:hasSerialization or a specialization of it
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasDefaultGeometry [] ;
		.
		""" ;
.

<S1-b-hasGeometry-hasSerialization-sub>
	a sh:PropertyShape ;
	sh:path geo:asWKT ;
	sh:maxCount 1 ;
	sh:message "Each node with an incoming geo:hasGeometry, or a specialization of it, can have a maximum of one outgoing geo:asWKT relation."@en ;
	skos:example 
		"""
		# A valid example: incoming into the Geometry Blank Node is geo:hasGeometry, outgoing is one geo:asWKT relation
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# A valid example: incoming into the Geometry Blank Node is geo:hasGeometry, outgoing is one geo:asWKT relation. RDF treats both entries as one triple since they are identical.
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: incoming into the Geometry Blank Node is geo:hasGeometry, outgoing are two geo:asWKT relations
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
				geo:asWKT "POINT (153.08 -27.32)"^^geo:wktLiteral ;
			] ;
		.
		""" ;
.

<S1-c-hasGeometry-hasSerialization-sub>
	a sh:PropertyShape ;
	sh:path geo:asGML ;
	sh:maxCount 1 ;
	sh:message "Each node with an incoming geo:hasGeometry, or a specialization of it, can have a maximum of one outgoing geo:asGML relation."@en ;
.


#################################################
# Shape 3 + 3b + 3c
#################################################

<S3-hasGeometry-hasGeometry>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:hasGeometry ;
	sh:property <S3-hasGeometry-hasGeometry-sub> ;
.
<S3-b-hasDefaultGeometry-hasGeometry>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:hasDefaultGeometry ;
	sh:property <S3-hasGeometry-hasGeometry-sub> ;
.
<S3-c-defaultGeometry-hasGeometry>
    a sh:NodeShape ;
	sh:targetObjectsOf geo:defaultGeometry ;
	sh:property <S3-hasGeometry-hasGeometry-sub> ;
.

<S3-hasGeometry-hasGeometry-sub>
	a sh:PropertyShape ;
	sh:path [ 
		sh:alternativePath (
			geo:hasGeometry
			geo:hasDefaultGeometry
			geo:defaultGeometry
		)
	] ;
	sh:maxCount 0 ;
	sh:message "A node that has an incoming geo:hasGeometry property, or specialization of it, cannot have an outgoing geo:hasGeometry property, or a specialization of, it at the same time (a geo:Feature cannot be a geo:Geometry at the same time)"@en ;
	skos:example 
		"""
		# A valid example: the Resource instance <resource-x-geom1> has an incoming geo:hasGeometry property and no outgoing geo:hasGeometry or specialization of it
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<resource-x> geo:hasGeometry <resource-x-geom1> .
		<resource-x-geom1> geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral .
		""" ,
		"""
		# An invalid example: the Resource instance <resource-x> has both an outgoing and an incoming geo:hasGeometry property (i.e. it's a geo:Feature and a geo:Geometry at the same time)
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<resource-y>
			geo:hasGeometry <resource-x> ;
		.
		<resource-x>
			geo:asWKT "POINT (153.084232 -27.322734)"^^geo:wktLiteral ;
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.		
		""" ,
		"""
		# A valid example: the Resource instance <resource-x-geom1> has an incoming geo:hasDefaultGeometry property and no outgoing geo:hasGeometry or specialization of it
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<resource-x> geo:hasDefaultGeometry <resource-x-geom1> .
		<resource-x-geom1> geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral .
		""" ,
		"""
		# An invalid example: the Resource instance <resource-x> has an incoming geo:hasDefaultGeometry property and an outgoing geo:hasGeometry (i.e. it's a geo:Feature and a geo:Geometry at the same time)
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<resource-y>
			geo:hasDefaultGeometry <resource-x> ;
		.
		<resource-x>
			geo:asWKT "POINT (153.084232 -27.322734)"^^geo:wktLiteral ;
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.		
		""" ;
.

#################################################
# Shape 4
#################################################

<S4-hasSerialization-literal>
	a sh:NodeShape ;
	sh:targetObjectsOf
		geo:hasSerialization ,
		geo:asGML ,
		geo:asWKT ,
		geo:asGeoJSON ,
		geo:asKML ,
		geo:asDGGS ;
	sh:nodeKind sh:Literal ;
	sh:message "The target of a geo:hasSerialization property, or a specialization of, it should be an RDF literal."@en ;
	skos:example 
		"""
		# A valid example: the target of a geo:asWKT property is an RDF literal
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: the target of a geo:asWKT property is not an RDF literal
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-y>
			geo:hasGeometry [
				geo:asWKT <geometry-serialization-node> ;
			] ;
		.		
		""" ;
.

#################################################
# Shape 5-6-7-8
#################################################

<S5-asWKT-wktLiteral>
	a sh:NodeShape ;
	sh:targetObjectsOf geo:asWKT ;
	sh:datatype geo:wktLiteral ;
	sh:message "The target of a geo:asWKT property should be an RDF literal with datatype geo:wktLiteral."@en ;
	skos:example 
		"""
		# A valid example: the target of a geo:asWKT property is an RDF literal with datatype geo:wktLiteral
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: the target of a geo:asWKT property is not an RDF literal
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-y>
			geo:hasGeometry [
				geo:asWKT <geometry-serialization-node> ;
			] ;
		.		
		""" ,
		"""
		# An invalid example: the target of a geo:asWKT property is an RDF literal but with the wrong datatype (xsd:string)
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-z>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)" ;
			] ;
		.		
		""" ;
.

<S6-asGML-gmlLiteral>
	a sh:NodeShape ;
	sh:targetObjectsOf geo:asGML ;
	sh:datatype geo:gmlLiteral ;
	sh:message "The target of a geo:asGML property should be an RDF literal with datatype geo:gmlLiteral."@en ;
.

#################################################
# Shape 9-10-11-12-13-14 (14a-14d)
#################################################
# each metadata property on geo:Geometry can exist only once

<S9-many-coordinateDimension-one>
	a sh:NodeShape ;
	sh:property <S9-many-coordinateDimension-one-sub> ;
	sh:targetSubjectsOf geo:coordinateDimension ;
.
	
<S9-many-coordinateDimension-one-sub>
	a sh:PropertyShape ;
	sh:path geo:coordinateDimension ;
	sh:maxCount 1 ;
	sh:message "A geo:Geometry node should have maximum of one outgoing geo:coordinateDimension property."@en ;
	skos:example
		"""
		# A valid example: the subject of a geo:coordinateDimension property has no other properties with the same name
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .
		
		<feature-x>
			geo:hasGeometry [
				geo:coordinateDimension 2 ;
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:dggsLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: the subject of a geo:coordinateDimension property has also another property with with the same property name but a different value
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .
		
		<feature-y>
			geo:hasGeometry [
				geo:coordinateDimension 2 , 3 ;
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:dggsLiteral ;
			] ;
		.
		""" ;
.	
	
<S10-many-dimension-one>
	a sh:NodeShape ;
	sh:property <S10-many-dimension-one-sub> ;
	sh:targetSubjectsOf geo:dimension ;
.
	
<S10-many-dimension-one-sub>
	a sh:PropertyShape ;
	sh:path geo:dimension ;
	sh:maxCount 1 ;
	sh:message "A geo:Geometry node should have maximum of one outgoing geo:dimension property."@en ;
.	

<S11-many-isEmpty-one>
	a sh:NodeShape ;
	sh:property <S11-many-isEmpty-one-sub> ;
	sh:targetSubjectsOf geo:isEmpty ;
.
	
<S11-many-isEmpty-one-sub>
	a sh:PropertyShape ;
	sh:path geo:isEmpty ;
	sh:maxCount 1 ;
	sh:message "A geo:Geometry node should have maximum of one outgoing geo:isEmpty property."@en ;
.	

<S12-many-isSimple-one>
	a sh:NodeShape ;
	sh:property <S12-many-isSimple-one-sub> ;
	sh:targetSubjectsOf geo:isSimple ;
.
	
<S12-many-isSimple-one-sub>
	a sh:PropertyShape ;
	sh:path geo:isSimple ;
	sh:maxCount 1 ;
	sh:message "A geo:Geometry node should have a maximum one outgoing geo:isSimple property."@en ;
.	

<S13-many-spatialDimension-one>
	a sh:NodeShape ;
	sh:property <S13-many-spatialDimension-one-sub> ;
	sh:targetSubjectsOf geo:spatialDimension ;
.
	
<S13-many-spatialDimension-one-sub>
	a sh:PropertyShape ;
	sh:path geo:spatialDimension ;
	sh:maxCount 1 ;
	sh:message "A geo:Geometry node should have maximum of one outgoing geo:spatialDimension property."@en ;
.

#################################################
# Shape 15-16-17-18-19
#################################################
# regex to check if content of WKT/GeoJSON/KML/GML literals complies with the basic patterns of such geometries, if possible (check start and/or end to find e.g. copy/paste errors with GeoSPARQL properties and datatypes, not to check if the content of a literal is fully compliant to the geometry format specification)

<S15-wkt-content>
	a sh:NodeShape ;
	sh:property <S15-wkt-content-sub-start> ; # the example in the SHACL spec (https://www.w3.org/TR/shacl/#constraints) refers to two property shapes: one for the start and one for the end
	sh:targetSubjectsOf geo:asWKT ;
.

<S15-wkt-content-sub-start>
	a sh:PropertyShape ;
	sh:path geo:asWKT ;
	sh:pattern "^\\s*$|^\\s*(P|C|S|L|T|<)" ;  # starts with P, C, S, L, T, or < for Point, TIN etc. or IRI
	sh:message "The content of an RDF literal with an incoming geo:asWKT relation must conform to a well-formed WKT string, as defined by its official specification (Simple Features Access)."@en ;
	skos:example 
		"""
		# A valid example: the RDF literal with an incoming geo:asWKT has a well-formed WKT string as its content
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "POINT (153.084230 -27.322738)"^^geo:wktLiteral ;
			] ;
		.
		""" ,
		"""
		# An invalid example: the RDF literal with an incoming geo:asWKT does not have a well-formed WKT string as its content, but rather a GeoJSON string
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x>
			geo:hasGeometry [
				geo:asWKT "{ \"type\": \"Point\", \"coordinates\": [149.06017784, -35.23612321] }"^^geo:wktLiteral ;
			] ;
		.
		""" ;
.

<S16-gml-content>
	a sh:NodeShape ;
	sh:property <S16-gml-content-sub-start> ; # the example in the SHACL spec (https://www.w3.org/TR/shacl/#constraints) refers to two property shapes: one for the start and one for the end
	sh:targetSubjectsOf geo:asGML ;
.

<S16-gml-content-sub-start>
	a sh:PropertyShape ;
	sh:path geo:asGML ;
	sh:pattern "^\\s*$|^\\s*(<)(.+)(>)\\s*$" ;  # starts with < ends with >
	sh:message "The content of an RDF literal with an incoming geo:asGML relation must conform to a well-formed GML geometry XML string, as defined by its official specification."@en ;
.

#################################################
# Shape 20
#################################################

<S20-dimension-coordinateDimension>
	a sh:NodeShape ;
	sh:property <S20-dimension-coordinateDimension-sub> ;
	sh:targetSubjectsOf geo:dimension ;
.

<S20-dimension-coordinateDimension-sub>
	a sh:PropertyShape ;
	sh:path geo:coordinateDimension ;
	sh:sparql [
		sh:message "If both geo:dimension and geo:coordinateDimension properties are asserted, the value of geo:dimension should be less than or equal to the value of geo:coordinateDimension"@en ;
		sh:select """
			PREFIX geo: <http://www.opengis.net/ont/geosparql#>
			SELECT $this
			WHERE {
				$this geo:dimension ?dim ;
					geo:coordinateDimension ?coordDim .
				FILTER ( ?dim > ?coordDim )
			}
		""" ;
	] ;
	skos:example 
		"""
		# A valid example: the geo:dimension has value is 2 while the geo:coordinateDimension has value 2
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x> geo:hasGeometry <feature-x-geom1> .
		<feature-x-geom1> geo:dimension 2 ;
			geo:coordinateDimension 2 ;
		.
		""" ,
		"""
		# A valid example: the geo:dimension has value is 2 while there is no geo:coordinateDimension
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x> geo:hasGeometry <feature-x-geom1> .
		<feature-x-geom1> geo:dimension 2 ;
		.
		""" ,
		"""
		# An invalid example: the geo:dimension has value is 3 while the geo:coordinateDimension has value 2
		@prefix geo: <http://www.opengis.net/ont/geosparql#> .

		<feature-x> geo:hasGeometry <feature-x-geom1> .
		<feature-x-geom1> geo:dimension 3 ;
			geo:coordinateDimension 2 ;
		.
		""" ;
.
