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

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

fix #13153 - Should not warn to upload/save "modified" layers with 0 objects

  • Property svn:eol-style set to native
File size: 43.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 should not be editable */
134    private final AtomicBoolean isReadOnly = 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 (isReadOnly()) {
428            // If the layer is read only then change the default icon to a clock
429            base = new ImageProvider("clock").setMaxSize(ImageSizes.LAYER);
430        }
431        return base.get();
432    }
433
434    /**
435     * Draw all primitives in this layer but do not draw modified ones (they
436     * are drawn by the edit layer).
437     * Draw nodes last to overlap the ways they belong to.
438     */
439    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
440        boolean active = mv.getLayerManager().getActiveLayer() == this;
441        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
442        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
443
444        // draw the hatched area for non-downloaded region. only draw if we're the active
445        // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
446        if (active && Config.getPref().getBoolean("draw.data.downloaded_area", true) && !data.getDataSources().isEmpty()) {
447            // initialize area with current viewport
448            Rectangle b = mv.getBounds();
449            // on some platforms viewport bounds seem to be offset from the left,
450            // over-grow it just to be sure
451            b.grow(100, 100);
452            Path2D p = new Path2D.Double();
453
454            // combine successively downloaded areas
455            for (Bounds bounds : data.getDataSourceBounds()) {
456                if (bounds.isCollapsed()) {
457                    continue;
458                }
459                p.append(mv.getState().getArea(bounds), false);
460            }
461            // subtract combined areas
462            Area a = new Area(b);
463            a.subtract(new Area(p));
464
465            // paint remainder
466            MapViewPoint anchor = mv.getState().getPointFor(new EastNorth(0, 0));
467            Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getInView().getX() % HATCHED_SIZE,
468                    anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
469            g.setPaint(new TexturePaint(hatched, anchorRect));
470            g.fill(a);
471        }
472
473        Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
474        painter.render(data, virtual, box);
475        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
476    }
477
478    @Override public String getToolTipText() {
479        DataCountVisitor counter = new DataCountVisitor();
480        for (final OsmPrimitive osm : data.allPrimitives()) {
481            osm.accept(counter);
482        }
483        int nodes = counter.nodes - counter.deletedNodes;
484        int ways = counter.ways - counter.deletedWays;
485        int rels = counter.relations - counter.deletedRelations;
486
487        StringBuilder tooltip = new StringBuilder("<html>")
488                .append(trn("{0} node", "{0} nodes", nodes, nodes))
489                .append("<br>")
490                .append(trn("{0} way", "{0} ways", ways, ways))
491                .append("<br>")
492                .append(trn("{0} relation", "{0} relations", rels, rels));
493
494        File f = getAssociatedFile();
495        if (f != null) {
496            tooltip.append("<br>").append(f.getPath());
497        }
498        tooltip.append("</html>");
499        return tooltip.toString();
500    }
501
502    @Override public void mergeFrom(final Layer from) {
503        final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
504        monitor.setCancelable(false);
505        if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
506            setUploadDiscouraged(true);
507        }
508        mergeFrom(((OsmDataLayer) from).data, monitor);
509        monitor.close();
510    }
511
512    /**
513     * merges the primitives in dataset <code>from</code> into the dataset of
514     * this layer
515     *
516     * @param from  the source data set
517     */
518    public void mergeFrom(final DataSet from) {
519        mergeFrom(from, null);
520    }
521
522    /**
523     * merges the primitives in dataset <code>from</code> into the dataset of this layer
524     *
525     * @param from  the source data set
526     * @param progressMonitor the progress monitor, can be {@code null}
527     */
528    public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
529        final DataSetMerger visitor = new DataSetMerger(data, from);
530        try {
531            visitor.merge(progressMonitor);
532        } catch (DataIntegrityProblemException e) {
533            Logging.error(e);
534            JOptionPane.showMessageDialog(
535                    Main.parent,
536                    e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
537                    tr("Error"),
538                    JOptionPane.ERROR_MESSAGE
539            );
540            return;
541        }
542
543        Area a = data.getDataSourceArea();
544
545        // copy the merged layer's data source info.
546        // only add source rectangles if they are not contained in the layer already.
547        for (DataSource src : from.getDataSources()) {
548            if (a == null || !a.contains(src.bounds.asRect())) {
549                data.addDataSource(src);
550            }
551        }
552
553        // copy the merged layer's API version
554        if (data.getVersion() == null) {
555            data.setVersion(from.getVersion());
556        }
557
558        int numNewConflicts = 0;
559        for (Conflict<?> c : visitor.getConflicts()) {
560            if (!data.getConflicts().hasConflict(c)) {
561                numNewConflicts++;
562                data.getConflicts().add(c);
563            }
564        }
565        // repaint to make sure new data is displayed properly.
566        invalidate();
567        // warn about new conflicts
568        MapFrame map = MainApplication.getMap();
569        if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
570            map.conflictDialog.warnNumNewConflicts(numNewConflicts);
571        }
572    }
573
574    @Override
575    public boolean isMergable(final Layer other) {
576        // allow merging between normal layers and discouraged layers with a warning (see #7684)
577        return other instanceof OsmDataLayer;
578    }
579
580    @Override
581    public void visitBoundingBox(final BoundingXYVisitor v) {
582        for (final Node n: data.getNodes()) {
583            if (n.isUsable()) {
584                v.visit(n);
585            }
586        }
587    }
588
589    /**
590     * Clean out the data behind the layer. This means clearing the redo/undo lists,
591     * really deleting all deleted objects and reset the modified flags. This should
592     * be done after an upload, even after a partial upload.
593     *
594     * @param processed A list of all objects that were actually uploaded.
595     *         May be <code>null</code>, which means nothing has been uploaded
596     */
597    public void cleanupAfterUpload(final Collection<? extends IPrimitive> processed) {
598        // return immediately if an upload attempt failed
599        if (processed == null || processed.isEmpty())
600            return;
601
602        MainApplication.undoRedo.clean(data);
603
604        // if uploaded, clean the modified flags as well
605        data.cleanupDeletedPrimitives();
606        data.beginUpdate();
607        try {
608            for (OsmPrimitive p: data.allPrimitives()) {
609                if (processed.contains(p)) {
610                    p.setModified(false);
611                }
612            }
613        } finally {
614            data.endUpdate();
615        }
616    }
617
618    @Override
619    public Object getInfoComponent() {
620        final DataCountVisitor counter = new DataCountVisitor();
621        for (final OsmPrimitive osm : data.allPrimitives()) {
622            osm.accept(counter);
623        }
624        final JPanel p = new JPanel(new GridBagLayout());
625
626        String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
627        if (counter.deletedNodes > 0) {
628            nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+')';
629        }
630
631        String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
632        if (counter.deletedWays > 0) {
633            wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+')';
634        }
635
636        String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
637        if (counter.deletedRelations > 0) {
638            relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+')';
639        }
640
641        p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
642        p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
643        p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
644        p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
645        p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))),
646                GBC.eop().insets(15, 0, 0, 0));
647        if (isUploadDiscouraged()) {
648            p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15, 0, 0, 0));
649        }
650        if (data.getUploadPolicy() == UploadPolicy.BLOCKED) {
651            p.add(new JLabel(tr("Upload is blocked")), GBC.eop().insets(15, 0, 0, 0));
652        }
653
654        return p;
655    }
656
657    @Override public Action[] getMenuEntries() {
658        List<Action> actions = new ArrayList<>();
659        actions.addAll(Arrays.asList(
660                LayerListDialog.getInstance().createActivateLayerAction(this),
661                LayerListDialog.getInstance().createShowHideLayerAction(),
662                LayerListDialog.getInstance().createDeleteLayerAction(),
663                SeparatorLayerAction.INSTANCE,
664                LayerListDialog.getInstance().createMergeLayerAction(this),
665                LayerListDialog.getInstance().createDuplicateLayerAction(this),
666                new LayerSaveAction(this),
667                new LayerSaveAsAction(this)));
668        if (ExpertToggleAction.isExpert()) {
669            actions.addAll(Arrays.asList(
670                    new LayerGpxExportAction(this),
671                    new ConvertToGpxLayerAction()));
672        }
673        actions.addAll(Arrays.asList(
674                SeparatorLayerAction.INSTANCE,
675                new RenameLayerAction(getAssociatedFile(), this)));
676        if (ExpertToggleAction.isExpert()) {
677            actions.add(new ToggleUploadDiscouragedLayerAction(this));
678        }
679        actions.addAll(Arrays.asList(
680                new ConsistencyTestAction(),
681                SeparatorLayerAction.INSTANCE,
682                new LayerListPopup.InfoAction(this)));
683        return actions.toArray(new Action[actions.size()]);
684    }
685
686    /**
687     * Converts given OSM dataset to GPX data.
688     * @param data OSM dataset
689     * @param file output .gpx file
690     * @return GPX data
691     */
692    public static GpxData toGpxData(DataSet data, File file) {
693        GpxData gpxData = new GpxData();
694        gpxData.storageFile = file;
695        Set<Node> doneNodes = new HashSet<>();
696        waysToGpxData(data.getWays(), gpxData, doneNodes);
697        nodesToGpxData(data.getNodes(), gpxData, doneNodes);
698        return gpxData;
699    }
700
701    private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes) {
702        /* When the dataset has been obtained from a gpx layer and now is being converted back,
703         * the ways have negative ids. The first created way corresponds to the first gpx segment,
704         * and has the highest id (i.e., closest to zero).
705         * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
706         * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
707         */
708        ways.stream()
709                .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
710                .forEachOrdered(w -> {
711            if (!w.isUsable()) {
712                return;
713            }
714            Collection<Collection<WayPoint>> trk = new ArrayList<>();
715            Map<String, Object> trkAttr = new HashMap<>();
716
717            String name = w.get("name");
718            if (name != null) {
719                trkAttr.put("name", name);
720            }
721
722            List<WayPoint> trkseg = null;
723            for (Node n : w.getNodes()) {
724                if (!n.isUsable()) {
725                    trkseg = null;
726                    continue;
727                }
728                if (trkseg == null) {
729                    trkseg = new ArrayList<>();
730                    trk.add(trkseg);
731                }
732                if (!n.isTagged()) {
733                    doneNodes.add(n);
734                }
735                trkseg.add(nodeToWayPoint(n));
736            }
737
738            gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
739        });
740    }
741
742    private static WayPoint nodeToWayPoint(Node n) {
743        WayPoint wpt = new WayPoint(n.getCoor());
744
745        // Position info
746
747        addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
748
749        if (!n.isTimestampEmpty()) {
750            wpt.put("time", DateUtils.fromTimestamp(n.getRawTimestamp()));
751            wpt.setTime();
752        }
753
754        addDoubleIfPresent(wpt, n, GpxConstants.PT_MAGVAR);
755        addDoubleIfPresent(wpt, n, GpxConstants.PT_GEOIDHEIGHT);
756
757        // Description info
758
759        addStringIfPresent(wpt, n, GpxConstants.GPX_NAME);
760        addStringIfPresent(wpt, n, GpxConstants.GPX_DESC, "description");
761        addStringIfPresent(wpt, n, GpxConstants.GPX_CMT, "comment");
762        addStringIfPresent(wpt, n, GpxConstants.GPX_SRC, "source", "source:position");
763
764        Collection<GpxLink> links = new ArrayList<>();
765        for (String key : new String[]{"link", "url", "website", "contact:website"}) {
766            String value = n.get(key);
767            if (value != null) {
768                links.add(new GpxLink(value));
769            }
770        }
771        wpt.put(GpxConstants.META_LINKS, links);
772
773        addStringIfPresent(wpt, n, GpxConstants.PT_SYM, "wpt_symbol");
774        addStringIfPresent(wpt, n, GpxConstants.PT_TYPE);
775
776        // Accuracy info
777        addStringIfPresent(wpt, n, GpxConstants.PT_FIX, "gps:fix");
778        addIntegerIfPresent(wpt, n, GpxConstants.PT_SAT, "gps:sat");
779        addDoubleIfPresent(wpt, n, GpxConstants.PT_HDOP, "gps:hdop");
780        addDoubleIfPresent(wpt, n, GpxConstants.PT_VDOP, "gps:vdop");
781        addDoubleIfPresent(wpt, n, GpxConstants.PT_PDOP, "gps:pdop");
782        addDoubleIfPresent(wpt, n, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
783        addIntegerIfPresent(wpt, n, GpxConstants.PT_DGPSID, "gps:dgpsid");
784
785        return wpt;
786    }
787
788    private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes) {
789        List<Node> sortedNodes = new ArrayList<>(nodes);
790        sortedNodes.removeAll(doneNodes);
791        Collections.sort(sortedNodes);
792        for (Node n : sortedNodes) {
793            if (n.isIncomplete() || n.isDeleted()) {
794                continue;
795            }
796            gpxData.waypoints.add(nodeToWayPoint(n));
797        }
798    }
799
800    private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
801        List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
802        possibleKeys.add(0, gpxKey);
803        for (String key : possibleKeys) {
804            String value = p.get(key);
805            if (value != null) {
806                try {
807                    int i = Integer.parseInt(value);
808                    // Sanity checks
809                    if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
810                        (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
811                        wpt.put(gpxKey, value);
812                        break;
813                    }
814                } catch (NumberFormatException e) {
815                    Logging.trace(e);
816                }
817            }
818        }
819    }
820
821    private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
822        List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
823        possibleKeys.add(0, gpxKey);
824        for (String key : possibleKeys) {
825            String value = p.get(key);
826            if (value != null) {
827                try {
828                    double d = Double.parseDouble(value);
829                    // Sanity checks
830                    if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
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 addStringIfPresent(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            // Sanity checks
847            if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
848                wpt.put(gpxKey, value);
849                break;
850            }
851        }
852    }
853
854    /**
855     * Converts OSM data behind this layer to GPX data.
856     * @return GPX data
857     */
858    public GpxData toGpxData() {
859        return toGpxData(data, getAssociatedFile());
860    }
861
862    /**
863     * Action that converts this OSM layer to a GPX layer.
864     */
865    public class ConvertToGpxLayerAction extends AbstractAction {
866        /**
867         * Constructs a new {@code ConvertToGpxLayerAction}.
868         */
869        public ConvertToGpxLayerAction() {
870            super(tr("Convert to GPX layer"));
871            new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
872            putValue("help", ht("/Action/ConvertToGpxLayer"));
873        }
874
875        @Override
876        public void actionPerformed(ActionEvent e) {
877            final GpxData gpxData = toGpxData();
878            final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
879            if (getAssociatedFile() != null) {
880                String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
881                gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
882            }
883            MainApplication.getLayerManager().addLayer(gpxLayer);
884            if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
885                MainApplication.getLayerManager().addLayer(new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer));
886            }
887            MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
888        }
889    }
890
891    /**
892     * Determines if this layer contains data at the given coordinate.
893     * @param coor the coordinate
894     * @return {@code true} if data sources bounding boxes contain {@code coor}
895     */
896    public boolean containsPoint(LatLon coor) {
897        // we'll assume that if this has no data sources
898        // that it also has no borders
899        if (this.data.getDataSources().isEmpty())
900            return true;
901
902        boolean layerBoundsPoint = false;
903        for (DataSource src : this.data.getDataSources()) {
904            if (src.bounds.contains(coor)) {
905                layerBoundsPoint = true;
906                break;
907            }
908        }
909        return layerBoundsPoint;
910    }
911
912    /**
913     * Replies the set of conflicts currently managed in this layer.
914     *
915     * @return the set of conflicts currently managed in this layer
916     */
917    public ConflictCollection getConflicts() {
918        return data.getConflicts();
919    }
920
921    @Override
922    public boolean isUploadable() {
923        return data.getUploadPolicy() != UploadPolicy.BLOCKED;
924    }
925
926    @Override
927    public boolean requiresUploadToServer() {
928        return isUploadable() && requiresUploadToServer;
929    }
930
931    @Override
932    public boolean requiresSaveToFile() {
933        return getAssociatedFile() != null && requiresSaveToFile;
934    }
935
936    @Override
937    public void onPostLoadFromFile() {
938        setRequiresSaveToFile(false);
939        setRequiresUploadToServer(isModified());
940        invalidate();
941    }
942
943    /**
944     * Actions run after data has been downloaded to this layer.
945     */
946    public void onPostDownloadFromServer() {
947        setRequiresSaveToFile(true);
948        setRequiresUploadToServer(isModified());
949        invalidate();
950    }
951
952    @Override
953    public void onPostSaveToFile() {
954        setRequiresSaveToFile(false);
955        setRequiresUploadToServer(isModified());
956    }
957
958    @Override
959    public void onPostUploadToServer() {
960        setRequiresUploadToServer(isModified());
961        // keep requiresSaveToDisk unchanged
962    }
963
964    private class ConsistencyTestAction extends AbstractAction {
965
966        ConsistencyTestAction() {
967            super(tr("Dataset consistency test"));
968        }
969
970        @Override
971        public void actionPerformed(ActionEvent e) {
972            String result = DatasetConsistencyTest.runTests(data);
973            if (result.isEmpty()) {
974                JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
975            } else {
976                JPanel p = new JPanel(new GridBagLayout());
977                p.add(new JLabel(tr("Following problems found:")), GBC.eol());
978                JosmTextArea info = new JosmTextArea(result, 20, 60);
979                info.setCaretPosition(0);
980                info.setEditable(false);
981                p.add(new JScrollPane(info), GBC.eop());
982
983                JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
984            }
985        }
986    }
987
988    @Override
989    public synchronized void destroy() {
990        super.destroy();
991        data.removeSelectionListener(this);
992        data.removeHighlightUpdateListener(this);
993    }
994
995    @Override
996    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
997        invalidate();
998        setRequiresSaveToFile(true);
999        setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1000    }
1001
1002    @Override
1003    public void selectionChanged(SelectionChangeEvent event) {
1004        invalidate();
1005    }
1006
1007    @Override
1008    public void projectionChanged(Projection oldValue, Projection newValue) {
1009         // No reprojection required. The dataset itself is registered as projection
1010         // change listener and already got notified.
1011    }
1012
1013    /**
1014     * Determines if upload is being discouraged.
1015     * (i.e. this dataset contains private data which should not be uploaded)
1016     * @return {@code true} if upload is being discouraged, {@code false} otherwise
1017     */
1018    @Override
1019    public final boolean isUploadDiscouraged() {
1020        return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1021    }
1022
1023    /**
1024     * Sets the "discouraged upload" flag.
1025     * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1026     * This feature allows to use "private" data layers.
1027     */
1028    public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1029        if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1030                (uploadDiscouraged ^ isUploadDiscouraged())) {
1031            data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1032            for (LayerStateChangeListener l : layerStateChangeListeners) {
1033                l.uploadDiscouragedChanged(this, uploadDiscouraged);
1034            }
1035        }
1036    }
1037
1038    @Override
1039    public final boolean isModified() {
1040        return data.isModified();
1041    }
1042
1043    @Override
1044    public boolean isSavable() {
1045        return true; // With OsmExporter
1046    }
1047
1048    @Override
1049    public boolean checkSaveConditions() {
1050        if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() -> {
1051            if (GraphicsEnvironment.isHeadless()) {
1052                return 2;
1053            }
1054            return new ExtendedDialog(
1055                    Main.parent,
1056                    tr("Empty document"),
1057                    tr("Save anyway"), tr("Cancel"))
1058                .setContent(tr("The document contains no data."))
1059                .setButtonIcons("save", "cancel")
1060                .showDialog().getValue();
1061        })) {
1062            return false;
1063        }
1064
1065        ConflictCollection conflictsCol = getConflicts();
1066        return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1067            new ExtendedDialog(
1068                    Main.parent,
1069                    /* I18N: Display title of the window showing conflicts */
1070                    tr("Conflicts"),
1071                    tr("Reject Conflicts and Save"), tr("Cancel"))
1072                .setContent(
1073                    tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1074                .setButtonIcons("save", "cancel")
1075                .showDialog().getValue()
1076        );
1077    }
1078
1079    /**
1080     * Check the data set if it would be empty on save. It is empty, if it contains
1081     * no objects (after all objects that are created and deleted without being
1082     * transferred to the server have been removed).
1083     *
1084     * @return <code>true</code>, if a save result in an empty data set.
1085     */
1086    private boolean isDataSetEmpty() {
1087        if (data != null) {
1088            for (OsmPrimitive osm : data.allNonDeletedPrimitives()) {
1089                if (!osm.isDeleted() || !osm.isNewOrUndeleted())
1090                    return false;
1091            }
1092        }
1093        return true;
1094    }
1095
1096    @Override
1097    public File createAndOpenSaveFileChooser() {
1098        String extension = PROPERTY_SAVE_EXTENSION.get();
1099        File file = getAssociatedFile();
1100        if (file == null && isRenamed()) {
1101            StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1102            if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1103                filename.append('.').append(extension);
1104            }
1105            file = new File(filename.toString());
1106        }
1107        return new FileChooserManager()
1108            .title(tr("Save OSM file"))
1109            .extension(extension)
1110            .file(file)
1111            .allTypes(true)
1112            .getFileForSave();
1113    }
1114
1115    @Override
1116    public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1117        UploadDialog dialog = UploadDialog.getUploadDialog();
1118        return new UploadLayerTask(
1119                dialog.getUploadStrategySpecification(),
1120                this,
1121                monitor,
1122                dialog.getChangeset());
1123    }
1124
1125    @Override
1126    public AbstractUploadDialog getUploadDialog() {
1127        UploadDialog dialog = UploadDialog.getUploadDialog();
1128        dialog.setUploadedPrimitives(new APIDataSet(data));
1129        return dialog;
1130    }
1131
1132    @Override
1133    public ProjectionBounds getViewProjectionBounds() {
1134        BoundingXYVisitor v = new BoundingXYVisitor();
1135        v.visit(data.getDataSourceBoundingBox());
1136        if (!v.hasExtend()) {
1137            v.computeBoundingBox(data.getNodes());
1138        }
1139        return v.getBounds();
1140    }
1141
1142    @Override
1143    public void highlightUpdated(HighlightUpdateEvent e) {
1144        invalidate();
1145    }
1146
1147    @Override
1148    public void setName(String name) {
1149        if (data != null) {
1150            data.setName(name);
1151        }
1152        super.setName(name);
1153    }
1154
1155    /**
1156     * Sets the isReadOnly flag for the OsmDataLayer as true
1157     */
1158    public void setReadOnly() {
1159        if (!isReadOnly.compareAndSet(false, true)) {
1160            Logging.warn("Trying to set readOnly flag on a readOnly layer ", this.getName());
1161        }
1162    }
1163
1164    /**
1165     * Sets the isReadOnly flag for the OsmDataLayer as false
1166     */
1167    public void unsetReadOnly() {
1168        if (!isReadOnly.compareAndSet(true, false)) {
1169            Logging.warn("Trying to unset readOnly flag on a non-readOnly layer ", this.getName());
1170        }
1171    }
1172
1173    /**
1174     * Returns the value of the isReadOnly flag for the OsmDataLayer
1175     * @return isReadOnly
1176     */
1177    public boolean isReadOnly() {
1178        return isReadOnly.get();
1179    }
1180}
Note: See TracBrowser for help on using the repository browser.