| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.actions;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 6 |
|
|---|
| 7 | import java.awt.event.ActionEvent;
|
|---|
| 8 | import java.awt.event.KeyEvent;
|
|---|
| 9 | import java.util.ArrayList;
|
|---|
| 10 | import java.util.Collection;
|
|---|
| 11 | import java.util.Collections;
|
|---|
| 12 | import java.util.List;
|
|---|
| 13 | import java.util.concurrent.Future;
|
|---|
| 14 |
|
|---|
| 15 | import org.openstreetmap.josm.gui.MainApplication;
|
|---|
| 16 | import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
|
|---|
| 17 | import org.openstreetmap.josm.gui.dialogs.layer.MergeGpxLayerDialog;
|
|---|
| 18 | import org.openstreetmap.josm.gui.layer.GpxLayer;
|
|---|
| 19 | import org.openstreetmap.josm.gui.layer.Layer;
|
|---|
| 20 | import org.openstreetmap.josm.gui.layer.OsmDataLayer;
|
|---|
| 21 | import org.openstreetmap.josm.gui.util.GuiHelper;
|
|---|
| 22 | import org.openstreetmap.josm.spi.preferences.Config;
|
|---|
| 23 | import org.openstreetmap.josm.tools.ImageProvider;
|
|---|
| 24 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 25 | import org.openstreetmap.josm.tools.Shortcut;
|
|---|
| 26 | import org.openstreetmap.josm.tools.Stopwatch;
|
|---|
| 27 | import org.openstreetmap.josm.tools.Utils;
|
|---|
| 28 |
|
|---|
| 29 | /**
|
|---|
| 30 | * Action that merges two or more OSM data layers.
|
|---|
| 31 | * @since 1890
|
|---|
| 32 | */
|
|---|
| 33 | public class MergeLayerAction extends AbstractMergeAction {
|
|---|
| 34 |
|
|---|
| 35 | /**
|
|---|
| 36 | * Constructs a new {@code MergeLayerAction}.
|
|---|
| 37 | */
|
|---|
| 38 | public MergeLayerAction() {
|
|---|
| 39 | super(tr("Merge layer"), "dialogs/mergedown",
|
|---|
| 40 | tr("Merge the current layer into another layer"),
|
|---|
| 41 | Shortcut.registerShortcut("system:merge", tr("Edit: {0}",
|
|---|
| 42 | tr("Merge layer")), KeyEvent.VK_M, Shortcut.CTRL),
|
|---|
| 43 | true, "action/mergelayer", true);
|
|---|
| 44 | setHelpId(ht("/Action/MergeLayer"));
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | /**
|
|---|
| 48 | * Submits merge of layers.
|
|---|
| 49 | * @param targetLayers possible target layers
|
|---|
| 50 | * @param sourceLayers source layers
|
|---|
| 51 | * @return a Future representing pending completion of the merge task, or {@code null}
|
|---|
| 52 | * @since 11885 (return type)
|
|---|
| 53 | */
|
|---|
| 54 | protected Future<?> doMerge(List<? extends Layer> targetLayers, final Collection<? extends Layer> sourceLayers) {
|
|---|
| 55 | final boolean onlygpx = targetLayers.stream().allMatch(l -> l instanceof GpxLayer);
|
|---|
| 56 | final TargetLayerDialogResult<Layer> res = askTargetLayer(targetLayers, onlygpx,
|
|---|
| 57 | tr("Cut timewise overlapping parts of tracks"),
|
|---|
| 58 | onlygpx && Config.getPref().getBoolean("mergelayer.gpx.cut", false), tr("Merge layer"));
|
|---|
| 59 | final Layer targetLayer = res.selectedTargetLayer;
|
|---|
| 60 | if (targetLayer == null)
|
|---|
| 61 | return null;
|
|---|
| 62 |
|
|---|
| 63 | if (onlygpx) {
|
|---|
| 64 | Config.getPref().putBoolean("mergelayer.gpx.cut", res.checkboxTicked);
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | final Object actionName = getValue(NAME);
|
|---|
| 68 | if (onlygpx && res.checkboxTicked) {
|
|---|
| 69 | List<GpxLayer> layers = new ArrayList<>();
|
|---|
| 70 | layers.add((GpxLayer) targetLayer);
|
|---|
| 71 | for (Layer sl : sourceLayers) {
|
|---|
| 72 | if (sl != null && !sl.equals(targetLayer)) {
|
|---|
| 73 | layers.add((GpxLayer) sl);
|
|---|
| 74 | }
|
|---|
| 75 | }
|
|---|
| 76 | final MergeGpxLayerDialog d = new MergeGpxLayerDialog(MainApplication.getMainFrame(), layers);
|
|---|
| 77 |
|
|---|
| 78 | if (d.showDialog().getValue() == 1) {
|
|---|
| 79 |
|
|---|
| 80 | final boolean connect = d.connectCuts();
|
|---|
| 81 | final List<GpxLayer> sortedLayers = d.getSortedLayers();
|
|---|
| 82 |
|
|---|
| 83 | return MainApplication.worker.submit(() -> {
|
|---|
| 84 | final Stopwatch stopwatch = Stopwatch.createStarted();
|
|---|
| 85 |
|
|---|
| 86 | for (int i = sortedLayers.size() - 2; i >= 0; i--) {
|
|---|
| 87 | final GpxLayer lower = sortedLayers.get(i + 1);
|
|---|
| 88 | sortedLayers.get(i).mergeFrom(lower, true, connect);
|
|---|
| 89 | GuiHelper.runInEDTAndWait(() -> getLayerManager().removeLayer(lower));
|
|---|
| 90 | }
|
|---|
| 91 |
|
|---|
| 92 | Logging.info(stopwatch.toString(String.valueOf(actionName)));
|
|---|
| 93 | });
|
|---|
| 94 | }
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | return MainApplication.worker.submit(() -> {
|
|---|
| 98 | final Stopwatch stopwatch = Stopwatch.createStarted();
|
|---|
| 99 | 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(stopwatch.toString(String.valueOf(actionName)));
|
|---|
| 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 boolean listenToSelectionChange() {
|
|---|
| 155 | return false;
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | @Override
|
|---|
| 159 | protected void updateEnabledState() {
|
|---|
| 160 | GuiHelper.runInEDT(() -> {
|
|---|
| 161 | final Layer sourceLayer = getSourceLayer();
|
|---|
| 162 | if (sourceLayer == null) {
|
|---|
| 163 | setEnabled(false);
|
|---|
| 164 | } else {
|
|---|
| 165 | try {
|
|---|
| 166 | setEnabled(!LayerListDialog.getInstance().getModel().getPossibleMergeTargets(sourceLayer).isEmpty());
|
|---|
| 167 | } catch (IllegalStateException e) {
|
|---|
| 168 | // May occur when destroying last layer / exiting JOSM, see #14476
|
|---|
| 169 | setEnabled(false);
|
|---|
| 170 | Logging.error(e);
|
|---|
| 171 | }
|
|---|
| 172 | }
|
|---|
| 173 | });
|
|---|
| 174 | }
|
|---|
| 175 |
|
|---|
| 176 | /**
|
|---|
| 177 | * Returns the source layer.
|
|---|
| 178 | * @return the source layer
|
|---|
| 179 | */
|
|---|
| 180 | protected Layer getSourceLayer() {
|
|---|
| 181 | return getLayerManager().getActiveLayer();
|
|---|
| 182 | }
|
|---|
| 183 |
|
|---|
| 184 | /**
|
|---|
| 185 | * Warns about a discouraged merge operation, ask for confirmation.
|
|---|
| 186 | * @param sourceLayer The source layer
|
|---|
| 187 | * @param targetLayer The target layer
|
|---|
| 188 | * @return {@code true} if the user wants to cancel, {@code false} if they want to continue
|
|---|
| 189 | */
|
|---|
| 190 | public static final boolean warnMergingUploadDiscouragedLayers(Layer sourceLayer, Layer targetLayer) {
|
|---|
| 191 | return GuiHelper.warnUser(tr("Merging layers with different upload policies"),
|
|---|
| 192 | "<html>" +
|
|---|
| 193 | tr("You are about to merge data between layers ''{0}'' and ''{1}''.<br /><br />"+
|
|---|
| 194 | "These layers have different upload policies and should not been merged as it.<br />"+
|
|---|
| 195 | "Merging them will result to enforce the stricter policy (upload discouraged) to ''{1}''.<br /><br />"+
|
|---|
| 196 | "<b>This is not the recommended way of merging such data</b>.<br />"+
|
|---|
| 197 | "You should instead check and merge each object, one by one, by using ''<i>Merge selection</i>''.<br /><br />"+
|
|---|
| 198 | "Are you sure you want to continue?",
|
|---|
| 199 | Utils.escapeReservedCharactersHTML(sourceLayer.getName()),
|
|---|
| 200 | Utils.escapeReservedCharactersHTML(targetLayer.getName()),
|
|---|
| 201 | Utils.escapeReservedCharactersHTML(targetLayer.getName()))+
|
|---|
| 202 | "</html>",
|
|---|
| 203 | ImageProvider.get("dialogs", "mergedown"), tr("Ignore this hint and merge anyway"));
|
|---|
| 204 | }
|
|---|
| 205 | }
|
|---|