001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.AlphaComposite;
010import java.awt.Color;
011import java.awt.Composite;
012import java.awt.Graphics2D;
013import java.awt.GridBagLayout;
014import java.awt.Image;
015import java.awt.Point;
016import java.awt.Rectangle;
017import java.awt.TexturePaint;
018import java.awt.event.ActionEvent;
019import java.awt.geom.Area;
020import java.awt.image.BufferedImage;
021import java.io.File;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.Callable;
031import java.util.concurrent.CopyOnWriteArrayList;
032
033import javax.swing.AbstractAction;
034import javax.swing.Action;
035import javax.swing.Icon;
036import javax.swing.ImageIcon;
037import javax.swing.JLabel;
038import javax.swing.JOptionPane;
039import javax.swing.JPanel;
040import javax.swing.JScrollPane;
041
042import org.openstreetmap.josm.Main;
043import org.openstreetmap.josm.actions.ExpertToggleAction;
044import org.openstreetmap.josm.actions.RenameLayerAction;
045import org.openstreetmap.josm.actions.SaveActionBase;
046import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
047import org.openstreetmap.josm.data.APIDataSet;
048import org.openstreetmap.josm.data.Bounds;
049import org.openstreetmap.josm.data.SelectionChangedListener;
050import org.openstreetmap.josm.data.conflict.Conflict;
051import org.openstreetmap.josm.data.conflict.ConflictCollection;
052import org.openstreetmap.josm.data.coor.LatLon;
053import org.openstreetmap.josm.data.gpx.GpxData;
054import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
055import org.openstreetmap.josm.data.gpx.WayPoint;
056import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
057import org.openstreetmap.josm.data.osm.DataSet;
058import org.openstreetmap.josm.data.osm.DataSetMerger;
059import org.openstreetmap.josm.data.osm.DataSource;
060import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
061import org.openstreetmap.josm.data.osm.IPrimitive;
062import org.openstreetmap.josm.data.osm.Node;
063import org.openstreetmap.josm.data.osm.OsmPrimitive;
064import org.openstreetmap.josm.data.osm.Relation;
065import org.openstreetmap.josm.data.osm.Way;
066import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
067import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
068import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
069import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
070import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
071import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
072import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
073import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
074import org.openstreetmap.josm.data.projection.Projection;
075import org.openstreetmap.josm.data.validation.TestError;
076import org.openstreetmap.josm.gui.ExtendedDialog;
077import org.openstreetmap.josm.gui.MapView;
078import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
079import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
080import org.openstreetmap.josm.gui.io.AbstractIOTask;
081import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
082import org.openstreetmap.josm.gui.io.UploadDialog;
083import org.openstreetmap.josm.gui.io.UploadLayerTask;
084import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
085import org.openstreetmap.josm.gui.progress.ProgressMonitor;
086import org.openstreetmap.josm.gui.util.GuiHelper;
087import org.openstreetmap.josm.gui.widgets.JosmTextArea;
088import org.openstreetmap.josm.tools.FilteredCollection;
089import org.openstreetmap.josm.tools.GBC;
090import org.openstreetmap.josm.tools.ImageProvider;
091import org.openstreetmap.josm.tools.date.DateUtils;
092
093/**
094 * A layer that holds OSM data from a specific dataset.
095 * The data can be fully edited.
096 *
097 * @author imi
098 * @since 17
099 */
100public class OsmDataLayer extends AbstractModifiableLayer implements Listener, SelectionChangedListener {
101    /** Property used to know if this layer has to be saved on disk */
102    public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
103    /** Property used to know if this layer has to be uploaded */
104    public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
105
106    private boolean requiresSaveToFile = false;
107    private boolean requiresUploadToServer = false;
108    private boolean isChanged = true;
109    private int highlightUpdateCount;
110
111    /**
112     * List of validation errors in this layer.
113     * @since 3669
114     */
115    public final List<TestError> validationErrors = new ArrayList<>();
116
117    protected void setRequiresSaveToFile(boolean newValue) {
118        boolean oldValue = requiresSaveToFile;
119        requiresSaveToFile = newValue;
120        if (oldValue != newValue) {
121            propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue);
122        }
123    }
124
125    protected void setRequiresUploadToServer(boolean newValue) {
126        boolean oldValue = requiresUploadToServer;
127        requiresUploadToServer = newValue;
128        if (oldValue != newValue) {
129            propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue);
130        }
131    }
132
133    /** the global counter for created data layers */
134    private static int dataLayerCounter = 0;
135
136    /**
137     * Replies a new unique name for a data layer
138     *
139     * @return a new unique name for a data layer
140     */
141    public static String createNewName() {
142        dataLayerCounter++;
143        return tr("Data Layer {0}", dataLayerCounter);
144    }
145
146    public static final class DataCountVisitor extends AbstractVisitor {
147        public int nodes;
148        public int ways;
149        public int relations;
150        public int deletedNodes;
151        public int deletedWays;
152        public int deletedRelations;
153
154        @Override
155        public void visit(final Node n) {
156            nodes++;
157            if (n.isDeleted()) {
158                deletedNodes++;
159            }
160        }
161
162        @Override
163        public void visit(final Way w) {
164            ways++;
165            if (w.isDeleted()) {
166                deletedWays++;
167            }
168        }
169
170        @Override
171        public void visit(final Relation r) {
172            relations++;
173            if (r.isDeleted()) {
174                deletedRelations++;
175            }
176        }
177    }
178
179    public interface CommandQueueListener {
180        void commandChanged(int queueSize, int redoSize);
181    }
182
183    /**
184     * Listener called when a state of this layer has changed.
185     */
186    public interface LayerStateChangeListener {
187        /**
188         * Notifies that the "upload discouraged" (upload=no) state has changed.
189         * @param layer The layer that has been modified
190         * @param newValue The new value of the state
191         */
192        void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue);
193    }
194
195    private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
196
197    /**
198     * Adds a layer state change listener
199     *
200     * @param listener the listener. Ignored if null or already registered.
201     * @since 5519
202     */
203    public void addLayerStateChangeListener(LayerStateChangeListener listener) {
204        if (listener != null) {
205            layerStateChangeListeners.addIfAbsent(listener);
206        }
207    }
208
209    /**
210     * Removes a layer property change listener
211     *
212     * @param listener the listener. Ignored if null or already registered.
213     * @since 5519
214     */
215    public void removeLayerPropertyChangeListener(LayerStateChangeListener listener) {
216        layerStateChangeListeners.remove(listener);
217    }
218
219    /**
220     * The data behind this layer.
221     */
222    public final DataSet data;
223
224    /**
225     * the collection of conflicts detected in this layer
226     */
227    private ConflictCollection conflicts;
228
229    /**
230     * a paint texture for non-downloaded area
231     */
232    private static TexturePaint hatched;
233
234    static {
235        createHatchTexture();
236    }
237
238    /**
239     * Replies background color for downloaded areas.
240     * @return background color for downloaded areas. Black by default
241     */
242    public static Color getBackgroundColor() {
243        return Main.pref.getColor(marktr("background"), Color.BLACK);
244    }
245
246    /**
247     * Replies background color for non-downloaded areas.
248     * @return background color for non-downloaded areas. Yellow by default
249     */
250    public static Color getOutsideColor() {
251        return Main.pref.getColor(marktr("outside downloaded area"), Color.YELLOW);
252    }
253
254    /**
255     * Initialize the hatch pattern used to paint the non-downloaded area
256     */
257    public static void createHatchTexture() {
258        BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
259        Graphics2D big = bi.createGraphics();
260        big.setColor(getBackgroundColor());
261        Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
262        big.setComposite(comp);
263        big.fillRect(0,0,15,15);
264        big.setColor(getOutsideColor());
265        big.drawLine(0,15,15,0);
266        Rectangle r = new Rectangle(0, 0, 15,15);
267        hatched = new TexturePaint(bi, r);
268    }
269
270    /**
271     * Construct a new {@code OsmDataLayer}.
272     * @param data OSM data
273     * @param name Layer name
274     * @param associatedFile Associated .osm file (can be null)
275     */
276    public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
277        super(name);
278        this.data = data;
279        this.setAssociatedFile(associatedFile);
280        conflicts = new ConflictCollection();
281        data.addDataSetListener(new DataSetListenerAdapter(this));
282        data.addDataSetListener(MultipolygonCache.getInstance());
283        DataSet.addSelectionListener(this);
284    }
285
286    protected Icon getBaseIcon() {
287        return ImageProvider.get("layer", "osmdata_small");
288    }
289
290    /**
291     * TODO: @return Return a dynamic drawn icon of the map data. The icon is
292     *         updated by a background thread to not disturb the running programm.
293     */
294    @Override public Icon getIcon() {
295        Icon baseIcon = getBaseIcon();
296        if (isUploadDiscouraged()) {
297            return ImageProvider.overlay(baseIcon,
298                    new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(8, 8, Image.SCALE_SMOOTH)),
299                    ImageProvider.OverlayPosition.SOUTHEAST);
300        } else {
301            return baseIcon;
302        }
303    }
304
305    /**
306     * Draw all primitives in this layer but do not draw modified ones (they
307     * are drawn by the edit layer).
308     * Draw nodes last to overlap the ways they belong to.
309     */
310    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
311        isChanged = false;
312        highlightUpdateCount = data.getHighlightUpdateCount();
313
314        boolean active = mv.getActiveLayer() == this;
315        boolean inactive = !active && Main.pref.getBoolean("draw.data.inactive_color", true);
316        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
317
318        // draw the hatched area for non-downloaded region. only draw if we're the active
319        // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
320        if (active && Main.pref.getBoolean("draw.data.downloaded_area", true) && !data.dataSources.isEmpty()) {
321            // initialize area with current viewport
322            Rectangle b = mv.getBounds();
323            // on some platforms viewport bounds seem to be offset from the left,
324            // over-grow it just to be sure
325            b.grow(100, 100);
326            Area a = new Area(b);
327
328            // now successively subtract downloaded areas
329            for (Bounds bounds : data.getDataSourceBounds()) {
330                if (bounds.isCollapsed()) {
331                    continue;
332                }
333                Point p1 = mv.getPoint(bounds.getMin());
334                Point p2 = mv.getPoint(bounds.getMax());
335                Rectangle r = new Rectangle(Math.min(p1.x, p2.x),Math.min(p1.y, p2.y),Math.abs(p2.x-p1.x),Math.abs(p2.y-p1.y));
336                a.subtract(new Area(r));
337            }
338
339            // paint remainder
340            g.setPaint(hatched);
341            g.fill(a);
342        }
343
344        Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
345        painter.render(data, virtual, box);
346        Main.map.conflictDialog.paintConflicts(g, mv);
347    }
348
349    @Override public String getToolTipText() {
350        int nodes = new FilteredCollection<>(data.getNodes(), OsmPrimitive.nonDeletedPredicate).size();
351        int ways = new FilteredCollection<>(data.getWays(), OsmPrimitive.nonDeletedPredicate).size();
352
353        String tool = trn("{0} node", "{0} nodes", nodes, nodes)+", ";
354        tool += trn("{0} way", "{0} ways", ways, ways);
355
356        if (data.getVersion() != null) {
357            tool += ", " + tr("version {0}", data.getVersion());
358        }
359        File f = getAssociatedFile();
360        if (f != null) {
361            tool = "<html>"+tool+"<br>"+f.getPath()+"</html>";
362        }
363        return tool;
364    }
365
366    @Override public void mergeFrom(final Layer from) {
367        final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
368        monitor.setCancelable(false);
369        if (from instanceof OsmDataLayer && ((OsmDataLayer)from).isUploadDiscouraged()) {
370            setUploadDiscouraged(true);
371        }
372        mergeFrom(((OsmDataLayer)from).data, monitor);
373        monitor.close();
374    }
375
376    /**
377     * merges the primitives in dataset <code>from</code> into the dataset of
378     * this layer
379     *
380     * @param from  the source data set
381     */
382    public void mergeFrom(final DataSet from) {
383        mergeFrom(from, null);
384    }
385
386    /**
387     * merges the primitives in dataset <code>from</code> into the dataset of
388     * this layer
389     *
390     * @param from  the source data set
391     * @param progressMonitor the progress monitor, can be {@code null}
392     */
393    public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
394        final DataSetMerger visitor = new DataSetMerger(data,from);
395        try {
396            visitor.merge(progressMonitor);
397        } catch (DataIntegrityProblemException e) {
398            JOptionPane.showMessageDialog(
399                    Main.parent,
400                    e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
401                    tr("Error"),
402                    JOptionPane.ERROR_MESSAGE
403            );
404            return;
405
406        }
407
408        Area a = data.getDataSourceArea();
409
410        // copy the merged layer's data source info.
411        // only add source rectangles if they are not contained in the layer already.
412        for (DataSource src : from.dataSources) {
413            if (a == null || !a.contains(src.bounds.asRect())) {
414                data.dataSources.add(src);
415            }
416        }
417
418        // copy the merged layer's API version
419        if (data.getVersion() == null) {
420            data.setVersion(from.getVersion());
421        }
422
423        int numNewConflicts = 0;
424        for (Conflict<?> c : visitor.getConflicts()) {
425            if (!conflicts.hasConflict(c)) {
426                numNewConflicts++;
427                conflicts.add(c);
428            }
429        }
430        // repaint to make sure new data is displayed properly.
431        if (Main.isDisplayingMapView()) {
432            Main.map.mapView.repaint();
433        }
434        // warn about new conflicts
435        if (numNewConflicts > 0 && Main.map != null && Main.map.conflictDialog != null) {
436            Main.map.conflictDialog.warnNumNewConflicts(numNewConflicts);
437        }
438    }
439
440    @Override public boolean isMergable(final Layer other) {
441        // isUploadDiscouraged commented to allow merging between normal layers and discouraged layers with a warning (see #7684)
442        return other instanceof OsmDataLayer;// && (isUploadDiscouraged() == ((OsmDataLayer)other).isUploadDiscouraged());
443    }
444
445    @Override public void visitBoundingBox(final BoundingXYVisitor v) {
446        for (final Node n: data.getNodes()) {
447            if (n.isUsable()) {
448                v.visit(n);
449            }
450        }
451    }
452
453    /**
454     * Clean out the data behind the layer. This means clearing the redo/undo lists,
455     * really deleting all deleted objects and reset the modified flags. This should
456     * be done after an upload, even after a partial upload.
457     *
458     * @param processed A list of all objects that were actually uploaded.
459     *         May be <code>null</code>, which means nothing has been uploaded
460     */
461    public void cleanupAfterUpload(final Collection<IPrimitive> processed) {
462        // return immediately if an upload attempt failed
463        if (processed == null || processed.isEmpty())
464            return;
465
466        Main.main.undoRedo.clean(this);
467
468        // if uploaded, clean the modified flags as well
469        data.cleanupDeletedPrimitives();
470        for (OsmPrimitive p: data.allPrimitives()) {
471            if (processed.contains(p)) {
472                p.setModified(false);
473            }
474        }
475    }
476
477
478    @Override public Object getInfoComponent() {
479        final DataCountVisitor counter = new DataCountVisitor();
480        for (final OsmPrimitive osm : data.allPrimitives()) {
481            osm.accept(counter);
482        }
483        final JPanel p = new JPanel(new GridBagLayout());
484
485        String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
486        if (counter.deletedNodes > 0) {
487            nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+")";
488        }
489
490        String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
491        if (counter.deletedWays > 0) {
492            wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+")";
493        }
494
495        String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
496        if (counter.deletedRelations > 0) {
497            relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+")";
498        }
499
500        p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
501        p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
502        p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
503        p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
504        p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))), GBC.eop().insets(15,0,0,0));
505        if (isUploadDiscouraged()) {
506            p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15,0,0,0));
507        }
508
509        return p;
510    }
511
512    @Override public Action[] getMenuEntries() {
513        List<Action> actions = new ArrayList<>();
514        actions.addAll(Arrays.asList(new Action[]{
515                LayerListDialog.getInstance().createActivateLayerAction(this),
516                LayerListDialog.getInstance().createShowHideLayerAction(),
517                LayerListDialog.getInstance().createDeleteLayerAction(),
518                SeparatorLayerAction.INSTANCE,
519                LayerListDialog.getInstance().createMergeLayerAction(this),
520                new LayerSaveAction(this),
521                new LayerSaveAsAction(this),
522        }));
523        if (ExpertToggleAction.isExpert()) {
524            actions.addAll(Arrays.asList(new Action[]{
525                    new LayerGpxExportAction(this),
526                    new ConvertToGpxLayerAction()}));
527        }
528        actions.addAll(Arrays.asList(new Action[]{
529                SeparatorLayerAction.INSTANCE,
530                new RenameLayerAction(getAssociatedFile(), this)}));
531        if (ExpertToggleAction.isExpert() && Main.pref.getBoolean("data.layer.upload_discouragement.menu_item", false)) {
532            actions.add(new ToggleUploadDiscouragedLayerAction(this));
533        }
534        actions.addAll(Arrays.asList(new Action[]{
535                new ConsistencyTestAction(),
536                SeparatorLayerAction.INSTANCE,
537                new LayerListPopup.InfoAction(this)}));
538        return actions.toArray(new Action[actions.size()]);
539    }
540
541    /**
542     * Converts given OSM dataset to GPX data.
543     * @param data OSM dataset
544     * @param file output .gpx file
545     * @return GPX data
546     */
547    public static GpxData toGpxData(DataSet data, File file) {
548        GpxData gpxData = new GpxData();
549        gpxData.storageFile = file;
550        HashSet<Node> doneNodes = new HashSet<>();
551        waysToGpxData(data.getWays(), gpxData, doneNodes);
552        nodesToGpxData(data.getNodes(), gpxData, doneNodes);
553        return gpxData;
554    }
555
556    private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, HashSet<Node> doneNodes) {
557        for (Way w : ways) {
558            if (!w.isUsable()) {
559                continue;
560            }
561            Collection<Collection<WayPoint>> trk = new ArrayList<>();
562            Map<String, Object> trkAttr = new HashMap<>();
563
564            if (w.get("name") != null) {
565                trkAttr.put("name", w.get("name"));
566            }
567
568            List<WayPoint> trkseg = null;
569            for (Node n : w.getNodes()) {
570                if (!n.isUsable()) {
571                    trkseg = null;
572                    continue;
573                }
574                if (trkseg == null) {
575                    trkseg = new ArrayList<>();
576                    trk.add(trkseg);
577                }
578                if (!n.isTagged()) {
579                    doneNodes.add(n);
580                }
581                WayPoint wpt = new WayPoint(n.getCoor());
582                if (!n.isTimestampEmpty()) {
583                    wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
584                    wpt.setTime();
585                }
586                trkseg.add(wpt);
587            }
588
589            gpxData.tracks.add(new ImmutableGpxTrack(trk, trkAttr));
590        }
591    }
592
593    private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, HashSet<Node> doneNodes) {
594        List<Node> sortedNodes = new ArrayList<>(nodes);
595        sortedNodes.removeAll(doneNodes);
596        Collections.sort(sortedNodes);
597        for (Node n : sortedNodes) {
598            if (n.isIncomplete() || n.isDeleted()) {
599                continue;
600            }
601            WayPoint wpt = new WayPoint(n.getCoor());
602            String name = n.get("name");
603            if (name != null) {
604                wpt.attr.put("name", name);
605            }
606            if (!n.isTimestampEmpty()) {
607                wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
608                wpt.setTime();
609            }
610            String desc = n.get("description");
611            if (desc != null) {
612                wpt.attr.put("desc", desc);
613            }
614
615            gpxData.waypoints.add(wpt);
616        }
617    }
618
619    /**
620     * Converts OSM data behind this layer to GPX data.
621     * @return GPX data
622     */
623    public GpxData toGpxData() {
624        return toGpxData(data, getAssociatedFile());
625    }
626
627    /**
628     * Action that converts this OSM layer to a GPX layer.
629     */
630    public class ConvertToGpxLayerAction extends AbstractAction {
631        /**
632         * Constructs a new {@code ConvertToGpxLayerAction}.
633         */
634        public ConvertToGpxLayerAction() {
635            super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx"));
636            putValue("help", ht("/Action/ConvertToGpxLayer"));
637        }
638        @Override
639        public void actionPerformed(ActionEvent e) {
640            Main.main.addLayer(new GpxLayer(toGpxData(), tr("Converted from: {0}", getName())));
641            Main.main.removeLayer(OsmDataLayer.this);
642        }
643    }
644
645    /**
646     * Determines if this layer contains data at the given coordinate.
647     * @param coor the coordinate
648     * @return {@code true} if data sources bounding boxes contain {@code coor}
649     */
650    public boolean containsPoint(LatLon coor) {
651        // we'll assume that if this has no data sources
652        // that it also has no borders
653        if (this.data.dataSources.isEmpty())
654            return true;
655
656        boolean layer_bounds_point = false;
657        for (DataSource src : this.data.dataSources) {
658            if (src.bounds.contains(coor)) {
659                layer_bounds_point = true;
660                break;
661            }
662        }
663        return layer_bounds_point;
664    }
665
666    /**
667     * Replies the set of conflicts currently managed in this layer.
668     *
669     * @return the set of conflicts currently managed in this layer
670     */
671    public ConflictCollection getConflicts() {
672        return conflicts;
673    }
674
675    @Override
676    public boolean requiresUploadToServer() {
677        return requiresUploadToServer;
678    }
679
680    @Override
681    public boolean requiresSaveToFile() {
682        return getAssociatedFile() != null && requiresSaveToFile;
683    }
684
685    @Override
686    public void onPostLoadFromFile() {
687        setRequiresSaveToFile(false);
688        setRequiresUploadToServer(isModified());
689    }
690
691    /**
692     * Actions run after data has been downloaded to this layer.
693     */
694    public void onPostDownloadFromServer() {
695        setRequiresSaveToFile(true);
696        setRequiresUploadToServer(isModified());
697    }
698
699    @Override
700    public boolean isChanged() {
701        return isChanged || highlightUpdateCount != data.getHighlightUpdateCount();
702    }
703
704    @Override
705    public void onPostSaveToFile() {
706        setRequiresSaveToFile(false);
707        setRequiresUploadToServer(isModified());
708    }
709
710    @Override
711    public void onPostUploadToServer() {
712        setRequiresUploadToServer(isModified());
713        // keep requiresSaveToDisk unchanged
714    }
715
716    private class ConsistencyTestAction extends AbstractAction {
717
718        public ConsistencyTestAction() {
719            super(tr("Dataset consistency test"));
720        }
721
722        @Override
723        public void actionPerformed(ActionEvent e) {
724            String result = DatasetConsistencyTest.runTests(data);
725            if (result.length() == 0) {
726                JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
727            } else {
728                JPanel p = new JPanel(new GridBagLayout());
729                p.add(new JLabel(tr("Following problems found:")), GBC.eol());
730                JosmTextArea info = new JosmTextArea(result, 20, 60);
731                info.setCaretPosition(0);
732                info.setEditable(false);
733                p.add(new JScrollPane(info), GBC.eop());
734
735                JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
736            }
737        }
738    }
739
740    @Override
741    public void destroy() {
742        DataSet.removeSelectionListener(this);
743    }
744
745    @Override
746    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
747        isChanged = true;
748        setRequiresSaveToFile(true);
749        setRequiresUploadToServer(true);
750    }
751
752    @Override
753    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
754        isChanged = true;
755    }
756
757    @Override
758    public void projectionChanged(Projection oldValue, Projection newValue) {
759         // No reprojection required. The dataset itself is registered as projection
760         // change listener and already got notified.
761    }
762
763    @Override
764    public final boolean isUploadDiscouraged() {
765        return data.isUploadDiscouraged();
766    }
767
768    /**
769     * Sets the "discouraged upload" flag.
770     * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged. This feature allows to use "private" data layers.
771     */
772    public final void setUploadDiscouraged(boolean uploadDiscouraged) {
773        if (uploadDiscouraged ^ isUploadDiscouraged()) {
774            data.setUploadDiscouraged(uploadDiscouraged);
775            for (LayerStateChangeListener l : layerStateChangeListeners) {
776                l.uploadDiscouragedChanged(this, uploadDiscouraged);
777            }
778        }
779    }
780
781    @Override
782    public final boolean isModified() {
783        return data.isModified();
784    }
785
786    @Override
787    public boolean isSavable() {
788        return true; // With OsmExporter
789    }
790
791    @Override
792    public boolean checkSaveConditions() {
793        if (isDataSetEmpty()) {
794            if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() {
795                @Override
796                public Integer call() {
797                    ExtendedDialog dialog = new ExtendedDialog(
798                            Main.parent,
799                            tr("Empty document"),
800                            new String[] {tr("Save anyway"), tr("Cancel")}
801                    );
802                    dialog.setContent(tr("The document contains no data."));
803                    dialog.setButtonIcons(new String[] {"save.png", "cancel.png"});
804                    return dialog.showDialog().getValue();
805                }
806            })) {
807                return false;
808            }
809        }
810
811        ConflictCollection conflicts = getConflicts();
812        if (conflicts != null && !conflicts.isEmpty()) {
813            if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() {
814                @Override
815                public Integer call() {
816                    ExtendedDialog dialog = new ExtendedDialog(
817                            Main.parent,
818                            /* I18N: Display title of the window showing conflicts */
819                            tr("Conflicts"),
820                            new String[] {tr("Reject Conflicts and Save"), tr("Cancel")}
821                    );
822                    dialog.setContent(tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"));
823                    dialog.setButtonIcons(new String[] {"save.png", "cancel.png"});
824                    return dialog.showDialog().getValue();
825                }
826            })) {
827                return false;
828            }
829        }
830        return true;
831    }
832
833    /**
834     * Check the data set if it would be empty on save. It is empty, if it contains
835     * no objects (after all objects that are created and deleted without being
836     * transferred to the server have been removed).
837     *
838     * @return <code>true</code>, if a save result in an empty data set.
839     */
840    private boolean isDataSetEmpty() {
841        if (data != null) {
842            for (OsmPrimitive osm : data.allNonDeletedPrimitives())
843                if (!osm.isDeleted() || !osm.isNewOrUndeleted())
844                    return false;
845        }
846        return true;
847    }
848
849    @Override
850    public File createAndOpenSaveFileChooser() {
851        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save OSM file"), "osm");
852    }
853
854    @Override
855    public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
856        UploadDialog dialog = UploadDialog.getUploadDialog();
857        return new UploadLayerTask(
858                dialog.getUploadStrategySpecification(),
859                this,
860                monitor,
861                dialog.getChangeset());
862    }
863
864    @Override
865    public AbstractUploadDialog getUploadDialog() {
866        UploadDialog dialog = UploadDialog.getUploadDialog();
867        dialog.setUploadedPrimitives(new APIDataSet(data));
868        return dialog;
869    }
870}