- Timestamp:
- 2018-10-16T00:32:46+02:00 (6 years ago)
- Location:
- trunk
- Files:
-
- 7 added
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/actions/AbstractMergeAction.java
r14336 r14338 9 9 10 10 import javax.swing.DefaultListCellRenderer; 11 import javax.swing.JCheckBox; 11 12 import javax.swing.JLabel; 12 13 import javax.swing.JList; … … 40 41 label.setToolTipText(layer.getToolTipText()); 41 42 return label; 43 } 44 } 45 46 /** 47 * <code>TargetLayerDialogResult</code> returned by {@link #askTargetLayer(List, String, boolean)} 48 * containing the selectedTargetLayer and whether the checkbox is ticked 49 * @param <T> type of layer 50 * @since 14338 51 */ 52 public static class TargetLayerDialogResult<T extends Layer> { 53 /** 54 * The selected target layer of type T 55 */ 56 public T selectedTargetLayer; 57 /** 58 * Whether the checkbox is ticked 59 */ 60 public boolean checkboxTicked = false; 61 62 /** 63 * Constructs a new {@link TargetLayerDialogResult} 64 */ 65 public TargetLayerDialogResult() { 66 } 67 68 /** 69 * Constructs a new {@link TargetLayerDialogResult} 70 * @param sel the selected target layer of type T 71 */ 72 public TargetLayerDialogResult(T sel) { 73 selectedTargetLayer = sel; 74 } 75 76 /** 77 * Constructs a new {@link TargetLayerDialogResult} 78 * @param sel the selected target layer of type T 79 * @param ch whether the checkbox was ticked 80 */ 81 public TargetLayerDialogResult(T sel, boolean ch) { 82 selectedTargetLayer = sel; 83 checkboxTicked = ch; 42 84 } 43 85 } … … 84 126 */ 85 127 protected static Layer askTargetLayer(List<Layer> targetLayers) { 128 return askTargetLayer(targetLayers, false, null, false).selectedTargetLayer; 129 } 130 131 /** 132 * Ask user to choose the target layer and shows a checkbox. 133 * @param targetLayers list of candidate target layers. 134 * @param checkbox The text of the checkbox shown to the user. 135 * @param checkboxDefault whether the checkbox is ticked by default 136 * @return The {@link TargetLayerDialogResult} containing the chosen target layer and the state of the checkbox 137 */ 138 protected static TargetLayerDialogResult<Layer> askTargetLayer(List<Layer> targetLayers, String checkbox, boolean checkboxDefault) { 139 return askTargetLayer(targetLayers, true, checkbox, checkboxDefault); 140 } 141 142 /** 143 * Ask user to choose the target layer and shows a checkbox. 144 * @param targetLayers list of candidate target layers. 145 * @param showCheckbox whether the checkbox is shown 146 * @param checkbox The text of the checkbox shown to the user. 147 * @param checkboxDefault whether the checkbox is ticked by default 148 * @return The {@link TargetLayerDialogResult} containing the chosen target layer and the state of the checkbox 149 */ 150 protected static TargetLayerDialogResult<Layer> askTargetLayer(List<Layer> targetLayers, boolean showCheckbox, 151 String checkbox, boolean checkboxDefault) { 86 152 return askTargetLayer(targetLayers.toArray(new Layer[0]), 87 tr("Please select the target layer."), 153 tr("Please select the target layer."), checkbox, 88 154 tr("Select target layer"), 89 tr("Merge"), "dialogs/mergedown" );90 } 91 92 /** 93 * Ask s atarget layer.155 tr("Merge"), "dialogs/mergedown", showCheckbox, checkboxDefault); 156 } 157 158 /** 159 * Ask user to choose the target layer. 94 160 * @param <T> type of layer 95 161 * @param targetLayers array of proposed target layers … … 100 166 * @return chosen target layer 101 167 */ 168 public static <T extends Layer> T askTargetLayer(T[] targetLayers, String label, String title, String buttonText, String buttonIcon) { 169 return askTargetLayer(targetLayers, label, null, title, buttonText, buttonIcon, false, false).selectedTargetLayer; 170 } 171 172 /** 173 * Ask user to choose the target layer. Can show a checkbox. 174 * @param <T> type of layer 175 * @param targetLayers array of proposed target layers 176 * @param label label displayed in dialog 177 * @param checkbox text of the checkbox displayed 178 * @param title title of dialog 179 * @param buttonText text of button used to select target layer 180 * @param buttonIcon icon name of button used to select target layer 181 * @param showCheckbox whether the checkbox is shown 182 * @param checkboxDefault whether the checkbox is ticked by default 183 * @return The {@link TargetLayerDialogResult} containing the chosen target layer and the state of the checkbox 184 * @since 14338 185 */ 102 186 @SuppressWarnings("unchecked") 103 public static <T extends Layer> T askTargetLayer(T[] targetLayers, String label, String title, String buttonText, String buttonIcon) { 187 public static <T extends Layer> TargetLayerDialogResult<T> askTargetLayer(T[] targetLayers, String label, String checkbox, String title, 188 String buttonText, String buttonIcon, boolean showCheckbox, boolean checkboxDefault) { 104 189 JosmComboBox<T> layerList = new JosmComboBox<>(targetLayers); 105 190 layerList.setRenderer(new LayerListCellRenderer()); … … 109 194 pnl.add(new JLabel(label), GBC.eol()); 110 195 pnl.add(layerList, GBC.eol().fill(GBC.HORIZONTAL)); 196 197 JCheckBox cb = null; 198 if (showCheckbox) { 199 cb = new JCheckBox(checkbox); 200 cb.setSelected(checkboxDefault); 201 pnl.add(cb, GBC.eol()); 202 } 111 203 112 204 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), title, buttonText, tr("Cancel")); … … 115 207 ed.showDialog(); 116 208 if (ed.getValue() != 1) { 117 return n ull;118 } 119 return (T) layerList.getSelectedItem();209 return new TargetLayerDialogResult<>(); 210 } 211 return new TargetLayerDialogResult<>((T) layerList.getSelectedItem(), cb != null && cb.isSelected()); 120 212 } 121 213 -
trunk/src/org/openstreetmap/josm/actions/MergeLayerAction.java
r12636 r14338 7 7 import java.awt.event.ActionEvent; 8 8 import java.awt.event.KeyEvent; 9 import java.util.ArrayList; 9 10 import java.util.Collection; 10 11 import java.util.Collections; … … 14 15 import org.openstreetmap.josm.gui.MainApplication; 15 16 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 17 import org.openstreetmap.josm.gui.dialogs.layer.MergeGpxLayerDialog; 18 import org.openstreetmap.josm.gui.layer.GpxLayer; 16 19 import org.openstreetmap.josm.gui.layer.Layer; 17 20 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 18 21 import org.openstreetmap.josm.gui.util.GuiHelper; 22 import org.openstreetmap.josm.spi.preferences.Config; 19 23 import org.openstreetmap.josm.tools.ImageProvider; 20 24 import org.openstreetmap.josm.tools.Logging; … … 48 52 */ 49 53 protected Future<?> doMerge(List<Layer> targetLayers, final Collection<Layer> sourceLayers) { 50 final Layer targetLayer = askTargetLayer(targetLayers); 54 final boolean onlygpx = targetLayers.stream().noneMatch(l -> !(l instanceof GpxLayer)); 55 final TargetLayerDialogResult<Layer> res = askTargetLayer(targetLayers, onlygpx, 56 tr("Cut timewise overlapping parts of tracks"), 57 onlygpx && Config.getPref().getBoolean("mergelayer.gpx.cut", false)); 58 final Layer targetLayer = res.selectedTargetLayer; 51 59 if (targetLayer == null) 52 60 return null; 61 62 if (onlygpx) { 63 Config.getPref().putBoolean("mergelayer.gpx.cut", res.checkboxTicked); 64 } 65 53 66 final Object actionName = getValue(NAME); 67 if (onlygpx && res.checkboxTicked) { 68 List<GpxLayer> layers = new ArrayList<>(); 69 layers.add((GpxLayer) targetLayer); 70 for (Layer sl : sourceLayers) { 71 if (sl != null && !sl.equals(targetLayer)) { 72 layers.add((GpxLayer) sl); 73 } 74 } 75 final MergeGpxLayerDialog d = new MergeGpxLayerDialog(MainApplication.getMainFrame(), layers); 76 77 if (d.showDialog().getValue() == 1) { 78 79 final boolean connect = d.connectCuts(); 80 final List<GpxLayer> sortedLayers = d.getSortedLayers(); 81 82 return MainApplication.worker.submit(() -> { 83 final long start = System.currentTimeMillis(); 84 85 for (int i = sortedLayers.size() - 2; i >= 0; i--) { 86 final GpxLayer lower = sortedLayers.get(i + 1); 87 sortedLayers.get(i).mergeFrom(lower, true, connect); 88 GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(lower)); 89 } 90 91 Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start))); 92 }); 93 } 94 } 95 54 96 return MainApplication.worker.submit(() -> { 55 final long start = System.currentTimeMillis(); 56 boolean layerMerged = false; 57 for (final Layer sourceLayer: sourceLayers) { 58 if (sourceLayer != null && !sourceLayer.equals(targetLayer)) { 59 if (sourceLayer instanceof OsmDataLayer && targetLayer instanceof OsmDataLayer 60 && ((OsmDataLayer) sourceLayer).isUploadDiscouraged() != ((OsmDataLayer) targetLayer).isUploadDiscouraged() 61 && Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() -> 62 warnMergingUploadDiscouragedLayers(sourceLayer, targetLayer)))) { 63 break; 64 } 65 targetLayer.mergeFrom(sourceLayer); 66 GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(sourceLayer)); 67 layerMerged = true; 97 final long start = System.currentTimeMillis(); 98 boolean layerMerged = false; 99 for (final Layer sourceLayer: sourceLayers) { 100 if (sourceLayer != null && !sourceLayer.equals(targetLayer)) { 101 if (sourceLayer instanceof OsmDataLayer && targetLayer instanceof OsmDataLayer 102 && ((OsmDataLayer) sourceLayer).isUploadDiscouraged() != ((OsmDataLayer) targetLayer).isUploadDiscouraged() 103 && Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() -> 104 warnMergingUploadDiscouragedLayers(sourceLayer, targetLayer)))) { 105 break; 68 106 } 107 targetLayer.mergeFrom(sourceLayer); 108 GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(sourceLayer)); 109 layerMerged = true; 69 110 } 70 if (layerMerged) { 71 getLayerManager().setActiveLayer(targetLayer); 72 Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start))); 73 } 111 } 112 113 if (layerMerged) { 114 getLayerManager().setActiveLayer(targetLayer); 115 Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start))); 116 } 74 117 }); 75 118 } -
trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java
r14120 r14338 59 59 private final ArrayList<GpxTrack> privateTracks = new ArrayList<>(); 60 60 /** 61 * G XProutes in this file61 * GPX routes in this file 62 62 */ 63 63 private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>(); … … 109 109 private final ListenerList<GpxDataChangeListener> listeners = ListenerList.create(); 110 110 111 static class TimestampConfictException extends Exception {} 112 113 private List<GpxTrackSegmentSpan> segSpans; 114 111 115 /** 112 116 * Merges data from another object. … … 114 118 */ 115 119 public synchronized void mergeFrom(GpxData other) { 120 mergeFrom(other, false, false); 121 } 122 123 /** 124 * Merges data from another object. 125 * @param other existing GPX data 126 * @param cutOverlapping whether overlapping parts of the given track should be removed 127 * @param connect whether the tracks should be connected on cuts 128 * @since 14338 129 */ 130 public synchronized void mergeFrom(GpxData other, boolean cutOverlapping, boolean connect) { 116 131 if (storageFile == null && other.storageFile != null) { 117 132 storageFile = other.storageFile; … … 131 146 } 132 147 } 133 other.privateTracks.forEach(this::addTrack); 148 149 if (cutOverlapping) { 150 for (GpxTrack trk : other.privateTracks) { 151 cutOverlapping(trk, connect); 152 } 153 } else { 154 other.privateTracks.forEach(this::addTrack); 155 } 134 156 other.privateRoutes.forEach(this::addRoute); 135 157 other.privateWaypoints.forEach(this::addWaypoint); 136 158 dataSources.addAll(other.dataSources); 137 159 fireInvalidate(); 160 } 161 162 private void cutOverlapping(GpxTrack trk, boolean connect) { 163 List<GpxTrackSegment> segsOld = new ArrayList<>(trk.getSegments()); 164 List<GpxTrackSegment> segsNew = new ArrayList<>(); 165 for (GpxTrackSegment seg : segsOld) { 166 GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg); 167 if (s != null && anySegmentOverlapsWith(s)) { 168 List<WayPoint> wpsNew = new ArrayList<>(); 169 List<WayPoint> wpsOld = new ArrayList<>(seg.getWayPoints()); 170 if (s.isInverted()) { 171 Collections.reverse(wpsOld); 172 } 173 boolean split = false; 174 WayPoint prevLastOwnWp = null; 175 Date prevWpTime = null; 176 for (WayPoint wp : wpsOld) { 177 Date wpTime = wp.setTimeFromAttribute(); 178 boolean overlap = false; 179 if (wpTime != null) { 180 for (GpxTrackSegmentSpan ownspan : getSegmentSpans()) { 181 if (wpTime.after(ownspan.firstTime) && wpTime.before(ownspan.lastTime)) { 182 overlap = true; 183 if (connect) { 184 if (!split) { 185 wpsNew.add(ownspan.getFirstWp()); 186 } else { 187 connectTracks(prevLastOwnWp, ownspan, trk.getAttributes()); 188 } 189 prevLastOwnWp = ownspan.getLastWp(); 190 } 191 split = true; 192 break; 193 } else if (connect && prevWpTime != null 194 && prevWpTime.before(ownspan.firstTime) 195 && wpTime.after(ownspan.lastTime)) { 196 // the overlapping high priority track is shorter than the distance 197 // between two waypoints of the low priority track 198 if (split) { 199 connectTracks(prevLastOwnWp, ownspan, trk.getAttributes()); 200 prevLastOwnWp = ownspan.getLastWp(); 201 } else { 202 wpsNew.add(ownspan.getFirstWp()); 203 // splitting needs to be handled here, 204 // because other high priority tracks between the same waypoints could follow 205 if (!wpsNew.isEmpty()) { 206 segsNew.add(new ImmutableGpxTrackSegment(wpsNew)); 207 } 208 if (!segsNew.isEmpty()) { 209 privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes())); 210 } 211 segsNew = new ArrayList<>(); 212 wpsNew = new ArrayList<>(); 213 wpsNew.add(ownspan.getLastWp()); 214 // therefore no break, because another segment could overlap, see above 215 } 216 } 217 } 218 prevWpTime = wpTime; 219 } 220 if (!overlap) { 221 if (split) { 222 //track has to be split, because we have an overlapping short track in the middle 223 if (!wpsNew.isEmpty()) { 224 segsNew.add(new ImmutableGpxTrackSegment(wpsNew)); 225 } 226 if (!segsNew.isEmpty()) { 227 privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes())); 228 } 229 segsNew = new ArrayList<>(); 230 wpsNew = new ArrayList<>(); 231 if (connect && prevLastOwnWp != null) { 232 wpsNew.add(new WayPoint(prevLastOwnWp)); 233 } 234 prevLastOwnWp = null; 235 split = false; 236 } 237 wpsNew.add(new WayPoint(wp)); 238 } 239 } 240 if (!wpsNew.isEmpty()) { 241 segsNew.add(new ImmutableGpxTrackSegment(wpsNew)); 242 } 243 } else { 244 segsNew.add(seg); 245 } 246 } 247 if (segsNew.equals(segsOld)) { 248 privateTracks.add(trk); 249 } else if (!segsNew.isEmpty()) { 250 privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes())); 251 } 252 } 253 254 private void connectTracks(WayPoint prevWp, GpxTrackSegmentSpan span, Map<String, Object> attr) { 255 if (prevWp != null && !span.lastEquals(prevWp)) { 256 privateTracks.add(new ImmutableGpxTrack(Arrays.asList(Arrays.asList(new WayPoint(prevWp), span.getFirstWp())), attr)); 257 } 258 } 259 260 static class GpxTrackSegmentSpan { 261 262 public final Date firstTime; 263 public final Date lastTime; 264 private final boolean inv; 265 private final WayPoint firstWp; 266 private final WayPoint lastWp; 267 268 GpxTrackSegmentSpan(WayPoint a, WayPoint b) { 269 Date at = a.getTime(); 270 Date bt = b.getTime(); 271 inv = bt.before(at); 272 if (inv) { 273 firstWp = b; 274 firstTime = bt; 275 lastWp = a; 276 lastTime = at; 277 } else { 278 firstWp = a; 279 firstTime = at; 280 lastWp = b; 281 lastTime = bt; 282 } 283 } 284 285 public WayPoint getFirstWp() { 286 return new WayPoint(firstWp); 287 } 288 289 public WayPoint getLastWp() { 290 return new WayPoint(lastWp); 291 } 292 293 // no new instances needed, therefore own methods for that 294 295 public boolean firstEquals(Object other) { 296 return firstWp.equals(other); 297 } 298 299 public boolean lastEquals(Object other) { 300 return lastWp.equals(other); 301 } 302 303 public boolean isInverted() { 304 return inv; 305 } 306 307 public boolean overlapsWith(GpxTrackSegmentSpan other) { 308 return (firstTime.before(other.lastTime) && other.firstTime.before(lastTime)) 309 || (other.firstTime.before(lastTime) && firstTime.before(other.lastTime)); 310 } 311 312 public static GpxTrackSegmentSpan tryGetFromSegment(GpxTrackSegment seg) { 313 WayPoint b = getNextWpWithTime(seg, true); 314 if (b != null) { 315 WayPoint e = getNextWpWithTime(seg, false); 316 if (e != null) { 317 return new GpxTrackSegmentSpan(b, e); 318 } 319 } 320 return null; 321 } 322 323 private static WayPoint getNextWpWithTime(GpxTrackSegment seg, boolean forward) { 324 List<WayPoint> wps = new ArrayList<>(seg.getWayPoints()); 325 for (int i = forward ? 0 : wps.size() - 1; i >= 0 && i < wps.size(); i += forward ? 1 : -1) { 326 if (wps.get(i).setTimeFromAttribute() != null) { 327 return wps.get(i); 328 } 329 } 330 return null; 331 } 332 } 333 334 /** 335 * Get a list of SegmentSpans containing the beginning and end of each segment 336 * @return the list of SegmentSpans 337 * @since 14338 338 */ 339 public synchronized List<GpxTrackSegmentSpan> getSegmentSpans() { 340 if (segSpans == null) { 341 segSpans = new ArrayList<>(); 342 for (GpxTrack trk : privateTracks) { 343 for (GpxTrackSegment seg : trk.getSegments()) { 344 GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg); 345 if (s != null) { 346 segSpans.add(s); 347 } 348 } 349 } 350 segSpans.sort((o1, o2) -> { 351 return o1.firstTime.compareTo(o2.firstTime); 352 }); 353 } 354 return segSpans; 355 } 356 357 private boolean anySegmentOverlapsWith(GpxTrackSegmentSpan other) { 358 for (GpxTrackSegmentSpan s : getSegmentSpans()) { 359 if (s.overlapsWith(other)) { 360 return true; 361 } 362 } 363 return false; 138 364 } 139 365 -
trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
r14134 r14338 307 307 if (!(from instanceof GpxLayer)) 308 308 throw new IllegalArgumentException("not a GpxLayer: " + from); 309 data.mergeFrom(((GpxLayer) from).data); 309 mergeFrom((GpxLayer) from, false, false); 310 } 311 312 /** 313 * Merges the given GpxLayer into this layer and can remove timewise overlapping parts of the given track 314 * @param from The GpxLayer that gets merged into this one 315 * @param cutOverlapping whether overlapping parts of the given track should be removed 316 * @param connect whether the tracks should be connected on cuts 317 * @since 14338 318 */ 319 public void mergeFrom(GpxLayer from, boolean cutOverlapping, boolean connect) { 320 data.mergeFrom(from.data, cutOverlapping, connect); 310 321 invalidate(); 311 322 } -
trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxDataTest.java
r14120 r14338 8 8 import static org.junit.Assert.assertTrue; 9 9 10 import java.io.IOException; 10 11 import java.util.ArrayList; 11 12 import java.util.Arrays; … … 28 29 import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeListener; 29 30 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 31 import org.openstreetmap.josm.io.GpxReaderTest; 30 32 import org.openstreetmap.josm.testutils.JOSMTestRules; 31 33 import org.openstreetmap.josm.tools.ListenerList; 34 import org.xml.sax.SAXException; 32 35 33 36 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; … … 55 58 data = new GpxData(); 56 59 } 57 58 60 59 61 /** … … 83 85 assertTrue(data.getWaypoints().contains(newWP)); 84 86 assertTrue(data.getWaypoints().contains(existingWP)); 87 } 88 89 /** 90 * Test method for {@link GpxData#mergeFrom(GpxData, boolean, boolean)} including cutting/connecting tracks using actual files. 91 * @throws Exception if the track cannot be parsed 92 */ 93 @Test 94 public void testMergeFromFiles() throws Exception { 95 testMerge(false, false, "Merged-all"); // regular merging 96 testMerge(true, false, "Merged-cut"); // cut overlapping tracks, but do not connect them 97 testMerge(true, true, "Merged-cut-connect"); // cut overlapping tracks and connect them 98 } 99 100 private static void testMerge(boolean cut, boolean connect, String exp) throws IOException, SAXException { 101 final GpxData own = getGpx("Layer1"); 102 final GpxData other = getGpx("Layer2"); 103 final GpxData expected = getGpx(exp); 104 own.mergeFrom(other, cut, connect); 105 assertEquals(expected, own); 106 } 107 108 private static GpxData getGpx(String file) throws IOException, SAXException { 109 return GpxReaderTest.parseGpxData(TestUtils.getTestDataRoot() + "mergelayers/" + file + ".gpx"); 85 110 } 86 111 … … 449 474 TestUtils.assumeWorkingEqualsVerifier(); 450 475 EqualsVerifier.forClass(GpxData.class).usingGetClass() 451 .withIgnoredFields("attr", "creator", "fromServer", "storageFile", "listeners", "tracks", "routes", "waypoints", "proxy" )476 .withIgnoredFields("attr", "creator", "fromServer", "storageFile", "listeners", "tracks", "routes", "waypoints", "proxy", "segSpans") 452 477 .withPrefabValues(WayPoint.class, new WayPoint(LatLon.NORTH_POLE), new WayPoint(LatLon.SOUTH_POLE)) 453 478 .withPrefabValues(ListenerList.class, ListenerList.create(), ListenerList.create())
Note:
See TracChangeset
for help on using the changeset viewer.