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