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