/*************************************************************************** * Copyright (c) 2002 Jürgen Riegel * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include "PreCompiled.h" #include #include #include "Application.h" #include "Document.h" #include "private/DocumentP.h" #include "DocumentObject.h" #include "ExpressionParser.h" #include "GeoFeatureGroupExtension.h" #include "Origin.h" #include "OriginGroupExtension.h" #include "ObjectIdentifier.h" using namespace App; using namespace boost; void Document::writeDependencyGraphViz(std::ostream& out) { // // caching vertex to DocObject // std::map VertexMap; // for(std::map::const_iterator It1= _DepConMap.begin();It1 != // _DepConMap.end(); ++It1) // VertexMap[It1->second] = It1->first; out << "digraph G {" << std::endl; out << "\tordering=out;" << std::endl; out << "\tnode [shape = box];" << std::endl; for (const auto& It : d->objectMap) { out << "\t" << It.first << ";" << std::endl; std::vector OutList = It.second->getOutList(); for (const auto& It2 : OutList) { if (It2) { out << "\t" << It.first << "->" << It2->getNameInDocument() << ";" << std::endl; } } } /* graph_traits::edge_iterator ei, ei_end; for (tie(ei,ei_end) = edges(_DepList); ei != ei_end; ++ei) out << "\t" << VertexMap[source(*ei, _DepList)]->getNameInDocument() << " -> " << VertexMap[target(*ei, _DepList)]->getNameInDocument() << ";" << endl; */ out << "}" << std::endl; } void Document::exportGraphviz(std::ostream& out) const { /* Type defs for a graph with graphviz attributes */ using GraphvizAttributes = std::map; using Graph = boost::subgraph, property>, property>>>>>; /** * @brief The GraphCreator class * * This class creates the dependency graph for a document. * */ class GraphCreator { public: explicit GraphCreator(struct DocumentP* _d) : d(_d) , seed(std::random_device()()) , distribution(0, 255) { build(); } const Graph& getGraph() const { return DepList; } private: void build() { // Set attribute(s) for main graph get_property(DepList, graph_graph_attribute)["compound"] = "true"; addSubgraphs(); buildAdjacencyList(); addEdges(); markCycles(); markOutOfScopeLinks(); } /** * @brief getId returns a canonical string for a DocumentObject. * @param docObj Document object to get an ID from * @return A string */ std::string getId(const DocumentObject* docObj) { std::string id; if (docObj->isAttachedToDocument()) { auto doc = docObj->getDocument(); id.append(doc->getName()); id.append("#"); id.append(docObj->getNameInDocument()); } return id; } /** * @brief getId returns a canonical string for an ObjectIdentifier; * @param path * @return A string */ std::string getId(const ObjectIdentifier& path) { DocumentObject* docObj = path.getDocumentObject(); if (!docObj) { return {}; } return std::string((docObj)->getDocument()->getName()) + "#" + docObj->getNameInDocument() + "." + path.getPropertyName() + path.getSubPathStr(); } std::string getClusterName(const DocumentObject* docObj) const { return std::string("cluster") + docObj->getNameInDocument(); } void setGraphLabel(Graph& g, const DocumentObject* obj) const { std::string name(obj->getNameInDocument()); std::string label(obj->Label.getValue()); if (name == label) { get_property(g, graph_graph_attribute)["label"] = name; } else { get_property(g, graph_graph_attribute)["label"] = name + "\n(" + label + ")"; } } /** * @brief setGraphAttributes Set graph attributes on a subgraph for a DocumentObject node. * @param obj DocumentObject */ void setGraphAttributes(const DocumentObject* obj) { assert(GraphList.find(obj) != GraphList.end()); get_property(*GraphList[obj], graph_name) = getClusterName(obj); get_property(*GraphList[obj], graph_graph_attribute)["bgcolor"] = "#e0e0e0"; get_property(*GraphList[obj], graph_graph_attribute)["style"] = "rounded,filled"; setGraphLabel(*GraphList[obj], obj); } /** * @brief setPropertyVertexAttributes Set vertex attributes for a Property node in a graph. * @param g Graph * @param vertex Property node * @param name Name of node */ void setPropertyVertexAttributes(Graph& g, Vertex vertex, const std::string& name) { get(vertex_attribute, g)[vertex]["label"] = name; get(vertex_attribute, g)[vertex]["shape"] = "box"; get(vertex_attribute, g)[vertex]["style"] = "dashed"; get(vertex_attribute, g)[vertex]["fontsize"] = "8pt"; } /** * @brief addExpressionSubgraphIfNeeded Add a subgraph to the main graph if it is needed, * i.e. there are defined at least one expression in the document object, or other objects * are referencing properties in it. * @param obj DocumentObject to assess. * @param CSSubgraphs Boolean if the GeoFeatureGroups are created as subgraphs */ void addExpressionSubgraphIfNeeded(DocumentObject* obj, bool CSsubgraphs) { auto expressions = obj->ExpressionEngine.getExpressions(); if (!expressions.empty()) { Graph* graph = nullptr; graph = &DepList; if (CSsubgraphs) { auto group = GeoFeatureGroupExtension::getGroupOfObject(obj); if (group) { auto it = GraphList.find(group); if (it != GraphList.end()) { graph = it->second; } } } // If documentObject has an expression, create a subgraph for it if (graph && !GraphList[obj]) { GraphList[obj] = &graph->create_subgraph(); setGraphAttributes(obj); } // Create subgraphs for all document objects that it depends on; it will depend on // some property there for (const auto& expr : expressions) { std::map deps; expr.second->getIdentifiers(deps); for (const auto& dep : deps) { if (dep.second) { continue; } DocumentObject* o = dep.first.getDocumentObject(); // Doesn't exist already? if (o && !GraphList[o]) { if (CSsubgraphs) { auto group = GeoFeatureGroupExtension::getGroupOfObject(o); auto graph2 = group ? GraphList[group] : &DepList; if (graph2) { GraphList[o] = &graph2->create_subgraph(); setGraphAttributes(o); } } else if (graph) { GraphList[o] = &graph->create_subgraph(); setGraphAttributes(o); } } } } } } /** * @brief add Add @docObj to the graph, including all expressions (and dependencies) it * includes. * @param docObj The document object to add. * @param name Name of node. */ void add(DocumentObject* docObj, const std::string& name, const std::string& label, bool CSSubgraphs) { // don't add objects twice if (std::find(objects.begin(), objects.end(), docObj) != objects.end()) { return; } // find the correct graph to add the vertex to. Check first expression graphs, // afterwards the parent CS and origin graphs Graph* sgraph = GraphList[docObj]; if (CSSubgraphs) { if (!sgraph) { auto group = GeoFeatureGroupExtension::getGroupOfObject(docObj); if (group) { if (docObj->isDerivedFrom(App::DatumElement::getClassTypeId())) { sgraph = GraphList[group->getExtensionByType() ->Origin.getValue()]; } else { sgraph = GraphList[group]; } } } if (!sgraph) { if (docObj->isDerivedFrom(DatumElement::getClassTypeId())) { sgraph = GraphList[static_cast(docObj)->getOrigin()]; } } } if (!sgraph) { sgraph = &DepList; } // Keep a list of all added document objects. objects.insert(docObj); // Add vertex to graph. Track global and local index LocalVertexList[getId(docObj)] = add_vertex(*sgraph); GlobalVertexList[getId(docObj)] = vertex_no++; // If node is in main graph, style it with rounded corners. If not, make it invisible. if (!GraphList[docObj]) { get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["style"] = "filled"; get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["shape"] = "Mrecord"; // Set node label if (name == label) { get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["label"] = name; } else { get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["label"] = name + "\n(" + label + ")"; } } else { get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["style"] = "invis"; get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["fixedsize"] = "true"; get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["width"] = "0"; get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["height"] = "0"; } // Add expressions and its dependencies auto expressions {docObj->ExpressionEngine.getExpressions()}; for (const auto& expr : expressions) { auto found = std::as_const(GlobalVertexList).find(getId(expr.first)); if (found == GlobalVertexList.end()) { int vid = LocalVertexList[getId(expr.first)] = add_vertex(*sgraph); GlobalVertexList[getId(expr.first)] = vertex_no++; setPropertyVertexAttributes(*sgraph, vid, expr.first.toString()); } } // Add all dependencies for (const auto& expression : expressions) { // Get dependencies std::map deps; expression.second->getIdentifiers(deps); // Create subgraphs for all documentobjects that it depends on; it will depend on // some property there for (const auto& dep : deps) { if (dep.second) { continue; } DocumentObject* depObjDoc = dep.first.getDocumentObject(); auto found = GlobalVertexList.find(getId(dep.first)); if (found == GlobalVertexList.end()) { Graph* depSgraph = GraphList[depObjDoc] ? GraphList[depObjDoc] : &DepList; LocalVertexList[getId(dep.first)] = add_vertex(*depSgraph); GlobalVertexList[getId(dep.first)] = vertex_no++; setPropertyVertexAttributes(*depSgraph, LocalVertexList[getId(dep.first)], dep.first.getPropertyName() + dep.first.getSubPathStr()); } } } } void recursiveCSSubgraphs(DocumentObject* cs, DocumentObject* parent) { auto graph = parent ? GraphList[parent] : &DepList; // check if the value for the key 'parent' is null if (!graph) { return; } auto& sub = graph->create_subgraph(); GraphList[cs] = ⊂ get_property(sub, graph_name) = getClusterName(cs); // build random color string std::stringstream stream; stream << "#" << std::setfill('0') << std::setw(2) << std::hex << distribution(seed) << std::setfill('0') << std::setw(2) << std::hex << distribution(seed) << std::setfill('0') << std::setw(2) << std::hex << distribution(seed) << 80; std::string result(stream.str()); get_property(sub, graph_graph_attribute)["bgcolor"] = result; get_property(sub, graph_graph_attribute)["style"] = "rounded,filled"; setGraphLabel(sub, cs); for (auto obj : cs->getOutList()) { if (obj->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId())) { // in case of dependencies loops check if obj is already part of the // map to avoid infinite recursions auto it = GraphList.find(obj); if (it == GraphList.end()) { recursiveCSSubgraphs(obj, cs); } } } // setup the origin if available if (cs->hasExtension(App::OriginGroupExtension::getExtensionClassTypeId())) { auto origin = cs->getExtensionByType()->Origin.getValue(); if (!origin) { std::cerr << "Origin feature not found" << std::endl; return; } auto& osub = sub.create_subgraph(); GraphList[origin] = &osub; get_property(osub, graph_name) = getClusterName(origin); get_property(osub, graph_graph_attribute)["bgcolor"] = "none"; setGraphLabel(osub, origin); } } void addSubgraphs() { ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/DependencyGraph"); bool CSSubgraphs = depGrp->GetBool("GeoFeatureSubgraphs", true); if (CSSubgraphs) { // first build up the coordinate system subgraphs for (auto objectIt : d->objectArray) { // ignore groups inside other groups, these will be processed in one of the next // recursive calls. App::Origin now has the GeoFeatureGroupExtension but it // should not move its group symbol outside its parent if (!objectIt->isDerivedFrom(Origin::getClassTypeId()) && objectIt->hasExtension( GeoFeatureGroupExtension::getExtensionClassTypeId()) && GeoFeatureGroupExtension::getGroupOfObject(objectIt) == nullptr) { recursiveCSSubgraphs(objectIt, nullptr); } } } // Internal document objects for (const auto& It : d->objectMap) { addExpressionSubgraphIfNeeded(It.second, CSSubgraphs); } // Add external document objects for (const auto& it : d->objectMap) { std::vector OutList = it.second->getOutList(); for (auto obj : OutList) { if (obj) { std::map::const_iterator item = GlobalVertexList.find(getId(obj)); if (item == GlobalVertexList.end()) { addExpressionSubgraphIfNeeded(obj, CSSubgraphs); } } } } } // Filling up the adjacency List void buildAdjacencyList() { ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/DependencyGraph"); bool CSSubgraphs = depGrp->GetBool("GeoFeatureSubgraphs", true); // Add internal document objects for (const auto& It : d->objectMap) { add(It.second, It.second->getNameInDocument(), It.second->Label.getValue(), CSSubgraphs); } // Add external document objects for (const auto& It : d->objectMap) { std::vector OutList = It.second->getOutList(); for (auto obj : OutList) { if (obj) { std::map::const_iterator item = GlobalVertexList.find(getId(obj)); if (item == GlobalVertexList.end()) { if (obj->isAttachedToDocument()) { add(obj, std::string(obj->getDocument()->getName()) + "#" + obj->getNameInDocument(), std::string(obj->getDocument()->getName()) + "#" + obj->Label.getValue(), CSSubgraphs); } } } } } } void addEdges() { // Get edge properties for main graph const boost::property_map::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList); // Track edges between document objects connected by expression dependencies std::set> existingEdges; // Add edges between properties for (const auto& docObj : objects) { // Add expressions and its dependencies auto expressions = docObj->ExpressionEngine.getExpressions(); for (const auto& expr : expressions) { std::map deps; expr.second->getIdentifiers(deps); // Create subgraphs for all documentobjects that it depends on; it will depend // on some property there for (const auto& dep : deps) { if (dep.second) { continue; } DocumentObject* depObjDoc = dep.first.getDocumentObject(); Edge edge; bool inserted; tie(edge, inserted) = add_edge(GlobalVertexList[getId(expr.first)], GlobalVertexList[getId(dep.first)], DepList); // Add this edge to the set of all expression generated edges existingEdges.insert(std::make_pair(docObj, depObjDoc)); // Edges between properties should be a bit smaller, and dashed edgeAttrMap[edge]["arrowsize"] = "0.5"; edgeAttrMap[edge]["style"] = "dashed"; } } } ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/DependencyGraph"); bool omitGeoFeatureGroups = depGrp->GetBool("GeoFeatureSubgraphs", true); // Add edges between document objects for (const auto& It : d->objectMap) { if (omitGeoFeatureGroups && It.second->isDerivedFrom(Origin::getClassTypeId())) { continue; } std::map dups; std::vector OutList = It.second->getOutList(); const DocumentObject* docObj = It.second; const bool docObj_is_group = docObj->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId()); for (auto obj : OutList) { if (obj) { if (omitGeoFeatureGroups && docObj_is_group && GeoFeatureGroupExtension::getGroupOfObject(obj) == docObj) { continue; } // Count duplicate edges bool inserted = edge(GlobalVertexList[getId(docObj)], GlobalVertexList[getId(obj)], DepList) .second; if (inserted) { dups[obj]++; continue; } // Skip edge if an expression edge already exists if (existingEdges.find(std::make_pair(docObj, obj)) != existingEdges.end()) { continue; } // Add edge Edge edge; tie(edge, inserted) = add_edge(GlobalVertexList[getId(docObj)], GlobalVertexList[getId(obj)], DepList); // Set properties to make arrows go between subgraphs if needed if (GraphList[docObj]) { edgeAttrMap[edge]["ltail"] = getClusterName(docObj); } if (GraphList[obj]) { edgeAttrMap[edge]["lhead"] = getClusterName(obj); } } } // Set labels for duplicate edges for (const auto& dup : dups) { Edge e(edge(GlobalVertexList[getId(It.second)], GlobalVertexList[getId(dup.first)], DepList) .first); std::stringstream s; s << " " << (dup.second + 1) << "x"; edgeAttrMap[e]["label"] = s.str(); } } } using EdgeMap = std::unordered_multimap; void removeEdges(EdgeMap& in_edges, EdgeMap& out_edges, std::pair i_pair, std::function select_vertex) { auto i = i_pair.first; while (i != i_pair.second) { // Remove from in edges in other nodes auto in_i_pair = in_edges.equal_range(select_vertex(i->second)); auto in_i = in_i_pair.first; while (in_i != in_i_pair.second) { if (in_i->second == i->second) { in_i = in_edges.erase(in_i); } else { ++in_i; } } // Remove node from out_edges i = out_edges.erase(i); } } #if defined(__clang__) #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif void markCycles() { bool changed = true; std::unordered_set in_use; EdgeMap in_edges; EdgeMap out_edges; // Add all vertices to the in_use set graph_traits::vertex_iterator vi, vi_end; tie(vi, vi_end) = vertices(DepList); for (; vi != vi_end; ++vi) { in_use.insert(*vi); } // Add all edges to the in_edges and out_edges multimaps graph_traits::edge_iterator ei, ei_end; tie(ei, ei_end) = edges(DepList); for (; ei != ei_end; ++ei) { in_edges.insert(std::make_pair(target(*ei, DepList), *ei)); out_edges.insert(std::make_pair(source(*ei, DepList), *ei)); } // Go through dependency graph and remove nodes with either no input or output // A normal DAG without any cycles will get all its edges removed. // If one or more cycles exist in the graph, there will remain nodes with // both in and out edges. while (changed) { auto uvi = in_use.begin(); auto uvi_end = in_use.end(); // Flag that no changes has occurred so far. If the loop goes through // without this flag being set to true, we are done. changed = false; while (uvi != uvi_end) { auto i_in_deg_pair = in_edges.equal_range(*uvi); auto i_out_deg_pair = out_edges.equal_range(*uvi); if (i_in_deg_pair.first == in_edges.end() && i_out_deg_pair.first == out_edges.end()) { uvi = in_use.erase(uvi); continue; } // Remove out edges of nodes that don't have a single edge in if (i_in_deg_pair.first == in_edges.end()) { removeEdges(in_edges, out_edges, i_out_deg_pair, [&](Edge e) { return target(e, DepList); }); changed = true; i_out_deg_pair = out_edges.equal_range(*uvi); } // Remove in edges of nodes that don't have a single edge out if (i_out_deg_pair.first == out_edges.end()) { removeEdges(out_edges, in_edges, i_in_deg_pair, [&](Edge e) { return source(e, DepList); }); changed = true; } ++uvi; } } // Update colors in graph const boost::property_map::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList); for (auto ei : out_edges) { edgeAttrMap[ei.second]["color"] = "red"; } } #if defined(__clang__) #elif defined(__GNUC__) #pragma GCC diagnostic pop #endif void markOutOfScopeLinks() { const boost::property_map::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList); for (auto obj : objects) { std::vector invalids; GeoFeatureGroupExtension::getInvalidLinkObjects(obj, invalids); // isLinkValid returns true for non-link properties for (auto linkedObj : invalids) { auto res = edge(GlobalVertexList[getId(obj)], GlobalVertexList[getId(linkedObj)], DepList); if (res.second) { edgeAttrMap[res.first]["color"] = "orange"; } } } } const struct DocumentP* d; Graph DepList; int vertex_no {0}; std::map LocalVertexList; std::map GlobalVertexList; std::set objects; std::map GraphList; // random color generation std::mt19937 seed; std::uniform_int_distribution distribution; }; GraphCreator g(d); boost::write_graphviz(out, g.getGraph()); }