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

Last change on this file since 17440 was 17440, checked in by GerdP, 3 years ago

see #17184: Memory leaks

  • remove more listeners in destroy()
  • reset fields which might reference OSM objects
  • call data.clearSelection when data layer is destroyed so that fewer actions keep references on OSM elements in that layer

My goal here: When a layer is closed JOSM should really allow to GC all OSM data and GPX data that was loaded before.

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