Node
The Node class in Geomapi is the abstract metadata class from which all other classes inherit. While this node should not be frequently used (unless to govern unknown geospatial data), and has limited capabilities, it governs the basic properties and RDF Graph interaction.
https://rdflib.readthedocs.io/
As such, the Node class incorporates all functionalities to read and write metadata to RDF Graphs, and format it approprietly to be used in geomatics analyses.
The code below shows how to create a abstract Node class works and how it interacts with RDF Graphs.
First the geomapi and external packages are imported
#IMPORT PACKAGES
from rdflib import Graph
import os
import numpy as np
#IMPORT MODULES
from context import geomapi #context import for documentation only
from geomapi.nodes import Node
Jupyter environment detected. Enabling Open3D WebVisualizer.
[Open3D INFO] WebRTC GUI backend enabled.
[Open3D INFO] WebRTCWindowSystem: HTTP handshake server disabled.
Node Creation
A Node can be created using any number of stated parameters. All these parameters correspond to a property which is input protected.
Node( subject = None, # (URIRef, optional) : A subject to use as identifier for the Node.
graph = None, # (Graph, optional) : An RDF Graph to parse.
graphPath = None, # (Path, optional) : The path of an RDF Graph to parse.
name = None, # (str, optional) : A name of the Node.
path = None, # (Path, optional) : A filepath to a resource.
timestamp = None, # (str, optional) : Timestamp for the node.
resource = None, # (optional) : Resource associated with the node.
cartesianTransform = None, # (np.ndarray, optional) : The (4x4) transformation matrix.
orientedBoundingBox = None, # (o3d.geometry.OrientedBoundingBox, optional) : The oriented bounding box of the node.
convexHull = None, # (o3d.geometry.TriangleMesh, optional) : The convex hull of the node.
loadResource = False, # Load the resource at initialization?
)
Create empty Node
Node classes can be initialized without any inputs.
In this case, most properties get a standard value based on the given parameters at initialization. These ensure every node has compatibility with our functions. This set of properties will always have a value, no matter what combination of parameters are given at initialization.
# Create an empty node
node=Node()
# Print the standard property values
print("subject:",node.subject)
print("name:",node.name)
print("timestamp:",node.timestamp)
print("cartesianTransform:",node.cartesianTransform)
print("convexHull:",node.convexHull)
print("orientedBoundingBox:",node.orientedBoundingBox)
subject: http://3cad1a0d-2f0c-11f0-8ee8-e8c8298c9966
name: 3cad1a0d-2f0c-11f0-8ee8-e8c8298c9966
timestamp: 2025-05-12T10:36:45
cartesianTransform: [[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
convexHull: TriangleMesh with 8 points and 12 triangles.
orientedBoundingBox: OrientedBoundingBox: center: (0, 0, 0), extent: 1, 1, 1)
Create with Subject
The subject serves as the key identifier for the Node with RDF Graphs and thus is restricted from using characters that can break its serialisation. In contrast, the name property is a string without any conditions.
node=Node(subject = '[this<has$to^change]')
print(node.subject)
print(node.name)
http://_this_has_to_change_
_this_has_to_change_
But, it is important to note that the name is always derived from the subject if no name is given and vise versa.
node=Node(name = '[this<has$to^change]')
print(node.name)
print(node.subject)
[this<has$to^change]
http://_this_has_to_change_
The type of these subjects is a URIRef which is compatible with any standardised Graph Navigation. Notice that both online (http:///) and local (file:///) subjects can be used with Geomapi (although the focus is more in offline processing).
Create with Resource
When creating a Node with a resource, it can be done either directly with the resource, or with the path to the resource.
A resource can be a big piece of data, this is why it is not always wanted to load the whole resource at initialization. This is why the loadResource
parameter is default to False
For more info on specific resources, see the corresponding Node type
node = Node(path=r"../../..\tests\testfiles\mesh\railway.obj", loadResource=False)
node.load_resource() # Use specialized node fo each type of resource.
Resource not loaded, but path is defined, call `load_resource()` to access it.
True
Create with graph(Path)
Every node class can be initialized from a graph or the path to the graph. A graph is defined as an RDF file containing a list of subjects, predicates and values.
# Example graph
graphPath = r"../../../tests\testfiles\graphs\baseNode.ttl"
graph=Graph().parse(graphPath)
print(graph.serialize())
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix geomapi: <https://w3id.org/geomapi#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://newNode1> a geomapi:Node ;
rdfs:label "newNode1"^^xsd:string ;
dcterms:created "2023-11-23T10:08:01"^^xsd:dateTime ;
geomapi:cartesianTransform """[[-1 0 0 1]
[0 -1 0 1]
[0 0 -1 1]
[0 0 0 1]]"""^^geomapi:matrix ;
geomapi:path "../mesh/parking.obj"^^xsd:string .
<http://newNode2> a geomapi:Node ;
rdfs:label "newNode2"^^xsd:string ;
dcterms:created "2023-11-23T10:08:02"^^xsd:dateTime ;
geomapi:cartesianTransform """[[1 0 0 0]
[0 1 0 0]
[0 0 1 0]
[0 0 0 1]]"""^^geomapi:matrix ;
geomapi:path "../mesh/parking.obj"^^xsd:string .
Ontology link
You may notice that the predicates in the graph do not all match the properties of the Node.
All metadata properties have a link to the the ontology. Other properties like resource
and graph
are not present in the graph, since thy do not represent metadata, rather actual binary data and are to large and inefficient to store in a graph.
Some properties have different names when they are defined in the graph:
python name |
predicate |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Graph(Path) with Subject
Since the graph contains multiple subjects, it is advised to always add the subject to the parameters.
node = Node(graph=graph, subject="newNode2")
print("subject:", node.subject)
node = Node(graphPath=graphPath, subject="newNode2")
print("subject:", node.subject)
Resource not loaded, but path is defined, call `load_resource()` to access it.
subject: http://newNode2
Resource not loaded, but path is defined, call `load_resource()` to access it.
subject: http://newNode2
Graph(Path) without subject
When no subject is given, but multiple subjects are present in the graph, it picks the first one after serialization.
node = Node(graph=graph)
print("subject:", node.subject)
node = Node(graphPath=graphPath)
print("subject:", node.subject)
Resource not loaded, but path is defined, call `load_resource()` to access it.
subject: http://newNode1
Resource not loaded, but path is defined, call `load_resource()` to access it.
subject: http://newNode2
Set the properties from the Graph
Upon initialisation from a graph or graphPath, the graph’s triples are assigned to the instance’s properties.
node=Node(graph=graph)
print("subject:",node.subject)
print("name:",node.name)
print("timestamp:",node.timestamp)
print("cartesianTransform:",node.cartesianTransform)
Resource not loaded, but path is defined, call `load_resource()` to access it.
subject: http://newNode1
name: newNode1
timestamp: 2023-11-23T10:08:01
cartesianTransform: [[-1. 0. 0. 1.]
[ 0. -1. 0. 1.]
[ 0. 0. -1. 1.]
[ 0. 0. 0. 1.]]
NOTE: Paths are stored relative to the graphPath so graph files and files can be moved without breaking the serialization. Moreover, when a graphPath is present, it is used to reconstruct the absolute paths wihtin the node.
node=Node(graph=graph)
print(node.path) # -> absolute path can not be reconstructed, Uses cwd for location
node=Node(graphPath=graphPath)
print(node.path)
Resource not loaded, but path is defined, call `load_resource()` to access it.
C:\Users\jelle\Documents\DoctoraatLocal\geomapi\docs\source\mesh\parking.obj
Resource not loaded, but path is defined, call `load_resource()` to access it.
C:\Users\jelle\Documents\DoctoraatLocal\geomapi\tests\testfiles\mesh\parking.obj
Node Serialization
When a Node is created in can be serialized back to an RDF graph.
Node to Graph
When generating the graph from a Node, all the initial properties are re-serialized. When you define new variables to the class, you need to add them to the serializeAttributes
parameter in the get_graph()
function.
NOTE: Actual data is not serialized incl. resources (point clouds, meshes, etc.), the graphPath, etc. These would not fit with semantic web technology concepts and can be hundreds of gigabytes in filesize.
Instead, resources should be stored separately in their respective file formats while the graphs govern their metadata.
node=Node('myNode',
myAttr=0.5,
myAttr2=5,
myAttr3=np.array([1,2,3]))
print(node.get_graph(serializeAttributes=["myAttr", "myAttr2"]).serialize()) # add the custom attributes to the list
The euler angles are derived from the rotation matrix, please note that this representation has a number of disadvantages
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix geomapi: <https://w3id.org/geomapi#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://myNode> a geomapi:Node ;
rdfs:label "myNode"^^xsd:string ;
dcterms:created "2025-05-12T15:36:42"^^xsd:dateTime ;
geomapi:cartesianTransform """[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]"""^^geomapi:matrix ;
geomapi:convexHull """[[-0.5 -0.5 -0.5]
[ 0.5 -0.5 -0.5]
[-0.5 -0.5 0.5]
[ 0.5 -0.5 0.5]
[-0.5 0.5 -0.5]
[ 0.5 0.5 -0.5]
[-0.5 0.5 0.5]
[ 0.5 0.5 0.5]]"""^^geomapi:matrix ;
geomapi:myAttr 5e-01 ;
geomapi:myAttr2 5 ;
geomapi:orientedBoundingBox "[0. 0. 0. 1. 1. 1. 0. 0. 0.]"^^geomapi:matrix .
XSD datatypes are used to serialize the data. str is used if no type is recognized.
Save to Graph
Storing one or more nodes in a graph on drive is an extension of the to_graph() function.
Just add a new graphPath or use the existing one, and set save==True
node=Node('myNode',
myAttr=0.5,
myAttr2=5,
myAttr3=np.array([1,2,3]))
newGraphPath = os.path.join(r"../../../tests/testfiles/resources",'myGraph.ttl')
node.get_graph(graphPath = newGraphPath, serializeAttributes=["myAttr", "myAttr2", "myAttr3"], save=True)
newNode=Node(graphPath=newGraphPath)
print(node.graph.serialize())
The euler angles are derived from the rotation matrix, please note that this representation has a number of disadvantages
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix geomapi: <https://w3id.org/geomapi#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://myNode> a geomapi:Node ;
rdfs:label "myNode"^^xsd:string ;
dcterms:created "2025-05-13T14:17:24"^^xsd:dateTime ;
geomapi:cartesianTransform """[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]"""^^geomapi:matrix ;
geomapi:convexHull """[[-0.5 -0.5 -0.5]
[ 0.5 -0.5 -0.5]
[-0.5 -0.5 0.5]
[ 0.5 -0.5 0.5]
[-0.5 0.5 -0.5]
[ 0.5 0.5 -0.5]
[-0.5 0.5 0.5]
[ 0.5 0.5 0.5]]"""^^geomapi:matrix ;
geomapi:myAttr 5e-01 ;
geomapi:myAttr2 5 ;
geomapi:myAttr3 "[1 2 3]" ;
geomapi:orientedBoundingBox "[0. 0. 0. 1. 1. 1. 0. 0. 0.]"^^geomapi:matrix .
Node Transformation
Since every nod has a cartesian transform, it can be transformed using the node.transform()
function.
The transformation also updates the convexHull
and orientedBoundingBox
node = Node()
print(node.cartesianTransform)
transformation = np.array([[0,0,1,0],[0,1,0,0],[1,0,0,0],[0,0,0,1]])
node.transform(transformation=transformation)
print("applying transformation: (-1)")
print(node.cartesianTransform,"\n")
node = Node()
rotation = np.array([90,0,0]) #eulers in degrees
node.transform(rotation=rotation)
print("applying rotation: (90,0,0)")
print(node.cartesianTransform,"\n")
node = Node()
translation = np.array([1,2,3])
node.transform(translation=translation)
print("applying translation: (1,2,3)")
print(node.cartesianTransform)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
applying transformation: (-1)
[[0. 0. 1. 0.]
[0. 1. 0. 0.]
[1. 0. 0. 0.]
[0. 0. 0. 1.]]
applying rotation: (90,0,0)
[[ 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
[ 0.000000e+00 6.123234e-17 -1.000000e+00 0.000000e+00]
[ 0.000000e+00 1.000000e+00 6.123234e-17 0.000000e+00]
[ 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00]]
applying translation: (1,2,3)
[[1. 0. 0. 1.]
[0. 1. 0. 2.]
[0. 0. 1. 3.]
[0. 0. 0. 1.]]
Node Visualisation
When a Node has a resource, the show()
function displays the resource using the relevant visualizer.
node.show() # The standard node has no resource to display
Further reading
Please refer to the full API documentation of the Node class for more details about the functionality