diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index d0d33af19..f93ffe1ff 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -112,6 +112,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/conductorautonumerotation.cpp ${QET_DIR}/sources/conductorautonumerotation.h ${QET_DIR}/sources/conductornumexport.cpp + ${QET_DIR}/sources/wiringlistexport.h + ${QET_DIR}/sources/wiringlistexport.cpp ${QET_DIR}/sources/conductornumexport.h ${QET_DIR}/sources/conductorprofile.cpp ${QET_DIR}/sources/conductorprofile.h diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index f9cfbd7d0..8724fdf25 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -45,6 +45,7 @@ #include "TerminalStrip/ui/terminalstripeditorwindow.h" #include "ui/diagrameditorhandlersizewidget.h" #include "TerminalStrip/ui/addterminalstripitemdialog.h" +#include "wiringlistexport.h" #ifdef BUILD_WITHOUT_KF5 #else @@ -465,13 +466,23 @@ void QETDiagramEditor::setUpActions() wne.toCsv(); } }); - -#ifdef QET_EXPORT_PROJECT_DB - m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this); - connect(m_export_project_db, &QAction::triggered, [this]() { - projectDataBase::exportDb(this->currentProject()->dataBase(), this); + // Export wiring list to CSV + m_project_export_wiring_list = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter le plan de câblage"), this); + connect(m_project_export_wiring_list, &QAction::triggered, [this]() { + QETProject *project = this->currentProject(); + if (project) + { + WiringListExport wle(project, this); + wle.toCsv(); + } }); -#endif + + #ifdef QET_EXPORT_PROJECT_DB + m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this); + connect(m_export_project_db, &QAction::triggered, [this]() { + projectDataBase::exportDb(this->currentProject()->dataBase(), this); + }); + #endif //MDI view style m_tabbed_view_mode = new QAction(tr("en utilisant des onglets"), this); @@ -835,6 +846,7 @@ void QETDiagramEditor::setUpMenu() menu_project -> addAction(m_project_export_conductor_num); menu_project -> addAction(m_terminal_strip_dialog); menu_project -> addAction(m_project_terminalBloc); + menu_project -> addAction(m_project_export_wiring_list); #ifdef QET_EXPORT_PROJECT_DB menu_project -> addSeparator(); menu_project -> addAction(m_export_project_db); @@ -1579,6 +1591,7 @@ void QETDiagramEditor::slot_updateActions() m_csv_export -> setEnabled(editable_project); m_project_export_conductor_num-> setEnabled(opened_project); m_terminal_strip_dialog -> setEnabled(editable_project); + m_project_export_wiring_list -> setEnabled(opened_project); #ifdef QET_EXPORT_PROJECT_DB m_export_project_db -> setEnabled(editable_project); #endif diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 966f2c6fa..a0090285a 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -197,6 +197,7 @@ class QETDiagramEditor : public QETMainWindow *m_terminal_strip_dialog = nullptr, /// +#include +#include +#include +#include +#include +#include +#include + +WiringListExport::WiringListExport(QETProject *project, QWidget *parent) : +QObject(parent), +m_project(project), +m_parent(parent) +{ +} + +QString WiringListExport::normalizeUuid(const QString &u) const +{ + QString res = u; + res.remove('{').remove('}'); + return res.trimmed().toLower(); +} + +QString WiringListExport::findDiagramFolio(const QDomElement &diagramElem) const +{ + if (diagramElem.isNull()) return ""; + if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio"); + if (diagramElem.hasAttribute("title")) return diagramElem.attribute("title"); + return ""; +} + +QDomElement WiringListExport::climbToDiagram(QDomNode node) const +{ + while (!node.isNull()) { + if (node.isElement() && node.toElement().tagName().toLower() == "diagram") { + return node.toElement(); + } + node = node.parentNode(); + } + return QDomElement(); +} + +QMap WiringListExport::collectElementsInfo(const QDomElement &root) const +{ + QMap infoMap; + QDomNodeList elements = root.elementsByTagName("element"); + + for (int i = 0; i < elements.size(); ++i) { + QDomElement el = elements.at(i).toElement(); + QString uuid = normalizeUuid(el.attribute("uuid", el.attribute("id", ""))); + if (uuid.isEmpty()) continue; + + ElementInfo info; + info.folio = findDiagramFolio(climbToDiagram(el)); + + QDomElement linksNode = el.firstChildElement("links_uuids"); + if (!linksNode.isNull()) { + QDomNodeList linkUuids = linksNode.elementsByTagName("link_uuid"); + for (int j = 0; j < linkUuids.size(); ++j) { + QString luuid = normalizeUuid(linkUuids.at(j).toElement().attribute("uuid")); + if (!luuid.isEmpty()) info.links.append(luuid); + } + } + + QDomElement elInfoNode = el.firstChildElement("elementInformations"); + if (!elInfoNode.isNull()) { + QDomNodeList eics = elInfoNode.elementsByTagName("elementInformation"); + for (int j = 0; j < eics.size(); ++j) { + QDomElement eic = eics.at(j).toElement(); + QString nameAttr = eic.attribute("name").toLower(); + if (nameAttr == "label") info.label = eic.text().trimmed(); + if (nameAttr == "name") info.name = eic.text().trimmed(); + } + } + + QString typeVal = el.attribute("type").toLower(); + if (typeVal.contains("naechste") || typeVal.contains("vorherige") || + typeVal.contains("next") || typeVal.contains("previous")) { + info.isPlaceholder = true; + } + + infoMap.insert(uuid, info); + } + return infoMap; +} + +QList WiringListExport::collectConductors(const QDomElement &root) const +{ + QList conductors; + QDomNodeList conductorNodes = root.elementsByTagName("conductor"); + + for (int i = 0; i < conductorNodes.size(); ++i) { + QDomElement cond = conductorNodes.at(i).toElement(); + + if (cond.attribute("num") == "Brücke") continue; + + ConductorData data; + data.index = i; + data.el1_uuid = normalizeUuid(cond.attribute("element1", cond.attribute("element1id", ""))); + data.el2_uuid = normalizeUuid(cond.attribute("element2", cond.attribute("element2id", ""))); + + data.element1_label = cond.attribute("element1_label"); + data.element2_label = cond.attribute("element2_label"); + data.terminalname1 = cond.attribute("terminalname1"); + data.terminalname2 = cond.attribute("terminalname2"); + data.tension_protocol = cond.attribute("tension_protocol"); + data.conductor_color = cond.attribute("conductor_color"); + data.conductor_section = cond.attribute("conductor_section"); + data.function = cond.attribute("function"); + + QDomElement diag = climbToDiagram(cond); + data.folio = findDiagramFolio(diag); + if (data.folio.isEmpty()) data.folio = cond.attribute("folio", cond.attribute("page", "")); + + conductors.append(data); + } + return conductors; +} + +void WiringListExport::resolveEndpoints(QList &conductors, const QMap &elementsInfo) const +{ + QRegularExpression numericLabelRe("^\\d+(\\.\\d+)?$"); + + QMap> el_to_cons; + for (const ConductorData &c : conductors) { + if (!c.el1_uuid.isEmpty()) el_to_cons[c.el1_uuid].append(c); + if (!c.el2_uuid.isEmpty()) el_to_cons[c.el2_uuid].append(c); + } + + for (int i = 0; i < conductors.size(); ++i) { + ConductorData &c = conductors[i]; + + auto resolveSide = [&](const QString &startUuid, QString &outLabel, QString &outTerminal) { + if (startUuid.isEmpty() || !elementsInfo.contains(startUuid)) return; + + const ElementInfo &startInfo = elementsInfo[startUuid]; + if (!startInfo.links.isEmpty() || startInfo.isPlaceholder) { + QQueue q; + QSet visited; + q.enqueue(startUuid); + visited.insert(startUuid); + + int depth = 0; + while (!q.isEmpty() && depth < 3) { + int levelSize = q.size(); + for (int k = 0; k < levelSize; ++k) { + QString curr = q.dequeue(); + + if (elementsInfo.contains(curr)) { + const ElementInfo &currInfo = elementsInfo[curr]; + + if (!currInfo.isPlaceholder && !currInfo.label.isEmpty() && !numericLabelRe.match(currInfo.label).hasMatch()) { + outLabel = currInfo.label; + return; + } + + for (const QString &lnk : currInfo.links) { + if (!visited.contains(lnk)) { + visited.insert(lnk); + q.enqueue(lnk); + } + } + } + + for (const ConductorData &cond : el_to_cons.value(curr)) { + if (cond.index == c.index) continue; + + QString other; + QString terminalHint; + if (cond.el1_uuid == curr) { + other = cond.el2_uuid; + terminalHint = cond.terminalname2; + } else { + other = cond.el1_uuid; + terminalHint = cond.terminalname1; + } + + if (!other.isEmpty() && !visited.contains(other)) { + if (elementsInfo.contains(other)) { + const ElementInfo &oInfo = elementsInfo[other]; + if (!oInfo.isPlaceholder && !oInfo.label.isEmpty() && !numericLabelRe.match(oInfo.label).hasMatch()) { + outLabel = oInfo.label; + if (outTerminal.isEmpty()) outTerminal = terminalHint; + return; + } + } + visited.insert(other); + q.enqueue(other); + } + } + } + depth++; + } + } else { + if (outLabel.isEmpty()) { + outLabel = startInfo.label.isEmpty() ? startInfo.name : startInfo.label; + } + } + }; + + bool p1 = elementsInfo.value(c.el1_uuid).isPlaceholder; + bool p2 = elementsInfo.value(c.el2_uuid).isPlaceholder; + + if (c.element1_label.isEmpty() || p1) { + if (p1) c.element1_label = ""; + resolveSide(c.el1_uuid, c.element1_label, c.terminalname1); + } + if (c.element2_label.isEmpty() || p2) { + if (p2) c.element2_label = ""; + resolveSide(c.el2_uuid, c.element2_label, c.terminalname2); + } + } +} + +void WiringListExport::toCsv() +{ + if (!m_project) return; + + QDomDocument doc = m_project->toXml(); + + if (doc.isNull()) { + QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet.")); + return; + } + + QFileDialog dialog(m_parent); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setWindowTitle(tr("Exporter le plan de câblage")); + dialog.setDefaultSuffix("csv"); + dialog.setNameFilter(tr("Fichiers CSV (*.csv)")); + + if (dialog.exec() != QDialog::Accepted) return; + QString fileName = dialog.selectedFiles().first(); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture.")); + return; + } + + QMap elementsInfo = collectElementsInfo(doc.documentElement()); + QList conductors = collectConductors(doc.documentElement()); + + resolveEndpoints(conductors, elementsInfo); + + QList uniqueConductors; + QSet seenConnections; + + for (const ConductorData &c : conductors) { + if (c.element1_label.isEmpty() && c.element2_label.isEmpty()) continue; + + QString sideA = c.element1_label + ":" + c.terminalname1; + QString sideB = c.element2_label + ":" + c.terminalname2; + + QString key = (sideA < sideB) ? (sideA + "||" + sideB) : (sideB + "||" + sideA); + + if (!seenConnections.contains(key)) { + seenConnections.insert(key); + uniqueConductors.append(c); + } + } + + QTextStream out(&file); + out << tr("Page", "Wiring list CSV header") << ";" + << tr("Composant 1", "Wiring list CSV header") << ";" + << tr("Borne 1", "Wiring list CSV header") << ";" + << tr("Composant 2", "Wiring list CSV header") << ";" + << tr("Borne 2", "Wiring list CSV header") << ";" + << tr("Tension / Protocole", "Wiring list CSV header") << ";" + << tr("Couleur du fil", "Wiring list CSV header") << ";" + << tr("Section du fil", "Wiring list CSV header") << ";" + << tr("Fonction", "Wiring list CSV header") << "\n"; + + for (const ConductorData &c : uniqueConductors) { + out << c.folio << ";" + << c.element1_label << ";" + << c.terminalname1 << ";" + << c.element2_label << ";" + << c.terminalname2 << ";" + << c.tension_protocol << ";" + << c.conductor_color << ";" + << c.conductor_section << ";" + << c.function << "\n"; + } + + file.close(); + QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !")); +} diff --git a/sources/wiringlistexport.h b/sources/wiringlistexport.h new file mode 100644 index 000000000..e865c58e2 --- /dev/null +++ b/sources/wiringlistexport.h @@ -0,0 +1,71 @@ +#ifndef WIRINGLISTEXPORT_H +#define WIRINGLISTEXPORT_H + +#include +#include +#include +#include +#include + +class QETProject; +class QWidget; +class QDomElement; +class QDomNode; + +// Internal data structures for parsing the XML graph +struct ElementInfo { + QString folio; + QStringList links; + QString label; + QString name; + bool isPlaceholder = false; +}; + +struct ConductorData { + int index = 0; + QString el1_uuid; + QString el2_uuid; + QString element1_label; + QString element2_label; + QString terminalname1; + QString terminalname2; + QString tension_protocol; + QString conductor_color; + QString conductor_section; + QString function; + QString folio; + + // Resolved endpoints + QString chosen_a_uuid; + QString chosen_a_label; + QString chosen_b_uuid; + QString chosen_b_label; +}; + +/** + * @brief The WiringListExport class + * Handles the export of the wiring list (Verdrahtungsplan) to a CSV file. + * Automatically resolves links and placeholders to find physical endpoints. + */ +class WiringListExport : public QObject +{ + Q_OBJECT +public: + explicit WiringListExport(QETProject *project, QWidget *parent = nullptr); + void toCsv(); + +private: + QETProject *m_project; + QWidget *m_parent; + + QString normalizeUuid(const QString &u) const; + QString findDiagramFolio(const QDomElement &diagramElem) const; + QDomElement climbToDiagram(QDomNode node) const; + + QMap collectElementsInfo(const QDomElement &root) const; + QList collectConductors(const QDomElement &root) const; + + void resolveEndpoints(QList &conductors, const QMap &elementsInfo) const; +}; + +#endif // WIRINGLISTEXPORT_H