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)); 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}