|
@@ -14,18 +14,26 @@
|
|
|
#define Y_AXIS_NUMBER_GAP_LENGTH 20
|
|
|
|
|
|
|
|
|
-GraphView::GraphView(QWidget* parent, QString title) :QWidget(parent), title(title), linePen(Qt::blue), rectPen(Qt::red), axisPen(Qt::black)
|
|
|
+GraphView::GraphView(QWidget* parent, QString title, Bound fixedBound):QWidget(parent), title(title), fixedBound(fixedBound), linePen(Qt::blue), rectPen(QColor(255,255,255,255)), axisPen(Qt::black)
|
|
|
{
|
|
|
|
|
|
linePen.setJoinStyle(Qt::PenJoinStyle::RoundJoin);
|
|
|
-
|
|
|
+ qDebug() << "minX:" << fixedBound.minX << " maxX:" << fixedBound.maxX << " minY:" << fixedBound.minY << " maxY:" << fixedBound.maxY;
|
|
|
+ rectPen.setWidth(0);
|
|
|
//Populate data with DummyData
|
|
|
//Draw Points
|
|
|
std::random_device rd; //Will be used to obtain a seed for the random number engine
|
|
|
std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
|
|
|
std::uniform_real_distribution<double> doubleDistr(0.0, 1.0);
|
|
|
hueoffset = doubleDistr(gen);
|
|
|
-
|
|
|
+ QRect graphDisplayRect(rect());
|
|
|
+ graphDisplayRect.setBottom(graphDisplayRect.bottom() - 20);
|
|
|
+ graphDisplayRect.setLeft(graphDisplayRect.left() + 50);
|
|
|
+ graphDisplayRect.setTop(graphDisplayRect.top() + 20);
|
|
|
+ graphDisplayRect.setRight(graphDisplayRect.right() - 10);
|
|
|
+ graphDisplayRect.setWidth(graphDisplayRect.width() - (graphDisplayRect.width() % X_AXIS_GAP_AMOUNT) + 1);
|
|
|
+ graphDisplayRect.setHeight(graphDisplayRect.height() - (graphDisplayRect.height() % Y_AXIS_GAP_AMOUNT) + 1);
|
|
|
+
|
|
|
}
|
|
|
|
|
|
GraphView::~GraphView()
|
|
@@ -50,9 +58,94 @@ void GraphView::paintEvent(QPaintEvent* event)
|
|
|
graphDisplayRect.setWidth(graphDisplayRect.width() - (graphDisplayRect.width() % X_AXIS_GAP_AMOUNT) + 1);
|
|
|
graphDisplayRect.setHeight(graphDisplayRect.height() - (graphDisplayRect.height() % Y_AXIS_GAP_AMOUNT) + 1);
|
|
|
|
|
|
+ painter.setBrush(rectPen.color());
|
|
|
+ painter.drawRect(graphDisplayRect);
|
|
|
+ painter.setBrush(Qt::BrushStyle::NoBrush);
|
|
|
+
|
|
|
+ double stregth_factorX = graphDisplayRect.width() / rangeGraphX;
|
|
|
+ double stregth_factorY = graphDisplayRect.height() / rangeGraphY;
|
|
|
+ for (GraphSeries graphSeries : graphSeriesVec) {
|
|
|
+ linePen.setColor(graphSeries.color);
|
|
|
+ painter.setPen(linePen);
|
|
|
+ QPointF translation(graphDisplayRect.left(), graphDisplayRect.top());
|
|
|
+
|
|
|
+ if (graphSeries.type == GraphSeries::SeriesType::Line || graphSeries.type == GraphSeries::SeriesType::LineDot) {
|
|
|
+ linePen.setWidth(graphSeries.lineWidth);
|
|
|
+ painter.setPen(linePen);
|
|
|
+ QPainterPath painterPath;
|
|
|
+
|
|
|
+ int i;
|
|
|
+ for (i = 0; i < graphSeries.data->size(); i++) {
|
|
|
+ QPointF& point = graphSeries.data->at(i);
|
|
|
+ /*if (!inBoundX(point)) continue;*/
|
|
|
+ painterPath.moveTo(transformPoint(point, stregth_factorX, stregth_factorY));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ for (; i < graphSeries.data->size(); i++) {
|
|
|
+ QPointF& point = graphSeries.data->at(i);
|
|
|
+ //if (!inBoundX(point)) break;
|
|
|
+ //if (point.y() > actualBound.maxY) {
|
|
|
+ // //Generate new Point
|
|
|
+ // QPointF& oldpoint = graphSeries.data->at(i - 1);
|
|
|
+ // double xdif = std::abs(point.x() - oldpoint.x());
|
|
|
+ // double ydif = std::abs(point.y() - oldpoint.y());
|
|
|
+ // double newX = (xdif / ydif) * actualBound.maxY;
|
|
|
+ // QPointF newPoint(newX, actualBound.maxY);
|
|
|
+ //
|
|
|
+ //}
|
|
|
+ //else {
|
|
|
+
|
|
|
+ //}
|
|
|
+ painterPath.lineTo(transformPoint(point, stregth_factorX, stregth_factorY));
|
|
|
+ }
|
|
|
+ /*
|
|
|
+ std::pair<std::vector<QPointF>::iterator, std::vector<QPointF>::iterator> pair = graphSeries.getFirstLastIndexInSeriesThatsInBound(actualBound.minX, actualBound.maxX);
|
|
|
+ qDebug() << "hello";
|
|
|
+ painterPath.moveTo(transformPoint(*pair.first++, stregth_factorX, stregth_factorY));
|
|
|
+ for (std::vector<QPointF>::iterator it = pair.first; it != pair.second; it++) {
|
|
|
+ painterPath.lineTo(transformPoint(*it, stregth_factorX, stregth_factorY));
|
|
|
+ }*/
|
|
|
+ painterPath.translate(translation);
|
|
|
+ painter.drawPath(painterPath);
|
|
|
+ }
|
|
|
+ if (graphSeries.type == GraphSeries::SeriesType::Dot || graphSeries.type == GraphSeries::SeriesType::LineDot) {
|
|
|
+ painter.setBrush(graphSeries.color);
|
|
|
+ for (int i = 0; i < graphSeries.data->size(); i++) {
|
|
|
+ painter.drawEllipse(transformPoint(graphSeries.data->at(i), stregth_factorX, stregth_factorY) + translation, graphSeries.circleRadius, graphSeries.circleRadius);
|
|
|
+ }
|
|
|
+ painter.setBrush(Qt::BrushStyle::NoBrush);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ //Draw White Rect for axis
|
|
|
+ painter.setPen(rectPen);
|
|
|
+
|
|
|
+ painter.setBrush(rectPen.color());
|
|
|
+ QRect leftRect(rect().topLeft(), QPoint(graphDisplayRect.left() - 1, rect().bottom()));
|
|
|
+ QRect topRect(rect().topLeft(), QPoint(rect().right(), graphDisplayRect.top() - 1));
|
|
|
+ QRect bottomRect(QPoint(rect().left(), graphDisplayRect.bottom()), rect().bottomRight());
|
|
|
+ QRect rightRect(QPoint(graphDisplayRect.right(), rect().top()), rect().bottomRight());
|
|
|
+ painter.drawRect(leftRect);
|
|
|
+ painter.drawRect(topRect);
|
|
|
+ painter.drawRect(bottomRect);
|
|
|
+ painter.drawRect(rightRect);
|
|
|
+
|
|
|
+ painter.setBrush(Qt::BrushStyle::NoBrush);
|
|
|
+ //painter.drawRect(yAxisRect);
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
//Font for Axis;
|
|
|
painter.setFont(QFont("Arial", 8));
|
|
|
//draw X-Axis
|
|
|
+ painter.setPen(axisPen);
|
|
|
QRect xAxisRect(QPoint(graphDisplayRect.left(), graphDisplayRect.bottom()), QPoint(graphDisplayRect.right(), rect().bottom()));
|
|
|
QPainterPath xAxisPath;
|
|
|
xAxisPath.moveTo(xAxisRect.left(), xAxisRect.top());
|
|
@@ -84,65 +177,135 @@ void GraphView::paintEvent(QPaintEvent* event)
|
|
|
|
|
|
|
|
|
|
|
|
+ qDebug() << "minX:" << actualBound.minX << " maxX:" << actualBound.maxX << " minY:" << actualBound.minY << " maxY:" << actualBound.maxY;
|
|
|
+
|
|
|
|
|
|
+
|
|
|
+}
|
|
|
|
|
|
+QPointF GraphView::transformPoint(QPointF& point, double stregth_factorX, double stregth_factorY) const
|
|
|
+{
|
|
|
+ return QPointF((point.x() - actualBound.minX) * stregth_factorX, (actualBound.maxY - point.y()) * stregth_factorY);
|
|
|
+}
|
|
|
|
|
|
- painter.setPen(linePen);
|
|
|
- double stregth_factorX = graphDisplayRect.width() / rangeGraphX;
|
|
|
- double stregth_factorY = graphDisplayRect.height() / rangeGraphY;
|
|
|
- for (GraphSeries graphSeries : graphSeriesVec) {
|
|
|
- linePen.setColor(graphSeries.color);
|
|
|
- painter.setPen(linePen);
|
|
|
- QPointF translation(graphDisplayRect.left(), graphDisplayRect.top());
|
|
|
+bool GraphView::inBoundX(QPointF& point)
|
|
|
+{
|
|
|
|
|
|
- if (graphSeries.type == GraphSeries::SeriesType::Line || graphSeries.type == GraphSeries::SeriesType::LineDot) {
|
|
|
- linePen.setWidth(graphSeries.lineWidth);
|
|
|
- painter.setPen(linePen);
|
|
|
- QPainterPath painterPath;
|
|
|
- painterPath.moveTo(transformPoint(graphSeries.data->at(0), stregth_factorX, stregth_factorY));
|
|
|
- for (int i = 1; i < graphSeries.data->size(); i++) {
|
|
|
- painterPath.lineTo(transformPoint(graphSeries.data->at(i), stregth_factorX, stregth_factorY));
|
|
|
- }
|
|
|
- painterPath.translate(translation);
|
|
|
- painter.drawPath(painterPath);
|
|
|
+ return point.x() >= actualBound.minX && point.x() <= actualBound.maxX;
|
|
|
+}
|
|
|
+
|
|
|
+void GraphView::keyPressEvent(QKeyEvent* event)
|
|
|
+{
|
|
|
+ switch (event->key()) {
|
|
|
+ case Qt::Key_Up:
|
|
|
+ qDebug() << "Up pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::MoveUp);
|
|
|
+ break;
|
|
|
+ case Qt::Key_Right:
|
|
|
+ qDebug() << "Right pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::MoveRight);
|
|
|
+ break;
|
|
|
+ case Qt::Key_Left:
|
|
|
+ qDebug() << "Key_Left pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::MoveLeft);
|
|
|
+ break;
|
|
|
+ case Qt::Key_Down:
|
|
|
+ qDebug() << "Key_Down pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::MoveDown);
|
|
|
+ break;
|
|
|
+ case Qt::Key_PageUp:
|
|
|
+ case Qt::Key_Plus:
|
|
|
+ qDebug() << "Key_Down pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomIn);
|
|
|
+ break;
|
|
|
+ case Qt::Key_PageDown:
|
|
|
+ case Qt::Key_Minus:
|
|
|
+ qDebug() << "Key_Down pressed";
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomOut);
|
|
|
+ break;
|
|
|
+ case Qt::Key_R:
|
|
|
+ fixedBound = totalBound;
|
|
|
+ calculateRangeXY();
|
|
|
+ generateAxisNumberStrings();
|
|
|
+ repaint();
|
|
|
+ default:
|
|
|
+ QWidget::keyPressEvent(event);
|
|
|
+ }
|
|
|
+ //If not reacted forward:
|
|
|
+ QWidget::keyPressEvent(event);
|
|
|
+}
|
|
|
+
|
|
|
+void GraphView::wheelEvent(QWheelEvent* event)
|
|
|
+{
|
|
|
+ QPoint numPixels = event->pixelDelta();
|
|
|
+ QPoint numDegrees = event->angleDelta() / 8;
|
|
|
+
|
|
|
+ if (!numPixels.isNull()) {
|
|
|
+ //scrollWithPixels(numPixels);
|
|
|
+ qDebug() << "NumPixels:" << numPixels;
|
|
|
+ switch (numPixels.y()) {
|
|
|
+ case -1:
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomIn);
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomOut);
|
|
|
+ default:
|
|
|
+ break;
|
|
|
}
|
|
|
- if (graphSeries.type == GraphSeries::SeriesType::Dot || graphSeries.type == GraphSeries::SeriesType::LineDot) {
|
|
|
- painter.setBrush(graphSeries.color);
|
|
|
- for (int i = 0; i < graphSeries.data->size(); i++) {
|
|
|
- painter.drawEllipse(transformPoint(graphSeries.data->at(i), stregth_factorX, stregth_factorY) + translation, graphSeries.circleRadius, graphSeries.circleRadius);
|
|
|
- }
|
|
|
- painter.setBrush(Qt::BrushStyle::NoBrush);
|
|
|
+ }
|
|
|
+ else if (!numDegrees.isNull()) {
|
|
|
+ QPoint numSteps = numDegrees / 15;
|
|
|
+ qDebug() << "numSteps:" << numSteps;
|
|
|
+ switch (numSteps.y()) {
|
|
|
+ case 1:
|
|
|
+ qDebug() << "ZoomIn";
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomIn);
|
|
|
+ break;
|
|
|
+ case -1:
|
|
|
+ qDebug() << "ZoomOut";
|
|
|
+ fixedBound.move(this, Bound::Change::ZoomOut);
|
|
|
+ default:
|
|
|
+ break;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ event->accept();
|
|
|
}
|
|
|
|
|
|
-QPointF GraphView::transformPoint(QPointF& point, double stregth_factorX, double stregth_factorY)
|
|
|
+void GraphView::calculateTotalGraphBound()
|
|
|
{
|
|
|
- return QPointF((point.x() - minGraphX) * stregth_factorX, (maxGraphY - point.y()) * stregth_factorY);
|
|
|
+ qDebug() << "calculateTotalGraphBound";
|
|
|
+ totalBound.minX = std::min_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.minX < b.minX; })->minX;
|
|
|
+ totalBound.maxX = std::max_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.maxX < b.maxX; })->maxX;
|
|
|
+ totalBound.minY = std::min_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.minY < b.minY; })->minY;
|
|
|
+ totalBound.maxY = std::max_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.maxY < b.maxY; })->maxY;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
-void GraphView::calculateMinMaxGraphXY()
|
|
|
+void GraphView::calculateRangeXY()
|
|
|
{
|
|
|
- minGraphX = std::min_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.minX < b.minX; })->minX;
|
|
|
- maxGraphX = std::max_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.maxX < b.maxX; })->maxX;
|
|
|
- minGraphY = std::min_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.minY < b.minY; })->minY;
|
|
|
- maxGraphY = std::max_element(std::begin(graphSeriesVec), std::end(graphSeriesVec), [](const GraphSeries& a, const GraphSeries& b) -> bool {return a.maxY < b.maxY; })->maxY;
|
|
|
- rangeGraphX = std::abs(maxGraphX - minGraphX);
|
|
|
- rangeGraphY = std::abs(maxGraphY - minGraphY);
|
|
|
+ qDebug() << "calculateRangeXY";
|
|
|
+ actualBound = useFixedBound ? fixedBound : totalBound;
|
|
|
+ qDebug() << "calculateRangeXY " << "minX:" << actualBound.minX << " maxX:" << actualBound.maxX << " minY:" << actualBound.minY << " maxY:" << actualBound.maxY;
|
|
|
+
|
|
|
+ rangeGraphX = std::abs(actualBound.maxX - actualBound.minX);
|
|
|
+ rangeGraphY = std::abs(actualBound.maxY - actualBound.minY);
|
|
|
if (std::abs(rangeGraphX) < 0.01) {
|
|
|
- minGraphX -= 1.0;
|
|
|
- maxGraphX += 1.0;
|
|
|
+ actualBound.minX -= 1.0;
|
|
|
+ actualBound.maxX += 1.0;
|
|
|
rangeGraphX = 2;
|
|
|
}
|
|
|
if (std::abs(rangeGraphY) < 0.01) {
|
|
|
- minGraphY -= 1.0;
|
|
|
- maxGraphY += 1.0;
|
|
|
+ actualBound.minY -= 1.0;
|
|
|
+ actualBound.maxY += 1.0;
|
|
|
rangeGraphY = 2;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void GraphView::calculateMinMaxXY(std::vector<QPointF>& line, GraphSeries& lgs)
|
|
|
+//TODO: graphview
|
|
|
+void GraphView::calculateMinMaxXY(GraphSeries& lgs)
|
|
|
{
|
|
|
+ qDebug() << "calculateMinMaxXY";
|
|
|
auto pairX = std::minmax_element(lgs.data->begin(), lgs.data->end(), [](const QPointF& a, const QPointF& b) -> bool {return a.x() < b.x(); });
|
|
|
lgs.minX = pairX.first->x();
|
|
|
lgs.maxX = pairX.second->x();
|
|
@@ -153,17 +316,21 @@ void GraphView::calculateMinMaxXY(std::vector<QPointF>& line, GraphSeries& lgs)
|
|
|
|
|
|
void GraphView::addSeries(std::vector<QPointF>& line, QColor color, GraphSeries::SeriesType type)
|
|
|
{
|
|
|
+ qDebug() << "addSeries";
|
|
|
if (line.empty()) {
|
|
|
return;
|
|
|
}
|
|
|
GraphSeries lgs;
|
|
|
lgs.data = &line;
|
|
|
lgs.type = type;
|
|
|
- calculateMinMaxXY(line, lgs);
|
|
|
+ calculateMinMaxXY(lgs);
|
|
|
lgs.color = color;
|
|
|
graphSeriesVec.push_back(lgs);
|
|
|
- calculateMinMaxGraphXY();
|
|
|
+ calculateTotalGraphBound();
|
|
|
+ if (!useFixedBound) calculateRangeXY();
|
|
|
generateAxisNumberStrings();
|
|
|
+ qDebug() << "addSeries " << "minX:" << actualBound.minX << " maxX:" << actualBound.maxX << " minY:" << actualBound.minY << " maxY:" << actualBound.maxY;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
QColor GraphView::generateNextColorForGraph()
|
|
@@ -178,8 +345,21 @@ QColor GraphView::generateNextColorForGraph()
|
|
|
void GraphView::generateAxisNumberStrings()
|
|
|
{
|
|
|
for (int i = 0; i < 11; i++) {
|
|
|
- xAxisNumbers[i] = QString::number(minGraphX + i * (rangeGraphX / (double)X_AXIS_GAP_AMOUNT), 'f', 1);
|
|
|
- yAxisNumbers[i] = QString::number(minGraphY + i * (rangeGraphY / (double)Y_AXIS_GAP_AMOUNT), 'f', 1);
|
|
|
+ xAxisNumbers[i] = QString::number(actualBound.minX + i * (rangeGraphX / (double)X_AXIS_GAP_AMOUNT), 'f', 1);
|
|
|
+ yAxisNumbers[i] = QString::number(actualBound.minY + i * (rangeGraphY / (double)Y_AXIS_GAP_AMOUNT), 'f', 1);
|
|
|
+ }
|
|
|
+ qDebug() << "generateAxisNumberStrings " << "minX:" << actualBound.minX << " maxX:" << actualBound.maxX << " minY:" << actualBound.minY << " maxY:" << actualBound.maxY;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+void GraphView::setUseFixedBound(bool value)
|
|
|
+{
|
|
|
+ //if value == other update else do nothing
|
|
|
+ if (useFixedBound != value) {
|
|
|
+ useFixedBound = value;
|
|
|
+ calculateRangeXY();
|
|
|
+ actualBound = fixedBound;
|
|
|
+ generateAxisNumberStrings();
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -222,8 +402,50 @@ void GraphView::addDots(std::vector<QPointF>& dots, QColor color)
|
|
|
addSeries(dots, color, GraphSeries::SeriesType::Dot);
|
|
|
}
|
|
|
|
|
|
+std::pair<std::vector<QPointF>::iterator , std::vector<QPointF>::iterator> GraphSeries::getFirstLastIndexInSeriesThatsInBound(int minX, int maxX)
|
|
|
+{
|
|
|
+ std::vector<QPointF>::iterator minIter = std::find_if(data->begin(), data->end(), [minX](const QPointF& p) -> bool {return p.x() >= minX; });
|
|
|
+ std::vector<QPointF>::iterator maxIter = std::find_if(minIter, data->end(), [maxX](const QPointF& p) -> bool {return p.x() > maxX; });
|
|
|
+ return std::make_pair(minIter, --maxIter);
|
|
|
+}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+void Bound::move(GraphView* widget,const Change& type, const double changePercentage)
|
|
|
+{
|
|
|
+ double changeX = std::abs(minX - maxX) * changePercentage;
|
|
|
+ double changeY = std::abs(minY - maxY) * changePercentage;
|
|
|
+ switch (type) {
|
|
|
+ case Change::MoveLeft:
|
|
|
+ minX -= changeX;
|
|
|
+ maxX -= changeX;
|
|
|
+ break;
|
|
|
+ case Change::MoveRight:
|
|
|
+ minX += changeX;
|
|
|
+ maxX += changeX;
|
|
|
+ break;
|
|
|
+ case Change::MoveUp:
|
|
|
+ minY += changeY;
|
|
|
+ maxY += changeY;
|
|
|
+ break;
|
|
|
+ case Change::MoveDown:
|
|
|
+ minY -= changeY;
|
|
|
+ maxY -= changeY;
|
|
|
+ break;
|
|
|
+ case Change::ZoomOut:
|
|
|
+ minY -= changeY;
|
|
|
+ maxY += changeY;
|
|
|
+ minX -= changeX;
|
|
|
+ maxX += changeX;
|
|
|
+ break;
|
|
|
+ case Change::ZoomIn:
|
|
|
+ minY += changeY;
|
|
|
+ maxY -= changeY;
|
|
|
+ minX += changeX;
|
|
|
+ maxX -= changeX;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ widget->calculateRangeXY();
|
|
|
+ widget->generateAxisNumberStrings();
|
|
|
+ widget->repaint();
|
|
|
+}
|