From 314534436d13e46b74c189b1c0d6af82484a79ca Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 27 Sep 2024 16:32:41 +0200 Subject: [PATCH] Add .ttl + Well Known Text geometry serializer --- src/ifcconvert/IfcConvert.cpp | 11 +- .../ifcopenshell/geom/main.py | 10 + src/ifcwrap/IfcGeomWrapper.i | 1 + src/ifcwrap/IfcPython.i | 3 +- src/serializers/TtlWktSerializer.cpp | 316 ++++++++++++++++++ src/serializers/TtlWktSerializer.h | 47 +++ 6 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 src/serializers/TtlWktSerializer.cpp create mode 100644 src/serializers/TtlWktSerializer.h diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index fac54d6025..c95a838584 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -39,6 +39,7 @@ #include "../serializers/XmlSerializer.h" #include "../serializers/SvgSerializer.h" #include "../serializers/USDSerializer.h" +#include "../serializers/TtlWktSerializer.h" #include "../ifcgeom/IfcGeomFilter.h" #include "../ifcgeom/Iterator.h" @@ -132,6 +133,7 @@ void print_usage(bool suggest_help = true) #ifdef IFOPSH_WITH_CGAL << " .cityjson City JSON format for geospatial data\n" #endif + << " .ttl TTL/WKT RDF Turtle with Well-Known-Text geometry\n" << " .ifc IFC-SPF Industry Foundation Classes\n" << "\n" << "If no output filename given, " << IfcUtil::path::from_utf8(DEFAULT_EXTENSION) << " will be used as the output file.\n"; @@ -647,7 +649,8 @@ int main(int argc, char** argv) { IFC = IfcUtil::path::from_utf8(".ifc"), USD = IfcUtil::path::from_utf8(".usd"), USDA = IfcUtil::path::from_utf8(".usda"), - USDC = IfcUtil::path::from_utf8(".usdc"); + USDC = IfcUtil::path::from_utf8(".usdc"), + TTL = IfcUtil::path::from_utf8(".ttl"); // @todo clean up serializer selection // @todo detect program options that conflict with the chosen serializer @@ -826,6 +829,10 @@ int main(int argc, char** argv) { geometry_settings.get().value = true; } + if (output_extension == TTL) { + geometry_settings.get().value = ifcopenshell::geometry::settings::TriangulationMethod::POLYHEDRON_WITH_HOLES; + } + boost::shared_ptr serializer; /**< @todo use std::unique_ptr when possible */ if (output_extension == OBJ) { // Do not use temp file for MTL as it's such a small file. @@ -861,6 +868,8 @@ int main(int argc, char** argv) { serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings); #endif #endif + } else if (output_extension == TTL) { + serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings); } else { cerr_ << "[Error] Unknown output filename extension '" << output_extension << "'\n"; write_log(!quiet); diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 1773275bea..5beaa10342 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -570,3 +570,13 @@ class serializers: hdf5 = ifcopenshell_wrapper.HdfSerializer except: pass + + # ttl is always available since it doesn't depend on any C++ libraries, + # just people might be using an outdated binary + if hasattr(ifcopenshell_wrapper, 'TtlWktSerializer'): + @staticmethod + def ttl( + out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings + ) -> ifcopenshell_wrapper.SvgSerializer: + out_filename = transform_string(out_filename) + return ifcopenshell_wrapper.TtlWktSerializer(out_filename, geometry_settings, settings) diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 9b1ab46395..002fd03aa0 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -253,6 +253,7 @@ namespace { %include "../serializers/WavefrontObjSerializer.h" %include "../serializers/XmlSerializer.h" %include "../serializers/GltfSerializer.h" +%include "../serializers/TtlWktSerializer.h" %extend ifcopenshell::geometry::taxonomy::style { size_t instance_id() const { diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index 72fe60f21d..a12457f092 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -193,7 +193,8 @@ constexpr bool is_std_vector_vector_v = is_std_vector_vector::value; #include "../serializers/HdfSerializer.h" #include "../serializers/XmlSerializer.h" #include "../serializers/GltfSerializer.h" - + #include "../serializers/TtlWktSerializer.h" + #ifdef HAS_SCHEMA_2x3 #include "../ifcparse/Ifc2x3.h" #endif diff --git a/src/serializers/TtlWktSerializer.cpp b/src/serializers/TtlWktSerializer.cpp new file mode 100644 index 0000000000..7fd8962c01 --- /dev/null +++ b/src/serializers/TtlWktSerializer.cpp @@ -0,0 +1,316 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#include "TtlWktSerializer.h" + +#include +#include +#include +#include +#include +#include + +namespace { + const char* const LINESTRING = "LINESTRING"; + const char* const POLYGON = "POLYGON"; + + void emit_polyhedral_surface( + std::ostream& os, + const std::vector& vertices, + const std::vector>>& 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& vertices, + const std::vector& 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& vertices, + const std::vector& lines, + bool force_2d = false) + { + std::unordered_map> adjacencyList; + std::unordered_set 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 component; // To store the current component + std::queue 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> 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) { + std::wostringstream escaped; + 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" + << std::hex << std::setw(4) << std::setfill(L'0') + << (c & 0xFFFF); + } else { + escaped.put(c); + } + break; + } + } + escaped << "\""; + return IfcUtil::convert_utf8(escaped.str()); + } + + template + std::string capture_output(Fn fn, Ts... ts) { + std::ostringstream oss; + fn(oss, ts...); + return oss.str(); + } +} + +TtlWktSerializer::TtlWktSerializer(const stream_or_filename& filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings) + : WriteOnlyGeometrySerializer(geometry_settings, settings) + , filename_(filename) +{ + const auto& tri_setting = geometry_settings.get().get(); + if (tri_setting != ifcopenshell::geometry::settings::POLYHEDRON_WITH_HOLES) { + throw std::runtime_error("The RDF Turtle WKT serializer needs POLYHEDRON_WITH_HOLES triangulation output"); + } + filename_.stream << std::setprecision(settings.get().get()); +} + +bool TtlWktSerializer::ready() +{ + return filename_.is_ready(); +} + +void TtlWktSerializer::writeHeader() +{ + filename_.stream << "# File generated by IfcOpenShell " << IFCOPENSHELL_VERSION << "\n"; + filename_.stream << "@prefix geo: .\n"; + filename_.stream << "@prefix ex: .\n"; + filename_.stream << "@prefix dcterms: .\n"; + filename_.stream << "@prefix rdfs: .\n\n\n"; +} + +void TtlWktSerializer::write(const IfcGeom::TriangulationElement* o) +{ + auto ttl_object_id = [&]() { + return boost::replace_all_copy(object_id(o), "-", "_"); + }; + + filename_.stream << "ex:" << ttl_object_id() << " a geo:Feature ;\n"; + filename_.stream << " dcterms:identifier " << escape_for_turtle( + IfcUtil::convert_utf8_to_utf32(o->guid())) << " ;\n"; + filename_.stream << " rdfs:label " << escape_for_turtle( + IfcUtil::convert_utf8_to_utf32(o->name()) + ) << " ;\n"; + filename_.stream << " geo:hasGeometry ex:" << ttl_object_id() << "_geometry .\n\n"; + + if (!o->geometry().polyhedral_faces_with_holes().empty()) { + filename_.stream << "ex:" << ttl_object_id() << "_geometry a geo:Geometry ;\n"; + filename_.stream << " geo:asWKT " << escape_for_turtle( + IfcUtil::convert_utf8_to_utf32( + capture_output( + emit_polyhedral_surface, + o->geometry().verts(), + o->geometry().polyhedral_faces_with_holes())) + ) << "^^geo:wktLiteral .\n\n"; + + Eigen::Map> vertex_map(o->geometry().verts().data(), 3, o->geometry().verts().size() / 3); + + boost::optional>::const_iterator> lowest_face; + double lowest_z = std::numeric_limits::infinity(); + + for (const auto& f : o->geometry().polyhedral_faces_with_holes()) { + auto v0 = vertex_map.transpose().row(f[0][0]); + auto v1 = vertex_map.transpose().row(f[0][1]); + auto v2 = vertex_map.transpose().row(f[0][2]); + auto v1_v0 = v1 - v0; + auto v2_v0 = v2 - v0; + + Eigen::Vector3d cross_product = v1_v0.cross(v2_v0); + cross_product.normalize(); + // @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(); + } + } + + if (lowest_face) { + filename_.stream << "ex:" << ttl_object_id() << " geo:hasGeometry ex:" << ttl_object_id() << "_footprint_geometry .\n\n"; + filename_.stream << "ex:" << ttl_object_id() << "_footprint_geometry a geo:Geometry ;\n"; + filename_.stream << " geo:asWKT " << escape_for_turtle( + IfcUtil::convert_utf8_to_utf32( + 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 { + filename_.stream << "ex:" << ttl_object_id() << "_geometry a geo:Geometry ;\n"; + 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( + IfcUtil::convert_utf8_to_utf32( + capture_output( + emit_line_strings, + o->geometry().verts(), + o->geometry().edges(), + force_2d)) + ) << "^^geo:wktLiteral .\n\n"; + } +} diff --git a/src/serializers/TtlWktSerializer.h b/src/serializers/TtlWktSerializer.h new file mode 100644 index 0000000000..5e064b47ed --- /dev/null +++ b/src/serializers/TtlWktSerializer.h @@ -0,0 +1,47 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#ifndef TTLWKTSERIALIZER_H +#define TTLWKTSERIALIZER_H + +#include +#include +#include + +#include "../serializers/serializers_api.h" +#include "../ifcgeom/GeometrySerializer.h" + + // http://people.sc.fsu.edu/~jburkardt/txt/obj_format.txt +class SERIALIZERS_API TtlWktSerializer : public WriteOnlyGeometrySerializer { +private: + stream_or_filename filename_; +public: + TtlWktSerializer(const stream_or_filename& filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings); + virtual ~TtlWktSerializer() {} + bool ready(); + void writeHeader(); + void write(const IfcGeom::TriangulationElement* o); + void write(const IfcGeom::BRepElement* /*o*/) {} + void finalize() {} + bool isTesselated() const { return true; } + void setUnitNameAndMagnitude(const std::string& /*name*/, float /*magnitude*/) {} + void setFile(IfcParse::IfcFile*) {} +}; + +#endif