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:
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())
A graphPath without a subject -> take graph snippet of first subject
node=Node(graphPath=graphPath)
print(node.graph.serialize())
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]" .