From 9d071d2a2cec9a7662a02669488569a017f0ea35 Mon Sep 17 00:00:00 2001 From: Daniel Wilhelm Date: Mon, 13 Feb 2017 21:25:04 -0700 Subject: 8.9 --- wx+/graph.cpp | 1694 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 847 insertions(+), 847 deletions(-) mode change 100644 => 100755 wx+/graph.cpp (limited to 'wx+/graph.cpp') diff --git a/wx+/graph.cpp b/wx+/graph.cpp old mode 100644 new mode 100755 index d45a0e85..50c1daa6 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -1,847 +1,847 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include "graph.h" -#include -#include -#include -#include -#include -#include "dc.h" - -using namespace zen; - - -//todo: support zoom via mouse wheel? - -const wxEventType zen::wxEVT_GRAPH_SELECTION = wxNewEventType(); - - -double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size -{ - if (blockSize <= 0) - return 0; - - const double k = std::floor(std::log10(blockSize)); - const double e = std::pow(10, k); - if (numeric::isNull(e)) - return 0; - const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10) - assert(1 <= a && a < 10); - - //have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5 - const double steps[] = { 1, 2, 2.5, 5, 10 }; - return e * numeric::nearMatch(a, std::begin(steps), std::end(steps)); -} - - -namespace -{ -wxColor getDefaultColor(size_t pos) -{ - switch (pos % 10) - { - case 0: - return { 0, 69, 134 }; //blue - case 1: - return { 255, 66, 14 }; //red - case 2: - return { 255, 211, 32 }; //yellow - case 3: - return { 87, 157, 28 }; //green - case 4: - return { 126, 0, 33 }; //royal - case 5: - return { 131, 202, 255 }; //light blue - case 6: - return { 49, 64, 4 }; //dark green - case 7: - return { 174, 207, 0 }; //light green - case 8: - return { 75, 31, 111 }; //purple - case 9: - return { 255, 149, 14 }; //orange - } - assert(false); - return *wxBLACK; -} - - -class ConvertCoord //convert between screen and input data coordinates -{ -public: - ConvertCoord(double valMin, double valMax, size_t screenSize) : - min_(valMin), - scaleToReal(screenSize == 0 ? 0 : (valMax - valMin) / screenSize), - scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)), - outOfBoundsLow (-1 * scaleToReal + valMin), - outOfBoundsHigh((screenSize + 1) * scaleToReal + valMin) { if (outOfBoundsLow > outOfBoundsHigh) std::swap(outOfBoundsLow, outOfBoundsHigh); } - - double screenToReal(double screenPos) const //map [0, screenSize] -> [valMin, valMax] - { - return screenPos * scaleToReal + min_; - } - double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) - { - return (realPos - min_) * scaleToScr; - } - int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds! - { - //catch large double values: if double is larger than what int can represent => undefined behavior! - numeric::clamp(realPos, outOfBoundsLow, outOfBoundsHigh); - return numeric::round(realToScreen(realPos)); - } - -private: - double min_; - double scaleToReal; - double scaleToScr; - - double outOfBoundsLow; - double outOfBoundsHigh; -}; - - -//enlarge value range to display to a multiple of a "useful" block size -//returns block cound -int widenRange(double& valMin, double& valMax, //in/out - int graphAreaSize, //in pixel - int optimalBlockSizePx, // - const LabelFormatter& labelFmt) -{ - if (graphAreaSize <= 0) return 0; - - const double minValRangePerBlock = (valMax - valMin) / graphAreaSize; - const double proposedValRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; - double valRangePerBlock = labelFmt.getOptimalBlockSize(proposedValRangePerBlock); - assert(numeric::isNull(proposedValRangePerBlock) || valRangePerBlock > minValRangePerBlock); - - if (numeric::isNull(valRangePerBlock)) //valMin == valMax or strange "optimal block size" - return 1; - - //don't allow sub-pixel blocks! => avoid erroneously high GDI render work load! - if (valRangePerBlock < minValRangePerBlock) - valRangePerBlock = std::ceil(minValRangePerBlock / valRangePerBlock) * valRangePerBlock; - - double blockMin = std::floor(valMin / valRangePerBlock); //store as double, not int: truncation possible, e.g. if valRangePerBlock == 1 - double blockMax = std::ceil (valMax / valRangePerBlock); // - int blockCount = numeric::round(blockMax - blockMin); - assert(blockCount >= 0); - - //handle valMin == valMax == integer - if (blockCount <= 0) - { - ++blockMax; - blockCount = 1; - } - - valMin = blockMin * valRangePerBlock; - valMax = blockMax * valRangePerBlock; - return blockCount; -} - - -void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) -{ - assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); - if (blockCount <= 0) - return; - - wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - const double valRangePerBlock = (xMax - xMin) / blockCount; - - for (int i = 1; i < blockCount; ++i) - { - //draw grey vertical lines - const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision - const int x = graphArea.x + cvrtX.realToScreenRound(valX); - - if (graphArea.height > 0) - dc.DrawLine(wxPoint(x, graphArea.y), wxPoint(x, graphArea.y + graphArea.height)); //wxDC::DrawLine() doesn't draw last pixel - - //draw x axis labels - const wxString label = labelFmt.formatText(valX, valRangePerBlock); - const wxSize labelExtent = dc.GetMultiLineTextExtent(label); - dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center - } -} - - -void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) -{ - assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); - if (blockCount <= 0) - return; - - wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - const double valRangePerBlock = (yMax - yMin) / blockCount; - - for (int i = 1; i < blockCount; ++i) - { - //draw grey horizontal lines - const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision - const int y = graphArea.y + cvrtY.realToScreenRound(valY); - - if (graphArea.width > 0) - dc.DrawLine(wxPoint(graphArea.x, y), wxPoint(graphArea.x + graphArea.width, y)); //wxDC::DrawLine() doesn't draw last pixel - - //draw y axis labels - const wxString label = labelFmt.formatText(valY, valRangePerBlock); - const wxSize labelExtent = dc.GetMultiLineTextExtent(label); - dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center - } -} - - -void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos) -{ - if (txt.empty()) return; - const int borderX = 5; - const int borderY = 2; //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"? - - wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - wxSize txtExtent = dc.GetMultiLineTextExtent(txt); - txtExtent.x += 2 * borderX; - txtExtent.y += 2 * borderY; - - wxPoint drawPos = graphArea.GetTopLeft(); - switch (pos) - { - case Graph2D::CORNER_TOP_LEFT: - break; - case Graph2D::CORNER_TOP_RIGHT: - drawPos.x += graphArea.width - txtExtent.GetWidth(); - break; - case Graph2D::CORNER_BOTTOM_LEFT: - drawPos.y += graphArea.height - txtExtent.GetHeight(); - break; - case Graph2D::CORNER_BOTTOM_RIGHT: - drawPos.x += graphArea.width - txtExtent.GetWidth(); - drawPos.y += graphArea.height - txtExtent.GetHeight(); - break; - } - dc.DrawText(txt, drawPos + wxPoint(borderX, borderY)); -} - - -//calculate intersection of polygon with half-plane -template -void cutPoints(std::vector& curvePoints, std::vector& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut) -{ - assert(curvePoints.size() == oobMarker.size()); - - if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return; - - auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line - - std::vector curvePointsTmp; - std::vector oobMarkerTmp; - curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one - oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D! - - auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); }; - - bool pointInside = isInside(curvePoints[0]); - if (pointInside) - savePoint(curvePoints[0], isMarkedOob(0)); - - for (size_t index = 1; index < curvePoints.size(); ++index) - { - if (isInside(curvePoints[index]) != pointInside) - { - pointInside = !pointInside; - const CurvePoint is = getIntersection(curvePoints[index - 1], curvePoints[index]); //getIntersection returns "to" when delta is zero - savePoint(is, !pointInside || isMarkedOob(index - 1)); - } - if (pointInside) - savePoint(curvePoints[index], isMarkedOob(index)); - } - - //make sure the output polygon area is correctly shaped if either begin or end points are cut - if (doPolygonCut) //note: impacts min/max height-calculations! - if (curvePoints.size() >= 3) - if (isInside(curvePoints.front()) != pointInside) - { - assert(!oobMarkerTmp.empty()); - oobMarkerTmp.back() = true; - - const CurvePoint is = getIntersection(curvePoints.back(), curvePoints.front()); - savePoint(is, true); - } - - curvePointsTmp.swap(curvePoints); - oobMarkerTmp .swap(oobMarker); -} - - -struct GetIntersectionX -{ - GetIntersectionX(double x) : x_(x) {} - CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const - { - const double deltaX = to.x - from.x; - const double deltaY = to.y - from.y; - return numeric::isNull(deltaX) ? to : CurvePoint(x_, from.y + (x_ - from.x) / deltaX * deltaY); - } - -private: - const double x_; -}; - -struct GetIntersectionY -{ - GetIntersectionY(double y) : y_(y) {} - CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const - { - const double deltaX = to.x - from.x; - const double deltaY = to.y - from.y; - return numeric::isNull(deltaY) ? to : CurvePoint(from.x + (y_ - from.y) / deltaY * deltaX, y_); - } - -private: - const double y_; -}; - -void cutPointsOutsideX(std::vector& curvePoints, std::vector& oobMarker, double minX, double maxX, bool doPolygonCut) -{ - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut); - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut); -} - -void cutPointsOutsideY(std::vector& curvePoints, std::vector& oobMarker, double minY, double maxY, bool doPolygonCut) -{ - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut); - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut); -} -} - - -std::vector ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const -{ - std::vector points; - - if (pixelWidth <= 1) return points; - const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] - - const std::pair rangeX = getRangeX(); - - const double screenLow = cvrtX.realToScreen(std::max(rangeX.first, minX)); //=> xLow >= 0 - const double screenHigh = cvrtX.realToScreen(std::min(rangeX.second, maxX)); //=> xHigh <= pixelWidth - 1 - //if double is larger than what int can represent => undefined behavior! - //=> convert to int *after* checking value range! - if (screenLow <= screenHigh) - { - const int posFrom = std::ceil (screenLow ); //do not step outside [minX, maxX] in loop below! - const int posTo = std::floor(screenHigh); // - //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC - - for (int i = posFrom; i <= posTo; ++i) - { - const double x = cvrtX.screenToReal(i); - points.emplace_back(x, getValue(x)); - } - } - return points; -} - - -std::vector SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const -{ - std::vector points; - if (pixelWidth <= 1) return points; - const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] - const std::pair rangeX = getRangeX(); - - auto addPoint = [&](const CurvePoint& pt) - { - if (!points.empty()) - { - if (pt.x <= points.back().x) //allow ascending x-positions only! algorithm below may cause double-insertion after empty x-ranges! - return; - - if (addSteps_) - if (pt.y != points.back().y) - points.emplace_back(CurvePoint(pt.x, points.back().y)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy - } - points.push_back(pt); - }; - - const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX)); - const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX)); - - for (int i = posFrom; i <= posTo; ++i) - { - const double x = cvrtX.screenToReal(i); - Opt ptLe = getLessEq(x); - Opt ptGe = getGreaterEq(x); - //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGe in the following! - const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1; - const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1; - assert(!ptLe || posLe <= i); //check for invalid return values - assert(!ptGe || posGe >= i); // - /* - Breakdown of all combinations of posLe, posGe and expected action (n >= 1) - Note: For every empty x-range of at least one pixel, both next and previous points must be saved to keep the interpolating line stable!!! - - posLe | posGe | action - +-------+-------+-------- - | none | none | break - | i | none | save ptLe; break - | i - n | none | break; - +-------+-------+-------- - | none | i | save ptGe; continue - | i | i | save one of ptLe, ptGe; continue - | i - n | i | save ptGe; continue - +-------+-------+-------- - | none | i + n | save ptGe; jump to position posGe + 1 - | i | i + n | save ptLe; if n == 1: continue; else: save ptGe; jump to position posGe + 1 - | i - n | i + n | save ptLe, ptGe; jump to position posGe + 1 - +-------+-------+-------- - */ - if (posGe < i) - { - if (posLe == i) - addPoint(*ptLe); - break; - } - else if (posGe == i) //test if point would be mapped to pixel x-position i - { - if (posLe == i) // - addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe); - else - addPoint(*ptGe); - } - else - { - if (posLe <= i) - addPoint(*ptLe); - - if (posLe != i || posGe > i + 1) - { - addPoint(*ptGe); - i = posGe; //skip sparse area: +1 will be added by for-loop! - } - } - } - return points; -} - - -Graph2D::Graph2D(wxWindow* parent, - wxWindowID winid, - const wxPoint& pos, - const wxSize& size, - long style, - const wxString& name) : wxPanel(parent, winid, pos, size, style, name) -{ - Connect(wxEVT_PAINT, wxPaintEventHandler(Graph2D::onPaintEvent), nullptr, this); - Connect(wxEVT_SIZE, wxSizeEventHandler (Graph2D::onSizeEvent ), nullptr, this); - //http://wiki.wxwidgets.org/Flicker-Free_Drawing - Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Graph2D::onEraseBackGround), nullptr, this); - - //SetDoubleBuffered(true); slow as hell! - - SetBackgroundStyle(wxBG_STYLE_PAINT); - - Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(Graph2D::OnMouseLeftDown), nullptr, this); - Connect(wxEVT_MOTION, wxMouseEventHandler(Graph2D::OnMouseMovement), nullptr, this); - Connect(wxEVT_LEFT_UP, wxMouseEventHandler(Graph2D::OnMouseLeftUp), nullptr, this); - Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(Graph2D::OnMouseCaptureLost), nullptr, this); -} - - -void Graph2D::onPaintEvent(wxPaintEvent& event) -{ - //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! - BufferedPaintDC dc(*this, doubleBuffer); - render(dc); -} - - -void Graph2D::OnMouseLeftDown(wxMouseEvent& event) -{ - activeSel = std::make_unique(*this, event.GetPosition()); - - if (!event.ControlDown()) - oldSel.clear(); - Refresh(); -} - - -void Graph2D::OnMouseMovement(wxMouseEvent& event) -{ - if (activeSel.get()) - { - activeSel->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() - Refresh(); - } -} - - -void Graph2D::OnMouseLeftUp(wxMouseEvent& event) -{ - if (activeSel.get()) - { - if (activeSel->getStartPos() != activeSel->refCurrentPos()) //if it's just a single mouse click: discard selection - { - GraphSelectEvent selEvent(activeSel->refSelection()); //fire off GraphSelectEvent - if (wxEvtHandler* handler = GetEventHandler()) - handler->AddPendingEvent(selEvent); - - oldSel.push_back(activeSel->refSelection()); //commit selection - } - - activeSel.reset(); - Refresh(); - } -} - - -void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event) -{ - activeSel.reset(); - Refresh(); -} - - -void Graph2D::setCurve(const std::shared_ptr& data, const CurveAttributes& ca) -{ - curves_.clear(); - addCurve(data, ca); -} - - -void Graph2D::addCurve(const std::shared_ptr& data, const CurveAttributes& ca) -{ - CurveAttributes newAttr = ca; - if (newAttr.autoColor) - newAttr.setColor(getDefaultColor(curves_.size())); - curves_.emplace_back(data, newAttr); - Refresh(); -} - - -void Graph2D::render(wxDC& dc) const -{ - using namespace numeric; - - //set label font right at the start so that it is considered by wxDC::GetTextExtent() below! - dc.SetFont(labelFont); - - const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! - { - //clear complete client area; set label background color - const wxColor backCol = GetBackgroundColour(); //user-configurable! - //wxPanel::GetClassDefaultAttributes().colBg : - //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); - wxDCPenChanger dummy (dc, backCol); - wxDCBrushChanger dummy2(dc, backCol); - dc.DrawRectangle(clientRect); - } - - /* - ----------------------- - | | x-label | - ----------------------- - |y-label | graph area | - |---------------------- - */ - wxRect graphArea = clientRect; - int xLabelPosY = clientRect.y; - int yLabelPosX = clientRect.x; - - switch (attr.labelposX) - { - case LABEL_X_TOP: - graphArea.y += attr.xLabelHeight; - graphArea.height -= attr.xLabelHeight; - break; - case LABEL_X_BOTTOM: - xLabelPosY += clientRect.height - attr.xLabelHeight; - graphArea.height -= attr.xLabelHeight; - break; - case LABEL_X_NONE: - break; - } - switch (attr.labelposY) - { - case LABEL_Y_LEFT: - graphArea.x += attr.yLabelWidth; - graphArea.width -= attr.yLabelWidth; - break; - case LABEL_Y_RIGHT: - yLabelPosX += clientRect.width - attr.yLabelWidth; - graphArea.width -= attr.yLabelWidth; - break; - case LABEL_Y_NONE: - break; - } - - { - //paint graph background (excluding label area) - wxDCPenChanger dummy (dc, getBorderColor()); - wxDCBrushChanger dummy2(dc, attr.backgroundColor); - //accessibility: consider system text and background colors; small drawback: color of graphs is NOT connected to the background! => responsibility of client to use correct colors - - dc.DrawRectangle(graphArea); - graphArea.Deflate(1, 1); //attention more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-const!!! - } - - //set label areas respecting graph area border! - const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, attr.xLabelHeight); - const wxRect yLabelArea(yLabelPosX, graphArea.y, attr.yLabelWidth, graphArea.height); - const wxPoint graphAreaOrigin = graphArea.GetTopLeft(); - - //detect x value range - double minX = attr.minXauto ? std::numeric_limits::infinity() : attr.minX; //automatic: ensure values are initialized by first curve - double maxX = attr.maxXauto ? -std::numeric_limits::infinity() : attr.maxX; // - for (auto it = curves_.begin(); it != curves_.end(); ++it) - if (const CurveData* curve = it->first.get()) - { - const std::pair rangeX = curve->getRangeX(); - assert(rangeX.first <= rangeX.second + 1.0e-9); - //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18 - - if (attr.minXauto) - minX = std::min(minX, rangeX.first); - if (attr.maxXauto) - maxX = std::max(maxX, rangeX.second); - } - - const wxSize minimalBlockSizePx = dc.GetTextExtent(L"00"); - - if (minX <= maxX && maxX - minX < std::numeric_limits::infinity()) //valid x-range - { - int blockCountX = 0; - //enlarge minX, maxX to a multiple of a "useful" block size - if (attr.labelposX != LABEL_X_NONE && attr.labelFmtX.get()) - blockCountX = widenRange(minX, maxX, //in/out - graphArea.width, - minimalBlockSizePx.GetWidth() * 7, - *attr.labelFmtX); - - //get raw values + detect y value range - double minY = attr.minYauto ? std::numeric_limits::infinity() : attr.minY; //automatic: ensure values are initialized by first curve - double maxY = attr.maxYauto ? -std::numeric_limits::infinity() : attr.maxY; // - - std::vector> curvePoints(curves_.size()); - std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start an out-of-bounds line - - for (size_t index = 0; index < curves_.size(); ++index) - if (const CurveData* curve = curves_[index].first.get()) - { - std::vector& points = curvePoints[index]; - auto& marker = oobMarker [index]; - - points = curve->getPoints(minX, maxX, graphArea.width); - marker.resize(points.size()); //default value: false - if (!points.empty()) - { - //cut points outside visible x-range now in order to calculate height of visible line fragments only! - const bool doPolygonCut = curves_[index].second.fillMode == CurveAttributes::FILL_POLYGON; //impacts auto minY/maxY!! - cutPointsOutsideX(points, marker, minX, maxX, doPolygonCut); - - if (attr.minYauto || attr.maxYauto) - { - auto itPair = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; }); - if (attr.minYauto) - minY = std::min(minY, itPair.first->y); - if (attr.maxYauto) - maxY = std::max(maxY, itPair.second->y); - } - } - } - - if (minY <= maxY) //valid y-range - { - int blockCountY = 0; - //enlarge minY, maxY to a multiple of a "useful" block size - if (attr.labelposY != LABEL_Y_NONE && attr.labelFmtY.get()) - blockCountY = widenRange(minY, maxY, //in/out - graphArea.height, - minimalBlockSizePx.GetHeight() * 3, - *attr.labelFmtY); - - if (graphArea.width <= 1 || graphArea.height <= 1) return; - const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1] - const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0] - - //calculate curve coordinates on graph area - std::vector> drawPoints(curves_.size()); - - for (size_t index = 0; index < curves_.size(); ++index) - { - auto& cp = curvePoints[index]; - - //add two artificial points to fill the curve area towards x-axis => do this before cutPointsOutsideY() to handle curve leaving upper bound - if (curves_[index].second.fillMode == CurveAttributes::FILL_CURVE) - if (!cp.empty()) - { - cp.emplace_back(CurvePoint(cp.back ().x, minY)); //add lower right and left corners - cp.emplace_back(CurvePoint(cp.front().x, minY)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy - oobMarker[index].back() = true; - oobMarker[index].push_back(true); - oobMarker[index].push_back(true); - } - - //cut points outside visible y-range before calculating pixels: - //1. realToScreenRound() deforms out-of-range values! - //2. pixels that are grossly out of range can be a severe performance problem when drawing on the DC (Windows) - const bool doPolygonCut = curves_[index].second.fillMode != CurveAttributes::FILL_NONE; - cutPointsOutsideY(cp, oobMarker[index], minY, maxY, doPolygonCut); - - auto& dp = drawPoints[index]; - for (const CurvePoint& pt : cp) - dp.push_back(wxPoint(cvrtX.realToScreenRound(pt.x), - cvrtY.realToScreenRound(pt.y)) + graphAreaOrigin); - } - - //update active mouse selection - if (activeSel.get() && graphArea.width > 0 && graphArea.height > 0) - { - auto widen = [](double* low, double* high) - { - if (*low > *high) - std::swap(low, high); - *low -= 0.5; - *high += 0.5; - }; - - const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea - const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin; - - //normalize positions: a mouse selection is symmetric and *not* an half-open range! - double screenFromX = clampCpy(screenStart .x, 0, graphArea.width - 1); - double screenFromY = clampCpy(screenStart .y, 0, graphArea.height - 1); - double screenToX = clampCpy(screenCurrent.x, 0, graphArea.width - 1); - double screenToY = clampCpy(screenCurrent.y, 0, graphArea.height - 1); - widen(&screenFromX, &screenToX); //use full pixel range for selection! - widen(&screenFromY, &screenToY); - - //save current selection as "double" coordinates - activeSel->refSelection().from = CurvePoint(cvrtX.screenToReal(screenFromX), - cvrtY.screenToReal(screenFromY)); - - activeSel->refSelection().to = CurvePoint(cvrtX.screenToReal(screenToX), - cvrtY.screenToReal(screenToY)); - } - - //#################### begin drawing #################### - //1. draw colored area under curves - for (auto it = curves_.begin(); it != curves_.end(); ++it) - if (it->second.fillMode != CurveAttributes::FILL_NONE) - { - const std::vector& points = drawPoints[it - curves_.begin()]; - if (points.size() >= 3) - { - wxDCBrushChanger dummy(dc, it->second.fillColor); - wxDCPenChanger dummy2(dc, it->second.fillColor); - dc.DrawPolygon(static_cast(points.size()), &points[0]); - } - } - - //2. draw all currently set mouse selections (including active selection) - std::vector allSelections = oldSel; - if (activeSel) - allSelections.push_back(activeSel->refSelection()); - { - //alpha channel not supported on wxMSW, so draw selection before curves - wxDCBrushChanger dummy(dc, wxColor(168, 202, 236)); //light blue - wxDCPenChanger dummy2(dc, wxColor( 51, 153, 255)); //dark blue - - auto shrink = [](double* low, double* high) - { - if (*low > *high) - std::swap(low, high); - *low += 0.5; - *high -= 0.5; - if (*low > *high) - *low = *high = (*low + *high) / 2; - }; - - for (const SelectionBlock& sel : allSelections) - { - //harmonize with active mouse selection above - double screenFromX = cvrtX.realToScreen(sel.from.x); - double screenFromY = cvrtY.realToScreen(sel.from.y); - double screenToX = cvrtX.realToScreen(sel.to.x); - double screenToY = cvrtY.realToScreen(sel.to.y); - shrink(&screenFromX, &screenToX); - shrink(&screenFromY, &screenToY); - - clamp(screenFromX, 0.0, graphArea.width - 1.0); - clamp(screenFromY, 0.0, graphArea.height - 1.0); - clamp(screenToX, 0.0, graphArea.width - 1.0); - clamp(screenToY, 0.0, graphArea.height - 1.0); - - const wxPoint pixelFrom = wxPoint(numeric::round(screenFromX), - numeric::round(screenFromY)) + graphAreaOrigin; - const wxPoint pixelTo = wxPoint(numeric::round(screenToX), - numeric::round(screenToY)) + graphAreaOrigin; - switch (attr.mouseSelMode) - { - case SELECT_NONE: - break; - case SELECT_RECTANGLE: - dc.DrawRectangle(wxRect(pixelFrom, pixelTo)); - break; - case SELECT_X_AXIS: - dc.DrawRectangle(wxRect(wxPoint(pixelFrom.x, graphArea.y), wxPoint(pixelTo.x, graphArea.y + graphArea.height - 1))); - break; - case SELECT_Y_AXIS: - dc.DrawRectangle(wxRect(wxPoint(graphArea.x, pixelFrom.y), wxPoint(graphArea.x + graphArea.width - 1, pixelTo.y))); - break; - } - } - } - - //3. draw labels and background grid - drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr.labelFmtX); - drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY); - - //4. finally draw curves - { - dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside - ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion()); - - for (auto it = curves_.begin(); it != curves_.end(); ++it) - { - wxDCPenChanger dummy(dc, wxPen(it->second.color, it->second.lineWidth)); - - const size_t index = it - curves_.begin(); - const std::vector& points = drawPoints[index]; - const auto& marker = oobMarker [index]; - assert(points.size() == marker.size()); - - //draw all parts of the curve except for the out-of-bounds fragments - size_t drawIndexFirst = 0; - while (drawIndexFirst < points.size()) - { - size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), true) - marker.begin(); - if (drawIndexLast < points.size()) ++drawIndexLast; - - const int pointCount = static_cast(drawIndexLast - drawIndexFirst); - if (pointCount > 0) - { - if (pointCount >= 2) //on OS X wxWidgets has a nasty assert on this - dc.DrawLines(pointCount, &points[drawIndexFirst]); - dc.DrawPoint(points[drawIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel - } - drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), false) - marker.begin(); - } - } - } - - //5. draw corner texts - for (const auto& ct : attr.cornerTexts) - drawCornerText(dc, graphArea, ct.second, ct.first); - } - } -} +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "graph.h" +#include +#include +#include +#include +#include +#include "dc.h" + +using namespace zen; + + +//todo: support zoom via mouse wheel? + +const wxEventType zen::wxEVT_GRAPH_SELECTION = wxNewEventType(); + + +double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size +{ + if (blockSize <= 0) + return 0; + + const double k = std::floor(std::log10(blockSize)); + const double e = std::pow(10, k); + if (numeric::isNull(e)) + return 0; + const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10) + assert(1 <= a && a < 10); + + //have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5 + const double steps[] = { 1, 2, 2.5, 5, 10 }; + return e * numeric::nearMatch(a, std::begin(steps), std::end(steps)); +} + + +namespace +{ +wxColor getDefaultColor(size_t pos) +{ + switch (pos % 10) + { + case 0: + return { 0, 69, 134 }; //blue + case 1: + return { 255, 66, 14 }; //red + case 2: + return { 255, 211, 32 }; //yellow + case 3: + return { 87, 157, 28 }; //green + case 4: + return { 126, 0, 33 }; //royal + case 5: + return { 131, 202, 255 }; //light blue + case 6: + return { 49, 64, 4 }; //dark green + case 7: + return { 174, 207, 0 }; //light green + case 8: + return { 75, 31, 111 }; //purple + case 9: + return { 255, 149, 14 }; //orange + } + assert(false); + return *wxBLACK; +} + + +class ConvertCoord //convert between screen and input data coordinates +{ +public: + ConvertCoord(double valMin, double valMax, size_t screenSize) : + min_(valMin), + scaleToReal(screenSize == 0 ? 0 : (valMax - valMin) / screenSize), + scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)), + outOfBoundsLow (-1 * scaleToReal + valMin), + outOfBoundsHigh((screenSize + 1) * scaleToReal + valMin) { if (outOfBoundsLow > outOfBoundsHigh) std::swap(outOfBoundsLow, outOfBoundsHigh); } + + double screenToReal(double screenPos) const //map [0, screenSize] -> [valMin, valMax] + { + return screenPos * scaleToReal + min_; + } + double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) + { + return (realPos - min_) * scaleToScr; + } + int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds! + { + //catch large double values: if double is larger than what int can represent => undefined behavior! + numeric::clamp(realPos, outOfBoundsLow, outOfBoundsHigh); + return numeric::round(realToScreen(realPos)); + } + +private: + double min_; + double scaleToReal; + double scaleToScr; + + double outOfBoundsLow; + double outOfBoundsHigh; +}; + + +//enlarge value range to display to a multiple of a "useful" block size +//returns block cound +int widenRange(double& valMin, double& valMax, //in/out + int graphAreaSize, //in pixel + int optimalBlockSizePx, // + const LabelFormatter& labelFmt) +{ + if (graphAreaSize <= 0) return 0; + + const double minValRangePerBlock = (valMax - valMin) / graphAreaSize; + const double proposedValRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; + double valRangePerBlock = labelFmt.getOptimalBlockSize(proposedValRangePerBlock); + assert(numeric::isNull(proposedValRangePerBlock) || valRangePerBlock > minValRangePerBlock); + + if (numeric::isNull(valRangePerBlock)) //valMin == valMax or strange "optimal block size" + return 1; + + //don't allow sub-pixel blocks! => avoid erroneously high GDI render work load! + if (valRangePerBlock < minValRangePerBlock) + valRangePerBlock = std::ceil(minValRangePerBlock / valRangePerBlock) * valRangePerBlock; + + double blockMin = std::floor(valMin / valRangePerBlock); //store as double, not int: truncation possible, e.g. if valRangePerBlock == 1 + double blockMax = std::ceil (valMax / valRangePerBlock); // + int blockCount = numeric::round(blockMax - blockMin); + assert(blockCount >= 0); + + //handle valMin == valMax == integer + if (blockCount <= 0) + { + ++blockMax; + blockCount = 1; + } + + valMin = blockMin * valRangePerBlock; + valMax = blockMax * valRangePerBlock; + return blockCount; +} + + +void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +{ + assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); + if (blockCount <= 0) + return; + + wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + const double valRangePerBlock = (xMax - xMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + //draw grey vertical lines + const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int x = graphArea.x + cvrtX.realToScreenRound(valX); + + if (graphArea.height > 0) + dc.DrawLine(wxPoint(x, graphArea.y), wxPoint(x, graphArea.y + graphArea.height)); //wxDC::DrawLine() doesn't draw last pixel + + //draw x axis labels + const wxString label = labelFmt.formatText(valX, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center + } +} + + +void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +{ + assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); + if (blockCount <= 0) + return; + + wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + const double valRangePerBlock = (yMax - yMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + //draw grey horizontal lines + const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int y = graphArea.y + cvrtY.realToScreenRound(valY); + + if (graphArea.width > 0) + dc.DrawLine(wxPoint(graphArea.x, y), wxPoint(graphArea.x + graphArea.width, y)); //wxDC::DrawLine() doesn't draw last pixel + + //draw y axis labels + const wxString label = labelFmt.formatText(valY, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center + } +} + + +void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos) +{ + if (txt.empty()) return; + const int borderX = 5; + const int borderY = 2; //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"? + + wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + wxSize txtExtent = dc.GetMultiLineTextExtent(txt); + txtExtent.x += 2 * borderX; + txtExtent.y += 2 * borderY; + + wxPoint drawPos = graphArea.GetTopLeft(); + switch (pos) + { + case Graph2D::CORNER_TOP_LEFT: + break; + case Graph2D::CORNER_TOP_RIGHT: + drawPos.x += graphArea.width - txtExtent.GetWidth(); + break; + case Graph2D::CORNER_BOTTOM_LEFT: + drawPos.y += graphArea.height - txtExtent.GetHeight(); + break; + case Graph2D::CORNER_BOTTOM_RIGHT: + drawPos.x += graphArea.width - txtExtent.GetWidth(); + drawPos.y += graphArea.height - txtExtent.GetHeight(); + break; + } + dc.DrawText(txt, drawPos + wxPoint(borderX, borderY)); +} + + +//calculate intersection of polygon with half-plane +template +void cutPoints(std::vector& curvePoints, std::vector& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut) +{ + assert(curvePoints.size() == oobMarker.size()); + + if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return; + + auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line + + std::vector curvePointsTmp; + std::vector oobMarkerTmp; + curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one + oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D! + + auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); }; + + bool pointInside = isInside(curvePoints[0]); + if (pointInside) + savePoint(curvePoints[0], isMarkedOob(0)); + + for (size_t index = 1; index < curvePoints.size(); ++index) + { + if (isInside(curvePoints[index]) != pointInside) + { + pointInside = !pointInside; + const CurvePoint is = getIntersection(curvePoints[index - 1], curvePoints[index]); //getIntersection returns "to" when delta is zero + savePoint(is, !pointInside || isMarkedOob(index - 1)); + } + if (pointInside) + savePoint(curvePoints[index], isMarkedOob(index)); + } + + //make sure the output polygon area is correctly shaped if either begin or end points are cut + if (doPolygonCut) //note: impacts min/max height-calculations! + if (curvePoints.size() >= 3) + if (isInside(curvePoints.front()) != pointInside) + { + assert(!oobMarkerTmp.empty()); + oobMarkerTmp.back() = true; + + const CurvePoint is = getIntersection(curvePoints.back(), curvePoints.front()); + savePoint(is, true); + } + + curvePointsTmp.swap(curvePoints); + oobMarkerTmp .swap(oobMarker); +} + + +struct GetIntersectionX +{ + GetIntersectionX(double x) : x_(x) {} + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaX) ? to : CurvePoint(x_, from.y + (x_ - from.x) / deltaX * deltaY); + } + +private: + const double x_; +}; + +struct GetIntersectionY +{ + GetIntersectionY(double y) : y_(y) {} + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaY) ? to : CurvePoint(from.x + (y_ - from.y) / deltaY * deltaX, y_); + } + +private: + const double y_; +}; + +void cutPointsOutsideX(std::vector& curvePoints, std::vector& oobMarker, double minX, double maxX, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut); +} + +void cutPointsOutsideY(std::vector& curvePoints, std::vector& oobMarker, double minY, double maxY, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut); +} +} + + +std::vector ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector points; + + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + + const std::pair rangeX = getRangeX(); + + const double screenLow = cvrtX.realToScreen(std::max(rangeX.first, minX)); //=> xLow >= 0 + const double screenHigh = cvrtX.realToScreen(std::min(rangeX.second, maxX)); //=> xHigh <= pixelWidth - 1 + //if double is larger than what int can represent => undefined behavior! + //=> convert to int *after* checking value range! + if (screenLow <= screenHigh) + { + const int posFrom = std::ceil (screenLow ); //do not step outside [minX, maxX] in loop below! + const int posTo = std::floor(screenHigh); // + //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + points.emplace_back(x, getValue(x)); + } + } + return points; +} + + +std::vector SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector points; + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const std::pair rangeX = getRangeX(); + + auto addPoint = [&](const CurvePoint& pt) + { + if (!points.empty()) + { + if (pt.x <= points.back().x) //allow ascending x-positions only! algorithm below may cause double-insertion after empty x-ranges! + return; + + if (addSteps_) + if (pt.y != points.back().y) + points.emplace_back(CurvePoint(pt.x, points.back().y)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + } + points.push_back(pt); + }; + + const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX)); + const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX)); + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + Opt ptLe = getLessEq(x); + Opt ptGe = getGreaterEq(x); + //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGe in the following! + const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1; + const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1; + assert(!ptLe || posLe <= i); //check for invalid return values + assert(!ptGe || posGe >= i); // + /* + Breakdown of all combinations of posLe, posGe and expected action (n >= 1) + Note: For every empty x-range of at least one pixel, both next and previous points must be saved to keep the interpolating line stable!!! + + posLe | posGe | action + +-------+-------+-------- + | none | none | break + | i | none | save ptLe; break + | i - n | none | break; + +-------+-------+-------- + | none | i | save ptGe; continue + | i | i | save one of ptLe, ptGe; continue + | i - n | i | save ptGe; continue + +-------+-------+-------- + | none | i + n | save ptGe; jump to position posGe + 1 + | i | i + n | save ptLe; if n == 1: continue; else: save ptGe; jump to position posGe + 1 + | i - n | i + n | save ptLe, ptGe; jump to position posGe + 1 + +-------+-------+-------- + */ + if (posGe < i) + { + if (posLe == i) + addPoint(*ptLe); + break; + } + else if (posGe == i) //test if point would be mapped to pixel x-position i + { + if (posLe == i) // + addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe); + else + addPoint(*ptGe); + } + else + { + if (posLe <= i) + addPoint(*ptLe); + + if (posLe != i || posGe > i + 1) + { + addPoint(*ptGe); + i = posGe; //skip sparse area: +1 will be added by for-loop! + } + } + } + return points; +} + + +Graph2D::Graph2D(wxWindow* parent, + wxWindowID winid, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : wxPanel(parent, winid, pos, size, style, name) +{ + Connect(wxEVT_PAINT, wxPaintEventHandler(Graph2D::onPaintEvent), nullptr, this); + Connect(wxEVT_SIZE, wxSizeEventHandler (Graph2D::onSizeEvent ), nullptr, this); + //http://wiki.wxwidgets.org/Flicker-Free_Drawing + Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Graph2D::onEraseBackGround), nullptr, this); + + //SetDoubleBuffered(true); slow as hell! + + SetBackgroundStyle(wxBG_STYLE_PAINT); + + Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(Graph2D::OnMouseLeftDown), nullptr, this); + Connect(wxEVT_MOTION, wxMouseEventHandler(Graph2D::OnMouseMovement), nullptr, this); + Connect(wxEVT_LEFT_UP, wxMouseEventHandler(Graph2D::OnMouseLeftUp), nullptr, this); + Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(Graph2D::OnMouseCaptureLost), nullptr, this); +} + + +void Graph2D::onPaintEvent(wxPaintEvent& event) +{ + //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! + BufferedPaintDC dc(*this, doubleBuffer); + render(dc); +} + + +void Graph2D::OnMouseLeftDown(wxMouseEvent& event) +{ + activeSel = std::make_unique(*this, event.GetPosition()); + + if (!event.ControlDown()) + oldSel.clear(); + Refresh(); +} + + +void Graph2D::OnMouseMovement(wxMouseEvent& event) +{ + if (activeSel.get()) + { + activeSel->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() + Refresh(); + } +} + + +void Graph2D::OnMouseLeftUp(wxMouseEvent& event) +{ + if (activeSel.get()) + { + if (activeSel->getStartPos() != activeSel->refCurrentPos()) //if it's just a single mouse click: discard selection + { + GraphSelectEvent selEvent(activeSel->refSelection()); //fire off GraphSelectEvent + if (wxEvtHandler* handler = GetEventHandler()) + handler->AddPendingEvent(selEvent); + + oldSel.push_back(activeSel->refSelection()); //commit selection + } + + activeSel.reset(); + Refresh(); + } +} + + +void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event) +{ + activeSel.reset(); + Refresh(); +} + + +void Graph2D::setCurve(const std::shared_ptr& data, const CurveAttributes& ca) +{ + curves_.clear(); + addCurve(data, ca); +} + + +void Graph2D::addCurve(const std::shared_ptr& data, const CurveAttributes& ca) +{ + CurveAttributes newAttr = ca; + if (newAttr.autoColor) + newAttr.setColor(getDefaultColor(curves_.size())); + curves_.emplace_back(data, newAttr); + Refresh(); +} + + +void Graph2D::render(wxDC& dc) const +{ + using namespace numeric; + + //set label font right at the start so that it is considered by wxDC::GetTextExtent() below! + dc.SetFont(labelFont); + + const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! + { + //clear complete client area; set label background color + const wxColor backCol = GetBackgroundColour(); //user-configurable! + //wxPanel::GetClassDefaultAttributes().colBg : + //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + wxDCPenChanger dummy (dc, backCol); + wxDCBrushChanger dummy2(dc, backCol); + dc.DrawRectangle(clientRect); + } + + /* + ----------------------- + | | x-label | + ----------------------- + |y-label | graph area | + |---------------------- + */ + wxRect graphArea = clientRect; + int xLabelPosY = clientRect.y; + int yLabelPosX = clientRect.x; + + switch (attr.labelposX) + { + case LABEL_X_TOP: + graphArea.y += attr.xLabelHeight; + graphArea.height -= attr.xLabelHeight; + break; + case LABEL_X_BOTTOM: + xLabelPosY += clientRect.height - attr.xLabelHeight; + graphArea.height -= attr.xLabelHeight; + break; + case LABEL_X_NONE: + break; + } + switch (attr.labelposY) + { + case LABEL_Y_LEFT: + graphArea.x += attr.yLabelWidth; + graphArea.width -= attr.yLabelWidth; + break; + case LABEL_Y_RIGHT: + yLabelPosX += clientRect.width - attr.yLabelWidth; + graphArea.width -= attr.yLabelWidth; + break; + case LABEL_Y_NONE: + break; + } + + { + //paint graph background (excluding label area) + wxDCPenChanger dummy (dc, getBorderColor()); + wxDCBrushChanger dummy2(dc, attr.backgroundColor); + //accessibility: consider system text and background colors; small drawback: color of graphs is NOT connected to the background! => responsibility of client to use correct colors + + dc.DrawRectangle(graphArea); + graphArea.Deflate(1, 1); //attention more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-const!!! + } + + //set label areas respecting graph area border! + const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, attr.xLabelHeight); + const wxRect yLabelArea(yLabelPosX, graphArea.y, attr.yLabelWidth, graphArea.height); + const wxPoint graphAreaOrigin = graphArea.GetTopLeft(); + + //detect x value range + double minX = attr.minXauto ? std::numeric_limits::infinity() : attr.minX; //automatic: ensure values are initialized by first curve + double maxX = attr.maxXauto ? -std::numeric_limits::infinity() : attr.maxX; // + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (const CurveData* curve = it->first.get()) + { + const std::pair rangeX = curve->getRangeX(); + assert(rangeX.first <= rangeX.second + 1.0e-9); + //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18 + + if (attr.minXauto) + minX = std::min(minX, rangeX.first); + if (attr.maxXauto) + maxX = std::max(maxX, rangeX.second); + } + + const wxSize minimalBlockSizePx = dc.GetTextExtent(L"00"); + + if (minX <= maxX && maxX - minX < std::numeric_limits::infinity()) //valid x-range + { + int blockCountX = 0; + //enlarge minX, maxX to a multiple of a "useful" block size + if (attr.labelposX != LABEL_X_NONE && attr.labelFmtX.get()) + blockCountX = widenRange(minX, maxX, //in/out + graphArea.width, + minimalBlockSizePx.GetWidth() * 7, + *attr.labelFmtX); + + //get raw values + detect y value range + double minY = attr.minYauto ? std::numeric_limits::infinity() : attr.minY; //automatic: ensure values are initialized by first curve + double maxY = attr.maxYauto ? -std::numeric_limits::infinity() : attr.maxY; // + + std::vector> curvePoints(curves_.size()); + std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start an out-of-bounds line + + for (size_t index = 0; index < curves_.size(); ++index) + if (const CurveData* curve = curves_[index].first.get()) + { + std::vector& points = curvePoints[index]; + auto& marker = oobMarker [index]; + + points = curve->getPoints(minX, maxX, graphArea.width); + marker.resize(points.size()); //default value: false + if (!points.empty()) + { + //cut points outside visible x-range now in order to calculate height of visible line fragments only! + const bool doPolygonCut = curves_[index].second.fillMode == CurveAttributes::FILL_POLYGON; //impacts auto minY/maxY!! + cutPointsOutsideX(points, marker, minX, maxX, doPolygonCut); + + if (attr.minYauto || attr.maxYauto) + { + auto itPair = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; }); + if (attr.minYauto) + minY = std::min(minY, itPair.first->y); + if (attr.maxYauto) + maxY = std::max(maxY, itPair.second->y); + } + } + } + + if (minY <= maxY) //valid y-range + { + int blockCountY = 0; + //enlarge minY, maxY to a multiple of a "useful" block size + if (attr.labelposY != LABEL_Y_NONE && attr.labelFmtY.get()) + blockCountY = widenRange(minY, maxY, //in/out + graphArea.height, + minimalBlockSizePx.GetHeight() * 3, + *attr.labelFmtY); + + if (graphArea.width <= 1 || graphArea.height <= 1) return; + const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0] + + //calculate curve coordinates on graph area + std::vector> drawPoints(curves_.size()); + + for (size_t index = 0; index < curves_.size(); ++index) + { + auto& cp = curvePoints[index]; + + //add two artificial points to fill the curve area towards x-axis => do this before cutPointsOutsideY() to handle curve leaving upper bound + if (curves_[index].second.fillMode == CurveAttributes::FILL_CURVE) + if (!cp.empty()) + { + cp.emplace_back(CurvePoint(cp.back ().x, minY)); //add lower right and left corners + cp.emplace_back(CurvePoint(cp.front().x, minY)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + oobMarker[index].back() = true; + oobMarker[index].push_back(true); + oobMarker[index].push_back(true); + } + + //cut points outside visible y-range before calculating pixels: + //1. realToScreenRound() deforms out-of-range values! + //2. pixels that are grossly out of range can be a severe performance problem when drawing on the DC (Windows) + const bool doPolygonCut = curves_[index].second.fillMode != CurveAttributes::FILL_NONE; + cutPointsOutsideY(cp, oobMarker[index], minY, maxY, doPolygonCut); + + auto& dp = drawPoints[index]; + for (const CurvePoint& pt : cp) + dp.push_back(wxPoint(cvrtX.realToScreenRound(pt.x), + cvrtY.realToScreenRound(pt.y)) + graphAreaOrigin); + } + + //update active mouse selection + if (activeSel.get() && graphArea.width > 0 && graphArea.height > 0) + { + auto widen = [](double* low, double* high) + { + if (*low > *high) + std::swap(low, high); + *low -= 0.5; + *high += 0.5; + }; + + const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea + const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin; + + //normalize positions: a mouse selection is symmetric and *not* an half-open range! + double screenFromX = clampCpy(screenStart .x, 0, graphArea.width - 1); + double screenFromY = clampCpy(screenStart .y, 0, graphArea.height - 1); + double screenToX = clampCpy(screenCurrent.x, 0, graphArea.width - 1); + double screenToY = clampCpy(screenCurrent.y, 0, graphArea.height - 1); + widen(&screenFromX, &screenToX); //use full pixel range for selection! + widen(&screenFromY, &screenToY); + + //save current selection as "double" coordinates + activeSel->refSelection().from = CurvePoint(cvrtX.screenToReal(screenFromX), + cvrtY.screenToReal(screenFromY)); + + activeSel->refSelection().to = CurvePoint(cvrtX.screenToReal(screenToX), + cvrtY.screenToReal(screenToY)); + } + + //#################### begin drawing #################### + //1. draw colored area under curves + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (it->second.fillMode != CurveAttributes::FILL_NONE) + { + const std::vector& points = drawPoints[it - curves_.begin()]; + if (points.size() >= 3) + { + wxDCBrushChanger dummy(dc, it->second.fillColor); + wxDCPenChanger dummy2(dc, it->second.fillColor); + dc.DrawPolygon(static_cast(points.size()), &points[0]); + } + } + + //2. draw all currently set mouse selections (including active selection) + std::vector allSelections = oldSel; + if (activeSel) + allSelections.push_back(activeSel->refSelection()); + { + //alpha channel not supported on wxMSW, so draw selection before curves + wxDCBrushChanger dummy(dc, wxColor(168, 202, 236)); //light blue + wxDCPenChanger dummy2(dc, wxColor( 51, 153, 255)); //dark blue + + auto shrink = [](double* low, double* high) + { + if (*low > *high) + std::swap(low, high); + *low += 0.5; + *high -= 0.5; + if (*low > *high) + *low = *high = (*low + *high) / 2; + }; + + for (const SelectionBlock& sel : allSelections) + { + //harmonize with active mouse selection above + double screenFromX = cvrtX.realToScreen(sel.from.x); + double screenFromY = cvrtY.realToScreen(sel.from.y); + double screenToX = cvrtX.realToScreen(sel.to.x); + double screenToY = cvrtY.realToScreen(sel.to.y); + shrink(&screenFromX, &screenToX); + shrink(&screenFromY, &screenToY); + + clamp(screenFromX, 0.0, graphArea.width - 1.0); + clamp(screenFromY, 0.0, graphArea.height - 1.0); + clamp(screenToX, 0.0, graphArea.width - 1.0); + clamp(screenToY, 0.0, graphArea.height - 1.0); + + const wxPoint pixelFrom = wxPoint(numeric::round(screenFromX), + numeric::round(screenFromY)) + graphAreaOrigin; + const wxPoint pixelTo = wxPoint(numeric::round(screenToX), + numeric::round(screenToY)) + graphAreaOrigin; + switch (attr.mouseSelMode) + { + case SELECT_NONE: + break; + case SELECT_RECTANGLE: + dc.DrawRectangle(wxRect(pixelFrom, pixelTo)); + break; + case SELECT_X_AXIS: + dc.DrawRectangle(wxRect(wxPoint(pixelFrom.x, graphArea.y), wxPoint(pixelTo.x, graphArea.y + graphArea.height - 1))); + break; + case SELECT_Y_AXIS: + dc.DrawRectangle(wxRect(wxPoint(graphArea.x, pixelFrom.y), wxPoint(graphArea.x + graphArea.width - 1, pixelTo.y))); + break; + } + } + } + + //3. draw labels and background grid + drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr.labelFmtX); + drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY); + + //4. finally draw curves + { + dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside + ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion()); + + for (auto it = curves_.begin(); it != curves_.end(); ++it) + { + wxDCPenChanger dummy(dc, wxPen(it->second.color, it->second.lineWidth)); + + const size_t index = it - curves_.begin(); + const std::vector& points = drawPoints[index]; + const auto& marker = oobMarker [index]; + assert(points.size() == marker.size()); + + //draw all parts of the curve except for the out-of-bounds fragments + size_t drawIndexFirst = 0; + while (drawIndexFirst < points.size()) + { + size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), true) - marker.begin(); + if (drawIndexLast < points.size()) ++drawIndexLast; + + const int pointCount = static_cast(drawIndexLast - drawIndexFirst); + if (pointCount > 0) + { + if (pointCount >= 2) //on OS X wxWidgets has a nasty assert on this + dc.DrawLines(pointCount, &points[drawIndexFirst]); + dc.DrawPoint(points[drawIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel + } + drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), false) - marker.begin(); + } + } + } + + //5. draw corner texts + for (const auto& ct : attr.cornerTexts) + drawCornerText(dc, graphArea, ct.second, ct.first); + } + } +} -- cgit