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