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 
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 {
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");
128 
129  }
130  }
131 
132  if (m_screenInputs.count() > 1) {
133 
134  // There are enough axis points so quit
136 
137  }
138 
139  return rtn;
140 }
141 
142 CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire3AxisPoints (const QPointF &posScreen,
143  const QPointF &posGraph)
144 {
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 (anyPointsRepeatPair (m_screenInputs)) {
167 
168  m_isError = true;
169  m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
171 
172  } else if (anyPointsRepeatPair (m_graphOutputs)) {
173 
174  m_isError = true;
175  m_errorMessage = QObject::tr ("New axis point cannot have the same graph coordinates as an existing axis point");
177 
178  } else if ((numberPoints == 3) && threePointsAreCollinear (m_screenInputsTransform)) {
179 
180  m_isError = true;
181  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
183 
184  } else if ((numberPoints == 3) && threePointsAreCollinear (m_graphOutputsTransform)) {
185 
186  m_isError = true;
187  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
189 
190  }
191  }
192 
193  if (m_screenInputs.count() > 2) {
194 
195  // There are enough axis points so quit
197 
198  }
199 
200  return rtn;
201 }
202 
203 CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire4AxisPoints (bool isXOnly,
204  const QPointF &posScreen,
205  const QPointF &posGraph)
206 {
208 
209  // Update range variables
210  int numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
211  if ((numberPoints == 0) || (posGraph.x () < m_xGraphLow)) { m_xGraphLow = posGraph.x (); }
212  if ((numberPoints == 0) || (posGraph.y () < m_yGraphLow)) { m_yGraphLow = posGraph.y (); }
213  if ((numberPoints == 0) || (posGraph.x () > m_xGraphHigh)) { m_xGraphHigh = posGraph.x (); }
214  if ((numberPoints == 0) || (posGraph.y () > m_yGraphHigh)) { m_yGraphHigh = posGraph.y (); }
215 
216  if (numberPoints < 4) {
217 
218  // Append the new point
219  if (isXOnly) {
220 
221  m_screenInputsX.push_back (posScreen);
222  m_graphOutputsX.push_back (posGraph.x());
223 
224  } else {
225 
226  m_screenInputsY.push_back (posScreen);
227  m_graphOutputsY.push_back (posGraph.y());
228 
229  }
230 
231  numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
232  if (numberPoints == 4) {
233  loadTransforms4 ();
234  }
235  }
236 
237  if (m_screenInputsX.count() > 2) {
238 
239  m_isError = true;
240  m_errorMessage = QObject::tr ("Too many x axis points. There should only be two");
242 
243  } else if (m_screenInputsY.count() > 2) {
244 
245  m_isError = true;
246  m_errorMessage = QObject::tr ("Too many y axis points. There should only be two");
248 
249  } else {
250 
251  if ((m_screenInputsX.count() == 2) &&
252  (m_screenInputsY.count() == 2)) {
253 
254  // Done, although an error may intrude
256  }
257 
258  // Error checking
259  if (anyPointsRepeatPair (m_screenInputsX) ||
260  anyPointsRepeatPair (m_screenInputsY)) {
261 
262  m_isError = true;
263  m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
265 
266  } else if (anyPointsRepeatSingle (m_graphOutputsX) ||
267  anyPointsRepeatSingle (m_graphOutputsY)) {
268 
269  m_isError = true;
270  m_errorMessage = QObject::tr ("New axis point cannot have the same graph coordinates as an existing axis point");
272 
273  } else if ((numberPoints == 4) && threePointsAreCollinear (m_screenInputsTransform)) {
274 
275  m_isError = true;
276  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
278 
279  } else if ((numberPoints == 4) && threePointsAreCollinear (m_graphOutputsTransform)) {
280 
281  m_isError = true;
282  m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
284 
285  }
286  }
287 
288  return rtn;
289 }
290 
292 {
293  return m_documentAxesPointsRequired;
294 }
295 
296 void CallbackAxisPointsAbstract::loadTransforms2 ()
297 {
298  // To get a third point from two existing points we compute the vector between the first 2 points and then take
299  // the cross product with the out-of-plane unit vector to get the perpendicular vector, and the endpoint of that
300  // is used as the third point. This implicitly assumes that the graph-to-screen coordinates scaling is the
301  // same in both directions. The advantage of this approach is that no assumptions are made about the inputs
302 
303  double d0To1ScreenX = m_screenInputs.at (1).x () - m_screenInputs.at (0).x ();
304  double d0To1ScreenY = m_screenInputs.at (1).y () - m_screenInputs.at (0).y ();
305  double d0To1ScreenZ = 0;
306  double d0To1GraphX = m_graphOutputs.at (1).x () - m_graphOutputs.at (0).x ();
307  double d0To1GraphY = m_graphOutputs.at (1).y () - m_graphOutputs.at (0).y ();
308  double d0To1GraphZ = 0;
309 
310  double unitNormalX = 0;
311  double unitNormalY = 0;
312  double unitNormalZ = 1;
313 
314  double d0To2ScreenX = unitNormalY * d0To1ScreenZ - unitNormalZ * d0To1ScreenY;
315  double d0To2ScreenY = unitNormalZ * d0To1ScreenX - unitNormalX * d0To1ScreenZ;
316  double d0To2GraphX = unitNormalY * d0To1GraphZ - unitNormalZ * d0To1GraphY;
317  double d0To2GraphY = unitNormalZ * d0To1GraphX - unitNormalX * d0To1GraphZ;
318 
319  // Hack since +Y for screen coordinates is down but up for graph coordinates. Users expect +Y to be up
320  // so we rotate screen delta by 180 degrees
321  const double FLIP_Y_SCREEN = -1.0;
322 
323  double screenX2 = m_screenInputs.at (0).x () + FLIP_Y_SCREEN * d0To2ScreenX;
324  double screenY2 = m_screenInputs.at (0).y () + FLIP_Y_SCREEN * d0To2ScreenY;
325  double graphX2 = m_graphOutputs.at (0).x () + d0To2GraphX;
326  double graphY2 = m_graphOutputs.at (0).y () + d0To2GraphY;
327 
328  // Screen coordinates
329  m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), screenX2,
330  m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), screenY2,
331  1.0 , 1.0 , 1.0 );
332 
333  // Graph coordinates
334  m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), graphX2,
335  m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), graphY2,
336  1.0 , 1.0 , 1.0 );
337 }
338 
339 void CallbackAxisPointsAbstract::loadTransforms3 ()
340 {
341  // Screen coordinates
342  m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), m_screenInputs.at(2).x(),
343  m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), m_screenInputs.at(2).y(),
344  1.0 , 1.0 , 1.0 );
345 
346  // Graph coordinates
347  m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), m_graphOutputs.at(2).x(),
348  m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), m_graphOutputs.at(2).y(),
349  1.0 , 1.0 , 1.0 );
350 }
351 
352 void CallbackAxisPointsAbstract::loadTransforms4 ()
353 {
354  double x1Screen = m_screenInputsX.at(0).x();
355  double y1Screen = m_screenInputsX.at(0).y();
356  double x2Screen = m_screenInputsX.at(1).x();
357  double y2Screen = m_screenInputsX.at(1).y();
358  double x3Screen = m_screenInputsY.at(0).x();
359  double y3Screen = m_screenInputsY.at(0).y();
360  double x4Screen = m_screenInputsY.at(1).x();
361  double y4Screen = m_screenInputsY.at(1).y();
362 
363  // Each of the four axes points has only one coordinate
364  double x1Graph = m_graphOutputsX.at(0);
365  double x2Graph = m_graphOutputsX.at(1);
366  double y3Graph = m_graphOutputsY.at(0);
367  double y4Graph = m_graphOutputsY.at(1);
368 
369  // Intersect the two lines of the two axes. The lines are defined parametrically for the screen coordinates, with
370  // points 1 and 2 on the x axis and points 3 and 4 on the y axis, as:
371  // x = (1 - sx) * x1 + sx * x2
372  // y = (1 - sx) * y1 + sx * y2
373  // x = (1 - sy) * x3 + sy * x4
374  // y = (1 - sy) * y3 + sy * y4
375  // Intersection of the 2 lines is at (x,y). Solving for sx and sy using Cramer's rule where Ax=b
376  // (x1 - x3) (x1 - x2 x4 - x3) (sx)
377  // (y1 - y3) = (y1 - y2 y4 - y3) (sy)
378  double A00 = x1Screen - x2Screen;
379  double A01 = x4Screen - x3Screen;
380  double A10 = y1Screen - y2Screen;
381  double A11 = y4Screen - y3Screen;
382  double b0 = x1Screen - x3Screen;
383  double b1 = y1Screen - y3Screen;
384  double numeratorx = (b0 * A11 - A01 * b1);
385  double numeratory = (A00 * b1 - b0 * A10);
386  double denominator = (A00 * A11 - A01 * A10);
387  double sx = numeratorx / denominator;
388  double sy = numeratory / denominator;
389 
390  // Intersection point. For the graph coordinates, the initial implementation assumes cartesian coordinates
391  double xIntScreen = (1.0 - sx) * x1Screen + sx * x2Screen;
392  double yIntScreen = (1.0 - sy) * y3Screen + sy * y4Screen;
393  double xIntGraph, yIntGraph;
394  if (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LINEAR) {
395  xIntGraph = (1.0 - sx) * x1Graph + sx * x2Graph;
396  } else {
397  xIntGraph = qExp ((1.0 - sx) * qLn (x1Graph) + sx * qLn (x2Graph));
398  }
399  if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LINEAR) {
400  yIntGraph = (1.0 - sy) * y3Graph + sy * y4Graph;
401  } else {
402  yIntGraph = qExp ((1.0 - sy) * qLn (y3Graph) + sy * qLn (y4Graph));
403  }
404 
405  // Distances of 4 axis points from interception
406  double distance1 = qSqrt ((x1Screen - xIntScreen) * (x1Screen - xIntScreen) +
407  (y1Screen - yIntScreen) * (y1Screen - yIntScreen));
408  double distance2 = qSqrt ((x2Screen - xIntScreen) * (x2Screen - xIntScreen) +
409  (y2Screen - yIntScreen) * (y2Screen - yIntScreen));
410  double distance3 = qSqrt ((x3Screen - xIntScreen) * (x3Screen - xIntScreen) +
411  (y3Screen - yIntScreen) * (y3Screen - yIntScreen));
412  double distance4 = qSqrt ((x4Screen - xIntScreen) * (x4Screen - xIntScreen) +
413  (y4Screen - yIntScreen) * (y4Screen - yIntScreen));
414 
415  // We now have too many data points with both x and y coordinates:
416  // (xInt,yInt) (xInt,y3) (xInt,y4) (x1,yInt) (x2,yInt)
417  // so we pick just 3, making sure that those 3 are widely separated
418  // (xInt,yInt) (x axis point furthest from xInt,yInt) (y axis point furthest from xInt,yInt)
419  double xFurthestXAxisScreen, yFurthestXAxisScreen, xFurthestYAxisScreen, yFurthestYAxisScreen;
420  double xFurthestXAxisGraph, yFurthestXAxisGraph, xFurthestYAxisGraph, yFurthestYAxisGraph;
421  if (distance1 < distance2) {
422  xFurthestXAxisScreen = x2Screen;
423  yFurthestXAxisScreen = y2Screen;
424  xFurthestXAxisGraph = x2Graph;
425  yFurthestXAxisGraph = yIntGraph;
426  } else {
427  xFurthestXAxisScreen = x1Screen;
428  yFurthestXAxisScreen = y1Screen;
429  xFurthestXAxisGraph = x1Graph;
430  yFurthestXAxisGraph = yIntGraph;
431  }
432  if (distance3 < distance4) {
433  xFurthestYAxisScreen = x4Screen;
434  yFurthestYAxisScreen = y4Screen;
435  xFurthestYAxisGraph = xIntGraph;
436  yFurthestYAxisGraph = y4Graph;
437  } else {
438  xFurthestYAxisScreen = x3Screen;
439  yFurthestYAxisScreen = y3Screen;
440  xFurthestYAxisGraph = xIntGraph;
441  yFurthestYAxisGraph = y3Graph;
442  }
443 
444  // Screen coordinates
445  m_screenInputsTransform = QTransform (xIntScreen, xFurthestXAxisScreen, xFurthestYAxisScreen,
446  yIntScreen, yFurthestXAxisScreen, yFurthestYAxisScreen,
447  1.0 , 1.0 , 1.0 );
448 
449  // Graph coordinates
450  m_graphOutputsTransform = QTransform (xIntGraph, xFurthestXAxisGraph, xFurthestYAxisGraph,
451  yIntGraph, yFurthestXAxisGraph, yFurthestYAxisGraph,
452  1.0 , 1.0 , 1.0 );
453 }
454 
456 {
457  return m_graphOutputsTransform;
458 }
459 
461 {
462  return m_screenInputsTransform;
463 }
464 
466 {
467  if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2) {
468  return m_screenInputs.count();
469  } else if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) {
470  return m_screenInputs.count();
471  } else {
472  return m_screenInputsX.count() + m_screenInputsY.count();
473  }
474 }
475 
476 bool CallbackAxisPointsAbstract::threePointsAreCollinear (const QTransform &transform)
477 {
478  return (transform.determinant() == 0);
479 }
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
CallbackSearchReturn
Return values for search callback methods.
Continue normal execution of the search.
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.
Immediately terminate the current search.
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.