diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index 80e4e06b4a..4e0f0abf19 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -266,6 +266,7 @@ SET(FreeCADApp_CPP_SRCS ColorModel.cpp ComplexGeoData.cpp ComplexGeoDataPyImp.cpp + ElementMap.cpp Enumeration.cpp IndexedName.cpp MappedElement.cpp @@ -274,6 +275,7 @@ SET(FreeCADApp_CPP_SRCS MaterialPyImp.cpp Metadata.cpp MetadataPyImp.cpp + ElementNamingUtils.cpp StringHasher.cpp StringHasherPyImp.cpp StringIDPyImp.cpp @@ -295,6 +297,7 @@ SET(FreeCADApp_HPP_SRCS MappedElement.h Material.h Metadata.h + ElementNamingUtils.h StringHasher.h ) diff --git a/src/App/ComplexGeoData.cpp b/src/App/ComplexGeoData.cpp index 253a686be7..df0c78265b 100644 --- a/src/App/ComplexGeoData.cpp +++ b/src/App/ComplexGeoData.cpp @@ -27,10 +27,10 @@ # include #endif -#include #include #include "ComplexGeoData.h" + #include #include #include @@ -166,102 +166,3 @@ bool ComplexGeoData::getCenterOfGravity(Base::Vector3d&) const return false; } -const std::string &ComplexGeoData::elementMapPrefix() { - static std::string prefix(";"); - return prefix; -} - -const char *ComplexGeoData::isMappedElement(const char *name) { - if(name && boost::starts_with(name,elementMapPrefix())) - return name+elementMapPrefix().size(); - return nullptr; -} - -std::string ComplexGeoData::newElementName(const char *name) { - if(!name) - return std::string(); - const char *dot = strrchr(name,'.'); - if(!dot || dot==name) - return name; - const char *c = dot-1; - for(;c!=name;--c) { - if(*c == '.') { - ++c; - break; - } - } - if(isMappedElement(c)) - return std::string(name,dot-name); - return name; -} - -std::string ComplexGeoData::oldElementName(const char *name) { - if(!name) - return std::string(); - const char *dot = strrchr(name,'.'); - if(!dot || dot==name) - return name; - const char *c = dot-1; - for(;c!=name;--c) { - if(*c == '.') { - ++c; - break; - } - } - if(isMappedElement(c)) - return std::string(name,c-name)+(dot+1); - return name; -} - -std::string ComplexGeoData::noElementName(const char *name) { - if(!name) - return std::string(); - auto element = findElementName(name); - if(element) - return std::string(name,element-name); - return name; -} - -const char *ComplexGeoData::findElementName(const char *subname) { - if(!subname || !subname[0] || isMappedElement(subname)) - return subname; - const char *dot = strrchr(subname,'.'); - if(!dot) - return subname; - const char *element = dot+1; - if(dot==subname || isMappedElement(element)) - return element; - for(--dot;dot!=subname;--dot) { - if(*dot == '.') { - ++dot; - break; - } - } - if(isMappedElement(dot)) - return dot; - return element; -} - -const std::string &ComplexGeoData::tagPostfix() { - static std::string postfix(elementMapPrefix() + ":T"); - return postfix; -} - -const std::string &ComplexGeoData::indexPostfix() { - static std::string postfix(elementMapPrefix() + ":I"); - return postfix; -} - -const std::string &ComplexGeoData::missingPrefix() { - static std::string prefix("?"); - return prefix; -} - -bool ComplexGeoData::hasMissingElement(const char *subname) { - if(!subname) - return false; - auto dot = strrchr(subname,'.'); - if(dot) - subname = dot+1; - return boost::starts_with(subname,missingPrefix()); -} diff --git a/src/App/ComplexGeoData.h b/src/App/ComplexGeoData.h index 8bbee9c971..ae59fde241 100644 --- a/src/App/ComplexGeoData.h +++ b/src/App/ComplexGeoData.h @@ -164,41 +164,6 @@ public: virtual bool getCenterOfGravity(Base::Vector3d& center) const; //@} - /** @name Element name mapping */ - //@{ - /// Special prefix to mark the beginning of a mapped sub-element name - static const std::string &elementMapPrefix(); - /// Special postfix to mark the following tag - static const std::string &tagPostfix(); - /// Special postfix to mark the index of an array element - static const std::string &indexPostfix(); - /// Special prefix to mark a missing element - static const std::string &missingPrefix(); - /// Check if a subname contains missing element - static bool hasMissingElement(const char *subname); - /** Check if the name starts with elementMapPrefix() - * - * @param name: input name - * @return Returns the name stripped with elementMapPrefix(), or 0 if not - * start with the prefix - */ - static const char *isMappedElement(const char *name); - - /// Strip out the trailing element name if there is mapped element name precedes it. - static std::string newElementName(const char *name); - /// Strip out the mapped element name if there is one. - static std::string oldElementName(const char *name); - /// Strip out the old and new element name if there is one. - static std::string noElementName(const char *name); - - /// Find the start of an element name in a subname - static const char *findElementName(const char *subname); - - static inline const char *hasMappedElementName(const char *subname) { - return isMappedElement(findElementName(subname)); - } - //@} - protected: /// from local to outside diff --git a/src/App/DocumentObject.cpp b/src/App/DocumentObject.cpp index 396cdfb868..86329aa688 100644 --- a/src/App/DocumentObject.cpp +++ b/src/App/DocumentObject.cpp @@ -34,7 +34,7 @@ #include #include "Application.h" -#include "ComplexGeoData.h" +#include "ElementNamingUtils.h" #include "Document.h" #include "DocumentObject.h" #include "DocumentObjectExtension.h" @@ -1094,7 +1094,7 @@ DocumentObject *DocumentObject::resolve(const char *subname, // following it. So finding the last dot will give us the end of the last // object name. const char *dot=nullptr; - if(Data::ComplexGeoData::isMappedElement(subname) || + if(Data::isMappedElement(subname) || !(dot=strrchr(subname,'.')) || dot == subname) { @@ -1117,7 +1117,7 @@ DocumentObject *DocumentObject::resolve(const char *subname, if(!elementMapChecked) { elementMapChecked = true; const char *sub = dot==subname?dot:dot+1; - if(Data::ComplexGeoData::isMappedElement(sub)) { + if(Data::isMappedElement(sub)) { lastDot = dot; if(dot==subname) break; diff --git a/src/App/DocumentObserver.cpp b/src/App/DocumentObserver.cpp index 35930e84ca..9ca582d311 100644 --- a/src/App/DocumentObserver.cpp +++ b/src/App/DocumentObserver.cpp @@ -26,7 +26,7 @@ #include #include "Application.h" -#include "ComplexGeoData.h" +#include "ElementNamingUtils.h" #include "Document.h" #include "DocumentObserver.h" #include "GeoFeature.h" @@ -353,11 +353,11 @@ const std::string &SubObjectT::getSubName() const { } std::string SubObjectT::getSubNameNoElement() const { - return Data::ComplexGeoData::noElementName(subname.c_str()); + return Data::noElementName(subname.c_str()); } const char *SubObjectT::getElementName() const { - return Data::ComplexGeoData::findElementName(subname.c_str()); + return Data::findElementName(subname.c_str()); } std::string SubObjectT::getNewElementName() const { diff --git a/src/App/ElementMap.cpp b/src/App/ElementMap.cpp new file mode 100644 index 0000000000..6e7c99ab58 --- /dev/null +++ b/src/App/ElementMap.cpp @@ -0,0 +1,1158 @@ + +#include "ElementMap.h" +#include "ElementNamingUtils.h" + +#include "App/Application.h" +#include "Base/Console.h" + +// #include +#include +#include + +#include +#include + + +FC_LOG_LEVEL_INIT("ElementMap", true, 2); + +namespace Data +{ + + +// Because the existence of hierarchical element maps, for the same document +// we may store an element map more than once in multiple objects. And because +// we may want to support partial loading, we choose to tolerate such redundancy +// for now. +// +// In order to not waste memory space when the file is loaded, we use the +// following two maps to assign a one-time id for each unique element map. The +// id will be saved together with the element map. +// +// When restoring, we'll read back the id and lookup for an existing element map +// with the same id, and skip loading the current map if one is found. +// +// TODO: Note that the same redundancy can be found when saving OCC shapes, +// because we currently save shapes for each object separately. After restoring, +// any shape sharing is lost. But again, we do want to keep separate shape files +// because of partial loading. The same technique used here can be applied to +// restore shape sharing. +static std::unordered_map _ElementMapToId; +static std::unordered_map _IdToElementMap; + + +void ElementMap::init() { + static bool inited; + if (!inited) { + inited = true; + ::App::GetApplication().signalStartSaveDocument.connect( + [](const ::App::Document&, const std::string&) { + _ElementMapToId.clear(); + }); + ::App::GetApplication().signalFinishSaveDocument.connect( + [](const ::App::Document&, const std::string&) { + _ElementMapToId.clear(); + }); + ::App::GetApplication().signalStartRestoreDocument.connect([](const ::App::Document&) { + _IdToElementMap.clear(); + }); + ::App::GetApplication().signalFinishRestoreDocument.connect([](const ::App::Document&) { + _IdToElementMap.clear(); + }); + } +} + +ElementMap::ElementMap() +{ + init(); +} + + +void ElementMap::beforeSave(const ::App::StringHasherRef& hasher) const +{ + unsigned& id = _ElementMapToId[this]; + if (!id) + id = _ElementMapToId.size(); + this->_id = id; + + for (auto& v : this->indexedNames) { + for (const MappedNameRef& ref : v.second.names) { + for (const MappedNameRef* r = &ref; r; r = r->next.get()) { + for (const ::App::StringIDRef& sid : r->sids) { + if (sid.isFromSameHasher(hasher)) + sid.mark(); + } + } + } + for (auto& vv : v.second.children) { + if (vv.second.elementMap) + vv.second.elementMap->beforeSave(hasher); + for (auto& sid : vv.second.sids) { + if (sid.isFromSameHasher(hasher)) + sid.mark(); + } + } + } +} + +void ElementMap::save(std::ostream& s, int index, + const std::map& childMapSet, + const std::map& postfixMap) const +{ + s << "\nElementMap " << index << ' ' << this->_id << ' ' << this->indexedNames.size() << '\n'; + + for (auto& v : this->indexedNames) { + s << '\n' << v.first << '\n'; + + s << "\nChildCount " << v.second.children.size() << '\n'; + for (auto& vv : v.second.children) { + auto& child = vv.second; + int mapIndex = 0; + if (child.elementMap) { + auto it = childMapSet.find(child.elementMap.get()); + if (it == childMapSet.end() || it->second == 0) + FC_ERR("Invalid child element map"); + else + mapIndex = it->second; + } + s << child.indexedName.getIndex() << ' ' << child.offset << ' ' << child.count << ' ' + << child.tag << ' ' << mapIndex << ' '; + s.write(child.postfix.constData(), child.postfix.size()); + s << ' ' << '0'; + for (auto& sid : child.sids) { + if (sid.isMarked()) + s << '.' << sid.value(); + } + s << '\n'; + } + + s << "\nNameCount " << v.second.names.size() << '\n'; + if (v.second.names.empty()) + continue; + + boost::io::ios_flags_saver ifs(s); + s << std::hex; + + for (auto& ref : v.second.names) { + for (auto r = &ref; r; r = r->next.get()) { + if (!r->name) + break; + + ::App::StringID::IndexID prefixid; + prefixid.id = 0; + IndexedName idx(r->name.dataBytes()); + bool printName = true; + if (idx) { + auto key = QByteArray::fromRawData(idx.getType(), qstrlen(idx.getType())); + auto it = postfixMap.find(key); + if (it != postfixMap.end()) { + s << ':' << it->second << '.' << idx.getIndex(); + printName = false; + } + } + else { + prefixid = ::App::StringID::fromString(r->name.dataBytes()); + if (prefixid.id) { + for (auto& sid : r->sids) { + if (sid.isMarked() && sid.value() == prefixid.id) { + s << '$'; + s.write(r->name.dataBytes().constData(), + r->name.dataBytes().size()); + printName = false; + break; + } + } + if (printName) + prefixid.id = 0; + } + } + if (printName) { + s << ';'; + s.write(r->name.dataBytes().constData(), r->name.dataBytes().size()); + } + + const QByteArray& postfix = r->name.postfixBytes(); + if (postfix.isEmpty()) + s << ".0"; + else { + auto it = postfixMap.find(postfix); + assert(it != postfixMap.end()); + s << '.' << it->second; + } + for (auto& sid : r->sids) { + if (sid.isMarked() && sid.value() != prefixid.id) + s << '.' << sid.value(); + } + + s << ' '; + } + s << "0\n"; + } + } + s << "\nEndMap\n"; +} + +void ElementMap::save(std::ostream& s) const +{ + std::map childMapSet; + std::vector childMaps; + std::map postfixMap; + std::vector postfixes; + + collectChildMaps(childMapSet, childMaps, postfixMap, postfixes); + + s << this->_id << " PostfixCount " << postfixes.size() << '\n'; + for (auto& p : postfixes) { + s.write(p.constData(), p.size()); + s << '\n'; + } + int index = 0; + s << "\nMapCount " << childMaps.size() << '\n'; + for (auto& elementMap : childMaps) + elementMap->save(s, ++index, childMapSet, postfixMap); +} + +ElementMapPtr ElementMap::restore(::App::StringHasherRef hasher, std::istream& s) +{ + const char* msg = "Invalid element map"; + + unsigned id; + int count = 0; + std::string tmp; + if (!(s >> id >> tmp >> count) || tmp != "PostfixCount") + FC_THROWM(Base::RuntimeError, msg); + + auto& map = _IdToElementMap[id]; + if (map) + return map; + + std::vector postfixes; + postfixes.reserve(count); + for (int i = 0; i < count; ++i) { + postfixes.emplace_back(); + s >> postfixes.back(); + } + + std::vector childMaps; + count = 0; + if (!(s >> tmp >> count) || tmp != "MapCount" || count == 0) + FC_THROWM(Base::RuntimeError, msg); + childMaps.reserve(count - 1); + for (int i = 0; i < count - 1; ++i) { + childMaps.push_back( + std::make_shared()->restore(hasher, s, childMaps, postfixes)); + } + + return restore(hasher, s, childMaps, postfixes); +} + +ElementMapPtr ElementMap::restore(::App::StringHasherRef hasher, std::istream& s, + std::vector& childMaps, + const std::vector& postfixes) +{ + const char* msg = "Invalid element map"; + std::string tmp; + int index = 0; + int typeCount = 0; + unsigned id = 0; + if (!(s >> tmp >> index >> id >> typeCount) || tmp != "ElementMap") + FC_THROWM(Base::RuntimeError, msg); + + auto& map = _IdToElementMap[id]; + if (map) { + do { + if (!std::getline(s, tmp)) + FC_THROWM(Base::RuntimeError, "unexpected end of child element map"); + } while (tmp != "EndMap"); + return map; + } + map = shared_from_this(); //FIXME does nothing? + + const char* hasherWarn = nullptr; + const char* hasherIDWarn = nullptr; + const char* postfixWarn = nullptr; + const char* childSIDWarn = nullptr; + std::vector tokens; + + for (int i = 0; i < typeCount; ++i) { + int count; + if (!(s >> tmp)) + FC_THROWM(Base::RuntimeError, "missing element type"); + IndexedName idx(tmp.c_str(), 1); + + if (!(s >> tmp >> count) || tmp != "ChildCount") + FC_THROWM(Base::RuntimeError, "missing element child count"); + + auto& indices = this->indexedNames[idx.getType()]; + for (int j = 0; j < count; ++j) { + int cindex; + int offset; + int count; + long tag; + int mapIndex; + if (!(s >> cindex >> offset >> count >> tag >> mapIndex >> tmp)) + FC_THROWM(Base::RuntimeError, "Invalid element child"); + if (cindex < 0) + FC_THROWM(Base::RuntimeError, "Invalid element child index"); + if (offset < 0) + FC_THROWM(Base::RuntimeError, "Invalid element child offset"); + if (mapIndex >= index || mapIndex < 0 || mapIndex > (int)childMaps.size()) + FC_THROWM(Base::RuntimeError, "Invalid element child map index"); + auto& child = indices.children[cindex + offset + count]; + child.indexedName = IndexedName::fromConst(idx.getType(), cindex); + child.offset = offset; + child.count = count; + child.tag = tag; + if (mapIndex > 0) + child.elementMap = childMaps[mapIndex - 1]; + else + child.elementMap = nullptr; + child.postfix = tmp.c_str(); + this->childElements[child.postfix].childMap = &child; + this->childElementSize += child.count; + + if (!(s >> tmp)) + FC_THROWM(Base::RuntimeError, "Invalid element child string id"); + + tokens.clear(); + boost::split(tokens, tmp, boost::is_any_of(".")); + if (tokens.size() > 1) { + child.sids.reserve(tokens.size() - 1); + for (unsigned k = 1; k < tokens.size(); ++k) { + // The element child string ID is saved as decimal + // instead of hex by accident. To simplify maintenance + // of backward compatibility, it is not corrected, and + // just restored as decimal here. + // + // long n = strtol(tokens[k].c_str(), nullptr, 16); + long n = strtol(tokens[k].c_str(), nullptr, 10); + auto sid = hasher->getID(n); + if (!sid) + childSIDWarn = "Missing element child string id"; + else + child.sids.push_back(sid); + } + } + } + + if (!(s >> tmp >> count) || tmp != "NameCount") + FC_THROWM(Base::RuntimeError, "missing element name count"); + + boost::io::ios_flags_saver ifs(s); + s >> std::hex; + + indices.names.resize(count); + for (int j = 0; j < count; ++j) { + idx.setIndex(j); + auto* ref = &indices.names[j]; + int k = 0; + while (1) { + if (!(s >> tmp)) + FC_THROWM(Base::RuntimeError, "Failed to read element name"); + if (tmp == "0") + break; + if (k++ != 0) { + ref->next.reset(new MappedNameRef); + ref = ref->next.get(); + } + tokens.clear(); + boost::split(tokens, tmp, boost::is_any_of(".")); + if (tokens.size() < 2) + FC_THROWM(Base::RuntimeError, "Invalid element entry"); + + int offset = 1; + ::App::StringID::IndexID prefixid; + prefixid.id = 0; + + switch (tokens[0][0]) { + case ':': { + if (tokens.size() < 3) + FC_THROWM(Base::RuntimeError, "Invalid element entry"); + ++offset; + long n = strtol(tokens[0].c_str() + 1, nullptr, 16); + if (n <= 0 || n > (int)postfixes.size()) + FC_THROWM(Base::RuntimeError, "Invalid element name index"); + long m = strtol(tokens[1].c_str(), nullptr, 16); + ref->name = MappedName(IndexedName::fromConst(postfixes[n - 1].c_str(), m)); + break; + } + case '$': + ref->name = MappedName(tokens[0].c_str() + 1); + prefixid = ::App::StringID::fromString(ref->name.dataBytes()); + break; + case ';': + ref->name = MappedName(tokens[0].c_str() + 1); + break; + default: + FC_THROWM(Base::RuntimeError, "Invalid element name marker"); + } + + if (tokens[offset] != "0") { + long n = strtol(tokens[offset].c_str(), nullptr, 16); + if (n <= 0 || n > (int)postfixes.size()) + postfixWarn = "Invalid element postfix index"; + else + ref->name += postfixes[n - 1]; + } + + this->mappedNames.emplace(ref->name, idx); + + if (!hasher) { + if (offset + 1 < (int)tokens.size()) + hasherWarn = "No hasher"; + continue; + } + + ref->sids.reserve(tokens.size() - offset - 1 + prefixid.id ? 1 : 0); + if (prefixid.id) { + auto sid = hasher->getID(prefixid.id); + if (!sid) + hasherIDWarn = "Missing element name prefix id"; + else + ref->sids.push_back(sid); + } + for (int l = offset + 1; l < (int)tokens.size(); ++l) { + long id = strtol(tokens[l].c_str(), nullptr, 16); + auto sid = hasher->getID(id); + if (!sid) + hasherIDWarn = "Invalid element name string id"; + else + ref->sids.push_back(sid); + } + } + } + } + if (hasherWarn) + FC_WARN(hasherWarn); + if (hasherIDWarn) + FC_WARN(hasherIDWarn); + if (postfixWarn) + FC_WARN(postfixWarn); + if (childSIDWarn) + FC_WARN(childSIDWarn); + + if (!(s >> tmp) || tmp != "EndMap") + FC_THROWM(Base::RuntimeError, "unexpected end of child element map"); + + return shared_from_this(); +} + +MappedName ElementMap::addName(MappedName& name, const IndexedName& idx, const ElementIDRefs& sids, + bool overwrite, IndexedName* existing) +{ + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + if (name.find("#") >= 0 && name.findTagInElementName() < 0) { + FC_ERR("missing tag postfix " << name); + } + } + do { + if (overwrite) + erase(idx); + auto ret = mappedNames.insert(std::make_pair(name, idx)); + if (ret.second) { // element just inserted did not exist yet in the map + ret.first->first.compact();// FIXME see MappedName.cpp + mappedRef(idx).append(ret.first->first, sids); + FC_TRACE(idx << " -> " << name); + return ret.first->first; + } + if (ret.first->second == idx) { + FC_TRACE("duplicate " << idx << " -> " << name); + return ret.first->first; + } + if (!overwrite) { + if (existing) + *existing = ret.first->second; + return MappedName(); + } + + erase(ret.first->first); + } while (true); +} + +void ElementMap::addPostfix(const QByteArray& postfix, std::map& postfixMap, + std::vector& postfixes) +{ + if (postfix.isEmpty()) + return; + auto res = postfixMap.insert(std::make_pair(postfix, 0)); + if (res.second) { + postfixes.push_back(postfix); + res.first->second = (int)postfixes.size(); + } +} + +MappedName ElementMap::setElementName(const IndexedName& element, + const MappedName& name, + long masterTag, + const ElementIDRefs* sid, + bool overwrite) +{ + if (!element) + throw Base::ValueError("Invalid input"); + if (!name) { + erase(element); + return MappedName(); + } + + for (int i = 0, count = name.size(); i < count; ++i) { + char c = name[i]; + if (c == '.' || std::isspace((int)c)) + FC_THROWM(Base::RuntimeError, "Illegal character in mapped name: " << name); + } + for (const char* s = element.getType(); *s; ++s) { + char c = *s; + if (c == '.' || std::isspace((int)c)) + FC_THROWM(Base::RuntimeError, "Illegal character in element name: " << element); + } + + ElementIDRefs _sid; + if (!sid) + sid = &_sid; + + std::ostringstream ss; + Data::MappedName n(name); + for (int i = 0;;) { + IndexedName existing; + MappedName res = this->addName(n, element, *sid, overwrite, &existing); + if (res) + return res; + if (++i == 100) { + FC_ERR("unresolved duplicate element mapping '" << name << ' ' << element << '/' + << existing); + return name; + } + if (sid != &_sid) + _sid = *sid; + n = renameDuplicateElement(i, element, existing, name, _sid, masterTag); + if (!n) + return name; + sid = &_sid; + } +} + +// try to hash element name while preserving the source tag +void ElementMap::encodeElementName(char element_type, MappedName& name, std::ostringstream& ss, + ElementIDRefs* sids, long masterTag, const char* postfix, + long tag, bool forceTag) const +{ + if (postfix && postfix[0]) { + if (!boost::starts_with(postfix, ELEMENT_MAP_PREFIX)) + ss << ELEMENT_MAP_PREFIX; + ss << postfix; + } + long inputTag = 0; + if (!forceTag && !ss.tellp()) { + if (!tag || tag == masterTag) + return; + name.findTagInElementName(&inputTag, nullptr, nullptr, nullptr, true); + if (inputTag == tag) + return; + } + else if (!tag || (!forceTag && tag == masterTag)) { + int pos = name.findTagInElementName(&inputTag, nullptr, nullptr, nullptr, true); + if (inputTag) { + tag = inputTag; + // About to encode the same tag used last time. This usually means + // the owner object is doing multi step modeling. Let's not + // recursively encode the same tag too many time. It will be a + // waste of memory, because the intermediate shapes has no + // corresponding objects, so no real value for history tracing. + // + // On the other hand, we still need to distinguish the original name + // from the input object from the element name of the intermediate + // shapes. So we limit ourselves to encode only one extra level + // using the same tag. In order to do that, we need to dehash the + // previous level name, and check for its tag. + Data::MappedName n(name, 0, pos); + Data::MappedName prev = dehashElementName(n); + long prevTag = 0; + prev.findTagInElementName(&prevTag, nullptr, nullptr, nullptr, true); + if (prevTag == inputTag || prevTag == -inputTag) + name = n; + } + } + + if (sids && this->hasher) { + name = hashElementName(name, *sids); + if (!forceTag && !tag && ss.tellp()) + forceTag = true; + } + if (forceTag || tag) { + assert(element_type); + int pos = ss.tellp(); + boost::io::ios_flags_saver ifs(ss); + ss << POSTFIX_TAG << std::hex; + if (tag < 0) + ss << '-' << -tag; + else if (tag) + ss << tag; + assert(pos >= 0); + if (pos != 0) + ss << ':' << pos; + ss << ',' << element_type; + } + name += ss.str(); +} + +MappedName ElementMap::hashElementName(const MappedName& name, ElementIDRefs& sids) const +{ + if (!this->hasher || !name) + return name; + if (name.find(ELEMENT_MAP_PREFIX) < 0) + return name; + App::StringIDRef sid = this->hasher->getID(name, sids); + const auto& related = sid.relatedIDs(); + if (related == sids) { + sids.clear(); + sids.push_back(sid); + } + else { + ElementIDRefs tmp; + tmp.push_back(sid); + for (auto& s : sids) { + if (related.indexOf(s) < 0) + tmp.push_back(s); + } + sids = tmp; + } + return MappedName(sid.toString()); +} + +MappedName ElementMap::dehashElementName(const MappedName& name) const +{ + if (name.empty()) + return name; + if (!this->hasher) + return name; + auto id = App::StringID::fromString(name.toRawBytes()); + if (!id) + return name; + auto sid = this->hasher->getID(id); + if (!sid) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_TRACE)) + FC_WARN("failed to find hash id " << id); + else + FC_LOG("failed to find hash id " << id); + return name; + } + if (sid.isHashed()) { + FC_LOG("cannot dehash id " << id); + return name; + } + MappedName ret( + sid.toString());// FIXME .toString() was missing in original function. is this correct? + FC_TRACE("dehash " << name << " -> " << ret); + return ret; +} + +MappedName ElementMap::renameDuplicateElement(int index, const IndexedName& element, + const IndexedName& element2, const MappedName& name, + ElementIDRefs& sids, long masterTag) +{ + int idx; +#ifdef FC_DEBUG + idx = index; +#else + static std::random_device _RD; + static std::mt19937 _RGEN(_RD()); + static std::uniform_int_distribution<> _RDIST(1,10000); + (void)index; + idx = _RDIST(_RGEN); +#endif + std::ostringstream ss; + ss << ELEMENT_MAP_PREFIX << 'D' << std::hex << idx; + MappedName renamed(name); + encodeElementName(element.getType()[0], renamed, ss, &sids, masterTag); + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("duplicate element mapping '" << name << " -> " << renamed << ' ' << element << '/' + << element2); + return renamed; +} + +void ElementMap::erase(const MappedName& name) +{ + auto it = this->mappedNames.find(name); + if (it == this->mappedNames.end()) + return; + MappedNameRef* ref = findMappedRef(it->second); + if (!ref) + return; + ref->erase(name); + this->mappedNames.erase(it); + return; +} + +void ElementMap::erase(const IndexedName& idx) +{ + auto iter = this->indexedNames.find(idx.getType()); + if (iter == this->indexedNames.end()) + return; + auto& indices = iter->second; + if (idx.getIndex() >= (int)indices.names.size()) + return; + auto& ref = indices.names[idx.getIndex()]; + for (auto* r = &ref; r; r = r->next.get()) + this->mappedNames.erase(r->name); + ref.clear(); + return; +} + +unsigned long ElementMap::size() const +{ + return mappedNames.size() + childElementSize; +} + +bool ElementMap::empty() const +{ + return mappedNames.empty() && childElementSize == 0; +} + +IndexedName ElementMap::find(const MappedName& name, ElementIDRefs* sids) const +{ + auto it = mappedNames.find(name); + if (it == mappedNames.end()) { + if (childElements.isEmpty()) + return IndexedName(); + + int len = 0; + if (name.findTagInElementName(nullptr, &len, nullptr, nullptr, false, false) < 0) + return IndexedName(); + QByteArray key = name.toRawBytes(len); + auto it = this->childElements.find(key); + if (it == this->childElements.end()) + return IndexedName(); + + const auto& child = *it.value().childMap; + IndexedName res; + + MappedName childName = MappedName::fromRawData(name, 0, len); + if (child.elementMap) + res = child.elementMap->find(childName, sids); + else + res = childName.toIndexedName(); + + if (res && boost::equals(res.getType(), child.indexedName.getType()) + && child.indexedName.getIndex() <= res.getIndex() + && child.indexedName.getIndex() + child.count > res.getIndex()) { + res.setIndex(res.getIndex() + it.value().childMap->offset); + return res; + } + + return IndexedName(); + } + + if (sids) { + const MappedNameRef* ref = findMappedRef(it->second); + for (; ref; ref = ref->next.get()) { + if (ref->name == name) { + if (!sids->size()) + *sids = ref->sids; + else + *sids += ref->sids; + break; + } + } + } + return it->second; +} + +MappedName ElementMap::find(const IndexedName& idx, ElementIDRefs* sids) const +{ + if (!idx) + return MappedName(); + + auto iter = this->indexedNames.find(idx.getType()); + if (iter == this->indexedNames.end()) + return MappedName(); + + auto& indices = iter->second; + if (idx.getIndex() < (int)indices.names.size()) { + const MappedNameRef& ref = indices.names[idx.getIndex()]; + if (ref.name) { + if (sids) { + if (!sids->size()) + *sids = ref.sids; + else + *sids += ref.sids; + } + return ref.name; + } + } + + auto it = indices.children.upper_bound(idx.getIndex()); + if (it != indices.children.end() + && it->second.indexedName.getIndex() + it->second.offset <= idx.getIndex()) { + auto& child = it->second; + MappedName name; + IndexedName childIdx(idx.getType(), idx.getIndex() - child.offset); + if (child.elementMap) + name = child.elementMap->find(childIdx, sids); + else + name = MappedName(childIdx); + if (name) { + name += child.postfix; + return name; + } + } + return MappedName(); +} + +std::vector> ElementMap::findAll(const IndexedName& idx) const +{ + std::vector> res; + if (!idx) + return res; + + auto iter = this->indexedNames.find(idx.getType()); + if (iter == this->indexedNames.end()) + return res; + + auto& indices = iter->second; + if (idx.getIndex() < (int)indices.names.size()) { + const MappedNameRef& ref = indices.names[idx.getIndex()]; + int count = 0; + for (auto r = &ref; r; r = r->next.get()) { + if (r->name) + ++count; + } + if (count) { + res.reserve(count); + for (auto r = &ref; r; r = r->next.get()) { + if (r->name) + res.emplace_back(r->name, r->sids); + } + return res; + } + } + + auto it = indices.children.upper_bound(idx.getIndex()); + if (it != indices.children.end() + && it->second.indexedName.getIndex() + it->second.offset <= idx.getIndex()) { + auto& child = it->second; + IndexedName childIdx(idx.getType(), idx.getIndex() - child.offset); + if (child.elementMap) { + res = child.elementMap->findAll(childIdx); + for (auto& v : res) + v.first += child.postfix; + } + else + res.emplace_back(MappedName(childIdx) + child.postfix, ElementIDRefs()); + } + + return res; +} + +const MappedNameRef* ElementMap::findMappedRef(const IndexedName& idx) const +{ + auto iter = this->indexedNames.find(idx.getType()); + if (iter == this->indexedNames.end()) + return nullptr; + auto& indices = iter->second; + if (idx.getIndex() >= (int)indices.names.size()) + return nullptr; + return &indices.names[idx.getIndex()]; +} + +MappedNameRef* ElementMap::findMappedRef(const IndexedName& idx) +{ + auto iter = this->indexedNames.find(idx.getType()); + if (iter == this->indexedNames.end()) + return nullptr; + auto& indices = iter->second; + if (idx.getIndex() >= (int)indices.names.size()) + return nullptr; + return &indices.names[idx.getIndex()]; +} + +MappedNameRef& ElementMap::mappedRef(const IndexedName& idx) +{ + assert(idx); + auto& indices = this->indexedNames[idx.getType()]; + if (idx.getIndex() >= (int)indices.names.size()) + indices.names.resize(idx.getIndex() + 1); + return indices.names[idx.getIndex()]; +} + +bool ElementMap::hasChildElementMap() const +{ + return !childElements.empty(); +} + +void ElementMap::hashChildMaps(long masterTag) +{ + if (childElements.empty() || !this->hasher) + return; + std::ostringstream ss; + for (auto& indexedNameIndexedElements : this->indexedNames) { + for (auto& indexedChild : indexedNameIndexedElements.second.children) { + auto& child = indexedChild.second; + int len = 0; + long tag; + int pos = MappedName::fromRawData(child.postfix) + .findTagInElementName(&tag, &len, nullptr, nullptr, false, false); + if (pos > 10) { + MappedName postfix = hashElementName( + MappedName::fromRawData(child.postfix.constData(), pos), child.sids); + ss.str(""); + ss << MAPPED_CHILD_ELEMENTS_PREFIX << postfix; + MappedName tmp; + encodeElementName( + child.indexedName[0], tmp, ss, nullptr, masterTag, nullptr, child.tag, true); + this->childElements.remove(child.postfix); + child.postfix = tmp.toBytes(); + this->childElements[child.postfix].childMap = &child; + } + } + } +} + +void ElementMap::collectChildMaps(std::map& childMapSet, + std::vector& childMaps, + std::map& postfixMap, + std::vector& postfixes) const +{ + auto res = childMapSet.insert(std::make_pair(this, 0)); + if (!res.second) + return; + + for (auto& v : this->indexedNames) { + addPostfix(QByteArray::fromRawData(v.first, qstrlen(v.first)), postfixMap, postfixes); + + for (auto& vv : v.second.children) { + auto& child = vv.second; + if (child.elementMap) + child.elementMap->collectChildMaps(childMapSet, childMaps, postfixMap, postfixes); + } + } + + for (auto& v : this->mappedNames) + addPostfix(v.first.constPostfix(), postfixMap, postfixes); + + childMaps.push_back(this); + res.first->second = (int)childMaps.size(); +} + +void ElementMap::addChildElements(long masterTag, const std::vector& children) +{ + std::ostringstream ss; + ss << std::hex; + + // To avoid possibly very long recursive child map lookup, resulting very + // long mapped names, we try to resolve the grand child map now. + std::vector expansion; + for (auto it = children.begin(); it != children.end(); ++it) { + auto& child = *it; + if (!child.elementMap || child.elementMap->childElements.empty()) { + if (expansion.size()) + expansion.push_back(child); + continue; + } + auto& indices = child.elementMap->indexedNames[child.indexedName.getType()]; + if (indices.children.empty()) { + if (expansion.size()) + expansion.push_back(child); + continue; + } + + // Note that it is allow to have both mapped names and child map. We + // may have to split the current child mapping into pieces. + + int start = child.indexedName.getIndex(); + int end = start + child.count; + for (auto iter = indices.children.upper_bound(start); iter != indices.children.end(); + ++iter) { + auto& grandchild = iter->second; + int istart = grandchild.indexedName.getIndex() + grandchild.offset; + int iend = istart + grandchild.count; + if (end <= istart) + break; + if (istart >= end) { + if (expansion.size()) { + expansion.push_back(child); + expansion.back().indexedName.setIndex(start); + expansion.back().count = end - start; + } + break; + } + if (expansion.empty()) { + expansion.reserve(children.size() + 10); + expansion.insert(expansion.end(), children.begin(), it); + } + expansion.push_back(child); + auto* entry = &expansion.back(); + if (istart > start) { + entry->indexedName.setIndex(start); + entry->count = istart - start; + + expansion.push_back(child); + entry = &expansion.back(); + } + else + istart = start; + + if (iend > end) + iend = end; + + entry->indexedName.setIndex(istart - grandchild.offset); + entry->count = iend - istart; + entry->offset += grandchild.offset; + entry->elementMap = grandchild.elementMap; + entry->sids += grandchild.sids; + if (grandchild.postfix.size()) { + if (entry->postfix.size() + && !entry->postfix.startsWith(ELEMENT_MAP_PREFIX)) { + entry->postfix = + grandchild.postfix + ELEMENT_MAP_PREFIX + entry->postfix; + } + else + entry->postfix = grandchild.postfix + entry->postfix; + } + + start = iend; + if (start >= end) + break; + } + if (expansion.size() && start < end) { + expansion.push_back(child); + expansion.back().indexedName.setIndex(start); + expansion.back().count = end - start; + } + } + + for (auto& child : expansion.size() ? expansion : children) { + if (!child.indexedName || !child.count) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_ERR("invalid mapped child element"); + continue; + } + + ss.str(""); + MappedName tmp; + + ChildMapInfo* entry = nullptr; + + // do child mapping only if the child element count >= 5 + if (child.count >= 5 || !child.elementMap) { + encodeElementName(child.indexedName[0], + tmp, + ss, + nullptr, + masterTag, + child.postfix.constData(), + child.tag, + true); + + // Perform some disambiguation in case the same shape is mapped + // multiple times, e.g. draft array. + entry = &childElements[tmp.toBytes()]; + int mapIndex = entry->mapIndices[child.elementMap.get()]++; + ++entry->index; + if (entry->index != 1 && child.elementMap && mapIndex == 0) { + // This child has duplicated 'tag' and 'postfix', but it + // has its own element map. We'll expand this map now. + entry = nullptr; + } + } + + if (!entry) { + IndexedName childIdx(child.indexedName); + IndexedName idx(childIdx.getType(), childIdx.getIndex() + child.offset); + for (int i = 0; i < child.count; ++i, ++childIdx, ++idx) { + ElementIDRefs sids; + MappedName name = child.elementMap->find(childIdx, &sids); + if (!name) { + if (!child.tag || child.tag == masterTag) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("unmapped element"); + continue; + } + name = MappedName(childIdx); + } + ss.str(""); + encodeElementName( + idx[0], name, ss, &sids, masterTag, child.postfix.constData(), child.tag); + setElementName(idx, name, masterTag, &sids); + } + continue; + } + + if (entry->index != 1) { + // There is some ambiguity in child mapping. We need some + // additional postfix for disambiguation. NOTE: We are not + // using ComplexGeoData::indexPostfix() so as to not confuse + // other code that actually uses this postfix for indexing + // purposes. Here, we just need some postfix for + // disambiguation. We don't need to extract the index. + ss.str(""); + ss << ELEMENT_MAP_PREFIX << ":C" << entry->index - 1; + + tmp.clear(); + encodeElementName(child.indexedName[0], + tmp, + ss, + nullptr, + masterTag, + child.postfix.constData(), + child.tag, + true); + + entry = &childElements[tmp.toBytes()]; + if (entry->childMap) { + FC_ERR("duplicate mapped child element"); + continue; + } + } + + auto& indices = this->indexedNames[child.indexedName.getType()]; + auto res = indices.children.emplace( + child.indexedName.getIndex() + child.offset + child.count, child); + if (!res.second) { + if (!entry->childMap) + this->childElements.remove(tmp.toBytes()); + FC_ERR("duplicate mapped child element"); + continue; + } + + auto& insertedChild = res.first->second; + insertedChild.postfix = tmp.toBytes(); + entry->childMap = &insertedChild; + childElementSize += insertedChild.count; + } +} + +std::vector ElementMap::getChildElements() const +{ + std::vector res; + res.reserve(this->childElements.size()); + for (auto& v : this->childElements) + res.push_back(*v.childMap); + return res; +} + +std::vector ElementMap::getAll() const +{ + std::vector ret; + ret.reserve(size()); + for (auto& v : this->mappedNames) + ret.emplace_back(v.first, v.second); + for (auto& v : this->childElements) { + auto& child = *v.childMap; + IndexedName idx(child.indexedName); + idx.setIndex(idx.getIndex() + child.offset); + IndexedName childIdx(child.indexedName); + for (int i = 0; i < child.count; ++i, ++idx, ++childIdx) { + MappedName name; + if (child.elementMap) + name = child.elementMap->find(childIdx); + else + name = MappedName(childIdx); + if (name) { + name += child.postfix; + ret.emplace_back(name, idx); + } + } + } + return ret; +} + + +}// Namespace Data diff --git a/src/App/ElementMap.h b/src/App/ElementMap.h index e49de60048..ffe4eda642 100644 --- a/src/App/ElementMap.h +++ b/src/App/ElementMap.h @@ -27,22 +27,264 @@ #define DATA_ELEMENTMAP_H #include "FCGlobal.h" -#include "IndexedName.h" -namespace Data { +#include "Application.h" +#include "MappedElement.h" +#include "StringHasher.h" -static constexpr const char *POSTFIX_TAG = ";:H"; -static constexpr const char *POSTFIX_DECIMAL_TAG = ";:T"; -static constexpr const char *POSTFIX_EXTERNAL_TAG = ";:X"; -static constexpr const char *POSTFIX_CHILD = ";:C"; -static constexpr const char *POSTFIX_INDEX = ";:I"; -static constexpr const char *POSTFIX_UPPER = ";:U"; -static constexpr const char *POSTFIX_LOWER = ";:L"; -static constexpr const char *POSTFIX_MOD = ";:M"; -static constexpr const char *POSTFIX_GEN = ";:G"; -static constexpr const char *POSTFIX_MODGEN = ";:MG"; -static constexpr const char *POSTFIX_DUPLICATE = ";D"; +#include +#include +#include +#include +#include -} // namespace data -#endif // DATA_ELEMENTMAP_H +namespace Data +{ + +class ElementMap; +typedef std::shared_ptr ElementMapPtr; + +/* This class provides for ComplexGeoData's ability to provide proper naming. + * Specifically, ComplexGeoData uses this class for it's `_id` property. + * Most of the operations work with the `indexedNames` and `mappedNames` maps. + * `indexedNames` maps a string to both a name queue and children. + * each of those children store an IndexedName, offset details, postfix, ids, and + * possibly a recursive elementmap + * `mappedNames` maps a MappedName to a specific IndexedName. + */ +class AppExport ElementMap: public std::enable_shared_from_this //TODO can remove shared_from_this? +{ +public: + /** Default constructor: hooks internal functions to \c signalSaveDocument and + * \c signalStartRestoreDocument. This is related to the save and restore process + * of the map. + */ + ElementMap(); + + /** Ensures that naming is properly assigned. It then marks as "used" all the StringID + * that are used to make up this particular map and are stored in the hasher passed + * as a parameter. Finally do this recursively for all childEelementMaps as well. + * + * @param hasher where all the StringID needed to build the map are stored. + */ + // FIXME this should be made part of \c save, to achieve symmetry with the restore method + void beforeSave(const ::App::StringHasherRef& hasher) const; + + /** Serialize this map. Calls \c collectChildMaps to get \c childMapSet and + * \c postfixMap, then calls the other (private) save function with those parameters. + * @param s: serialized stream + */ + void save(std::ostream& s) const; + + /** Deserialize and restore this map. This function restores \c childMaps and + * \c postfixes from the stream, then calls the other (private) restore function with those + * parameters. + * @param hasher: where all the StringIDs are stored + * @param s: stream to deserialize + */ + ElementMapPtr restore(::App::StringHasherRef hasher, std::istream& s); + + + /** Add a sub-element name mapping. + * + * @param element: the original \c Type + \c Index element name + * @param name: the mapped sub-element name. May or may not start with + * elementMapPrefix(). + * @param sid: in case you use a hasher to hash the element name, pass in + * the string id reference using this parameter. You can have more than one + * string id associated with the same name. + * @param overwrite: if true, it will overwrite existing names + * + * @return Returns the stored mapped element name. + * + * An element can have multiple mapped names. However, a name can only be + * mapped to one element + * + * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access, + * now you must pass in `long masterTag` explicitly. + */ + MappedName setElementName(const IndexedName& element, + const MappedName& name, + long masterTag, + const ElementIDRefs* sid = nullptr, + bool overwrite = false); + + /* Generates a new MappedName from the current details. + * + * The result is streamed to `ss` and stored in `name`. + * + * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access, + * now you must pass in `long masterTag` explicitly. + */ + void encodeElementName(char element_type, + MappedName& name, + std::ostringstream& ss, + ElementIDRefs* sids, + long masterTag, + const char* postfix = 0, + long tag = 0, + bool forceTag = false) const; + + /// Remove \c name from the map + void erase(const MappedName& name); + + /// Remove \c idx and all the MappedNames associated with it + void erase(const IndexedName& idx); + + unsigned long size() const; + + bool empty() const; + + IndexedName find(const MappedName& name, ElementIDRefs* sids = nullptr) const; + + MappedName find(const IndexedName& idx, ElementIDRefs* sids = nullptr) const; + + std::vector> findAll(const IndexedName& idx) const; + + // prefix searching is disabled, as TopoShape::getRelatedElement() is + // deprecated in favor of GeoFeature::getRelatedElement(). Besides, there + // is efficient way to support child element map if we were to implement + // prefix search. +#if 0 + std::vector findAllStartsWith(const char *prefix) const; +#endif + + bool hasChildElementMap() const; + + /* Ensures that for each IndexedName mapped to IndexedElements, that + * each child is properly hashed (cached). + * + * Note: the original proc was in the context of ComplexGeoData, which provided `Tag` access, + * now you must pass in `long masterTag` explicitly. + */ + void hashChildMaps(long masterTag); + + struct AppExport MappedChildElements + { + IndexedName indexedName; + int count; + int offset; + long tag; + ElementMapPtr elementMap; + QByteArray postfix; + ElementIDRefs sids; + + // prefix() has been moved to PostfixStringReferences.h + }; + + /* Note: the original addChildElements passed `ComplexGeoData& master` for getting the `Tag`, + * now it just passes `long masterTag`.*/ + void addChildElements(long masterTag, const std::vector& children); + + std::vector getChildElements() const; + + std::vector getAll() const; + +private: + /** Serialize this map + * @param s: serialized stream + * @param childMapSet: where all child element maps are stored + * @param postfixMap. where all postfixes are stored + */ + void save(std::ostream& s, int index, const std::map& childMapSet, + const std::map& postfixMap) const; + + /** Deserialize and restore this map. + * @param hasher: where all the StringIDs are stored + * @param s: stream to deserialize + * @param childMaps: where all child element maps are stored + * @param postfixes. where all postfixes are stored + */ + ElementMapPtr restore(::App::StringHasherRef hasher, std::istream& s, + std::vector& childMaps, + const std::vector& postfixes); + + /** Associate the MappedName \c name with the IndexedName \c idx. + * @param name: the name to add + * @param idx: the indexed name that \c name will be bound to + * @param sids: where StringIDs that make up the name are stored + * @param overwrite: if true, all the names associated with \c idx will be discarded + * @param existing: out variable: if not overwriting, and \c name is already + * associated with another indexedName, set \c existing to that indexedname + * @return the name just added, or an empty name if it wasn't added. + */ + MappedName addName(MappedName& name, const IndexedName& idx, const ElementIDRefs& sids, + bool overwrite, IndexedName* existing); + + /** Utility function that adds \c postfix to \c postfixMap, and to \c postfixes + * if it was not present in the map. + */ + static void addPostfix(const QByteArray& postfix, std::map& postfixMap, + std::vector& postfixes); + + /* Note: the original proc passed `ComplexGeoData& master` for getting the `Tag`, + * now it just passes `long masterTag`.*/ + virtual MappedName renameDuplicateElement(int index, const IndexedName& element, + const IndexedName& element2, const MappedName& name, + ElementIDRefs& sids, long masterTag); + + /** Convenience method to hash the main element name + * + * @param name: main element name + * @param sid: store any output string ID references + * @return the hashed element name; + */ + MappedName hashElementName(const MappedName& name, ElementIDRefs& sids) const; + + /// Reverse hashElementName() + MappedName dehashElementName(const MappedName& name) const; + + //FIXME duplicate code? as in copy/paste + const MappedNameRef* findMappedRef(const IndexedName& idx) const; + MappedNameRef* findMappedRef(const IndexedName& idx); + + MappedNameRef& mappedRef(const IndexedName& idx); + + void collectChildMaps(std::map& childMapSet, + std::vector& childMaps, + std::map& postfixMap, + std::vector& postfixes) const; + + struct CStringComp + { + public: + bool operator()(const char* str1, const char* str2) const + { + return std::strcmp(str1, str2) < 0; + } + }; + + struct IndexedElements + { + std::deque names; + std::map children; + }; + + std::map indexedNames; + + std::map> mappedNames; + + struct ChildMapInfo + { + int index = 0; + MappedChildElements* childMap = nullptr; + std::map mapIndices; + }; + + QHash childElements; + std::size_t childElementSize = 0; + + mutable unsigned _id = 0; + + void init(); + +public: + /// String hasher for element name shortening + App::StringHasherRef hasher; +}; + + +}// namespace Data + +#endif// DATA_ELEMENTMAP_H diff --git a/src/App/ElementNamingUtils.cpp b/src/App/ElementNamingUtils.cpp new file mode 100644 index 0000000000..c9e42a6e52 --- /dev/null +++ b/src/App/ElementNamingUtils.cpp @@ -0,0 +1,88 @@ +#include "ElementNamingUtils.h" + +#include + + +const char *Data::isMappedElement(const char *name) { + if(name && boost::starts_with(name, ELEMENT_MAP_PREFIX)) + return name + ELEMENT_MAP_PREFIX_SIZE; + return nullptr; +} + +std::string Data::newElementName(const char *name) { + if(!name) + return std::string(); + const char *dot = strrchr(name,'.'); + if(!dot || dot==name) + return name; + const char *c = dot-1; + for(;c!=name;--c) { + if(*c == '.') { + ++c; + break; + } + } + if(isMappedElement(c)) + return std::string(name,dot-name); + return name; +} + +std::string Data::oldElementName(const char *name) { + if(!name) + return std::string(); + const char *dot = strrchr(name,'.'); + if(!dot || dot==name) + return name; + const char *c = dot-1; + for(;c!=name;--c) { + if(*c == '.') { + ++c; + break; + } + } + if(isMappedElement(c)) + return std::string(name,c-name)+(dot+1); + return name; +} + +std::string Data::noElementName(const char *name) { + if(!name) + return std::string(); + auto element = findElementName(name); + if(element) + return std::string(name,element-name); + return name; +} + +const char *Data::findElementName(const char *subname) { + if(!subname || !subname[0] || isMappedElement(subname)) + return subname; + const char *dot = strrchr(subname,'.'); + if(!dot) + return subname; + const char *element = dot+1; + if(dot==subname || isMappedElement(element)) + return element; + for(--dot;dot!=subname;--dot) { + if(*dot == '.') { + ++dot; + break; + } + } + if(isMappedElement(dot)) + return dot; + return element; +} + +bool Data::hasMissingElement(const char *subname) { + if(!subname) + return false; + auto dot = strrchr(subname,'.'); + if(dot) + subname = dot+1; + return boost::starts_with(subname, MISSING_PREFIX); +} + +const char *Data::hasMappedElementName(const char *subname) { + return isMappedElement(findElementName(subname)); +} \ No newline at end of file diff --git a/src/App/ElementNamingUtils.h b/src/App/ElementNamingUtils.h new file mode 100644 index 0000000000..25b86972e9 --- /dev/null +++ b/src/App/ElementNamingUtils.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include "FCGlobal.h" + + +namespace Data +{ + +/// Special prefix to mark the beginning of a mapped sub-element name +constexpr const char* ELEMENT_MAP_PREFIX = ";"; +constexpr size_t ELEMENT_MAP_PREFIX_SIZE = 1; + +/// Special prefix to mark a missing element +constexpr const char* MISSING_PREFIX = "?"; + +// IMPORTANT: For all the constants below, the semicolon ";" +// at the start is ELEMENT_MAP_PREFIX + +constexpr const char* MAPPED_CHILD_ELEMENTS_PREFIX = ";:R"; + +/// Special postfix to mark the following tag +constexpr const char* POSTFIX_TAG = ";:H"; +constexpr size_t POSTFIX_TAG_SIZE = 3; + +constexpr const char* POSTFIX_DECIMAL_TAG = ";:T"; +constexpr const char* POSTFIX_EXTERNAL_TAG = ";:X"; +constexpr const char* POSTFIX_CHILD = ";:C"; + +/// Special postfix to mark the index of an array element +constexpr const char* POSTFIX_INDEX = ";:I"; +constexpr const char* POSTFIX_UPPER = ";:U"; +constexpr const char* POSTFIX_LOWER = ";:L"; +constexpr const char* POSTFIX_MOD = ";:M"; +constexpr const char* POSTFIX_GEN = ";:G"; +constexpr const char* POSTFIX_MODGEN = ";:MG"; +constexpr const char* POSTFIX_DUPLICATE = ";D"; + + +/// Check if a subname contains missing element +AppExport bool hasMissingElement(const char *subname); + +/** Check if the name starts with elementMapPrefix() + * + * @param name: input name + * @return Returns the name stripped with elementMapPrefix(), or 0 if not + * start with the prefix + */ +AppExport const char *isMappedElement(const char *name); + +/// Strip out the trailing element name if there is mapped element name preceeds it. +AppExport std::string newElementName(const char *name); + +/// Strip out the mapped element name if there is one. +AppExport std::string oldElementName(const char *name); + +/// Strip out the old and new element name if there is one. +AppExport std::string noElementName(const char *name); + +/// Find the start of an element name in a subname +AppExport const char *findElementName(const char *subname); + +AppExport const char *hasMappedElementName(const char *subname); + + +}// namespace Data \ No newline at end of file diff --git a/src/App/GeoFeature.cpp b/src/App/GeoFeature.cpp index 26b48f7a75..75d0d55975 100644 --- a/src/App/GeoFeature.cpp +++ b/src/App/GeoFeature.cpp @@ -27,7 +27,7 @@ #include "GeoFeature.h" #include "GeoFeatureGroupExtension.h" -#include "ComplexGeoData.h" +#include "ElementNamingUtils.h" using namespace App; @@ -101,7 +101,7 @@ DocumentObject *GeoFeature::resolveElement(DocumentObject *obj, const char *subn return nullptr; if(!subname) subname = ""; - const char *element = Data::ComplexGeoData::findElementName(subname); + const char *element = Data::findElementName(subname); if(_element) *_element = element; auto sobj = obj->getSubObject(subname); if(!sobj) @@ -114,7 +114,7 @@ DocumentObject *GeoFeature::resolveElement(DocumentObject *obj, const char *subn return nullptr; if(!element || !element[0]) { if(append) - elementName.second = Data::ComplexGeoData::oldElementName(subname); + elementName.second = Data::oldElementName(subname); return sobj; } @@ -122,7 +122,7 @@ DocumentObject *GeoFeature::resolveElement(DocumentObject *obj, const char *subn if(!append) elementName.second = element; else - elementName.second = Data::ComplexGeoData::oldElementName(subname); + elementName.second = Data::oldElementName(subname); return sobj; } if(!append) diff --git a/src/App/Link.cpp b/src/App/Link.cpp index f4483d4e28..de27dcaf41 100644 --- a/src/App/Link.cpp +++ b/src/App/Link.cpp @@ -29,7 +29,7 @@ #include #include "Application.h" -#include "ComplexGeoData.h" +#include "ElementNamingUtils.h" #include "ComplexGeoDataPy.h" #include "Document.h" #include "DocumentObserver.h" @@ -1051,7 +1051,7 @@ DocumentObject *LinkBaseExtension::getLink(int depth) const{ } int LinkBaseExtension::getArrayIndex(const char *subname, const char **psubname) { - if(!subname || Data::ComplexGeoData::isMappedElement(subname)) + if(!subname || Data::isMappedElement(subname)) return -1; const char *dot = strchr(subname,'.'); if(!dot) dot= subname+strlen(subname); @@ -1073,7 +1073,7 @@ int LinkBaseExtension::getArrayIndex(const char *subname, const char **psubname) } int LinkBaseExtension::getElementIndex(const char *subname, const char **psubname) const { - if(!subname || Data::ComplexGeoData::isMappedElement(subname)) + if(!subname || Data::isMappedElement(subname)) return -1; int idx = -1; const char *dot = strchr(subname,'.'); @@ -1313,7 +1313,7 @@ bool LinkBaseExtension::extensionGetSubObject(DocumentObject *&ret, const char * return true; ret = elements[idx]->getSubObject(subname,pyObj,mat,true,depth+1); // do not resolve the link if this element is the last referenced object - if(!subname || Data::ComplexGeoData::isMappedElement(subname) || !strchr(subname,'.')) + if(!subname || Data::isMappedElement(subname) || !strchr(subname,'.')) ret = elements[idx]; return true; } @@ -1381,7 +1381,7 @@ bool LinkBaseExtension::extensionGetSubObject(DocumentObject *&ret, const char * std::string postfix; if(ret) { // do not resolve the link if we are the last referenced object - if(subname && !Data::ComplexGeoData::isMappedElement(subname) && strchr(subname,'.')) { + if(subname && !Data::isMappedElement(subname) && strchr(subname,'.')) { if(mat) *mat = matNext; } @@ -1394,7 +1394,7 @@ bool LinkBaseExtension::extensionGetSubObject(DocumentObject *&ret, const char * } else { if(idx) { - postfix = Data::ComplexGeoData::indexPostfix(); + postfix = Data::POSTFIX_INDEX; postfix += std::to_string(idx); } if(mat) @@ -1488,7 +1488,7 @@ void LinkBaseExtension::parseSubName() const { } const auto &subs = xlink->getSubValues(); auto subname = subs.front().c_str(); - auto element = Data::ComplexGeoData::findElementName(subname); + auto element = Data::findElementName(subname); if(!element || !element[0]) { mySubName = subs[0]; if(hasSubElement) @@ -1499,7 +1499,7 @@ void LinkBaseExtension::parseSubName() const { mySubName = std::string(subname,element-subname); for(std::size_t i=1;i subset(mySubElements.begin(),mySubElements.end()); auto sub = xlink->getSubValues().front(); - auto element = Data::ComplexGeoData::findElementName(sub.c_str()); + auto element = Data::findElementName(sub.c_str()); if(element && element[0]) { subset.insert(element); sub.resize(element - sub.c_str()); diff --git a/src/App/MappedName.cpp b/src/App/MappedName.cpp index ba9b90c2ac..1f9779ef45 100644 --- a/src/App/MappedName.cpp +++ b/src/App/MappedName.cpp @@ -26,19 +26,26 @@ # include #endif -//#include - #include "MappedName.h" -using namespace Data; +#include "Base/Console.h" + +//#include +#include +#include -void MappedName::compact() +FC_LOG_LEVEL_INIT("MappedName", true, 2); + +namespace Data { + +void MappedName::compact() const { + auto self = const_cast(this); //FIXME this is a workaround for a single call in ElementMap::addName() if (this->raw) { - this->data = QByteArray(this->data.constData(), this->data.size()); - this->raw = false; + self->data = QByteArray(self->data.constData(), self->data.size()); + self->raw = false; } #if 0 @@ -51,3 +58,158 @@ void MappedName::compact() #endif } + +int MappedName::findTagInElementName(long* tag, int* len, const char* postfix, + char* type, bool negative, bool recursive) const +{ + bool hex = true; + int pos = this->rfind(POSTFIX_TAG); + + // Example name, tagPosfix == ;:H + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // ^ + // | + // pos + + if(pos < 0) { + pos = this->rfind(POSTFIX_DECIMAL_TAG); + if (pos < 0) + return -1; + hex = false; + } + int offset = pos + (int)POSTFIX_TAG_SIZE; + long _tag = 0; + int _len = 0; + char sep = 0; + char sep2 = 0; + char tp = 0; + char eof = 0; + + int size; + const char *s = this->toConstString(offset, size); + + // check if the number followed by the tagPosfix is negative + bool isNegative = (s[0] == '-'); + if (isNegative) { + ++s; + --size; + } + boost::iostreams::stream iss(s, size); + if (!hex) { + // no hex is an older version of the encoding scheme + iss >> _tag >> sep; + } else { + // The purpose of tag postfix is to encode one model operation. The + // 'tag' field is used to record the own object ID of that model shape, + // and the 'len' field indicates the length of the operation codes + // before the tag postfix. These fields are in hex. The trailing 'F' is + // the shape type of this element, 'F' for face, 'E' edge, and 'V' vertex. + // + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // | | ^^ ^^ + // | | | | + // ---len = 0x10--- tag len + + iss >> std::hex; + // _tag field can be skipped, if it is 0 + if (s[0] == ',' || s[0] == ':') + iss >> sep; + else + iss >> _tag >> sep; + } + + if (isNegative) + _tag = -_tag; + + if (sep == ':') { + // ':' is followed by _len field. + // + // For decTagPostfix() (i.e. older encoding scheme), this is the length + // of the string before the entire postfix (A postfix may contain + // multiple segments usually separated by ELEMENT_MAP_PREFIX. + // + // For newer POSTFIX_TAG, this counts the number of characters that + // proceeds this tag postfix segment that forms the op code (see + // example above). + // + // The reason of this change is so that the postfix can stay the same + // regardless of the prefix, which can increase memory efficiency. + // + iss >> _len >> sep2 >> tp >> eof; + + // The next separator to look for is either ':' for older tag postfix, or ',' + if (!hex && sep2 == ':') + sep2 = ','; + } + else if (hex && sep == ',') { + // ',' is followed by a single character that indicates the element type. + iss >> tp >> eof; + sep = ':'; + sep2 = ','; + } + + if (_len < 0 || sep != ':' || sep2 != ',' || tp == 0 || eof != 0) + return -1; + + if (hex) { + if (pos-_len < 0) + return -1; + if (_len && recursive && (tag || len)) { + // in case of recursive tag postfix (used by hierarchy element + // map), look for any embedded tag postifx + int next = MappedName::fromRawData(*this, pos-_len, _len).rfind(POSTFIX_TAG); + if (next >= 0) { + next += pos - _len; + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // ^ ^ + // | | + // next pos + // + // There maybe other operation codes after this embedded tag + // postfix, search for the sperator. + // + int end; + if (pos == next) + end = -1; + else + end = MappedName::fromRawData(*this, next+1, pos-next-1).find(ELEMENT_MAP_PREFIX); + if (end >= 0) { + end += next+1; + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // ^ + // | + // end + _len = pos - end; + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // | | + // -- len -- + } else + _len = 0; + } + } + + // Now convert the 'len' field back to the length of the remaining name + // + // #94;:G0;XTR;:H19:8,F;:H1a,F;BND:-1:0;:H1b:10,F + // | | + // ----------- len ----------- + _len = pos - _len; + } + if(type) + *type = tp; + if(tag) { + if (_tag == 0 && recursive) + return MappedName(*this, 0, _len).findTagInElementName(tag, len, postfix, type, negative); + if(_tag>0 || negative) + *tag = _tag; + else + *tag = -_tag; + } + if(len) + *len = _len; + if(postfix) + this->toString(*postfix, pos); + return pos; +} + +} \ No newline at end of file diff --git a/src/App/MappedName.h b/src/App/MappedName.h index 8c507b4af3..184a763b87 100644 --- a/src/App/MappedName.h +++ b/src/App/MappedName.h @@ -26,15 +26,19 @@ #define APP_MAPPED_NAME_H +#include #include #include #include #include +#include #include "ComplexGeoData.h" #include "IndexedName.h" +#include "StringHasher.h" +#include "ElementNamingUtils.h" namespace Data @@ -62,8 +66,8 @@ public: if (!name) { return; } - if (boost::starts_with(name, ComplexGeoData::elementMapPrefix())) { - name += ComplexGeoData::elementMapPrefix().size(); + if (boost::starts_with(name, ELEMENT_MAP_PREFIX)) { + name += ELEMENT_MAP_PREFIX_SIZE; } data = size < 0 ? QByteArray(name) : QByteArray(name, size); @@ -78,9 +82,9 @@ public: { auto size = nameString.size(); const char* name = nameString.c_str(); - if (boost::starts_with(nameString, ComplexGeoData::elementMapPrefix())) { - name += ComplexGeoData::elementMapPrefix().size(); - size -= ComplexGeoData::elementMapPrefix().size(); + if (boost::starts_with(nameString, ELEMENT_MAP_PREFIX)) { + name += ELEMENT_MAP_PREFIX_SIZE; + size -= ELEMENT_MAP_PREFIX_SIZE; } data = QByteArray(name, static_cast(size)); } @@ -622,7 +626,7 @@ public: const char* appendToBufferWithPrefix(std::string& buf) const { if (!toIndexedName()) { - buf += ComplexGeoData::elementMapPrefix(); + buf += ELEMENT_MAP_PREFIX; } appendToBuffer(buf); return buf.c_str(); @@ -714,7 +718,7 @@ public: } /// Ensure that this data is unshared, making a copy if necessary. - void compact(); + void compact() const; /// Boolean conversion is the inverse of empty(), returning true if there is data in either the /// data or postfix, and false if there is nothing in either. @@ -786,8 +790,7 @@ public: if (!searchTarget) { return -1; } - if (startPosition < 0 - || startPosition >= this->data.size()) { + if (startPosition < 0 || startPosition >= this->data.size()) { if (startPosition >= data.size()) { startPosition -= data.size(); } @@ -887,6 +890,22 @@ public: offset); } + /// Extract tag and other information from a encoded element name + /// + /// \param tag: optional pointer to receive the extracted tag + /// \param len: optional pointer to receive the length field after the tag field. + /// This gives the length of the previous hashsed element name starting + /// from the beginning of the give element name. + /// \param postfix: optional pointer to receive the postfix starting at the found tag field. + /// \param type: optional pointer to receive the element type character + /// \param negative: return negative tag as it is. If disabled, then always return positive tag. + /// Negative tag is sometimes used for element disambiguation. + /// \param recursive: recursively find the last non-zero tag + /// + /// \return Return the end position of the tag field, or return -1 if not found. + int findTagInElementName(long* tag = 0, int* len = 0, const char* postfix = 0, char* type = 0, + bool negative = false, bool recursive = true) const; + /// Get a hash for this MappedName std::size_t hash() const { @@ -899,6 +918,110 @@ private: bool raw; }; + +typedef QVector<::App::StringIDRef> ElementIDRefs; + +struct MappedNameRef +{ + MappedName name; + ElementIDRefs sids; + std::unique_ptr next; + + MappedNameRef() = default; + + MappedNameRef(const MappedName& name, const ElementIDRefs& sids = ElementIDRefs()) + : name(name), + sids(sids) + { + compact(); + } + + MappedNameRef(const MappedNameRef& other) + : name(other.name), + sids(other.sids) + {} + + MappedNameRef(MappedNameRef&& other) + : name(std::move(other.name)), + sids(std::move(other.sids)), + next(std::move(other.next)) + {} + + MappedNameRef& operator=(MappedNameRef&& other) + { + name = std::move(other.name); + sids = std::move(other.sids); + next = std::move(other.next); + return *this; + } + + explicit operator bool() const + { + return !name.empty(); + } + + void append(const MappedName& name, const ElementIDRefs sids = ElementIDRefs()) + { + if (!name) + return; + if (!this->name) { + this->name = name; + this->sids = sids; + compact(); + return; + } + std::unique_ptr n(new MappedNameRef(name, sids)); + if (!this->next) + this->next = std::move(n); + else { + this->next.swap(n); + this->next->next = std::move(n); + } + } + + void compact() + { + if (sids.size() > 1) { + std::sort(sids.begin(), sids.end()); + sids.erase(std::unique(sids.begin(), sids.end()), sids.end()); + } + } + + bool erase(const MappedName& name) + { + if (this->name == name) { + this->name.clear(); + this->sids.clear(); + if (this->next) { + this->name = std::move(this->next->name); + this->sids = std::move(this->next->sids); + std::unique_ptr tmp; + tmp.swap(this->next); + this->next = std::move(tmp->next); + } + return true; + } + + for (std::unique_ptr* p = &this->next; *p; p = &(*p)->next) { + if ((*p)->name == name) { + std::unique_ptr tmp; + tmp.swap(*p); + *p = std::move(tmp->next); + return true; + } + } + return false; + } + + void clear() + { + this->name.clear(); + this->sids.clear(); + this->next.reset(); + } +}; + + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/Base/Writer.h b/src/Base/Writer.h index b7bf5a757b..64ed502613 100644 --- a/src/Base/Writer.h +++ b/src/Base/Writer.h @@ -30,6 +30,7 @@ #include #include #include +#include #ifdef _MSC_VER #include diff --git a/src/Gui/CommandLink.cpp b/src/Gui/CommandLink.cpp index cc4f70081c..edb69c974e 100644 --- a/src/Gui/CommandLink.cpp +++ b/src/Gui/CommandLink.cpp @@ -28,7 +28,7 @@ #endif #include -#include +#include #include #include #include @@ -294,8 +294,8 @@ void StdCmdLinkMakeRelative::activated(int) { if(!sel.pObject || !sel.pObject->getNameInDocument()) continue; auto key = std::make_pair(sel.pObject, - Data::ComplexGeoData::noElementName(sel.SubName)); - auto element = Data::ComplexGeoData::findElementName(sel.SubName); + Data::noElementName(sel.SubName)); + auto element = Data::findElementName(sel.SubName); auto &info = linkInfo[key]; info.first = sel.pResolvedObject; if(element && element[0]) diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp index ed84eb045b..a009cf99f6 100644 --- a/src/Gui/Document.cpp +++ b/src/Gui/Document.cpp @@ -36,11 +36,11 @@ #endif #include -#include #include #include #include #include +#include #include #include #include @@ -408,7 +408,7 @@ bool Document::setEdit(Gui::ViewProvider* p, int ModNum, const char *subname) d->_editSubname.clear(); if (subname) { - const char *element = Data::ComplexGeoData::findElementName(subname); + const char *element = Data::findElementName(subname); if (element) { d->_editSubname = std::string(subname,element-subname); d->_editSubElement = element; diff --git a/src/Gui/SoFCUnifiedSelection.cpp b/src/Gui/SoFCUnifiedSelection.cpp index 99e9a63e04..6356a1c797 100644 --- a/src/Gui/SoFCUnifiedSelection.cpp +++ b/src/Gui/SoFCUnifiedSelection.cpp @@ -71,7 +71,7 @@ #endif #include -#include +#include #include #include "SoFCUnifiedSelection.h" @@ -624,7 +624,7 @@ bool SoFCUnifiedSelection::setSelection(const std::vector &infos, bo objectName << ", " << subName); std::string newElement; if(subSelected) { - newElement = Data::ComplexGeoData::newElementName(subSelected); + newElement = Data::newElementName(subSelected); subSelected = newElement.c_str(); std::string nextsub; const char *next = strrchr(subSelected,'.'); diff --git a/src/Gui/TaskElementColors.cpp b/src/Gui/TaskElementColors.cpp index 8ff6d4d2d8..17b649e1b1 100644 --- a/src/Gui/TaskElementColors.cpp +++ b/src/Gui/TaskElementColors.cpp @@ -28,7 +28,7 @@ # include #endif -#include +#include #include #include "TaskElementColors.h" @@ -84,7 +84,7 @@ public: auto obj = vpParent->getObject(); editDoc = obj->getDocument()->getName(); editObj = obj->getNameInDocument(); - editSub = Data::ComplexGeoData::noElementName(editSub.c_str()); + editSub = Data::noElementName(editSub.c_str()); } } if(editDoc.empty()) { @@ -162,7 +162,7 @@ public: c.setRgbF(color.r,color.g,color.b,1.0-color.a); px.fill(c); auto item = new QListWidgetItem(QIcon(px), - QString::fromLatin1(Data::ComplexGeoData::oldElementName(v.first.c_str()).c_str()), + QString::fromLatin1(Data::oldElementName(v.first.c_str()).c_str()), ui->elementList); item->setData(Qt::UserRole,c); item->setData(Qt::UserRole+1,QString::fromLatin1(v.first.c_str())); @@ -419,7 +419,7 @@ void ElementColors::onHideSelectionClicked() { if(!subs.empty()) { for(auto &sub : subs) { if(boost::starts_with(sub,d->editSub)) { - auto name = Data::ComplexGeoData::noElementName(sub.c_str()+d->editSub.size()); + auto name = Data::noElementName(sub.c_str()+d->editSub.size()); name += ViewProvider::hiddenMarker(); d->addItem(-1,name.c_str()); } diff --git a/src/Gui/ViewProviderLink.cpp b/src/Gui/ViewProviderLink.cpp index e67067d146..c22f080676 100644 --- a/src/Gui/ViewProviderLink.cpp +++ b/src/Gui/ViewProviderLink.cpp @@ -48,7 +48,7 @@ #endif #include -#include +#include #include #include #include @@ -635,7 +635,7 @@ public: break; } // new style mapped sub-element - if(Data::ComplexGeoData::isMappedElement(dot+1)) + if(Data::isMappedElement(dot+1)) break; auto next = strchr(dot+1,'.'); if(!next) { @@ -1031,7 +1031,7 @@ void LinkView::setLinkViewObject(ViewProviderDocumentObject *vpd, subInfo.clear(); for(const auto &sub : subs) { if(sub.empty()) continue; - const char *subelement = Data::ComplexGeoData::findElementName(sub.c_str()); + const char *subelement = Data::findElementName(sub.c_str()); std::string subname = sub.substr(0,subelement-sub.c_str()); auto it = subInfo.find(subname); if(it == subInfo.end()) { diff --git a/src/Mod/Part/App/AppPartPy.cpp b/src/Mod/Part/App/AppPartPy.cpp index 7a1c791784..98960575fe 100644 --- a/src/Mod/Part/App/AppPartPy.cpp +++ b/src/Mod/Part/App/AppPartPy.cpp @@ -74,6 +74,7 @@ #include #include #include +#include #include #include #include @@ -102,7 +103,6 @@ #include "TopoShapeSolidPy.h" #include "TopoShapeWirePy.h" - #ifdef FCUseFreeType # include "FT2FC.h" #endif @@ -2286,14 +2286,14 @@ private: const char *subname; if (!PyArg_ParseTuple(args.ptr(), "s",&subname)) throw Py::Exception(); - auto element = Data::ComplexGeoData::findElementName(subname); + auto element = Data::findElementName(subname); std::string sub(subname,element-subname); Py::List list; list.append(Py::String(sub)); const char *dot = strchr(element,'.'); if(!dot) dot = element+strlen(element); - const char *mapped = Data::ComplexGeoData::isMappedElement(element); + const char *mapped = Data::isMappedElement(element); if(mapped) list.append(Py::String(std::string(mapped,dot-mapped))); else @@ -2317,8 +2317,8 @@ private: if (!subname.empty() && subname[subname.size()-1]!='.') subname += '.'; if (mapped && mapped[0]) { - if (!Data::ComplexGeoData::isMappedElement(mapped)) - subname += Data::ComplexGeoData::elementMapPrefix(); + if (!Data::isMappedElement(mapped)) + subname += Data::ELEMENT_MAP_PREFIX; subname += mapped; } if (element && element[0]) { diff --git a/src/Mod/Part/App/PartFeature.cpp b/src/Mod/Part/App/PartFeature.cpp index ff00ff8435..3453ad1515 100644 --- a/src/Mod/Part/App/PartFeature.cpp +++ b/src/Mod/Part/App/PartFeature.cpp @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -65,7 +66,6 @@ #include "PartPyCXX.h" #include "TopoShapePy.h" - using namespace Part; namespace bp = boost::placeholders; @@ -121,7 +121,7 @@ App::DocumentObject *Feature::getSubObject(const char *subname, { // having '.' inside subname means it is referencing some children object, // instead of any sub-element from ourself - if(subname && !Data::ComplexGeoData::isMappedElement(subname) && strchr(subname,'.')) + if(subname && !Data::isMappedElement(subname) && strchr(subname,'.')) return App::DocumentObject::getSubObject(subname,pyObj,pmat,transform,depth); Base::Matrix4D _mat; @@ -280,7 +280,7 @@ static TopoShape _getTopoShape(const App::DocumentObject *obj, const char *subna if(powner) *powner = nullptr; std::string _subname; - auto subelement = Data::ComplexGeoData::findElementName(subname); + auto subelement = Data::findElementName(subname); if(!needSubElement && subname) { // strip out element name if not needed if(subelement && *subelement) { @@ -413,7 +413,7 @@ static TopoShape _getTopoShape(const App::DocumentObject *obj, const char *subna continue; }else{ if(link && !link->getShowElementValue()) - shape = baseShape.makeTransform(mat,(TopoShape::indexPostfix()+childName).c_str()); + shape = baseShape.makeTransform(mat,(Data::POSTFIX_INDEX + childName).c_str()); else { shape = baseShape.makeTransform(mat); } diff --git a/src/Mod/Part/App/TopoShape.cpp b/src/Mod/Part/App/TopoShape.cpp index c6d3908c20..98a6428d0b 100644 --- a/src/Mod/Part/App/TopoShape.cpp +++ b/src/Mod/Part/App/TopoShape.cpp @@ -160,6 +160,7 @@ #endif // _PreComp_ #include +#include #include #include #include @@ -472,7 +473,7 @@ TopAbs_ShapeEnum TopoShape::shapeType(const char *type, bool silent) { } } if(!silent) { - if(Data::ComplexGeoData::hasMissingElement(type)) + if(Data::hasMissingElement(type)) FC_THROWM(Base::CADKernelError,"missing shape element: " << (type?type:"?")); FC_THROWM(Base::CADKernelError,"invalid shape type: " << (type?type:"?")); } diff --git a/src/Mod/PartDesign/App/Feature.cpp b/src/Mod/PartDesign/App/Feature.cpp index d2a2946947..d147e0d507 100644 --- a/src/Mod/PartDesign/App/Feature.cpp +++ b/src/Mod/PartDesign/App/Feature.cpp @@ -34,6 +34,7 @@ #include "App/DocumentObject.h" #include +#include #include "App/OriginFeature.h" #include @@ -237,7 +238,7 @@ Body* Feature::getFeatureBody() const { App::DocumentObject *Feature::getSubObject(const char *subname, PyObject **pyObj, Base::Matrix4D *pmat, bool transform, int depth) const { - if (subname && subname != Data::ComplexGeoData::findElementName(subname)) { + if (subname && subname != Data::findElementName(subname)) { const char * dot = strchr(subname,'.'); if (dot) { auto body = PartDesign::Body::findBodyOf(this); diff --git a/src/Mod/PartDesign/App/ShapeBinder.cpp b/src/Mod/PartDesign/App/ShapeBinder.cpp index 1011d7d22d..209b84f7cf 100644 --- a/src/Mod/PartDesign/App/ShapeBinder.cpp +++ b/src/Mod/PartDesign/App/ShapeBinder.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include "ShapeBinder.h" @@ -382,7 +383,7 @@ App::DocumentObject* SubShapeBinder::getSubObject(const char* subname, PyObject* auto sobj = Part::Feature::getSubObject(subname, pyObj, mat, transform, depth); if (sobj) return sobj; - if (Data::ComplexGeoData::findElementName(subname) == subname) + if (Data::findElementName(subname) == subname) return nullptr; const char* dot = strchr(subname, '.'); @@ -405,7 +406,7 @@ App::DocumentObject* SubShapeBinder::getSubObject(const char* subname, PyObject* } else if (!boost::equals(sobj->getNameInDocument(), name)) continue; - name = Data::ComplexGeoData::noElementName(sub.c_str()); + name = Data::noElementName(sub.c_str()); name += dot + 1; if (mat && transform) *mat *= Placement.getValue().toMatrix(); @@ -640,7 +641,7 @@ void SubShapeBinder::update(SubShapeBinder::UpdateOption options) { std::ostringstream ss; ss << "Failed to obtain shape " << obj->getFullName() << '.' - << Data::ComplexGeoData::oldElementName(sub.c_str()); + << Data::oldElementName(sub.c_str()); errMsg = ss.str(); } } diff --git a/tests/src/App/ElementMap.cpp b/tests/src/App/ElementMap.cpp index be019ceacc..13819bd723 100644 --- a/tests/src/App/ElementMap.cpp +++ b/tests/src/App/ElementMap.cpp @@ -2,17 +2,537 @@ #include "gtest/gtest.h" -#include "App/ElementMap.h" +#include +#include -#include +// NOLINTBEGIN(readability-magic-numbers) + + +// this is a "holder" class used for simpler testing of ElementMap in the context of a class +class LessComplexPart +{ +public: + LessComplexPart(long tag, const std::string& nameStr, App::StringHasherRef hasher) + : elementMapPtr(std::make_shared()) + , Tag(tag) + , name(nameStr) + { + // object also have Vertexes etc and the face count varies; but that is not important + // here since we are not testing a real model + // the "MappedName" is left blank for now + Data::IndexedName face1("Face", 1); + Data::IndexedName face2("Face", 2); + Data::IndexedName face3("Face", 3); + Data::IndexedName face4("Face", 4); + Data::IndexedName face5("Face", 5); + Data::IndexedName face6("Face", 6); + elementMapPtr->hasher = hasher; + elementMapPtr->setElementName(face1, Data::MappedName(face1), Tag); + elementMapPtr->setElementName(face2, Data::MappedName(face2), Tag); + elementMapPtr->setElementName(face3, Data::MappedName(face3), Tag); + elementMapPtr->setElementName(face4, Data::MappedName(face4), Tag); + elementMapPtr->setElementName(face5, Data::MappedName(face5), Tag); + elementMapPtr->setElementName(face6, Data::MappedName(face6), Tag); + } + + Data::ElementMapPtr elementMapPtr; + mutable long Tag; + Data::MappedName name; +}; class ElementMapTest: public ::testing::Test { +protected: + static void SetUpTestSuite() + { + int argc = 1; + char* argv[] = {"FreeCAD"}; + App::Application::Config()["ExeName"] = "FreeCAD"; + App::Application::init(argc, argv); + } + + void SetUp() override + { + App::GetApplication().newDocument("test", "testUser"); + _sids = &_sid; + _hasher = Base::Reference(new App::StringHasher); + } + + // void TearDown() override {} + + Data::ElementIDRefs _sid; + QVector* _sids; + App::StringHasherRef _hasher; }; TEST_F(ElementMapTest, defaultConstruction) { // Act + Data::ElementMap elementMap = Data::ElementMap(); // Assert + EXPECT_EQ(elementMap.size(), 0); } + +TEST_F(ElementMapTest, setElementNameDefaults) +{ + // Arrange + Data::ElementMap elementMap; + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + + // Act + auto resultName = elementMap.setElementName(element, mappedName, 0); + auto mappedToElement = elementMap.find(element); + + // Assert + EXPECT_EQ(resultName, mappedName); + EXPECT_EQ(mappedToElement, mappedName); +} + +TEST_F(ElementMapTest, setElementNameNoOverwrite) +{ + // Arrange + Data::ElementMap elementMap; + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + + // Act + auto resultName = elementMap.setElementName(element, mappedName, 0); + auto resultName2 = elementMap.setElementName(element, anotherMappedName, 0, _sids, false); + auto mappedToElement = elementMap.find(element); + auto findAllResult = elementMap.findAll(element); + + // Assert + EXPECT_EQ(resultName, mappedName); + EXPECT_EQ(resultName2, anotherMappedName); + EXPECT_EQ(mappedToElement, mappedName); + EXPECT_EQ(findAllResult.size(), 2); + EXPECT_EQ(findAllResult[0].first, mappedName); + EXPECT_EQ(findAllResult[1].first, anotherMappedName); +} + +TEST_F(ElementMapTest, setElementNameWithOverwrite) +{ + // Arrange + Data::ElementMap elementMap; + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + + // Act + auto resultName = elementMap.setElementName(element, mappedName, 0); + auto resultName2 = elementMap.setElementName(element, anotherMappedName, 0, _sids, true); + auto mappedToElement = elementMap.find(element); + auto findAllResult = elementMap.findAll(element); + + // Assert + EXPECT_EQ(resultName, mappedName); + EXPECT_EQ(resultName2, anotherMappedName); + EXPECT_EQ(mappedToElement, anotherMappedName); + EXPECT_EQ(findAllResult.size(), 1); + EXPECT_EQ(findAllResult[0].first, anotherMappedName); +} + +TEST_F(ElementMapTest, setElementNameWithHashing) +{ + // Arrange + Data::ElementMap elementMap; + std::ostringstream ss; + Data::IndexedName element("Edge", 1); + Data::MappedName elementNameHolder(element);// Will get modified by the encoder + const Data::MappedName expectedName(element); + + // Act + elementMap.encodeElementName( + element.getType()[0], elementNameHolder, ss, nullptr, 0, nullptr, 0); + auto resultName = elementMap.setElementName(element, elementNameHolder, 0, _sids); + auto mappedToElement = elementMap.find(element); + + // Assert + EXPECT_EQ(resultName, expectedName); + EXPECT_EQ(mappedToElement, expectedName); +} + +TEST_F(ElementMapTest, eraseMappedName) +{ + // Arrange + Data::ElementMap elementMap; + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + elementMap.setElementName(element, mappedName, 0); + elementMap.setElementName(element, anotherMappedName, 0); + + // Act + auto sizeBefore = elementMap.size(); + auto findAllBefore = elementMap.findAll(element); + + elementMap.erase(anotherMappedName); + auto sizeAfter = elementMap.size(); + auto findAllAfter = elementMap.findAll(element); + + elementMap.erase(anotherMappedName); + auto sizeAfterRepeat = elementMap.size(); + auto findAllAfterRepeat = elementMap.findAll(element); + + // Assert + EXPECT_EQ(sizeBefore, 2); + EXPECT_EQ(findAllBefore.size(), 2); + EXPECT_EQ(findAllBefore[0].first, mappedName); + EXPECT_EQ(findAllBefore[1].first, anotherMappedName); + + EXPECT_EQ(sizeAfter, 1); + EXPECT_EQ(findAllAfter.size(), 1); + EXPECT_EQ(findAllAfter[0].first, mappedName); + + EXPECT_EQ(sizeAfterRepeat, 1); + EXPECT_EQ(findAllAfterRepeat.size(), 1); + EXPECT_EQ(findAllAfterRepeat[0].first, mappedName); +} + +TEST_F(ElementMapTest, eraseIndexedName) +{ + // Arrange + // Create two elements, edge1 and edge2, that have two mapped names each. + Data::ElementMap elementMap; + + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + elementMap.setElementName(element, mappedName, 0); + elementMap.setElementName(element, anotherMappedName, 0); + + Data::IndexedName element2("Edge", 2); + Data::MappedName mappedName2("TEST2"); + Data::MappedName anotherMappedName2("ANOTHERTEST2"); + elementMap.setElementName(element2, mappedName2, 0); + elementMap.setElementName(element2, anotherMappedName2, 0); + + // Act + auto sizeBefore = elementMap.size(); + auto findAllBefore = elementMap.findAll(element2); + + elementMap.erase(element2); + auto sizeAfter = elementMap.size(); + auto findAllAfter = elementMap.findAll(element2); + + elementMap.erase(element2); + auto sizeAfterRepeat = elementMap.size(); + auto findAllAfterRepeat = elementMap.findAll(element2); + + // Assert + EXPECT_EQ(sizeBefore, 4); + EXPECT_EQ(findAllBefore.size(), 2); + EXPECT_EQ(findAllBefore[0].first, mappedName2); + EXPECT_EQ(findAllBefore[1].first, anotherMappedName2); + + EXPECT_EQ(sizeAfter, 2); + EXPECT_EQ(findAllAfter.size(), 0); + + EXPECT_EQ(sizeAfterRepeat, 2); + EXPECT_EQ(findAllAfterRepeat.size(), 0); +} + +TEST_F(ElementMapTest, findMappedName) +{ + // Arrange + // Create two elements, edge1 and edge2, that have two mapped names each. + Data::ElementMap elementMap; + + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + elementMap.setElementName(element, mappedName, 0); + elementMap.setElementName(element, anotherMappedName, 0); + + Data::IndexedName element2("Edge", 2); + Data::MappedName mappedName2("TEST2"); + Data::MappedName anotherMappedName2("ANOTHERTEST2"); + elementMap.setElementName(element2, mappedName2, 0); + elementMap.setElementName(element2, anotherMappedName2, 0); + + // Act + auto findResult = elementMap.find(mappedName); + auto findResult2 = elementMap.find(mappedName2); + + // Assert + EXPECT_EQ(findResult, element); + EXPECT_EQ(findResult2, element2); +} + +TEST_F(ElementMapTest, findIndexedName) +{ + // Arrange + // Create two elements, edge1 and edge2, that have two mapped names each. + Data::ElementMap elementMap; + + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + elementMap.setElementName(element, mappedName, 0); + elementMap.setElementName(element, anotherMappedName, 0); + + Data::IndexedName element2("Edge", 2); + Data::MappedName mappedName2("TEST2"); + Data::MappedName anotherMappedName2("ANOTHERTEST2"); + elementMap.setElementName(element2, mappedName2, 0); + elementMap.setElementName(element2, anotherMappedName2, 0); + + // Act + // they return the first mapped name + auto findResult = elementMap.find(element); + auto findResult2 = elementMap.find(element2); + + // Assert + EXPECT_EQ(findResult, mappedName); + EXPECT_EQ(findResult2, mappedName2); +} + +TEST_F(ElementMapTest, findAll) +{ + // Arrange + // Create two elements, edge1 and edge2, that have two mapped names each. + Data::ElementMap elementMap; + + Data::IndexedName element("Edge", 1); + Data::MappedName mappedName("TEST"); + Data::MappedName anotherMappedName("ANOTHERTEST"); + elementMap.setElementName(element, mappedName, 0); + elementMap.setElementName(element, anotherMappedName, 0); + + Data::IndexedName element2("Edge", 2); + Data::MappedName mappedName2("TEST2"); + Data::MappedName anotherMappedName2("ANOTHERTEST2"); + elementMap.setElementName(element2, mappedName2, 0); + elementMap.setElementName(element2, anotherMappedName2, 0); + + // Act + // they return the first mapped name + auto findResult = elementMap.findAll(element); + auto findResult2 = elementMap.findAll(element2); + + // Assert + EXPECT_EQ(findResult.size(), 2); + EXPECT_EQ(findResult[0].first, mappedName); + EXPECT_EQ(findResult[1].first, anotherMappedName); + EXPECT_EQ(findResult2.size(), 2); + EXPECT_EQ(findResult2[0].first, mappedName2); + EXPECT_EQ(findResult2[1].first, anotherMappedName2); +} + +TEST_F(ElementMapTest, mimicOnePart) +{ + // Arrange + // pattern: new doc, create Cube + // for a single part, there is no "naming algo" to speak of + std::ostringstream ss; + auto docName = "Unnamed"; + LessComplexPart cube(1L, "Box", _hasher); + + // Act + auto children = cube.elementMapPtr->getAll(); + ss << docName << "#" << cube.name << "." + << cube.elementMapPtr->find(Data::IndexedName("Face", 6)); + + // Assert + EXPECT_EQ(children.size(), 6); + EXPECT_EQ(children[0].index.toString(), "Face1"); + EXPECT_EQ(children[0].name.toString(), "Face1"); + EXPECT_EQ(children[1].index.toString(), "Face2"); + EXPECT_EQ(children[1].name.toString(), "Face2"); + EXPECT_EQ(children[2].index.toString(), "Face3"); + EXPECT_EQ(children[2].name.toString(), "Face3"); + EXPECT_EQ(children[3].index.toString(), "Face4"); + EXPECT_EQ(children[3].name.toString(), "Face4"); + EXPECT_EQ(children[4].index.toString(), "Face5"); + EXPECT_EQ(children[4].name.toString(), "Face5"); + EXPECT_EQ(children[5].index.toString(), "Face6"); + EXPECT_EQ(children[5].name.toString(), "Face6"); + EXPECT_EQ(ss.str(), "Unnamed#Box.Face6"); +} + +TEST_F(ElementMapTest, mimicSimpleUnion) +{ + // Arrange + // pattern: new doc, create Cube, create Cylinder, Union of both (Cube first) + std::ostringstream ss; + std::ostringstream finalSs; + char* docName = "Unnamed"; + + LessComplexPart cube(1L, "Box", _hasher); + LessComplexPart cylinder(2L, "Cylinder", _hasher); + // Union (Fusion) operation via the Part Workbench + LessComplexPart unionPart(3L, "Fusion", _hasher); + + // we are only going to simulate one face for testing purpose + Data::IndexedName uface3("Face", 3); + auto PartOp = "FUS";// Part::OpCodes::Fuse; + + // Act + // act: simulate a union/fuse operation + auto parent = cube.elementMapPtr->getAll()[5]; + Data::MappedName postfixHolder(std::string(Data::POSTFIX_MOD) + "2"); + unionPart.elementMapPtr->encodeElementName( + postfixHolder[0], postfixHolder, ss, nullptr, unionPart.Tag, nullptr, unionPart.Tag); + auto postfixStr = postfixHolder.toString() + Data::ELEMENT_MAP_PREFIX + PartOp; + + // act: with the fuse op, name against the cube's Face6 + Data::MappedName uface3Holder(parent.index); + // we will invoke the encoder for face 3 + unionPart.elementMapPtr->encodeElementName( + uface3Holder[0], uface3Holder, ss, nullptr, unionPart.Tag, postfixStr.c_str(), cube.Tag); + unionPart.elementMapPtr->setElementName(uface3, uface3Holder, unionPart.Tag, nullptr, true); + + // act: generate a full toponame string for testing purposes + finalSs << docName << "#" << unionPart.name; + finalSs << "."; + finalSs << Data::ELEMENT_MAP_PREFIX + unionPart.elementMapPtr->find(uface3).toString(); + finalSs << "."; + finalSs << uface3; + + // Assert + EXPECT_EQ(postfixStr, ":M2;FUS"); + EXPECT_EQ(unionPart.elementMapPtr->find(uface3).toString(), "Face6;:M2;FUS;:H1:8,F"); + EXPECT_EQ(finalSs.str(), "Unnamed#Fusion.;Face6;:M2;FUS;:H1:8,F.Face3"); + + // explanation of "Fusion.;Face6;:M2;FUS;:H2:3,F.Face3" toponame + // Note: every postfix is prefixed by semicolon + // Note: the start/middle/end are separated by periods + // + // "Fusion" means that we are on the "Fusion" object. + // "." we are done with the first part + // ";Face6" means default inheritance comes from face 6 of the parent (which is a cube) + // ";:M2" means that a Workbench op has happened. "M" is the "Mod" directory in the source tree? + // ";FUS" means that a Fusion operation has happened. Notice the lack of a colon. + // ";:H2" means the subtending object (cylinder) has a tag of 2 + // ":3" means the writing position is 3; literally how far into the current postfix we are + // ",F" means are of type "F" which is short for "Face" of Face3 of Fusion. + // "." we are done with the second part + // "Face3" is the localized name +} + +TEST_F(ElementMapTest, mimicOperationAgainstSelf) +{ + // Arrange + // pattern: new doc, create Cube, Mystery Op with self as target + std::ostringstream ss; + LessComplexPart finalPart(99L, "MysteryOp", _hasher); + // we are only going to simulate one face for testing purpose + Data::IndexedName uface3("Face", 3); + auto PartOp = "MYS"; + auto ownFace6 = finalPart.elementMapPtr->getAll()[5]; + Data::MappedName uface3Holder(ownFace6.index); + auto workbenchId = std::string(Data::POSTFIX_MOD) + "9999"; + + // Act + // act: with the mystery op, name against its own Face6 for some reason + Data::MappedName postfixHolder(workbenchId); + finalPart.elementMapPtr->encodeElementName( + postfixHolder[0], postfixHolder, ss, nullptr, finalPart.Tag, nullptr, finalPart.Tag); + auto postfixStr = postfixHolder.toString() + Data::ELEMENT_MAP_PREFIX + PartOp; + // we will invoke the encoder for face 3 + finalPart.elementMapPtr->encodeElementName(uface3Holder[0], + uface3Holder, + ss, + nullptr, + finalPart.Tag, + postfixStr.c_str(), + finalPart.Tag); + // override not forced + finalPart.elementMapPtr->setElementName(uface3, uface3Holder, finalPart.Tag, nullptr, false); + + // Assert + EXPECT_EQ(postfixStr, ":M9999;MYS"); + EXPECT_EQ(finalPart.elementMapPtr->find(uface3).toString(), "Face3");// override not forced + EXPECT_EQ(uface3Holder.toString(), "Face6;:M9999;MYS;:H63:b,F"); + // explaining ";Face6;:M2;MYS;:H2:3,F" name: + // + // ";Face6" means default inheritance comes from face 6 of the ownFace6 (which is itself) + // ";:M9999" means that a Workbench op happened. "M" is the "Mod" directory in the source tree? + // ";MYS" means that a "Mystery" operation has happened. Notice the lack of a colon. + // ";:H63" means the subtending object (cylinder) has a tag of 99 (63 in hex) + // ":b" means the writing position is b (hex); literally how far into the current postfix we are + // ",F" means are of type "F" which is short for "Face" of Face3 of Fusion. +} + +TEST_F(ElementMapTest, hasChildElementMapTest) +{ + // Arrange + Data::ElementMap::MappedChildElements child = { + Data::IndexedName("face", 1), 2, 7, 4L, Data::ElementMapPtr(), QByteArray(""), _sid}; + std::vector children = {child}; + LessComplexPart cubeFull(3L, "FullBox", _hasher); + cubeFull.elementMapPtr->addChildElements(cubeFull.Tag, children); + // + LessComplexPart cubeWithoutChildren(2L, "EmptyBox", _hasher); + + // Act + bool resultFull = cubeFull.elementMapPtr->hasChildElementMap(); + bool resultWhenEmpty = cubeWithoutChildren.elementMapPtr->hasChildElementMap(); + + // Assert + EXPECT_TRUE(resultFull); + EXPECT_FALSE(resultWhenEmpty); +} + +TEST_F(ElementMapTest, hashChildMapsTest) +{ + // Arrange + LessComplexPart cube(1L, "Box", _hasher); + auto childOneName = Data::IndexedName("Ping", 1); + Data::ElementMap::MappedChildElements childOne = { + childOneName, + 2, + 7, + 3L, + Data::ElementMapPtr(), + QByteArray("abcdefghij"),// postfix must be 10 or more bytes to invoke hasher + _sid}; + std::vector children = {childOne}; + cube.elementMapPtr->addChildElements(cube.Tag, children); + auto before = _hasher->getIDMap(); + + // Act + cube.elementMapPtr->hashChildMaps(cube.Tag); + + // Assert + auto after = _hasher->getIDMap(); + EXPECT_EQ(before.size(), 0); + EXPECT_EQ(after.size(), 1); +} + +TEST_F(ElementMapTest, addAndGetChildElementsTest) +{ + // Arrange + LessComplexPart cube(1L, "Box", _hasher); + Data::ElementMap::MappedChildElements childOne = { + Data::IndexedName("Ping", 1), + 2, + 7, + 3L, + Data::ElementMapPtr(), + QByteArray("abcdefghij"),// postfix must be 10 or more bytes to invoke hasher + _sid}; + Data::ElementMap::MappedChildElements childTwo = { + Data::IndexedName("Pong", 2), 2, 7, 4L, Data::ElementMapPtr(), QByteArray("abc"), _sid}; + std::vector children = {childOne, childTwo}; + + // Act + cube.elementMapPtr->addChildElements(cube.Tag, children); + auto result = cube.elementMapPtr->getChildElements(); + + // Assert + EXPECT_EQ(result.size(), 2); + EXPECT_TRUE( + std::any_of(result.begin(), result.end(), [](Data::ElementMap::MappedChildElements e) { + return e.indexedName.toString() == "Ping1"; + })); + EXPECT_TRUE( + std::any_of(result.begin(), result.end(), [](Data::ElementMap::MappedChildElements e) { + return e.indexedName.toString() == "Pong2"; + })); +} + +// NOLINTEND(readability-magic-numbers) diff --git a/tests/src/App/MappedName.cpp b/tests/src/App/MappedName.cpp index 954c427f7b..4b11ec5f6e 100644 --- a/tests/src/App/MappedName.cpp +++ b/tests/src/App/MappedName.cpp @@ -51,7 +51,7 @@ TEST(MappedName, namedConstructionWithMaxSize) TEST(MappedName, namedConstructionDiscardPrefix) { // Arrange - std::string name = Data::ComplexGeoData::elementMapPrefix() + "TEST"; + std::string name = std::string(Data::ELEMENT_MAP_PREFIX) + "TEST"; // Act Data::MappedName mappedName(name.c_str()); @@ -80,7 +80,7 @@ TEST(MappedName, stringNamedConstruction) TEST(MappedName, stringNamedConstructionDiscardPrefix) { // Arrange - std::string name = Data::ComplexGeoData::elementMapPrefix() + "TEST"; + std::string name = std::string(Data::ELEMENT_MAP_PREFIX) + "TEST"; // Act Data::MappedName mappedName(name); @@ -560,7 +560,7 @@ TEST(MappedName, appendToBufferWithPrefix) // Arrange Data::MappedName mappedName(Data::MappedName("TEST"), "POSTFIXTEST"); std::string buffer("STUFF"); - std::string elemMapPrefix = Data::ComplexGeoData::elementMapPrefix(); + std::string elemMapPrefix = Data::ELEMENT_MAP_PREFIX; // Act mappedName.appendToBufferWithPrefix(buffer); @@ -586,7 +586,7 @@ TEST(MappedName, toPrefixedString) // Arrange Data::MappedName mappedName(Data::MappedName("TEST"), "POSTFIXTEST"); std::string buffer("STUFF"); - std::string elemMapPrefix = Data::ComplexGeoData::elementMapPrefix(); + std::string elemMapPrefix = Data::ELEMENT_MAP_PREFIX; // Act buffer += mappedName.toPrefixedString();