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.Collection;
010import java.util.Collections;
011import java.util.List;
012import java.util.concurrent.Future;
013
014import org.openstreetmap.josm.gui.MainApplication;
015import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
016import org.openstreetmap.josm.gui.layer.Layer;
017import org.openstreetmap.josm.gui.layer.OsmDataLayer;
018import org.openstreetmap.josm.gui.util.GuiHelper;
019import org.openstreetmap.josm.tools.ImageProvider;
020import org.openstreetmap.josm.tools.Logging;
021import org.openstreetmap.josm.tools.Shortcut;
022import org.openstreetmap.josm.tools.Utils;
023
024/**
025 * Action that merges two or more OSM data layers.
026 * @since 1890
027 */
028public class MergeLayerAction extends AbstractMergeAction {
029
030    /**
031     * Constructs a new {@code MergeLayerAction}.
032     */
033    public MergeLayerAction() {
034        super(tr("Merge layer"), "dialogs/mergedown",
035            tr("Merge the current layer into another layer"),
036            Shortcut.registerShortcut("system:merge", tr("Edit: {0}",
037            tr("Merge")), KeyEvent.VK_M, Shortcut.CTRL),
038            true, "action/mergelayer", true);
039        putValue("help", ht("/Action/MergeLayer"));
040    }
041
042    /**
043     * Submits merge of layers.
044     * @param targetLayers possible target layers
045     * @param sourceLayers source layers
046     * @return a Future representing pending completion of the merge task, or {@code null}
047     * @since 11885 (return type)
048     */
049    protected Future<?> doMerge(List<Layer> targetLayers, final Collection<Layer> sourceLayers) {
050        final Layer targetLayer = askTargetLayer(targetLayers);
051        if (targetLayer == null)
052            return null;
053        final Object actionName = getValue(NAME);
054        return MainApplication.worker.submit(() -> {
055                final long start = System.currentTimeMillis();
056                boolean layerMerged = false;
057                for (final Layer sourceLayer: sourceLayers) {
058                    if (sourceLayer != null && !sourceLayer.equals(targetLayer)) {
059                        if (sourceLayer instanceof OsmDataLayer && targetLayer instanceof OsmDataLayer
060                                && ((OsmDataLayer) sourceLayer).isUploadDiscouraged() != ((OsmDataLayer) targetLayer).isUploadDiscouraged()
061                                && Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() ->
062                                    warnMergingUploadDiscouragedLayers(sourceLayer, targetLayer)))) {
063                            break;
064                        }
065                        targetLayer.mergeFrom(sourceLayer);
066                        GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(sourceLayer));
067                        layerMerged = true;
068                    }
069                }
070                if (layerMerged) {
071                    getLayerManager().setActiveLayer(targetLayer);
072                    Logging.info(tr("{0} completed in {1}", actionName, Utils.getDurationString(System.currentTimeMillis() - start)));
073                }
074        });
075    }
076
077    /**
078     * Merges a list of layers together.
079     * @param sourceLayers The layers to merge
080     * @return a Future representing pending completion of the merge task, or {@code null}
081     * @since 11885 (return type)
082     */
083    public Future<?> merge(List<Layer> sourceLayers) {
084        return doMerge(sourceLayers, sourceLayers);
085    }
086
087    /**
088     * Merges the given source layer with another one, determined at runtime.
089     * @param sourceLayer The source layer to merge
090     * @return a Future representing pending completion of the merge task, or {@code null}
091     * @since 11885 (return type)
092     */
093    public Future<?> merge(Layer sourceLayer) {
094        if (sourceLayer == null)
095            return null;
096        List<Layer> targetLayers = LayerListDialog.getInstance().getModel().getPossibleMergeTargets(sourceLayer);
097        if (targetLayers.isEmpty()) {
098            warnNoTargetLayersForSourceLayer(sourceLayer);
099            return null;
100        }
101        return doMerge(targetLayers, Collections.singleton(sourceLayer));
102    }
103
104    @Override
105    public void actionPerformed(ActionEvent e) {
106        merge(getSourceLayer());
107    }
108
109    @Override
110    protected void updateEnabledState() {
111        GuiHelper.runInEDT(() -> {
112                final Layer sourceLayer = getSourceLayer();
113                if (sourceLayer == null) {
114                    setEnabled(false);
115                } else {
116                    try {
117                        setEnabled(!LayerListDialog.getInstance().getModel().getPossibleMergeTargets(sourceLayer).isEmpty());
118                    } catch (IllegalStateException e) {
119                        // May occur when destroying last layer / exiting JOSM, see #14476
120                        setEnabled(false);
121                        Logging.error(e);
122                    }
123                }
124        });
125    }
126
127    /**
128     * Returns the source layer.
129     * @return the source layer
130     */
131    protected Layer getSourceLayer() {
132        return getLayerManager().getActiveLayer();
133    }
134
135    /**
136     * Warns about a discouraged merge operation, ask for confirmation.
137     * @param sourceLayer The source layer
138     * @param targetLayer The target layer
139     * @return {@code true} if the user wants to cancel, {@code false} if they want to continue
140     */
141    public static final boolean warnMergingUploadDiscouragedLayers(Layer sourceLayer, Layer targetLayer) {
142        return GuiHelper.warnUser(tr("Merging layers with different upload policies"),
143                "<html>" +
144                tr("You are about to merge data between layers ''{0}'' and ''{1}''.<br /><br />"+
145                        "These layers have different upload policies and should not been merged as it.<br />"+
146                        "Merging them will result to enforce the stricter policy (upload discouraged) to ''{1}''.<br /><br />"+
147                        "<b>This is not the recommended way of merging such data</b>.<br />"+
148                        "You should instead check and merge each object, one by one, by using ''<i>Merge selection</i>''.<br /><br />"+
149                        "Are you sure you want to continue?",
150                        Utils.escapeReservedCharactersHTML(sourceLayer.getName()),
151                        Utils.escapeReservedCharactersHTML(targetLayer.getName()),
152                        Utils.escapeReservedCharactersHTML(targetLayer.getName()))+
153                "</html>",
154                ImageProvider.get("dialogs", "mergedown"), tr("Ignore this hint and merge anyway"));
155    }
156}