2577 lines
67 KiB
C++
2577 lines
67 KiB
C++
/////////////////////////////////////////////////////////////////////////////
|
|
// Name: mathplot.cpp
|
|
// Purpose: Framework for plotting in wxWindows
|
|
// Original Author: David Schalig
|
|
// Maintainer: Davide Rondini
|
|
// Contributors: Jose Luis Blanco, Val Greene, Maciej Suminski, Tomasz Wlostowski
|
|
// Created: 21/07/2003
|
|
// Last edit: 2023
|
|
// Copyright: (c) David Schalig, Davide Rondini
|
|
// Copyright (c) 2021-2023 KiCad Developers, see AUTHORS.txt for contributors.
|
|
// Licence: wxWindows licence
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <wx/window.h>
|
|
|
|
// Comment out for release operation:
|
|
// (Added by J.L.Blanco, Aug 2007)
|
|
//#define MATHPLOT_DO_LOGGING
|
|
|
|
#ifdef __BORLANDC__
|
|
#pragma hdrstop
|
|
#endif
|
|
|
|
#ifndef WX_PRECOMP
|
|
#include "wx/object.h"
|
|
#include "wx/font.h"
|
|
#include "wx/colour.h"
|
|
#include "wx/sizer.h"
|
|
#include "wx/intl.h"
|
|
#include "wx/dcclient.h"
|
|
#include "wx/cursor.h"
|
|
#include "gal/cursors.h"
|
|
#endif
|
|
|
|
#include <widgets/mathplot.h>
|
|
#include <wx/graphics.h>
|
|
#include <wx/image.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdio> // used only for debug
|
|
#include <ctime> // used for representation of x axes involving date
|
|
#include <set>
|
|
|
|
// Memory leak debugging
|
|
#ifdef _DEBUG
|
|
#define new DEBUG_NEW
|
|
#endif
|
|
|
|
// Legend margins
|
|
#define mpLEGEND_MARGIN 5
|
|
#define mpLEGEND_LINEWIDTH 10
|
|
|
|
// See doxygen comments.
|
|
double mpWindow::zoomIncrementalFactor = 1.1;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpLayer
|
|
// -----------------------------------------------------------------------------
|
|
|
|
IMPLEMENT_ABSTRACT_CLASS( mpLayer, wxObject )
|
|
|
|
mpLayer::mpLayer() :
|
|
m_type( mpLAYER_UNDEF )
|
|
{
|
|
SetPen( (wxPen&) *wxBLACK_PEN );
|
|
SetFont( (wxFont&) *wxNORMAL_FONT );
|
|
m_continuous = false; // Default
|
|
m_showName = true; // Default
|
|
m_visible = true;
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpInfoLayer
|
|
// -----------------------------------------------------------------------------
|
|
IMPLEMENT_DYNAMIC_CLASS( mpInfoLayer, mpLayer )
|
|
|
|
mpInfoLayer::mpInfoLayer()
|
|
{
|
|
m_dim = wxRect( 0, 0, 1, 1 );
|
|
m_brush = *wxTRANSPARENT_BRUSH;
|
|
m_reference.x = 0; m_reference.y = 0;
|
|
m_winX = 1; // parent->GetScrX();
|
|
m_winY = 1; // parent->GetScrY();
|
|
m_type = mpLAYER_INFO;
|
|
}
|
|
|
|
|
|
mpInfoLayer::mpInfoLayer( wxRect rect, const wxBrush* brush ) :
|
|
m_dim( rect )
|
|
{
|
|
m_brush = *brush;
|
|
m_reference.x = rect.x;
|
|
m_reference.y = rect.y;
|
|
m_winX = 1; // parent->GetScrX();
|
|
m_winY = 1; // parent->GetScrY();
|
|
m_type = mpLAYER_INFO;
|
|
}
|
|
|
|
|
|
mpInfoLayer::~mpInfoLayer()
|
|
{
|
|
}
|
|
|
|
|
|
bool mpInfoLayer::Inside( const wxPoint& point ) const
|
|
{
|
|
return m_dim.Contains( point );
|
|
}
|
|
|
|
|
|
void mpInfoLayer::Move( wxPoint delta )
|
|
{
|
|
m_dim.SetX( m_reference.x + delta.x );
|
|
m_dim.SetY( m_reference.y + delta.y );
|
|
}
|
|
|
|
|
|
void mpInfoLayer::UpdateReference()
|
|
{
|
|
m_reference.x = m_dim.x;
|
|
m_reference.y = m_dim.y;
|
|
}
|
|
|
|
|
|
void mpInfoLayer::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
if( m_visible )
|
|
{
|
|
// Adjust relative position inside the window
|
|
int scrx = w.GetScrX();
|
|
int scry = w.GetScrY();
|
|
|
|
// Avoid dividing by 0
|
|
if( scrx == 0 )
|
|
scrx = 1;
|
|
|
|
if( scry == 0 )
|
|
scry = 1;
|
|
|
|
if( (m_winX != scrx) || (m_winY != scry) )
|
|
{
|
|
if( m_winX > 1 )
|
|
m_dim.x = (int) floor( (double) (m_dim.x * scrx / m_winX) );
|
|
|
|
if( m_winY > 1 )
|
|
{
|
|
m_dim.y = (int) floor( (double) (m_dim.y * scry / m_winY) );
|
|
UpdateReference();
|
|
}
|
|
|
|
// Finally update window size
|
|
m_winX = scrx;
|
|
m_winY = scry;
|
|
}
|
|
|
|
dc.SetPen( m_pen );
|
|
dc.SetBrush( m_brush );
|
|
dc.DrawRectangle( m_dim.x, m_dim.y, m_dim.width, m_dim.height );
|
|
}
|
|
}
|
|
|
|
|
|
wxPoint mpInfoLayer::GetPosition() const
|
|
{
|
|
return m_dim.GetPosition();
|
|
}
|
|
|
|
|
|
wxSize mpInfoLayer::GetSize() const
|
|
{
|
|
return m_dim.GetSize();
|
|
}
|
|
|
|
|
|
mpInfoLegend::mpInfoLegend() :
|
|
mpInfoLayer()
|
|
{
|
|
}
|
|
|
|
|
|
mpInfoLegend::mpInfoLegend( wxRect rect, const wxBrush* brush ) :
|
|
mpInfoLayer( rect, brush )
|
|
{
|
|
}
|
|
|
|
|
|
mpInfoLegend::~mpInfoLegend()
|
|
{
|
|
}
|
|
|
|
|
|
void mpInfoLegend::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
if( m_visible )
|
|
{
|
|
// Adjust relative position inside the window
|
|
int scrx = w.GetScrX();
|
|
int scry = w.GetScrY();
|
|
|
|
if( m_winX != scrx || m_winY != scry )
|
|
{
|
|
if( m_winX > 1 )
|
|
m_dim.x = (int) floor( (double) (m_dim.x * scrx / m_winX) );
|
|
|
|
if( m_winY > 1 )
|
|
{
|
|
m_dim.y = (int) floor( (double) (m_dim.y * scry / m_winY) );
|
|
UpdateReference();
|
|
}
|
|
|
|
// Finally update window size
|
|
m_winX = scrx;
|
|
m_winY = scry;
|
|
}
|
|
|
|
dc.SetBrush( m_brush );
|
|
dc.SetFont( m_font );
|
|
|
|
const int baseWidth = mpLEGEND_MARGIN * 2 + mpLEGEND_LINEWIDTH;
|
|
int textX = baseWidth, textY = mpLEGEND_MARGIN;
|
|
int plotCount = 0;
|
|
int posY = 0;
|
|
int tmpX = 0;
|
|
int tmpY = 0;
|
|
mpLayer* layer = nullptr;
|
|
wxPen lpen;
|
|
wxString label;
|
|
|
|
for( unsigned int p = 0; p < w.CountAllLayers(); p++ )
|
|
{
|
|
layer = w.GetLayer( p );
|
|
|
|
if( layer->GetLayerType() == mpLAYER_PLOT && layer->IsVisible() )
|
|
{
|
|
label = layer->GetDisplayName();
|
|
dc.GetTextExtent( label, &tmpX, &tmpY );
|
|
textX = ( textX > tmpX + baseWidth ) ? textX : tmpX + baseWidth + mpLEGEND_MARGIN;
|
|
textY += tmpY;
|
|
}
|
|
}
|
|
|
|
dc.SetPen( m_pen );
|
|
dc.SetBrush( m_brush );
|
|
m_dim.width = textX;
|
|
|
|
if( textY != mpLEGEND_MARGIN ) // Don't draw any thing if there are no visible layers
|
|
{
|
|
textY += mpLEGEND_MARGIN;
|
|
m_dim.height = textY;
|
|
dc.DrawRectangle( m_dim.x, m_dim.y, m_dim.width, m_dim.height );
|
|
|
|
for( unsigned int p2 = 0; p2 < w.CountAllLayers(); p2++ )
|
|
{
|
|
layer = w.GetLayer( p2 );
|
|
|
|
if( layer->GetLayerType() == mpLAYER_PLOT && layer->IsVisible() )
|
|
{
|
|
label = layer->GetDisplayName();
|
|
lpen = layer->GetPen();
|
|
dc.GetTextExtent( label, &tmpX, &tmpY );
|
|
dc.SetPen( lpen );
|
|
posY = m_dim.y + mpLEGEND_MARGIN + plotCount * tmpY + (tmpY >> 1);
|
|
dc.DrawLine( m_dim.x + mpLEGEND_MARGIN, // X start coord
|
|
posY, // Y start coord
|
|
m_dim.x + mpLEGEND_LINEWIDTH + mpLEGEND_MARGIN, // X end coord
|
|
posY );
|
|
dc.DrawText( label,
|
|
m_dim.x + baseWidth,
|
|
m_dim.y + mpLEGEND_MARGIN + plotCount * tmpY );
|
|
plotCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpLayer implementations - functions
|
|
// -----------------------------------------------------------------------------
|
|
|
|
IMPLEMENT_ABSTRACT_CLASS( mpFX, mpLayer )
|
|
|
|
mpFX::mpFX( const wxString& name, int flags )
|
|
{
|
|
SetName( name );
|
|
m_flags = flags;
|
|
m_type = mpLAYER_PLOT;
|
|
}
|
|
|
|
|
|
void mpFX::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
if( m_visible )
|
|
{
|
|
dc.SetPen( m_pen );
|
|
|
|
wxCoord startPx = w.GetMarginLeft();
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
wxCoord iy = 0;
|
|
|
|
if( m_pen.GetWidth() <= 1 )
|
|
{
|
|
for( wxCoord i = startPx; i < endPx; ++i )
|
|
{
|
|
iy = w.y2p( GetY( w.p2x( i ) ) );
|
|
|
|
// Draw the point only if you can draw outside margins or if the point is inside margins
|
|
if( (iy >= minYpx) && (iy <= maxYpx) )
|
|
dc.DrawPoint( i, iy );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( wxCoord i = startPx; i < endPx; ++i )
|
|
{
|
|
iy = w.y2p( GetY( w.p2x( i ) ) );
|
|
|
|
// Draw the point only if you can draw outside margins or if the point is inside margins
|
|
if( iy >= minYpx && iy <= maxYpx )
|
|
dc.DrawLine( i, iy, i, iy );
|
|
}
|
|
}
|
|
|
|
if( !m_name.IsEmpty() && m_showName )
|
|
{
|
|
dc.SetFont( m_font );
|
|
|
|
wxCoord tx, ty;
|
|
dc.GetTextExtent( m_name, &tx, &ty );
|
|
|
|
if( (m_flags & mpALIGNMASK) == mpALIGN_RIGHT )
|
|
tx = (w.GetScrX() - tx) - w.GetMarginRight() - 8;
|
|
else if( (m_flags & mpALIGNMASK) == mpALIGN_CENTER )
|
|
tx = ( (w.GetScrX() - w.GetMarginRight() - w.GetMarginLeft() - tx) / 2 ) + w.GetMarginLeft();
|
|
else
|
|
tx = w.GetMarginLeft() + 8;
|
|
|
|
dc.DrawText( m_name, tx, w.y2p( GetY( w.p2x( tx ) ) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
IMPLEMENT_ABSTRACT_CLASS( mpFY, mpLayer )
|
|
|
|
mpFY::mpFY( const wxString& name, int flags )
|
|
{
|
|
SetName( name );
|
|
m_flags = flags;
|
|
m_type = mpLAYER_PLOT;
|
|
}
|
|
|
|
|
|
void mpFY::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
if( m_visible )
|
|
{
|
|
dc.SetPen( m_pen );
|
|
|
|
wxCoord i, ix;
|
|
|
|
wxCoord startPx = w.GetMarginLeft();
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
if( m_pen.GetWidth() <= 1 )
|
|
{
|
|
for( i = minYpx; i < maxYpx; ++i )
|
|
{
|
|
ix = w.x2p( GetX( w.p2y( i ) ) );
|
|
|
|
if( (ix >= startPx) && (ix <= endPx) )
|
|
dc.DrawPoint( ix, i );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( i = 0; i< w.GetScrY(); ++i )
|
|
{
|
|
ix = w.x2p( GetX( w.p2y( i ) ) );
|
|
|
|
if( (ix >= startPx) && (ix <= endPx) )
|
|
dc.DrawLine( ix, i, ix, i );
|
|
}
|
|
}
|
|
|
|
if( !m_name.IsEmpty() && m_showName )
|
|
{
|
|
dc.SetFont( m_font );
|
|
|
|
wxCoord tx, ty;
|
|
dc.GetTextExtent( m_name, &tx, &ty );
|
|
|
|
if( (m_flags & mpALIGNMASK) == mpALIGN_TOP )
|
|
ty = w.GetMarginTop() + 8;
|
|
else if( (m_flags & mpALIGNMASK) == mpALIGN_CENTER )
|
|
ty = ( ( w.GetScrY() - w.GetMarginTop() - w.GetMarginBottom() - ty) / 2 ) + w.GetMarginTop();
|
|
else
|
|
ty = w.GetScrY() - 8 - ty - w.GetMarginBottom();
|
|
|
|
dc.DrawText( m_name, w.x2p( GetX( w.p2y( ty ) ) ), ty );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
IMPLEMENT_ABSTRACT_CLASS( mpFXY, mpLayer )
|
|
|
|
mpFXY::mpFXY( const wxString& name, int flags )
|
|
{
|
|
SetName( name );
|
|
m_flags = flags;
|
|
m_type = mpLAYER_PLOT;
|
|
m_scaleX = nullptr;
|
|
m_scaleY = nullptr;
|
|
|
|
// Avoid not initialized members:
|
|
maxDrawX = minDrawX = maxDrawY = minDrawY = 0;
|
|
}
|
|
|
|
|
|
void mpFXY::UpdateViewBoundary( wxCoord xnew, wxCoord ynew )
|
|
{
|
|
// Keep track of how many points have been drawn and the bounding box
|
|
maxDrawX = (xnew > maxDrawX) ? xnew : maxDrawX;
|
|
minDrawX = (xnew < minDrawX) ? xnew : minDrawX;
|
|
maxDrawY = (maxDrawY > ynew) ? maxDrawY : ynew;
|
|
minDrawY = (minDrawY < ynew) ? minDrawY : ynew;
|
|
// drawnPoints++;
|
|
}
|
|
|
|
|
|
void mpFXY::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
// If trace doesn't have any data yet then it won't have any scale set. In any case, there's
|
|
// nothing to plot.
|
|
if( !GetCount() )
|
|
return;
|
|
|
|
wxCHECK_RET( m_scaleX, wxS( "X scale was not set" ) );
|
|
wxCHECK_RET( m_scaleY, wxS( "Y scale was not set" ) );
|
|
|
|
if( !m_visible )
|
|
return;
|
|
|
|
wxCoord startPx = w.GetMarginLeft();
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
// Check for a collapsed window before we try to allocate a negative number of points
|
|
if( endPx <= startPx || minYpx >= maxYpx )
|
|
return;
|
|
|
|
dc.SetPen( m_pen );
|
|
|
|
double x, y;
|
|
// Do this to reset the counters to evaluate bounding box for label positioning
|
|
Rewind();
|
|
GetNextXY( x, y );
|
|
maxDrawX = x;
|
|
minDrawX = x;
|
|
maxDrawY = y;
|
|
minDrawY = y;
|
|
// drawnPoints = 0;
|
|
Rewind();
|
|
|
|
dc.SetClippingRegion( startPx, minYpx, endPx - startPx + 1, maxYpx - minYpx + 1 );
|
|
|
|
if( !m_continuous )
|
|
{
|
|
bool first = true;
|
|
wxCoord ix = 0;
|
|
std::set<wxCoord> ys;
|
|
|
|
while( GetNextXY( x, y ) )
|
|
{
|
|
double px = m_scaleX->TransformToPlot( x );
|
|
double py = m_scaleY->TransformToPlot( y );
|
|
wxCoord newX = w.x2p( px );
|
|
|
|
if( first )
|
|
{
|
|
ix = newX;
|
|
first = false;
|
|
}
|
|
|
|
if( newX == ix ) // continue until a new X coordinate is reached
|
|
{
|
|
// collect all unique points
|
|
ys.insert( w.y2p( py ) );
|
|
continue;
|
|
}
|
|
|
|
for( auto& iy: ys )
|
|
{
|
|
if( (ix >= startPx) && (ix <= endPx) && (iy >= minYpx) && (iy <= maxYpx) )
|
|
{
|
|
// for some reason DrawPoint does not use the current pen, so we use
|
|
// DrawLine for fat pens
|
|
if( m_pen.GetWidth() <= 1 )
|
|
dc.DrawPoint( ix, iy );
|
|
else
|
|
dc.DrawLine( ix, iy, ix, iy );
|
|
|
|
UpdateViewBoundary( ix, iy );
|
|
}
|
|
}
|
|
|
|
ys.clear();
|
|
ix = newX;
|
|
ys.insert( w.y2p( py ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int count = 0;
|
|
int x0 = 0; // X position of merged current vertical line
|
|
int ymin0 = 0; // y min coord of merged current vertical line
|
|
int ymax0 = 0; // y max coord of merged current vertical line
|
|
int dupx0 = 0; // count of currently merged vertical lines
|
|
wxPoint line_start; // starting point of the current line to draw
|
|
|
|
// A buffer to store coordinates of lines to draw
|
|
std::vector<wxPoint>pointList;
|
|
pointList.reserve( ( endPx - startPx ) * 2 );
|
|
|
|
double nextX;
|
|
double nextY;
|
|
bool hasNext = GetNextXY( nextX, nextY );
|
|
bool offRight = false;
|
|
|
|
// Note: we can use dc.DrawLines() only for a reasonable number or points (<10,000),
|
|
// because at least on Windows dc.DrawLines() can hang for a lot of points. Note that
|
|
// this includes the intermediate points when drawing dotted lines.
|
|
|
|
// Our first-pass optimization is to exclude points outside the view, and aggregate all
|
|
// contiguous y values found at a single x value into a vertical line.
|
|
while( hasNext )
|
|
{
|
|
x = nextX;
|
|
y = nextY;
|
|
hasNext = GetNextXY( nextX, nextY );
|
|
|
|
double px = m_scaleX->TransformToPlot( x );
|
|
double py = m_scaleY->TransformToPlot( y );
|
|
|
|
wxCoord x1 = w.x2p( px );
|
|
wxCoord y1 = w.y2p( py );
|
|
|
|
// Note that we can't start *right* at the edge of the view because we need to
|
|
// interpolate between two points, one of which might be outside the view.
|
|
// Note: x1 is a value truncated from px by w.x2p(). So to be sure the first point
|
|
// is drawn, the x1 low limit is startPx-1 in plot coordinates
|
|
if( x1 < startPx-1 )
|
|
{
|
|
wxCoord nextX1 = w.x2p( m_scaleX->TransformToPlot( nextX ) );
|
|
|
|
if( nextX1 < startPx-1 )
|
|
continue;
|
|
}
|
|
else if( x1 > endPx )
|
|
{
|
|
if( offRight )
|
|
continue;
|
|
else
|
|
offRight = true;
|
|
}
|
|
|
|
if( !count || line_start.x != x1 )
|
|
{
|
|
// We've aggregated a bunch of y values with a shared x value, so we need to draw
|
|
// a vertical line. However, short vertical segments spoil anti-aliasing on
|
|
// Retina displays, so only draw them if they're "significant" (the user should
|
|
// zoom in if they need a more accurate picture).
|
|
if( count && dupx0 > 1 && abs( ymax0 - ymin0 ) > 2 )
|
|
dc.DrawLine( x0, ymin0, x0, ymax0 );
|
|
|
|
x0 = x1;
|
|
ymin0 = ymax0 = y1;
|
|
dupx0 = 0;
|
|
|
|
pointList.emplace_back( wxPoint( x1, y1 ) );
|
|
|
|
line_start.x = x1;
|
|
line_start.y = y1;
|
|
count++;
|
|
}
|
|
else
|
|
{
|
|
ymin0 = std::min( ymin0, y1 );
|
|
ymax0 = std::max( ymax0, y1 );
|
|
x0 = x1;
|
|
dupx0++;
|
|
}
|
|
}
|
|
|
|
if( pointList.size() > 1 )
|
|
{
|
|
// Second pass optimization is to merge horizontal segments. This improves the look
|
|
// of dotted lines, keeps the point count down, and it's easy.
|
|
//
|
|
// This pass also includes a final protection to keep MSW from hanging by chunking to
|
|
// a size it can handle.
|
|
std::vector<wxPoint> drawPoints;
|
|
drawPoints.reserve( ( endPx - startPx ) * 2 );
|
|
|
|
#ifdef __WXMSW__
|
|
int chunkSize = 10000;
|
|
#else
|
|
int chunkSize = 100000;
|
|
#endif
|
|
if( dc.GetPen().GetStyle() == wxPENSTYLE_DOT )
|
|
chunkSize /= 500;
|
|
|
|
drawPoints.push_back( pointList[0] ); // push the first point in list
|
|
|
|
for( size_t ii = 1; ii < pointList.size()-1; ii++ )
|
|
{
|
|
// Skip intermediate points between the first point and the last point of the
|
|
// segment candidate
|
|
if( drawPoints.back().y == pointList[ii].y &&
|
|
drawPoints.back().y == pointList[ii+1].y )
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
drawPoints.push_back( pointList[ii] );
|
|
|
|
if( (int) drawPoints.size() > chunkSize )
|
|
{
|
|
dc.DrawLines( (int) drawPoints.size(), &drawPoints[0] );
|
|
drawPoints.clear();
|
|
|
|
// Restart the line with the current point
|
|
drawPoints.push_back( pointList[ii] );
|
|
}
|
|
}
|
|
}
|
|
|
|
// push the last point to draw in list
|
|
if( drawPoints.back() != pointList.back() )
|
|
drawPoints.push_back( pointList.back() );
|
|
|
|
dc.DrawLines( (int) drawPoints.size(), &drawPoints[0] );
|
|
}
|
|
}
|
|
|
|
if( !m_name.IsEmpty() && m_showName )
|
|
{
|
|
dc.SetFont( m_font );
|
|
|
|
wxCoord tx, ty;
|
|
dc.GetTextExtent( m_name, &tx, &ty );
|
|
|
|
if( (m_flags & mpALIGNMASK) == mpALIGN_NW )
|
|
{
|
|
tx = minDrawX + 8;
|
|
ty = maxDrawY + 8;
|
|
}
|
|
else if( (m_flags & mpALIGNMASK) == mpALIGN_NE )
|
|
{
|
|
tx = maxDrawX - tx - 8;
|
|
ty = maxDrawY + 8;
|
|
}
|
|
else if( (m_flags & mpALIGNMASK) == mpALIGN_SE )
|
|
{
|
|
tx = maxDrawX - tx - 8;
|
|
ty = minDrawY - ty - 8;
|
|
}
|
|
else
|
|
{
|
|
// mpALIGN_SW
|
|
tx = minDrawX + 8;
|
|
ty = minDrawY - ty - 8;
|
|
}
|
|
|
|
dc.DrawText( m_name, tx, ty );
|
|
}
|
|
|
|
dc.DestroyClippingRegion();
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpLayer implementations - furniture (scales, ...)
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#define mpLN10 2.3025850929940456840179914546844
|
|
|
|
void mpScaleX::recalculateTicks( wxDC& dc, mpWindow& w )
|
|
{
|
|
double minV, maxV, minVvis, maxVvis;
|
|
|
|
GetDataRange( minV, maxV );
|
|
getVisibleDataRange( w, minVvis, maxVvis );
|
|
|
|
m_absVisibleMaxV = std::max( std::abs( minVvis ), std::abs( maxVvis ) );
|
|
|
|
m_tickValues.clear();
|
|
m_tickLabels.clear();
|
|
|
|
double minErr = 1000000000000.0;
|
|
double bestStep = 1.0;
|
|
int m_scrX = w.GetXScreen();
|
|
|
|
for( int i = 10; i <= 20; i += 2 )
|
|
{
|
|
double curr_step = fabs( maxVvis - minVvis ) / (double) i;
|
|
double base = pow( 10, floor( log10( curr_step ) ) );
|
|
double stepInt = floor( curr_step / base ) * base;
|
|
double err = fabs( curr_step - stepInt );
|
|
|
|
if( err < minErr )
|
|
{
|
|
minErr = err;
|
|
bestStep = stepInt;
|
|
}
|
|
}
|
|
|
|
double numberSteps = floor( ( maxVvis - minVvis ) / bestStep );
|
|
|
|
// Half the number of ticks according to window size.
|
|
// The value 96 is used to have only 4 ticks when m_scrX is 268.
|
|
// For each 96 device context units, is possible to add a new tick.
|
|
while( numberSteps - 2.0 >= m_scrX/96.0 )
|
|
{
|
|
bestStep *= 2;
|
|
numberSteps = floor( ( maxVvis - minVvis ) / bestStep );
|
|
}
|
|
|
|
double v = floor( minVvis / bestStep ) * bestStep;
|
|
double zeroOffset = 100000000.0;
|
|
|
|
while( v < maxVvis )
|
|
{
|
|
m_tickValues.push_back( v );
|
|
|
|
if( fabs( v ) < zeroOffset )
|
|
zeroOffset = fabs( v );
|
|
|
|
v += bestStep;
|
|
}
|
|
|
|
if( zeroOffset <= bestStep )
|
|
{
|
|
for( double& t : m_tickValues )
|
|
t -= zeroOffset;
|
|
}
|
|
|
|
for( double t : m_tickValues )
|
|
m_tickLabels.emplace_back( t );
|
|
|
|
updateTickLabels( dc, w );
|
|
}
|
|
|
|
|
|
mpScaleBase::mpScaleBase()
|
|
{
|
|
m_rangeSet = false;
|
|
m_axisLocked = false;
|
|
m_axisMin = 0;
|
|
m_axisMax = 0;
|
|
m_nameFlags = mpALIGN_BORDER_BOTTOM;
|
|
|
|
// initialize these members mainly to avoid not initialized values
|
|
m_offset = 0.0;
|
|
m_scale = 1.0;
|
|
m_absVisibleMaxV = 0.0;
|
|
m_flags = 0; // Flag for axis alignment
|
|
m_ticks = true; // Flag to toggle between ticks or grid
|
|
m_minV = 0.0;
|
|
m_maxV = 0.0;
|
|
m_maxLabelHeight = 1;
|
|
m_maxLabelWidth = 1;
|
|
}
|
|
|
|
|
|
void mpScaleBase::computeLabelExtents( wxDC& dc, mpWindow& w )
|
|
{
|
|
m_maxLabelHeight = 0;
|
|
m_maxLabelWidth = 0;
|
|
|
|
for( const TICK_LABEL& tickLabel : m_tickLabels )
|
|
{
|
|
int tx, ty;
|
|
const wxString s = tickLabel.label;
|
|
|
|
dc.GetTextExtent( s, &tx, &ty );
|
|
m_maxLabelHeight = std::max( ty, m_maxLabelHeight );
|
|
m_maxLabelWidth = std::max( tx, m_maxLabelWidth );
|
|
}
|
|
}
|
|
|
|
|
|
void mpScaleBase::updateTickLabels( wxDC& dc, mpWindow& w )
|
|
{
|
|
formatLabels();
|
|
computeLabelExtents( dc, w );
|
|
}
|
|
|
|
|
|
void mpScaleY::getVisibleDataRange( mpWindow& w, double& minV, double& maxV )
|
|
{
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
double pymin = w.p2y( minYpx );
|
|
double pymax = w.p2y( maxYpx );
|
|
|
|
minV = TransformFromPlot( pymax );
|
|
maxV = TransformFromPlot( pymin );
|
|
}
|
|
|
|
|
|
void mpScaleY::computeSlaveTicks( mpWindow& w )
|
|
{
|
|
// No need for slave ticks when there aren't 2 main ticks for them to go between
|
|
if( m_masterScale->m_tickValues.size() < 2 )
|
|
return;
|
|
|
|
m_tickValues.clear();
|
|
m_tickLabels.clear();
|
|
|
|
double p0 = m_masterScale->TransformToPlot( m_masterScale->m_tickValues[0] );
|
|
double p1 = m_masterScale->TransformToPlot( m_masterScale->m_tickValues[1] );
|
|
|
|
m_scale = 1.0 / ( m_maxV - m_minV );
|
|
m_offset = -m_minV;
|
|
|
|
double y_slave0 = p0 / m_scale;
|
|
double y_slave1 = p1 / m_scale;
|
|
|
|
double dy_slave = (y_slave1 - y_slave0);
|
|
double exponent = floor( log10( dy_slave ) );
|
|
double base = dy_slave / pow( 10.0, exponent );
|
|
|
|
double dy_scaled = ceil( 2.0 * base ) / 2.0 * pow( 10.0, exponent );
|
|
|
|
double minvv, maxvv;
|
|
|
|
getVisibleDataRange( w, minvv, maxvv );
|
|
|
|
minvv = floor( minvv / dy_scaled ) * dy_scaled;
|
|
|
|
m_scale = 1.0 / ( m_maxV - m_minV );
|
|
m_scale *= dy_slave / dy_scaled;
|
|
|
|
m_offset = p0 / m_scale - minvv;
|
|
|
|
m_tickValues.clear();
|
|
|
|
m_absVisibleMaxV = 0;
|
|
|
|
for( double tickValue : m_masterScale->m_tickValues )
|
|
{
|
|
double m = TransformFromPlot( m_masterScale->TransformToPlot( tickValue ) );
|
|
m_tickValues.push_back( m );
|
|
m_tickLabels.emplace_back( m );
|
|
m_absVisibleMaxV = std::max( m_absVisibleMaxV, fabs( m ) );
|
|
}
|
|
}
|
|
|
|
|
|
void mpScaleY::recalculateTicks( wxDC& dc, mpWindow& w )
|
|
{
|
|
double minVvis, maxVvis;
|
|
|
|
if( m_axisLocked )
|
|
{
|
|
minVvis = m_axisMin;
|
|
maxVvis = m_axisMax;
|
|
m_offset = -m_axisMin;
|
|
m_scale = 1.0 / ( m_axisMax - m_axisMin );
|
|
}
|
|
else if( m_masterScale )
|
|
{
|
|
computeSlaveTicks( w );
|
|
updateTickLabels( dc, w );
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
getVisibleDataRange( w, minVvis, maxVvis );
|
|
}
|
|
|
|
m_absVisibleMaxV = std::max( std::abs( minVvis ), std::abs( maxVvis ) );
|
|
m_tickValues.clear();
|
|
m_tickLabels.clear();
|
|
|
|
double minErr = 1000000000000.0;
|
|
double bestStep = 1.0;
|
|
int m_scrY = w.GetYScreen();
|
|
|
|
for( int i = 10; i <= 20; i += 2 )
|
|
{
|
|
double curr_step = fabs( maxVvis - minVvis ) / (double) i;
|
|
double base = pow( 10, floor( log10( curr_step ) ) );
|
|
double stepInt = floor( curr_step / base ) * base;
|
|
double err = fabs( curr_step - stepInt );
|
|
|
|
if( err< minErr )
|
|
{
|
|
minErr = err;
|
|
bestStep = stepInt;
|
|
}
|
|
}
|
|
|
|
double numberSteps = floor( ( maxVvis - minVvis ) / bestStep );
|
|
|
|
// Half the number of ticks according to window size.
|
|
// For each 32 device context units, is possible to add a new tick.
|
|
while( numberSteps >= m_scrY/32.0 )
|
|
{
|
|
bestStep *= 2;
|
|
numberSteps = floor( ( maxVvis - minVvis ) / bestStep );
|
|
}
|
|
|
|
double v = floor( minVvis / bestStep ) * bestStep;
|
|
double zeroOffset = 100000000.0;
|
|
const int iterLimit = 1000;
|
|
int i = 0;
|
|
|
|
while( v <= maxVvis && i < iterLimit )
|
|
{
|
|
m_tickValues.push_back( v );
|
|
|
|
if( fabs( v ) < zeroOffset )
|
|
zeroOffset = fabs( v );
|
|
|
|
v += bestStep;
|
|
i++;
|
|
}
|
|
|
|
// something weird happened...
|
|
if( i == iterLimit )
|
|
m_tickValues.clear();
|
|
|
|
if( zeroOffset <= bestStep )
|
|
{
|
|
for( double& t : m_tickValues )
|
|
t -= zeroOffset;
|
|
}
|
|
|
|
for( double t : m_tickValues )
|
|
m_tickLabels.emplace_back( t );
|
|
|
|
updateTickLabels( dc, w );
|
|
}
|
|
|
|
|
|
void mpScaleXBase::getVisibleDataRange( mpWindow& w, double& minV, double& maxV )
|
|
{
|
|
wxCoord startPx = w.GetMarginLeft();
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
double pxmin = w.p2x( startPx );
|
|
double pxmax = w.p2x( endPx );
|
|
|
|
minV = TransformFromPlot( pxmin );
|
|
maxV = TransformFromPlot( pxmax );
|
|
}
|
|
|
|
|
|
void mpScaleXLog::recalculateTicks( wxDC& dc, mpWindow& w )
|
|
{
|
|
double minV, maxV, minVvis, maxVvis;
|
|
|
|
GetDataRange( minV, maxV );
|
|
getVisibleDataRange( w, minVvis, maxVvis );
|
|
|
|
// double decades = log( maxV / minV ) / log(10);
|
|
double minDecade = pow( 10, floor( log10( minV ) ) );
|
|
double maxDecade = pow( 10, ceil( log10( maxV ) ) );
|
|
double visibleDecades = log( maxVvis / minVvis ) / log( 10 );
|
|
double step = 10.0;
|
|
int m_scrX = w.GetXScreen();
|
|
|
|
double d;
|
|
|
|
m_tickValues.clear();
|
|
m_tickLabels.clear();
|
|
|
|
if( minDecade == 0.0 )
|
|
return;
|
|
|
|
// Half the number of ticks according to window size.
|
|
// The value 96 is used to have only 4 ticks when m_scrX is 268.
|
|
// For each 96 device context units, is possible to add a new tick.
|
|
while( visibleDecades - 2 >= m_scrX / 96.0 )
|
|
{
|
|
step *= 10.0;
|
|
visibleDecades = log( maxVvis / minVvis ) / log( step );
|
|
}
|
|
|
|
for( d = minDecade; d<=maxDecade; d *= step )
|
|
{
|
|
m_tickLabels.emplace_back( d );
|
|
|
|
for( double dd = d; dd < d * step; dd += d )
|
|
{
|
|
if( visibleDecades < 2 )
|
|
m_tickLabels.emplace_back( dd );
|
|
|
|
m_tickValues.push_back( dd );
|
|
}
|
|
}
|
|
|
|
updateTickLabels( dc, w );
|
|
}
|
|
|
|
|
|
IMPLEMENT_ABSTRACT_CLASS( mpScaleXBase, mpLayer )
|
|
IMPLEMENT_DYNAMIC_CLASS( mpScaleX, mpScaleXBase )
|
|
IMPLEMENT_DYNAMIC_CLASS( mpScaleXLog, mpScaleXBase )
|
|
|
|
mpScaleXBase::mpScaleXBase( const wxString& name, int flags, bool ticks, unsigned int type )
|
|
{
|
|
SetName( name );
|
|
SetFont( (wxFont&) *wxSMALL_FONT );
|
|
SetPen( (wxPen&) *wxGREY_PEN );
|
|
m_flags = flags;
|
|
m_ticks = ticks;
|
|
m_type = mpLAYER_AXIS;
|
|
}
|
|
|
|
|
|
mpScaleX::mpScaleX( const wxString& name, int flags, bool ticks, unsigned int type ) :
|
|
mpScaleXBase( name, flags, ticks, type )
|
|
{
|
|
}
|
|
|
|
|
|
mpScaleXLog::mpScaleXLog( const wxString& name, int flags, bool ticks, unsigned int type ) :
|
|
mpScaleXBase( name, flags, ticks, type )
|
|
{
|
|
}
|
|
|
|
|
|
void mpScaleXBase::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
int tx, ty;
|
|
|
|
m_offset = -m_minV;
|
|
m_scale = 1.0 / ( m_maxV - m_minV );
|
|
|
|
recalculateTicks( dc, w );
|
|
|
|
if( m_visible )
|
|
{
|
|
dc.SetPen( m_pen );
|
|
dc.SetFont( m_font );
|
|
int orgy = 0;
|
|
|
|
const int extend = w.GetScrX(); ///2;
|
|
|
|
if( m_flags == mpALIGN_CENTER )
|
|
orgy = w.y2p( 0 );
|
|
|
|
if( m_flags == mpALIGN_TOP )
|
|
orgy = w.GetMarginTop();
|
|
|
|
if( m_flags == mpALIGN_BOTTOM )
|
|
orgy = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
if( m_flags == mpALIGN_BORDER_BOTTOM )
|
|
orgy = w.GetScrY() - 1;
|
|
|
|
if( m_flags == mpALIGN_BORDER_TOP )
|
|
orgy = 1;
|
|
|
|
wxCoord startPx = w.GetMarginLeft();
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
|
|
// int tmp=-65535;
|
|
int labelH = m_maxLabelHeight; // Control labels height to decide where to put axis name (below labels or on top of axis)
|
|
|
|
// int maxExtent = tc.MaxLabelWidth();
|
|
for( double tp : m_tickValues )
|
|
{
|
|
double px = TransformToPlot( tp );
|
|
const int p = (int) ( ( px - w.GetPosX() ) * w.GetScaleX() );
|
|
|
|
if( p >= startPx && p <= endPx )
|
|
{
|
|
if( m_ticks ) // draw axis ticks
|
|
{
|
|
if( m_flags == mpALIGN_BORDER_BOTTOM )
|
|
dc.DrawLine( p, orgy, p, orgy - 4 );
|
|
else
|
|
dc.DrawLine( p, orgy, p, orgy + 4 );
|
|
}
|
|
else // draw grid dotted lines
|
|
{
|
|
m_pen.SetStyle( wxPENSTYLE_DOT );
|
|
dc.SetPen( m_pen );
|
|
|
|
if( m_flags == mpALIGN_BOTTOM )
|
|
{
|
|
m_pen.SetStyle( wxPENSTYLE_DOT );
|
|
dc.SetPen( m_pen );
|
|
dc.DrawLine( p, orgy + 4, p, minYpx );
|
|
m_pen.SetStyle( wxPENSTYLE_SOLID );
|
|
dc.SetPen( m_pen );
|
|
dc.DrawLine( p, orgy + 4, p, orgy - 4 );
|
|
}
|
|
else
|
|
{
|
|
if( m_flags == mpALIGN_TOP )
|
|
dc.DrawLine( p, orgy - 4, p, maxYpx );
|
|
else
|
|
dc.DrawLine( p, minYpx, p, maxYpx );
|
|
}
|
|
|
|
m_pen.SetStyle( wxPENSTYLE_SOLID );
|
|
dc.SetPen( m_pen );
|
|
}
|
|
}
|
|
}
|
|
|
|
m_pen.SetStyle( wxPENSTYLE_SOLID );
|
|
dc.SetPen( m_pen );
|
|
dc.DrawLine( startPx, minYpx, endPx, minYpx );
|
|
dc.DrawLine( startPx, maxYpx, endPx, maxYpx );
|
|
|
|
// Actually draw labels, taking care of not overlapping them, and distributing them
|
|
// regularly
|
|
for( const TICK_LABEL& tickLabel : m_tickLabels )
|
|
{
|
|
if( !tickLabel.visible )
|
|
continue;
|
|
|
|
double px = TransformToPlot( tickLabel.pos );
|
|
const int p = (int) ( ( px - w.GetPosX() ) * w.GetScaleX() );
|
|
|
|
if( (p >= startPx) && (p <= endPx) )
|
|
{
|
|
// Write ticks labels in s string
|
|
wxString s = tickLabel.label;
|
|
|
|
dc.GetTextExtent( s, &tx, &ty );
|
|
|
|
if( (m_flags == mpALIGN_BORDER_BOTTOM) || (m_flags == mpALIGN_TOP) )
|
|
dc.DrawText( s, p - tx / 2, orgy - 4 - ty );
|
|
else
|
|
dc.DrawText( s, p - tx / 2, orgy + 4 );
|
|
}
|
|
}
|
|
|
|
// Draw axis name
|
|
dc.GetTextExtent( m_name, &tx, &ty );
|
|
|
|
switch( m_nameFlags )
|
|
{
|
|
case mpALIGN_BORDER_BOTTOM:
|
|
dc.DrawText( m_name, extend - tx - 4, orgy - 8 - ty - labelH );
|
|
break;
|
|
|
|
case mpALIGN_BOTTOM:
|
|
dc.DrawText( m_name, (endPx + startPx) / 2 - tx / 2, orgy + 6 + labelH );
|
|
break;
|
|
|
|
case mpALIGN_CENTER:
|
|
dc.DrawText( m_name, extend - tx - 4, orgy - 4 - ty );
|
|
break;
|
|
|
|
case mpALIGN_TOP:
|
|
if( w.GetMarginTop() > (ty + labelH + 8) )
|
|
dc.DrawText( m_name, (endPx - startPx - tx) >> 1, orgy - 6 - ty - labelH );
|
|
else
|
|
dc.DrawText( m_name, extend - tx - 4, orgy + 4 );
|
|
|
|
break;
|
|
|
|
case mpALIGN_BORDER_TOP:
|
|
dc.DrawText( m_name, extend - tx - 4, orgy + 6 + labelH );
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
IMPLEMENT_DYNAMIC_CLASS( mpScaleY, mpLayer )
|
|
|
|
mpScaleY::mpScaleY( const wxString& name, int flags, bool ticks )
|
|
{
|
|
SetName( name );
|
|
SetFont( (wxFont&) *wxSMALL_FONT );
|
|
SetPen( (wxPen&) *wxGREY_PEN );
|
|
m_flags = flags;
|
|
m_ticks = ticks;
|
|
m_type = mpLAYER_AXIS;
|
|
m_masterScale = nullptr;
|
|
m_nameFlags = mpALIGN_BORDER_LEFT;
|
|
}
|
|
|
|
|
|
void mpScaleY::Plot( wxDC& dc, mpWindow& w )
|
|
{
|
|
m_offset = -m_minV;
|
|
m_scale = 1.0 / ( m_maxV - m_minV );
|
|
|
|
recalculateTicks( dc, w );
|
|
|
|
if( m_visible )
|
|
{
|
|
dc.SetPen( m_pen );
|
|
dc.SetFont( m_font );
|
|
|
|
int orgx = 0;
|
|
|
|
if( m_flags == mpALIGN_CENTER )
|
|
orgx = w.x2p( 0 );
|
|
|
|
if( m_flags == mpALIGN_LEFT )
|
|
orgx = w.GetMarginLeft();
|
|
|
|
if( m_flags == mpALIGN_RIGHT )
|
|
orgx = w.GetScrX() - w.GetMarginRight();
|
|
|
|
if( m_flags == mpALIGN_FAR_RIGHT )
|
|
orgx = w.GetScrX() - ( w.GetMarginRight() / 2 );
|
|
|
|
if( m_flags == mpALIGN_BORDER_RIGHT )
|
|
orgx = w.GetScrX() - 1;
|
|
|
|
if( m_flags == mpALIGN_BORDER_LEFT )
|
|
orgx = 1;
|
|
|
|
wxCoord endPx = w.GetScrX() - w.GetMarginRight();
|
|
wxCoord minYpx = w.GetMarginTop();
|
|
wxCoord maxYpx = w.GetScrY() - w.GetMarginBottom();
|
|
// Draw line
|
|
dc.DrawLine( orgx, minYpx, orgx, maxYpx );
|
|
|
|
wxCoord tx, ty;
|
|
wxString s;
|
|
wxString fmt;
|
|
|
|
int labelW = 0;
|
|
// Before staring cycle, calculate label height
|
|
int labelHeight = 0;
|
|
s.Printf( fmt, 0 );
|
|
dc.GetTextExtent( s, &tx, &labelHeight );
|
|
|
|
for( double tp : m_tickValues )
|
|
{
|
|
double py = TransformToPlot( tp );
|
|
const int p = (int) ( ( w.GetPosY() - py ) * w.GetScaleY() );
|
|
|
|
if( p >= minYpx && p <= maxYpx )
|
|
{
|
|
if( m_ticks ) // Draw axis ticks
|
|
{
|
|
if( m_flags == mpALIGN_BORDER_LEFT )
|
|
dc.DrawLine( orgx, p, orgx + 4, p );
|
|
else
|
|
dc.DrawLine( orgx - 4, p, orgx, p );
|
|
}
|
|
else
|
|
{
|
|
dc.DrawLine( orgx - 4, p, orgx + 4, p );
|
|
|
|
m_pen.SetStyle( wxPENSTYLE_DOT );
|
|
dc.SetPen( m_pen );
|
|
|
|
dc.DrawLine( orgx - 4, p, endPx, p );
|
|
|
|
m_pen.SetStyle( wxPENSTYLE_SOLID );
|
|
dc.SetPen( m_pen );
|
|
}
|
|
|
|
// Print ticks labels
|
|
}
|
|
}
|
|
|
|
for( const TICK_LABEL& tickLabel : m_tickLabels )
|
|
{
|
|
double py = TransformToPlot( tickLabel.pos );
|
|
const int p = (int) ( ( w.GetPosY() - py ) * w.GetScaleY() );
|
|
|
|
if( !tickLabel.visible )
|
|
continue;
|
|
|
|
if( p >= minYpx && p <= maxYpx )
|
|
{
|
|
s = tickLabel.label;
|
|
dc.GetTextExtent( s, &tx, &ty );
|
|
|
|
if( m_flags == mpALIGN_BORDER_LEFT || m_flags == mpALIGN_RIGHT || m_flags == mpALIGN_FAR_RIGHT )
|
|
dc.DrawText( s, orgx + 4, p - ty / 2 );
|
|
else
|
|
dc.DrawText( s, orgx - 4 - tx, p - ty / 2 ); // ( s, orgx+4, p-ty/2);
|
|
}
|
|
}
|
|
|
|
// Draw axis name
|
|
|
|
dc.GetTextExtent( m_name, &tx, &ty );
|
|
|
|
switch( m_nameFlags )
|
|
{
|
|
case mpALIGN_BORDER_LEFT:
|
|
dc.DrawText( m_name, labelW + 8, 4 );
|
|
break;
|
|
|
|
case mpALIGN_LEFT:
|
|
dc.DrawText( m_name, orgx - ( tx / 2 ), minYpx - ty - 4 );
|
|
break;
|
|
|
|
case mpALIGN_CENTER:
|
|
dc.DrawText( m_name, orgx + 4, 4 );
|
|
break;
|
|
|
|
case mpALIGN_RIGHT:
|
|
case mpALIGN_FAR_RIGHT:
|
|
dc.DrawText( m_name, orgx - ( tx / 2 ), minYpx - ty - 4 );
|
|
break;
|
|
|
|
case mpALIGN_BORDER_RIGHT:
|
|
dc.DrawText( m_name, orgx - 6 - tx - labelW, 4 );
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpWindow
|
|
// -----------------------------------------------------------------------------
|
|
|
|
IMPLEMENT_DYNAMIC_CLASS( mpWindow, wxWindow )
|
|
|
|
BEGIN_EVENT_TABLE( mpWindow, wxWindow )
|
|
EVT_PAINT( mpWindow::OnPaint )
|
|
EVT_SIZE( mpWindow::OnSize )
|
|
|
|
EVT_MIDDLE_DOWN( mpWindow::OnMouseMiddleDown ) // JLB
|
|
EVT_RIGHT_UP( mpWindow::OnShowPopupMenu )
|
|
EVT_MOUSEWHEEL( mpWindow::onMouseWheel ) // JLB
|
|
EVT_MAGNIFY( mpWindow::onMagnify )
|
|
EVT_MOTION( mpWindow::onMouseMove ) // JLB
|
|
EVT_LEFT_DOWN( mpWindow::onMouseLeftDown )
|
|
EVT_LEFT_UP( mpWindow::onMouseLeftRelease )
|
|
|
|
EVT_MENU( mpID_CENTER, mpWindow::OnCenter )
|
|
EVT_MENU( mpID_FIT, mpWindow::OnFit )
|
|
EVT_MENU( mpID_ZOOM_IN, mpWindow::onZoomIn )
|
|
EVT_MENU( mpID_ZOOM_OUT, mpWindow::onZoomOut )
|
|
EVT_MENU( mpID_ZOOM_UNDO, mpWindow::onZoomUndo )
|
|
EVT_MENU( mpID_ZOOM_REDO, mpWindow::onZoomRedo )
|
|
END_EVENT_TABLE()
|
|
|
|
mpWindow::mpWindow() :
|
|
wxWindow(),
|
|
m_minX( 0.0 ),
|
|
m_maxX( 0.0 ),
|
|
m_minY( 0.0 ),
|
|
m_maxY( 0.0 ),
|
|
m_scaleX( 1.0 ),
|
|
m_scaleY( 1.0 ),
|
|
m_posX( 0.0 ),
|
|
m_posY( 0.0 ),
|
|
m_scrX( 64 ),
|
|
m_scrY( 64 ),
|
|
m_clickedX( 0 ),
|
|
m_clickedY( 0 ),
|
|
m_yLocked( false ),
|
|
m_desiredXmin( 0.0 ),
|
|
m_desiredXmax( 1.0 ),
|
|
m_desiredYmin( 0.0 ),
|
|
m_desiredYmax( 1.0 ),
|
|
m_marginTop( 0 ),
|
|
m_marginRight( 0 ),
|
|
m_marginBottom( 0 ),
|
|
m_marginLeft( 0 ),
|
|
m_last_lx( 0 ),
|
|
m_last_ly( 0 ),
|
|
m_buff_bmp( nullptr ),
|
|
m_enableDoubleBuffer( false ),
|
|
m_enableMouseNavigation( true ),
|
|
m_enableMouseWheelPan( false ),
|
|
m_enableLimitedView( false ),
|
|
m_movingInfoLayer( nullptr ),
|
|
m_zooming( false )
|
|
{
|
|
if( wxGraphicsContext *ctx = m_buff_dc.GetGraphicsContext() )
|
|
{
|
|
if( !ctx->SetInterpolationQuality( wxINTERPOLATION_BEST )
|
|
|| !ctx->SetInterpolationQuality( wxINTERPOLATION_GOOD ) )
|
|
{
|
|
ctx->SetInterpolationQuality( wxINTERPOLATION_FAST );
|
|
}
|
|
|
|
ctx->SetAntialiasMode( wxANTIALIAS_DEFAULT );
|
|
}
|
|
}
|
|
|
|
mpWindow::mpWindow( wxWindow* parent, wxWindowID id ) :
|
|
wxWindow( parent, id, wxDefaultPosition, wxDefaultSize, 0, wxT( "mathplot" ) ),
|
|
m_minX( 0.0 ),
|
|
m_maxX( 0.0 ),
|
|
m_minY( 0.0 ),
|
|
m_maxY( 0.0 ),
|
|
m_scaleX( 1.0 ),
|
|
m_scaleY( 1.0 ),
|
|
m_posX( 0.0 ),
|
|
m_posY( 0.0 ),
|
|
m_scrX( 64 ),
|
|
m_scrY( 64 ),
|
|
m_clickedX( 0 ),
|
|
m_clickedY( 0 ),
|
|
m_yLocked( false ),
|
|
m_desiredXmin( 0.0 ),
|
|
m_desiredXmax( 1.0 ),
|
|
m_desiredYmin( 0.0 ),
|
|
m_desiredYmax( 1.0 ),
|
|
m_marginTop( 0 ),
|
|
m_marginRight( 0 ),
|
|
m_marginBottom( 0 ),
|
|
m_marginLeft( 0 ),
|
|
m_last_lx( 0 ),
|
|
m_last_ly( 0 ),
|
|
m_buff_bmp( nullptr ),
|
|
m_enableDoubleBuffer( false ),
|
|
m_enableMouseNavigation( true ),
|
|
m_enableMouseWheelPan( false ),
|
|
m_enableLimitedView( false ),
|
|
m_movingInfoLayer( nullptr ),
|
|
m_zooming( false )
|
|
{
|
|
m_popmenu.Append( mpID_ZOOM_UNDO, _( "Undo Last Zoom" ), _( "Return zoom to level prior to last zoom action" ) );
|
|
m_popmenu.Append( mpID_ZOOM_REDO, _( "Redo Last Zoom" ), _( "Return zoom to level prior to last zoom undo" ) );
|
|
m_popmenu.AppendSeparator();
|
|
m_popmenu.Append( mpID_ZOOM_IN, _( "Zoom In" ), _( "Zoom in plot view." ) );
|
|
m_popmenu.Append( mpID_ZOOM_OUT, _( "Zoom Out" ), _( "Zoom out plot view." ) );
|
|
m_popmenu.Append( mpID_CENTER, _( "Center on Cursor" ), _( "Center plot view to this position" ) );
|
|
m_popmenu.Append( mpID_FIT, _( "Fit on Screen" ), _( "Set plot view to show all items" ) );
|
|
|
|
m_layers.clear();
|
|
SetBackgroundColour( *wxWHITE );
|
|
m_bgColour = *wxWHITE;
|
|
m_fgColour = *wxBLACK;
|
|
|
|
SetSizeHints( 128, 128 );
|
|
|
|
// J.L.Blanco: Eliminates the "flick" with the double buffer.
|
|
SetBackgroundStyle( wxBG_STYLE_CUSTOM );
|
|
|
|
if( wxGraphicsContext* ctx = m_buff_dc.GetGraphicsContext() )
|
|
{
|
|
if( !ctx->SetInterpolationQuality( wxINTERPOLATION_BEST )
|
|
|| !ctx->SetInterpolationQuality( wxINTERPOLATION_GOOD ) )
|
|
{
|
|
ctx->SetInterpolationQuality( wxINTERPOLATION_FAST );
|
|
}
|
|
|
|
ctx->SetAntialiasMode( wxANTIALIAS_DEFAULT );
|
|
}
|
|
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
mpWindow::~mpWindow()
|
|
{
|
|
// Free all the layers:
|
|
DelAllLayers( true, false );
|
|
|
|
delete m_buff_bmp;
|
|
m_buff_bmp = nullptr;
|
|
}
|
|
|
|
|
|
// Mouse handler, for detecting when the user drag with the right button or just "clicks" for the menu
|
|
// JLB
|
|
void mpWindow::OnMouseMiddleDown( wxMouseEvent& event )
|
|
{
|
|
m_mouseMClick.x = event.GetX();
|
|
m_mouseMClick.y = event.GetY();
|
|
}
|
|
|
|
|
|
void mpWindow::onMagnify( wxMouseEvent& event )
|
|
{
|
|
if( !m_enableMouseNavigation )
|
|
{
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
float zoom = event.GetMagnification() + 1.0f;
|
|
wxPoint pos( event.GetX(), event.GetY() );
|
|
|
|
if( zoom > 1.0f )
|
|
ZoomIn( pos, zoom );
|
|
else if( zoom < 1.0f )
|
|
ZoomOut( pos, 1.0f / zoom );
|
|
}
|
|
|
|
|
|
// Process mouse wheel events
|
|
// JLB
|
|
void mpWindow::onMouseWheel( wxMouseEvent& event )
|
|
{
|
|
if( !m_enableMouseNavigation )
|
|
{
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
int change = event.GetWheelRotation();
|
|
const int axis = event.GetWheelAxis();
|
|
double changeUnitsX = change / m_scaleX;
|
|
double changeUnitsY = change / m_scaleY;
|
|
|
|
if( ( !m_enableMouseWheelPan && ( event.ControlDown() || event.ShiftDown() ) )
|
|
|| ( m_enableMouseWheelPan && !event.ControlDown() ) )
|
|
{
|
|
// Scrolling
|
|
if( m_enableMouseWheelPan )
|
|
{
|
|
if( axis == wxMOUSE_WHEEL_HORIZONTAL || event.ShiftDown() )
|
|
{
|
|
SetXView( m_posX + changeUnitsX, m_desiredXmax + changeUnitsX,
|
|
m_desiredXmin + changeUnitsX );
|
|
}
|
|
else if( !m_yLocked )
|
|
{
|
|
SetYView( m_posY + changeUnitsY, m_desiredYmax + changeUnitsY,
|
|
m_desiredYmin + changeUnitsY );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( event.ControlDown() )
|
|
{
|
|
SetXView( m_posX + changeUnitsX, m_desiredXmax + changeUnitsX,
|
|
m_desiredXmin + changeUnitsX );
|
|
}
|
|
else if( !m_yLocked )
|
|
{
|
|
SetYView( m_posY + changeUnitsY, m_desiredYmax + changeUnitsY,
|
|
m_desiredYmin + changeUnitsY );
|
|
}
|
|
}
|
|
|
|
UpdateAll();
|
|
}
|
|
else
|
|
{
|
|
// zoom in/out
|
|
wxPoint clickPt( event.GetX(), event.GetY() );
|
|
|
|
if( event.GetWheelRotation() > 0 )
|
|
ZoomIn( clickPt );
|
|
else
|
|
ZoomOut( clickPt );
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
// If the user "drags" with the right button pressed, do "pan"
|
|
// JLB
|
|
void mpWindow::onMouseMove( wxMouseEvent& event )
|
|
{
|
|
if( !m_enableMouseNavigation )
|
|
{
|
|
event.Skip();
|
|
return;
|
|
}
|
|
|
|
wxCursor cursor = wxCURSOR_MAGNIFIER;
|
|
|
|
if( event.m_middleDown )
|
|
{
|
|
cursor = wxCURSOR_ARROW;
|
|
|
|
// The change:
|
|
int Ax = m_mouseMClick.x - event.GetX();
|
|
int Ay = m_mouseMClick.y - event.GetY();
|
|
|
|
// For the next event, use relative to this coordinates.
|
|
m_mouseMClick.x = event.GetX();
|
|
m_mouseMClick.y = event.GetY();
|
|
|
|
if( Ax )
|
|
{
|
|
double Ax_units = Ax / m_scaleX;
|
|
SetXView( m_posX + Ax_units, m_desiredXmax + Ax_units, m_desiredXmin + Ax_units );
|
|
}
|
|
|
|
if( Ay )
|
|
{
|
|
double Ay_units = -Ay / m_scaleY;
|
|
SetYView( m_posY + Ay_units, m_desiredYmax + Ay_units, m_desiredYmin + Ay_units );
|
|
}
|
|
|
|
if( Ax || Ay )
|
|
UpdateAll();
|
|
}
|
|
else if( event.m_leftDown )
|
|
{
|
|
if( m_movingInfoLayer )
|
|
{
|
|
if( dynamic_cast<mpInfoLegend*>( m_movingInfoLayer ) )
|
|
cursor = wxCURSOR_SIZING;
|
|
else
|
|
cursor = wxCURSOR_SIZEWE;
|
|
|
|
wxPoint moveVector( event.GetX() - m_mouseLClick.x, event.GetY() - m_mouseLClick.y );
|
|
m_movingInfoLayer->Move( moveVector );
|
|
m_zooming = false;
|
|
}
|
|
else
|
|
{
|
|
cursor = wxCURSOR_MAGNIFIER;
|
|
|
|
wxClientDC dc( this );
|
|
wxPen pen( m_fgColour, 1, wxPENSTYLE_DOT );
|
|
dc.SetPen( pen );
|
|
dc.SetBrush( *wxTRANSPARENT_BRUSH );
|
|
dc.DrawRectangle( m_mouseLClick.x, m_mouseLClick.y,
|
|
event.GetX() - m_mouseLClick.x, event.GetY() - m_mouseLClick.y );
|
|
m_zooming = true;
|
|
m_zoomRect.x = m_mouseLClick.x;
|
|
m_zoomRect.y = m_mouseLClick.y;
|
|
m_zoomRect.width = event.GetX() - m_mouseLClick.x;
|
|
m_zoomRect.height = event.GetY() - m_mouseLClick.y;
|
|
}
|
|
|
|
UpdateAll();
|
|
}
|
|
else
|
|
{
|
|
for( mpLayer* layer : m_layers)
|
|
{
|
|
if( layer->IsInfo() && layer->IsVisible() )
|
|
{
|
|
mpInfoLayer* infoLayer = (mpInfoLayer*) layer;
|
|
|
|
if( infoLayer->Inside( event.GetPosition() ) )
|
|
{
|
|
if( dynamic_cast<mpInfoLegend*>( infoLayer ) )
|
|
cursor = wxCURSOR_SIZING;
|
|
else
|
|
cursor = wxCURSOR_SIZEWE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SetCursor( cursor );
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void mpWindow::onMouseLeftDown( wxMouseEvent& event )
|
|
{
|
|
m_mouseLClick.x = event.GetX();
|
|
m_mouseLClick.y = event.GetY();
|
|
m_zooming = true;
|
|
wxPoint pointClicked = event.GetPosition();
|
|
m_movingInfoLayer = IsInsideInfoLayer( pointClicked );
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void mpWindow::onMouseLeftRelease( wxMouseEvent& event )
|
|
{
|
|
wxPoint release( event.GetX(), event.GetY() );
|
|
wxPoint press( m_mouseLClick.x, m_mouseLClick.y );
|
|
|
|
m_zooming = false;
|
|
|
|
if( m_movingInfoLayer != nullptr )
|
|
{
|
|
m_movingInfoLayer->UpdateReference();
|
|
m_movingInfoLayer = nullptr;
|
|
}
|
|
else
|
|
{
|
|
if( release != press )
|
|
ZoomRect( press, release );
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void mpWindow::Fit()
|
|
{
|
|
if( UpdateBBox() )
|
|
Fit( m_minX, m_maxX, m_minY, m_maxY );
|
|
}
|
|
|
|
|
|
// JL
|
|
void mpWindow::Fit( double xMin, double xMax, double yMin, double yMax,
|
|
const wxCoord* printSizeX, const wxCoord* printSizeY )
|
|
{
|
|
// Save desired borders:
|
|
m_desiredXmin = xMin; m_desiredXmax = xMax;
|
|
m_desiredYmin = yMin; m_desiredYmax = yMax;
|
|
|
|
// Give a small margin to plot area
|
|
double xExtra = fabs( xMax - xMin ) * 0.00;
|
|
double yExtra = fabs( yMax - yMin ) * 0.03;
|
|
|
|
xMin -= xExtra;
|
|
xMax += xExtra;
|
|
yMin -= yExtra;
|
|
yMax += yExtra;
|
|
|
|
if( printSizeX != nullptr && printSizeY != nullptr )
|
|
{
|
|
// Printer:
|
|
m_scrX = *printSizeX;
|
|
m_scrY = *printSizeY;
|
|
}
|
|
else
|
|
{
|
|
// Normal case (screen):
|
|
GetClientSize( &m_scrX, &m_scrY );
|
|
}
|
|
|
|
double Ax = xMax - xMin;
|
|
double Ay = yMax - yMin;
|
|
|
|
m_scaleX = (Ax != 0) ? (m_scrX - m_marginLeft - m_marginRight) / Ax : 1;
|
|
m_scaleY = (Ay != 0) ? (m_scrY - m_marginTop - m_marginBottom) / Ay : 1;
|
|
|
|
// Adjusts corner coordinates: This should be simply:
|
|
// m_posX = m_minX;
|
|
// m_posY = m_maxY;
|
|
// But account for centering if we have lock aspect:
|
|
m_posX = (xMin + xMax) / 2 - ( (m_scrX - m_marginLeft - m_marginRight) / 2 + m_marginLeft ) /
|
|
m_scaleX;
|
|
m_posY = (yMin + yMax) / 2 + ( (m_scrY - m_marginTop - m_marginBottom) / 2 + m_marginTop ) /
|
|
m_scaleY;
|
|
|
|
// It is VERY IMPORTANT to DO NOT call Refresh if we are drawing to the printer!!
|
|
// Otherwise, the DC dimensions will be those of the window instead of the printer device
|
|
if( printSizeX == nullptr || printSizeY == nullptr )
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
void mpWindow::AdjustLimitedView()
|
|
{
|
|
if( !m_enableLimitedView )
|
|
return;
|
|
|
|
// m_min and m_max are plot limits for curves
|
|
// xMin, xMax, yMin, yMax are the full limits (plot limit + margin)
|
|
const double xMin = m_minX - m_marginLeft / m_scaleX;
|
|
const double xMax = m_maxX + m_marginRight / m_scaleX;
|
|
const double yMin = m_minY - m_marginBottom / m_scaleY;
|
|
const double yMax = m_maxY + m_marginTop / m_scaleY;
|
|
|
|
if( m_desiredXmin < xMin )
|
|
{
|
|
double diff = xMin - m_desiredXmin;
|
|
m_posX += diff;
|
|
m_desiredXmax += diff;
|
|
m_desiredXmin = xMin;
|
|
}
|
|
|
|
if( m_desiredXmax > xMax )
|
|
{
|
|
double diff = m_desiredXmax - xMax;
|
|
m_posX -= diff;
|
|
m_desiredXmin -= diff;
|
|
m_desiredXmax = xMax;
|
|
}
|
|
|
|
if( m_desiredYmin < yMin )
|
|
{
|
|
double diff = yMin - m_desiredYmin;
|
|
m_posY += diff;
|
|
m_desiredYmax += diff;
|
|
m_desiredYmin = yMin;
|
|
}
|
|
|
|
if( m_desiredYmax > yMax )
|
|
{
|
|
double diff = m_desiredYmax - yMax;
|
|
m_posY -= diff;
|
|
m_desiredYmin -= diff;
|
|
m_desiredYmax = yMax;
|
|
}
|
|
}
|
|
|
|
|
|
bool mpWindow::SetXView( double pos, double desiredMax, double desiredMin )
|
|
{
|
|
m_posX = pos;
|
|
m_desiredXmax = desiredMax;
|
|
m_desiredXmin = desiredMin;
|
|
AdjustLimitedView();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool mpWindow::SetYView( double pos, double desiredMax, double desiredMin )
|
|
{
|
|
m_posY = pos;
|
|
m_desiredYmax = desiredMax;
|
|
m_desiredYmin = desiredMin;
|
|
AdjustLimitedView();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomIn( const wxPoint& centerPoint )
|
|
{
|
|
ZoomIn( centerPoint, zoomIncrementalFactor );
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomIn( const wxPoint& centerPoint, double zoomFactor )
|
|
{
|
|
pushZoomUndo( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
wxPoint c( centerPoint );
|
|
|
|
if( c == wxDefaultPosition )
|
|
{
|
|
GetClientSize( &m_scrX, &m_scrY );
|
|
c.x = (m_scrX - m_marginLeft - m_marginRight) / 2 + m_marginLeft; // c.x = m_scrX/2;
|
|
c.y = (m_scrY - m_marginTop - m_marginBottom) / 2 - m_marginTop; // c.y = m_scrY/2;
|
|
}
|
|
else
|
|
{
|
|
c.x = std::max( c.x, m_marginLeft );
|
|
c.x = std::min( c.x, m_scrX - m_marginRight );
|
|
c.y = std::max( c.y, m_marginTop );
|
|
c.y = std::min( c.y, m_scrY - m_marginBottom );
|
|
}
|
|
|
|
// Preserve the position of the clicked point:
|
|
double prior_layer_x = p2x( c.x );
|
|
double prior_layer_y = p2y( c.y );
|
|
|
|
// Zoom in:
|
|
const double MAX_SCALE = 1e6;
|
|
double newScaleX = m_scaleX * zoomFactor;
|
|
double newScaleY = m_scaleY * zoomFactor;
|
|
|
|
// Baaaaad things happen when you zoom in too much..
|
|
if( newScaleX <= MAX_SCALE && newScaleY <= MAX_SCALE )
|
|
{
|
|
m_scaleX = newScaleX;
|
|
|
|
if( !m_yLocked )
|
|
m_scaleY = newScaleY;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Adjust the new m_posx/y:
|
|
m_posX = prior_layer_x - c.x / m_scaleX;
|
|
|
|
if( !m_yLocked )
|
|
m_posY = prior_layer_y + c.y / m_scaleY;
|
|
|
|
m_desiredXmin = m_posX;
|
|
m_desiredXmax = m_posX + (m_scrX - m_marginLeft - m_marginRight) / m_scaleX;
|
|
m_desiredYmax = m_posY;
|
|
m_desiredYmin = m_posY - (m_scrY - m_marginTop - m_marginBottom) / m_scaleY;
|
|
AdjustLimitedView();
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomOut( const wxPoint& centerPoint )
|
|
{
|
|
ZoomOut( centerPoint, zoomIncrementalFactor );
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomOut( const wxPoint& centerPoint, double zoomFactor )
|
|
{
|
|
pushZoomUndo( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
wxPoint c( centerPoint );
|
|
|
|
if( c == wxDefaultPosition )
|
|
{
|
|
GetClientSize( &m_scrX, &m_scrY );
|
|
c.x = (m_scrX - m_marginLeft - m_marginRight) / 2 + m_marginLeft;
|
|
c.y = (m_scrY - m_marginTop - m_marginBottom) / 2 - m_marginTop;
|
|
}
|
|
|
|
// Preserve the position of the clicked point:
|
|
double prior_layer_x = p2x( c.x );
|
|
double prior_layer_y = p2y( c.y );
|
|
|
|
// Zoom out:
|
|
m_scaleX = m_scaleX / zoomFactor;
|
|
|
|
if( !m_yLocked )
|
|
m_scaleY = m_scaleY / zoomFactor;
|
|
|
|
// Adjust the new m_posx/y:
|
|
m_posX = prior_layer_x - c.x / m_scaleX;
|
|
|
|
if( !m_yLocked )
|
|
m_posY = prior_layer_y + c.y / m_scaleY;
|
|
|
|
m_desiredXmin = m_posX;
|
|
m_desiredXmax = m_posX + (m_scrX - m_marginLeft - m_marginRight) / m_scaleX;
|
|
m_desiredYmax = m_posY;
|
|
m_desiredYmin = m_posY - (m_scrY - m_marginTop - m_marginBottom) / m_scaleY;
|
|
|
|
AdjustLimitedView();
|
|
|
|
if( !CheckXLimits( m_desiredXmax, m_desiredXmin )
|
|
|| !CheckYLimits( m_desiredYmax, m_desiredYmin ) )
|
|
{
|
|
Fit();
|
|
}
|
|
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomRect( wxPoint p0, wxPoint p1 )
|
|
{
|
|
pushZoomUndo( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
// Compute the 2 corners in graph coordinates:
|
|
double p0x = p2x( p0.x );
|
|
double p0y = p2y( p0.y );
|
|
double p1x = p2x( p1.x );
|
|
double p1y = p2y( p1.y );
|
|
|
|
// Order them:
|
|
double zoom_x_min = p0x<p1x ? p0x : p1x;
|
|
double zoom_x_max = p0x>p1x ? p0x : p1x;
|
|
double zoom_y_min = p0y<p1y ? p0y : p1y;
|
|
double zoom_y_max = p0y>p1y ? p0y : p1y;
|
|
|
|
if( m_yLocked )
|
|
{
|
|
zoom_y_min = m_desiredYmin;
|
|
zoom_y_max = m_desiredYmax;
|
|
}
|
|
|
|
Fit( zoom_x_min, zoom_x_max, zoom_y_min, zoom_y_max );
|
|
AdjustLimitedView();
|
|
}
|
|
|
|
|
|
void mpWindow::pushZoomUndo( const std::array<double, 4>& aZoom )
|
|
{
|
|
m_undoZoomStack.push( aZoom );
|
|
|
|
while( !m_redoZoomStack.empty() )
|
|
m_redoZoomStack.pop();
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomUndo()
|
|
{
|
|
if( m_undoZoomStack.size() )
|
|
{
|
|
m_redoZoomStack.push( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
std::array<double, 4> zoom = m_undoZoomStack.top();
|
|
m_undoZoomStack.pop();
|
|
|
|
Fit( zoom[0], zoom[1], zoom[2], zoom[3] );
|
|
AdjustLimitedView();
|
|
}
|
|
}
|
|
|
|
|
|
void mpWindow::ZoomRedo()
|
|
{
|
|
if( m_redoZoomStack.size() )
|
|
{
|
|
m_undoZoomStack.push( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
std::array<double, 4> zoom = m_redoZoomStack.top();
|
|
m_redoZoomStack.pop();
|
|
|
|
Fit( zoom[0], zoom[1], zoom[2], zoom[3] );
|
|
AdjustLimitedView();
|
|
}
|
|
}
|
|
|
|
|
|
void mpWindow::OnShowPopupMenu( wxMouseEvent& event )
|
|
{
|
|
m_clickedX = event.GetX();
|
|
m_clickedY = event.GetY();
|
|
|
|
m_popmenu.Enable( mpID_ZOOM_UNDO, !m_undoZoomStack.empty() );
|
|
m_popmenu.Enable( mpID_ZOOM_REDO, !m_redoZoomStack.empty() );
|
|
|
|
PopupMenu( &m_popmenu, event.GetX(), event.GetY() );
|
|
}
|
|
|
|
|
|
void mpWindow::OnFit( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
pushZoomUndo( { m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax } );
|
|
|
|
Fit();
|
|
}
|
|
|
|
|
|
void mpWindow::OnCenter( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
GetClientSize( &m_scrX, &m_scrY );
|
|
int centerX = (m_scrX - m_marginLeft - m_marginRight) / 2;
|
|
int centerY = (m_scrY - m_marginTop - m_marginBottom) / 2;
|
|
SetPos( p2x( m_clickedX - centerX ), p2y( m_clickedY - centerY ) );
|
|
}
|
|
|
|
|
|
void mpWindow::onZoomIn( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
ZoomIn( wxPoint( m_mouseMClick.x, m_mouseMClick.y ) );
|
|
}
|
|
|
|
|
|
void mpWindow::onZoomOut( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
ZoomOut();
|
|
}
|
|
|
|
|
|
void mpWindow::onZoomUndo( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
ZoomUndo();
|
|
}
|
|
|
|
|
|
void mpWindow::onZoomRedo( wxCommandEvent& WXUNUSED( event ) )
|
|
{
|
|
ZoomRedo();
|
|
}
|
|
|
|
|
|
void mpWindow::OnSize( wxSizeEvent& WXUNUSED( event ) )
|
|
{
|
|
// Try to fit again with the new window size:
|
|
Fit( m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax );
|
|
}
|
|
|
|
|
|
bool mpWindow::AddLayer( mpLayer* layer, bool refreshDisplay )
|
|
{
|
|
if( layer )
|
|
{
|
|
m_layers.push_back( layer );
|
|
|
|
if( refreshDisplay )
|
|
UpdateAll();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool mpWindow::DelLayer( mpLayer* layer, bool alsoDeleteObject, bool refreshDisplay )
|
|
{
|
|
wxLayerList::iterator layIt;
|
|
|
|
for( layIt = m_layers.begin(); layIt != m_layers.end(); layIt++ )
|
|
{
|
|
if( *layIt == layer )
|
|
{
|
|
// Also delete the object?
|
|
if( alsoDeleteObject )
|
|
delete *layIt;
|
|
|
|
m_layers.erase( layIt ); // this deleted the reference only
|
|
|
|
if( refreshDisplay )
|
|
UpdateAll();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void mpWindow::DelAllLayers( bool alsoDeleteObject, bool refreshDisplay )
|
|
{
|
|
while( m_layers.size()>0 )
|
|
{
|
|
// Also delete the object?
|
|
if( alsoDeleteObject )
|
|
delete m_layers[0];
|
|
|
|
m_layers.erase( m_layers.begin() ); // this deleted the reference only
|
|
}
|
|
|
|
if( refreshDisplay )
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
void mpWindow::OnPaint( wxPaintEvent& WXUNUSED( event ) )
|
|
{
|
|
wxPaintDC paintDC( this );
|
|
|
|
paintDC.GetSize( &m_scrX, &m_scrY ); // This is the size of the visible area only!
|
|
|
|
// Selects direct or buffered draw:
|
|
wxDC* targetDC = &paintDC;
|
|
|
|
// J.L.Blanco @ Aug 2007: Added double buffer support
|
|
if( m_enableDoubleBuffer )
|
|
{
|
|
if( m_last_lx != m_scrX || m_last_ly != m_scrY )
|
|
{
|
|
delete m_buff_bmp;
|
|
m_buff_bmp = new wxBitmap( m_scrX, m_scrY );
|
|
m_buff_dc.SelectObject( *m_buff_bmp );
|
|
m_last_lx = m_scrX;
|
|
m_last_ly = m_scrY;
|
|
}
|
|
|
|
targetDC = &m_buff_dc;
|
|
}
|
|
|
|
if( wxGraphicsContext* ctx = targetDC->GetGraphicsContext() )
|
|
{
|
|
if( !ctx->SetInterpolationQuality( wxINTERPOLATION_BEST ) )
|
|
if( !ctx->SetInterpolationQuality( wxINTERPOLATION_GOOD ) )
|
|
ctx->SetInterpolationQuality( wxINTERPOLATION_FAST );
|
|
|
|
ctx->SetAntialiasMode( wxANTIALIAS_DEFAULT );
|
|
}
|
|
|
|
// Draw background:
|
|
targetDC->SetPen( *wxTRANSPARENT_PEN );
|
|
wxBrush brush( GetBackgroundColour() );
|
|
targetDC->SetBrush( brush );
|
|
targetDC->SetTextForeground( m_fgColour );
|
|
targetDC->DrawRectangle( 0, 0, m_scrX, m_scrY );
|
|
|
|
// Draw all the layers:
|
|
for( mpLayer* layer : m_layers )
|
|
layer->Plot( *targetDC, *this );
|
|
|
|
if( m_zooming )
|
|
{
|
|
wxPen pen( m_fgColour, 1, wxPENSTYLE_DOT );
|
|
targetDC->SetPen( pen );
|
|
targetDC->SetBrush( *wxTRANSPARENT_BRUSH );
|
|
targetDC->DrawRectangle( m_zoomRect );
|
|
}
|
|
|
|
// If doublebuffer, draw now to the window:
|
|
if( m_enableDoubleBuffer )
|
|
paintDC.Blit( 0, 0, m_scrX, m_scrY, targetDC, 0, 0 );
|
|
}
|
|
|
|
|
|
bool mpWindow::UpdateBBox()
|
|
{
|
|
m_minX = 0.0;
|
|
m_maxX = 1.0;
|
|
m_minY = 0.0;
|
|
m_maxY = 1.0;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void mpWindow::UpdateAll()
|
|
{
|
|
UpdateBBox();
|
|
Refresh( false );
|
|
}
|
|
|
|
|
|
void mpWindow::SetScaleX( double scaleX )
|
|
{
|
|
if( scaleX != 0 )
|
|
m_scaleX = scaleX;
|
|
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
// New methods implemented by Davide Rondini
|
|
|
|
mpLayer* mpWindow::GetLayer( int position ) const
|
|
{
|
|
if( ( position >= (int) m_layers.size() ) || position < 0 )
|
|
return nullptr;
|
|
|
|
return m_layers[position];
|
|
}
|
|
|
|
|
|
const mpLayer* mpWindow::GetLayerByName( const wxString& name ) const
|
|
{
|
|
for( const mpLayer* layer : m_layers )
|
|
{
|
|
if( !layer->GetName().Cmp( name ) )
|
|
return layer;
|
|
}
|
|
|
|
return nullptr; // Not found
|
|
}
|
|
|
|
|
|
void mpWindow::GetBoundingBox( double* bbox ) const
|
|
{
|
|
bbox[0] = m_minX;
|
|
bbox[1] = m_maxX;
|
|
bbox[2] = m_minY;
|
|
bbox[3] = m_maxY;
|
|
}
|
|
|
|
|
|
bool mpWindow::SaveScreenshot( const wxString& filename, wxBitmapType type, wxSize imageSize,
|
|
bool fit )
|
|
{
|
|
int sizeX, sizeY;
|
|
|
|
if( imageSize == wxDefaultSize )
|
|
{
|
|
sizeX = m_scrX;
|
|
sizeY = m_scrY;
|
|
}
|
|
else
|
|
{
|
|
sizeX = imageSize.x;
|
|
sizeY = imageSize.y;
|
|
SetScr( sizeX, sizeY );
|
|
}
|
|
|
|
wxBitmap screenBuffer( sizeX, sizeY );
|
|
wxMemoryDC screenDC;
|
|
screenDC.SelectObject( screenBuffer );
|
|
screenDC.SetPen( *wxWHITE_PEN );
|
|
screenDC.SetTextForeground( m_fgColour );
|
|
wxBrush brush( GetBackgroundColour() );
|
|
screenDC.SetBrush( brush );
|
|
screenDC.DrawRectangle( 0, 0, sizeX, sizeY );
|
|
|
|
if( fit )
|
|
Fit( m_minX, m_maxX, m_minY, m_maxY, &sizeX, &sizeY );
|
|
else
|
|
Fit( m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax, &sizeX, &sizeY );
|
|
|
|
// Draw all the layers:
|
|
for( mpLayer* layer : m_layers )
|
|
layer->Plot( screenDC, *this );
|
|
|
|
if( imageSize != wxDefaultSize )
|
|
{
|
|
// Restore dimensions
|
|
int bk_scrX = m_scrX;
|
|
int bk_scrY = m_scrY;
|
|
SetScr( bk_scrX, bk_scrY );
|
|
Fit( m_desiredXmin, m_desiredXmax, m_desiredYmin, m_desiredYmax, &bk_scrX, &bk_scrY );
|
|
UpdateAll();
|
|
}
|
|
|
|
// Once drawing is complete, actually save screen shot
|
|
wxImage screenImage = screenBuffer.ConvertToImage();
|
|
return screenImage.SaveFile( filename, type );
|
|
}
|
|
|
|
|
|
void mpWindow::SetMargins( int top, int right, int bottom, int left )
|
|
{
|
|
m_marginTop = top;
|
|
m_marginRight = right;
|
|
m_marginBottom = bottom;
|
|
m_marginLeft = left;
|
|
}
|
|
|
|
|
|
mpInfoLayer* mpWindow::IsInsideInfoLayer( wxPoint& point )
|
|
{
|
|
for( mpLayer* layer : m_layers )
|
|
{
|
|
if( layer->IsInfo() )
|
|
{
|
|
mpInfoLayer* tmpLyr = static_cast<mpInfoLayer*>( layer );
|
|
|
|
if( tmpLyr->Inside( point ) )
|
|
return tmpLyr;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
void mpWindow::SetLayerVisible( const wxString& name, bool viewable )
|
|
{
|
|
if( mpLayer* lx = GetLayerByName( name ) )
|
|
{
|
|
lx->SetVisible( viewable );
|
|
UpdateAll();
|
|
}
|
|
}
|
|
|
|
|
|
bool mpWindow::IsLayerVisible( const wxString& name ) const
|
|
{
|
|
if( const mpLayer* lx = GetLayerByName( name ) )
|
|
return lx->IsVisible();
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void mpWindow::SetLayerVisible( const unsigned int position, bool viewable )
|
|
{
|
|
if( mpLayer* lx = GetLayer( position ) )
|
|
{
|
|
lx->SetVisible( viewable );
|
|
UpdateAll();
|
|
}
|
|
}
|
|
|
|
|
|
bool mpWindow::IsLayerVisible( unsigned int position ) const
|
|
{
|
|
if( const mpLayer* lx = GetLayer( position ) )
|
|
return lx->IsVisible();
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void mpWindow::SetColourTheme( const wxColour& bgColour, const wxColour& drawColour,
|
|
const wxColour& axesColour )
|
|
{
|
|
SetBackgroundColour( bgColour );
|
|
SetForegroundColour( drawColour );
|
|
m_bgColour = bgColour;
|
|
m_fgColour = drawColour;
|
|
m_axColour = axesColour;
|
|
|
|
// Cycle between layers to set colours and properties to them
|
|
for( mpLayer* layer : m_layers )
|
|
{
|
|
if( layer->GetLayerType() == mpLAYER_AXIS )
|
|
{
|
|
wxPen axisPen = layer->GetPen(); // Get the old pen to modify only colour, not style or width
|
|
axisPen.SetColour( axesColour );
|
|
layer->SetPen( axisPen );
|
|
}
|
|
|
|
if( layer->GetLayerType() == mpLAYER_INFO )
|
|
{
|
|
wxPen infoPen = layer->GetPen(); // Get the old pen to modify only colour, not style or width
|
|
infoPen.SetColour( drawColour );
|
|
layer->SetPen( infoPen );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mpFXYVector implementation - by Jose Luis Blanco (AGO-2007)
|
|
// -----------------------------------------------------------------------------
|
|
|
|
IMPLEMENT_DYNAMIC_CLASS( mpFXYVector, mpFXY )
|
|
|
|
// Constructor
|
|
mpFXYVector::mpFXYVector( const wxString& name, int flags ) :
|
|
mpFXY( name, flags )
|
|
{
|
|
m_index = 0;
|
|
m_minX = -1;
|
|
m_maxX = 1;
|
|
m_minY = -1;
|
|
m_maxY = 1;
|
|
m_type = mpLAYER_PLOT;
|
|
}
|
|
|
|
|
|
double mpScaleX::TransformToPlot( double x ) const
|
|
{
|
|
return (x + m_offset) * m_scale;
|
|
}
|
|
|
|
|
|
double mpScaleX::TransformFromPlot( double xplot ) const
|
|
{
|
|
return xplot / m_scale - m_offset;
|
|
}
|
|
|
|
|
|
double mpScaleY::TransformToPlot( double x ) const
|
|
{
|
|
return (x + m_offset) * m_scale;
|
|
}
|
|
|
|
|
|
double mpScaleY::TransformFromPlot( double xplot ) const
|
|
{
|
|
return xplot / m_scale - m_offset;
|
|
}
|
|
|
|
|
|
double mpScaleXLog::TransformToPlot( double x ) const
|
|
{
|
|
double xlogmin = log10( m_minV );
|
|
double xlogmax = log10( m_maxV );
|
|
|
|
return ( log10( x ) - xlogmin) / (xlogmax - xlogmin);
|
|
}
|
|
|
|
|
|
double mpScaleXLog::TransformFromPlot( double xplot ) const
|
|
{
|
|
double xlogmin = log10( m_minV );
|
|
double xlogmax = log10( m_maxV );
|
|
|
|
return pow( 10.0, xplot * (xlogmax - xlogmin) + xlogmin );
|
|
}
|
|
|
|
|
|
void mpFXYVector::Rewind()
|
|
{
|
|
m_index = 0;
|
|
}
|
|
|
|
|
|
size_t mpFXYVector::GetCount() const
|
|
{
|
|
return m_xs.size();
|
|
}
|
|
|
|
|
|
bool mpFXYVector::GetNextXY( double& x, double& y )
|
|
{
|
|
if( m_index >= m_xs.size() )
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
x = m_xs[m_index];
|
|
y = m_ys[m_index++];
|
|
return m_index <= m_xs.size();
|
|
}
|
|
}
|
|
|
|
|
|
void mpFXYVector::Clear()
|
|
{
|
|
m_xs.clear();
|
|
m_ys.clear();
|
|
}
|
|
|
|
|
|
void mpFXYVector::SetData( const std::vector<double>& xs, const std::vector<double>& ys )
|
|
{
|
|
// Check if the data vectors are of the same size
|
|
if( xs.size() != ys.size() )
|
|
return;
|
|
|
|
// Copy the data:
|
|
m_xs = xs;
|
|
m_ys = ys;
|
|
|
|
// Update internal variables for the bounding box.
|
|
if( xs.size() > 0 )
|
|
{
|
|
m_minX = xs[0];
|
|
m_maxX = xs[0];
|
|
m_minY = ys[0];
|
|
m_maxY = ys[0];
|
|
|
|
for( const double x : xs )
|
|
{
|
|
if( x < m_minX )
|
|
m_minX = x;
|
|
|
|
if( x > m_maxX )
|
|
m_maxX = x;
|
|
}
|
|
|
|
for( const double y : ys )
|
|
{
|
|
if( y < m_minY )
|
|
m_minY = y;
|
|
|
|
if( y > m_maxY )
|
|
m_maxY = y;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_minX = 0;
|
|
m_maxX = 0;
|
|
m_minY = 0;
|
|
m_maxY = 0;
|
|
}
|
|
}
|
|
|
|
|
|
void mpFXY::SetScale( mpScaleBase* scaleX, mpScaleBase* scaleY )
|
|
{
|
|
m_scaleX = scaleX;
|
|
m_scaleY = scaleY;
|
|
|
|
UpdateScales();
|
|
}
|
|
|
|
|
|
void mpFXY::UpdateScales()
|
|
{
|
|
if( m_scaleX )
|
|
m_scaleX->ExtendDataRange( GetMinX(), GetMaxX() );
|
|
|
|
if( m_scaleY )
|
|
m_scaleY->ExtendDataRange( GetMinY(), GetMaxY() );
|
|
}
|
|
|
|
|
|
double mpFXY::s2x( double plotCoordX ) const
|
|
{
|
|
return m_scaleX ? m_scaleX->TransformFromPlot( plotCoordX ) : plotCoordX;
|
|
}
|
|
|
|
|
|
double mpFXY::s2y( double plotCoordY ) const
|
|
{
|
|
return m_scaleY ? m_scaleY->TransformFromPlot( plotCoordY ) : plotCoordY;
|
|
}
|
|
|
|
|
|
double mpFXY::x2s( double x ) const
|
|
{
|
|
return m_scaleX ? m_scaleX->TransformToPlot( x ) : x;
|
|
}
|
|
|
|
|
|
double mpFXY::y2s( double y ) const
|
|
{
|
|
return m_scaleY ? m_scaleY->TransformToPlot( y ) : y;
|
|
}
|