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