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