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}