/******************************************************************************** * * * 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" #ifdef IFOPSH_WITH_OPENCASCADE #include "../ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #endif #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::ostringstream 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('0') << (c & 0xFFFF); } else { escaped.put(c); } break; } } escaped << "\""; return escaped.str(); } template std::string capture_output(Fn fn, Ts... ts) { std::ostringstream oss; oss << std::setprecision(std::numeric_limits::digits10 + 1); fn(oss, ts...); return oss.str(); } } 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) , filename_(filename) { const auto& tri_setting = geometry_settings.get().get(); if (settings_.get().get()) { const auto& it_output = geometry_settings.get().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"); } } filename_.stream << std::setprecision(settings.get().get()); } bool TtlWktSerializer::ready() { return filename_.is_ready(); } void TtlWktSerializer::writeHeader() { using namespace ifcopenshell::geometry::settings; filename_.stream << "# File generated by IfcOpenShell " << IFCOPENSHELL_VERSION << "\n"; filename_.stream << "@prefix geo: .\n"; if (settings_.get().has()) { filename_.stream << "@prefix base: <" << settings_.get().get() << "> .\n"; } else { filename_.stream << "@prefix base: .\n"; } filename_.stream << "@prefix dcterms: .\n"; filename_.stream << "@prefix rdfs: .\n\n\n"; } void TtlWktSerializer::write(const IfcGeom::TriangulationElement* o) { filename_.stream << ttl_object_id(o) << " a geo:Feature ;\n"; filename_.stream << " dcterms:identifier " << escape_for_turtle( IfcUtil::convert_utf8(o->guid())) << " ;\n"; filename_.stream << " rdfs:label " << escape_for_turtle( IfcUtil::convert_utf8(o->name()) ) << " ;\n"; filename_.stream << " geo:hasGeometry " << ttl_object_id(o, "_geometry") << " .\n\n"; if (!o->geometry().polyhedral_faces_with_holes().empty()) { filename_.stream << ttl_object_id(o, "_geometry") << " a geo:Geometry ;\n"; filename_.stream << " geo:asWKT " << escape_for_turtle( IfcUtil::convert_utf8( 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()) { 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; } 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(); lowest_z = v0.z(); } } if (lowest_face) { 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"; filename_.stream << " geo:asWKT " << escape_for_turtle( IfcUtil::convert_utf8( 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 << ttl_object_id(o, "_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( capture_output( emit_line_strings, o->geometry().verts(), o->geometry().edges(), force_2d)) ) << "^^geo:wktLiteral .\n\n"; } } 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( IfcUtil::convert_utf8(brep_obj->guid())) << " ;\n"; filename_.stream << " rdfs:label " << escape_for_turtle( IfcUtil::convert_utf8(brep_obj->name()) ) << " .\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); 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"; std::map polygons_by_area; double rectangle_area = (x2 - x1) * (y2 - y1); bool emitted_warning = false; for (int iter = 0; iter < 10; ++iter) { gp_Pln pln(gp_Pnt(0, 0, zmin + section_height + iter * (height - 1.) / 10.), gp::DZ()); 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 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 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) { logger_.Warning("SER", 36, "Found larger polygon area (" + std::to_string(polygons_by_area.rbegin()->first) + ")."); } break; } else if (!emitted_warning) { 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."); emitted_warning = true; } } } if (polygons_by_area.size() > 0) { // Emit polygon with largest area auto it = polygons_by_area.rbegin(); filename_.stream << it->second; } #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().has()) { base = settings_.get().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().get(); }