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