001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.List;
013import java.util.concurrent.Future;
014
015import org.openstreetmap.josm.gui.MainApplication;
016import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
017import org.openstreetmap.josm.gui.dialogs.layer.MergeGpxLayerDialog;
018import org.openstreetmap.josm.gui.layer.GpxLayer;
019import org.openstreetmap.josm.gui.layer.Layer;
020import org.openstreetmap.josm.gui.layer.OsmDataLayer;
021import org.openstreetmap.josm.gui.util.GuiHelper;
022import org.openstreetmap.josm.spi.preferences.Config;
023import org.openstreetmap.josm.tools.ImageProvider;
024import org.openstreetmap.josm.tools.Logging;
025import org.openstreetmap.josm.tools.Shortcut;
026import org.openstreetmap.josm.tools.Utils;
027
028/**
029 * Action that merges two or more OSM data layers.
030 * @since 1890
031 */
032public class MergeLayerAction extends AbstractMergeAction {
033
034    /**
035     * Constructs a new {@code MergeLayerAction}.
036     */
037    public MergeLayerAction() {
038        super(tr("Merge layer"), "dialogs/mergedown",
039            tr("Merge the current layer into another layer"),
040            Shortcut.registerShortcut("system:merge", tr("Edit: {0}",
041            tr("Merge")), KeyEvent.VK_M, Shortcut.CTRL),
042            true, "action/mergelayer", true);
043        setHelpId(ht("/Action/MergeLayer"));
044    }
045
046    /**
047     * Submits merge of layers.
048     * @param targetLayers possible target layers
049     * @param sourceLayers source layers
050     * @return a Future representing pending completion of the merge task, or {@code null}
051     * @since 11885 (return type)
052     */
053    protected Future<?> doMerge(List<Layer> targetLayers, final Collection<Layer> sourceLayers) {
054        final boolean onlygpx = targetLayers.stream().noneMatch(l -> !(l instanceof GpxLayer));
055        final TargetLayerDialogResult<Layer> res = askTargetLayer(targetLayers, onlygpx,
056                tr("Cut timewise overlapping parts of tracks"),
057                onlygpx && Config.getPref().getBoolean("mergelayer.gpx.cut", false), tr("Merge layer"));
058        final Layer targetLayer = res.selectedTargetLayer;
059        if (targetLayer == null)
060            return null;
061
062        if (onlygpx) {
063            Config.getPref().putBoolean("mergelayer.gpx.cut", res.checkboxTicked);
064        }
065
066        final Object actionName = getValue(NAME);
067        if (onlygpx && res.checkboxTicked) {
068            List<GpxLayer> layers = new ArrayList<>();
069            layers.add((GpxLayer) targetLayer);
070            for (Layer sl : sourceLayers) {
071                if (sl != null && !sl.equals(targetLayer)) {
072                    layers.add((GpxLayer) sl);
073                }
074            }
075            final MergeGpxLayerDialog d = new MergeGpxLayerDialog(MainApplication.getMainFrame(), layers);
076
077            if (d.showDialog().getValue() == 1) {
078
079                final boolean connect = d.connectCuts();
080                final List<GpxLayer> sortedLayers = d.getSortedLayers();
081
082                return MainApplication.worker.submit(() -> {
083                    final long start = System.currentTimeMillis();
084
085                    for (int i = sortedLayers.size() - 2; i >= 0; i--) {
086                        final GpxLayer lower = sortedLayers.get(i + 1);
087                        sortedLayers.get(i).mergeFrom(lower, true, connect);
088                        GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(lower));
089                    }
090
091                    Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start)));
092                });
093            }
094        }
095
096        return MainApplication.worker.submit(() -> {
097            final long start = System.currentTimeMillis();
098            boolean layerMerged = false;
099            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;
106                    }
107                    targetLayer.mergeFrom(sourceLayer);
108                    GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(sourceLayer));
109                    layerMerged = true;
110                }
111            }
112
113            if (layerMerged) {
114                getLayerManager().setActiveLayer(targetLayer);
115                Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start)));
116            }
117        });
118    }
119
120    /**
121     * Merges a list of layers together.
122     * @param sourceLayers The layers to merge
123     * @return a Future representing pending completion of the merge task, or {@code null}
124     * @since 11885 (return type)
125     */
126    public Future<?> merge(List<Layer> sourceLayers) {
127        return doMerge(sourceLayers, sourceLayers);
128    }
129
130    /**
131     * Merges the given source layer with another one, determined at runtime.
132     * @param sourceLayer The source layer to merge
133     * @return a Future representing pending completion of the merge task, or {@code null}
134     * @since 11885 (return type)
135     */
136    public Future<?> merge(Layer sourceLayer) {
137        if (sourceLayer == null)
138            return null;
139        List<Layer> targetLayers = LayerListDialog.getInstance().getModel().getPossibleMergeTargets(sourceLayer);
140        if (targetLayers.isEmpty()) {
141            warnNoTargetLayersForSourceLayer(sourceLayer);
142            return null;
143        }
144        return doMerge(targetLayers, Collections.singleton(sourceLayer));
145    }
146
147    @Override
148    public void actionPerformed(ActionEvent e) {
149        merge(getSourceLayer());
150    }
151
152    @Override
153    protected void updateEnabledState() {
154        GuiHelper.runInEDT(() -> {
155                final Layer sourceLayer = getSourceLayer();
156                if (sourceLayer == null) {
157                    setEnabled(false);
158                } else {
159                    try {
160                        setEnabled(!LayerListDialog.getInstance().getModel().getPossibleMergeTargets(sourceLayer).isEmpty());
161                    } catch (IllegalStateException e) {
162                        // May occur when destroying last layer / exiting JOSM, see #14476
163                        setEnabled(false);
164                        Logging.error(e);
165                    }
166                }
167        });
168    }
169
170    /**
171     * Returns the source layer.
172     * @return the source layer
173     */
174    protected Layer getSourceLayer() {
175        return getLayerManager().getActiveLayer();
176    }
177
178    /**
179     * Warns about a discouraged merge operation, ask for confirmation.
180     * @param sourceLayer The source layer
181     * @param targetLayer The target layer
182     * @return {@code true} if the user wants to cancel, {@code false} if they want to continue
183     */
184    public static final boolean warnMergingUploadDiscouragedLayers(Layer sourceLayer, Layer targetLayer) {
185        return GuiHelper.warnUser(tr("Merging layers with different upload policies"),
186                "<html>" +
187                tr("You are about to merge data between layers ''{0}'' and ''{1}''.<br /><br />"+
188                        "These layers have different upload policies and should not been merged as it.<br />"+
189                        "Merging them will result to enforce the stricter policy (upload discouraged) to ''{1}''.<br /><br />"+
190                        "<b>This is not the recommended way of merging such data</b>.<br />"+
191                        "You should instead check and merge each object, one by one, by using ''<i>Merge selection</i>''.<br /><br />"+
192                        "Are you sure you want to continue?",
193                        Utils.escapeReservedCharactersHTML(sourceLayer.getName()),
194                        Utils.escapeReservedCharactersHTML(targetLayer.getName()),
195                        Utils.escapeReservedCharactersHTML(targetLayer.getName()))+
196                "</html>",
197                ImageProvider.get("dialogs", "mergedown"), tr("Ignore this hint and merge anyway"));
198    }
199}