source: josm/trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java @ 13453

Last change on this file since 13453 was 13453, checked in by Don-vip, 5 months ago

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

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