/*
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 "cablerenumber.h"
#include "cable.h"
#include "../autoNum/assignvariables.h"
#include "../autoNum/numerotationcontextcommands.h"
#include "../diagram.h"
#include "../qetproject.h"
#include
#include
#include
#include
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 ordered;
QSet 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::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;
}