001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.AlphaComposite;
010import java.awt.Color;
011import java.awt.Composite;
012import java.awt.Graphics2D;
013import java.awt.GridBagLayout;
014import java.awt.Rectangle;
015import java.awt.TexturePaint;
016import java.awt.datatransfer.Transferable;
017import java.awt.datatransfer.UnsupportedFlavorException;
018import java.awt.event.ActionEvent;
019import java.awt.geom.Area;
020import java.awt.geom.Path2D;
021import java.awt.geom.Rectangle2D;
022import java.awt.image.BufferedImage;
023import java.io.File;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.Optional;
036import java.util.Set;
037import java.util.concurrent.CopyOnWriteArrayList;
038import java.util.concurrent.atomic.AtomicBoolean;
039import java.util.concurrent.atomic.AtomicInteger;
040import java.util.regex.Pattern;
041
042import javax.swing.AbstractAction;
043import javax.swing.Action;
044import javax.swing.Icon;
045import javax.swing.JLabel;
046import javax.swing.JOptionPane;
047import javax.swing.JPanel;
048import javax.swing.JScrollPane;
049
050import org.openstreetmap.josm.actions.ExpertToggleAction;
051import org.openstreetmap.josm.actions.RenameLayerAction;
052import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
053import org.openstreetmap.josm.data.APIDataSet;
054import org.openstreetmap.josm.data.Bounds;
055import org.openstreetmap.josm.data.DataSource;
056import org.openstreetmap.josm.data.ProjectionBounds;
057import org.openstreetmap.josm.data.UndoRedoHandler;
058import org.openstreetmap.josm.data.conflict.Conflict;
059import org.openstreetmap.josm.data.conflict.ConflictCollection;
060import org.openstreetmap.josm.data.coor.EastNorth;
061import org.openstreetmap.josm.data.coor.LatLon;
062import org.openstreetmap.josm.data.gpx.GpxConstants;
063import org.openstreetmap.josm.data.gpx.GpxData;
064import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
065import org.openstreetmap.josm.data.gpx.GpxLink;
066import org.openstreetmap.josm.data.gpx.GpxTrack;
067import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
068import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
069import org.openstreetmap.josm.data.gpx.WayPoint;
070import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
071import org.openstreetmap.josm.data.osm.DataSelectionListener;
072import org.openstreetmap.josm.data.osm.DataSet;
073import org.openstreetmap.josm.data.osm.DataSetMerger;
074import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
075import org.openstreetmap.josm.data.osm.DownloadPolicy;
076import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
077import org.openstreetmap.josm.data.osm.IPrimitive;
078import org.openstreetmap.josm.data.osm.Node;
079import org.openstreetmap.josm.data.osm.OsmPrimitive;
080import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
081import org.openstreetmap.josm.data.osm.Relation;
082import org.openstreetmap.josm.data.osm.Tagged;
083import org.openstreetmap.josm.data.osm.UploadPolicy;
084import org.openstreetmap.josm.data.osm.Way;
085import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
086import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
087import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
088import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
089import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
090import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
091import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
092import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
093import org.openstreetmap.josm.data.preferences.BooleanProperty;
094import org.openstreetmap.josm.data.preferences.IntegerProperty;
095import org.openstreetmap.josm.data.preferences.NamedColorProperty;
096import org.openstreetmap.josm.data.preferences.StringProperty;
097import org.openstreetmap.josm.data.projection.Projection;
098import org.openstreetmap.josm.data.validation.TestError;
099import org.openstreetmap.josm.gui.ExtendedDialog;
100import org.openstreetmap.josm.gui.MainApplication;
101import org.openstreetmap.josm.gui.MapFrame;
102import org.openstreetmap.josm.gui.MapView;
103import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
104import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
105import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
106import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
107import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
108import org.openstreetmap.josm.gui.io.AbstractIOTask;
109import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
110import org.openstreetmap.josm.gui.io.UploadDialog;
111import org.openstreetmap.josm.gui.io.UploadLayerTask;
112import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
113import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
114import org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter;
115import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
116import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
117import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
118import org.openstreetmap.josm.gui.progress.ProgressMonitor;
119import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
120import org.openstreetmap.josm.gui.util.GuiHelper;
121import org.openstreetmap.josm.gui.widgets.FileChooserManager;
122import org.openstreetmap.josm.gui.widgets.JosmTextArea;
123import org.openstreetmap.josm.spi.preferences.Config;
124import org.openstreetmap.josm.tools.AlphanumComparator;
125import org.openstreetmap.josm.tools.CheckParameterUtil;
126import org.openstreetmap.josm.tools.GBC;
127import org.openstreetmap.josm.tools.ImageOverlay;
128import org.openstreetmap.josm.tools.ImageProvider;
129import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
130import org.openstreetmap.josm.tools.Logging;
131import org.openstreetmap.josm.tools.UncheckedParseException;
132import org.openstreetmap.josm.tools.date.DateUtils;
133
134/**
135 * A layer that holds OSM data from a specific dataset.
136 * The data can be fully edited.
137 *
138 * @author imi
139 * @since 17
140 */
141public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
142    private static final int HATCHED_SIZE = 15;
143    /** Property used to know if this layer has to be saved on disk */
144    public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
145    /** Property used to know if this layer has to be uploaded */
146    public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
147
148    private boolean requiresSaveToFile;
149    private boolean requiresUploadToServer;
150    /** Flag used to know if the layer is being uploaded */
151    private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
152
153    /**
154     * List of validation errors in this layer.
155     * @since 3669
156     */
157    public final List<TestError> validationErrors = new ArrayList<>();
158
159    /**
160     * The default number of relations in the recent relations cache.
161     * @see #getRecentRelations()
162     */
163    public static final int DEFAULT_RECENT_RELATIONS_NUMBER = 20;
164    /**
165     * The number of relations to use in the recent relations cache.
166     * @see #getRecentRelations()
167     */
168    public static final IntegerProperty PROPERTY_RECENT_RELATIONS_NUMBER = new IntegerProperty("properties.last-closed-relations-size",
169            DEFAULT_RECENT_RELATIONS_NUMBER);
170    /**
171     * The extension that should be used when saving the OSM file.
172     */
173    public static final StringProperty PROPERTY_SAVE_EXTENSION = new StringProperty("save.extension.osm", "osm");
174
175    /**
176     * Property to determine if labels must be hidden while dragging the map.
177     */
178    public static final BooleanProperty PROPERTY_HIDE_LABELS_WHILE_DRAGGING = new BooleanProperty("mappaint.hide.labels.while.dragging", true);
179
180    private static final NamedColorProperty PROPERTY_BACKGROUND_COLOR = new NamedColorProperty(marktr("background"), Color.BLACK);
181    private static final NamedColorProperty PROPERTY_OUTSIDE_COLOR = new NamedColorProperty(marktr("outside downloaded area"), Color.YELLOW);
182
183    /** List of recent relations */
184    private final Map<Relation, Void> recentRelations = new LruCache(PROPERTY_RECENT_RELATIONS_NUMBER.get()+1);
185
186    /**
187     * Returns list of recently closed relations or null if none.
188     * @return list of recently closed relations or <code>null</code> if none
189     * @since 12291 (signature)
190     * @since 9668
191     */
192    public List<Relation> getRecentRelations() {
193        ArrayList<Relation> list = new ArrayList<>(recentRelations.keySet());
194        Collections.reverse(list);
195        return list;
196    }
197
198    /**
199     * Adds recently closed relation.
200     * @param relation new entry for the list of recently closed relations
201     * @see #PROPERTY_RECENT_RELATIONS_NUMBER
202     * @since 9668
203     */
204    public void setRecentRelation(Relation relation) {
205        recentRelations.put(relation, null);
206        MapFrame map = MainApplication.getMap();
207        if (map != null && map.relationListDialog != null) {
208            map.relationListDialog.enableRecentRelations();
209        }
210    }
211
212    /**
213     * Remove relation from list of recent relations.
214     * @param relation relation to remove
215     * @since 9668
216     */
217    public void removeRecentRelation(Relation relation) {
218        recentRelations.remove(relation);
219        MapFrame map = MainApplication.getMap();
220        if (map != null && map.relationListDialog != null) {
221            map.relationListDialog.enableRecentRelations();
222        }
223    }
224
225    protected void setRequiresSaveToFile(boolean newValue) {
226        boolean oldValue = requiresSaveToFile;
227        requiresSaveToFile = newValue;
228        if (oldValue != newValue) {
229            GuiHelper.runInEDT(() ->
230                propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue)
231            );
232        }
233    }
234
235    protected void setRequiresUploadToServer(boolean newValue) {
236        boolean oldValue = requiresUploadToServer;
237        requiresUploadToServer = newValue;
238        if (oldValue != newValue) {
239            GuiHelper.runInEDT(() ->
240                propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue)
241            );
242        }
243    }
244
245    /** the global counter for created data layers */
246    private static final AtomicInteger dataLayerCounter = new AtomicInteger();
247
248    /**
249     * Replies a new unique name for a data layer
250     *
251     * @return a new unique name for a data layer
252     */
253    public static String createNewName() {
254        return createLayerName(dataLayerCounter.incrementAndGet());
255    }
256
257    static String createLayerName(Object arg) {
258        return tr("Data Layer {0}", arg);
259    }
260
261    static final class LruCache extends LinkedHashMap<Relation, Void> {
262        private static final long serialVersionUID = 1L;
263        LruCache(int initialCapacity) {
264            super(initialCapacity, 1.1f, true);
265        }
266
267        @Override
268        protected boolean removeEldestEntry(Map.Entry<Relation, Void> eldest) {
269            return size() > PROPERTY_RECENT_RELATIONS_NUMBER.get();
270        }
271    }
272
273    /**
274     * A listener that counts the number of primitives it encounters
275     */
276    public static final class DataCountVisitor implements OsmPrimitiveVisitor {
277        /**
278         * Nodes that have been visited
279         */
280        public int nodes;
281        /**
282         * Ways that have been visited
283         */
284        public int ways;
285        /**
286         * Relations that have been visited
287         */
288        public int relations;
289        /**
290         * Deleted nodes that have been visited
291         */
292        public int deletedNodes;
293        /**
294         * Deleted ways that have been visited
295         */
296        public int deletedWays;
297        /**
298         * Deleted relations that have been visited
299         */
300        public int deletedRelations;
301        /**
302         * Incomplete nodes that have been visited
303         */
304        public int incompleteNodes;
305        /**
306         * Incomplete ways that have been visited
307         */
308        public int incompleteWays;
309        /**
310         * Incomplete relations that have been visited
311         */
312        public int incompleteRelations;
313
314        @Override
315        public void visit(final Node n) {
316            nodes++;
317            if (n.isDeleted()) {
318                deletedNodes++;
319            }
320            if (n.isIncomplete()) {
321                incompleteNodes++;
322            }
323        }
324
325        @Override
326        public void visit(final Way w) {
327            ways++;
328            if (w.isDeleted()) {
329                deletedWays++;
330            }
331            if (w.isIncomplete()) {
332                incompleteWays++;
333            }
334        }
335
336        @Override
337        public void visit(final Relation r) {
338            relations++;
339            if (r.isDeleted()) {
340                deletedRelations++;
341            }
342            if (r.isIncomplete()) {
343                incompleteRelations++;
344            }
345        }
346    }
347
348    /**
349     * Listener called when a state of this layer has changed.
350     * @since 10600 (functional interface)
351     */
352    @FunctionalInterface
353    public interface LayerStateChangeListener {
354        /**
355         * Notifies that the "upload discouraged" (upload=no) state has changed.
356         * @param layer The layer that has been modified
357         * @param newValue The new value of the state
358         */
359        void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue);
360    }
361
362    private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
363
364    /**
365     * Adds a layer state change listener
366     *
367     * @param listener the listener. Ignored if null or already registered.
368     * @since 5519
369     */
370    public void addLayerStateChangeListener(LayerStateChangeListener listener) {
371        if (listener != null) {
372            layerStateChangeListeners.addIfAbsent(listener);
373        }
374    }
375
376    /**
377     * Removes a layer state change listener
378     *
379     * @param listener the listener. Ignored if null or already registered.
380     * @since 10340
381     */
382    public void removeLayerStateChangeListener(LayerStateChangeListener listener) {
383        layerStateChangeListeners.remove(listener);
384    }
385
386    /**
387     * The data behind this layer.
388     */
389    public final DataSet data;
390    private DataSetListenerAdapter dataSetListenerAdapter;
391
392    /**
393     * a texture for non-downloaded area
394     */
395    private static volatile BufferedImage hatched;
396
397    static {
398        createHatchTexture();
399    }
400
401    /**
402     * Replies background color for downloaded areas.
403     * @return background color for downloaded areas. Black by default
404     */
405    public static Color getBackgroundColor() {
406        return PROPERTY_BACKGROUND_COLOR.get();
407    }
408
409    /**
410     * Replies background color for non-downloaded areas.
411     * @return background color for non-downloaded areas. Yellow by default
412     */
413    public static Color getOutsideColor() {
414        return PROPERTY_OUTSIDE_COLOR.get();
415    }
416
417    /**
418     * Initialize the hatch pattern used to paint the non-downloaded area
419     */
420    public static void createHatchTexture() {
421        BufferedImage bi = new BufferedImage(HATCHED_SIZE, HATCHED_SIZE, BufferedImage.TYPE_INT_ARGB);
422        Graphics2D big = bi.createGraphics();
423        big.setColor(getBackgroundColor());
424        Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
425        big.setComposite(comp);
426        big.fillRect(0, 0, HATCHED_SIZE, HATCHED_SIZE);
427        big.setColor(getOutsideColor());
428        big.drawLine(-1, 6, 6, -1);
429        big.drawLine(4, 16, 16, 4);
430        hatched = bi;
431    }
432
433    /**
434     * Construct a new {@code OsmDataLayer}.
435     * @param data OSM data
436     * @param name Layer name
437     * @param associatedFile Associated .osm file (can be null)
438     */
439    public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
440        super(name);
441        CheckParameterUtil.ensureParameterNotNull(data, "data");
442        this.data = data;
443        this.data.setName(name);
444        this.dataSetListenerAdapter = new DataSetListenerAdapter(this);
445        this.setAssociatedFile(associatedFile);
446        data.addDataSetListener(dataSetListenerAdapter);
447        data.addDataSetListener(MultipolygonCache.getInstance());
448        data.addHighlightUpdateListener(this);
449        data.addSelectionListener(this);
450        if (name != null && name.startsWith(createLayerName("")) && Character.isDigit(
451                (name.substring(createLayerName("").length()) + "XX" /*avoid StringIndexOutOfBoundsException*/).charAt(1))) {
452            while (AlphanumComparator.getInstance().compare(createLayerName(dataLayerCounter), name) < 0) {
453                final int i = dataLayerCounter.incrementAndGet();
454                if (i > 1_000_000) {
455                    break; // to avoid looping in unforeseen case
456                }
457            }
458        }
459    }
460
461    /**
462     * Returns the {@link DataSet} behind this layer.
463     * @return the {@link DataSet} behind this layer.
464     * @since 13558
465     */
466    @Override
467    public DataSet getDataSet() {
468        return data;
469    }
470
471    /**
472     * Return the image provider to get the base icon
473     * @return image provider class which can be modified
474     * @since 8323
475     */
476    protected ImageProvider getBaseIconProvider() {
477        return new ImageProvider("layer", "osmdata_small");
478    }
479
480    @Override
481    public Icon getIcon() {
482        ImageProvider base = getBaseIconProvider().setMaxSize(ImageSizes.LAYER);
483        if (data.getDownloadPolicy() != null && data.getDownloadPolicy() != DownloadPolicy.NORMAL) {
484            base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.0, 1.0, 0.5));
485        }
486        if (data.getUploadPolicy() != null && data.getUploadPolicy() != UploadPolicy.NORMAL) {
487            base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0));
488        }
489
490        if (isUploadInProgress()) {
491            // If the layer is being uploaded then change the default icon to a clock
492            base = new ImageProvider("clock").setMaxSize(ImageSizes.LAYER);
493        } else if (isLocked()) {
494            // If the layer is read only then change the default icon to a lock
495            base = new ImageProvider("lock").setMaxSize(ImageSizes.LAYER);
496        }
497        return base.get();
498    }
499
500    /**
501     * Draw all primitives in this layer but do not draw modified ones (they
502     * are drawn by the edit layer).
503     * Draw nodes last to overlap the ways they belong to.
504     */
505    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
506        boolean active = mv.getLayerManager().getActiveLayer() == this;
507        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
508        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
509
510        // draw the hatched area for non-downloaded region. only draw if we're the active
511        // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
512        if (active && DrawingPreference.SOURCE_BOUNDS_PROP.get() && !data.getDataSources().isEmpty()) {
513            // initialize area with current viewport
514            Rectangle b = mv.getBounds();
515            // on some platforms viewport bounds seem to be offset from the left,
516            // over-grow it just to be sure
517            b.grow(100, 100);
518            Path2D p = new Path2D.Double();
519
520            // combine successively downloaded areas
521            for (Bounds bounds : data.getDataSourceBounds()) {
522                if (bounds.isCollapsed()) {
523                    continue;
524                }
525                p.append(mv.getState().getArea(bounds), false);
526            }
527            // subtract combined areas
528            Area a = new Area(b);
529            a.subtract(new Area(p));
530
531            // paint remainder
532            MapViewPoint anchor = mv.getState().getPointFor(new EastNorth(0, 0));
533            Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getInView().getX() % HATCHED_SIZE,
534                    anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
535            if (hatched != null) {
536                g.setPaint(new TexturePaint(hatched, anchorRect));
537            }
538            try {
539                g.fill(a);
540            } catch (ArrayIndexOutOfBoundsException e) {
541                // #16686 - AIOOBE in java.awt.TexturePaintContext$Int.setRaster
542                Logging.error(e);
543            }
544        }
545
546        AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
547        painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
548                || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
549        painter.render(data, virtual, box);
550        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
551    }
552
553    @Override public String getToolTipText() {
554        DataCountVisitor counter = new DataCountVisitor();
555        for (final OsmPrimitive osm : data.allPrimitives()) {
556            osm.accept(counter);
557        }
558        int nodes = counter.nodes - counter.deletedNodes;
559        int ways = counter.ways - counter.deletedWays;
560        int rels = counter.relations - counter.deletedRelations;
561
562        StringBuilder tooltip = new StringBuilder("<html>")
563                .append(trn("{0} node", "{0} nodes", nodes, nodes))
564                .append("<br>")
565                .append(trn("{0} way", "{0} ways", ways, ways))
566                .append("<br>")
567                .append(trn("{0} relation", "{0} relations", rels, rels));
568
569        File f = getAssociatedFile();
570        if (f != null) {
571            tooltip.append("<br>").append(f.getPath());
572        }
573        tooltip.append("</html>");
574        return tooltip.toString();
575    }
576
577    @Override public void mergeFrom(final Layer from) {
578        final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
579        monitor.setCancelable(false);
580        if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
581            setUploadDiscouraged(true);
582        }
583        mergeFrom(((OsmDataLayer) from).data, monitor);
584        monitor.close();
585    }
586
587    /**
588     * merges the primitives in dataset <code>from</code> into the dataset of
589     * this layer
590     *
591     * @param from  the source data set
592     */
593    public void mergeFrom(final DataSet from) {
594        mergeFrom(from, null);
595    }
596
597    /**
598     * merges the primitives in dataset <code>from</code> into the dataset of this layer
599     *
600     * @param from  the source data set
601     * @param progressMonitor the progress monitor, can be {@code null}
602     */
603    public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
604        final DataSetMerger visitor = new DataSetMerger(data, from);
605        try {
606            visitor.merge(progressMonitor);
607        } catch (DataIntegrityProblemException e) {
608            Logging.error(e);
609            JOptionPane.showMessageDialog(
610                    MainApplication.getMainFrame(),
611                    e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
612                    tr("Error"),
613                    JOptionPane.ERROR_MESSAGE
614            );
615            return;
616        }
617
618        int numNewConflicts = 0;
619        for (Conflict<?> c : visitor.getConflicts()) {
620            if (!data.getConflicts().hasConflict(c)) {
621                numNewConflicts++;
622                data.getConflicts().add(c);
623            }
624        }
625        // repaint to make sure new data is displayed properly.
626        invalidate();
627        // warn about new conflicts
628        MapFrame map = MainApplication.getMap();
629        if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
630            map.conflictDialog.warnNumNewConflicts(numNewConflicts);
631        }
632    }
633
634    @Override
635    public boolean isMergable(final Layer other) {
636        // allow merging between normal layers and discouraged layers with a warning (see #7684)
637        return other instanceof OsmDataLayer;
638    }
639
640    @Override
641    public void visitBoundingBox(final BoundingXYVisitor v) {
642        for (final Node n: data.getNodes()) {
643            if (n.isUsable()) {
644                v.visit(n);
645            }
646        }
647    }
648
649    /**
650     * Clean out the data behind the layer. This means clearing the redo/undo lists,
651     * really deleting all deleted objects and reset the modified flags. This should
652     * be done after an upload, even after a partial upload.
653     *
654     * @param processed A list of all objects that were actually uploaded.
655     *         May be <code>null</code>, which means nothing has been uploaded
656     */
657    public void cleanupAfterUpload(final Collection<? extends IPrimitive> processed) {
658        // return immediately if an upload attempt failed
659        if (processed == null || processed.isEmpty())
660            return;
661
662        UndoRedoHandler.getInstance().clean(data);
663
664        // if uploaded, clean the modified flags as well
665        data.cleanupDeletedPrimitives();
666        data.beginUpdate();
667        try {
668            for (OsmPrimitive p: data.allPrimitives()) {
669                if (processed.contains(p)) {
670                    p.setModified(false);
671                }
672            }
673        } finally {
674            data.endUpdate();
675        }
676    }
677
678    private static String counterText(String text, int deleted, int incomplete) {
679        StringBuilder sb = new StringBuilder(text);
680        if (deleted > 0 || incomplete > 0) {
681            sb.append(" (");
682            if (deleted > 0) {
683                sb.append(trn("{0} deleted", "{0} deleted", deleted, deleted));
684            }
685            if (deleted > 0 && incomplete > 0) {
686                sb.append(", ");
687            }
688            if (incomplete > 0) {
689                sb.append(trn("{0} incomplete", "{0} incomplete", incomplete, incomplete));
690            }
691            sb.append(')');
692        }
693        return sb.toString();
694    }
695
696    @Override
697    public Object getInfoComponent() {
698        final DataCountVisitor counter = new DataCountVisitor();
699        for (final OsmPrimitive osm : data.allPrimitives()) {
700            osm.accept(counter);
701        }
702        final JPanel p = new JPanel(new GridBagLayout());
703
704        String nodeText = counterText(trn("{0} node", "{0} nodes", counter.nodes, counter.nodes),
705                counter.deletedNodes, counter.incompleteNodes);
706        String wayText = counterText(trn("{0} way", "{0} ways", counter.ways, counter.ways),
707                counter.deletedWays, counter.incompleteWays);
708        String relationText = counterText(trn("{0} relation", "{0} relations", counter.relations, counter.relations),
709                counter.deletedRelations, counter.incompleteRelations);
710
711        p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
712        p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
713        p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
714        p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
715        p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))),
716                GBC.eop().insets(15, 0, 0, 0));
717        if (isUploadDiscouraged()) {
718            p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15, 0, 0, 0));
719        }
720        if (data.getUploadPolicy() == UploadPolicy.BLOCKED) {
721            p.add(new JLabel(tr("Upload is blocked")), GBC.eop().insets(15, 0, 0, 0));
722        }
723
724        return p;
725    }
726
727    @Override public Action[] getMenuEntries() {
728        List<Action> actions = new ArrayList<>();
729        actions.addAll(Arrays.asList(
730                LayerListDialog.getInstance().createActivateLayerAction(this),
731                LayerListDialog.getInstance().createShowHideLayerAction(),
732                LayerListDialog.getInstance().createDeleteLayerAction(),
733                SeparatorLayerAction.INSTANCE,
734                LayerListDialog.getInstance().createMergeLayerAction(this),
735                LayerListDialog.getInstance().createDuplicateLayerAction(this),
736                new LayerSaveAction(this),
737                new LayerSaveAsAction(this)));
738        if (ExpertToggleAction.isExpert()) {
739            actions.addAll(Arrays.asList(
740                    new LayerGpxExportAction(this),
741                    new ConvertToGpxLayerAction()));
742        }
743        actions.addAll(Arrays.asList(
744                SeparatorLayerAction.INSTANCE,
745                new RenameLayerAction(getAssociatedFile(), this)));
746        if (ExpertToggleAction.isExpert()) {
747            actions.add(new ToggleUploadDiscouragedLayerAction(this));
748        }
749        actions.addAll(Arrays.asList(
750                new ConsistencyTestAction(),
751                SeparatorLayerAction.INSTANCE,
752                new LayerListPopup.InfoAction(this)));
753        return actions.toArray(new Action[0]);
754    }
755
756    /**
757     * Converts given OSM dataset to GPX data.
758     * @param data OSM dataset
759     * @param file output .gpx file
760     * @return GPX data
761     */
762    public static GpxData toGpxData(DataSet data, File file) {
763        GpxData gpxData = new GpxData();
764        if (data.getGPXNamespaces() != null) {
765            gpxData.getNamespaces().addAll(data.getGPXNamespaces());
766        }
767        gpxData.storageFile = file;
768        Set<Node> doneNodes = new HashSet<>();
769        waysToGpxData(data.getWays(), gpxData, doneNodes);
770        nodesToGpxData(data.getNodes(), gpxData, doneNodes);
771        return gpxData;
772    }
773
774    private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes) {
775        /* When the dataset has been obtained from a gpx layer and now is being converted back,
776         * the ways have negative ids. The first created way corresponds to the first gpx segment,
777         * and has the highest id (i.e., closest to zero).
778         * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
779         * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
780         */
781        ways.stream()
782                .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
783                .forEachOrdered(w -> {
784            if (!w.isUsable()) {
785                return;
786            }
787            List<IGpxTrackSegment> trk = new ArrayList<>();
788            Map<String, Object> trkAttr = new HashMap<>();
789
790            GpxExtensionCollection trkExts = new GpxExtensionCollection();
791            GpxExtensionCollection segExts = new GpxExtensionCollection();
792            for (Entry<String, String> e : w.getKeys().entrySet()) {
793                String k = e.getKey().startsWith(GpxConstants.GPX_PREFIX) ? e.getKey().substring(GpxConstants.GPX_PREFIX.length()) : e.getKey();
794                String v = e.getValue();
795                if (GpxConstants.RTE_TRK_KEYS.contains(k)) {
796                    trkAttr.put(k, v);
797                } else {
798                    k = GpxConstants.EXTENSION_ABBREVIATIONS.entrySet()
799                            .stream()
800                            .filter(s -> s.getValue().equals(e.getKey()))
801                            .map(s -> s.getKey().substring(GpxConstants.GPX_PREFIX.length()))
802                            .findAny()
803                            .orElse(k);
804                    if (k.startsWith("extension")) {
805                        String[] chain = k.split(":");
806                        if (chain.length >= 3 && "segment".equals(chain[2])) {
807                            segExts.addFlat(chain, v);
808                        } else {
809                            trkExts.addFlat(chain, v);
810                        }
811                    }
812
813                }
814            }
815            List<WayPoint> trkseg = new ArrayList<>();
816            for (Node n : w.getNodes()) {
817                if (!n.isUsable()) {
818                    if (!trkseg.isEmpty()) {
819                        trk.add(new GpxTrackSegment(trkseg));
820                        trkseg.clear();
821                    }
822                    continue;
823                }
824                if (!n.isTagged() || containsOnlyGpxTags(n)) {
825                    doneNodes.add(n);
826                }
827                trkseg.add(nodeToWayPoint(n));
828            }
829            trk.add(new GpxTrackSegment(trkseg));
830            trk.forEach(gpxseg -> gpxseg.getExtensions().addAll(segExts));
831            GpxTrack gpxtrk = new GpxTrack(trk, trkAttr);
832            gpxtrk.getExtensions().addAll(trkExts);
833            gpxData.addTrack(gpxtrk);
834        });
835    }
836
837    private static boolean containsOnlyGpxTags(Tagged t) {
838        for (String key : t.getKeys().keySet()) {
839            if (!GpxConstants.WPT_KEYS.contains(key) && !key.startsWith(GpxConstants.GPX_PREFIX)) {
840                return false;
841            }
842        }
843        return true;
844    }
845
846    /**
847     * Reads the Gpx key from the given {@link OsmPrimitive}, with or without &quot;gpx:&quot; prefix
848     * @param prim OSM primitive
849     * @param key GPX key without prefix
850     * @return the value or <code>null</code> if not present
851     * @since 15419
852     */
853    public static String gpxVal(OsmPrimitive prim, String key) {
854        return Optional.ofNullable(prim.get(GpxConstants.GPX_PREFIX + key)).orElse(prim.get(key));
855    }
856
857    /**
858     * @param n the {@code Node} to convert
859     * @return {@code WayPoint} object
860     * @since 13210
861     */
862    public static WayPoint nodeToWayPoint(Node n) {
863        return nodeToWayPoint(n, Long.MIN_VALUE);
864    }
865
866    /**
867     * @param n the {@code Node} to convert
868     * @param time a timestamp value in milliseconds from the epoch.
869     * @return {@code WayPoint} object
870     * @since 13210
871     */
872    public static WayPoint nodeToWayPoint(Node n, long time) {
873        WayPoint wpt = new WayPoint(n.getCoor());
874
875        // Position info
876
877        addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
878
879        try {
880            String v;
881            if (time > Long.MIN_VALUE) {
882                wpt.setTimeInMillis(time);
883            } else if ((v = gpxVal(n, GpxConstants.PT_TIME)) != null) {
884                wpt.setTimeInMillis(DateUtils.tsFromString(v));
885            } else if (!n.isTimestampEmpty()) {
886                wpt.setTime(Integer.toUnsignedLong(n.getRawTimestamp()));
887            }
888        } catch (UncheckedParseException e) {
889            Logging.error(e);
890        }
891
892        addDoubleIfPresent(wpt, n, GpxConstants.PT_MAGVAR);
893        addDoubleIfPresent(wpt, n, GpxConstants.PT_GEOIDHEIGHT);
894
895        // Description info
896
897        addStringIfPresent(wpt, n, GpxConstants.GPX_NAME);
898        addStringIfPresent(wpt, n, GpxConstants.GPX_DESC, "description");
899        addStringIfPresent(wpt, n, GpxConstants.GPX_CMT, "comment");
900        addStringIfPresent(wpt, n, GpxConstants.GPX_SRC, "source", "source:position");
901
902        Collection<GpxLink> links = new ArrayList<>();
903        for (String key : new String[]{"link", "url", "website", "contact:website"}) {
904            String value = gpxVal(n, key);
905            if (value != null) {
906                links.add(new GpxLink(value));
907            }
908        }
909        wpt.put(GpxConstants.META_LINKS, links);
910
911        addStringIfPresent(wpt, n, GpxConstants.PT_SYM, "wpt_symbol");
912        addStringIfPresent(wpt, n, GpxConstants.PT_TYPE);
913
914        // Accuracy info
915        addStringIfPresent(wpt, n, GpxConstants.PT_FIX, "gps:fix");
916        addIntegerIfPresent(wpt, n, GpxConstants.PT_SAT, "gps:sat");
917        addDoubleIfPresent(wpt, n, GpxConstants.PT_HDOP, "gps:hdop");
918        addDoubleIfPresent(wpt, n, GpxConstants.PT_VDOP, "gps:vdop");
919        addDoubleIfPresent(wpt, n, GpxConstants.PT_PDOP, "gps:pdop");
920        addDoubleIfPresent(wpt, n, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
921        addIntegerIfPresent(wpt, n, GpxConstants.PT_DGPSID, "gps:dgpsid");
922
923        return wpt;
924    }
925
926    private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes) {
927        List<Node> sortedNodes = new ArrayList<>(nodes);
928        sortedNodes.removeAll(doneNodes);
929        Collections.sort(sortedNodes);
930        for (Node n : sortedNodes) {
931            if (n.isIncomplete() || n.isDeleted()) {
932                continue;
933            }
934            gpxData.waypoints.add(nodeToWayPoint(n));
935        }
936    }
937
938    private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
939        List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
940        possibleKeys.add(0, gpxKey);
941        for (String key : possibleKeys) {
942            String value = gpxVal(p, key);
943            if (value != null) {
944                try {
945                    int i = Integer.parseInt(value);
946                    // Sanity checks
947                    if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
948                        (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
949                        wpt.put(gpxKey, value);
950                        break;
951                    }
952                } catch (NumberFormatException e) {
953                    Logging.trace(e);
954                }
955            }
956        }
957    }
958
959    private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
960        List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
961        possibleKeys.add(0, gpxKey);
962        for (String key : possibleKeys) {
963            String value = gpxVal(p, key);
964            if (value != null) {
965                try {
966                    double d = Double.parseDouble(value);
967                    // Sanity checks
968                    if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
969                        wpt.put(gpxKey, value);
970                        break;
971                    }
972                } catch (NumberFormatException e) {
973                    Logging.trace(e);
974                }
975            }
976        }
977    }
978
979    private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
980        List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
981        possibleKeys.add(0, gpxKey);
982        for (String key : possibleKeys) {
983            String value = gpxVal(p, key);
984            // Sanity checks
985            if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
986                wpt.put(gpxKey, value);
987                break;
988            }
989        }
990    }
991
992    /**
993     * Converts OSM data behind this layer to GPX data.
994     * @return GPX data
995     */
996    public GpxData toGpxData() {
997        return toGpxData(data, getAssociatedFile());
998    }
999
1000    /**
1001     * Action that converts this OSM layer to a GPX layer.
1002     */
1003    public class ConvertToGpxLayerAction extends AbstractAction {
1004        /**
1005         * Constructs a new {@code ConvertToGpxLayerAction}.
1006         */
1007        public ConvertToGpxLayerAction() {
1008            super(tr("Convert to GPX layer"));
1009            new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
1010            putValue("help", ht("/Action/ConvertToGpxLayer"));
1011        }
1012
1013        @Override
1014        public void actionPerformed(ActionEvent e) {
1015            final GpxData gpxData = toGpxData();
1016            final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
1017            if (getAssociatedFile() != null) {
1018                String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
1019                gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
1020            }
1021            MainApplication.getLayerManager().addLayer(gpxLayer, false);
1022            if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
1023                MainApplication.getLayerManager().addLayer(
1024                        new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false);
1025            }
1026            MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
1027        }
1028    }
1029
1030    /**
1031     * Determines if this layer contains data at the given coordinate.
1032     * @param coor the coordinate
1033     * @return {@code true} if data sources bounding boxes contain {@code coor}
1034     */
1035    public boolean containsPoint(LatLon coor) {
1036        // we'll assume that if this has no data sources
1037        // that it also has no borders
1038        if (this.data.getDataSources().isEmpty())
1039            return true;
1040
1041        boolean layerBoundsPoint = false;
1042        for (DataSource src : this.data.getDataSources()) {
1043            if (src.bounds.contains(coor)) {
1044                layerBoundsPoint = true;
1045                break;
1046            }
1047        }
1048        return layerBoundsPoint;
1049    }
1050
1051    /**
1052     * Replies the set of conflicts currently managed in this layer.
1053     *
1054     * @return the set of conflicts currently managed in this layer
1055     */
1056    public ConflictCollection getConflicts() {
1057        return data.getConflicts();
1058    }
1059
1060    @Override
1061    public boolean isDownloadable() {
1062        return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked();
1063    }
1064
1065    @Override
1066    public boolean isUploadable() {
1067        return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked();
1068    }
1069
1070    @Override
1071    public boolean requiresUploadToServer() {
1072        return isUploadable() && requiresUploadToServer;
1073    }
1074
1075    @Override
1076    public boolean requiresSaveToFile() {
1077        return getAssociatedFile() != null && requiresSaveToFile;
1078    }
1079
1080    @Override
1081    public void onPostLoadFromFile() {
1082        setRequiresSaveToFile(false);
1083        setRequiresUploadToServer(isModified());
1084        invalidate();
1085    }
1086
1087    /**
1088     * Actions run after data has been downloaded to this layer.
1089     */
1090    public void onPostDownloadFromServer() {
1091        setRequiresSaveToFile(true);
1092        setRequiresUploadToServer(isModified());
1093        invalidate();
1094    }
1095
1096    @Override
1097    public void onPostSaveToFile() {
1098        setRequiresSaveToFile(false);
1099        setRequiresUploadToServer(isModified());
1100    }
1101
1102    @Override
1103    public void onPostUploadToServer() {
1104        setRequiresUploadToServer(isModified());
1105        // keep requiresSaveToDisk unchanged
1106    }
1107
1108    private class ConsistencyTestAction extends AbstractAction {
1109
1110        ConsistencyTestAction() {
1111            super(tr("Dataset consistency test"));
1112        }
1113
1114        @Override
1115        public void actionPerformed(ActionEvent e) {
1116            String result = DatasetConsistencyTest.runTests(data);
1117            if (result.isEmpty()) {
1118                JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No problems found"));
1119            } else {
1120                JPanel p = new JPanel(new GridBagLayout());
1121                p.add(new JLabel(tr("Following problems found:")), GBC.eol());
1122                JosmTextArea info = new JosmTextArea(result, 20, 60);
1123                info.setCaretPosition(0);
1124                info.setEditable(false);
1125                p.add(new JScrollPane(info), GBC.eop());
1126
1127                JOptionPane.showMessageDialog(MainApplication.getMainFrame(), p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1128            }
1129        }
1130    }
1131
1132    @Override
1133    public synchronized void destroy() {
1134        super.destroy();
1135        data.removeSelectionListener(this);
1136        data.removeHighlightUpdateListener(this);
1137        data.removeDataSetListener(dataSetListenerAdapter);
1138        data.removeDataSetListener(MultipolygonCache.getInstance());
1139        removeClipboardDataFor(this);
1140        recentRelations.clear();
1141    }
1142
1143    protected static void removeClipboardDataFor(OsmDataLayer osm) {
1144        Transferable clipboardContents = ClipboardUtils.getClipboardContent();
1145        if (clipboardContents != null && clipboardContents.isDataFlavorSupported(OsmLayerTransferData.OSM_FLAVOR)) {
1146            try {
1147                Object o = clipboardContents.getTransferData(OsmLayerTransferData.OSM_FLAVOR);
1148                if (o instanceof OsmLayerTransferData && osm.equals(((OsmLayerTransferData) o).getLayer())) {
1149                    ClipboardUtils.clear();
1150                }
1151            } catch (UnsupportedFlavorException | IOException e) {
1152                Logging.error(e);
1153            }
1154        }
1155    }
1156
1157    @Override
1158    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1159        invalidate();
1160        setRequiresSaveToFile(true);
1161        setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1162    }
1163
1164    @Override
1165    public void selectionChanged(SelectionChangeEvent event) {
1166        invalidate();
1167    }
1168
1169    @Override
1170    public void projectionChanged(Projection oldValue, Projection newValue) {
1171         // No reprojection required. The dataset itself is registered as projection
1172         // change listener and already got notified.
1173    }
1174
1175    @Override
1176    public final boolean isUploadDiscouraged() {
1177        return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1178    }
1179
1180    /**
1181     * Sets the "discouraged upload" flag.
1182     * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1183     * This feature allows to use "private" data layers.
1184     */
1185    public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1186        if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1187                (uploadDiscouraged ^ isUploadDiscouraged())) {
1188            data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1189            for (LayerStateChangeListener l : layerStateChangeListeners) {
1190                l.uploadDiscouragedChanged(this, uploadDiscouraged);
1191            }
1192        }
1193    }
1194
1195    @Override
1196    public final boolean isModified() {
1197        return data.isModified();
1198    }
1199
1200    @Override
1201    public boolean isSavable() {
1202        return true; // With OsmExporter
1203    }
1204
1205    @Override
1206    public boolean checkSaveConditions() {
1207        if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() ->
1208            new ExtendedDialog(
1209                    MainApplication.getMainFrame(),
1210                    tr("Empty document"),
1211                    tr("Save anyway"), tr("Cancel"))
1212                .setContent(tr("The document contains no data."))
1213                .setButtonIcons("save", "cancel")
1214                .showDialog().getValue()
1215        )) {
1216            return false;
1217        }
1218
1219        ConflictCollection conflictsCol = getConflicts();
1220        return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1221            new ExtendedDialog(
1222                    MainApplication.getMainFrame(),
1223                    /* I18N: Display title of the window showing conflicts */
1224                    tr("Conflicts"),
1225                    tr("Reject Conflicts and Save"), tr("Cancel"))
1226                .setContent(
1227                    tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1228                .setButtonIcons("save", "cancel")
1229                .showDialog().getValue()
1230        );
1231    }
1232
1233    /**
1234     * Check the data set if it would be empty on save. It is empty, if it contains
1235     * no objects (after all objects that are created and deleted without being
1236     * transferred to the server have been removed).
1237     *
1238     * @return <code>true</code>, if a save result in an empty data set.
1239     */
1240    private boolean isDataSetEmpty() {
1241        if (data != null) {
1242            for (OsmPrimitive osm : data.allNonDeletedPrimitives()) {
1243                if (!osm.isDeleted() || !osm.isNewOrUndeleted())
1244                    return false;
1245            }
1246        }
1247        return true;
1248    }
1249
1250    @Override
1251    public File createAndOpenSaveFileChooser() {
1252        String extension = PROPERTY_SAVE_EXTENSION.get();
1253        File file = getAssociatedFile();
1254        if (file == null && isRenamed()) {
1255            StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1256            if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1257                filename.append('.').append(extension);
1258            }
1259            file = new File(filename.toString());
1260        }
1261        return new FileChooserManager()
1262            .title(tr("Save OSM file"))
1263            .extension(extension)
1264            .file(file)
1265            .additionalTypes(t -> t != WMSLayerImporter.FILE_FILTER && t != NoteExporter.FILE_FILTER && t != ValidatorErrorExporter.FILE_FILTER)
1266            .getFileForSave();
1267    }
1268
1269    @Override
1270    public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1271        UploadDialog dialog = UploadDialog.getUploadDialog();
1272        return new UploadLayerTask(
1273                dialog.getUploadStrategySpecification(),
1274                this,
1275                monitor,
1276                dialog.getChangeset());
1277    }
1278
1279    @Override
1280    public AbstractUploadDialog getUploadDialog() {
1281        UploadDialog dialog = UploadDialog.getUploadDialog();
1282        dialog.setUploadedPrimitives(new APIDataSet(data));
1283        return dialog;
1284    }
1285
1286    @Override
1287    public ProjectionBounds getViewProjectionBounds() {
1288        BoundingXYVisitor v = new BoundingXYVisitor();
1289        v.visit(data.getDataSourceBoundingBox());
1290        if (!v.hasExtend()) {
1291            v.computeBoundingBox(data.getNodes());
1292        }
1293        return v.getBounds();
1294    }
1295
1296    @Override
1297    public void highlightUpdated(HighlightUpdateEvent e) {
1298        invalidate();
1299    }
1300
1301    @Override
1302    public void setName(String name) {
1303        if (data != null) {
1304            data.setName(name);
1305        }
1306        super.setName(name);
1307    }
1308
1309    /**
1310     * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer.
1311     * @since 13434
1312     */
1313    public void setUploadInProgress() {
1314        if (!isUploadInProgress.compareAndSet(false, true)) {
1315            Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName());
1316        }
1317    }
1318
1319    /**
1320     * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer.
1321     * @since 13434
1322     */
1323    public void unsetUploadInProgress() {
1324        if (!isUploadInProgress.compareAndSet(true, false)) {
1325            Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName());
1326        }
1327    }
1328
1329    @Override
1330    public boolean isUploadInProgress() {
1331        return isUploadInProgress.get();
1332    }
1333}