Engauge Digitizer  2
GridLineFactory.cpp
1 /******************************************************************************************************
2  * (C) 2014 markummitchell@github.com. This file is part of Engauge Digitizer, which is released *
3  * under GNU General Public License version 2 (GPLv2) or (at your option) any later version. See file *
4  * LICENSE or go to gnu.org/licenses for details. Distribution requires prior written permission. *
5  ******************************************************************************************************/
6 
7 #include "DocumentModelCoords.h"
8 #include "DocumentModelGridDisplay.h"
9 #include "EngaugeAssert.h"
10 #include "EnumsToQt.h"
11 #include "GraphicsArcItem.h"
12 #include "GridLineFactory.h"
13 #include "GridLineLimiter.h"
14 #include "GridLines.h"
15 #include "GridLineStyle.h"
16 #include "Logger.h"
17 #include "MainWindowModel.h"
18 #include <QGraphicsScene>
19 #include <qmath.h>
20 #include <QTextStream>
21 #include "QtToString.h"
22 #include "Transformation.h"
23 
24 const int Z_VALUE_IN_FRONT = 100;
25 
26 // To emphasize that the axis lines are still there, we make these checker somewhat transparent
27 const double CHECKER_OPACITY = 0.6;
28 
29 const double PI = 3.1415926535;
30 const double TWO_PI = 2.0 * PI;
31 const double DEGREES_TO_RADIANS = PI / 180.0;
32 const double RADIANS_TO_TICS = 5760 / TWO_PI;
33 
34 GridLineFactory::GridLineFactory(QGraphicsScene &scene,
35  const DocumentModelCoords &modelCoords) :
36  m_scene (scene),
37  m_pointRadius (0.0),
38  m_modelCoords (modelCoords),
39  m_isChecker (false)
40 {
41  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::GridLineFactory";
42 }
43 
44 GridLineFactory::GridLineFactory(QGraphicsScene &scene,
45  int pointRadius,
46  const QList<Point> &pointsToIsolate,
47  const DocumentModelCoords &modelCoords) :
48  m_scene (scene),
49  m_pointRadius (pointRadius),
50  m_pointsToIsolate (pointsToIsolate),
51  m_modelCoords (modelCoords),
52  m_isChecker (true)
53 {
54  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::GridLineFactory"
55  << " pointRadius=" << pointRadius
56  << " pointsToIsolate=" << pointsToIsolate.count();
57 }
58 
59 void GridLineFactory::bindItemToScene(QGraphicsItem *item) const
60 {
61  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::bindItemToScene";
62 
63  item->setOpacity (CHECKER_OPACITY);
64  item->setZValue (Z_VALUE_IN_FRONT);
65  if (m_isChecker) {
66  item->setToolTip (QObject::tr ("Axes checker. If this does not align with the axes, then the axes points should be checked"));
67  }
68 
69  m_scene.addItem (item);
70 }
71 
73  double yFrom,
74  double xTo,
75  double yTo,
76  const Transformation &transformation)
77 {
78  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::createGridLine"
79  << " xFrom=" << xFrom
80  << " yFrom=" << yFrom
81  << " xTo=" << xTo
82  << " yTo=" << yTo;
83 
84  GridLine *gridLine = new GridLine ();
85 
86  // Originally a complicated algorithm tried to intercept a straight line from (xFrom,yFrom) to (xTo,yTo). That did not work well since:
87  // 1) Calculations for mostly orthogonal cartesian coordinates worked less well with non-orthogonal polar coordinates
88  // 2) Ambiguity in polar coordinates between the shorter and longer paths between (theta0,radius) and (theta1,radius)
89  //
90  // Current algorithm breaks up the interval between (xMin,yMin) and (xMax,yMax) into many smaller pieces and stitches the
91  // desired pieces together. For straight lines in linear graphs this algorithm is very much overkill, but there is no significant
92  // penalty and this approach works in every situation
93 
94  // Should give single-pixel resolution on most images, and 'good enough' resolution on extremely large images
95  const int NUM_STEPS = 1000;
96 
97  bool stateSegmentIsActive = false;
98  QPointF posStartScreen (0, 0);
99 
100  // Loop through steps. Final step i=NUM_STEPS does final processing if a segment is active
101  for (int i = 0; i <= NUM_STEPS; i++) {
102 
103  double s = (double) i / (double) NUM_STEPS;
104 
105  // Interpolate coordinates assuming normal linear scaling
106  double xGraph = (1.0 - s) * xFrom + s * xTo;
107  double yGraph = (1.0 - s) * yFrom + s * yTo;
108 
109  // Replace interpolated coordinates using log scaling if appropriate, preserving the same ranges
110  if (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LOG) {
111  xGraph = qExp ((1.0 - s) * qLn (xFrom) + s * qLn (xTo));
112  }
113  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LOG) {
114  yGraph = qExp ((1.0 - s) * qLn (yFrom) + s * qLn (yTo));
115  }
116 
117  QPointF pointScreen;
118  transformation.transformRawGraphToScreen (QPointF (xGraph, yGraph),
119  pointScreen);
120 
121  double distanceToNearestPoint = minScreenDistanceFromPoints (pointScreen);
122  if ((distanceToNearestPoint < m_pointRadius) ||
123  (i == NUM_STEPS)) {
124 
125  // Too close to point, so point is not included in side. Or this is the final iteration of the loop
126  if (stateSegmentIsActive) {
127 
128  // State transition
129  finishActiveGridLine (posStartScreen,
130  pointScreen,
131  yFrom,
132  yTo,
133  transformation,
134  *gridLine);
135  stateSegmentIsActive = false;
136 
137  }
138  } else {
139 
140  // Outside point, so include point in side
141  if (!stateSegmentIsActive) {
142 
143  // State transition
144  stateSegmentIsActive = true;
145  posStartScreen = pointScreen;
146 
147  }
148  }
149  }
150 
151  return gridLine;
152 }
153 
155  const MainWindowModel &modelMainWindow,
156  const Transformation &transformation,
157  GridLines &gridLines)
158 {
159  // At a minimum the transformation must be defined. Also, there is a brief interval between the definition of
160  // the transformation and the initialization of modelGridDisplay (at which point this method gets called again) and
161  // we do not want to create grid lines during that brief interval
162  if (transformation.transformIsDefined() &&
163  modelGridDisplay.stable()) {
164 
165  double startX = modelGridDisplay.startX ();
166  double startY = modelGridDisplay.startY ();
167  double stepX = modelGridDisplay.stepX ();
168  double stepY = modelGridDisplay.stepY ();
169  double stopX = modelGridDisplay.stopX ();
170  double stopY = modelGridDisplay.stopY ();
171 
172  // Limit the number of grid lines. This is a noop if the limit is not exceeded
173  GridLineLimiter gridLineLimiter;
174  stepX = gridLineLimiter.limitedStepXTheta (m_modelCoords,
175  modelMainWindow,
176  modelGridDisplay);
177  stepY = gridLineLimiter.limitedStepYRange (m_modelCoords,
178  modelMainWindow,
179  modelGridDisplay);
180 
181  // Apply if possible
182  if (stepX != 0 &&
183  stepY != 0) {
184 
185  QColor color (ColorPaletteToQColor (modelGridDisplay.paletteColor()));
186  QPen pen (QPen (color,
187  GRID_LINE_WIDTH,
188  GRID_LINE_STYLE));
189 
190  bool isLinearX = (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LINEAR);
191  for (double x = startX; x <= stopX; (isLinearX ? x += stepX : x *= stepX)) {
192 
193  GridLine *gridLine = createGridLine (x, startY, x, stopY, transformation);
194  gridLine->setPen (pen);
195  gridLines.add (gridLine);
196  }
197 
198  bool isLinearY = (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LINEAR);
199  for (double y = startY; y <= stopY; (isLinearY ? y += stepY : y *= stepY)) {
200 
201  GridLine *gridLine = createGridLine (startX, y, stopX, y, transformation);
202  gridLine->setPen (pen);
203  gridLines.add (gridLine);
204  }
205  }
206  }
207 }
208 
209 void GridLineFactory::createTransformAlign (const Transformation &transformation,
210  double radiusLinearCartesian,
211  const QPointF &posOriginScreen,
212  QTransform &transformAlign,
213  double &ellipseXAxis,
214  double &ellipseYAxis) const
215 {
216  // LOG4CPP_INFO_S is below
217 
218  // Compute a minimal transformation that aligns the graph x and y axes with the screen x and y axes. Specifically, shear,
219  // translation and rotation are allowed but not scaling. Scaling is bad since it messes up the line thickness of the drawn arc.
220  //
221  // Assumptions:
222  // 1) Keep the graph origin at the same screen coordinates
223  // 2) Keep the (+radius,0) the same pixel distance from the origin but moved to the same pixel row as the origin
224  // 3) Keep the (0,+radius) the same pixel distance from the origin but moved to the same pixel column as the origin
225 
226  // Get (+radius,0) and (0,+radius) points
227  QPointF posXRadiusY0Graph (radiusLinearCartesian, 0), posX0YRadiusGraph (0, radiusLinearCartesian);
228  QPointF posXRadiusY0Screen, posX0YRadiusScreen;
229  transformation.transformLinearCartesianGraphToScreen (posXRadiusY0Graph,
230  posXRadiusY0Screen);
231  transformation.transformLinearCartesianGraphToScreen (posX0YRadiusGraph,
232  posX0YRadiusScreen);
233 
234  // Compute arc/ellipse parameters
235  QPointF deltaXRadiusY0 = posXRadiusY0Screen - posOriginScreen;
236  QPointF deltaX0YRadius = posX0YRadiusScreen - posOriginScreen;
237  ellipseXAxis = qSqrt (deltaXRadiusY0.x () * deltaXRadiusY0.x () +
238  deltaXRadiusY0.y () * deltaXRadiusY0.y ());
239  ellipseYAxis = qSqrt (deltaX0YRadius.x () * deltaX0YRadius.x () +
240  deltaX0YRadius.y () * deltaX0YRadius.y ());
241 
242  // Compute the aligned coordinates, constrained by the rules listed above
243  QPointF posXRadiusY0AlignedScreen (posOriginScreen.x() + ellipseXAxis, posOriginScreen.y());
244  QPointF posX0YRadiusAlignedScreen (posOriginScreen.x(), posOriginScreen.y() - ellipseYAxis);
245 
246  transformAlign = Transformation::calculateTransformFromLinearCartesianPoints (posOriginScreen,
247  posXRadiusY0Screen,
248  posX0YRadiusScreen,
249  posOriginScreen,
250  posXRadiusY0AlignedScreen,
251  posX0YRadiusAlignedScreen);
252 
253  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::createTransformAlign"
254  << " transformation=" << QTransformToString (transformation.transformMatrix()).toLatin1().data() << endl
255  << " radiusLinearCartesian=" << radiusLinearCartesian
256  << " posXRadiusY0Screen=" << QPointFToString (posXRadiusY0Screen).toLatin1().data()
257  << " posX0YRadiusScreen=" << QPointFToString (posX0YRadiusScreen).toLatin1().data()
258  << " ellipseXAxis=" << ellipseXAxis
259  << " ellipseYAxis=" << ellipseYAxis
260  << " posXRadiusY0AlignedScreen=" << QPointFToString (posXRadiusY0AlignedScreen).toLatin1().data()
261  << " posX0YRadiusAlignedScreen=" << QPointFToString (posX0YRadiusAlignedScreen).toLatin1().data()
262  << " transformAlign=" << QTransformToString (transformAlign).toLatin1().data();
263 }
264 
265 QGraphicsItem *GridLineFactory::ellipseItem (const Transformation &transformation,
266  double radiusLinearCartesian,
267  const QPointF &posStartScreen,
268  const QPointF &posEndScreen) const
269 {
270  // LOG4CPP_INFO_S is below
271 
272  QPointF posStartGraph, posEndGraph;
273 
274  transformation.transformScreenToRawGraph (posStartScreen,
275  posStartGraph);
276  transformation.transformScreenToRawGraph (posEndScreen,
277  posEndGraph);
278 
279  // Get the angles about the origin of the start and end points
280  double angleStart = posStartGraph.x() * DEGREES_TO_RADIANS;
281  double angleEnd = posEndGraph.x() * DEGREES_TO_RADIANS;
282  if (angleEnd < angleStart) {
283  angleEnd += TWO_PI;
284  }
285  double angleSpan = angleEnd - angleStart;
286 
287  // Get origin
288  QPointF posOriginGraph (0, 0), posOriginScreen;
289  transformation.transformLinearCartesianGraphToScreen (posOriginGraph,
290  posOriginScreen);
291 
292  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::ellipseItem"
293  << " radiusLinearCartesian=" << radiusLinearCartesian
294  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
295  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
296  << " posOriginScreen=" << QPointFToString (posOriginScreen).toLatin1().data()
297  << " angleStart=" << angleStart / DEGREES_TO_RADIANS
298  << " angleEnd=" << angleEnd / DEGREES_TO_RADIANS
299  << " transformation=" << transformation;
300 
301  // Compute rotate/shear transform that aligns linear cartesian graph coordinates with screen coordinates, and ellipse parameters.
302  // Transform does not include scaling since that messes up the thickness of the drawn line, and does not include
303  // translation since that is not important
304  double ellipseXAxis, ellipseYAxis;
305  QTransform transformAlign;
306  createTransformAlign (transformation,
307  radiusLinearCartesian,
308  posOriginScreen,
309  transformAlign,
310  ellipseXAxis,
311  ellipseYAxis);
312 
313  // Create a circle in graph space with the specified radius
314  QRectF boundingRect (-1.0 * ellipseXAxis + posOriginScreen.x(),
315  -1.0 * ellipseYAxis + posOriginScreen.y(),
316  2 * ellipseXAxis,
317  2 * ellipseYAxis);
318  GraphicsArcItem *item = new GraphicsArcItem (boundingRect);
319  item->setStartAngle (angleStart * RADIANS_TO_TICS);
320  item->setSpanAngle (angleSpan * RADIANS_TO_TICS);
321 
322  item->setTransform (transformAlign.transposed ().inverted ());
323 
324  return item;
325 }
326 
327 void GridLineFactory::finishActiveGridLine (const QPointF &posStartScreen,
328  const QPointF &posEndScreen,
329  double yFrom,
330  double yTo,
331  const Transformation &transformation,
332  GridLine &gridLine) const
333 {
334  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::finishActiveGridLine"
335  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
336  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
337  << " yFrom=" << yFrom
338  << " yTo=" << yTo;
339 
340  QGraphicsItem *item;
341  if ((m_modelCoords.coordsType() == COORDS_TYPE_POLAR) &&
342  (yFrom == yTo)) {
343 
344  // Linear cartesian radius
345  double radiusLinearCartesian = yFrom;
346  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LOG) {
347  radiusLinearCartesian = transformation.logToLinearRadius(yFrom,
348  m_modelCoords.originRadius());
349  } else {
350  radiusLinearCartesian -= m_modelCoords.originRadius();
351  }
352 
353  // Draw along an arc since this is a side of constant radius, and we have polar coordinates
354  item = ellipseItem (transformation,
355  radiusLinearCartesian,
356  posStartScreen,
357  posEndScreen);
358 
359  } else {
360 
361  // Draw straight line
362  item = lineItem (posStartScreen,
363  posEndScreen);
364  }
365 
366  gridLine.add (item);
367  bindItemToScene (item);
368 }
369 
370 QGraphicsItem *GridLineFactory::lineItem (const QPointF &posStartScreen,
371  const QPointF &posEndScreen) const
372 {
373  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::lineItem"
374  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
375  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data();
376 
377  return new QGraphicsLineItem (QLineF (posStartScreen,
378  posEndScreen));
379 }
380 
381 double GridLineFactory::minScreenDistanceFromPoints (const QPointF &posScreen)
382 {
383  double minDistance = 0;
384  for (int i = 0; i < m_pointsToIsolate.count (); i++) {
385  const Point &pointCenter = m_pointsToIsolate.at (i);
386 
387  double dx = posScreen.x() - pointCenter.posScreen().x();
388  double dy = posScreen.y() - pointCenter.posScreen().y();
389 
390  double distance = qSqrt (dx * dx + dy * dy);
391  if (i == 0 || distance < minDistance) {
392  minDistance = distance;
393  }
394  }
395 
396  return minDistance;
397 }
void transformScreenToRawGraph(const QPointF &coordScreen, QPointF &coordGraph) const
Transform from cartesian pixel screen coordinates to cartesian/polar graph coordinates.
double stopX() const
Get method for x grid line upper bound (inclusive).
double stepX() const
Get method for x grid line increment.
Model for DlgSettingsGridDisplay and CmdSettingsGridDisplay.
static QTransform calculateTransformFromLinearCartesianPoints(const QPointF &posFrom0, const QPointF &posFrom1, const QPointF &posFrom2, const QPointF &posTo0, const QPointF &posTo1, const QPointF &posTo2)
Calculate QTransform using from/to points that have already been adjusted for, when applicable...
void createGridLinesForEvenlySpacedGrid(const DocumentModelGridDisplay &modelGridDisplay, const MainWindowModel &modelMainWindow, const Transformation &transformation, GridLines &gridLines)
Create a rectangular (cartesian) or annular (polar) grid of evenly spaced grid lines.
QTransform transformMatrix() const
Get method for copying only, for the transform matrix.
void transformLinearCartesianGraphToScreen(const QPointF &coordGraph, QPointF &coordScreen) const
Transform from linear cartesian graph coordinates to cartesian pixel screen coordinates.
static double logToLinearRadius(double r, double rCenter)
Convert radius scaling from log to linear. Calling code is responsible for determining if this is nec...
CoordScale coordScaleYRadius() const
Get method for linear/log scale on y/radius.
double originRadius() const
Get method for origin radius in polar mode.
Draw an arc as an ellipse but without lines from the center to the start and end points.
double startX() const
Get method for x grid line lower bound (inclusive).
Class that represents one digitized point. The screen-to-graph coordinate transformation is always ex...
Definition: Point.h:23
QPointF posScreen() const
Accessor for screen position.
Definition: Point.cpp:392
Affine transformation between screen and graph coordinates, based on digitized axis points...
CoordScale coordScaleXTheta() const
Get method for linear/log scale on x/theta.
Model for DlgSettingsMainWindow.
Container class for GridLine objects.
Definition: GridLines.h:18
double limitedStepYRange(const DocumentModelCoords &modelCoords, const MainWindowModel &modelMainWindow, const DocumentModelGridDisplay &modelGrid) const
Limit step value for y/range coordinate. This is a noop if the maximum grid line limit in MainWindowM...
CoordsType coordsType() const
Get method for coordinates type.
Model for DlgSettingsCoords and CmdSettingsCoords.
ColorPalette paletteColor() const
Get method for color.
double stopY() const
Get method for y grid line upper bound (inclusive).
double startY() const
Get method for y grid line lower bound (inclusive).
bool transformIsDefined() const
Transform is defined when at least three axis points have been digitized.
double stepY() const
Get method for y grid line increment.
GridLineFactory(QGraphicsScene &scene, const DocumentModelCoords &modelCoords)
Simple constructor for general use (i.e. not by Checker)
void add(GridLine *gridLine)
Add specified grid line. Ownership of all allocated QGraphicsItems is passed to new GridLine...
Definition: GridLines.cpp:14
bool stable() const
Get method for stable flag.
void setPen(const QPen &pen)
Set the pen style.
Definition: GridLine.cpp:47
Single grid line drawn a straight or curved line.
Definition: GridLine.h:20
Limit the number of grid lines so a bad combination of start/step/stop value will not lead to extreme...
void transformRawGraphToScreen(const QPointF &pointRaw, QPointF &pointScreen) const
Transform from raw graph coordinates to linear cartesian graph coordinates, then to screen coordinate...
double limitedStepXTheta(const DocumentModelCoords &modelCoords, const MainWindowModel &modelMainWindow, const DocumentModelGridDisplay &modelGrid) const
Limit step value for x/theta coordinate. This is a noop if the maximum grid line limit in MainWindowM...
GridLine * createGridLine(double xFrom, double yFrom, double xTo, double yTo, const Transformation &transformation)
Create grid line, either along constant X/theta or constant Y/radius side.
void add(QGraphicsItem *item)
Add graphics item which represents one segment of the line.
Definition: GridLine.cpp:42