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

A Node Can be initialised using a number of different parameters

from geomapi.node import Node
newNode = Node()

Without a Graph

If no Graph or -path is provided, You can create an empty node of any type with a (random) subject ID and no metadata.

newNode = Node(subject = "myNode")

With Graph

You can create a Node using a Graph or -Path, this will parse all the variables inside the Graph. If a subject is provided, it will use that specific subject for the Graph.

newNode = Node(graphPath = "http://www.w3.org/People/Berners-Lee/card")

Node Types

Currently, the API supports 6 Specialised Nodes:

from geomapi.nodes import ImageNode()
from geomapi.nodes import GeometryNode()
from geomapi.nodes import PointcloudNode()
from geomapi.nodes import MeshNode()
from geomapi.nodes import BIMNode()
from geomapi.nodes import LinesetNode()
from geomapi.nodes import OrthoNode()
from geomapi.nodes import SessionNode()

Each Node Can be created just like a regular node, but it has extra inputs to assing variables. Check out the tutorial section for info about each specific node.

Working with Sessions

Since each node Only points to 1 resource, we use Sessions to group them. A Session is a collection of Resources that are taken in a single recording session. This means that all they are all in the same coordinate system and are documenting the same information. Check out the tutorial section about SessionNodes to get started.

#IMPORT PACKAGES
from rdflib import Graph, URIRef, Literal
import open3d as o3d
import os
from pathlib import Path
import numpy as np

#IMPORT MODULES
from context import geomapi 
from geomapi.nodes import *
import geomapi.utils as ut
from geomapi.utils import geometryutils as gmu
import geomapi.tools as tl
%load_ext autoreload
%autoreload 2

Create empty Node

Node classes can be initiliased without any inputs. In this case, a GUID subject and a name are asigned upon initialisation.

node=Node()
print(node.subject)
print(type(node.subject))
print(node.name)
file:///a20c8d41-1d57-11ed-8772-c8f75043ce59
<class 'rdflib.term.URIRef'>
a20c8d41-1d57-11ed-8772-c8f75043ce59

This 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('[this<has$to^change]')
print(node.subject)
file:///_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 Node with properties

Node has the following standard concepts, which are input protected:

> subject (URIRef)
> name (str)
> path (str, optional at this level)
> graph (Graph)
> graphPath (str)
> timestamp(str,(yyyy-MM-ddTHH:mm:ss))
> resource (geometry, abstract at this level)
> cartesianTransform (np.array, abstract on this level)

Each of these properties also has call() methods, which attempt to form values based on present instance variables in the Node.

# properties
node=Node(subject='myNode',
          name='myName',
          path='mypathWithValidExtension.txt',
          graph=Graph(),
          graphPath='somePathWithaGraph.ttl',
          cartesianTransform=np.diag(np.diag(np.ones((4,4)))),
          timestamp="Tue Dec  7 09:38:13 2021",
          newAttribute1=0.0,
          newAttribute2='attrib2',)
{key:value for key, value in node.__dict__.items() if not key.startswith('__') and not callable(key)}
{'_subject': rdflib.term.URIRef('file:///myNode'),
 '_graph': <Graph identifier=N00a16e4e3610488190308520f358dd29 (<class 'rdflib.graph.Graph'>)>,
 '_graphPath': 'somePathWithaGraph.ttl',
 '_path': 'mypathWithValidExtension.txt',
 '_name': 'mypathWithValidExtension',
 '_timestamp': '2021-12-07T09:38:13',
 '_resource': None,
 '_cartesianTransform': None,
 'newAttribute1': 0.0,
 'newAttribute2': 'attrib2'}

Note that timestamp is formatted as “%Y-%m-%dT%H:%M:%S” and that any kwarg can be added to the class. cartesianTransform methods differs between ImageNode and other nodes so the abstractr Node class doesn’t incorporate functionality for this class.

Every Node class also has call methods for the internal properties. If these instance variables are None, they are reconstructed from other information that is present in the Node.

#call methods
graphPath = os.path.join(Path(os.getcwd()).parents[2],'test','testfiles','meshGraph.ttl')

node=Node(graphPath=graphPath)
print(node.get_name())
print(node.get_cartesianTransform())
print(node.get_path())
print(node.get_graph())
print(node.get_timestamp())
print(node.get_resource())
Basic Wall_000_WA_DummyWall 20mm_1130411
None
d:\Scan-to-BIM repository\geomapi\test\testfiles\Basic Wall_000_WA_DummyWall 20mm_1130411.txt
[a rdfg:Graph;rdflib:storage [a rdflib:Store;rdfs:label 'Memory']].
2022-04-06T15:16:28
None

Node from graph

Every node class can be initiliased from a graph. There are three possibilities:

filePath = os.path.join(Path(os.getcwd()).parents[2],'test','testfiles','bimGraph1.ttl')
graph=Graph().parse(filePath)

#only print first node
newGraph=Graph()
newGraph+=graph.triples((URIRef('file:///Basic_Wall_211_WA_Ff1_Glued_brickwork_sandlime_150mm_1118860_0KysUSO6T3_gOJKtAiUE7d'),None,None))
print(newGraph.serialize())
@prefix ns1: <http://ifcowl.openbimstandards.org/IFC2X3_Final#> .
@prefix ns2: <https://w3id.org/v4d/core#> .
@prefix ns3: <http://libe57.org#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<file:///Basic_Wall_211_WA_Ff1_Glued_brickwork_sandlime_150mm_1118860_0KysUSO6T3_gOJKtAiUE7d> a ns2:BIMNode ;
    ns1:className "IfcWall" ;
    ns1:globalId "0KysUSO6T3_gOJKtAiUE7d" ;
    ns1:ifcPath "IFC\\Academiestraat_building_1.ifc" ;
    ns1:phase "BIM-UF" ;
    ns3:cartesianBounds """[ 31.3840053   37.25142541 100.31983802 100.57972895   7.49
  10.48      ]""" ;
    ns3:cartesianTransform """[[  1.           0.           0.          34.91152793]
 [  0.           1.           0.         100.43864519]
 [  0.           0.           1.           9.31833333]
 [  0.           0.           0.           1.        ]]""" ;
    ns3:pointCount 24 ;
    ns2:accuracy "0.05"^^xsd:float ;
    ns2:faceCount 44 ;
    ns2:lod 300 ;
    ns2:name "Basic Wall:211_WA_Ff1_Glued brickwork sandlime 150mm:1118860" ;
    ns2:orientedBounds """[[ 31.38400511 100.42974533  10.48      ]
 [ 37.24861442 100.31982342  10.48      ]
 [ 31.38400511 100.42974533   7.49      ]
 [ 31.38681629 100.57972895  10.48      ]
 [ 37.2514256  100.46980704   7.49      ]
 [ 31.38681629 100.57972895   7.49      ]
 [ 37.2514256  100.46980704  10.48      ]
 [ 37.24861442 100.31982342   7.49      ]]""" ;
    ns2:path "BIM\\Basic_Wall_211_WA_Ff1_Glued_brickwork_sandlime_150mm_1118860_0KysUSO6T3_gOJKtAiUE7d.ply" .

1. A graph with a subject -> only retain graph snippet of that subject

subject=next(s for s in graph.subjects())
node=Node(subject,graph=graph)
print(node.graph.serialize())
This is the base Node functionality, overwite for each childNode to retrieve the relevant cartesianTransform
@prefix e57: <http://libe57.org#> .
@prefix openlabel: <https://www.asam.net/index.php?eID=dumpFile&t=f&f=3876&token=413e8c85031ae64cc35cf42d0768627514868b2f#> .
@prefix v4d: <https://w3id.org/v4d/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<file:///Floor_232_FL_Wide_slab_50mm_1017640> a v4d:MeshNode ;
    e57:cartesianBounds "[31.04750061 46.16619873 87.48919678 93.76270294 13.72000027 13.77000046]" ;
    e57:cartesianTransform """[[ 1.          0.          0.         38.63791847]\r
 [ 0.          1.          0.         91.44094992]\r
 [ 0.          0.          1.         13.74500036]\r
 [ 0.          0.          0.          1.        ]]""" ;
    e57:pointCount 32 ;
    v4d:accuracy "0.05"^^xsd:float ;
    v4d:faceCount 64 ;
    v4d:name "Floor_232_FL_Wide slab 50mm_1017640" ;
    v4d:path "MESH\\Floor_232_FL_Wide slab 50mm_1017640.obj" ;
    openlabel:sensor "Hololens 2" ;
    openlabel:timestamp "2022-04-06 15:16:27" .

2. A graph without a subject -> take graph snippet of first subject

node=Node(graph=graph)
print(node.graph.serialize())
This is the base Node functionality, overwite for each childNode to retrieve the relevant cartesianTransform
@prefix e57: <http://libe57.org#> .
@prefix openlabel: <https://www.asam.net/index.php?eID=dumpFile&t=f&f=3876&token=413e8c85031ae64cc35cf42d0768627514868b2f#> .
@prefix v4d: <https://w3id.org/v4d/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<file:///Basic_Wall_000_WA_DummyWall_20mm_1130411> a v4d:MeshNode ;
    e57:cartesianBounds """[-14.01650047 -13.62110043  69.10179901  69.13130188  13.97000027\r
  17.20999908]""" ;
    e57:cartesianTransform """[[  1.           0.           0.         -13.81880021]\r
 [  0.           1.           0.          69.11655045]\r
 [  0.           0.           1.          15.58999968]\r
 [  0.           0.           0.           1.        ]]""" ;
    e57:pointCount 8 ;
    v4d:accuracy "0.05"^^xsd:float ;
    v4d:faceCount 12 ;
    v4d:name "Basic Wall_000_WA_DummyWall 20mm_1130411" ;
    v4d:path "MESH\\Basic Wall_000_WA_DummyWall 20mm_1130411.obj" ;
    openlabel:sensor "Hololens 2" ;
    openlabel:timestamp "2022-04-06 15:16:28" .

3. A graph with a subject that is not in the graph -> error

node=Node(subject='myNode',graph=graph)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16704/3265785765.py in <module>
----> 1 node=Node(subject='myNode',graph=graph)

d:\Scan-to-BIM repository\geomapi\geomapi\nodes\node.py in __init__(self, subject, graph, graphPath, name, path, timestamp, resource, cartesianTransform, **kwargs)
     89                 self.get_metadata_from_graph(self._graph,self._subject)
     90             else:
---> 91                 raise ValueError( 'Subject not in graph')
     92         self.__dict__.update(kwargs)
     93 

ValueError: Subject not in graph

Node from graphPath

Every node class can be initiliased from a graphPath. The same three possibilities apply here as well:

  1. A graphPath with a subject -> only retain graph snippet of that subject

subject=next(s for s in graph.subjects())
node=Node(subject,graphPath=graphPath)
print(node.graph.serialize())
  1. A graphPath without a subject -> take graph snippet of first subject

node=Node(graphPath=graphPath)
print(node.graph.serialize())
  1. A graphPath with a subject that is not in the graph -> error

node=Node(subject='myNode',graphPath=graphPath)

Get Metadata from graph

Upon initialisation from a graph or graphPath, the graph’s triples are assigned as instance variables.

node=Node(graph=graph)
{key:value for key, value in node.__dict__.items() if not key.startswith('__') and not callable(key)}
This is the base Node functionality, overwite for each childNode to retrieve the relevant cartesianTransform
{'_subject': rdflib.term.URIRef('file:///1faada72-1493-11ed-8ec2-c8f75043ce59'),
 '_graph': <Graph identifier=Na23302d6b7a04f44851bb45d5e2dee8d (<class 'rdflib.graph.Graph'>)>,
 '_graphPath': None,
 '_path': 'BIM\\1faada72-1493-11ed-8ec2-c8f75043ce59.ply',
 '_name': '1faada72-1493-11ed-8ec2-c8f75043ce59',
 '_timestamp': None,
 '_resource': None,
 '_cartesianTransform': None,
 'type': 'https://w3id.org/v4d/core#BIMNode',
 'className': 'IfcOpeningElement',
 'globalId': '1sAc4Xyq99bfet1lGbGxNb',
 'ifcPath': 'IFC\\Academiestraat_building_1.ifc',
 'phase': 'BIM-UF',
 'cartesianBounds': array([-10.82253789,  -9.66331336,  72.20498616,  72.63245695,
         16.99      ,  17.04      ]),
 'pointCount': 8,
 'accuracy': 0.05,
 'faceCount': 12,
 'lod': 300,
 'orientedBounds': array([[-10.8129766 ,  72.63260805,  17.04      ],
        [ -9.66325013,  72.60493317,  17.04      ],
        [-10.82260366,  72.23266104,  17.04      ],
        [-10.8129766 ,  72.63260805,  16.99      ],
        [ -9.67287719,  72.20498616,  16.99      ],
        [-10.82260366,  72.23266104,  16.99      ],
        [ -9.66325013,  72.60493317,  16.99      ],
        [ -9.67287719,  72.20498616,  17.04      ]])}

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 

node=Node(graphPath=graphPath)
print(node.path)
This is the base Node functionality, overwite for each childNode to retrieve the relevant cartesianTransform
BIM\1faada72-1493-11ed-8ec2-c8f75043ce59.ply
This is the base Node functionality, overwite for each childNode to retrieve the relevant cartesianTransform
d:\Scan-to-BIM repository\geomapi\test\testfiles\MESH\Basic Wall_000_WA_DummyWall 20mm_1130411.obj

Node to Graph

Similarly, all instance variables are transferred to triples.

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 seperately 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]))
node.to_graph()
print(node.graph.serialize())
@prefix v4d: <https://w3id.org/v4d/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<file:///myNode> a v4d:Node ;
    v4d:myAttr "0.5"^^xsd:float ;
    v4d:myAttr2 5 ;
    v4d:myAttr3 "[1 2 3]" .

XSD datatypes are used to serialize the data. str is used if no type is recognized.

Graph ontologies

Geomapi currently uses the following ontologies. For unrecognised properties, [v4d] is used as the default Namespace.

import rdflib
exif = rdflib.Namespace('http://www.w3.org/2003/12/exif/ns#') # -> image properties
geo=rdflib.Namespace('http://www.opengis.net/ont/geosparql#') # -> coordinate system information
gom=rdflib.Namespace('https://w3id.org/gom#') # -> geometry representations 
omg=rdflib.Namespace('https://w3id.org/omg#') # -> geometry relations
fog=rdflib.Namespace('https://w3id.org/fog#')
v4d=rdflib.Namespace('https://w3id.org/v4d/core#') # -> Our project specific concepts
openlabel=rdflib.Namespace('https://www.asam.net/index.php?eID=dumpFile&t=f&f=3876&token=413e8c85031ae64cc35cf42d0768627514868b2f#') # geometry and CV concepts
e57=rdflib.Namespace('http://libe57.org#') # -> point cloud concepts
xcr=rdflib.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') # -> image concepts from RealityCapture
ifc=rdflib.Namespace('http://ifcowl.openbimstandards.org/IFC2X3_Final#') # -> BIM concepts

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(os.getcwd(),'myGraph.ttl')
node.to_graph(newGraphPath)

newNode=Node(graphPath=newGraphPath)
print(node.graph.serialize())
@prefix v4d: <https://w3id.org/v4d/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<file:///myNode> a v4d:Node ;
    v4d:myAttr "0.5"^^xsd:float ;
    v4d:myAttr2 5 ;
    v4d:myAttr3 "[1 2 3]" .