Engauge Digitizer 2
Loading...
Searching...
No Matches
CallbackAxisPointsAbstract.cpp
Go to the documentation of this file.
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
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
16const double ONE_PIXEL = 1.0; // Screen coordinates
17const double ZERO_EPSILON = 0.0; // Graph coordinates
18
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,
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
41bool 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
60bool 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
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
105CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire2AxisPoints (const QPointF &posScreen,
106 const QPointF &posGraph)
107{
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");
135
136 }
137 }
138
139 if (m_screenInputs.count() > 1) {
140
141 // There are enough axis points so quit
143
144 }
145
146 return rtn;
147}
148
149CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire3AxisPoints (const QPointF &posScreen,
150 const QPointF &posGraph)
151{
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");
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");
188
189 } else if ((numberPoints == 3) && threePointsAreCollinear (m_screenInputsTransform,
190 COORD_IS_LINEAR,
191 COORD_IS_LINEAR)) {
192
193 m_isError = true;
194 m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
196
197 } else if ((numberPoints == 3) && threePointsAreCollinear (m_graphOutputsTransform,
198 logXGraph (),
199 logYGraph ())) {
200
201 m_isError = true;
202 m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
204
205 }
206 }
207
208 if (m_screenInputs.count() > 2) {
209
210 // There are enough axis points so quit
212
213 }
214
215 return rtn;
216}
217
218CallbackSearchReturn CallbackAxisPointsAbstract::callbackRequire4AxisPoints (bool isXOnly,
219 const QPointF &posScreen,
220 const QPointF &posGraph)
221{
223
224 // Update range variables
225 int numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
226 if ((numberPoints == 0) || (posGraph.x () < m_xGraphLow)) { m_xGraphLow = posGraph.x (); }
227 if ((numberPoints == 0) || (posGraph.y () < m_yGraphLow)) { m_yGraphLow = posGraph.y (); }
228 if ((numberPoints == 0) || (posGraph.x () > m_xGraphHigh)) { m_xGraphHigh = posGraph.x (); }
229 if ((numberPoints == 0) || (posGraph.y () > m_yGraphHigh)) { m_yGraphHigh = posGraph.y (); }
230
231 if (numberPoints < 4) {
232
233 // Append the new point
234 if (isXOnly) {
235
236 m_screenInputsX.push_back (posScreen);
237 m_graphOutputsX.push_back (posGraph.x());
238
239 } else {
240
241 m_screenInputsY.push_back (posScreen);
242 m_graphOutputsY.push_back (posGraph.y());
243
244 }
245
246 numberPoints = m_screenInputsX.count() + m_screenInputsY.count();
247 if (numberPoints == 4) {
248 loadTransforms4 ();
249 }
250 }
251
252 if (m_screenInputsX.count() > 2) {
253
254 m_isError = true;
255 m_errorMessage = QObject::tr ("Too many x axis points. There should only be two");
257
258 } else if (m_screenInputsY.count() > 2) {
259
260 m_isError = true;
261 m_errorMessage = QObject::tr ("Too many y axis points. There should only be two");
263
264 } else {
265
266 if ((m_screenInputsX.count() == 2) &&
267 (m_screenInputsY.count() == 2)) {
268
269 // Done, although an error may intrude
271 }
272
273 // Error checking
274 if (anyPointsRepeatPair (m_screenInputsX,
275 ONE_PIXEL) ||
276 anyPointsRepeatPair (m_screenInputsY,
277 ONE_PIXEL)) {
278
279 m_isError = true;
280 m_errorMessage = QObject::tr ("New axis point cannot be at the same screen position as an existing axis point");
282
283 } else if (anyPointsRepeatSingle (m_graphOutputsX,
284 ZERO_EPSILON) ||
285 anyPointsRepeatSingle (m_graphOutputsY,
286 ZERO_EPSILON)) {
287
288 m_isError = true;
289 m_errorMessage = QObject::tr ("New axis point cannot have the same graph coordinates as an existing axis point");
291
292 } else if ((numberPoints == 4) && threePointsAreCollinear (m_screenInputsTransform,
293 COORD_IS_LINEAR,
294 COORD_IS_LINEAR)) {
295
296 m_isError = true;
297 m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line on the screen");
299
300 } else if ((numberPoints == 4) && threePointsAreCollinear (m_graphOutputsTransform,
301 logXGraph (),
302 logYGraph ())) {
303
304 m_isError = true;
305 m_errorMessage = QObject::tr ("No more than two axis points can lie along the same line in graph coordinates");
307
308 }
309 }
310
311 return rtn;
312}
313
315{
316 return m_documentAxesPointsRequired;
317}
318
319void CallbackAxisPointsAbstract::loadTransforms2 ()
320{
321 // To get a third point from two existing points we compute the vector between the first 2 points and then take
322 // the cross product with the out-of-plane unit vector to get the perpendicular vector, and the endpoint of that
323 // is used as the third point. This implicitly assumes that the graph-to-screen coordinates scaling is the
324 // same in both directions. The advantage of this approach is that no assumptions are made about the inputs
325
326 double d0To1ScreenX = m_screenInputs.at (1).x () - m_screenInputs.at (0).x ();
327 double d0To1ScreenY = m_screenInputs.at (1).y () - m_screenInputs.at (0).y ();
328 double d0To1ScreenZ = 0;
329 double d0To1GraphX = m_graphOutputs.at (1).x () - m_graphOutputs.at (0).x ();
330 double d0To1GraphY = m_graphOutputs.at (1).y () - m_graphOutputs.at (0).y ();
331 double d0To1GraphZ = 0;
332
333 double unitNormalX = 0;
334 double unitNormalY = 0;
335 double unitNormalZ = 1;
336
337 double d0To2ScreenX = unitNormalY * d0To1ScreenZ - unitNormalZ * d0To1ScreenY;
338 double d0To2ScreenY = unitNormalZ * d0To1ScreenX - unitNormalX * d0To1ScreenZ;
339 double d0To2GraphX = unitNormalY * d0To1GraphZ - unitNormalZ * d0To1GraphY;
340 double d0To2GraphY = unitNormalZ * d0To1GraphX - unitNormalX * d0To1GraphZ;
341
342 // Hack since +Y for screen coordinates is down but up for graph coordinates. Users expect +Y to be up
343 // so we rotate screen delta by 180 degrees
344 const double FLIP_Y_SCREEN = -1.0;
345
346 double screenX2 = m_screenInputs.at (0).x () + FLIP_Y_SCREEN * d0To2ScreenX;
347 double screenY2 = m_screenInputs.at (0).y () + FLIP_Y_SCREEN * d0To2ScreenY;
348 double graphX2 = m_graphOutputs.at (0).x () + d0To2GraphX;
349 double graphY2 = m_graphOutputs.at (0).y () + d0To2GraphY;
350
351 // Screen coordinates
352 m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), screenX2,
353 m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), screenY2,
354 1.0 , 1.0 , 1.0 );
355
356 // Graph coordinates
357 m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), graphX2,
358 m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), graphY2,
359 1.0 , 1.0 , 1.0 );
360}
361
362void CallbackAxisPointsAbstract::loadTransforms3 ()
363{
364 // Screen coordinates
365 m_screenInputsTransform = QTransform (m_screenInputs.at(0).x(), m_screenInputs.at(1).x(), m_screenInputs.at(2).x(),
366 m_screenInputs.at(0).y(), m_screenInputs.at(1).y(), m_screenInputs.at(2).y(),
367 1.0 , 1.0 , 1.0 );
368
369 // Graph coordinates
370 m_graphOutputsTransform = QTransform (m_graphOutputs.at(0).x(), m_graphOutputs.at(1).x(), m_graphOutputs.at(2).x(),
371 m_graphOutputs.at(0).y(), m_graphOutputs.at(1).y(), m_graphOutputs.at(2).y(),
372 1.0 , 1.0 , 1.0 );
373}
374
375void CallbackAxisPointsAbstract::loadTransforms4 ()
376{
377 double x1Screen = m_screenInputsX.at(0).x();
378 double y1Screen = m_screenInputsX.at(0).y();
379 double x2Screen = m_screenInputsX.at(1).x();
380 double y2Screen = m_screenInputsX.at(1).y();
381 double x3Screen = m_screenInputsY.at(0).x();
382 double y3Screen = m_screenInputsY.at(0).y();
383 double x4Screen = m_screenInputsY.at(1).x();
384 double y4Screen = m_screenInputsY.at(1).y();
385
386 // Each of the four axes points has only one coordinate
387 double x1Graph = m_graphOutputsX.at(0);
388 double x2Graph = m_graphOutputsX.at(1);
389 double y3Graph = m_graphOutputsY.at(0);
390 double y4Graph = m_graphOutputsY.at(1);
391
392 // Intersect the two lines of the two axes. The lines are defined parametrically for the screen coordinates, with
393 // points 1 and 2 on the x axis and points 3 and 4 on the y axis, as:
394 // x = (1 - sx) * x1 + sx * x2
395 // y = (1 - sx) * y1 + sx * y2
396 // x = (1 - sy) * x3 + sy * x4
397 // y = (1 - sy) * y3 + sy * y4
398 // Intersection of the 2 lines is at (x,y). Solving for sx and sy using Cramer's rule where Ax=b
399 // (x1 - x3) (x1 - x2 x4 - x3) (sx)
400 // (y1 - y3) = (y1 - y2 y4 - y3) (sy)
401 double A00 = x1Screen - x2Screen;
402 double A01 = x4Screen - x3Screen;
403 double A10 = y1Screen - y2Screen;
404 double A11 = y4Screen - y3Screen;
405 double b0 = x1Screen - x3Screen;
406 double b1 = y1Screen - y3Screen;
407 double numeratorx = (b0 * A11 - A01 * b1);
408 double numeratory = (A00 * b1 - b0 * A10);
409 double denominator = (A00 * A11 - A01 * A10);
410 double sx = numeratorx / denominator;
411 double sy = numeratory / denominator;
412
413 // Intersection point. For the graph coordinates, the initial implementation assumes cartesian coordinates
414 double xIntScreen = (1.0 - sx) * x1Screen + sx * x2Screen;
415 double yIntScreen = (1.0 - sy) * y3Screen + sy * y4Screen;
416 double xIntGraph, yIntGraph;
417 if (m_modelCoords.coordScaleXTheta() == COORD_SCALE_LINEAR) {
418 xIntGraph = (1.0 - sx) * x1Graph + sx * x2Graph;
419 } else {
420 xIntGraph = qExp ((1.0 - sx) * qLn (x1Graph) + sx * qLn (x2Graph));
421 }
422 if (m_modelCoords.coordScaleYRadius() == COORD_SCALE_LINEAR) {
423 yIntGraph = (1.0 - sy) * y3Graph + sy * y4Graph;
424 } else {
425 yIntGraph = qExp ((1.0 - sy) * qLn (y3Graph) + sy * qLn (y4Graph));
426 }
427
428 // Distances of 4 axis points from interception
429 double distance1 = qSqrt ((x1Screen - xIntScreen) * (x1Screen - xIntScreen) +
430 (y1Screen - yIntScreen) * (y1Screen - yIntScreen));
431 double distance2 = qSqrt ((x2Screen - xIntScreen) * (x2Screen - xIntScreen) +
432 (y2Screen - yIntScreen) * (y2Screen - yIntScreen));
433 double distance3 = qSqrt ((x3Screen - xIntScreen) * (x3Screen - xIntScreen) +
434 (y3Screen - yIntScreen) * (y3Screen - yIntScreen));
435 double distance4 = qSqrt ((x4Screen - xIntScreen) * (x4Screen - xIntScreen) +
436 (y4Screen - yIntScreen) * (y4Screen - yIntScreen));
437
438 // We now have too many data points with both x and y coordinates:
439 // (xInt,yInt) (xInt,y3) (xInt,y4) (x1,yInt) (x2,yInt)
440 // so we pick just 3, making sure that those 3 are widely separated
441 // (xInt,yInt) (x axis point furthest from xInt,yInt) (y axis point furthest from xInt,yInt)
442 double xFurthestXAxisScreen, yFurthestXAxisScreen, xFurthestYAxisScreen, yFurthestYAxisScreen;
443 double xFurthestXAxisGraph, yFurthestXAxisGraph, xFurthestYAxisGraph, yFurthestYAxisGraph;
444 if (distance1 < distance2) {
445 xFurthestXAxisScreen = x2Screen;
446 yFurthestXAxisScreen = y2Screen;
447 xFurthestXAxisGraph = x2Graph;
448 yFurthestXAxisGraph = yIntGraph;
449 } else {
450 xFurthestXAxisScreen = x1Screen;
451 yFurthestXAxisScreen = y1Screen;
452 xFurthestXAxisGraph = x1Graph;
453 yFurthestXAxisGraph = yIntGraph;
454 }
455 if (distance3 < distance4) {
456 xFurthestYAxisScreen = x4Screen;
457 yFurthestYAxisScreen = y4Screen;
458 xFurthestYAxisGraph = xIntGraph;
459 yFurthestYAxisGraph = y4Graph;
460 } else {
461 xFurthestYAxisScreen = x3Screen;
462 yFurthestYAxisScreen = y3Screen;
463 xFurthestYAxisGraph = xIntGraph;
464 yFurthestYAxisGraph = y3Graph;
465 }
466
467 // Screen coordinates
468 m_screenInputsTransform = QTransform (xIntScreen, xFurthestXAxisScreen, xFurthestYAxisScreen,
469 yIntScreen, yFurthestXAxisScreen, yFurthestYAxisScreen,
470 1.0 , 1.0 , 1.0 );
471
472 // Graph coordinates
473 m_graphOutputsTransform = QTransform (xIntGraph, xFurthestXAxisGraph, xFurthestYAxisGraph,
474 yIntGraph, yFurthestXAxisGraph, yFurthestYAxisGraph,
475 1.0 , 1.0 , 1.0 );
476}
477
478CallbackAxisPointsAbstract::LinearOrLog CallbackAxisPointsAbstract::logXGraph () const
479{
480 return m_modelCoords.coordScaleXTheta() == COORD_SCALE_LOG ? COORD_IS_LOG : COORD_IS_LINEAR;
481}
482
483CallbackAxisPointsAbstract::LinearOrLog CallbackAxisPointsAbstract::logYGraph () const
484{
485 return m_modelCoords.coordScaleYRadius() == COORD_SCALE_LOG ? COORD_IS_LOG : COORD_IS_LINEAR;
486}
487
489{
490 return m_graphOutputsTransform;
491}
492
494{
495 return m_screenInputsTransform;
496}
497
499{
500 if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_2) {
501 return unsigned (m_screenInputs.count());
502 } else if (m_documentAxesPointsRequired == DOCUMENT_AXES_POINTS_REQUIRED_3) {
503 return unsigned (m_screenInputs.count());
504 } else {
505 return unsigned (m_screenInputsX.count() + m_screenInputsY.count());
506 }
507}
508
509bool CallbackAxisPointsAbstract::threePointsAreCollinear (const QTransform &transformIn,
510 LinearOrLog logX,
511 LinearOrLog logY) const
512{
513 double m11 = (logX == COORD_IS_LOG) ? qLn (transformIn.m11()) : transformIn.m11();
514 double m12 = (logX == COORD_IS_LOG) ? qLn (transformIn.m12()) : transformIn.m12();
515 double m13 = (logX == COORD_IS_LOG) ? qLn (transformIn.m13()) : transformIn.m13();
516 double m21 = (logY == COORD_IS_LOG) ? qLn (transformIn.m21()) : transformIn.m21();
517 double m22 = (logY == COORD_IS_LOG) ? qLn (transformIn.m22()) : transformIn.m22();
518 double m23 = (logY == COORD_IS_LOG) ? qLn (transformIn.m23()) : transformIn.m23();
519 double m31 = transformIn.m31();
520 double m32 = transformIn.m32();
521 double m33 = transformIn.m33();
522
523 QTransform transform (m11, m12, m13,
524 m21, m22, m23,
525 m31, m32, m33);
526
527 return !transform.isInvertible ();
528}
const double ONE_PIXEL
const double ZERO_EPSILON
QList< QPointF > CoordPairVector
QList< double > CoordSingleVector
CallbackSearchReturn
Return values for search callback methods.
@ CALLBACK_SEARCH_RETURN_CONTINUE
Continue normal execution of the search.
@ CALLBACK_SEARCH_RETURN_INTERRUPT
Immediately terminate the current search.
@ COORD_SCALE_LINEAR
Definition CoordScale.h:13
@ COORD_SCALE_LOG
Definition CoordScale.h:14
@ DOCUMENT_AXES_POINTS_REQUIRED_3
@ DOCUMENT_AXES_POINTS_REQUIRED_2
DocumentAxesPointsRequired documentAxesPointsRequired() const
Number of axes points required for the transformation.
QTransform matrixGraph() const
Returns graph coordinates matrix after transformIsDefined has already indicated success.
CallbackSearchReturn callback(const QString &curveName, const Point &point)
Callback method.
CallbackAxisPointsAbstract(const DocumentModelCoords &modelCoords, DocumentAxesPointsRequired documentAxesPointsRequired)
Constructor for when all of the existing axis points are to be processed as is.
unsigned int numberAxisPoints() const
Number of axis points which is less than 3 if the axes curve is incomplete.
QTransform matrixScreen() const
Returns screen coordinates matrix after transformIsDefined has already indicated success.
Model for DlgSettingsCoords and CmdSettingsCoords.
Class that represents one digitized point. The screen-to-graph coordinate transformation is always ex...
Definition Point.h:26
QPointF posGraph(ApplyHasCheck applyHasCheck=KEEP_HAS_CHECK) const
Accessor for graph position. Skip check if copying one instance to another.
Definition Point.cpp:395
QPointF posScreen() const
Accessor for screen position.
Definition Point.cpp:404
QString identifier() const
Unique identifier for a specific Point.
Definition Point.cpp:268
bool isXOnly() const
In DOCUMENT_AXES_POINTS_REQUIRED_4 modes, this is true/false if y/x coordinate is undefined.
Definition Point.cpp:286