Files
qelectrotech-source-mirror/sources/cable/cablerenumber.cpp
T
Kellermorph d55a3c7881 Fix cable numbering rule questions: per project, and a first rule counts
Two defects found while re-testing the cable management after the
upstream merge, plus one change of intent for the numbering question.

The question "define a cable numbering rule now?" kept its answer in
the program settings, so saying no once silenced every project of the
program -- the exact opposite of what it should do. The answer now
travels with the project itself, stored on the <cable_autonums>
element as ask_numbering_rule="false"; a file which does not hold the
answer asks again, and taking the rule away in the project properties
or in the numbering window brings the question back for that project
alone. Read-only projects are no longer asked at all, since there is
nowhere in them to define a rule.

The numbering window could not hand out the first rule of a project:
it drew the rule, worked the numbers out with it, and only wrote the
rule into the project together with those numbers -- while the
planning refused to run on a project which had no rule saved yet.
The counters of a cable rule are kept under its fixed name anyway, so
planning now falls back to that name when the project holds no rule
yet: a rule which is being drawn in the window counts from the moment
it is complete, and the first renumbering of a project works.
2026-10-09 20:22:45 +02:00

293 lines
9.2 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
#include "cablerenumber.h"
#include "cable.h"
#include "../autoNum/assignvariables.h"
#include "../autoNum/numerotationcontextcommands.h"
#include "../diagram.h"
#include "../qetproject.h"
#include <QCoreApplication>
#include <QSet>
#include <algorithm>
#include <limits>
namespace {
/**
@brief One cable of the project, and where it stands so that the
cables can be laid out in an order everybody can see again.
*/
struct OrderedCable
{
Cable *cable = nullptr;
///The lowest folio the cable is drawn on
int folio = 0;
///How far it reaches to the left and how far to the top on
///that folio
qreal left = 0.0;
qreal top = 0.0;
///The folio the rule is read on for this cable
Diagram *diagram = nullptr;
};
/**
@brief Read a rule once, the way the numbering of a cable reads it.
The rule is read on a copy, since reading it fills in what its
folio-bound parts are worth on that one folio, and that must not
creep into the rule itself.
@param context the rule as it stands at this point of the numbering
@param diagram the folio the number is worked out on
@param key the name the rule is kept under in the project
@return the number, empty when the rule says nothing here
*/
QString readRule(NumerotationContext context, Diagram *diagram, const QString &key)
{
const QString formula = autonum::numerotationContextToFormula(context);
autonum::sequentialNumbers seq;
autonum::setSequential(formula, seq, context, diagram, key);
return autonum::AssignVariables::formulaToLabel(formula, seq, diagram);
}
} // namespace
/**
@brief CableRenumber::byHandCount
@param project
@return how many cables of that project carry a name typed in by hand
*/
int CableRenumber::byHandCount(QETProject *project)
{
if (!project) return 0;
int count = 0;
for (Cable *cable : project->cables())
{
if (cable && cable->designationByHand()) {
++count;
}
}
return count;
}
/**
@brief CableRenumber::plan
Work the number of every cable of the project out, without writing
anywhere. The cables are put in the order they are numbered in --
folio by folio, and on each folio along whichever axis leads -- and
the rule is then read from its beginning once for each of them, so
that the numbering runs on without a hole.
@param project the project whose cables are numbered
@param rule the numbering rule to read
@param x_axis_first true when the leftmost cable of a folio comes first
@param include_by_hand false to leave the names typed in by hand alone
@return the numbers, or what stopped the numbering
*/
CableRenumberPlan CableRenumber::plan(QETProject *project,
const NumerotationContext &rule,
bool x_axis_first,
bool include_by_hand)
{
CableRenumberPlan result;
if (!project)
{
result.error = QCoreApplication::translate(
"CableRenumber", "Aucun projet n'est ouvert.");
return result;
}
if (project->isReadOnly())
{
result.error = QCoreApplication::translate(
"CableRenumber",
"Ce projet est en lecture seule : ses câbles ne peuvent pas "
"être renumérotés.");
return result;
}
//The rule may be one the numbering window is drawing right now
//and has not written into the project yet: the numbering writes
//it together with the numbers themselves, so the project does
//not have to hold it beforehand. What the counters are kept
//under is the name the rule is stored with, which for cables is
//fixed either way -- so numbering starts from a project which
//has no rule saved yet just as well.
QString key = project->cableCurrentAutoNum();
if (key.isEmpty()) {
key = QETProject::cableAutoNumRuleName();
}
if (rule.isEmpty())
{
result.error = QCoreApplication::translate(
"CableRenumber",
"Aucune règle de numérotation des câbles n'est définie pour ce "
"projet. Définissez-en une dans cette fenêtre avant de "
"renuméroter ses câbles.");
return result;
}
//Where every cable stands, and which ones are left alone: their
//names stay as they are, so the numbering must not hand the
//same number out again
QList<OrderedCable> ordered;
QSet<QString> taken;
for (Cable *cable : project->cables())
{
if (!cable) continue;
if (!include_by_hand && cable->designationByHand())
{
if (!cable->designation().isEmpty()) {
taken.insert(cable->designation());
}
continue;
}
OrderedCable entry;
entry.cable = cable;
for (const CablePartData &part : cable->parts())
{
Diagram *diagram = project->diagramByUuid(part.diagram);
if (!diagram) continue;
const int index = project->folioIndex(diagram);
const qreal left = qMin(part.p1.x(), part.p2.x());
const qreal top = qMin(part.p1.y(), part.p2.y());
if (!entry.diagram || index < entry.folio)
{
entry.folio = index;
entry.diagram = diagram;
entry.left = left;
entry.top = top;
} else if (index == entry.folio) {
entry.left = qMin(entry.left, left);
entry.top = qMin(entry.top, top);
}
}
//A cable which stands on no folio the project still knows
//is numbered last rather than first by accident
if (!entry.diagram) {
entry.folio = std::numeric_limits<int>::max();
}
ordered.append(entry);
}
//Folio by folio, and on each folio along the axis which leads:
//the other axis is only asked when the leading one says two
//cables are level, so that the numbering never depends on the
//order the cables happen to be stored in
std::sort(ordered.begin(), ordered.end(),
[x_axis_first](const OrderedCable &a, const OrderedCable &b)
{
if (a.folio != b.folio) return a.folio < b.folio;
const qreal a_first = x_axis_first ? a.left : a.top;
const qreal b_first = x_axis_first ? b.left : b.top;
if (a_first != b_first) return a_first < b_first;
const qreal a_second = x_axis_first ? a.top : a.left;
const qreal b_second = x_axis_first ? b.top : b.left;
if (a_second != b_second) return a_second < b_second;
return a.cable->uuid().toString() < b.cable->uuid().toString();
});
//Read the rule from the beginning, once for each cable: its
//counters are put back first, since numbering a project over
//starts at the beginning whatever the last number was
NumerotationContext context = autonum::resetContextCounters(rule);
int current_folio = -1;
for (const OrderedCable &entry : std::as_const(ordered))
{
if (!entry.diagram)
{
result.error = QCoreApplication::translate(
"CableRenumber",
"Un câble n'est sur aucun folio du projet : rien n'a été "
"changé.");
return result;
}
//A counter bound to the folio counts from the beginning of
//that folio: the first cable of a folio gets the first
//number of it, and the folios after it start again rather
//than carrying the number of the folio before on
if (entry.folio != current_folio)
{
context = autonum::resetFolioCounters(context);
current_folio = entry.folio;
}
const QString candidate = readRule(context, entry.diagram, key);
if (candidate.isEmpty())
{
result.error = QCoreApplication::translate(
"CableRenumber",
"La règle en cours ne donne aucun numéro ici : rien n'a été "
"changé.");
return result;
}
//A rule which does not count hands out the same number
//twice, and two cables of one project may not be called
//the same thing
if (result.designations.contains(candidate))
{
result.error = QCoreApplication::translate(
"CableRenumber",
"La règle en cours donne deux fois le même numéro (%1) : "
"elle doit compter pour renuméroter un projet entier. "
"Rien n'a été changé.")
.arg(candidate);
return result;
}
//A name which stays because it was typed in keeps its
//number, and the numbering may not hand that number out
if (taken.contains(candidate))
{
result.error = QCoreApplication::translate(
"CableRenumber",
"Le numéro %1 est déjà porté par un câble dont le nom est "
"laissé tel quel : rien n'a été changé.")
.arg(candidate);
return result;
}
taken.insert(candidate);
result.cables.append(entry.cable);
result.designations.append(candidate);
//Step the rule on for the next cable; a rule which cannot
//step stays where it is and is caught by the check above
NumerotationContextCommands ncc(context, entry.diagram);
const NumerotationContext following = ncc.next();
if (following.size() == context.size()) {
context = following;
}
}
//Where the rule stands after the last cable has been numbered:
//the caller writes that into the project, so numbering goes on
//from there instead of from wherever the rule happened to stand
//before this numbering ran
result.after = context;
return result;
}