--- /dev/null
+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;
+ }
+}