]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/function/sew/SewTrackSegmentsFunction.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / function / sew / SewTrackSegmentsFunction.java
diff --git a/src/tim/prune/function/sew/SewTrackSegmentsFunction.java b/src/tim/prune/function/sew/SewTrackSegmentsFunction.java
new file mode 100644 (file)
index 0000000..44ad9db
--- /dev/null
@@ -0,0 +1,331 @@
+package tim.prune.function.sew;
+
+import java.util.TreeSet;
+
+import tim.prune.App;
+import tim.prune.GenericFunction;
+import tim.prune.I18nManager;
+import tim.prune.UpdateMessageBroker;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Track;
+import tim.prune.function.Cancellable;
+import tim.prune.gui.GenericProgressDialog;
+import tim.prune.undo.UndoException;
+import tim.prune.undo.UndoSewSegments;
+
+/**
+ * Function to sew the track segments together if possible,
+ * reversing and moving as required
+ */
+public class SewTrackSegmentsFunction extends GenericFunction implements Runnable, Cancellable
+{
+       /** Set of sorted segment endpoints */
+       private TreeSet<SegmentEnd> _nodes = null;
+       /** Cancel flag */
+       private boolean _cancelled = false;
+
+
+       /** Constructor */
+       public SewTrackSegmentsFunction(App inApp) {
+               super(inApp);
+       }
+
+       /** @return name key */
+       public String getNameKey() {
+               return "function.sewsegments";
+       }
+
+       /**
+        * Execute the function
+        */
+       public void begin()
+       {
+               // Run in separate thread, with progress bar
+               new Thread(this).start();
+       }
+
+       /**
+        * Run the function in a separate thread
+        */
+       public void run()
+       {
+               // Make a progress bar
+               GenericProgressDialog progressDialog = new GenericProgressDialog(getNameKey(), null, _parentFrame, this);
+               progressDialog.show();
+               // Make an undo object to store the current points and sequence
+               UndoSewSegments undo = new UndoSewSegments(_app.getTrackInfo().getTrack());
+
+               // Make list of all the segment ends
+               _nodes = buildNodeList(_app.getTrackInfo().getTrack());
+               final int numNodes = (_nodes == null ? 0 : _nodes.size());
+               if (numNodes < 4)
+               {
+                       System.out.println("Can't do anything with this, not enough segments");
+                       progressDialog.close();
+               }
+               else
+               {
+                       progressDialog.showProgress(10, 100); // Say 10% for building the nodes
+
+                       // Disable messaging because we're probably doing a lot of reverses and moves
+                       UpdateMessageBroker.enableMessaging(false);
+                       // Set now contains all pairs of segment ends, ends at the same location are adjacent
+                       // Now we're just interested in pairs of nodes, not three or more at the same location
+                       SegmentEnd firstNode = null, secondNode = null;
+                       int numJoins = 0, currNode = 0;
+                       for (SegmentEnd node : _nodes)
+                       {
+                               if (!node.isActive()) {continue;}
+                               if (firstNode == null)
+                               {
+                                       firstNode = node;
+                               }
+                               else if (secondNode == null)
+                               {
+                                       if (node.atSamePointAs(firstNode)) {
+                                               secondNode = node;
+                                       }
+                                       else {
+                                               firstNode = node;
+                                       }
+                               }
+                               else if (node.atSamePointAs(secondNode))
+                               {
+                                       // Found three colocated nodes, not interested
+                                       firstNode = secondNode = null;
+                               }
+                               else
+                               {
+                                       // Found a pair
+                                       joinSegments(firstNode, secondNode);
+                                       numJoins++;
+                                       firstNode = node; secondNode = null;
+                               }
+                               if (_cancelled) {break;}
+                               final double fractionDone = 1.0 * currNode / numNodes;
+                               progressDialog.showProgress(10 + (int) (fractionDone * 80), 100);
+                               currNode++;
+                       }
+                       if (firstNode != null && secondNode != null)
+                       {
+                               joinSegments(firstNode, secondNode);
+                               numJoins++;
+                       }
+
+                       progressDialog.showProgress(90, 100); // Say 90%, only duplicate point deletion left
+
+                       // Delete the duplicate points
+                       final int numDeleted = _cancelled ? 0 : deleteSegmentStartPoints(_app.getTrackInfo().getTrack());
+
+                       progressDialog.close();
+                       // Enable the messaging again
+                       UpdateMessageBroker.enableMessaging(true);
+                       if (_cancelled) // TODO: Also revert if any of the operations failed
+                       {
+                               // try to restore using undo object
+                               try {
+                                       undo.performUndo(_app.getTrackInfo());
+                               }
+                               catch (UndoException ue) {
+                                       _app.showErrorMessage("oops", "CANNOT UNDO");
+                               }
+                       }
+                       else if (numJoins > 0 || numDeleted > 0)
+                       {
+                               // Give Undo object back to App to confirm
+                               final String confirmMessage = (numJoins > 0 ? I18nManager.getTextWithNumber("confirm.sewsegments", numJoins)
+                                       : "" + numDeleted + " " + I18nManager.getText("confirm.deletepoint.multi"));
+                               _app.completeFunction(undo, confirmMessage);
+                               UpdateMessageBroker.informSubscribers();
+                       }
+                       else
+                       {
+                               // Nothing done
+                               _app.showErrorMessageNoLookup(getNameKey(), I18nManager.getTextWithNumber("error.sewsegments.nothingdone", numNodes/2));
+                       }
+               }
+       }
+
+       /**
+        * Build a sorted list of all the segment start points and end points
+        * Creates a TreeSet containing two SegmentEnd objects for each segment
+        * @param inTrack track object
+        * @return sorted list of segment ends
+        */
+       private static TreeSet<SegmentEnd> buildNodeList(Track inTrack)
+       {
+               TreeSet<SegmentEnd> nodes = new TreeSet<SegmentEnd>();
+               final int numPoints = inTrack.getNumPoints();
+               DataPoint prevTrackPoint = null;
+               int       prevTrackPointIndex = -1;
+               SegmentEnd segmentStart = null;
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = inTrack.getPoint(i);
+                       if (!point.isWaypoint() && !point.hasMedia())
+                       {
+                               if (point.getSegmentStart())
+                               {
+                                       // Start of new segment - does previous one need to be saved?
+                                       if (segmentStart != null && prevTrackPointIndex > 0 && prevTrackPointIndex != segmentStart.getPointIndex())
+                                       {
+                                               // Finish previous segment and store in list
+                                               SegmentEnd segmentEnd = new SegmentEnd(prevTrackPoint, prevTrackPointIndex);
+                                               segmentStart.setOtherEnd(segmentEnd);
+                                               segmentEnd.setOtherEnd(segmentStart);
+                                               // Don't add closed loops
+                                               if (!segmentStart.atSamePointAs(segmentEnd))
+                                               {
+                                                       nodes.add(segmentStart);
+                                                       nodes.add(segmentEnd);
+                                               }
+                                       }
+                                       // Remember segment start
+                                       segmentStart = new SegmentEnd(point, i);
+                               }
+                               prevTrackPoint = point;
+                               prevTrackPointIndex = i;
+                       }
+               }
+               // Probably need to deal with segmentStart and prevTrackPoint, prevTrackPointIndex
+               if (segmentStart != null && prevTrackPointIndex > 0 && prevTrackPointIndex != segmentStart.getPointIndex())
+               {
+                       // Finish last segment and store in list
+                       SegmentEnd segmentEnd = new SegmentEnd(prevTrackPoint, prevTrackPointIndex);
+                       segmentStart.setOtherEnd(segmentEnd);
+                       segmentEnd.setOtherEnd(segmentStart);
+                       // Don't add closed loops
+                       if (!segmentStart.atSamePointAs(segmentEnd))
+                       {
+                               nodes.add(segmentStart);
+                               nodes.add(segmentEnd);
+                       }
+               }
+               return nodes;
+       }
+
+       /**
+        * Join the two segments together represented by the given nodes
+        * @param inFirstNode first node (order doesn't matter)
+        * @param inSecondNode other node
+        */
+       private void joinSegments(SegmentEnd inFirstNode, SegmentEnd inSecondNode)
+       {
+               final Track track = _app.getTrackInfo().getTrack();
+               // System.out.println("Join: (" + inFirstNode.getPointIndex() + "-" + inFirstNode.getOtherPointIndex() + ") with ("
+               //      + inSecondNode.getPointIndex() + "-" + inSecondNode.getOtherPointIndex() + ")");
+               // System.out.println("    : " + (inFirstNode.isStart() ? "start" : "end") + " to " + (inSecondNode.isStart() ? "start" : "end"));
+               final boolean moveSecondBeforeFirst = inFirstNode.isStart();
+               if (inFirstNode.isStart() == inSecondNode.isStart())
+               {
+                       if (track.reverseRange(inSecondNode.getEarlierIndex(), inSecondNode.getLaterIndex()))
+                       {
+                               inSecondNode.reverseSegment();
+                               // System.out.println("    : Reverse segment: " + inSecondNode.getEarlierIndex() + " - " + inSecondNode.getLaterIndex());
+                       }
+                       else {
+                               System.err.println("Oops, reverse range didn't work");
+                               // TODO: Abort?
+                       }
+               }
+               if (moveSecondBeforeFirst)
+               {
+                       if ((inSecondNode.getLaterIndex()+1) != inFirstNode.getPointIndex())
+                       {
+                               // System.out.println("    : Move second segment before first");
+                               cutAndMoveSegment(inSecondNode.getEarlierIndex(), inSecondNode.getLaterIndex(), inFirstNode.getPointIndex());
+                       }
+               }
+               else if ((inFirstNode.getLaterIndex()+1) != inSecondNode.getPointIndex())
+               {
+                       // System.out.println("    : Move first segment before second (because " + (inFirstNode.getLaterIndex()+1) + " isn't " + inSecondNode.getPointIndex() + ")");
+                       cutAndMoveSegment(inFirstNode.getEarlierIndex(), inFirstNode.getLaterIndex(), inSecondNode.getPointIndex());
+               }
+               // Now merge the SegmentEnds so that they're not split up again
+               if (inSecondNode.getEarlierIndex() == (inFirstNode.getLaterIndex()+1)) {
+                       // System.out.println("second node is now directly after the first node");
+               }
+               else if (inFirstNode.getEarlierIndex() == (inSecondNode.getLaterIndex()+1)) {
+                       //System.out.println("first node is now directly after the second node");
+               }
+               else {
+                       System.err.println("Why aren't the segments directly consecutive after the join?");
+               }
+               // Find the earliest and latest ends of these two segments
+               SegmentEnd earlierSegmentEnd = (inFirstNode.getEarlierIndex() < inSecondNode.getEarlierIndex() ? inFirstNode : inSecondNode).getEarlierEnd();
+               SegmentEnd laterSegmentEnd   = (inFirstNode.getLaterIndex() > inSecondNode.getLaterIndex() ? inFirstNode : inSecondNode).getLaterEnd();
+               // Get rid of the inner two segment ends, join the earliest and latest together
+               earlierSegmentEnd.getOtherEnd().deactivate();
+               laterSegmentEnd.getOtherEnd().deactivate();
+               earlierSegmentEnd.setOtherEnd(laterSegmentEnd);
+               laterSegmentEnd.setOtherEnd(earlierSegmentEnd);
+       }
+
+       /**
+        * Cut and move the segment to a different position
+        * @param inSegmentStart start index of segment
+        * @param inSegmentEnd end index of segment
+        * @param inMoveToPos index before which the segment should be moved
+        */
+       private void cutAndMoveSegment(int inSegmentStart, int inSegmentEnd, int inMoveToPos)
+       {
+               if (!_app.getTrackInfo().getTrack().cutAndMoveSection(inSegmentStart, inSegmentEnd, inMoveToPos))
+               {
+                       System.err.println("   Oops, cut and move didn't work");
+                       // TODO: Throw exception? Return false?
+               }
+               else
+               {
+                       // Loop over each node to inform it of the index changes
+                       for (SegmentEnd node : _nodes) {
+                               node.adjustPointIndex(inSegmentStart, inSegmentEnd, inMoveToPos);
+                       }
+               }
+       }
+
+       /**
+        * The final step of the sewing, removing the duplicate points at the start of each segment
+        * @param inTrack track object
+        * @return number of points deleted
+        */
+       private static int deleteSegmentStartPoints(Track inTrack)
+       {
+               final int numPoints = inTrack.getNumPoints();
+               boolean[] deleteFlags = new boolean[numPoints];
+               // Loop over points in track, setting delete flags
+               int numToDelete = 0;
+               DataPoint prevPoint = null;
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = inTrack.getPoint(i);
+                       if (!point.isWaypoint())
+                       {
+                               if (prevPoint != null && point.getSegmentStart() && point.isDuplicate(prevPoint))
+                               {
+                                       deleteFlags[i] = true;
+                                       numToDelete++;
+                               }
+                               prevPoint = point;
+                       }
+               }
+               // Make new datapoint array of the right size
+               DataPoint[] pointCopies = new DataPoint[numPoints - numToDelete];
+               // Loop over points again, keeping the ones we want
+               int copyIndex = 0;
+               for (int i=0; i<numPoints; i++)
+               {
+                       if (!deleteFlags[i]) {
+                               pointCopies[copyIndex] = inTrack.getPoint(i);
+                               copyIndex++;
+                       }
+               }
+               // Finally, replace the copied points in the track
+               inTrack.replaceContents(pointCopies);
+               return numToDelete;
+       }
+
+       /** Function cancelled by progress dialog */
+       public void cancel() {
+               _cancelled = true;
+       }
+}