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