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

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

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

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