/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech 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 General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "terminalstripautoimport.h" #include "../elementprovider.h" #include "../qetgraphicsitem/terminalelement.h" #include "../qetinformation.h" #include "../qetproject.h" #include "../utils/qetutils.h" #include "UndoCommand/addterminalstripcommand.h" #include "UndoCommand/addterminaltostripcommand.h" #include "UndoCommand/sortterminalstripcommand.h" #include "physicalterminal.h" #include "realterminal.h" #include "terminalstrip.h" #include #include #include namespace { /** * Terminals going to one strip : a label prefix plus the plant and * the location typed on the terminals. A plant or location set on * the terminals must match the strip exactly, an empty one accepts * every strip of the same name (in most projects the plant and the * location live on the strip, not on the terminals). */ struct StripGroup { QVector> free_terminals; QVector, TerminalStrip *>> placed_terminals; QString name, plant, location; }; QString elementInfo(const QSharedPointer &terminal, const QString &key) { auto element_ = terminal->element(); if (!element_) { return QString(); } return element_->elementData().m_informations.value(key).toString().trimmed(); } //An empty plant is saved and read back as "=" and an empty //location as "+" (the placeholders of the title block) : all //three spellings mean "no plant" / "no location" QString normalizedPlant(const QString &plant_) { const auto value_ = plant_.trimmed(); return (value_.isEmpty() || value_ == QLatin1String("=")) ? QString() : value_; } QString normalizedLocation(const QString &location_) { const auto value_ = location_.trimmed(); return (value_.isEmpty() || value_ == QLatin1String("+")) ? QString() : value_; } QString groupKey(const QString &name, const QString &plant, const QString &location) { return name + QChar(0x1f) + plant + QChar(0x1f) + location; } /** * @brief stripOf * The strip of the group : same name, and same plant and location * when the terminals carry some. Empty plant and location on the * terminals mean "no opinion" : any strip of the name is reused, * the one already holding terminals winning over an empty one. * @return the strip to work on, null when none has to exist yet. */ TerminalStrip *stripOf(QETProject *project, const QString &name, const QString &plant, const QString &location) { const bool strict_ = !plant.isEmpty() || !location.isEmpty(); TerminalStrip *best_ = nullptr; int best_score_ = -1; for (auto strip_ : project->terminalStrip()) { if (!strip_ || strip_->name() != name) { continue; } const bool same_ = normalizedPlant(strip_->installation()) == plant && normalizedLocation(strip_->location()) == location; if (strict_ && !same_) { continue; } //Same plant and location first, then the strip already //holding terminals over an empty one const int score_ = (same_ ? 4 : 0) + (strip_->realTerminals().isEmpty() ? 0 : 1); if (score_ > best_score_) { best_ = strip_; best_score_ = score_; } } return best_; } } /** * @brief TerminalStripAutoImport::importFreeTerminals * Put the terminals of @a project in the strip named after their label * prefix ("X1:2" goes to the strip "X1"), creating the strip when it * does not exist yet. The whole run is one undoable step. * * @a move_placed picks the action : false only places free terminals, * placed ones are never touched. True also moves a placed terminal * whose label prefix or plant and location no longer match its strip. * * In both cases the order the user gave to a strip is never changed, * only a strip created by the run itself is sorted. */ TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTerminals( QETProject *project, bool move_placed) { if (!project) { return ImportResult{}; } //Group every real terminal of the project, the free ones and //the ones already in a strip alike QMap groups_; const auto collect_ = [&groups_](const QSharedPointer &real_, TerminalStrip *current_) { if (!real_ || !real_->element()) { return; //Spare terminals (no element) are left alone } const auto name_ = stripNameForLabel(real_->label()); if (name_.isEmpty()) { return; } const auto plant_ = normalizedPlant( elementInfo(real_, QETInformation::ELMT_PLANT)); const auto location_ = normalizedLocation( elementInfo(real_, QETInformation::ELMT_LOCATION)); auto &group_ = groups_[groupKey(name_, plant_, location_)]; group_.name = name_; group_.plant = plant_; group_.location = location_; if (current_) { group_.placed_terminals.append({real_, current_}); } else { group_.free_terminals.append(real_); } }; const ElementProvider provider(project); for (auto terminal_ : provider.freeTerminal()) { if (terminal_) { collect_(terminal_->realTerminal(), nullptr); } } for (auto strip_ : project->terminalStrip()) { if (!strip_) { continue; } for (const auto &real_ : strip_->realTerminals()) { collect_(real_, strip_); } } if (groups_.isEmpty()) { return ImportResult{}; } for (auto it_ = groups_.begin(); it_ != groups_.end(); ++it_) { auto by_label_ = [](const QSharedPointer &a_, const QSharedPointer &b_) { return QETUtils::naturalLessThan(a_->label(), b_->label()); }; std::sort(it_.value().free_terminals.begin(), it_.value().free_terminals.end(), by_label_); std::sort(it_.value().placed_terminals.begin(), it_.value().placed_terminals.end(), [&by_label_](const auto &a_, const auto &b_) { return by_label_(a_.first, b_.first); }); } auto names_ = groups_.keys(); std::sort(names_.begin(), names_.end(), QETUtils::naturalLessThan); //First phase : work out what has to change, without touching the //project. When there is nothing to do, nothing is pushed : no //undo step is recorded and the project stays untouched. struct Plan { const StripGroup *group{nullptr}; TerminalStrip *strip{nullptr}; //Target strip, null : to create }; QVector plans; for (const auto &key_ : std::as_const(names_)) { auto &group_ = groups_[key_]; Plan plan_; plan_.group = &group_; plan_.strip = stripOf(project, group_.name, group_.plant, group_.location); //Free terminals always, placed ones only when the action //is allowed to re-group them bool changed_ = !group_.free_terminals.isEmpty(); if (move_placed) { for (const auto &entry_ : std::as_const(group_.placed_terminals)) { changed_ = changed_ || entry_.second != plan_.strip; } } if (changed_) { plans.append(plan_); } } if (plans.isEmpty()) { return ImportResult{}; } //Second phase : the changes, as one undoable step int added_ = 0, moved_ = 0, created_strips_ = 0; auto undo_ = project->undoStack(); undo_->beginMacro(move_placed ? QCoreApplication::translate("TerminalStripAutoImport", "Re-group all terminals") : QCoreApplication::translate("TerminalStripAutoImport", "Assign free terminals")); for (const auto &plan_ : std::as_const(plans)) { const auto &group_ = *plan_.group; bool created_ = false; auto strip_ = plan_.strip; if (!strip_) { strip_ = project->newTerminalStrip( group_.plant, group_.location, group_.name); undo_->push(new AddTerminalStripCommand(strip_, project)); ++created_strips_; created_ = true; } if (!group_.free_terminals.isEmpty()) { undo_->push(new AddTerminalToStripCommand(group_.free_terminals, strip_)); added_ += group_.free_terminals.size(); } //A terminal whose label prefix or plant and location no //longer match its strip moves to the right one, but only //when the action is allowed to re-group placed terminals if (move_placed) { for (const auto &entry_ : std::as_const(group_.placed_terminals)) { if (entry_.second == strip_) { continue; } undo_->push(new MoveTerminalCommand( entry_.first->physicalTerminal(), entry_.second, strip_)); ++moved_; } } //A strip just created is sorted, a strip already known //keeps the order its user gave it if (created_) { undo_->push(new SortTerminalStripCommand(strip_)); } } undo_->endMacro(); ImportResult result_; result_.terminals = added_ + moved_; result_.strips = created_strips_; return result_; }