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

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

see #8039, see #10456 - fix regressions and code style issues

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