VisualizationGraphWidget.cpp
427 lines
| 17.6 KiB
| text/x-c
|
CppLexer
r95 | #include "Visualization/VisualizationGraphWidget.h" | |||
Alexandre Leroux
|
r207 | #include "Visualization/IVisualizationWidgetVisitor.h" | ||
r243 | #include "Visualization/VisualizationGraphHelper.h" | |||
Alexandre Leroux
|
r480 | #include "Visualization/VisualizationGraphRenderingDelegate.h" | ||
r58 | #include "ui_VisualizationGraphWidget.h" | |||
r235 | #include <Data/ArrayData.h> | |||
#include <Data/IDataSeries.h> | ||||
Alexandre Leroux
|
r470 | #include <Settings/SqpSettingsDefs.h> | ||
r235 | #include <SqpApplication.h> | |||
r118 | #include <Variable/Variable.h> | |||
r235 | #include <Variable/VariableController.h> | |||
r118 | #include <unordered_map> | |||
Alexandre Leroux
|
r219 | Q_LOGGING_CATEGORY(LOG_VisualizationGraphWidget, "VisualizationGraphWidget") | ||
Alexandre Leroux
|
r179 | namespace { | ||
/// Key pressed to enable zoom on horizontal axis | ||||
const auto HORIZONTAL_ZOOM_MODIFIER = Qt::NoModifier; | ||||
/// Key pressed to enable zoom on vertical axis | ||||
const auto VERTICAL_ZOOM_MODIFIER = Qt::ControlModifier; | ||||
Alexandre Leroux
|
r470 | /// Gets a tolerance value from application settings. If the setting can't be found, the default | ||
/// value passed in parameter is returned | ||||
double toleranceValue(const QString &key, double defaultValue) noexcept | ||||
{ | ||||
return QSettings{}.value(key, defaultValue).toDouble(); | ||||
} | ||||
Alexandre Leroux
|
r179 | } // namespace | ||
r118 | struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { | |||
Alexandre Leroux
|
r480 | explicit VisualizationGraphWidgetPrivate() | ||
: m_DoSynchronize{true}, m_IsCalibration{false}, m_RenderingDelegate{nullptr} | ||||
{ | ||||
} | ||||
r445 | ||||
// Return the operation when range changed | ||||
VisualizationGraphWidgetZoomType getZoomType(const QCPRange &t1, const QCPRange &t2); | ||||
r444 | ||||
r118 | // 1 variable -> n qcpplot | |||
Alexandre Leroux
|
r270 | std::multimap<std::shared_ptr<Variable>, QCPAbstractPlottable *> m_VariableToPlotMultiMap; | ||
r444 | bool m_DoSynchronize; | |||
r445 | bool m_IsCalibration; | |||
Alexandre Leroux
|
r480 | QCPItemTracer *m_TextTracer; | ||
/// Delegate used to attach rendering features to the plot | ||||
std::unique_ptr<VisualizationGraphRenderingDelegate> m_RenderingDelegate; | ||||
r118 | }; | |||
Alexandre Leroux
|
r205 | VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget *parent) | ||
r120 | : QWidget{parent}, | |||
ui{new Ui::VisualizationGraphWidget}, | ||||
r118 | impl{spimpl::make_unique_impl<VisualizationGraphWidgetPrivate>()} | |||
r58 | { | |||
ui->setupUi(this); | ||||
Alexandre Leroux
|
r178 | |||
Alexandre Leroux
|
r480 | // The delegate must be initialized after the ui as it uses the plot | ||
impl->m_RenderingDelegate = std::make_unique<VisualizationGraphRenderingDelegate>(*ui->widget); | ||||
Alexandre Leroux
|
r266 | ui->graphNameLabel->setText(name); | ||
// 'Close' options : widget is deleted when closed | ||||
setAttribute(Qt::WA_DeleteOnClose); | ||||
connect(ui->closeButton, &QToolButton::clicked, this, &VisualizationGraphWidget::close); | ||||
ui->closeButton->setIcon(sqpApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton)); | ||||
Alexandre Leroux
|
r196 | |||
Alexandre Leroux
|
r178 | // Set qcpplot properties : | ||
Alexandre Leroux
|
r180 | // - Drag (on x-axis) and zoom are enabled | ||
Alexandre Leroux
|
r179 | // - Mouse wheel on qcpplot is intercepted to determine the zoom orientation | ||
Alexandre Leroux
|
r178 | ui->widget->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); | ||
Alexandre Leroux
|
r180 | ui->widget->axisRect()->setRangeDrag(Qt::Horizontal); | ||
Alexandre Leroux
|
r481 | |||
r445 | connect(ui->widget, &QCustomPlot::mousePress, this, &VisualizationGraphWidget::onMousePress); | |||
connect(ui->widget, &QCustomPlot::mouseRelease, this, | ||||
&VisualizationGraphWidget::onMouseRelease); | ||||
Alexandre Leroux
|
r481 | connect(ui->widget, &QCustomPlot::mouseMove, this, &VisualizationGraphWidget::onMouseMove); | ||
Alexandre Leroux
|
r179 | connect(ui->widget, &QCustomPlot::mouseWheel, this, &VisualizationGraphWidget::onMouseWheel); | ||
r444 | connect(ui->widget->xAxis, static_cast<void (QCPAxis::*)(const QCPRange &, const QCPRange &)>( | |||
&QCPAxis::rangeChanged), | ||||
r445 | this, &VisualizationGraphWidget::onRangeChanged, Qt::DirectConnection); | |||
Alexandre Leroux
|
r269 | |||
// Activates menu when right clicking on the graph | ||||
ui->widget->setContextMenuPolicy(Qt::CustomContextMenu); | ||||
connect(ui->widget, &QCustomPlot::customContextMenuRequested, this, | ||||
&VisualizationGraphWidget::onGraphMenuRequested); | ||||
r298 | ||||
connect(this, &VisualizationGraphWidget::requestDataLoading, &sqpApp->variableController(), | ||||
&VariableController::onRequestDataLoading); | ||||
r58 | } | |||
Alexandre Leroux
|
r227 | |||
r58 | VisualizationGraphWidget::~VisualizationGraphWidget() | |||
{ | ||||
delete ui; | ||||
} | ||||
r118 | ||||
r444 | void VisualizationGraphWidget::enableSynchronize(bool enable) | |||
{ | ||||
impl->m_DoSynchronize = enable; | ||||
} | ||||
r118 | void VisualizationGraphWidget::addVariable(std::shared_ptr<Variable> variable) | |||
{ | ||||
Alexandre Leroux
|
r184 | // Uses delegate to create the qcpplot components according to the variable | ||
r243 | auto createdPlottables = VisualizationGraphHelper::create(variable, *ui->widget); | |||
Alexandre Leroux
|
r184 | |||
for (auto createdPlottable : qAsConst(createdPlottables)) { | ||||
r235 | impl->m_VariableToPlotMultiMap.insert({variable, createdPlottable}); | |||
Alexandre Leroux
|
r184 | } | ||
r235 | ||||
r298 | connect(variable.get(), SIGNAL(updated()), this, SLOT(onDataCacheVariableUpdated())); | |||
r118 | } | |||
r314 | void VisualizationGraphWidget::addVariableUsingGraph(std::shared_ptr<Variable> variable) | |||
{ | ||||
// when adding a variable, we need to set its time range to the current graph range | ||||
auto grapheRange = ui->widget->xAxis->range(); | ||||
r512 | auto dateTime = SqpRange{grapheRange.lower, grapheRange.upper}; | |||
r314 | variable->setDateTime(dateTime); | |||
auto variableDateTimeWithTolerance = dateTime; | ||||
Alexandre Leroux
|
r470 | // add tolerance for each side | ||
auto toleranceFactor | ||||
= toleranceValue(GENERAL_TOLERANCE_AT_INIT_KEY, GENERAL_TOLERANCE_AT_INIT_DEFAULT_VALUE); | ||||
auto tolerance = toleranceFactor * (dateTime.m_TEnd - dateTime.m_TStart); | ||||
r314 | variableDateTimeWithTolerance.m_TStart -= tolerance; | |||
variableDateTimeWithTolerance.m_TEnd += tolerance; | ||||
// Uses delegate to create the qcpplot components according to the variable | ||||
auto createdPlottables = VisualizationGraphHelper::create(variable, *ui->widget); | ||||
for (auto createdPlottable : qAsConst(createdPlottables)) { | ||||
impl->m_VariableToPlotMultiMap.insert({variable, createdPlottable}); | ||||
} | ||||
connect(variable.get(), SIGNAL(updated()), this, SLOT(onDataCacheVariableUpdated())); | ||||
// CHangement detected, we need to ask controller to request data loading | ||||
emit requestDataLoading(variable, variableDateTimeWithTolerance); | ||||
} | ||||
Alexandre Leroux
|
r270 | void VisualizationGraphWidget::removeVariable(std::shared_ptr<Variable> variable) noexcept | ||
{ | ||||
Alexandre Leroux
|
r271 | // Each component associated to the variable : | ||
// - is removed from qcpplot (which deletes it) | ||||
// - is no longer referenced in the map | ||||
auto componentsIt = impl->m_VariableToPlotMultiMap.equal_range(variable); | ||||
for (auto it = componentsIt.first; it != componentsIt.second;) { | ||||
ui->widget->removePlottable(it->second); | ||||
it = impl->m_VariableToPlotMultiMap.erase(it); | ||||
} | ||||
// Updates graph | ||||
ui->widget->replot(); | ||||
Alexandre Leroux
|
r270 | } | ||
r512 | void VisualizationGraphWidget::setRange(std::shared_ptr<Variable> variable, const SqpRange &range) | |||
r438 | { | |||
r447 | // Note: in case of different axes that depends on variable, we could start with a code like | |||
// that: | ||||
r438 | // auto componentsIt = impl->m_VariableToPlotMultiMap.equal_range(variable); | |||
// for (auto it = componentsIt.first; it != componentsIt.second;) { | ||||
// } | ||||
ui->widget->xAxis->setRange(range.m_TStart, range.m_TEnd); | ||||
r444 | ui->widget->replot(); | |||
} | ||||
r512 | SqpRange VisualizationGraphWidget::graphRange() const noexcept | |||
r444 | { | |||
auto grapheRange = ui->widget->xAxis->range(); | ||||
r512 | return SqpRange{grapheRange.lower, grapheRange.upper}; | |||
r444 | } | |||
r512 | void VisualizationGraphWidget::setGraphRange(const SqpRange &range) | |||
r444 | { | |||
r445 | qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange START"); | |||
r444 | ui->widget->xAxis->setRange(range.m_TStart, range.m_TEnd); | |||
ui->widget->replot(); | ||||
qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange END"); | ||||
r438 | } | |||
Alexandre Leroux
|
r207 | void VisualizationGraphWidget::accept(IVisualizationWidgetVisitor *visitor) | ||
r118 | { | |||
Alexandre Leroux
|
r208 | if (visitor) { | ||
visitor->visit(this); | ||||
} | ||||
Alexandre Leroux
|
r219 | else { | ||
qCCritical(LOG_VisualizationGraphWidget()) | ||||
<< tr("Can't visit widget : the visitor is null"); | ||||
} | ||||
r118 | } | |||
Alexandre Leroux
|
r209 | bool VisualizationGraphWidget::canDrop(const Variable &variable) const | ||
{ | ||||
/// @todo : for the moment, a graph can always accomodate a variable | ||||
Q_UNUSED(variable); | ||||
return true; | ||||
} | ||||
Alexandre Leroux
|
r327 | bool VisualizationGraphWidget::contains(const Variable &variable) const | ||
{ | ||||
// Finds the variable among the keys of the map | ||||
auto variablePtr = &variable; | ||||
auto findVariable | ||||
= [variablePtr](const auto &entry) { return variablePtr == entry.first.get(); }; | ||||
auto end = impl->m_VariableToPlotMultiMap.cend(); | ||||
auto it = std::find_if(impl->m_VariableToPlotMultiMap.cbegin(), end, findVariable); | ||||
return it != end; | ||||
} | ||||
r119 | QString VisualizationGraphWidget::name() const | |||
r118 | { | |||
Alexandre Leroux
|
r266 | return ui->graphNameLabel->text(); | ||
r118 | } | |||
Alexandre Leroux
|
r269 | void VisualizationGraphWidget::onGraphMenuRequested(const QPoint &pos) noexcept | ||
r118 | { | |||
Alexandre Leroux
|
r269 | QMenu graphMenu{}; | ||
Alexandre Leroux
|
r270 | // Iterates on variables (unique keys) | ||
for (auto it = impl->m_VariableToPlotMultiMap.cbegin(), | ||||
end = impl->m_VariableToPlotMultiMap.cend(); | ||||
it != end; it = impl->m_VariableToPlotMultiMap.upper_bound(it->first)) { | ||||
// 'Remove variable' action | ||||
graphMenu.addAction(tr("Remove variable %1").arg(it->first->name()), | ||||
[ this, var = it->first ]() { removeVariable(var); }); | ||||
Alexandre Leroux
|
r196 | } | ||
Alexandre Leroux
|
r269 | |||
if (!graphMenu.isEmpty()) { | ||||
graphMenu.exec(mapToGlobal(pos)); | ||||
Alexandre Leroux
|
r196 | } | ||
r118 | } | |||
Alexandre Leroux
|
r179 | |||
r444 | void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange &t2) | |||
Alexandre Leroux
|
r227 | { | ||
r471 | qCInfo(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::onRangeChanged") | |||
<< QThread::currentThread()->objectName(); | ||||
r444 | ||||
r512 | auto dateTimeRange = SqpRange{t1.lower, t1.upper}; | |||
r235 | ||||
r445 | auto zoomType = impl->getZoomType(t1, t2); | |||
r235 | for (auto it = impl->m_VariableToPlotMultiMap.cbegin(); | |||
it != impl->m_VariableToPlotMultiMap.cend(); ++it) { | ||||
r258 | ||||
r235 | auto variable = it->first; | |||
r444 | auto currentDateTime = dateTimeRange; | |||
r235 | ||||
Alexandre Leroux
|
r470 | auto toleranceFactor = toleranceValue(GENERAL_TOLERANCE_AT_UPDATE_KEY, | ||
GENERAL_TOLERANCE_AT_UPDATE_DEFAULT_VALUE); | ||||
r444 | auto tolerance = toleranceFactor * (currentDateTime.m_TEnd - currentDateTime.m_TStart); | |||
auto variableDateTimeWithTolerance = currentDateTime; | ||||
r433 | variableDateTimeWithTolerance.m_TStart -= tolerance; | |||
variableDateTimeWithTolerance.m_TEnd += tolerance; | ||||
r444 | qCDebug(LOG_VisualizationGraphWidget()) << "r" << currentDateTime; | |||
qCDebug(LOG_VisualizationGraphWidget()) << "t" << variableDateTimeWithTolerance; | ||||
qCDebug(LOG_VisualizationGraphWidget()) << "v" << variable->dateTime(); | ||||
r433 | // If new range with tol is upper than variable datetime parameters. we need to request new | |||
// data | ||||
if (!variable->contains(variableDateTimeWithTolerance)) { | ||||
r258 | ||||
r444 | auto variableDateTimeWithTolerance = currentDateTime; | |||
if (!variable->isInside(currentDateTime)) { | ||||
r258 | auto variableDateTime = variable->dateTime(); | |||
r444 | if (variable->contains(variableDateTimeWithTolerance)) { | |||
r445 | qCDebug(LOG_VisualizationGraphWidget()) | |||
r444 | << tr("TORM: Detection zoom in that need request:"); | |||
Alexandre Leroux
|
r460 | // add tolerance for each side | ||
r444 | tolerance | |||
= toleranceFactor * (currentDateTime.m_TEnd - currentDateTime.m_TStart); | ||||
variableDateTimeWithTolerance.m_TStart -= tolerance; | ||||
variableDateTimeWithTolerance.m_TEnd += tolerance; | ||||
} | ||||
else if (variableDateTime.m_TStart < currentDateTime.m_TStart) { | ||||
qCInfo(LOG_VisualizationGraphWidget()) << tr("TORM: Detection pan to right:"); | ||||
r263 | ||||
r444 | auto diffEndToKeepDelta = currentDateTime.m_TEnd - variableDateTime.m_TEnd; | |||
currentDateTime.m_TStart = variableDateTime.m_TStart + diffEndToKeepDelta; | ||||
r263 | // Tolerance have to be added to the right | |||
r438 | // add tolerance for right (end) side | |||
r444 | tolerance | |||
= toleranceFactor * (currentDateTime.m_TEnd - currentDateTime.m_TStart); | ||||
r260 | variableDateTimeWithTolerance.m_TEnd += tolerance; | |||
r258 | } | |||
r444 | else if (variableDateTime.m_TEnd > currentDateTime.m_TEnd) { | |||
r445 | qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: Detection pan to left: "); | |||
r444 | auto diffStartToKeepDelta | |||
= variableDateTime.m_TStart - currentDateTime.m_TStart; | ||||
currentDateTime.m_TEnd = variableDateTime.m_TEnd - diffStartToKeepDelta; | ||||
r263 | // Tolerance have to be added to the left | |||
r438 | // add tolerance for left (start) side | |||
r444 | tolerance | |||
= toleranceFactor * (currentDateTime.m_TEnd - currentDateTime.m_TStart); | ||||
r260 | variableDateTimeWithTolerance.m_TStart -= tolerance; | |||
r258 | } | |||
r318 | else { | |||
r445 | qCCritical(LOG_VisualizationGraphWidget()) | |||
r318 | << tr("Detection anormal zoom detection: "); | |||
} | ||||
r258 | } | |||
else { | ||||
r445 | qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: Detection zoom out: "); | |||
Alexandre Leroux
|
r460 | // add tolerance for each side | ||
r444 | tolerance = toleranceFactor * (currentDateTime.m_TEnd - currentDateTime.m_TStart); | |||
r260 | variableDateTimeWithTolerance.m_TStart -= tolerance; | |||
variableDateTimeWithTolerance.m_TEnd += tolerance; | ||||
r444 | zoomType = VisualizationGraphWidgetZoomType::ZoomOut; | |||
r258 | } | |||
r433 | if (!variable->contains(dateTimeRange)) { | |||
r445 | qCDebug(LOG_VisualizationGraphWidget()) | |||
r444 | << "TORM: Modif on variable datetime detected" << currentDateTime; | |||
variable->setDateTime(currentDateTime); | ||||
r433 | } | |||
r260 | ||||
r445 | qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: Request data detection: "); | |||
r260 | // CHangement detected, we need to ask controller to request data loading | |||
r298 | emit requestDataLoading(variable, variableDateTimeWithTolerance); | |||
r235 | } | |||
r318 | else { | |||
r444 | qCInfo(LOG_VisualizationGraphWidget()) | |||
<< tr("TORM: Detection zoom in that doesn't need request: "); | ||||
zoomType = VisualizationGraphWidgetZoomType::ZoomIn; | ||||
r318 | } | |||
Alexandre Leroux
|
r227 | } | ||
r444 | ||||
r445 | if (impl->m_DoSynchronize && !impl->m_IsCalibration) { | |||
r512 | auto oldDateTime = SqpRange{t2.lower, t2.upper}; | |||
r444 | qCDebug(LOG_VisualizationGraphWidget()) | |||
<< tr("TORM: VisualizationGraphWidget::Synchronize notify !!") | ||||
<< QThread::currentThread()->objectName(); | ||||
emit synchronize(dateTimeRange, oldDateTime, zoomType); | ||||
} | ||||
Alexandre Leroux
|
r227 | } | ||
Alexandre Leroux
|
r481 | void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept | ||
{ | ||||
// Handles plot rendering when mouse is moving | ||||
impl->m_RenderingDelegate->onMouseMove(event); | ||||
} | ||||
Alexandre Leroux
|
r179 | void VisualizationGraphWidget::onMouseWheel(QWheelEvent *event) noexcept | ||
{ | ||||
auto zoomOrientations = QFlags<Qt::Orientation>{}; | ||||
r260 | // Lambda that enables a zoom orientation if the key modifier related to this orientation | |||
// has | ||||
Alexandre Leroux
|
r179 | // been pressed | ||
auto enableOrientation | ||||
= [&zoomOrientations, event](const auto &orientation, const auto &modifier) { | ||||
auto orientationEnabled = event->modifiers().testFlag(modifier); | ||||
zoomOrientations.setFlag(orientation, orientationEnabled); | ||||
}; | ||||
enableOrientation(Qt::Vertical, VERTICAL_ZOOM_MODIFIER); | ||||
enableOrientation(Qt::Horizontal, HORIZONTAL_ZOOM_MODIFIER); | ||||
ui->widget->axisRect()->setRangeZoom(zoomOrientations); | ||||
} | ||||
r235 | ||||
r445 | void VisualizationGraphWidget::onMousePress(QMouseEvent *event) noexcept | |||
{ | ||||
impl->m_IsCalibration = event->modifiers().testFlag(Qt::ControlModifier); | ||||
} | ||||
void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept | ||||
{ | ||||
impl->m_IsCalibration = false; | ||||
} | ||||
r235 | void VisualizationGraphWidget::onDataCacheVariableUpdated() | |||
{ | ||||
r243 | // NOTE: | |||
r260 | // We don't want to call the method for each component of a variable unitarily, but for | |||
// all | ||||
r243 | // its components at once (eg its three components in the case of a vector). | |||
// The unordered_multimap does not do this easily, so the question is whether to: | ||||
// - use an ordered_multimap and the algos of std to group the values by key | ||||
// - use a map (unique keys) and store as values directly the list of components | ||||
r433 | auto grapheRange = ui->widget->xAxis->range(); | |||
r512 | auto dateTime = SqpRange{grapheRange.lower, grapheRange.upper}; | |||
r433 | ||||
r235 | for (auto it = impl->m_VariableToPlotMultiMap.cbegin(); | |||
it != impl->m_VariableToPlotMultiMap.cend(); ++it) { | ||||
auto variable = it->first; | ||||
r441 | qCDebug(LOG_VisualizationGraphWidget()) | |||
r433 | << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated S" | |||
<< variable->dateTime(); | ||||
r441 | qCDebug(LOG_VisualizationGraphWidget()) | |||
r433 | << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated E" << dateTime; | |||
if (dateTime.contains(variable->dateTime()) || dateTime.intersect(variable->dateTime())) { | ||||
VisualizationGraphHelper::updateData(QVector<QCPAbstractPlottable *>{} << it->second, | ||||
variable->dataSeries(), variable->dateTime()); | ||||
} | ||||
r235 | } | |||
} | ||||
r445 | ||||
VisualizationGraphWidgetZoomType | ||||
VisualizationGraphWidget::VisualizationGraphWidgetPrivate::getZoomType(const QCPRange &t1, | ||||
const QCPRange &t2) | ||||
{ | ||||
// t1.lower <= t2.lower && t2.upper <= t1.upper | ||||
auto zoomType = VisualizationGraphWidgetZoomType::Unknown; | ||||
if (t1.lower <= t2.lower && t2.upper <= t1.upper) { | ||||
zoomType = VisualizationGraphWidgetZoomType::ZoomOut; | ||||
} | ||||
else if (t1.lower > t2.lower && t1.upper > t2.upper) { | ||||
zoomType = VisualizationGraphWidgetZoomType::PanRight; | ||||
} | ||||
else if (t1.lower < t2.lower && t1.upper < t2.upper) { | ||||
zoomType = VisualizationGraphWidgetZoomType::PanLeft; | ||||
} | ||||
else if (t1.lower > t2.lower && t2.upper > t1.upper) { | ||||
zoomType = VisualizationGraphWidgetZoomType::ZoomIn; | ||||
} | ||||
else { | ||||
qCCritical(LOG_VisualizationGraphWidget()) << "getZoomType: Unknown type detected"; | ||||
} | ||||
return zoomType; | ||||
} | ||||