Changeset 14338 in josm for trunk/src/org/openstreetmap


Ignore:
Timestamp:
2018-10-16T00:32:46+02:00 (6 years ago)
Author:
Don-vip
Message:

fix #16755 - Cut overlapping GPX layers when merging (patch by Bjoeni, modified)

Location:
trunk/src/org/openstreetmap/josm
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/AbstractMergeAction.java

    r14336 r14338  
    99
    1010import javax.swing.DefaultListCellRenderer;
     11import javax.swing.JCheckBox;
    1112import javax.swing.JLabel;
    1213import javax.swing.JList;
     
    4041            label.setToolTipText(layer.getToolTipText());
    4142            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;
    4284        }
    4385    }
     
    84126     */
    85127    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) {
    86152        return askTargetLayer(targetLayers.toArray(new Layer[0]),
    87                 tr("Please select the target layer."),
     153                tr("Please select the target layer."), checkbox,
    88154                tr("Select target layer"),
    89                 tr("Merge"), "dialogs/mergedown");
    90     }
    91 
    92     /**
    93      * Asks a target layer.
     155                tr("Merge"), "dialogs/mergedown", showCheckbox, checkboxDefault);
     156    }
     157
     158    /**
     159     * Ask user to choose the target layer.
    94160     * @param <T> type of layer
    95161     * @param targetLayers array of proposed target layers
     
    100166     * @return chosen target layer
    101167     */
     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     */
    102186    @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) {
    104189        JosmComboBox<T> layerList = new JosmComboBox<>(targetLayers);
    105190        layerList.setRenderer(new LayerListCellRenderer());
     
    109194        pnl.add(new JLabel(label), GBC.eol());
    110195        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        }
    111203
    112204        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), title, buttonText, tr("Cancel"));
     
    115207        ed.showDialog();
    116208        if (ed.getValue() != 1) {
    117             return null;
    118         }
    119         return (T) layerList.getSelectedItem();
     209            return new TargetLayerDialogResult<>();
     210        }
     211        return new TargetLayerDialogResult<>((T) layerList.getSelectedItem(), cb != null && cb.isSelected());
    120212    }
    121213
  • trunk/src/org/openstreetmap/josm/actions/MergeLayerAction.java

    r12636 r14338  
    77import java.awt.event.ActionEvent;
    88import java.awt.event.KeyEvent;
     9import java.util.ArrayList;
    910import java.util.Collection;
    1011import java.util.Collections;
     
    1415import org.openstreetmap.josm.gui.MainApplication;
    1516import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
     17import org.openstreetmap.josm.gui.dialogs.layer.MergeGpxLayerDialog;
     18import org.openstreetmap.josm.gui.layer.GpxLayer;
    1619import org.openstreetmap.josm.gui.layer.Layer;
    1720import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    1821import org.openstreetmap.josm.gui.util.GuiHelper;
     22import org.openstreetmap.josm.spi.preferences.Config;
    1923import org.openstreetmap.josm.tools.ImageProvider;
    2024import org.openstreetmap.josm.tools.Logging;
     
    4852     */
    4953    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;
    5159        if (targetLayer == null)
    5260            return null;
     61
     62        if (onlygpx) {
     63            Config.getPref().putBoolean("mergelayer.gpx.cut", res.checkboxTicked);
     64        }
     65
    5366        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
    5496        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;
    68106                    }
     107                    targetLayer.mergeFrom(sourceLayer);
     108                    GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(sourceLayer));
     109                    layerMerged = true;
    69110                }
    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            }
    74117        });
    75118    }
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java

    r14120 r14338  
    5959    private final ArrayList<GpxTrack> privateTracks = new ArrayList<>();
    6060    /**
    61      * GXP routes in this file
     61     * GPX routes in this file
    6262     */
    6363    private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
     
    109109    private final ListenerList<GpxDataChangeListener> listeners = ListenerList.create();
    110110
     111    static class TimestampConfictException extends Exception {}
     112
     113    private List<GpxTrackSegmentSpan> segSpans;
     114
    111115    /**
    112116     * Merges data from another object.
     
    114118     */
    115119    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) {
    116131        if (storageFile == null && other.storageFile != null) {
    117132            storageFile = other.storageFile;
     
    131146            }
    132147        }
    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        }
    134156        other.privateRoutes.forEach(this::addRoute);
    135157        other.privateWaypoints.forEach(this::addWaypoint);
    136158        dataSources.addAll(other.dataSources);
    137159        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;
    138364    }
    139365
  • trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java

    r14134 r14338  
    307307        if (!(from instanceof GpxLayer))
    308308            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);
    310321        invalidate();
    311322    }
Note: See TracChangeset for help on using the changeset viewer.