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