/******************************************************************************** * * * 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 . * * * ********************************************************************************/ #ifdef WITH_GLTF #include "JsonSerializer.h" #include #include #include #include "../../ifcparse/si_prefix.h" #include "../../ifcparse/utils.h" #include "../../ifcparse/logger.h" using json = nlohmann::json; namespace { class format_value_visitor : public boost::static_visitor { public: format_value_visitor() = default; template json operator()(const T& t) const { if constexpr (std::is_same_v, derived> || std::is_same_v, boost::dynamic_bitset<>> || std::is_same_v, express::Base> || std::is_same_v, std::vector> || std::is_same_v, std::vector> || std::is_same_v, std::vector> || std::is_same_v, std::vector>> || std::is_same_v, std::vector> || std::is_same_v, std::vector>> || std::is_same_v, std::vector>> || std::is_same_v, std::vector>> || std::is_same_v, empty_aggregate_t> || std::is_same_v, empty_aggregate_of_aggregate_t> || std::is_same_v, blank>) { return ""; } else if constexpr (std::is_same_v, boost::logic::tribool>) { // @todo handle indeterminate return ""; } else if constexpr (std::is_same_v, std::string>) { return t; } else if constexpr (std::is_same_v, enumeration_reference>) { return t.value(); } else { return t; } } }; class get_type_visitor : public boost::static_visitor { public: get_type_visitor() = default; template std::string operator()(const T& t) const { // @todo more types return "number"; } }; // Returns related entity instances using IFC's objectified relationship // model. The second and third argument require a member function pointer. template auto get_related(T t, F f, G g) { auto li = (t.*f)(); std::vector acc; for (auto& u : li) { try { auto vs = (u.template as().*g)(); if constexpr (std::is_base_of_v) { if (auto vv = vs.template as()) { acc.push_back(vv); } } else if constexpr (std::is_base_of_v) { if (auto vv = vs.concrete().template as()) { acc.push_back(vv); } } else { for (auto& v : vs) { if (auto vv = v.template as()) { acc.push_back(vv); } } } } catch (ifcopenshell::exception& e) { logger::error(e); } } return acc; } void format_entity_instance(express::Base instance, json& tree, express::Base parent = express::Base()) { /* { "id" : string, // Element GUID (IFC GloballyUniqueId) "name" : string, // Element name "longName" ?: string, // Long name (for spatial elements) "type" : string, // IFC entity type "parent" : string | null, // Parent element GUID (null for root) "groups" ?: string[], // Array of group GUIDs "ObjectType" ?: string, // ObjectType attribute (for IfcObject) "tag" ?: string, // Tag attribute (for IfcElement) "attributes" ?: { // Special attributes "elevation" ?: number // Elevation for IfcBuildingStorey }, "propertySetIds" ?: string[] // Array of property set GUIDs } */ json child; auto write_to_json = [&](const std::string& keyJson, const std::string& keyIfc) { attribute_value val; try { val = instance.as().get(keyIfc); } catch (const ifcopenshell::exception&) { // simply laziness like no attribute Tag on IfcProject return; } if (!val.isNull()) { child[keyJson] = val.apply_visitor(format_value_visitor{}); } }; write_to_json("id", "GlobalId"); write_to_json("name", "Name"); write_to_json("longName", "LongName"); child["type"] = instance.declaration().name(); if (parent) { if (auto rt = parent.as()) { child["parent"] = rt.GlobalId(); } } // @todo groups write_to_json("ObjectType", "ObjectType"); write_to_json("tag", "Tag"); if (auto storey = instance.as()) { auto elevation = storey.Elevation(); if (elevation) { child["attributes"] = json::object({{"elevation", *elevation}}); } } if (auto obj = instance.as()) { auto property_sets = get_related(obj, &IfcSchema::IfcObject::IsDefinedBy, &IfcSchema::IfcRelDefinesByProperties::RelatingPropertyDefinition); if (!property_sets.empty()) { child["propertySetIds"] = json::array(); for (auto& pset : property_sets) { child["propertySetIds"].push_back(pset.GlobalId()); } } } tree.push_back(child); } // A function to be called recursively. Template specialization is used // to descend into decomposition, containment and property relationships. template void descend(A instance, json& tree, express::Base parent = express::Base()) { if (instance.declaration().is(IfcSchema::IfcObjectDefinition::Class())) { descend(instance.template as(), tree, parent); } else { format_entity_instance(instance, tree); } } // @todo would be nice to generalize this with the XML version // // Descends into the tree by recursing into IfcRelContainedInSpatialStructure, // IfcRelDecomposes, IfcRelDefinesByType, IfcRelDefinesByProperties relations. template <> void descend(IfcSchema::IfcObjectDefinition product, json& tree, express::Base parent) { if (product.declaration().is(IfcSchema::IfcElement::Class())) { auto voids = product.as().FillsVoids(); if (voids.size() == 1 && voids.front().RelatingOpeningElement() != parent) { // Fills are placed under their corresponding opening, return early to avoid duplication. return; } } format_entity_instance(product, tree, parent); if (auto opening = product.as()) { auto fills = get_related( opening, &IfcSchema::IfcOpeningElement::HasFillings, &IfcSchema::IfcRelFillsElement::RelatedBuildingElement); for (auto& f : fills) { descend(f, tree, product); } } if (auto structure = product.as()) { auto elements = get_related(structure, &IfcSchema::IfcSpatialStructureElement::ContainsElements, &IfcSchema::IfcRelContainedInSpatialStructure::RelatedElements); for (auto& el : elements) { descend(el, tree, product); } } if (auto element = product.as()) { auto openings = get_related( element, &IfcSchema::IfcElement::HasOpenings, &IfcSchema::IfcRelVoidsElement::RelatedOpeningElement); for (auto& op : openings) { descend(op, tree, product); } } #ifdef SCHEMA_IfcRelDecomposes_HAS_RelatedObjects auto structures = get_related(product, &IfcSchema::IfcObjectDefinition::IsDecomposedBy, &IfcSchema::IfcRelDecomposes::RelatedObjects); #else auto structures = get_related(product, &IfcSchema::IfcObjectDefinition::IsDecomposedBy, &IfcSchema::IfcRelAggregates::RelatedObjects); auto nested = get_related(product, &IfcSchema::IfcObjectDefinition::IsNestedBy, &IfcSchema::IfcRelNests::RelatedObjects); structures.insert(structures.end(), nested.begin(), nested.end()); #endif for (auto& ob : structures) { descend(ob, tree, product); } // psets are handled as part of format_entity_instance() // all other relationships are not needed in JSON output } IfcSchema::IfcValue get_value_from_prop(IfcSchema::IfcProperty& prop) { if (auto psv = prop.as()) { if (auto nv = psv.NominalValue()) { return nv; } } // @todo other unit types return IfcSchema::IfcValue{}; } IfcSchema::IfcUnit get_unit_from_prop(IfcSchema::IfcProperty& prop) { if (auto psv = prop.as()) { if (auto un = psv.Unit()) { return un; } } // @todo other unit types return IfcSchema::IfcUnit{}; } } // namespace void POSTFIX_SCHEMA(JsonSerializer)::finalize() { json output; auto projects = file->instances_by_type(); if (projects.size() != 1) { logger::message(logger::LOG_ERROR, "Expected a single IfcProject"); return; } IfcSchema::IfcProject project = projects.front(); auto catch_exceptions = [this](const auto& fn) { try { return fn(); } catch (const std::exception& e) { logger::error(e); static std::invoke_result_t v; return v; } }; output["id"] = catch_exceptions([&]() { return file->header().file_name().name(); }); output["projectId"] = catch_exceptions([&]() { return project.GlobalId(); }); output["author"] = catch_exceptions([&]() { return file->header().file_name().author().empty() ? "unknown" : file->header().file_name().author().front(); }); output["createdAt"] = catch_exceptions([&]() { return file->header().file_name().time_stamp(); }); output["schema"] = catch_exceptions([&]() { return file->header().file_schema().schema_identifiers().front(); }); // without schema we would not be here output["creatingApplication"] = catch_exceptions([&]() { return file->header().file_name().originating_system(); }); output["properties"] = json::array(); output["propertySets"] = json::array(); output["units"] = json::array(); output["projectUnits"] = json::object(); output["metaObjects"] = json::array(); output["groups"] = json::array(); // Maps for deduplication of properties and quantities std::map property_to_index; std::unordered_map json_to_index; // Obtain sequence of units because properties, quantities reference them by index. // IfcUnit is a select of IfcDerivedUnit, IfcMonetaryUnit and IfcNamedUnit. // Unfortunately, instances_by_type() does not support select types directly (even though there isn't a real reason for that). std::vector units; { auto vs = file->instances_by_type(); for (auto& v : vs) { units.push_back(v); } } { auto vs = file->instances_by_type(); for (auto& v : vs) { units.push_back(v); } } { auto vs = file->instances_by_type(); for (auto& v : vs) { units.push_back(v); } } auto format_property = [&](const express::Entity& prop_) { json jprop; /* { "name": "LoadBearing", "ifcPropertyType": "IfcPropertySingleValue", "ifcValueType": "IfcBoolean", "value": "True", "valueType": "boolean" }, */ if (auto prop = prop_.as()) { jprop["name"] = prop.Name(); jprop["ifcPropertyType"] = prop.declaration().name(); if (auto val = get_value_from_prop(prop)) { jprop["ifcValueType"] = val.concrete().declaration().name(); jprop["value"] = val.concrete().get_attribute_value(0).apply_visitor(format_value_visitor{}); jprop["valueType"] = val.concrete().get_attribute_value(0).apply_visitor(get_type_visitor{}); } if (auto unit = get_unit_from_prop(prop)) { jprop["unit"] = std::distance(units.begin(), std::find(units.begin(), units.end(), unit)); } } return jprop; }; auto format_quantity = [&](const express::Entity& qto_) { json jprop; /* { "name": "GrossVolume", "ifcPropertyType": "IfcQuantityVolume", "value": 12.5, "valueType": "ElementQuantity", "unit": 3 } */ if (auto qto = qto_.as()) { jprop["name"] = qto.Name(); jprop["ifcPropertyType"] = qto.declaration().name(); if (auto prop = qto.as()) { jprop["ifcValueType"] = prop.declaration().as_entity()->attributes()[0]->name(); jprop["value"] = prop.get_attribute_value(3).apply_visitor(format_value_visitor{}); jprop["valueType"] = "number"; if (auto unit = prop.Unit()) { jprop["unit"] = std::distance(units.begin(), std::find(units.begin(), units.end(), unit)); } } } return jprop; }; auto deduplicate = [&](auto base_formatter) { return [&, base_formatter](const express::Entity& prop) mutable -> std::size_t { if (auto it = property_to_index.find(prop); it != property_to_index.end()) { return it->second; } // Build JSON for this property json j = base_formatter(prop); // Check if an identical JSON object is already in the global list auto [it2, inserted] = json_to_index.try_emplace(j, output["properties"].size()); if (inserted) { // First time we've seen this JSON -> append to output output["properties"].push_back(j); } std::size_t idx = it2->second; property_to_index.emplace(prop, idx); // remember for this pointer too return idx; }; }; auto property_index_for = deduplicate(format_property); auto quantity_index_for = deduplicate(format_quantity); auto pset_predef_or_qsets = file->instances_by_type(); for (auto& inst : pset_predef_or_qsets) { std::vector property_indices; if (auto pset = inst.as()) { auto props = pset.HasProperties(); for (auto& prop : props) { std::size_t index = property_index_for(prop); property_indices.push_back(index); } } else if (auto qset = inst.as()) { auto qtos = qset.Quantities(); for (auto& qto : qtos) { std::size_t index = quantity_index_for(qto); property_indices.push_back(index); } #ifdef SCHEMA_HAS_IfcPreDefinedPropertySet // ifc2x3 does not have this type yet, just inherits from IfcPropertySetDefinition } else if (auto pset = inst.as()) { #else } else { #endif /* // not all_attributes() only the attributes defined on this particular concrete type // @todo actually I don't know how to map PreDefinedPropertySet yet auto attributes = inst->declaration().attributes(); for (auto* a : attributes) { auto val = inst->get(a->name()); if (val.isNull()) { continue; } val.apply_visitor(format_value_visitor{}); } */ } /* { "id" : "3fG7k$Hj2_9Pxd8vD_xg7", "name" : "Pset_WallCommon", "type" : "IfcPropertySet", "properties" : [ 0, 1, 2 ] }, */ output["propertySets"].push_back(json::object({{"id", inst.GlobalId()}, {"name", *inst.Name()}, // @todo optional {"type", inst.declaration().name()}, {"properties", property_indices}})); } for (auto& unit : units) { /* { "name": string, // Unit symbol/name "className": string, // IFC unit class "unitEnum"?: string, // Unit type enum "unitType"?: string, // Unit type (alternative) "prefix"?: string, // SI prefix (for IfcSIUnit) "userDefinedType"?: string, // User-defined type "conversionFactor"?: { // Conversion factor (for IfcConversionBasedUnit) "valueComponent": { "value": number, "valueType": string, "ifcValueType": string }, "unitComponent": number // Reference to base unit index }, "elements"?: [ // For IfcDerivedUnit { "unit": number, // Reference to unit index "exponent": number // Exponent value } ], "dimensions"?: { // IfcDimensionalExponents "LengthExponent": number, "MassExponent": number, "TimeExponent": number, "ElectricCurrentExponent": number, "ThermodynamicTemperatureExponent": number, "AmountOfSubstanceExponent": number, "LuminousIntensityExponent": number } } */ json junit; junit["className"] = unit.concrete().declaration().name(); if (auto siunit = unit.concrete().as()) { // @todo figure out how to encode name for si units std::string unit_name = ""; junit["unitEnum"] = IfcSchema::IfcUnitEnum::ToString(siunit.UnitType()); if (siunit.Prefix()) { junit["prefix"] = IfcSchema::IfcSIPrefix::ToString(*siunit.Prefix()); unit_name.push_back(IfcSchema::IfcSIPrefix::ToString(*siunit.Prefix())[0]); } unit_name.push_back(IfcSchema::IfcSIUnitName::ToString(siunit.Name())[0]); boost::to_lower(unit_name); junit["name"] = unit_name; } else if (auto convunit = unit.concrete().as()) { junit["name"] = convunit.Name(); junit["unitEnum"] = IfcSchema::IfcUnitEnum::ToString(convunit.UnitType()); if (convunit.ConversionFactor()) { json jconv; auto val = convunit.ConversionFactor().ValueComponent(); jconv["valueComponent"] = { {"value", val.concrete().get_attribute_value(0).apply_visitor(format_value_visitor{})}, {"valueType", val.concrete().get_attribute_value(0).apply_visitor(get_type_visitor{})} }; jconv["unitComponent"] = std::distance(units.begin(), std::find(units.begin(), units.end(), convunit.ConversionFactor().UnitComponent())); junit["conversionFactor"] = jconv; } } else if (auto derunit = unit.concrete().as()) { #ifdef SCHEMA_IfcDerivedUnit_HAS_Name // 4.3 onwards if (derunit.Name()) { junit["name"] = *derunit.Name(); } #endif json jelements = json::array(); auto elements = derunit.Elements(); for (auto& elem : elements) { jelements.push_back({ {"unit", std::distance(units.begin(), std::find(units.begin(), units.end(), elem.Unit()))}, {"exponent", elem.Exponent()} }); } junit["elements"] = jelements; } if (auto namedunit = unit.concrete().as()) { // support for derived attributes is only available in python if (!namedunit.as()) { if (auto dimexp = namedunit.Dimensions()) { junit["dimensions"] = { {"LengthExponent", dimexp.LengthExponent()}, {"MassExponent", dimexp.MassExponent()}, {"TimeExponent", dimexp.TimeExponent()}, {"ElectricCurrentExponent", dimexp.ElectricCurrentExponent()}, {"ThermodynamicTemperatureExponent", dimexp.ThermodynamicTemperatureExponent()}, {"AmountOfSubstanceExponent", dimexp.AmountOfSubstanceExponent()}, {"LuminousIntensityExponent", dimexp.LuminousIntensityExponent()}}; } } } output["units"].push_back(junit); } auto project_units = project.UnitsInContext().Units(); /* { "LENGTHUNIT": number, "AREAUNIT": number, "VOLUMEUNIT": number, "PLANEANGLEUNIT": number, "MASSUNIT": number, "TIMEUNIT": number, // ... other unit types }*/ for (auto pu : project_units) { auto it = std::find(units.begin(), units.end(), pu); if (auto nu = pu.as()) { if (it != units.end()) { output["projectUnits"][IfcSchema::IfcUnitEnum::ToString(nu.UnitType())] = std::distance(units.begin(), it); } } } /* // meta objects { "id": string, // Element GUID (IFC GloballyUniqueId) "name": string, // Element name "longName"?: string, // Long name (for spatial elements) "type": string, // IFC entity type "parent": string | null, // Parent element GUID (null for root) "groups"?: string[], // Array of group GUIDs "ObjectType"?: string, // ObjectType attribute (for IfcObject) "tag"?: string, // Tag attribute (for IfcElement) "attributes"?: { // Special attributes "elevation"?: number // Elevation for IfcBuildingStorey }, "propertySetIds"?: string[] // Array of property set GUIDs } */ descend(project, output["metaObjects"]); std::ofstream f(ifcopenshell::path::from_utf8(json_filename).c_str()); f << output.dump(4); } #endif