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

Last change on this file since 17847 was 17847, checked in by simon04, 3 years ago

see #20829 - Avoid heap allocations in OsmDataLayer.toGpxData

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