Engauge Digitizer  2
 All Classes Files Functions Variables Enumerations Enumerator Friends Pages
ExportFileFunctions.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 "CallbackGatherXThetaValuesFunctions.h"
8 #include "CurveConnectAs.h"
9 #include "Document.h"
10 #include "EngaugeAssert.h"
11 #include "ExportFileFunctions.h"
12 #include "ExportLayoutFunctions.h"
13 #include "ExportOrdinalsSmooth.h"
14 #include "ExportXThetaValuesMergedFunctions.h"
15 #include "FormatCoordsUnits.h"
16 #include "Logger.h"
17 #include <QTextStream>
18 #include <QVector>
19 #include "Spline.h"
20 #include "SplinePair.h"
21 #include "Transformation.h"
22 #include <vector>
23 
24 using namespace std;
25 
27 {
28 }
29 
30 void ExportFileFunctions::exportAllPerLineXThetaValuesMerged (const DocumentModelExportFormat &modelExportOverride,
31  const Document &document,
32  const MainWindowModel &modelMainWindow,
33  const QStringList &curvesIncluded,
34  const ExportValuesXOrY &xThetaValues,
35  const QString &delimiter,
36  const Transformation &transformation,
37  QTextStream &str) const
38 {
39  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::exportAllPerLineXThetaValuesMerged";
40 
41  int curveCount = curvesIncluded.count();
42  int xThetaCount = xThetaValues.count();
43  QVector<QVector<QString*> > yRadiusValues (curveCount, QVector<QString*> (xThetaCount));
44  initializeYRadiusValues (curvesIncluded,
45  xThetaValues,
46  yRadiusValues);
47  loadYRadiusValues (modelExportOverride,
48  document,
49  modelMainWindow,
50  curvesIncluded,
51  transformation,
52  xThetaValues,
53  yRadiusValues);
54 
55  outputXThetaYRadiusValues (modelExportOverride,
56  document.modelCoords(),
57  modelMainWindow,
58  curvesIncluded,
59  xThetaValues,
60  transformation,
61  yRadiusValues,
62  delimiter,
63  str);
64  destroy2DArray (yRadiusValues);
65 }
66 
67 void ExportFileFunctions::exportOnePerLineXThetaValuesMerged (const DocumentModelExportFormat &modelExportOverride,
68  const Document &document,
69  const MainWindowModel &modelMainWindow,
70  const QStringList &curvesIncluded,
71  const ExportValuesXOrY &xThetaValues,
72  const QString &delimiter,
73  const Transformation &transformation,
74  QTextStream &str) const
75 {
76  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::exportOnePerLineXThetaValuesMerged";
77 
78  bool isFirst = true;
79 
80  QStringList::const_iterator itr;
81  for (itr = curvesIncluded.begin(); itr != curvesIncluded.end(); itr++) {
82 
83  insertLineSeparator (isFirst,
84  modelExportOverride.header(),
85  str);
86 
87  // This curve
88  const int CURVE_COUNT = 1;
89  QString curveIncluded = *itr;
90  QStringList curvesIncluded (curveIncluded);
91 
92  int xThetaCount = xThetaValues.count();
93  QVector<QVector<QString*> > yRadiusValues (CURVE_COUNT, QVector<QString*> (xThetaCount));
94  initializeYRadiusValues (curvesIncluded,
95  xThetaValues,
96  yRadiusValues);
97  loadYRadiusValues (modelExportOverride,
98  document,
99  modelMainWindow,
100  curvesIncluded,
101  transformation,
102  xThetaValues,
103  yRadiusValues);
104  outputXThetaYRadiusValues (modelExportOverride,
105  document.modelCoords(),
106  modelMainWindow,
107  curvesIncluded,
108  xThetaValues,
109  transformation,
110  yRadiusValues,
111  delimiter,
112  str);
113  destroy2DArray (yRadiusValues);
114  }
115 }
116 
118  const Document &document,
119  const MainWindowModel &modelMainWindow,
120  const Transformation &transformation,
121  QTextStream &str) const
122 {
123  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::exportToFile";
124 
125  // Identify curves to be included
126  QStringList curvesIncluded = curvesToInclude (modelExportOverride,
127  document,
128  document.curvesGraphsNames(),
129  CONNECT_AS_FUNCTION_SMOOTH,
130  CONNECT_AS_FUNCTION_STRAIGHT);
131 
132  // Delimiter
133  const QString delimiter = exportDelimiterToText (modelExportOverride.delimiter(),
134  modelExportOverride.header() == EXPORT_HEADER_GNUPLOT);
135 
136  // Get x/theta values to be used
137  CallbackGatherXThetaValuesFunctions ftor (modelExportOverride,
138  curvesIncluded,
139  transformation);
140  Functor2wRet<const QString &, const Point &, CallbackSearchReturn> ftorWithCallback = functor_ret (ftor,
142  document.iterateThroughCurvesPointsGraphs(ftorWithCallback);
143 
144  ExportXThetaValuesMergedFunctions exportXTheta (modelExportOverride,
145  ftor.xThetaValuesRaw(),
146  transformation);
147  ExportValuesXOrY xThetaValuesMerged = exportXTheta.xThetaValues ();
148 
149  // Skip if every curve was a relation
150  if (xThetaValuesMerged.count() > 0) {
151 
152  // Export in one of two layouts
153  if (modelExportOverride.layoutFunctions() == EXPORT_LAYOUT_ALL_PER_LINE) {
154  exportAllPerLineXThetaValuesMerged (modelExportOverride,
155  document,
156  modelMainWindow,
157  curvesIncluded,
158  xThetaValuesMerged,
159  delimiter,
160  transformation,
161  str);
162  } else {
163  exportOnePerLineXThetaValuesMerged (modelExportOverride,
164  document,
165  modelMainWindow,
166  curvesIncluded,
167  xThetaValuesMerged,
168  delimiter,
169  transformation,
170  str);
171  }
172  }
173 }
174 
175 void ExportFileFunctions::initializeYRadiusValues (const QStringList &curvesIncluded,
176  const ExportValuesXOrY &xThetaValuesMerged,
177  QVector<QVector<QString*> > &yRadiusValues) const
178 {
179  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::initializeYRadiusValues";
180 
181  // Initialize every entry with empty string
182  int curveCount = curvesIncluded.count();
183  int xThetaCount = xThetaValuesMerged.count();
184  for (int row = 0; row < xThetaCount; row++) {
185  for (int col = 0; col < curveCount; col++) {
186  yRadiusValues [col] [row] = new QString;
187  }
188  }
189 }
190 
191 double ExportFileFunctions::linearlyInterpolate (const Points &points,
192  double xThetaValue,
193  const Transformation &transformation) const
194 {
195  // LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::linearlyInterpolate";
196 
197  // If point is within the range of the function points then interpolation will be used, otherwise
198  // extrapolation will be used
199  double yRadius = 0;
200  QPointF posGraphBefore; // Not set until ip=1
201  bool foundIt = false;
202  for (int ip = 0; !foundIt && (ip < points.count()); ip++) {
203 
204  const Point &point = points.at (ip);
205  QPointF posGraph;
206  transformation.transformScreenToRawGraph (point.posScreen(),
207  posGraph);
208 
209  // Cases where we have found it at this point in the code
210  // (1) interpolation case where (xBefore < xThetaValue < xAfter)
211  // (2) extrapolation case where (xThetaValue < xBefore < xAfter and ip=0) for which we delay finding it until ip=1 so we have
212  // two points for extrapolating. This case is why we have the subtle test for ip>0 in the next line
213  if (xThetaValue <= posGraph.x() && (ip > 0)) {
214 
215  foundIt = true;
216 
217  // Case 1 comments: xThetaValue is between posGraphBefore and posGraph. Note that if posGraph.x()=posGraphBefore.x() then
218  // previous iteration of loop would have been used for interpolation, and then the loop was exited. Range of s is 0<s<1
219  // Case 2 comments: Range of s is s<0
220  double s = (xThetaValue - posGraphBefore.x()) / (posGraph.x() - posGraphBefore.x());
221  yRadius = (1.0 -s) * posGraphBefore.y() + s * posGraph.y();
222 
223  break;
224  }
225 
226  posGraphBefore = posGraph;
227  }
228 
229  if (!foundIt) {
230 
231  if (points.count() > 1) {
232 
233  // Extrapolation will be used since point is out of the range of the function points. Specifically, it is greater than the
234  // last x value in the function. Range of s is 1<s
235  int N = points.count();
236  const Point &pointLast = points.at (N - 1);
237  const Point &pointBefore = points.at (N - 2);
238  QPointF posGraphLast;
239  transformation.transformScreenToRawGraph (pointLast.posScreen(),
240  posGraphLast);
241  transformation.transformScreenToRawGraph (pointBefore.posScreen(),
242  posGraphBefore);
243  double s = (xThetaValue - posGraphBefore.x()) / (posGraphLast.x() - posGraphBefore.x());
244  yRadius = (1.0 - s) * posGraphBefore.y() + s * posGraphLast.y();
245 
246  } else if (points.count() == 1) {
247 
248  // Just use the single point
249  yRadius = posGraphBefore.y();
250 
251  } else {
252 
253  ENGAUGE_ASSERT (false);
254 
255  }
256  }
257 
258  return yRadius;
259 }
260 
261 void ExportFileFunctions::loadYRadiusValues (const DocumentModelExportFormat &modelExportOverride,
262  const Document &document,
263  const MainWindowModel &modelMainWindow,
264  const QStringList &curvesIncluded,
265  const Transformation &transformation,
266  const ExportValuesXOrY &xThetaValues,
267  QVector<QVector<QString*> > &yRadiusValues) const
268 {
269  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::loadYRadiusValues";
270 
271  // Loop through curves
272  int curveCount = curvesIncluded.count();
273  for (int col = 0; col < curveCount; col++) {
274 
275  const QString curveName = curvesIncluded.at (col);
276 
277  const Curve *curve = document.curveForCurveName (curveName);
278  const Points points = curve->points ();
279 
280  if (modelExportOverride.pointsSelectionFunctions() == EXPORT_POINTS_SELECTION_FUNCTIONS_RAW) {
281 
282  // No interpolation. Raw points
283  loadYRadiusValuesForCurveRaw (document.modelCoords(),
284  modelMainWindow,
285  points,
286  xThetaValues,
287  transformation,
288  yRadiusValues [col]);
289  } else {
290 
291  // Interpolation
292  if (curve->curveStyle().lineStyle().curveConnectAs() == CONNECT_AS_FUNCTION_SMOOTH) {
293 
294  loadYRadiusValuesForCurveInterpolatedSmooth (document.modelCoords(),
295  modelMainWindow,
296  points,
297  xThetaValues,
298  transformation,
299  yRadiusValues [col]);
300 
301  } else {
302 
303  loadYRadiusValuesForCurveInterpolatedStraight (document.modelCoords(),
304  modelMainWindow,
305  points,
306  xThetaValues,
307  transformation,
308  yRadiusValues [col]);
309  }
310  }
311  }
312 }
313 
314 void ExportFileFunctions::loadYRadiusValuesForCurveInterpolatedSmooth (const DocumentModelCoords &modelCoords,
315  const MainWindowModel &modelMainWindow,
316  const Points &points,
317  const ExportValuesXOrY &xThetaValues,
318  const Transformation &transformation,
319  QVector<QString*> &yRadiusValues) const
320 {
321  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::loadYRadiusValuesForCurveInterpolatedSmooth";
322 
323  // Convert screen coordinates to graph coordinates, in vectors suitable for spline fitting
324  vector<double> t;
325  vector<SplinePair> xy;
326  ExportOrdinalsSmooth ordinalsSmooth;
327 
328  ordinalsSmooth.loadSplinePairsWithTransformation (points,
329  transformation,
330  t,
331  xy);
332 
333  // Formatting
334  FormatCoordsUnits format;
335  QString dummyXThetaOut;
336 
337  if (points.count() == 0) {
338 
339  // Since there are no values, leave the field empty
340  for (int row = 0; row < xThetaValues.count(); row++) {
341  *(yRadiusValues [row]) = "";
342  }
343 
344  } else if (points.count() == 1 ||
345  points.count() == 2) {
346 
347  // Apply the single value everywhere (N=1) or do linear interpolation (N=2)
348  for (int row = 0; row < xThetaValues.count(); row++) {
349 
350  double xTheta = xThetaValues.at (row);
351  double yRadius;
352  if (points.count() == 1) {
353  yRadius = xy.at (0).y ();
354  } else {
355  double x0 = xy.at (0).x ();
356  double x1 = xy.at (1).x ();
357  double y0 = xy.at (0).y ();
358  double y1 = xy.at (1).y ();
359  if (x0 == x1) {
360  // Cannot do linear interpolation using two points at the same x value
361  yRadius = xy.at (0).y ();
362  } else {
363  double s = (xTheta - x0) / (x1 - x0);
364  yRadius = (1.0 - s) * y0 + s * y1;
365  }
366  }
367  format.unformattedToFormatted (xTheta,
368  yRadius,
369  modelCoords,
370  modelMainWindow,
371  dummyXThetaOut,
372  *(yRadiusValues [row]),
373  transformation);
374  }
375 
376  } else {
377 
378  // Iteration accuracy versus number of iterations 8->256, 10->1024, 12->4096. Single pixel accuracy out of
379  // typical image size of 1024x1024 means around 10 iterations gives decent accuracy for numbers much bigger
380  // than 1. A value of 12 gave some differences in the least significant figures of numbers like 10^-3 in
381  // the regression tests. Toggling between 30 and 32 made no difference in the regression tests.
382  const int MAX_ITERATIONS = 32;
383 
384  // Spline class requires at least one point
385  if (xy.size() > 0) {
386 
387  // Fit a spline
388  Spline spline (t,
389  xy);
390 
391  // Get value at desired points
392  for (int row = 0; row < xThetaValues.count(); row++) {
393 
394  double xTheta = xThetaValues.at (row);
395  SplinePair splinePairFound = spline.findSplinePairForFunctionX (xTheta,
396  MAX_ITERATIONS);
397  double yRadius = splinePairFound.y ();
398 
399  // Save y/radius value for this row into yRadiusValues, after appropriate formatting
400  QString dummyXThetaOut;
401  format.unformattedToFormatted (xTheta,
402  yRadius,
403  modelCoords,
404  modelMainWindow,
405  dummyXThetaOut,
406  *(yRadiusValues [row]),
407  transformation);
408  }
409  }
410  }
411 }
412 
413 void ExportFileFunctions::loadYRadiusValuesForCurveInterpolatedStraight (const DocumentModelCoords &modelCoords,
414  const MainWindowModel &modelMainWindow,
415  const Points &points,
416  const ExportValuesXOrY &xThetaValues,
417  const Transformation &transformation,
418  QVector<QString*> &yRadiusValues) const
419 {
420  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::loadYRadiusValuesForCurveInterpolatedStraight";
421 
422  FormatCoordsUnits format;
423 
424  // Get value at desired points
425  for (int row = 0; row < xThetaValues.count(); row++) {
426 
427  double xThetaValue = xThetaValues.at (row);
428 
429  double yRadius = linearlyInterpolate (points,
430  xThetaValue,
431  transformation);
432 
433  // Save y/radius value for this row into yRadiusValues, after appropriate formatting
434  QString dummyXThetaOut;
435  format.unformattedToFormatted (xThetaValue,
436  yRadius,
437  modelCoords,
438  modelMainWindow,
439  dummyXThetaOut,
440  *(yRadiusValues [row]),
441  transformation);
442  }
443 }
444 
445 void ExportFileFunctions::loadYRadiusValuesForCurveRaw (const DocumentModelCoords &modelCoords,
446  const MainWindowModel &modelMainWindow,
447  const Points &points,
448  const ExportValuesXOrY &xThetaValues,
449  const Transformation &transformation,
450  QVector<QString*> &yRadiusValues) const
451 {
452  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::loadYRadiusValuesForCurveRaw";
453 
454  FormatCoordsUnits format;
455 
456  // Since the curve points may be a subset of xThetaValues (in which case the non-applicable xThetaValues will have
457  // blanks for the yRadiusValues), we iterate over the smaller set
458  for (int pt = 0; pt < points.count(); pt++) {
459 
460  const Point &point = points.at (pt);
461 
462  QPointF posGraph;
463  transformation.transformScreenToRawGraph (point.posScreen(),
464  posGraph);
465 
466  // Find the closest point in xThetaValues. This is probably an N-squared algorithm, which is less than optimial,
467  // but the delay should be insignificant with normal-sized export files
468  double closestSeparation = 0.0;
469  int rowClosest = 0;
470  for (int row = 0; row < xThetaValues.count(); row++) {
471 
472  double xThetaValue = xThetaValues.at (row);
473 
474  double separation = qAbs (posGraph.x() - xThetaValue);
475 
476  if ((row == 0) ||
477  (separation < closestSeparation)) {
478 
479  closestSeparation = separation;
480  rowClosest = row;
481 
482  }
483  }
484 
485  // Save y/radius value for this row into yRadiusValues, after appropriate formatting
486  QString dummyXThetaOut;
487  format.unformattedToFormatted (posGraph.x(),
488  posGraph.y(),
489  modelCoords,
490  modelMainWindow,
491  dummyXThetaOut,
492  *(yRadiusValues [rowClosest]),
493  transformation);
494  }
495 }
496 
497 void ExportFileFunctions::outputXThetaYRadiusValues (const DocumentModelExportFormat &modelExportOverride,
498  const DocumentModelCoords &modelCoords,
499  const MainWindowModel &modelMainWindow,
500  const QStringList &curvesIncluded,
501  const ExportValuesXOrY &xThetaValuesMerged,
502  const Transformation &transformation,
503  QVector<QVector<QString*> > &yRadiusValues,
504  const QString &delimiter,
505  QTextStream &str) const
506 {
507  LOG4CPP_INFO_S ((*mainCat)) << "ExportFileFunctions::outputXThetaYRadiusValues";
508 
509  // Header
510  if (modelExportOverride.header() != EXPORT_HEADER_NONE) {
511  if (modelExportOverride.header() == EXPORT_HEADER_GNUPLOT) {
512  str << curveSeparator (str.string());
513  str << gnuplotComment();
514  }
515  str << modelExportOverride.xLabel();
516  QStringList::const_iterator itrHeader;
517  for (itrHeader = curvesIncluded.begin(); itrHeader != curvesIncluded.end(); itrHeader++) {
518  QString curveName = *itrHeader;
519  str << delimiter << curveName;
520  }
521  str << "\n";
522  }
523 
524  FormatCoordsUnits format;
525  const double DUMMY_Y_RADIUS = 1.0;
526 
527  for (int row = 0; row < xThetaValuesMerged.count(); row++) {
528 
529  if (rowHasAtLeastOneYRadiusEntry (yRadiusValues,
530  row)) {
531 
532  double xTheta = xThetaValuesMerged.at (row);
533 
534  // Output x/theta value for this row
535  QString xThetaString, yRadiusString;
536  format.unformattedToFormatted (xTheta,
537  DUMMY_Y_RADIUS,
538  modelCoords,
539  modelMainWindow,
540  xThetaString,
541  yRadiusString,
542  transformation);
543  str << xThetaString;
544 
545  for (int col = 0; col < yRadiusValues.count(); col++) {
546 
547  str << delimiter << *(yRadiusValues [col] [row]);
548  }
549 
550  str << "\n";
551  }
552  }
553 }
554 
555 bool ExportFileFunctions::rowHasAtLeastOneYRadiusEntry (const QVector<QVector<QString*> > &yRadiusValues,
556  int row) const
557 {
558  bool hasEntry = false;
559 
560  for (int col = 0; col < yRadiusValues.count(); col++) {
561 
562  QString entry = *(yRadiusValues [col] [row]);
563  if (!entry.isEmpty()) {
564 
565  hasEntry = true;
566  break;
567 
568  }
569  }
570 
571  return hasEntry;
572 }
void transformScreenToRawGraph(const QPointF &coordScreen, QPointF &coordGraph) const
Transform from cartesian pixel screen coordinates to cartesian/polar graph coordinates.
ExportPointsSelectionFunctions pointsSelectionFunctions() const
Get method for point selection for functions.
Creates the set of merged x/theta values for exporting functions, using interpolation.
ExportLayoutFunctions layoutFunctions() const
Get method for functions layout.
Cubic interpolation given independent and dependent value vectors.
Definition: Spline.h:21
const Points points() const
Return a shallow copy of the Points.
Definition: Curve.cpp:442
Model for DlgSettingsExportFormat and CmdSettingsExportFormat.
ExportValuesXOrY xThetaValues() const
Resulting x/theta values for all included functions.
LineStyle lineStyle() const
Get method for LineStyle.
Definition: CurveStyle.cpp:26
ExportFileFunctions()
Single constructor.
DocumentModelCoords modelCoords() const
Get method for DocumentModelCoords.
Definition: Document.cpp:665
double y() const
Get method for y.
Definition: SplinePair.cpp:71
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 callback(const QString &curveName, const Point &point)
Callback method.
ExportHeader header() const
Get method for header.
void unformattedToFormatted(double xThetaUnformatted, double yRadiusUnformatted, const DocumentModelCoords &modelCoords, const MainWindowModel &mainWindowModel, QString &xThetaFormatted, QString &yRadiusFormatted, const Transformation &transformation) const
Convert unformatted numeric value to formatted string. Transformation is used to determine best resol...
Affine transformation between screen and graph coordinates, based on digitized axis points...
QString xLabel() const
Get method for x label.
void loadSplinePairsWithTransformation(const Points &points, const Transformation &transformation, std::vector< double > &t, std::vector< SplinePair > &xy) const
Load t (=ordinal) and xy (=screen position) spline pairs, converting screen coordinates to graph coor...
Model for DlgSettingsMainWindow.
Utility class to interpolate points spaced evenly along a piecewise defined curve with fitted spline...
ExportDelimiter delimiter() const
Get method for delimiter.
Model for DlgSettingsCoords and CmdSettingsCoords.
Storage of one imported image and the data attached to that image.
Definition: Document.h:41
Container for one set of digitized Points.
Definition: Curve.h:32
QStringList curvesGraphsNames() const
See CurvesGraphs::curvesGraphsNames.
Definition: Document.cpp:319
Highest-level wrapper around other Formats classes.
const Curve * curveForCurveName(const QString &curveName) const
See CurvesGraphs::curveForCurveNames, although this also works for AXIS_CURVE_NAME.
Definition: Document.cpp:305
CurveStyle curveStyle() const
Return the curve style.
Definition: Curve.cpp:139
void iterateThroughCurvesPointsGraphs(const Functor2wRet< const QString &, const Point &, CallbackSearchReturn > &ftorWithCallback)
See Curve::iterateThroughCurvePoints, for all the graphs curves.
Definition: Document.cpp:439
CurveConnectAs curveConnectAs() const
Get method for connect type.
Definition: LineStyle.cpp:63
Callback for collecting X/Theta independent variables, for functions, in preparation for exporting...
Single X/Y pair for cubic spline interpolation initialization and calculations.
Definition: SplinePair.h:11
void exportToFile(const DocumentModelExportFormat &modelExportOverride, const Document &document, const MainWindowModel &modelMainWindow, const Transformation &transformation, QTextStream &str) const
Export Document points according to the settings.