Commit 58edb7c8 authored by David Saxton's avatar David Saxton

Can now intelligently position labels in the diagram to avoid overlapping plots,

etc. Added option to draw extrema points on plots (using the labels).

svn path=/trunk/KDE/kdeedu/kmplot/; revision=554833
parent eea268d3
......@@ -9,7 +9,7 @@ TODO
* New plot types:
* Differential e.g. f_n(x,y) d^nY/dx^n + ... + f_1(x,y) dY/dx + f_0(x, y) = 0
* Parameters:
* Make parameters more powerful
* Make parameters more powerful (e.g. time evolution).
* In tool menu:
* find "nollställen" ( en: where the function's value is 0)
* get slope for a x-point
......@@ -22,6 +22,9 @@ TODO
- different paper sizes
* An export dialog where you can set the size and enable/disable monocrome.
* More printing options.
* Move Coordinate System toolbar buttons to one drop-down list
* Make Appearance widget more powerful - add gradient for parameters, etc.
* Option to show extreme points of (Cartesian) functions (accessible via right-click, where else?..). This would replace the find min/max dialog box.
IN PROGRESS
=========================
......
......@@ -313,7 +313,7 @@ void MainDlg::setupActions()
connect( View::self()->m_menuSliderAction, SIGNAL(triggered(bool)), this, SLOT( toggleShowSliders() ) );
// Popup menu
//BEGIN function popup menu
KAction *mnuHide = new KAction(i18n("&Hide"), actionCollection(),"mnuhide" );
connect( mnuHide, SIGNAL(triggered(bool)), View::self(), SLOT( mnuHide_clicked() ) );
m_popupmenu->addAction( mnuHide );
......@@ -329,10 +329,17 @@ void MainDlg::setupActions()
m_popupmenu->addAction( mnuEdit );
m_popupmenu->addSeparator();
View::self()->m_showFunctionExtrema = new KToggleAction( i18n( "Show Extrema" ), actionCollection(), "showExtrema" );
View::self()->m_showFunctionExtrema->setIcon( KIcon( "minimum" ) );
connect( View::self()->m_showFunctionExtrema, SIGNAL(triggered(bool)), View::self(), SLOT(showExtrema(bool)) );
m_popupmenu->addAction( View::self()->m_showFunctionExtrema );
m_popupmenu->addAction( mnuYValue );
m_popupmenu->addAction( mnuMinValue );
m_popupmenu->addAction( mnuMaxValue );
m_popupmenu->addAction( mnuArea );
//END function popup menu
}
......
......@@ -129,8 +129,6 @@ public slots:
/// Pushes the previous document state to the undo stack and records the current one
void requestSaveCurrentState();
// ///I'm not sure it a delete-all-functions command is necessary
// void slotCleanWindow();
///Save a plot i.e. save the function name and all the settings for the plot
void slotSave();
///Save a plot and choose a name for it
......
......@@ -127,6 +127,31 @@ View::~View()
}
void View::initDrawLabels()
{
m_labelFont = QFont( Settings::labelFont(), Settings::labelFontSize() );
for ( int i = 0; i < LabelGridSize; ++i )
for ( int j = 0; j < LabelGridSize; ++j )
m_usedDiagramArea[i][j] = false;
// Add the axis
double x = CDiagr::self()->xToPixel( 0 );
double y = CDiagr::self()->yToPixel( 0 );
double x0 = CDiagr::self()->xToPixel( m_xmin );
double x1 = CDiagr::self()->xToPixel( m_xmax );
double y0 = CDiagr::self()->yToPixel( m_ymin );
double y1 = CDiagr::self()->yToPixel( m_ymax );
// x-axis
markDiagramAreaUsed( QRectF( x-20, y0, 40, y1-y0 ) );
// y-axis
markDiagramAreaUsed( QRectF( x0, y-20, x1-x0, 40 ) );
}
void View::draw( QPaintDevice * dev, PlotMedium medium )
{
// kDebug() << k_funcinfo << endl;
......@@ -139,6 +164,7 @@ void View::draw( QPaintDevice * dev, PlotMedium medium )
rc=DC.viewport();
m_width = rc.width();
m_height = rc.height();
initDrawLabels();
switch ( medium )
{
......@@ -235,6 +261,7 @@ void View::draw( QPaintDevice * dev, PlotMedium medium )
else
plotFunction(ufkt, &DC);
}
drawFunctionInfo( &DC );
DC.setClipping( false );
m_isDrawing=false;
......@@ -625,6 +652,7 @@ void View::plotImplicitInSquare( const Plot & plot, QPainter * painter, double x
QPointF next = CDiagr::self()->toPixel( QPointF( x, y ), CDiagr::ClipInfinite );
painter->drawLine( prev, next );
prev = next;
markDiagramPointUsed( next );
}
if ( outOfBounds )
......@@ -682,8 +710,9 @@ void View::plotFunction(Function *ufkt, QPainter *pDC)
p2 = CDiagr::self()->toPixel( realValue( plot, x, false ), CDiagr::ClipInfinite );
double min_mod = (ufkt->type() == Function::Cartesian) ? 0.1 : 5e-5;
double max_mod = (ufkt->type() == Function::Cartesian) ? 1e+1 : 5e+1;
bool dxAtMinimum = (dx <= base_dx*min_mod);
bool dxAtMaximum = (dx >= base_dx*(5e+1));
bool dxAtMaximum = (dx >= base_dx*max_mod);
bool dxTooBig = false;
bool dxTooSmall = false;
......@@ -715,7 +744,7 @@ void View::plotFunction(Function *ufkt, QPainter *pDC)
{
if ( QRectF( area ).intersects( bound ) )
{
dxTooBig = !dxAtMinimum && (length > (quickDraw ? 40.0 : 4.0));
dxTooBig = !dxAtMinimum && (length > (quickDraw ? max_mod : 4.0));
dxTooSmall = !dxAtMaximum && (length < (quickDraw ? 10.0 : 1.0));
}
else
......@@ -757,6 +786,8 @@ void View::plotFunction(Function *ufkt, QPainter *pDC)
else if ( penShouldDraw( totalLength, plot ) )
pDC->drawLine( p1, p2 );
}
markDiagramPointUsed( p2 );
}
p1=p2;
}
......@@ -772,19 +803,187 @@ void View::plotFunction(Function *ufkt, QPainter *pDC)
x=x+dx;
}
}
}
}
#if 0
// Draw the stationary points if the user has requested them to be shown
if ( ufkt->type() == Function::Cartesian )
void View::drawFunctionInfo( QPainter * painter )
{
// Don't draw info if translating the view
if ( m_zoomMode == Translating )
return;
foreach ( Function * function, XParser::self()->m_ufkt )
{
if ( stop_calculating )
break;
foreach ( Plot plot, function->allPlots() )
{
pDC->setPen( QPen( Qt::black, 10 ) );
// Draw extrema points?
if ( (function->type() == Function::Cartesian) && function->plotAppearance( plot.plotMode ).showExtrema )
{
QList<QPointF> stationaryPoints = findStationaryPoints( plot );
foreach ( QPointF realValue, stationaryPoints )
{
painter->setPen( QPen( Qt::black, 10 ) );
painter->drawPoint( CDiagr::self()->toPixel( realValue ) );
QString x = posToString( realValue.x(), (m_xmax-m_xmin)/area.width(), View::DecimalFormat );
QString y = posToString( realValue.y(), (m_ymax-m_ymin)/area.width(), View::DecimalFormat );
drawLabel( painter, function->plotAppearance( plot.plotMode ).color, realValue, QString( "x = %1 y = %2" ).arg(x).arg(y) );
}
}
}
}
}
QList<QPointF> stationaryPoints = findStationaryPoints( plot );
foreach ( QPointF realValue, stationaryPoints )
pDC->drawPoint( CDiagr::self()->toPixel( realValue ) );
void View::drawLabel( QPainter * painter, const QColor & color, const QPointF & realPos, const QString & text )
{
QPalette palette;
QColor outline = color;
QColor background = outline.light( 500 );
background.setAlpha( 127 );
QPointF pixelCenter = CDiagr::self()->toPixel( realPos );
QRectF rect( pixelCenter, QSizeF( 1, 1 ) );
int flags = Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignTop;
rect = painter->boundingRect( rect, flags, text ).adjusted( -8, -4, 4, 2 );
// Try and find a nice place for inserting the rectangle
int bestCost = int(1e7);
QPointF bestCenter = realPos;
for ( double x = pixelCenter.x() - 400; x <= pixelCenter.x() + 400; x += 40 )
{
for ( double y = pixelCenter.y() - 400; y <= pixelCenter.y() + 400; y += 40 )
{
QPointF center( x, y ) ;
rect.moveCenter( center );
double length = (x-pixelCenter.x())*(x-pixelCenter.x()) + (y-pixelCenter.y())*(y-pixelCenter.y());
int cost = rectCost( rect ) + int(length)/100;
if ( cost < bestCost )
{
bestCenter = center;
bestCost = cost;
}
}
#endif
}
rect.moveCenter( bestCenter );
markDiagramAreaUsed( rect );
painter->setBrush( background );
painter->setPen( outline );
painter->drawRect( rect );
// If the rectangle does not lie over realPos, then draw a line to realPos from the rectangle
if ( ! rect.contains( pixelCenter ) )
{
QPointF lineStart = bestCenter;
QLineF line( pixelCenter, bestCenter );
QPointF intersect = bestCenter;
// Where does line intersect the rectangle?
if ( QLineF( rect.topLeft(), rect.topRight() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
lineStart = intersect;
else if ( QLineF( rect.topRight(), rect.bottomRight() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
lineStart = intersect;
else if ( QLineF( rect.bottomRight(), rect.bottomLeft() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
lineStart = intersect;
else if ( QLineF( rect.bottomLeft(), rect.topLeft() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
lineStart = intersect;
painter->drawLine( lineStart, pixelCenter );
}
painter->setFont( m_labelFont );
painter->setPen( Qt::black );
painter->drawText( rect.adjusted( 8, 4, -4, -2 ), flags, text );
}
QRect View::usedDiagramRect( QRectF rect ) const
{
if ( !area.isValid() )
return QRect();
rect = wm.mapRect( rect );
rect = rect & area;
double x0 = (rect.left() - area.left()) / area.width();
double x1 = (rect.right() - area.left()) / area.width();
double y0 = (rect.top() - area.top()) / area.height();
double y1 = (rect.bottom() - area.top()) / area.height();
int i0 = int( x0 * LabelGridSize );
int i1 = int( x1 * LabelGridSize );
int j0 = int( y0 * LabelGridSize );
int j1 = int( y1 * LabelGridSize );
return QRect( i0, j0, i1-i0+1, j1-j0+1 );
}
void View::markDiagramAreaUsed( const QRectF & rect )
{
if ( m_zoomMode == Translating )
return;
QRect r = usedDiagramRect( rect );
for ( int i = r.left(); i <= r.right(); ++i )
for ( int j = r.top(); j <= r.bottom(); ++j )
m_usedDiagramArea[i][j] = true;
}
void View::markDiagramPointUsed( QPointF point )
{
if ( m_zoomMode == Translating )
return;
point = wm.map( point );
if ( ! QRectF(area).contains(point) )
return;
double x = (point.x() - area.left()) / area.width();
double y = (point.y() - area.top()) / area.height();
int i = int( x * LabelGridSize );
int j = int( y * LabelGridSize );
m_usedDiagramArea[i][j] = true;
}
int View::rectCost( const QRectF & rect ) const
{
int cost = 0;
// If the rectangle goes off the edge, mark it as very high cost)
QRectF mapped = wm.mapRect( rect );
QRectF intersect = mapped & area;
cost += int(mapped.width() * mapped.height()) - int(intersect.width() * intersect.height());
QRect r = usedDiagramRect( rect );
for ( int i = r.left(); i <= r.right(); ++i )
for ( int j = r.top(); j <= r.bottom(); ++j )
if ( m_usedDiagramArea[i][j] )
cost += 200;
return cost;
}
......@@ -1043,7 +1242,11 @@ QList< QPointF > View::findStationaryPoints( const Plot & plot )
plot.updateFunctionParameter();
QList< QPointF > stationaryPoints;
foreach ( double x, roots )
stationaryPoints << realValue( plot, x, false );
{
QPointF real = realValue( plot, x, false );
if ( real.y() >= m_ymin && real.y() <= m_ymax )
stationaryPoints << real;
}
return stationaryPoints;
}
......@@ -1051,7 +1254,7 @@ QList< QPointF > View::findStationaryPoints( const Plot & plot )
QList< double > View::findRoots( const Plot & plot, double min, double max, RootAccuracy accuracy )
{
QList< double > roots;
QMap< double, double > roots;
// Use this to detect finding the same root. This assumes that the same root
// will be converged to in unbroken x-intervals
......@@ -1072,12 +1275,22 @@ QList< double > View::findRoots( const Plot & plot, double min, double max, Root
if ( found && differentRoot )
{
roots << x;
roots.insert( x, x );
prevX = x;
}
}
return roots;
QList<double> list;
foreach ( double x, roots )
{
bool differentRoot = (qAbs(x-prevX) > (dx/2)) || list.isEmpty();
if ( differentRoot )
list << x;
prevX = x;
}
return list;
}
......@@ -1619,7 +1832,9 @@ void View::mousePressEvent(QMouseEvent *e)
m_popupmenushown = 2;
else
m_popupmenushown = 1;
m_showFunctionExtrema->setChecked( function->plotAppearance( m_currentPlot.plotMode ).showExtrema );
m_popupmenu->setTitle( popupTitle );
m_popupmenu->exec( QCursor::pos() );
}
......@@ -1953,7 +2168,7 @@ bool View::updateCrosshairPosition()
m_currentPlot.updateFunctionParameter();
Function * it = m_currentPlot.function();
if ( it && crosshairPositionValid( it ) )
if ( it && crosshairPositionValid( it ) && (m_popupmenushown != 1) )
{
// The user currently has a plot selected, with the mouse in a valid position
......@@ -2372,6 +2587,7 @@ void View::stopDrawing()
stop_calculating = true;
}
QPointF View::findMinMaxValue( const Plot & plot, ExtremaType type, double dmin, double dmax )
{
Function * ufkt = plot.function();
......@@ -2709,6 +2925,19 @@ void View::mnuRemove_clicked()
updateSliders();
m_modified = true;
}
void View::showExtrema( bool show )
{
Function * f = m_currentPlot.function();
if ( !f )
return;
f->plotAppearance( m_currentPlot.plotMode ).showExtrema = show;
drawPlot();
}
void View::mnuEdit_clicked()
{
MainDlg::self()->functionEditor()->setCurrentFunction( m_currentPlot.functionID() );
......@@ -2843,6 +3072,9 @@ bool View::shouldShowCrosshairs() const
case Translating:
return false;
}
if ( m_popupmenushown > 0 )
return false;
Function * it = m_currentPlot.function();
......
......@@ -139,6 +139,8 @@ class View : public QWidget
QPointer<KSliderWindow> m_sliderWindow;
/// Menu actions for the sliders
KToggleAction * m_menuSliderAction;
/// Menu action for showing function extrema
KToggleAction * m_showFunctionExtrema;
void updateSliders(); /// show only needed sliders
/**
......@@ -185,6 +187,7 @@ class View : public QWidget
void mnuHide_clicked();
void mnuRemove_clicked();
void mnuEdit_clicked();
void showExtrema( bool show );
///Slots for the zoom menu
void mnuZoomIn_clicked();
void mnuZoomOut_clicked();
......@@ -233,6 +236,22 @@ class View : public QWidget
* Draw an implicit function.
*/
void plotImplicit( Function * function, QPainter * );
/**
* Draw the extrema points, function names, etc. This needs to be done
* after the functions have all been drawn so that the label positioning
* knows where the plots have been drawn.
*/
void drawFunctionInfo( QPainter * painter );
/**
* Initializes for the drawLabel function, called before drawing has
* started.
*/
void initDrawLabels();
/**
* Draw text (e.g. showing the value of an extrema point or a function
* name) at the given (real) position.
*/
void drawLabel( QPainter * painter, const QColor & color, const QPointF & realPos, const QString & text );
/**
* Used by plotImplicit to draw the plot in the square associated with
* the given point. \p orientation is the edge that the plot trace
......@@ -430,6 +449,30 @@ class View : public QWidget
bool &m_modified;
/// False if KmPlot is started as a program, otherwise true
bool const m_readonly;
/// For drawing diagram labels
QFont m_labelFont;
/// Indicate which parts of the diagram have content (e.g. axis or
/// plots), so that they can be avoided when drawing diagram labels
static const int LabelGridSize = 50;
bool m_usedDiagramArea[LabelGridSize][LabelGridSize];
/**
* Marks the given diagram rectangle (in screen coords) as 'used'.
*/
void markDiagramAreaUsed( const QRectF & rect );
/**
* Marks the given diagram point (in screen coords) as 'used'.
*/
void markDiagramPointUsed( QPointF point );
/**
* \return the m_usedDiagramArea coords for the screen rect.
*/
QRect usedDiagramRect( QRectF rect ) const;
/**
* \return the cost of occupying the given rectangle (as in whether it
* overlaps other diagram content, etc).
*/
int rectCost( const QRectF & rect ) const;
enum ZoomMode
{
......
......@@ -72,6 +72,7 @@ PlotAppearance::PlotAppearance( )
lineWidth = 0.2;
color = Qt::black;
visible = false;
showExtrema = false;
style = Qt::SolidLine;
}
......@@ -81,7 +82,8 @@ bool PlotAppearance::operator !=( const PlotAppearance & other ) const
return (visible != other.visible) ||
(color != other.color) ||
(lineWidth != other.lineWidth) ||
(style != other.style);
(style != other.style) ||
(showExtrema != other.showExtrema);
}
......
......@@ -88,6 +88,7 @@ class PlotAppearance
QColor color; ///< color that the plot will be drawn in
bool visible; ///< whether to display this plot
Qt::PenStyle style; ///< pen style (e.g. dolif, dashes, dotted, etc)
bool showExtrema; ///< for cartesian functions, whether to show the extreme values of the function
bool operator != ( const PlotAppearance & other ) const;
......
......@@ -132,6 +132,16 @@
<label>Font name of the printed header table</label>
<whatsthis>Choose a font name for the table printed at the top of the page.</whatsthis>
<default code="true">KGlobalSettings::generalFont().family()</default>
</entry>
<entry name="LabelFont" type="String">
<label>Font name of diagram labels</label>
<whatsthis>Choose a font name for diagram labels.</whatsthis>
<default code="true">KGlobalSettings::generalFont().family()</default>
</entry>
<entry name="LabelFontSize" type="Int">
<label>Font size of diagram labels</label>
<whatsthis>Choose a font size for diagram labels.</whatsthis>
<default code="true">24</default>
</entry>
</group>
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment