Engauge Digitizer  2
Segment.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 "DocumentModelSegments.h"
8 #include "EngaugeAssert.h"
9 #include "gnuplot.h"
10 #include <iostream>
11 #include "Logger.h"
12 #include "mmsubs.h"
13 #include <qdebug.h>
14 #include <QFile>
15 #include <QGraphicsScene>
16 #include <qmath.h>
17 #include <QTextStream>
18 #include "QtToString.h"
19 #include "Segment.h"
20 #include "SegmentLine.h"
21 
22 Segment::Segment(QGraphicsScene &scene,
23  int y,
24  bool isGnuplot) :
25  m_scene (scene),
26  m_yLast (y),
27  m_length (0),
28  m_isGnuplot (isGnuplot)
29 {
30 }
31 
32 Segment::~Segment()
33 {
34  QList<SegmentLine*>::iterator itr;
35  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
36 
37  SegmentLine *segmentLine = *itr;
38  m_scene.removeItem (segmentLine);
39  }
40 }
41 
43  int y,
44  const DocumentModelSegments &modelSegments)
45 {
46  int xOld = x - 1;
47  int yOld = m_yLast;
48  int xNew = x;
49  int yNew = y;
50 
51  LOG4CPP_DEBUG_S ((*mainCat)) << "Segment::appendColumn"
52  << " segment=0x" << std::hex << static_cast<void*> (this) << std::dec
53  << " adding ("
54  << xOld << "," << yOld << ") to ("
55  << xNew << "," << yNew << ")";
56 
57  SegmentLine* line = new SegmentLine(m_scene,
58  modelSegments,
59  this);
60  ENGAUGE_CHECK_PTR(line);
61  line->setLine(QLineF (xOld,
62  yOld,
63  xNew,
64  yNew));
65 
66  // Do not show this line or its segment. this is handled later
67 
68  m_lines.append(line);
69 
70  // Update total length using distance formula
71  m_length += qSqrt((1.0) * (1.0) + (y - m_yLast) * (y - m_yLast));
72 
73  m_yLast = y;
74 }
75 
76 void Segment::createAcceptablePoint(bool *pFirst,
77  QList<QPoint> *pList,
78  double *xPrev,
79  double *yPrev,
80  double x,
81  double y)
82 {
83  int iOld = qFloor (*xPrev + 0.5);
84  int jOld = qFloor (*yPrev + 0.5);
85  int i = qFloor (x + 0.5);
86  int j = qFloor (y + 0.5);
87 
88  if (*pFirst || (iOld != i) || (jOld != j)) {
89  *xPrev = x;
90  *yPrev = y;
91 
92  ENGAUGE_CHECK_PTR(pList);
93  pList->append(QPoint(i, j));
94  }
95 
96  *pFirst = false;
97 }
98 
99 void Segment::dumpToGnuplot (QTextStream &strDump,
100  int xInt,
101  int yInt,
102  const SegmentLine *lineOld,
103  const SegmentLine *lineNew) const
104 {
105  // Only show this dump spew when logging is opened up completely
106  if (mainCat->getPriority() == log4cpp::Priority::DEBUG) {
107 
108  // Show "before" and "after" line info. Note that the merged line starts with lineOld->line().p1()
109  // and ends with lineNew->line().p2()
110  QString label = QString ("Old: (%1,%2) to (%3,%4), New: (%5,%6) to (%7,%8)")
111  .arg (lineOld->line().x1())
112  .arg (lineOld->line().y1())
113  .arg (lineOld->line().x2())
114  .arg (lineOld->line().y2())
115  .arg (lineNew->line().x1())
116  .arg (lineNew->line().y1())
117  .arg (lineNew->line().x2())
118  .arg (lineNew->line().y2());
119 
120  strDump << "unset label\n";
121  strDump << "set label \"" << label << "\" at graph 0, graph 0.02\n";
122  strDump << "set grid xtics\n";
123  strDump << "set grid ytics\n";
124 
125  // Get the bounds
126  int rows = 0, cols = 0;
127  QList<SegmentLine*>::const_iterator itr;
128  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
129 
130  SegmentLine *line = *itr;
131  ENGAUGE_CHECK_PTR (line);
132 
133  int x1 = qFloor (line->line().x1());
134  int y1 = qFloor (line->line().y1());
135  int x2 = qFloor (line->line().x2());
136  int y2 = qFloor (line->line().y2());
137 
138  rows = qMax (rows, y1 + 1);
139  rows = qMax (rows, y2 + 1);
140  cols = qMax (cols, x1 + 1);
141  cols = qMax (cols, x2 + 1);
142  }
143 
144  // Horizontal and vertical width is computed so merged line mostly fills the plot window,
145  // and (xInt,yInt) is at the center
146  int halfWidthX = qFloor (1.5 * qMax (qAbs (lineOld->line().dx()),
147  qAbs (lineNew->line().dx())));
148  int halfWidthY = qFloor (1.5 * qMax (qAbs (lineOld->line().dy()),
149  qAbs (lineNew->line().dy())));
150 
151  // Zoom in so changes are easier to see
152  strDump << "set xrange [" << (xInt - halfWidthX - 1) << ":" << (xInt + halfWidthX + 1) << "]\n";
153  strDump << "set yrange [" << (yInt - halfWidthY - 1) << ":" << (yInt + halfWidthY + 1) << "]\n";
154 
155  // One small curve shows xInt as horizontal line, and another shows yInt as vertical line.
156  // A small curve shows the replacement line
157  // Then two hhuge piecewise-defined curve show the pre-merge Segment pixels as two alternating colors
158  strDump << "plot \\\n"
159  << "\"-\" title \"\" with lines, \\\n"
160  << "\"-\" title \"\" with lines, \\\n"
161  << "\"-\" title \"Replacement\" with lines, \\\n"
162  << "\"-\" title \"Segment pixels Even\" with linespoints, \\\n"
163  << "\"-\" title \"Segment pixels Odd\" with linespoints\n"
164  << xInt << " " << (yInt - halfWidthY) << "\n"
165  << xInt << " " << (yInt + halfWidthY) << "\n"
166  << "end\n"
167  << (xInt - halfWidthX) << " " << yInt << "\n"
168  << (xInt + halfWidthY) << " " << yInt << "\n"
169  << "end\n"
170  << lineOld->line().x1() << " " << lineOld->line().y1() << "\n"
171  << lineNew->line().x2() << " " << lineNew->line().y2() << "\n"
172  << "end\n";
173 
174  // Fill the array from the list
175  QString even, odd;
176  QTextStream strEven (&even), strOdd (&odd);
177  for (int index = 0; index < m_lines.count(); index++) {
178 
179  SegmentLine *line = m_lines.at (index);
180  int x1 = qFloor (line->line().x1());
181  int y1 = qFloor (line->line().y1());
182  int x2 = qFloor (line->line().x2());
183  int y2 = qFloor (line->line().y2());
184 
185  if (index % 2 == 0) {
186  strEven << x1 << " " << y1 << "\n";
187  strEven << x2 << " " << y2 << "\n";
188  strEven << "\n";
189  } else {
190  strOdd << x1 << " " << y1 << "\n";
191  strOdd << x2 << " " << y2 << "\n";
192  strOdd << "\n";
193  }
194  }
195 
196  strDump << even << "\n";
197  strDump << "end\n";
198  strDump << odd << "\n";
199  strDump << "end\n";
200  strDump << "pause -1 \"Hit Enter to continue\"\n";
201  strDump << flush;
202  }
203 }
204 
205 QList<QPoint> Segment::fillPoints(const DocumentModelSegments &modelSegments)
206 {
207  LOG4CPP_INFO_S ((*mainCat)) << "Segment::fillPoints";
208 
209  if (modelSegments.fillCorners()) {
210  return fillPointsFillingCorners(modelSegments);
211  } else {
212  return fillPointsWithoutFillingCorners(modelSegments);
213  }
214 }
215 
216 QList<QPoint> Segment::fillPointsFillingCorners(const DocumentModelSegments &modelSegments)
217 {
218  QList<QPoint> list;
219 
220  if (m_lines.count() > 0) {
221 
222  double xLast = m_lines.first()->line().x1();
223  double yLast = m_lines.first()->line().y1();
224  double x, xNext;
225  double y, yNext;
226  double distanceCompleted = 0.0;
227 
228  // Variables for createAcceptablePoint
229  bool firstPoint = true;
230  double xPrev = m_lines.first()->line().x1();
231  double yPrev = m_lines.first()->line().y1();
232 
233  QList<SegmentLine*>::iterator itr;
234  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
235 
236  SegmentLine *line = *itr;
237 
238  ENGAUGE_CHECK_PTR(line);
239  xNext = double (line->line().x2());
240  yNext = double (line->line().y2());
241 
242  double xStart = double (line->line().x1());
243  double yStart = double (line->line().y1());
244  if (isCorner (yPrev, yStart, yNext)) {
245 
246  // Insert a corner point
247  createAcceptablePoint(&firstPoint, &list, &xPrev, &yPrev, xStart, yStart);
248  distanceCompleted = 0.0;
249  }
250 
251  // Distance formula
252  double segmentLength = sqrt((xNext - xLast) * (xNext - xLast) + (yNext - yLast) * (yNext - yLast));
253  if (segmentLength > 0.0) {
254 
255  // Loop since we might need to insert multiple points within a single line. This
256  // is the case when removeUnneededLines has consolidated many segment lines
257  while (distanceCompleted <= segmentLength) {
258 
259  double s = distanceCompleted / segmentLength;
260 
261  // Coordinates of new point
262  x = (1.0 - s) * xLast + s * xNext;
263  y = (1.0 - s) * yLast + s * yNext;
264 
265  createAcceptablePoint(&firstPoint, &list, &xPrev, &yPrev, x, y);
266 
267  distanceCompleted += modelSegments.pointSeparation();
268  }
269 
270  distanceCompleted -= segmentLength;
271  }
272 
273  xLast = xNext;
274  yLast = yNext;
275  }
276  }
277 
278  return list;
279 }
280 
281 QPointF Segment::firstPoint () const
282 {
283  LOG4CPP_INFO_S ((*mainCat)) << "Segment::firstPoint"
284  << " lineCount=" << m_lines.count();
285 
286  // There has to be at least one SegmentLine since this only gets called when a SegmentLine is clicked on
287  ENGAUGE_ASSERT (m_lines.count () > 0);
288 
289  SegmentLine *line = m_lines.first();
290  QPointF pos = line->line().p1();
291 
292  LOG4CPP_INFO_S ((*mainCat)) << "Segment::firstPoint"
293  << " pos=" << QPointFToString (pos).toLatin1().data();
294 
295  return pos;
296 }
297 
299 {
300  LOG4CPP_INFO_S ((*mainCat)) << "Segment::forwardMousePress"
301  << " segmentLines=" << m_lines.count();
302 
304 }
305 
306 bool Segment::isCorner (double yLast,
307  double yPrev,
308  double yNext) const
309 {
310  // Rather than deal with slopes, and a risk of dividing by zero, we just use the y deltas
311  double deltaYBefore = yPrev - yLast;
312  double deltaYAfter = yNext - yPrev;
313  bool upThenAcrossOrDown = (deltaYBefore > 0) && (deltaYAfter <= 0);
314  bool downThenAcrossOrUp = (deltaYBefore < 0) && (deltaYAfter >= 0);
315 
316  return upThenAcrossOrDown || downThenAcrossOrUp;
317 }
318 
319 QList<QPoint> Segment::fillPointsWithoutFillingCorners(const DocumentModelSegments &modelSegments)
320 {
321  QList<QPoint> list;
322 
323  if (m_lines.count() > 0) {
324 
325  double xLast = m_lines.first()->line().x1();
326  double yLast = m_lines.first()->line().y1();
327  double x, xNext;
328  double y, yNext;
329  double distanceCompleted = 0.0;
330 
331  // Variables for createAcceptablePoint
332  bool firstPoint = true;
333  double xPrev = m_lines.first()->line().x1();
334  double yPrev = m_lines.first()->line().y1();
335 
336  QList<SegmentLine*>::iterator itr;
337  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
338 
339  SegmentLine *line = *itr;
340 
341  ENGAUGE_CHECK_PTR(line);
342  xNext = double (line->line().x2());
343  yNext = double (line->line().y2());
344 
345  // Distance formula
346  double segmentLength = sqrt((xNext - xLast) * (xNext - xLast) + (yNext - yLast) * (yNext - yLast));
347  if (segmentLength > 0.0) {
348 
349  // Loop since we might need to insert multiple points within a single line. This
350  // is the case when removeUnneededLines has consolidated many segment lines
351  while (distanceCompleted <= segmentLength) {
352 
353  double s = distanceCompleted / segmentLength;
354 
355  // Coordinates of new point
356  x = (1.0 - s) * xLast + s * xNext;
357  y = (1.0 - s) * yLast + s * yNext;
358 
359  createAcceptablePoint(&firstPoint, &list, &xPrev, &yPrev, x, y);
360 
361  distanceCompleted += modelSegments.pointSeparation();
362  }
363 
364  distanceCompleted -= segmentLength;
365  }
366 
367  xLast = xNext;
368  yLast = yNext;
369  }
370  }
371 
372  return list;
373 }
374 
375 double Segment::length() const
376 {
377  return m_length;
378 }
379 
381 {
382  return m_lines.count();
383 }
384 
385 bool Segment::pointIsCloseToLine(double xLeft,
386  double yLeft,
387  double xInt,
388  double yInt,
389  double xRight,
390  double yRight)
391 {
392  double xProj, yProj, projectedDistanceOutsideLine, distanceToLine;
393  projectPointOntoLine(xInt, yInt, xLeft, yLeft, xRight, yRight, &xProj, &yProj, &projectedDistanceOutsideLine, &distanceToLine);
394 
395  return (
396  (xInt - xProj) * (xInt - xProj) +
397  (yInt - yProj) * (yInt - yProj) < 0.5 * 0.5);
398 }
399 
400 bool Segment::pointsAreCloseToLine(double xLeft,
401  double yLeft,
402  QList<QPoint> removedPoints,
403  double xRight,
404  double yRight)
405 {
406  QList<QPoint>::iterator itr;
407  for (itr = removedPoints.begin(); itr != removedPoints.end(); ++itr) {
408  if (!pointIsCloseToLine(xLeft,
409  yLeft,
410  double ((*itr).x()),
411  double ((*itr).y()),
412  xRight,
413  yRight)) {
414  return false;
415  }
416  }
417 
418  return true;
419 }
420 
421 void Segment::removeUnneededLines (int *foldedLines)
422 {
423  LOG4CPP_INFO_S ((*mainCat)) << "Segment::removeUnneededLines";
424 
425  QFile *fileDump = nullptr;
426  QTextStream *strDump = nullptr;
427  if (m_isGnuplot) {
428 
429  QString filename ("segment.gnuplot");
430 
431  std::cout << GNUPLOT_FILE_MESSAGE.toLatin1().data() << filename.toLatin1().data() << "\n";
432 
433  fileDump = new QFile (filename);
434  fileDump->open (QIODevice::WriteOnly | QIODevice::Text);
435  strDump = new QTextStream (fileDump);
436 
437  }
438 
439  // Pathological case is y=0.001*x*x, since the small slope can fool a naive algorithm
440  // into optimizing away all but one point at the origin and another point at the far right.
441  // From this we see that we cannot simply throw away points that were optimized away since they
442  // are needed later to see if we have diverged from the curve
443  SegmentLine *linePrevious = nullptr; // Previous line which corresponds to itrPrevious
444  QList<SegmentLine*>::iterator itr, itrPrevious;
445  QList<QPoint> removedPoints;
446  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
447 
448  SegmentLine *line = *itr;
449  ENGAUGE_CHECK_PTR(line);
450 
451  if (linePrevious != nullptr) {
452 
453  double xLeft = linePrevious->line().x1();
454  double yLeft = linePrevious->line().y1();
455  double xInt = linePrevious->line().x2();
456  double yInt = linePrevious->line().y2();
457 
458  // If linePrevious is the last line of one Segment and line is the first line of another Segment then
459  // it makes no sense to remove any point so we continue the loop
460  if (linePrevious->line().p2() == line->line().p1()) {
461 
462  double xRight = line->line().x2();
463  double yRight = line->line().y2();
464 
465  if (pointIsCloseToLine(xLeft, yLeft, xInt, yInt, xRight, yRight) &&
466  pointsAreCloseToLine(xLeft, yLeft, removedPoints, xRight, yRight)) {
467 
468  if (m_isGnuplot) {
469 
470  // Dump
471  dumpToGnuplot (*strDump,
472  qFloor (xInt),
473  qFloor (yInt),
474  linePrevious,
475  line);
476  }
477 
478  // Remove intermediate point, by removing older line and stretching new line to first point
479  ++(*foldedLines);
480 
481  LOG4CPP_DEBUG_S ((*mainCat)) << "Segment::removeUnneededLines"
482  << " segment=0x" << std::hex << static_cast<void*> (this) << std::dec
483  << " removing ("
484  << linePrevious->line().x1() << "," << linePrevious->line().y1() << ") to ("
485  << linePrevious->line().x2() << "," << linePrevious->line().y2() << ") "
486  << " and modifying ("
487  << line->line().x1() << "," << line->line().y1() << ") to ("
488  << line->line().x2() << "," << line->line().y2() << ") into ("
489  << xLeft << "," << yLeft << ") to ("
490  << xRight << "," << yRight << ")";
491 
492  removedPoints.append(QPoint(qFloor (xInt),
493  qFloor (yInt)));
494  m_lines.erase (itrPrevious);
495  delete linePrevious;
496 
497  // New line
498  line->setLine (xLeft, yLeft, xRight, yRight);
499 
500  } else {
501 
502  // Keeping this intermediate point and clear out the removed points list
503  removedPoints.clear();
504  }
505  }
506  }
507 
508  linePrevious = line;
509  itrPrevious = itr;
510 
511  // This theoretically should not be needed, but for some reason modifying the last point triggers a segfault
512  if (itr == m_lines.end()) {
513  break;
514  }
515  }
516 
517  if (strDump != nullptr) {
518 
519  // Final gnuplot processing
520  *strDump << "set terminal x11 persist\n";
521  fileDump->close ();
522  delete strDump;
523  delete fileDump;
524 
525  }
526 }
527 
528 void Segment::slotHover (bool hover)
529 {
530  LOG4CPP_INFO_S ((*mainCat)) << "Segment::slotHover";
531 
532  QList<SegmentLine*>::iterator itr, itrPrevious;
533  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
534 
535  SegmentLine *line = *itr;
536  line->setHover(hover);
537  }
538 }
539 
541 {
542  LOG4CPP_INFO_S ((*mainCat)) << "Segment::updateModelSegment";
543 
544  QList<SegmentLine*>::iterator itr;
545  for (itr = m_lines.begin(); itr != m_lines.end(); itr++) {
546 
547  SegmentLine *line = *itr;
548  line->updateModelSegment (modelSegments);
549  }
550 }
double pointSeparation() const
Get method for point separation.
void updateModelSegment(const DocumentModelSegments &modelSegments)
Update this segment line with new settings.
Definition: SegmentLine.cpp:88
void removeUnneededLines(int *foldedLines)
Try to compress a segment that was just completed, by folding together line from point i to point i+1...
Definition: Segment.cpp:421
bool fillCorners() const
Get method for fill corners.
Priority::Value getPriority() const
Returns unused priority.
Definition: Category.cpp:19
void slotHover(bool hover)
Slot for hover enter/leave events in the associated SegmentLines.
Definition: Segment.cpp:528
void forwardMousePress()
Forward mouse press event from a component SegmentLine that was just clicked on.
Definition: Segment.cpp:298
void appendColumn(int x, int y, const DocumentModelSegments &modelSegments)
Add some more pixels in a new column to an active segment.
Definition: Segment.cpp:42
QList< QPoint > fillPoints(const DocumentModelSegments &modelSegments)
Create evenly spaced points along the segment.
Definition: Segment.cpp:205
void signalMouseClickOnSegment(QPointF posSegmentStart)
Pass mouse press event, with coordinates of first point in the Segment since that info uniquely ident...
void updateModelSegment(const DocumentModelSegments &modelSegments)
Update this segment given the new settings.
Definition: Segment.cpp:540
int lineCount() const
Get method for number of lines.
Definition: Segment.cpp:380
Model for DlgSettingsSegments and CmdSettingsSegments.
QPointF firstPoint() const
Coordinates of first point in Segment.
Definition: Segment.cpp:281
This class is a special case of the standard QGraphicsLineItem for segments.
Definition: SegmentLine.h:17
void setHover(bool hover)
Apply/remove highlighting triggered by hover enter/leave.
Definition: SegmentLine.cpp:72
Segment(QGraphicsScene &scene, int yLast, bool isGnuplot)
Single constructor.
Definition: Segment.cpp:22
double length() const
Get method for length in pixels.
Definition: Segment.cpp:375