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 .

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