From a2db0a68a49f4856ed757367d916e9e792619756 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 1 May 2026 11:58:36 +1000 Subject: [PATCH] ifcviewer: port auto_local2global to C++ in Geolocation.{h,cpp} Mirrors ifcopenshell.util.geolocation: HelmertTransformation parameters (IfcMapConversion / IfcMapConversionScaled / IfcRigidOperation, plus IFC2X3 ePSet_MapConversion), get_wcs from IfcGeometricRepresentationContext, local2global, and auto_local2global. Lives in src/ifcviewer/ for now; will move out when ifcopenshell.util is ported to C++. Not yet wired into the streamer. A subsequent commit fixes the unit handling for the iterator's meter-by-default output. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer/Geolocation.cpp | 276 ++++++++++++++++++++++++++++++++++ src/ifcviewer/Geolocation.h | 73 +++++++++ 2 files changed, 349 insertions(+) create mode 100644 src/ifcviewer/Geolocation.cpp create mode 100644 src/ifcviewer/Geolocation.h diff --git a/src/ifcviewer/Geolocation.cpp b/src/ifcviewer/Geolocation.cpp new file mode 100644 index 0000000000..9649dc1255 --- /dev/null +++ b/src/ifcviewer/Geolocation.cpp @@ -0,0 +1,276 @@ +/******************************************************************************** + * * + * 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 "Geolocation.h" + +#include "../ifcparse/express.h" +#include "../ifcparse/file.h" +#include "../ifcparse/instance_data.h" +#include "../ifcparse/schema.h" + +#include +#include +#include + +namespace { + +// IfcAxis2Placement3D / IfcAxis2PlacementLinear -> column-major 4x4 matrix. +// Mirrors ifcopenshell.util.placement.a2p + get_axis2placement, but only the +// branches needed for IfcGeometricRepresentationContext.WorldCoordinateSystem. +std::optional getAxis2Placement(express::Base placement) { + if (!placement) return std::nullopt; + const auto& decl = placement.declaration(); + if (!(decl.is("IfcAxis2Placement3D") || decl.is("IfcAxis2PlacementLinear"))) { + return std::nullopt; + } + + auto entity = placement.as(); + + Eigen::Vector3d z(0.0, 0.0, 1.0); + Eigen::Vector3d x(1.0, 0.0, 0.0); + + auto axis_attr = entity.get("Axis"); + if (!axis_attr.isNull()) { + express::Base axis = axis_attr; + std::vector dr = + axis.as().get("DirectionRatios"); + if (dr.size() >= 3) z = Eigen::Vector3d(dr[0], dr[1], dr[2]); + } + + auto refdir_attr = entity.get("RefDirection"); + if (!refdir_attr.isNull()) { + express::Base refdir = refdir_attr; + std::vector dr = + refdir.as().get("DirectionRatios"); + if (dr.size() >= 3) x = Eigen::Vector3d(dr[0], dr[1], dr[2]); + } + + auto loc_attr = entity.get("Location"); + if (loc_attr.isNull()) return std::nullopt; + express::Base location = loc_attr; + auto coords_attr = location.as().get("Coordinates"); + if (coords_attr.isNull()) return std::nullopt; + std::vector coords = coords_attr; + if (coords.size() < 3) return std::nullopt; + + Eigen::Vector3d xn = x.normalized(); + Eigen::Vector3d zn = z.normalized(); + Eigen::Vector3d yn = zn.cross(xn).normalized(); + + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.block<3, 1>(0, 0) = xn; + m.block<3, 1>(0, 1) = yn; + m.block<3, 1>(0, 2) = zn; + m(0, 3) = coords[0]; + m(1, 3) = coords[1]; + m(2, 3) = coords[2]; + return m; +} + +// Read a numeric NominalValue out of an IfcPropertySingleValue. IFC2X3 +// ePSet_MapConversion stores eastings/northings/scale as IfcLengthMeasure or +// IfcReal wrapped inside IfcValue (a SELECT) — get_attribute_value(0) peels +// the wrapper. Returns nullopt if the value is missing or non-numeric. +std::optional readPropertyValueDouble(const express::Base& property) { + if (!property.declaration().is("IfcPropertySingleValue")) return std::nullopt; + auto pe = property.as(); + auto nv = pe.get("NominalValue"); + if (nv.isNull()) return std::nullopt; + express::Base wrapper = nv; + auto inner = wrapper.get_attribute_value(0); + if (inner.isNull()) return std::nullopt; + switch (inner.type()) { + case ifcopenshell::Argument_DOUBLE: return (double) inner; + case ifcopenshell::Argument_INT: return (double)(int) inner; + default: return std::nullopt; + } +} + +} // namespace + +std::optional +getHelmertTransformationParameters(ifcopenshell::file* ifc_file) { + HelmertTransformation p; + const std::string schema_name = ifc_file->schema()->name(); + + if (schema_name == "IFC2X3") { + auto projects = ifc_file->instances_by_type("IfcProject"); + if (projects.empty()) return std::nullopt; + const auto& project = projects[0]; + + bool found = false; + auto rels = project.as().get_inverse("IsDefinedBy"); + for (const auto& rel : rels) { + if (!rel.declaration().is("IfcRelDefinesByProperties")) continue; + express::Base pset_base = rel.get("RelatingPropertyDefinition"); + if (!pset_base.declaration().is("IfcPropertySet")) continue; + auto pset = pset_base.as(); + auto name_attr = pset.get("Name"); + if (name_attr.isNull()) continue; + std::string pset_name = name_attr; + if (pset_name != "ePSet_MapConversion") continue; + + std::vector props = pset.get("HasProperties"); + for (const auto& prop : props) { + if (!prop.declaration().is("IfcPropertySingleValue")) continue; + auto pe = prop.as(); + auto pname_attr = pe.get("Name"); + if (pname_attr.isNull()) continue; + std::string pname = pname_attr; + auto value = readPropertyValueDouble(prop); + if (!value) continue; + if (pname == "Eastings") p.e = *value; + else if (pname == "Northings") p.n = *value; + else if (pname == "OrthogonalHeight") p.h = *value; + else if (pname == "XAxisAbscissa") p.xaa = *value; + else if (pname == "XAxisOrdinate") p.xao = *value; + else if (pname == "Scale") p.scale = *value; + } + found = true; + break; + } + if (!found) return std::nullopt; + + // Python: `conversion.get("Scale", None) or 1` — 0 falls back to 1. + if (p.scale == 0.0) p.scale = 1.0; + p.factor_x = p.factor_y = p.factor_z = 1.0; + } else { + std::vector conversions; + try { + conversions = ifc_file->instances_by_type("IfcCoordinateOperation"); + } catch (...) { + // Schema doesn't know IfcCoordinateOperation. + return std::nullopt; + } + if (conversions.empty()) return std::nullopt; + const auto& conversion = conversions[0]; + auto entity = conversion.as(); + const std::string type_name = conversion.declaration().name(); + + auto get_or = [&](const std::string& name, double fallback) { + auto a = entity.get(name); + return a.isNull() ? fallback : (double) a; + }; + + if (conversion.declaration().is("IfcMapConversion")) { + p.e = get_or("Eastings", 0.0); + p.n = get_or("Northings", 0.0); + p.h = get_or("OrthogonalHeight", 0.0); + p.xaa = get_or("XAxisAbscissa", 0.0); + p.xao = get_or("XAxisOrdinate", 0.0); + p.scale = get_or("Scale", 1.0); + if (p.scale == 0.0) p.scale = 1.0; + + if (type_name == "IfcMapConversionScaled") { + p.factor_x = entity.get("FactorX"); + p.factor_y = entity.get("FactorY"); + p.factor_z = entity.get("FactorZ"); + } else { + p.factor_x = p.factor_y = p.factor_z = 1.0; + } + } else if (type_name == "IfcRigidOperation") { + // FirstCoordinate / SecondCoordinate are IfcLengthMeasure-typed + // values; the C++ binding auto-unwraps defined types of REAL. + p.e = get_or("FirstCoordinate", 0.0); + p.n = get_or("SecondCoordinate", 0.0); + p.h = get_or("Height", 0.0); + p.xaa = 1.0; + p.xao = 0.0; + p.scale = p.factor_x = p.factor_y = p.factor_z = 1.0; + } else { + return std::nullopt; + } + } + + if (p.xaa == 0.0 && p.xao == 0.0) { + p.xaa = 1.0; + p.xao = 0.0; + } + return p; +} + +std::optional getWcs(ifcopenshell::file* ifc_file) { + auto contexts = ifc_file->instances_by_type_excl_subtypes( + "IfcGeometricRepresentationContext"); + express::Base wcs; + bool found = false; + for (const auto& ctx : contexts) { + auto entity = ctx.as(); + auto wcs_attr = entity.get("WorldCoordinateSystem"); + if (wcs_attr.isNull()) continue; + wcs = (express::Base) wcs_attr; + found = true; + auto ctype_attr = entity.get("ContextType"); + if (!ctype_attr.isNull()) { + std::string ctype = ctype_attr; + if (ctype == "Model") break; + } + } + if (!found) return std::nullopt; + return getAxis2Placement(wcs); +} + +Eigen::Matrix4d local2global(const Eigen::Matrix4d& matrix, + const HelmertTransformation& p) { + const double theta = std::atan2(p.xao, p.xaa); + const double c = std::cos(theta); + const double s = std::sin(theta); + + Eigen::Matrix4d S = Eigen::Matrix4d::Identity(); + S(0, 0) = p.scale * p.factor_x; + S(1, 1) = p.scale * p.factor_y; + S(2, 2) = p.scale * p.factor_z; + + Eigen::Matrix4d R = Eigen::Matrix4d::Identity(); + R(0, 0) = c; R(0, 1) = -s; + R(1, 0) = s; R(1, 1) = c; + + Eigen::Matrix4d result = R * S * matrix; + // The scale was baked into the rotation+scale matrix so each axis column + // ended up scaled. Renormalise so the rotation part is pure orientation + // and the translation alone carries the scaled offsets. + for (int col = 0; col < 3; ++col) { + Eigen::Vector3d v = result.block<3, 1>(0, col); + const double n = v.norm(); + if (n > 0.0) result.block<3, 1>(0, col) = v / n; + } + result(0, 3) += p.e; + result(1, 3) += p.n; + result(2, 3) += p.h; + return result; +} + +Eigen::Matrix4d autoLocal2Global(ifcopenshell::file* ifc_file, + const Eigen::Matrix4d& matrix, + bool should_return_in_map_units) { + auto params = getHelmertTransformationParameters(ifc_file); + if (!params) return matrix; + + Eigen::Matrix4d m = matrix; + if (auto wcs = getWcs(ifc_file)) { + m = wcs->inverse() * m; + } + Eigen::Matrix4d result = local2global(m, *params); + if (!should_return_in_map_units) { + result(0, 3) /= params->scale; + result(1, 3) /= params->scale; + result(2, 3) /= params->scale; + } + return result; +} diff --git a/src/ifcviewer/Geolocation.h b/src/ifcviewer/Geolocation.h new file mode 100644 index 0000000000..e83640a718 --- /dev/null +++ b/src/ifcviewer/Geolocation.h @@ -0,0 +1,73 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +// Port of selected helpers from +// src/ifcopenshell-python/ifcopenshell/util/geolocation.py — primarily +// auto_local2global, which builds a 4x4 matrix that lifts an element's local +// transform into the model's global (georeferenced) frame. The python utils +// are expected to be ported to C++ in their own module later; this file is +// the temporary home until that lands. + +#ifndef GEOLOCATION_H +#define GEOLOCATION_H + +#include + +#include + +namespace ifcopenshell { class file; } + +struct HelmertTransformation { + double e = 0.0; // eastings offset + double n = 0.0; // northings offset + double h = 0.0; // orthogonal-height offset + double xaa = 1.0; // X-axis abscissa (cos of grid-rotation angle) + double xao = 0.0; // X-axis ordinate (sin of grid-rotation angle) + double scale = 1.0; // unit scale (project unit -> map unit) + double factor_x = 1.0; // combined scale factor along X + double factor_y = 1.0; // combined scale factor along Y + double factor_z = 1.0; // combined scale factor along Z +}; + +// Detect a Helmert transformation in the IFC model. Reads IfcMapConversion / +// IfcMapConversionScaled / IfcRigidOperation in IFC4+, or the +// IfcProject.ePSet_MapConversion property set in IFC2X3. Returns nullopt +// when the model has no map conversion. +std::optional +getHelmertTransformationParameters(ifcopenshell::file* ifc_file); + +// Read the IfcGeometricRepresentationContext.WorldCoordinateSystem (preferring +// the "Model" context) as a 4x4 matrix. Returns nullopt when the model has +// no parseable WCS. +std::optional getWcs(ifcopenshell::file* ifc_file); + +// Apply a Helmert transformation to a 4x4 local matrix. +Eigen::Matrix4d local2global(const Eigen::Matrix4d& matrix, + const HelmertTransformation& params); + +// Lift a 4x4 local matrix into global (map) coordinates using the IFC model's +// georeferencing data. When no map conversion is present the matrix is +// returned unchanged. When should_return_in_map_units is false, the +// translation column is divided by the map scale so the result is expressed +// in project length units. +Eigen::Matrix4d autoLocal2Global(ifcopenshell::file* ifc_file, + const Eigen::Matrix4d& matrix, + bool should_return_in_map_units = true); + +#endif // GEOLOCATION_H