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  gridLineLimiter.limitForYRadius (document,
184  transformation,
185  m_modelCoords,
186  modelMainWindow,
187  modelGridDisplay,
188  startY,
189  stepY);
190 
191  // Apply if possible
192  bool isLinearX = (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LINEAR);
193  bool isLinearY = (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LINEAR);
194  if (stepX > (isLinearX ? 0 : 1) &&
195  stepY > (isLinearY ? 0 : 1) &&
196  (isLinearX || (startX > 0)) &&
197  (isLinearY || (startY > 0))) {
198 
199  QColor color (ColorPaletteToQColor (modelGridDisplay.paletteColor()));
200  QPen pen (QPen (color,
201  GRID_LINE_WIDTH,
202  GRID_LINE_STYLE));
203 
204  for (double x = startX; x <= stopX; (isLinearX ? x += stepX : x *= stepX)) {
205 
206  GridLine *gridLine = createGridLine (x, startY, x, stopY, transformation);
207  gridLine->setPen (pen);
208  gridLines.add (gridLine);
209  }
210 
211  for (double y = startY; y <= stopY; (isLinearY ? y += stepY : y *= stepY)) {
212 
213  GridLine *gridLine = createGridLine (startX, y, stopX, y, transformation);
214  gridLine->setPen (pen);
215  gridLines.add (gridLine);
216  }
217  }
218  }
219 }
220 
221 void GridLineFactory::createTransformAlign (const Transformation &transformation,
222  double radiusLinearCartesian,
223  const QPointF &posOriginScreen,
224  QTransform &transformAlign,
225  double &ellipseXAxis,
226  double &ellipseYAxis) const
227 {
228  // LOG4CPP_INFO_S is below
229 
230  // Compute a minimal transformation that aligns the graph x and y axes with the screen x and y axes. Specifically, shear,
231  // translation and rotation are allowed but not scaling. Scaling is bad since it messes up the line thickness of the drawn arc.
232  //
233  // Assumptions:
234  // 1) Keep the graph origin at the same screen coordinates
235  // 2) Keep the (+radius,0) the same pixel distance from the origin but moved to the same pixel row as the origin
236  // 3) Keep the (0,+radius) the same pixel distance from the origin but moved to the same pixel column as the origin
237 
238  // Get (+radius,0) and (0,+radius) points
239  QPointF posXRadiusY0Graph (radiusLinearCartesian, 0), posX0YRadiusGraph (0, radiusLinearCartesian);
240  QPointF posXRadiusY0Screen, posX0YRadiusScreen;
241  transformation.transformLinearCartesianGraphToScreen (posXRadiusY0Graph,
242  posXRadiusY0Screen);
243  transformation.transformLinearCartesianGraphToScreen (posX0YRadiusGraph,
244  posX0YRadiusScreen);
245 
246  // Compute arc/ellipse parameters
247  QPointF deltaXRadiusY0 = posXRadiusY0Screen - posOriginScreen;
248  QPointF deltaX0YRadius = posX0YRadiusScreen - posOriginScreen;
249  ellipseXAxis = qSqrt (deltaXRadiusY0.x () * deltaXRadiusY0.x () +
250  deltaXRadiusY0.y () * deltaXRadiusY0.y ());
251  ellipseYAxis = qSqrt (deltaX0YRadius.x () * deltaX0YRadius.x () +
252  deltaX0YRadius.y () * deltaX0YRadius.y ());
253 
254  // Compute the aligned coordinates, constrained by the rules listed above
255  QPointF posXRadiusY0AlignedScreen (posOriginScreen.x() + ellipseXAxis, posOriginScreen.y());
256  QPointF posX0YRadiusAlignedScreen (posOriginScreen.x(), posOriginScreen.y() - ellipseYAxis);
257 
258  transformAlign = Transformation::calculateTransformFromLinearCartesianPoints (posOriginScreen,
259  posXRadiusY0Screen,
260  posX0YRadiusScreen,
261  posOriginScreen,
262  posXRadiusY0AlignedScreen,
263  posX0YRadiusAlignedScreen);
264 
265  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::createTransformAlign"
266  << " transformation=" << QTransformToString (transformation.transformMatrix()).toLatin1().data() << endl
267  << " radiusLinearCartesian=" << radiusLinearCartesian
268  << " posXRadiusY0Screen=" << QPointFToString (posXRadiusY0Screen).toLatin1().data()
269  << " posX0YRadiusScreen=" << QPointFToString (posX0YRadiusScreen).toLatin1().data()
270  << " ellipseXAxis=" << ellipseXAxis
271  << " ellipseYAxis=" << ellipseYAxis
272  << " posXRadiusY0AlignedScreen=" << QPointFToString (posXRadiusY0AlignedScreen).toLatin1().data()
273  << " posX0YRadiusAlignedScreen=" << QPointFToString (posX0YRadiusAlignedScreen).toLatin1().data()
274  << " transformAlign=" << QTransformToString (transformAlign).toLatin1().data();
275 }
276 
277 QGraphicsItem *GridLineFactory::ellipseItem (const Transformation &transformation,
278  double radiusLinearCartesian,
279  const QPointF &posStartScreen,
280  const QPointF &posEndScreen) const
281 {
282  // LOG4CPP_INFO_S is below
283 
284  QPointF posStartGraph, posEndGraph;
285 
286  transformation.transformScreenToRawGraph (posStartScreen,
287  posStartGraph);
288  transformation.transformScreenToRawGraph (posEndScreen,
289  posEndGraph);
290 
291  // Get the angles about the origin of the start and end points
292  double angleStart = posStartGraph.x() * DEGREES_TO_RADIANS;
293  double angleEnd = posEndGraph.x() * DEGREES_TO_RADIANS;
294  if (angleEnd < angleStart) {
295  angleEnd += TWO_PI;
296  }
297  double angleSpan = angleEnd - angleStart;
298 
299  // Get origin
300  QPointF posOriginGraph (0, 0), posOriginScreen;
301  transformation.transformLinearCartesianGraphToScreen (posOriginGraph,
302  posOriginScreen);
303 
304  LOG4CPP_INFO_S ((*mainCat)) << "GridLineFactory::ellipseItem"
305  << " radiusLinearCartesian=" << radiusLinearCartesian
306  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
307  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
308  << " posOriginScreen=" << QPointFToString (posOriginScreen).toLatin1().data()
309  << " angleStart=" << angleStart / DEGREES_TO_RADIANS
310  << " angleEnd=" << angleEnd / DEGREES_TO_RADIANS
311  << " transformation=" << transformation;
312 
313  // Compute rotate/shear transform that aligns linear cartesian graph coordinates with screen coordinates, and ellipse parameters.
314  // Transform does not include scaling since that messes up the thickness of the drawn line, and does not include
315  // translation since that is not important
316  double ellipseXAxis, ellipseYAxis;
317  QTransform transformAlign;
318  createTransformAlign (transformation,
319  radiusLinearCartesian,
320  posOriginScreen,
321  transformAlign,
322  ellipseXAxis,
323  ellipseYAxis);
324 
325  // Create a circle in graph space with the specified radius
326  QRectF boundingRect (-1.0 * ellipseXAxis + posOriginScreen.x(),
327  -1.0 * ellipseYAxis + posOriginScreen.y(),
328  2 * ellipseXAxis,
329  2 * ellipseYAxis);
330  GraphicsArcItem *item = new GraphicsArcItem (boundingRect);
331  item->setStartAngle (angleStart * RADIANS_TO_TICS);
332  item->setSpanAngle (angleSpan * RADIANS_TO_TICS);
333 
334  item->setTransform (transformAlign.transposed ().inverted ());
335 
336  return item;
337 }
338 
339 void GridLineFactory::finishActiveGridLine (const QPointF &posStartScreen,
340  const QPointF &posEndScreen,
341  double yFrom,
342  double yTo,
343  const Transformation &transformation,
344  GridLine &gridLine) const
345 {
346  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::finishActiveGridLine"
347  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
348  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data()
349  << " yFrom=" << yFrom
350  << " yTo=" << yTo;
351 
352  QGraphicsItem *item;
353  if ((m_modelCoords.coordsType() == COORDS_TYPE_POLAR) &&
354  (yFrom == yTo)) {
355 
356  // Linear cartesian radius
357  double radiusLinearCartesian = yFrom;
358  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LOG) {
359  radiusLinearCartesian = transformation.logToLinearRadius(yFrom,
360  m_modelCoords.originRadius());
361  } else {
362  radiusLinearCartesian -= m_modelCoords.originRadius();
363  }
364 
365  // Draw along an arc since this is a side of constant radius, and we have polar coordinates
366  item = ellipseItem (transformation,
367  radiusLinearCartesian,
368  posStartScreen,
369  posEndScreen);
370 
371  } else {
372 
373  // Draw straight line
374  item = lineItem (posStartScreen,
375  posEndScreen);
376  }
377 
378  gridLine.add (item);
379  bindItemToScene (item);
380 }
381 
382 QGraphicsItem *GridLineFactory::lineItem (const QPointF &posStartScreen,
383  const QPointF &posEndScreen) const
384 {
385  LOG4CPP_DEBUG_S ((*mainCat)) << "GridLineFactory::lineItem"
386  << " posStartScreen=" << QPointFToString (posStartScreen).toLatin1().data()
387  << " posEndScreen=" << QPointFToString (posEndScreen).toLatin1().data();
388 
389  return new QGraphicsLineItem (QLineF (posStartScreen,
390  posEndScreen));
391 }
392 
393 double GridLineFactory::minScreenDistanceFromPoints (const QPointF &posScreen)
394 {
395  double minDistance = 0;
396  for (int i = 0; i < m_pointsToIsolate.count (); i++) {
397  const Point &pointCenter = m_pointsToIsolate.at (i);
398 
399  double dx = posScreen.x() - pointCenter.posScreen().x();
400  double dy = posScreen.y() - pointCenter.posScreen().y();
401 
402  double distance = qSqrt (dx * dx + dy * dy);
403  if (i == 0 || distance < minDistance) {
404  minDistance = distance;
405  }
406  }
407 
408  return minDistance;
409 }
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.
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.
void limitForYRadius(const Document &document, const Transformation &transformation, const DocumentModelCoords &modelCoords, const MainWindowModel &modelMainWindow, const DocumentModelGridDisplay &modelGrid, double &startY, double &stepY) const
Limit step value for y/range coordinate. This is a noop if the maximum grid line limit in MainWindowM...
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
void limitForXTheta(const Document &document, const Transformation &transformation, const DocumentModelCoords &modelCoords, const MainWindowModel &modelMainWindow, const DocumentModelGridDisplay &modelGrid, double &startX, double &stepX) const
Limit step value for x/theta coordinate. This is a noop if the maximum grid line limit in MainWindowM...
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.