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  // Use \n rather than endl to prevent compiler warning "nonnull argument t compared to null"
268  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::createTransformAlign"
269  << " transformation=" << QTransformToString (transformation.transformMatrix()).toLatin1().data() << "\n"
270  << " radiusLinearCartesian=" << radiusLinearCartesian
271  << " posXRadiusY0Screen=" << QPointFToString (posXRadiusY0Screen).toLatin1().data()
272  << " posX0YRadiusScreen=" << QPointFToString (posX0YRadiusScreen).toLatin1().data()
273  << " ellipseXAxis=" << ellipseXAxis
274  << " ellipseYAxis=" << ellipseYAxis
275  << " posXRadiusY0AlignedScreen=" << QPointFToString (posXRadiusY0AlignedScreen).toLatin1().data()
276  << " posX0YRadiusAlignedScreen=" << QPointFToString (posX0YRadiusAlignedScreen).toLatin1().data()
277  << " transformAlign=" << QTransformToString (transformAlign).toLatin1().data();
278 }
279 
280 QGraphicsItem *GridLineFactory::ellipseItem (const Transformation &transformation,
281  double radiusLinearCartesian,
282  const QPointF &posStartScreen,
283  const QPointF &posEndScreen) const
284 {
285  // LOG4CPP_INFO_S is below
286 
287  QPointF posStartGraph, posEndGraph;
288 
289  transformation.transformScreenToRawGraph (posStartScreen,
290  posStartGraph);
291  transformation.transformScreenToRawGraph (posEndScreen,
292  posEndGraph);
293 
294  // Get the angles about the origin of the start and end points
295  double angleStart = posStartGraph.x() * DEGREES_TO_RADIANS;
296  double angleEnd = posEndGraph.x() * DEGREES_TO_RADIANS;
297  if (angleEnd < angleStart) {
298  angleEnd += TWO_PI;
299  }
300  double angleSpan = angleEnd - angleStart;
301 
302  // Get origin
303  QPointF posOriginGraph (0, 0), posOriginScreen;
304  transformation.transformLinearCartesianGraphToScreen (posOriginGraph,
305  posOriginScreen);
306 
307  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::ellipseItem"
308  << " radiusLinearCartesian=" << radiusLinearCartesian
309  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
310  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
311  << " posOriginScreen=" << QPointFToString (posOriginScreen).toLatin1().data()
312  << " angleStart=" << angleStart / DEGREES_TO_RADIANS
313  << " angleEnd=" << angleEnd / DEGREES_TO_RADIANS
314  << " transformation=" << transformation;
315 
316  // Compute rotate/shear transform that aligns linear cartesian graph coordinates with screen coordinates, and ellipse parameters.
317  // Transform does not include scaling since that messes up the thickness of the drawn line, and does not include
318  // translation since that is not important
319  double ellipseXAxis, ellipseYAxis;
320  QTransform transformAlign;
321  createTransformAlign (transformation,
322  radiusLinearCartesian,
323  posOriginScreen,
324  transformAlign,
325  ellipseXAxis,
326  ellipseYAxis);
327 
328  // Create a circle in graph space with the specified radius
329  QRectF boundingRect (-1.0 * ellipseXAxis + posOriginScreen.x(),
330  -1.0 * ellipseYAxis + posOriginScreen.y(),
331  2 * ellipseXAxis,
332  2 * ellipseYAxis);
333  GraphicsArcItem *item = new GraphicsArcItem (boundingRect);
334  item->setStartAngle (qFloor (angleStart * RADIANS_TO_TICS));
335  item->setSpanAngle (qFloor (angleSpan * RADIANS_TO_TICS));
336 
337  item->setTransform (transformAlign.transposed ().inverted ());
338 
339  return item;
340 }
341 
342 void GridLineFactory::finishActiveGridLine (const QPointF &posStartScreen,
343  const QPointF &posEndScreen,
344  double yFrom,
345  double yTo,
346  const Transformation &transformation,
347  GridLine &gridLine) const
348 {
349  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::finishActiveGridLine"
350  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
351  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
352  << " yFrom=" << yFrom
353  << " yTo=" << yTo;
354 
355  QGraphicsItem *item;
356  if ((m_modelCoords.coordsType() == COORDS_TYPE_POLAR) &&
357  (yFrom == yTo)) {
358 
359  // Linear cartesian radius
360  double radiusLinearCartesian = yFrom;
361  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LOG) {
362  radiusLinearCartesian = transformation.logToLinearRadius(yFrom,
363  m_modelCoords.originRadius());
364  } else {
365  radiusLinearCartesian -= m_modelCoords.originRadius();
366  }
367 
368  // Draw along an arc since this is a side of constant radius, and we have polar coordinates
369  item = ellipseItem (transformation,
370  radiusLinearCartesian,
371  posStartScreen,
372  posEndScreen);
373 
374  } else {
375 
376  // Draw straight line
377  item = lineItem (posStartScreen,
378  posEndScreen);
379  }
380 
381  gridLine.add (item);
382  bindItemToScene (item);
383 }
384 
385 QGraphicsItem *GridLineFactory::lineItem (const QPointF &posStartScreen,
386  const QPointF &posEndScreen) const
387 {
388  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::lineItem"
389  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
390  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data();
391 
392  return new QGraphicsLineItem (QLineF (posStartScreen,
393  posEndScreen));
394 }
395 
396 double GridLineFactory::minScreenDistanceFromPoints (const QPointF &posScreen)
397 {
398  double minDistance = 0;
399  for (int i = 0; i < m_pointsToIsolate.count (); i++) {
400  const Point &pointCenter = m_pointsToIsolate.at (i);
401 
402  double dx = posScreen.x() - pointCenter.posScreen().x();
403  double dy = posScreen.y() - pointCenter.posScreen().y();
404 
405  double distance = qSqrt (dx * dx + dy * dy);
406  if (i == 0 || distance < minDistance) {
407  minDistance = distance;
408  }
409  }
410 
411  return minDistance;
412 }
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:25
QPointF posScreen() const
Accessor for screen position.
Definition: Point.cpp:404
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:19
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.