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