2024-09-27 16:32:41 +02:00
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
# include "TtlWktSerializer.h"
2024-10-08 20:57:27 +02:00
# ifdef IFOPSH_WITH_OPENCASCADE
# include "../ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h"
# include <TopTools_HSequenceOfShape.hxx>
# include <BRepBuilderAPI_Transform.hxx>
# include <BRepBndLib.hxx>
# include <BRepAlgoAPI_Section.hxx>
# include <ShapeAnalysis_FreeBounds.hxx>
# include <BRepTools_WireExplorer.hxx>
# include <TopoDS.hxx>
2024-10-08 21:04:11 +02:00
# include <Bnd_Box.hxx>
# include <gp_Pln.hxx>
2025-11-07 10:59:33 +01:00
# include <TopoDS_Wire.hxx>
# include <BRepBuilderAPI_MakeFace.hxx>
# include <GProp_GProps.hxx>
# include <BRepGProp.hxx>
2024-10-08 20:57:27 +02:00
# endif
2024-09-27 16:32:41 +02:00
# include <iomanip>
# include <iostream>
# include <vector>
# include <unordered_set>
# include <unordered_map>
# include <queue>
namespace {
const char * const LINESTRING = " LINESTRING " ;
const char * const POLYGON = " POLYGON " ;
void emit_polyhedral_surface (
std : : ostream & os ,
const std : : vector < double > & vertices ,
const std : : vector < std : : vector < std : : vector < int > > > & faces )
{
os < < " POLYHEDRALSURFACE Z( " ;
for ( size_t i = 0 ; i < faces . size ( ) ; + + i ) {
const auto & face = faces [ i ] ;
os < < " ( " ;
for ( size_t j = 0 ; j < face . size ( ) ; + + j ) {
const auto & loop = face [ j ] ;
os < < " ( " ;
for ( size_t k = 0 ; k < loop . size ( ) ; + + k ) {
int index = loop [ k ] ;
for ( size_t l = 0 ; l < 3 ; + + l ) {
os < < vertices [ index * 3 + l ] ;
if ( l ! = 2 ) {
os < < " " ;
}
}
if ( k < loop . size ( ) - 1 ) {
os < < " , " ;
}
}
os < < " ) " ;
if ( j < face . size ( ) - 1 ) {
os < < " , " ;
}
}
os < < " ) " ;
if ( i < faces . size ( ) - 1 ) {
os < < " , " ;
}
}
os < < " ) " ;
}
void emit_line_component (
std : : ostream & os ,
const std : : vector < double > & vertices ,
const std : : vector < int > & component ,
bool force_2d = false ,
const char * const wkt_geometry_type = LINESTRING )
{
os < < wkt_geometry_type < < " " ;
if ( ! force_2d ) {
os < < " Z " ;
}
os < < " ( " ;
if ( wkt_geometry_type = = POLYGON ) {
os < < " ( " ;
}
for ( size_t i = 0 ; i < component . size ( ) + ( wkt_geometry_type = = POLYGON ? 1 : 0 ) ; + + i ) {
if ( i ! = 0 ) {
os < < " , " ;
}
for ( size_t l = 0 ; l < ( force_2d ? 2 : 3 ) ; + + l ) {
os < < vertices [ component [ i % component . size ( ) ] * 3 + l ] ;
if ( l ! = ( force_2d ? 1 : 2 ) ) {
os < < " " ;
}
}
}
os < < " ) " ;
if ( wkt_geometry_type = = POLYGON ) {
os < < " ) " ;
}
}
void emit_line_strings (
std : : ostream & os ,
const std : : vector < double > & vertices ,
const std : : vector < int > & lines ,
bool force_2d = false )
{
std : : unordered_map < int , std : : vector < int > > adjacencyList ;
std : : unordered_set < int > visited ;
for ( size_t i = 0 ; i < lines . size ( ) ; i + = 2 ) {
for ( size_t j = 0 ; j < 2 ; + + j ) {
const auto & p0 = lines [ i + ( j ? 1 : 0 ) ] ;
const auto & p1 = lines [ i + ( j ? 0 : 1 ) ] ;
adjacencyList [ p0 ] . push_back ( p1 ) ;
}
}
auto traverseComponent = [ & ] ( int start ) {
std : : vector < int > component ; // To store the current component
std : : queue < int > toVisit ;
toVisit . push ( start ) ;
visited . insert ( start ) ;
while ( ! toVisit . empty ( ) ) {
int current = toVisit . front ( ) ;
toVisit . pop ( ) ;
component . push_back ( current ) ;
for ( int neighbor : adjacencyList [ current ] ) {
if ( visited . find ( neighbor ) = = visited . end ( ) ) {
toVisit . push ( neighbor ) ;
visited . insert ( neighbor ) ;
}
}
}
return component ;
} ;
std : : vector < std : : vector < int > > components ;
for ( auto it = lines . begin ( ) ; it ! = lines . end ( ) ; it + = 2 ) {
if ( visited . find ( * it ) = = visited . end ( ) ) {
components . emplace_back ( std : : move ( traverseComponent ( * it ) ) ) ;
}
}
if ( components . size ( ) = = 1 ) {
emit_line_component ( os , vertices , components . front ( ) , force_2d ) ;
} else {
os < < " GEOMETRYCOLLECTION( " ;
for ( auto it = components . begin ( ) ; it ! = components . end ( ) ; + + it ) {
if ( it ! = components . begin ( ) ) {
os < < " , " ;
}
emit_line_component ( os , vertices , * it , force_2d ) ;
}
os < < " ) " ;
}
}
std : : string escape_for_turtle ( const std : : u32string & input ) {
2025-06-11 10:39:02 +02:00
std : : ostringstream escaped ;
2024-09-27 16:32:41 +02:00
escaped < < " \" " ;
for ( auto & c : input ) {
switch ( c ) {
case ' \\ ' :
escaped < < " \\ \\ " ;
break ;
case ' \" ' :
escaped < < " \\ \" " ;
break ;
case ' \n ' :
escaped < < " \\ n " ;
break ;
case ' \r ' :
escaped < < " \\ r " ;
break ;
case ' \t ' :
escaped < < " \\ t " ;
break ;
default :
if ( c < 0x20 | | c > 0x7E ) {
escaped < < " \\ u "
2025-06-11 10:39:02 +02:00
< < std : : hex < < std : : setw ( 4 ) < < std : : setfill ( ' 0 ' )
2024-09-27 16:32:41 +02:00
< < ( c & 0xFFFF ) ;
} else {
escaped . put ( c ) ;
}
break ;
}
}
escaped < < " \" " ;
2025-06-11 10:39:02 +02:00
return escaped . str ( ) ;
2024-09-27 16:32:41 +02:00
}
template < typename Fn , typename . . . Ts >
std : : string capture_output ( Fn fn , Ts . . . ts ) {
std : : ostringstream oss ;
2024-12-01 19:05:22 +01:00
oss < < std : : setprecision ( std : : numeric_limits < double > : : digits10 + 1 ) ;
2024-09-27 16:32:41 +02:00
fn ( oss , ts . . . ) ;
return oss . str ( ) ;
}
}
2026-06-10 18:30:54 +02:00
TtlWktSerializer : : TtlWktSerializer ( const stream_or_filename & filename , const ifcopenshell : : geometry : : Settings & geometry_settings , const ifcopenshell : : geometry : : SerializerSettings & settings , Logger & logger )
: WriteOnlyGeometrySerializer ( geometry_settings , settings , logger )
2024-09-27 16:32:41 +02:00
, filename_ ( filename )
{
const auto & tri_setting = geometry_settings . get < ifcopenshell : : geometry : : settings : : TriangulationType > ( ) . get ( ) ;
2024-10-08 20:57:27 +02:00
if ( settings_ . get < ifcopenshell : : geometry : : settings : : WktUseSection > ( ) . get ( ) ) {
const auto & it_output = geometry_settings . get < ifcopenshell : : geometry : : settings : : IteratorOutput > ( ) . get ( ) ;
if ( it_output ! = ifcopenshell : : geometry : : settings : : NATIVE ) {
throw std : : runtime_error ( " The RDF Turtle WKT serializer needs native geometry when section mode is enabled " ) ;
}
} else {
if ( tri_setting ! = ifcopenshell : : geometry : : settings : : POLYHEDRON_WITH_HOLES ) {
throw std : : runtime_error ( " The RDF Turtle WKT serializer needs POLYHEDRON_WITH_HOLES triangulation output " ) ;
}
2024-09-27 16:32:41 +02:00
}
filename_ . stream < < std : : setprecision ( settings . get < ifcopenshell : : geometry : : settings : : FloatingPointDigits > ( ) . get ( ) ) ;
}
bool TtlWktSerializer : : ready ( )
{
return filename_ . is_ready ( ) ;
}
void TtlWktSerializer : : writeHeader ( )
{
2024-09-28 14:46:57 +02:00
using namespace ifcopenshell : : geometry : : settings ;
2024-09-27 16:32:41 +02:00
filename_ . stream < < " # File generated by IfcOpenShell " < < IFCOPENSHELL_VERSION < < " \n " ;
filename_ . stream < < " @prefix geo: <http://www.opengis.net/ont/geosparql#> . \n " ;
2024-09-28 14:46:57 +02:00
if ( settings_ . get < BaseUri > ( ) . has ( ) ) {
filename_ . stream < < " @prefix base: < " < < settings_ . get < BaseUri > ( ) . get ( ) < < " > . \n " ;
} else {
filename_ . stream < < " @prefix base: <http://example.org/> . \n " ;
}
2024-09-27 16:32:41 +02:00
filename_ . stream < < " @prefix dcterms: <http://purl.org/dc/terms/> . \n " ;
filename_ . stream < < " @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . \n \n \n " ;
}
void TtlWktSerializer : : write ( const IfcGeom : : TriangulationElement * o )
{
2024-10-08 20:57:27 +02:00
filename_ . stream < < ttl_object_id ( o ) < < " a geo:Feature ; \n " ;
2024-09-27 16:32:41 +02:00
filename_ . stream < < " dcterms:identifier " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 ( o - > guid ( ) ) ) < < " ; \n " ;
2024-09-27 16:32:41 +02:00
filename_ . stream < < " rdfs:label " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 ( o - > name ( ) )
2024-09-27 16:32:41 +02:00
) < < " ; \n " ;
2024-10-08 20:57:27 +02:00
filename_ . stream < < " geo:hasGeometry " < < ttl_object_id ( o , " _geometry " ) < < " . \n \n " ;
2024-09-27 16:32:41 +02:00
if ( ! o - > geometry ( ) . polyhedral_faces_with_holes ( ) . empty ( ) ) {
2024-10-08 20:57:27 +02:00
filename_ . stream < < ttl_object_id ( o , " _geometry " ) < < " a geo:Geometry ; \n " ;
2024-09-27 16:32:41 +02:00
filename_ . stream < < " geo:asWKT " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 (
2024-09-27 16:32:41 +02:00
capture_output (
emit_polyhedral_surface ,
o - > geometry ( ) . verts ( ) ,
o - > geometry ( ) . polyhedral_faces_with_holes ( ) ) )
) < < " ^^geo:wktLiteral . \n \n " ;
Eigen : : Map < const Eigen : : Matrix < double , 3 , Eigen : : Dynamic > > vertex_map ( o - > geometry ( ) . verts ( ) . data ( ) , 3 , o - > geometry ( ) . verts ( ) . size ( ) / 3 ) ;
boost : : optional < std : : vector < std : : vector < int > > : : const_iterator > lowest_face ;
double lowest_z = std : : numeric_limits < double > : : infinity ( ) ;
for ( const auto & f : o - > geometry ( ) . polyhedral_faces_with_holes ( ) ) {
2024-10-02 11:12:09 +02:00
Eigen : : Vector3d v0 , v1 , v2 , v1_v0 , v2_v0 ;
for ( size_t i = 0 ; i < f [ 0 ] . size ( ) ; + + i ) {
v0 = vertex_map . transpose ( ) . row ( f [ 0 ] [ 0 + i ] ) ;
v1 = vertex_map . transpose ( ) . row ( f [ 0 ] [ 1 + i ] ) ;
v2 = vertex_map . transpose ( ) . row ( f [ 0 ] [ 2 + i ] ) ;
v1_v0 = v1 - v0 ;
v2_v0 = v2 - v0 ;
v1_v0 . normalize ( ) ;
v2_v0 . normalize ( ) ;
if ( ( std : : abs ( v1_v0 . dot ( v2_v0 ) ) + 1.e-9 ) > = 1.0 ) {
// Don't derive normal from collinear edges
continue ;
}
break ;
}
2024-09-27 16:32:41 +02:00
Eigen : : Vector3d cross_product = v1_v0 . cross ( v2_v0 ) ;
cross_product . normalize ( ) ;
2024-10-02 11:12:09 +02:00
2024-09-27 16:32:41 +02:00
// @nb we take abs because so that we can ignore face orientation and potential convatities rquire
if ( ( std : : abs ( cross_product . z ( ) ) + 1.e-9 ) > = 1.0 & & v0 . z ( ) < lowest_z ) {
lowest_face = f . begin ( ) ;
2024-10-07 20:45:52 +02:00
lowest_z = v0 . z ( ) ;
2024-09-27 16:32:41 +02:00
}
}
if ( lowest_face ) {
2024-10-08 20:57:27 +02:00
filename_ . stream < < ttl_object_id ( o ) < < " geo:hasGeometry " < < ttl_object_id ( o , " _footprint_geometry " ) < < " . \n \n " ;
filename_ . stream < < ttl_object_id ( o , " _footprint_geometry " ) < < " a geo:Geometry ; \n " ;
2024-09-27 16:32:41 +02:00
filename_ . stream < < " geo:asWKT " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 (
2024-09-27 16:32:41 +02:00
capture_output (
// @nb this is line_component, because this is the linestring
// from a faceboundary, not the edges as pairs of indices.
emit_line_component ,
o - > geometry ( ) . verts ( ) ,
* * lowest_face ,
true ,
POLYGON ) )
) < < " ^^geo:wktLiteral . \n \n " ;
}
} else {
2024-10-08 20:57:27 +02:00
filename_ . stream < < ttl_object_id ( o , " _geometry " ) < < " a geo:Geometry ; \n " ;
2024-09-27 16:32:41 +02:00
bool force_2d = true ;
double z_value ;
for ( size_t i = 2 ; i < o - > geometry ( ) . verts ( ) . size ( ) ; i + = 3 ) {
const auto & cur = o - > geometry ( ) . verts ( ) [ i ] ;
if ( i = = 2 ) {
z_value = cur ;
} else {
if ( z_value ! = cur ) {
force_2d = false ;
break ;
}
}
}
filename_ . stream < < " geo:asWKT " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 (
2024-09-27 16:32:41 +02:00
capture_output (
emit_line_strings ,
o - > geometry ( ) . verts ( ) ,
o - > geometry ( ) . edges ( ) ,
force_2d ) )
) < < " ^^geo:wktLiteral . \n \n " ;
}
}
2024-10-08 20:57:27 +02:00
void TtlWktSerializer : : write ( const IfcGeom : : BRepElement * brep_obj ) {
# ifdef IFOPSH_WITH_OPENCASCADE
filename_ . stream < < ttl_object_id ( brep_obj ) < < " a geo:Feature ; \n " ;
filename_ . stream < < " dcterms:identifier " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 ( brep_obj - > guid ( ) ) ) < < " ; \n " ;
2024-10-08 20:57:27 +02:00
filename_ . stream < < " rdfs:label " < < escape_for_turtle (
2025-06-11 10:39:02 +02:00
IfcUtil : : convert_utf8 ( brep_obj - > name ( ) )
2024-10-08 20:57:27 +02:00
) < < " . \n " ;
// @todo unify logic with SVG serializer
auto itm = brep_obj - > geometry ( ) . as_compound ( ) ;
TopoDS_Shape compound_local = ( ( ifcopenshell : : geometry : : OpenCascadeShape * ) itm ) - > shape ( ) ;
delete itm ;
gp_Trsf trsf ;
const auto & m = brep_obj - > transformation ( ) . data ( ) - > ccomponents ( ) ;
trsf . SetValues (
m ( 0 , 0 ) , m ( 0 , 1 ) , m ( 0 , 2 ) , m ( 0 , 3 ) ,
m ( 1 , 0 ) , m ( 1 , 1 ) , m ( 1 , 2 ) , m ( 1 , 3 ) ,
m ( 2 , 0 ) , m ( 2 , 1 ) , m ( 2 , 2 ) , m ( 2 , 3 )
) ;
BRepBuilderAPI_Transform make_transform_global ( compound_local , trsf , true ) ;
make_transform_global . Build ( ) ;
auto compound = make_transform_global . Shape ( ) ;
Bnd_Box bb ;
try {
BRepBndLib : : Add ( compound , bb ) ;
} catch ( const Standard_Failure & ) { }
// Empty geometry
if ( bb . IsVoid ( ) ) {
return ;
}
double x1 , y1 , zmin , x2 , y2 , zmax ;
bb . Get ( x1 , y1 , zmin , x2 , y2 , zmax ) ;
2025-06-02 13:30:06 +02:00
auto height = zmax - zmin ;
auto section_height = ( height < ( 1. + 1.e-5 ) ) ? ( height / 2.0 ) : 1.0 ;
filename_ . stream < < ttl_object_id ( brep_obj ) < < " geo:hasMetricLength " < < height < < " . \n \n " ;
2025-11-07 10:59:33 +01:00
std : : map < double , std : : string > polygons_by_area ;
2024-10-08 20:57:27 +02:00
2025-11-07 10:59:33 +01:00
double rectangle_area = ( x2 - x1 ) * ( y2 - y1 ) ;
bool emitted_warning = false ;
2024-10-08 20:57:27 +02:00
2025-11-07 10:59:33 +01:00
for ( int iter = 0 ; iter < 10 ; + + iter ) {
2024-10-08 20:57:27 +02:00
2025-11-07 10:59:33 +01:00
gp_Pln pln ( gp_Pnt ( 0 , 0 , zmin + section_height + iter * ( height - 1. ) / 10. ) , gp : : DZ ( ) ) ;
2024-10-08 20:57:27 +02:00
2025-11-07 10:59:33 +01:00
Handle ( TopTools_HSequenceOfShape ) wires = new TopTools_HSequenceOfShape ( ) ;
size_t N = 0 ;
TopoDS_Iterator it ( compound ) ;
// Iterate over components of compound to have better chance of matching section edges to closed wires
for ( ; it . More ( ) ; it . Next ( ) ) {
Handle ( TopTools_HSequenceOfShape ) edges = new TopTools_HSequenceOfShape ( ) ;
TopoDS_Shape result = BRepAlgoAPI_Section ( it . Value ( ) , pln ) ;
{
TopExp_Explorer exp ( result , TopAbs_EDGE ) ;
for ( ; exp . More ( ) ; exp . Next ( ) ) {
edges - > Append ( exp . Current ( ) ) ;
}
}
ShapeAnalysis_FreeBounds : : ConnectEdgesToWires ( edges , 1e-4 , false , wires ) ;
for ( int i = 1 ; i < = wires - > Length ( ) ; + + i ) {
const TopoDS_Wire & wire = TopoDS : : Wire ( wires - > Value ( i ) ) ;
if ( ! wire . Closed ( ) ) {
continue ;
}
BRepBuilderAPI_MakeFace mf ( wire ) ;
if ( ! mf . IsDone ( ) ) {
continue ;
}
auto face = mf . Face ( ) ;
// calculate face area
GProp_GProps props ;
BRepGProp : : SurfaceProperties ( face , props ) ;
auto area = props . Mass ( ) ;
BRepTools_WireExplorer it ( wire ) ;
std : : vector < double > loop_coords ;
for ( ; it . More ( ) ; it . Next ( ) ) {
const auto & v = it . CurrentVertex ( ) ;
auto pnt = BRep_Tool : : Pnt ( v ) ;
loop_coords . push_back ( pnt . X ( ) ) ;
loop_coords . push_back ( pnt . Y ( ) ) ;
loop_coords . push_back ( pnt . Z ( ) ) ;
}
std : : vector < int > loop_idxs ( loop_coords . size ( ) / 3 ) ;
for ( int i = 0 ; i < loop_idxs . size ( ) ; + + i ) {
loop_idxs [ i ] = i ;
}
std : : string postfix = " _section_geometry_ " + std : : to_string ( N + + ) ;
std : : ostringstream oss ;
oss < < ttl_object_id ( brep_obj ) < < " geo:hasGeometry " < < ttl_object_id ( brep_obj , postfix . c_str ( ) ) < < " . \n \n " ;
oss < < ttl_object_id ( brep_obj , postfix . c_str ( ) ) < < " a geo:Geometry ; \n " ;
oss < < " geo:asWKT " < < escape_for_turtle ( IfcUtil : : convert_utf8 ( capture_output ( emit_line_component , loop_coords , loop_idxs , true , POLYGON ) ) ) < < " ^^geo:wktLiteral . \n \n " ;
polygons_by_area [ area ] = oss . str ( ) ;
}
}
if ( polygons_by_area . size ( ) > 0 ) {
if ( ( polygons_by_area . rbegin ( ) - > first > ( 0.6 * rectangle_area ) ) | | ( height < ( 1. + 1.e-5 ) ) ) {
// Found sufficiently large polygon
if ( emitted_warning ) {
2026-06-10 18:30:54 +02:00
logger_ . Warning ( " SER " , 36 , " Found larger polygon area ( " + std : : to_string ( polygons_by_area . rbegin ( ) - > first ) + " ). " ) ;
2025-11-07 10:59:33 +01:00
}
break ;
} else if ( ! emitted_warning ) {
2026-06-10 18:30:54 +02:00
logger_ . Warning ( " SER " , 37 , " Section polygon area is small compared to bounding box area ( " + std : : to_string ( polygons_by_area . rbegin ( ) - > first ) + " < " + std : : to_string ( 0.6 * rectangle_area ) + " ). Trying again with different section height. " ) ;
2025-11-07 10:59:33 +01:00
emitted_warning = true ;
}
}
}
if ( polygons_by_area . size ( ) > 0 ) {
// Emit polygon with largest area
auto it = polygons_by_area . rbegin ( ) ;
filename_ . stream < < it - > second ;
}
2024-10-08 20:57:27 +02:00
# endif
}
std : : string TtlWktSerializer : : ttl_object_id ( const IfcGeom : : Element * o , const char * const postfix )
{
using namespace ifcopenshell : : geometry : : settings ;
auto oid = boost : : replace_all_copy ( object_id ( o ) , " - " , " _ " ) ;
if ( oid . find ( ' $ ' ) = = std : : string : : npos ) {
return " base: " + oid + ( postfix ? postfix : ( const char * const ) " " ) ;
} else {
std : : string base ;
if ( settings_ . get < BaseUri > ( ) . has ( ) ) {
base = settings_ . get < BaseUri > ( ) . get ( ) ;
} else {
base = " http://example.org/ " ;
}
return " < " + base + oid + ( postfix ? postfix : ( const char * const ) " " ) + " > " ;
}
}
bool TtlWktSerializer : : isTesselated ( ) const {
using namespace ifcopenshell : : geometry : : settings ;
return ! settings_ . get < WktUseSection > ( ) . get ( ) ;
}