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

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

fix #17003 - catch UncheckedParseException when converting OSM data to GPX

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