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