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
GridLineFactory(QGraphicsScene & scene,const DocumentModelCoords & modelCoords)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
GridLineFactory(QGraphicsScene & scene,int pointRadius,const QList<Point> & pointsToIsolate,const DocumentModelCoords & modelCoords)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
bindItemToScene(QGraphicsItem * item) const60 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
createGridLine(double xFrom,double yFrom,double xTo,double yTo,const Transformation & transformation)73 GridLine *GridLineFactory::createGridLine (double xFrom,
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
createGridLinesForEvenlySpacedGrid(const DocumentModelGridDisplay & modelGridDisplay,const Document & document,const MainWindowModel & modelMainWindow,const Transformation & transformation,GridLines & gridLines)155 void GridLineFactory::createGridLinesForEvenlySpacedGrid (const DocumentModelGridDisplay &modelGridDisplay,
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
createTransformAlign(const Transformation & transformation,double radiusLinearCartesian,const QPointF & posOriginScreen,QTransform & transformAlign,double & ellipseXAxis,double & ellipseYAxis) const223 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
ellipseItem(const Transformation & transformation,double radiusLinearCartesian,const QPointF & posStartScreen,const QPointF & posEndScreen) const280 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
finishActiveGridLine(const QPointF & posStartScreen,const QPointF & posEndScreen,double yFrom,double yTo,const Transformation & transformation,GridLine & gridLine) const342 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
lineItem(const QPointF & posStartScreen,const QPointF & posEndScreen) const385 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
minScreenDistanceFromPoints(const QPointF & posScreen)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 }
413