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

Last change on this file since 15422 was 15422, checked in by Don-vip, 5 years ago

see #2760 - javadoc

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