Engauge Digitizer  2
CallbackAxisPointsAbstract.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 "CallbackAxisPointsAbstract.h"
8 #include "EngaugeAssert.h"
9 #include "Logger.h"
10 #include "Point.h"
11 #include <qmath.h>
12 #include "QtToString.h"
13 #include "Transformation.h"
14 
15 // Epsilon test values
16 const double ONE_PIXEL = 1.0; // Screen coordinates
17 const double ZERO_EPSILON = 0.0; // Graph coordinates
18 
20  DocumentAxesPointsRequired documentAxesPointsRequired) :
21  m_modelCoords (modelCoords),
22  m_isError (false),
23  m_documentAxesPointsRequired (documentAxesPointsRequired)
24 {
25 }
26 
28  const QString pointIdentifierOverride,
29  const QPointF &posScreenOverride,
30  const QPointF &posGraphOverride,
31  DocumentAxesPointsRequired documentAxesPointsRequired) :
32  m_modelCoords (modelCoords),
33  m_pointIdentifierOverride (pointIdentifierOverride),
34  m_posScreenOverride (posScreenOverride),
35  m_posGraphOverride (posGraphOverride),
36  m_isError (false),
37  m_documentAxesPointsRequired (documentAxesPointsRequired)
38 {
39 }
40 
41 bool CallbackAxisPointsAbstract::anyPointsRepeatPair (const CoordPairVector &vector,
42  double epsilon) const
43 {
44  for (int pointLeft = 0; pointLeft < vector.count(); pointLeft++) {
45  for (int pointRight = pointLeft + 1; pointRight < vector.count(); pointRight++) {
46 
47  if (qAbs (vector.at(pointLeft).x() - vector.at(pointRight).x()) <= epsilon &&
48  qAbs (vector.at(pointLeft).y() - vector.at(pointRight).y()) <= epsilon) {
49 
50  // Points pointLeft and pointRight repeat each other, which means matrix cannot be inverted
51  return true;
52  }
53  }
54  }
55 
56  // No columns repeat
57  return false;
58 }
59 
60 bool CallbackAxisPointsAbstract::anyPointsRepeatSingle (const CoordSingleVector &vector,
61  double epsilon) const
62 {
63  for (int pointLeft = 0; pointLeft < vector.count(); pointLeft++) {
64  for (int pointRight = pointLeft + 1; pointRight < vector.count(); pointRight++) {
65 
66  if (qAbs (vector.at(pointLeft) - vector.at(pointRight)) <= epsilon) {
67 
68  // Points pointLeft and pointRight repeat each other, which means matrix cannot be inverted
69  return true;
70  }
71  }
72  }
73 
74  // No columns repeat
75  return false;
76 }
77 
78 CallbackSearchReturn CallbackAxisPointsAbstract::callback (const QString & /* curveName */,
79  const Point &point)
80 {
81  QPointF posScreen = point.posScreen ();
82  QPointF posGraph = point.posGraph ();
83 
84  if (m_pointIdentifierOverride == point.identifier ()) {
85 
86  // Override the old point coordinates with its new (if all tests are passed) coordinates
87  posScreen = m_posScreenOverride;
88  posGraph = m_posGraphOverride;
89  }
90 
91  // Try to compute transform
92  if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2) {
93  return callbackRequire2AxisPoints (posScreen,
94  posGraph);
95  } else if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) {
96  return callbackRequire3AxisPoints (posScreen,
97  posGraph);
98  } else {
99  return callbackRequire4AxisPoints (point.isXOnly(),
100  posScreen,
101  posGraph);
102  }
103 }
104 
105 CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire2AxisPoints (const QPointF &posScreen,
106  const QPointF &posGraph)
107 {
108  CallbackSearchReturn rtn = CALLBACK_SEARCH_RETURN_CONTINUE;
109 
110  // Update range variables. The same nonzero length value is stored in every x and y coordinate of every axis point
111  m_xGraphLow = 0;
112  m_yGraphLow = 0;
113  m_xGraphHigh = posGraph.x();
114  m_yGraphHigh = posGraph.x();
115 
116  int numberPoints = m_screenInputs.count();
117  if (numberPoints < 2) {
118 
119  // Append new point
120  m_screenInputs.push_back (posScreen);
121  m_graphOutputs.push_back (posGraph);
122  numberPoints = m_screenInputs.count();
123 
124  if (numberPoints == 2) {
125  loadTransforms2 ();
126  }
127 
128  // Error checking
129  if (anyPointsRepeatPair (m_screenInputs,
130  ONE_PIXEL)) {
131 
132  m_isError = true;
133  m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
134  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
135 
136  }
137  }
138 
139  if (m_screenInputs.count() > 1) {
140 
141  // There are enough axis points so quit
142  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
143 
144  }
145 
146  return rtn;
147 }
148 
149 CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire3AxisPoints (const QPointF &posScreen,
150  const QPointF &posGraph)
151 {
152  CallbackSearchReturn rtn = CALLBACK_SEARCH_RETURN_CONTINUE;
153 
154  // Update range variables
155  int numberPoints = m_screenInputs.count();
156  if ((numberPoints == 0) || (posGraph.x () < m_xGraphLow)) { m_xGraphLow = posGraph.x (); }
157  if ((numberPoints == 0) || (posGraph.y () < m_yGraphLow)) { m_yGraphLow = posGraph.y (); }
158  if ((numberPoints == 0) || (posGraph.x () > m_xGraphHigh)) { m_xGraphHigh = posGraph.x (); }
159  if ((numberPoints == 0) || (posGraph.y () > m_yGraphHigh)) { m_yGraphHigh = posGraph.y (); }
160 
161  if (numberPoints < 3) {
162 
163  // Append new point
164  m_screenInputs.push_back (posScreen);
165  m_graphOutputs.push_back (posGraph);
166  numberPoints = m_screenInputs.count();
167 
168  if (numberPoints == 3) {
169  loadTransforms3 ();
170  }
171 
172  // Error checking
173  if ((m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2 ||
174  m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) &&
175  anyPointsRepeatPair (m_screenInputs, ONE_PIXEL)) {
176 
177  m_isError = true;
178  m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
179  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
180 
181  } else if ((m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2 ||
182  m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) &&
183  anyPointsRepeatPair (m_graphOutputs, ZERO_EPSILON)) {
184 
185  m_isError = true;
186  m_errorMessage = QObject::tr ("New axis point cannot have the same graph coordinates as an existing axis point");
187  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
188 
189  } else if ((numberPoints == 3) && threePointsAreCollinear (m_screenInputsTransform)) {
190 
191  m_isError = true;
192  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
193  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
194 
195  } else if ((numberPoints == 3) && threePointsAreCollinear (m_graphOutputsTransform)) {
196 
197  m_isError = true;
198  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
199  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
200 
201  }
202  }
203 
204  if (m_screenInputs.count() > 2) {
205 
206  // There are enough axis points so quit
207  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
208 
209  }
210 
211  return rtn;
212 }
213 
214 CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire4AxisPoints (bool isXOnly,
215  const QPointF &posScreen,
216  const QPointF &posGraph)
217 {
218  CallbackSearchReturn rtn = CALLBACK_SEARCH_RETURN_CONTINUE;
219 
220  // Update range variables
221  int numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
222  if ((numberPoints == 0) || (posGraph.x () < m_xGraphLow)) { m_xGraphLow = posGraph.x (); }
223  if ((numberPoints == 0) || (posGraph.y () < m_yGraphLow)) { m_yGraphLow = posGraph.y (); }
224  if ((numberPoints == 0) || (posGraph.x () > m_xGraphHigh)) { m_xGraphHigh = posGraph.x (); }
225  if ((numberPoints == 0) || (posGraph.y () > m_yGraphHigh)) { m_yGraphHigh = posGraph.y (); }
226 
227  if (numberPoints < 4) {
228 
229  // Append the new point
230  if (isXOnly) {
231 
232  m_screenInputsX.push_back (posScreen);
233  m_graphOutputsX.push_back (posGraph.x());
234 
235  } else {
236 
237  m_screenInputsY.push_back (posScreen);
238  m_graphOutputsY.push_back (posGraph.y());
239 
240  }
241 
242  numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
243  if (numberPoints == 4) {
244  loadTransforms4 ();
245  }
246  }
247 
248  if (m_screenInputsX.count() > 2) {
249 
250  m_isError = true;
251  m_errorMessage = QObject::tr ("Too many x axis points. There should only be two");
252  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
253 
254  } else if (m_screenInputsY.count() > 2) {
255 
256  m_isError = true;
257  m_errorMessage = QObject::tr ("Too many y axis points. There should only be two");
258  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
259 
260  } else {
261 
262  if ((m_screenInputsX.count() == 2) &&
263  (m_screenInputsY.count() == 2)) {
264 
265  // Done, although an error may intrude
266  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
267  }
268 
269  // Error checking
270  if (anyPointsRepeatPair (m_screenInputsX,
271  ONE_PIXEL) ||
272  anyPointsRepeatPair (m_screenInputsY,
273  ONE_PIXEL)) {
274 
275  m_isError = true;
276  m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
277  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
278 
279  } else if (anyPointsRepeatSingle (m_graphOutputsX,
280  ZERO_EPSILON) ||
281  anyPointsRepeatSingle (m_graphOutputsY,
282  ZERO_EPSILON)) {
283 
284  m_isError = true;
285  m_errorMessage = QObject::tr ("New axis point cannot have the same graph coordinates as an existing axis point");
286  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
287 
288  } else if ((numberPoints == 4) && threePointsAreCollinear (m_screenInputsTransform)) {
289 
290  m_isError = true;
291  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
292  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
293 
294  } else if ((numberPoints == 4) && threePointsAreCollinear (m_graphOutputsTransform)) {
295 
296  m_isError = true;
297  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
298  rtn = CALLBACK_SEARCH_RETURN_INTERRUPT;
299 
300  }
301  }
302 
303  return rtn;
304 }
305 
307 {
308  return m_documentAxesPointsRequired;
309 }
310 
311 void CallbackAxisPointsAbstract::loadTransforms2 ()
312 {
313  // To get a third point from two existing points we compute the vector between the first 2 points and then take
314  // the cross product with the out-of-plane unit vector to get the perpendicular vector, and the endpoint of that
315  // is used as the third point. This implicitly assumes that the graph-to-screen coordinates scaling is the
316  // same in both directions. The advantage of this approach is that no assumptions are made about the inputs
317 
318  double d0To1ScreenX = m_screenInputs.at (1).x () - m_screenInputs.at (0).x ();
319  double d0To1ScreenY = m_screenInputs.at (1).y () - m_screenInputs.at (0).y ();
320  double d0To1ScreenZ = 0;
321  double d0To1GraphX = m_graphOutputs.at (1).x () - m_graphOutputs.at (0).x ();
322  double d0To1GraphY = m_graphOutputs.at (1).y () - m_graphOutputs.at (0).y ();
323  double d0To1GraphZ = 0;
324 
325  double unitNormalX = 0;
326  double unitNormalY = 0;
327  double unitNormalZ = 1;
328 
329  double d0To2ScreenX = unitNormalY * d0To1ScreenZ - unitNormalZ * d0To1ScreenY;
330  double d0To2ScreenY = unitNormalZ * d0To1ScreenX - unitNormalX * d0To1ScreenZ;
331  double d0To2GraphX = unitNormalY * d0To1GraphZ - unitNormalZ * d0To1GraphY;
332  double d0To2GraphY = unitNormalZ * d0To1GraphX - unitNormalX * d0To1GraphZ;
333 
334  // Hack since +Y for screen coordinates is down but up for graph coordinates. Users expect +Y to be up
335  // so we rotate screen delta by 180 degrees
336  const double FLIP_Y_SCREEN = -1.0;
337 
338  double screenX2 = m_screenInputs.at (0).x () + FLIP_Y_SCREEN * d0To2ScreenX;
339  double screenY2 = m_screenInputs.at (0).y () + FLIP_Y_SCREEN * d0To2ScreenY;
340  double graphX2 = m_graphOutputs.at (0).x () + d0To2GraphX;
341  double graphY2 = m_graphOutputs.at (0).y () + d0To2GraphY;
342 
343  // Screen coordinates
344  m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), screenX2,
345  m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), screenY2,
346  1.0 , 1.0 , 1.0 );
347 
348  // Graph coordinates
349  m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), graphX2,
350  m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), graphY2,
351  1.0 , 1.0 , 1.0 );
352 }
353 
354 void CallbackAxisPointsAbstract::loadTransforms3 ()
355 {
356  // Screen coordinates
357  m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), m_screenInputs.at(2).x(),
358  m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), m_screenInputs.at(2).y(),
359  1.0 , 1.0 , 1.0 );
360 
361  // Graph coordinates
362  m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), m_graphOutputs.at(2).x(),
363  m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), m_graphOutputs.at(2).y(),
364  1.0 , 1.0 , 1.0 );
365 }
366 
367 void CallbackAxisPointsAbstract::loadTransforms4 ()
368 {
369  double x1Screen = m_screenInputsX.at(0).x();
370  double y1Screen = m_screenInputsX.at(0).y();
371  double x2Screen = m_screenInputsX.at(1).x();
372  double y2Screen = m_screenInputsX.at(1).y();
373  double x3Screen = m_screenInputsY.at(0).x();
374  double y3Screen = m_screenInputsY.at(0).y();
375  double x4Screen = m_screenInputsY.at(1).x();
376  double y4Screen = m_screenInputsY.at(1).y();
377 
378  // Each of the four axes points has only one coordinate
379  double x1Graph = m_graphOutputsX.at(0);
380  double x2Graph = m_graphOutputsX.at(1);
381  double y3Graph = m_graphOutputsY.at(0);
382  double y4Graph = m_graphOutputsY.at(1);
383 
384  // Intersect the two lines of the two axes. The lines are defined parametrically for the screen coordinates, with
385  // points 1 and 2 on the x axis and points 3 and 4 on the y axis, as:
386  // x = (1 - sx) * x1 + sx * x2
387  // y = (1 - sx) * y1 + sx * y2
388  // x = (1 - sy) * x3 + sy * x4
389  // y = (1 - sy) * y3 + sy * y4
390  // Intersection of the 2 lines is at (x,y). Solving for sx and sy using Cramer's rule where Ax=b
391  // (x1 - x3) (x1 - x2 x4 - x3) (sx)
392  // (y1 - y3) = (y1 - y2 y4 - y3) (sy)
393  double A00 = x1Screen - x2Screen;
394  double A01 = x4Screen - x3Screen;
395  double A10 = y1Screen - y2Screen;
396  double A11 = y4Screen - y3Screen;
397  double b0 = x1Screen - x3Screen;
398  double b1 = y1Screen - y3Screen;
399  double numeratorx = (b0 * A11 - A01 * b1);
400  double numeratory = (A00 * b1 - b0 * A10);
401  double denominator = (A00 * A11 - A01 * A10);
402  double sx = numeratorx / denominator;
403  double sy = numeratory / denominator;
404 
405  // Intersection point. For the graph coordinates, the initial implementation assumes cartesian coordinates
406  double xIntScreen = (1.0 - sx) * x1Screen + sx * x2Screen;
407  double yIntScreen = (1.0 - sy) * y3Screen + sy * y4Screen;
408  double xIntGraph, yIntGraph;
409  if (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LINEAR) {
410  xIntGraph = (1.0 - sx) * x1Graph + sx * x2Graph;
411  } else {
412  xIntGraph = qExp ((1.0 - sx) * qLn (x1Graph) + sx * qLn (x2Graph));
413  }
414  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LINEAR) {
415  yIntGraph = (1.0 - sy) * y3Graph + sy * y4Graph;
416  } else {
417  yIntGraph = qExp ((1.0 - sy) * qLn (y3Graph) + sy * qLn (y4Graph));
418  }
419 
420  // Distances of 4 axis points from interception
421  double distance1 = qSqrt ((x1Screen - xIntScreen) * (x1Screen - xIntScreen) +
422  (y1Screen - yIntScreen) * (y1Screen - yIntScreen));
423  double distance2 = qSqrt ((x2Screen - xIntScreen) * (x2Screen - xIntScreen) +
424  (y2Screen - yIntScreen) * (y2Screen - yIntScreen));
425  double distance3 = qSqrt ((x3Screen - xIntScreen) * (x3Screen - xIntScreen) +
426  (y3Screen - yIntScreen) * (y3Screen - yIntScreen));
427  double distance4 = qSqrt ((x4Screen - xIntScreen) * (x4Screen - xIntScreen) +
428  (y4Screen - yIntScreen) * (y4Screen - yIntScreen));
429 
430  // We now have too many data points with both x and y coordinates:
431  // (xInt,yInt) (xInt,y3) (xInt,y4) (x1,yInt) (x2,yInt)
432  // so we pick just 3, making sure that those 3 are widely separated
433  // (xInt,yInt) (x axis point furthest from xInt,yInt) (y axis point furthest from xInt,yInt)
434  double xFurthestXAxisScreen, yFurthestXAxisScreen, xFurthestYAxisScreen, yFurthestYAxisScreen;
435  double xFurthestXAxisGraph, yFurthestXAxisGraph, xFurthestYAxisGraph, yFurthestYAxisGraph;
436  if (distance1 < distance2) {
437  xFurthestXAxisScreen = x2Screen;
438  yFurthestXAxisScreen = y2Screen;
439  xFurthestXAxisGraph = x2Graph;
440  yFurthestXAxisGraph = yIntGraph;
441  } else {
442  xFurthestXAxisScreen = x1Screen;
443  yFurthestXAxisScreen = y1Screen;
444  xFurthestXAxisGraph = x1Graph;
445  yFurthestXAxisGraph = yIntGraph;
446  }
447  if (distance3 < distance4) {
448  xFurthestYAxisScreen = x4Screen;
449  yFurthestYAxisScreen = y4Screen;
450  xFurthestYAxisGraph = xIntGraph;
451  yFurthestYAxisGraph = y4Graph;
452  } else {
453  xFurthestYAxisScreen = x3Screen;
454  yFurthestYAxisScreen = y3Screen;
455  xFurthestYAxisGraph = xIntGraph;
456  yFurthestYAxisGraph = y3Graph;
457  }
458 
459  // Screen coordinates
460  m_screenInputsTransform = QTransform (xIntScreen, xFurthestXAxisScreen, xFurthestYAxisScreen,
461  yIntScreen, yFurthestXAxisScreen, yFurthestYAxisScreen,
462  1.0 , 1.0 , 1.0 );
463 
464  // Graph coordinates
465  m_graphOutputsTransform = QTransform (xIntGraph, xFurthestXAxisGraph, xFurthestYAxisGraph,
466  yIntGraph, yFurthestXAxisGraph, yFurthestYAxisGraph,
467  1.0 , 1.0 , 1.0 );
468 }
469 
471 {
472  return m_graphOutputsTransform;
473 }
474 
476 {
477  return m_screenInputsTransform;
478 }
479 
481 {
482  if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2) {
483  return unsigned (m_screenInputs.count());
484  } else if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) {
485  return unsigned (m_screenInputs.count());
486  } else {
487  return unsigned (m_screenInputsX.count() + m_screenInputsY.count());
488  }
489 }
490 
491 bool CallbackAxisPointsAbstract::threePointsAreCollinear (const QTransform &transform)
492 {
493  return !transform.isInvertible ();
494 }
bool isXOnly() const
In DOCUMENT_AXES_POINTS_REQUIRED_4 modes, this is true/false if y/x coordinate is undefined.
Definition: Point.cpp:286
QTransform matrixScreen() const
Returns screen coordinates matrix after transformIsDefined has already indicated success.
Class that represents one digitized point. The screen-to-graph coordinate transformation is always ex...
Definition: Point.h:25
QPointF posScreen() const
Accessor for screen position.
Definition: Point.cpp:404
unsigned int numberAxisPoints() const
Number of axis points which is less than 3 if the axes curve is incomplete.
QString identifier() const
Unique identifier for a specific Point.
Definition: Point.cpp:268
CoordScale coordScaleXTheta() const
Get method for linear/log scale on x/theta.
QPointF posGraph(ApplyHasCheck applyHasCheck=KEEP_HAS_CHECK) const
Accessor for graph position. Skip check if copying one instance to another.
Definition: Point.cpp:395
Model for DlgSettingsCoords and CmdSettingsCoords.
CoordScale coordScaleYRadius() const
Get method for linear/log scale on y/radius.
CallbackAxisPointsAbstract(const DocumentModelCoords &modelCoords, DocumentAxesPointsRequired documentAxesPointsRequired)
Constructor for when all of the existing axis points are to be processed as is.
QTransform matrixGraph() const
Returns graph coordinates matrix after transformIsDefined has already indicated success.
DocumentAxesPointsRequired documentAxesPointsRequired() const
Number of axes points required for the transformation.
CallbackSearchReturn callback(const QString &curveName, const Point &point)
Callback method.