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

Last change on this file was 18753, checked in by taylor.smock, 11 months ago

Fix #23001: ClassCastException in GpxDrawHelper#calculateColors

This occurs when a working GPX file with HDOP information is converted to an OSM
Data Layer and back to a GPX Data Layer. This is fixed by (a) converting casts
for get operations to Number instead of Float/Integer and (b) actually
putting the number in the attribute map instead of a string.

  • Property svn:eol-style set to native
File size: 52.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.Set;
37import java.util.concurrent.CopyOnWriteArrayList;
38import java.util.concurrent.atomic.AtomicBoolean;
39import java.util.concurrent.atomic.AtomicInteger;
40import java.util.regex.Pattern;
41import java.util.stream.Collectors;
42import java.util.stream.Stream;
43
44import javax.swing.AbstractAction;
45import javax.swing.Action;
46import javax.swing.Icon;
47import javax.swing.JLabel;
48import javax.swing.JOptionPane;
49import javax.swing.JPanel;
50import javax.swing.JScrollPane;
51
52import org.openstreetmap.josm.actions.AutoScaleAction;
53import org.openstreetmap.josm.actions.ExpertToggleAction;
54import org.openstreetmap.josm.actions.RenameLayerAction;
55import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
56import org.openstreetmap.josm.data.APIDataSet;
57import org.openstreetmap.josm.data.Bounds;
58import org.openstreetmap.josm.data.Data;
59import org.openstreetmap.josm.data.ProjectionBounds;
60import org.openstreetmap.josm.data.UndoRedoHandler;
61import org.openstreetmap.josm.data.conflict.Conflict;
62import org.openstreetmap.josm.data.conflict.ConflictCollection;
63import org.openstreetmap.josm.data.coor.EastNorth;
64import org.openstreetmap.josm.data.coor.LatLon;
65import org.openstreetmap.josm.data.gpx.GpxConstants;
66import org.openstreetmap.josm.data.gpx.GpxData;
67import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
68import org.openstreetmap.josm.data.gpx.GpxLink;
69import org.openstreetmap.josm.data.gpx.GpxTrack;
70import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
71import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
72import org.openstreetmap.josm.data.gpx.WayPoint;
73import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
74import org.openstreetmap.josm.data.osm.DataSelectionListener;
75import org.openstreetmap.josm.data.osm.DataSet;
76import org.openstreetmap.josm.data.osm.DataSetMerger;
77import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
78import org.openstreetmap.josm.data.osm.DownloadPolicy;
79import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
80import org.openstreetmap.josm.data.osm.IPrimitive;
81import org.openstreetmap.josm.data.osm.Node;
82import org.openstreetmap.josm.data.osm.OsmPrimitive;
83import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
84import org.openstreetmap.josm.data.osm.Relation;
85import org.openstreetmap.josm.data.osm.Tagged;
86import org.openstreetmap.josm.data.osm.UploadPolicy;
87import org.openstreetmap.josm.data.osm.Way;
88import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
89import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
90import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
91import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
92import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
93import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
94import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
95import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
96import org.openstreetmap.josm.data.preferences.BooleanProperty;
97import org.openstreetmap.josm.data.preferences.IntegerProperty;
98import org.openstreetmap.josm.data.preferences.NamedColorProperty;
99import org.openstreetmap.josm.data.preferences.StringProperty;
100import org.openstreetmap.josm.data.projection.Projection;
101import org.openstreetmap.josm.data.validation.TestError;
102import org.openstreetmap.josm.gui.ExtendedDialog;
103import org.openstreetmap.josm.gui.MainApplication;
104import org.openstreetmap.josm.gui.MapFrame;
105import org.openstreetmap.josm.gui.MapView;
106import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
107import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
108import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
109import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
110import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
111import org.openstreetmap.josm.gui.io.AbstractIOTask;
112import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
113import org.openstreetmap.josm.gui.io.UploadDialog;
114import org.openstreetmap.josm.gui.io.UploadLayerTask;
115import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
116import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
117import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
118import org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter;
119import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
120import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
121import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
122import org.openstreetmap.josm.gui.progress.ProgressMonitor;
123import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
124import org.openstreetmap.josm.gui.util.GuiHelper;
125import org.openstreetmap.josm.gui.util.LruCache;
126import org.openstreetmap.josm.gui.widgets.FileChooserManager;
127import org.openstreetmap.josm.gui.widgets.JosmTextArea;
128import org.openstreetmap.josm.spi.preferences.Config;
129import org.openstreetmap.josm.tools.AlphanumComparator;
130import org.openstreetmap.josm.tools.CheckParameterUtil;
131import org.openstreetmap.josm.tools.GBC;
132import org.openstreetmap.josm.tools.ImageOverlay;
133import org.openstreetmap.josm.tools.ImageProvider;
134import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
135import org.openstreetmap.josm.tools.Logging;
136import org.openstreetmap.josm.tools.UncheckedParseException;
137import org.openstreetmap.josm.tools.Utils;
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 /** 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 (Utils.isEmpty(processed))
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 addConditionalInformation(p, IS_EMPTY_SYMBOL + " " + tr("Empty layer"), this.getDataSet().isEmpty());
713 addConditionalInformation(p, IS_DIRTY_SYMBOL + " " + tr("Unsaved changes"), this.isDirty());
714
715 return p;
716 }
717
718 private static void addConditionalInformation(JPanel p, String text, boolean condition) {
719 if (condition) {
720 p.add(new JLabel(text), GBC.eop().insets(15, 0, 0, 0));
721 }
722 }
723
724 @Override
725 public Action[] getMenuEntries() {
726 List<Action> actions = new ArrayList<>(Arrays.asList(
727 LayerListDialog.getInstance().createActivateLayerAction(this),
728 LayerListDialog.getInstance().createShowHideLayerAction(),
729 MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER),
730 LayerListDialog.getInstance().createDeleteLayerAction(),
731 SeparatorLayerAction.INSTANCE,
732 LayerListDialog.getInstance().createMergeLayerAction(this),
733 LayerListDialog.getInstance().createDuplicateLayerAction(this),
734 new LayerSaveAction(this),
735 new LayerSaveAsAction(this)));
736 if (ExpertToggleAction.isExpert()) {
737 actions.addAll(Arrays.asList(
738 new LayerGpxExportAction(this),
739 new ConvertToGpxLayerAction()));
740 }
741 actions.addAll(Arrays.asList(
742 SeparatorLayerAction.INSTANCE,
743 new RenameLayerAction(getAssociatedFile(), this)));
744 if (ExpertToggleAction.isExpert()) {
745 actions.add(new ToggleUploadDiscouragedLayerAction(this));
746 }
747 actions.addAll(Arrays.asList(
748 new ConsistencyTestAction(),
749 SeparatorLayerAction.INSTANCE,
750 new LayerListPopup.InfoAction(this)));
751 return actions.toArray(new Action[0]);
752 }
753
754 /**
755 * Converts given OSM dataset to GPX data.
756 * @param data OSM dataset
757 * @param file output .gpx file
758 * @return GPX data
759 */
760 public static GpxData toGpxData(DataSet data, File file) {
761 GpxData gpxData = new GpxData(true);
762 fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX);
763 gpxData.endUpdate();
764 return gpxData;
765 }
766
767 protected static void fillGpxData(GpxData gpxData, DataSet data, File file, String gpxPrefix) {
768 if (data.getGPXNamespaces() != null) {
769 gpxData.getNamespaces().addAll(data.getGPXNamespaces());
770 }
771 gpxData.storageFile = file;
772 Set<Node> doneNodes = new HashSet<>();
773 waysToGpxData(data.getWays(), gpxData, doneNodes, gpxPrefix);
774 nodesToGpxData(data.getNodes(), gpxData, doneNodes, gpxPrefix);
775 }
776
777 private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
778 /* When the dataset has been obtained from a gpx layer and now is being converted back,
779 * the ways have negative ids. The first created way corresponds to the first gpx segment,
780 * and has the highest id (i.e., closest to zero).
781 * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
782 * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
783 */
784 ways.stream()
785 .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
786 .forEachOrdered(w -> {
787 if (!w.isUsable()) {
788 return;
789 }
790 List<IGpxTrackSegment> trk = new ArrayList<>();
791 Map<String, Object> trkAttr = new HashMap<>();
792
793 GpxExtensionCollection trkExts = new GpxExtensionCollection();
794 GpxExtensionCollection segExts = new GpxExtensionCollection();
795 for (Entry<String, String> e : w.getKeys().entrySet()) {
796 String k = e.getKey().startsWith(gpxPrefix) ? e.getKey().substring(gpxPrefix.length()) : e.getKey();
797 String v = e.getValue();
798 if (GpxConstants.RTE_TRK_KEYS.contains(k)) {
799 trkAttr.put(k, v);
800 } else {
801 k = GpxConstants.EXTENSION_ABBREVIATIONS.entrySet()
802 .stream()
803 .filter(s -> s.getValue().equals(e.getKey()))
804 .map(s -> s.getKey().substring(gpxPrefix.length()))
805 .findAny()
806 .orElse(k);
807 if (k.startsWith("extension")) {
808 String[] chain = k.split(":", -1);
809 if (chain.length >= 3 && "segment".equals(chain[2])) {
810 segExts.addFlat(chain, v);
811 } else {
812 trkExts.addFlat(chain, v);
813 }
814 }
815
816 }
817 }
818 List<WayPoint> trkseg = new ArrayList<>();
819 for (Node n : w.getNodes()) {
820 if (!n.isUsable()) {
821 if (!trkseg.isEmpty()) {
822 trk.add(new GpxTrackSegment(trkseg));
823 trkseg.clear();
824 }
825 continue;
826 }
827 if (!n.isTagged() || containsOnlyGpxTags(n, gpxPrefix)) {
828 doneNodes.add(n);
829 }
830 trkseg.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
831 }
832 trk.add(new GpxTrackSegment(trkseg));
833 trk.forEach(gpxseg -> gpxseg.getExtensions().addAll(segExts));
834 GpxTrack gpxtrk = new GpxTrack(trk, trkAttr);
835 gpxtrk.getExtensions().addAll(trkExts);
836 gpxData.addTrack(gpxtrk);
837 });
838 }
839
840 private static boolean containsOnlyGpxTags(Tagged t, String gpxPrefix) {
841 return t.keys()
842 .allMatch(key -> GpxConstants.WPT_KEYS.contains(key) || key.startsWith(gpxPrefix));
843 }
844
845 /**
846 * Reads the Gpx key from the given {@link OsmPrimitive}, with or without &quot;gpx:&quot; prefix
847 * @param prim OSM primitive
848 * @param gpxPrefix the GPX prefix
849 * @param key GPX key without prefix
850 * @return the value or <code>null</code> if not present
851 */
852 private static String gpxVal(OsmPrimitive prim, String gpxPrefix, String key) {
853 String val = prim.get(gpxPrefix + key);
854 return val != null ? val : prim.get(key);
855 }
856
857 /**
858 * Converts a node to a waypoint with default {@link GpxConstants#GPX_PREFIX} for tags.
859 * @param n the {@code Node} to convert
860 * @param time a timestamp value in milliseconds from the epoch.
861 * @return {@code WayPoint} object
862 * @since 13210
863 */
864 public static WayPoint nodeToWayPoint(Node n, long time) {
865 return nodeToWayPoint(n, time, GpxConstants.GPX_PREFIX);
866 }
867
868 /**
869 * Converts a node to a waypoint with a configurable GPX prefix for tags.
870 * @param n the {@code Node} to convert
871 * @param time a timestamp value in milliseconds from the epoch.
872 * @param gpxPrefix the GPX prefix for tags
873 * @return {@code WayPoint} object
874 * @since 18078
875 */
876 public static WayPoint nodeToWayPoint(Node n, long time, String gpxPrefix) {
877 WayPoint wpt = new WayPoint(n.getCoor());
878
879 // Position info
880
881 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_ELE, null);
882
883 try {
884 String v;
885 if (time > Long.MIN_VALUE) {
886 wpt.setTimeInMillis(time);
887 } else if ((v = gpxVal(n, gpxPrefix, GpxConstants.PT_TIME)) != null) {
888 wpt.setInstant(DateUtils.parseInstant(v));
889 } else if (!n.isTimestampEmpty()) {
890 wpt.setInstant(n.getInstant());
891 }
892 } catch (UncheckedParseException | DateTimeException e) {
893 Logging.error(e);
894 }
895
896 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_MAGVAR, null);
897 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_GEOIDHEIGHT, null);
898
899 // Description info
900
901 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_NAME, null, null);
902 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_DESC, "description", null);
903 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_CMT, "comment", null);
904 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_SRC, "source", "source:position");
905
906 Collection<GpxLink> links = Stream.of("link", "url", "website", "contact:website")
907 .map(key -> gpxVal(n, gpxPrefix, key))
908 .filter(Objects::nonNull)
909 .map(GpxLink::new)
910 .collect(Collectors.toList());
911 wpt.put(GpxConstants.META_LINKS, links);
912
913 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SYM, "wpt_symbol", null);
914 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_TYPE, null, null);
915
916 // Accuracy info
917 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_FIX, "gps:fix", null);
918 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SAT, "gps:sat");
919 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_HDOP, "gps:hdop");
920 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_VDOP, "gps:vdop");
921 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_PDOP, "gps:pdop");
922 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
923 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_DGPSID, "gps:dgpsid");
924
925 return wpt;
926 }
927
928 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
929 List<Node> sortedNodes = new ArrayList<>(nodes);
930 sortedNodes.removeAll(doneNodes);
931 Collections.sort(sortedNodes);
932 for (Node n : sortedNodes) {
933 if (n.isIncomplete() || n.isDeleted()) {
934 continue;
935 }
936 gpxData.waypoints.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
937 }
938 }
939
940 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
941 String value = gpxVal(p, gpxPrefix, gpxKey);
942 if (value == null && osmKey != null) {
943 value = gpxVal(p, gpxPrefix, osmKey);
944 }
945 if (value != null) {
946 try {
947 final int i = Integer.parseInt(value);
948 // Sanity checks
949 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
950 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
951 wpt.put(gpxKey, i);
952 }
953 } catch (NumberFormatException e) {
954 Logging.trace(e);
955 }
956 }
957 }
958
959 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
960 String value = gpxVal(p, gpxPrefix, gpxKey);
961 if (value == null && osmKey != null) {
962 value = gpxVal(p, gpxPrefix, osmKey);
963 }
964 if (value != null) {
965 try {
966 final double d = Double.parseDouble(value);
967 // Sanity checks
968 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
969 wpt.put(gpxKey, d);
970 }
971 } catch (NumberFormatException e) {
972 Logging.trace(e);
973 }
974 }
975 }
976
977 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey, String osmKey2) {
978 String value = gpxVal(p, gpxPrefix, gpxKey);
979 if (value == null && osmKey != null) {
980 value = gpxVal(p, gpxPrefix, osmKey);
981 }
982 if (value == null && osmKey2 != null) {
983 value = gpxVal(p, gpxPrefix, osmKey2);
984 }
985 // Sanity checks
986 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
987 wpt.put(gpxKey, value);
988 }
989 }
990
991 /**
992 * Converts OSM data behind this layer to GPX data.
993 * @return GPX data
994 */
995 public GpxData toGpxData() {
996 return toGpxData(data, getAssociatedFile());
997 }
998
999 /**
1000 * Action that converts this OSM layer to a GPX layer.
1001 */
1002 public class ConvertToGpxLayerAction extends AbstractAction {
1003 /**
1004 * Constructs a new {@code ConvertToGpxLayerAction}.
1005 */
1006 public ConvertToGpxLayerAction() {
1007 super(tr("Convert to GPX layer"));
1008 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
1009 putValue("help", ht("/Action/ConvertToGpxLayer"));
1010 }
1011
1012 @Override
1013 public void actionPerformed(ActionEvent e) {
1014 String name = getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
1015 final GpxData gpxData = toGpxData();
1016 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", name), true);
1017 if (getAssociatedFile() != null) {
1018 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
1019 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
1020 gpxLayer.getGpxData().setModified(true);
1021 }
1022 MainApplication.getLayerManager().addLayer(gpxLayer, false);
1023 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
1024 MainApplication.getLayerManager().addLayer(
1025 new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false);
1026 }
1027 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
1028 }
1029 }
1030
1031 /**
1032 * Determines if this layer contains data at the given coordinate.
1033 * @param coor the coordinate
1034 * @return {@code true} if data sources bounding boxes contain {@code coor}
1035 */
1036 public boolean containsPoint(LatLon coor) {
1037 // we'll assume that if this has no data sources
1038 // that it also has no borders
1039 if (this.data.getDataSources().isEmpty())
1040 return true;
1041
1042 return this.data.getDataSources().stream()
1043 .anyMatch(src -> src.bounds.contains(coor));
1044 }
1045
1046 /**
1047 * Replies the set of conflicts currently managed in this layer.
1048 *
1049 * @return the set of conflicts currently managed in this layer
1050 */
1051 public ConflictCollection getConflicts() {
1052 return data.getConflicts();
1053 }
1054
1055 @Override
1056 public boolean isDownloadable() {
1057 return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked();
1058 }
1059
1060 @Override
1061 public boolean isUploadable() {
1062 return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked();
1063 }
1064
1065 @Override
1066 public boolean requiresUploadToServer() {
1067 return isUploadable() && requiresUploadToServer;
1068 }
1069
1070 @Override
1071 public boolean requiresSaveToFile() {
1072 return getAssociatedFile() != null && requiresSaveToFile;
1073 }
1074
1075 @Override
1076 public String getLabel() {
1077 String label = super.getLabel();
1078 if (this.isDirty()) {
1079 label += " " + IS_DIRTY_SYMBOL;
1080 }
1081 if (this.getDataSet().isEmpty()) {
1082 label += " " + IS_EMPTY_SYMBOL;
1083 }
1084 return label;
1085 }
1086
1087 @Override
1088 public void onPostLoadFromFile() {
1089 setRequiresSaveToFile(false);
1090 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1091 invalidate();
1092 }
1093
1094 /**
1095 * Actions run after data has been downloaded to this layer.
1096 */
1097 public void onPostDownloadFromServer() {
1098 setRequiresSaveToFile(true);
1099 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1100 invalidate();
1101 }
1102
1103 @Override
1104 public void onPostSaveToFile() {
1105 setRequiresSaveToFile(false);
1106 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1107 }
1108
1109 @Override
1110 public void onPostUploadToServer() {
1111 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1112 // keep requiresSaveToDisk unchanged
1113 }
1114
1115 private class ConsistencyTestAction extends AbstractAction {
1116
1117 ConsistencyTestAction() {
1118 super(tr("Dataset consistency test"));
1119 }
1120
1121 @Override
1122 public void actionPerformed(ActionEvent e) {
1123 String result = DatasetConsistencyTest.runTests(data);
1124 if (result.isEmpty()) {
1125 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No problems found"));
1126 } else {
1127 JPanel p = new JPanel(new GridBagLayout());
1128 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
1129 JosmTextArea info = new JosmTextArea(result, 20, 60);
1130 info.setCaretPosition(0);
1131 info.setEditable(false);
1132 p.add(new JScrollPane(info), GBC.eop());
1133
1134 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1135 }
1136 }
1137 }
1138
1139 @Override
1140 public synchronized void destroy() {
1141 super.destroy();
1142 data.removeSelectionListener(this);
1143 data.removeHighlightUpdateListener(this);
1144 data.removeDataSetListener(dataSetListenerAdapter);
1145 data.removeDataSetListener(MultipolygonCache.getInstance());
1146 data.clearSelection();
1147 validationErrors.clear();
1148 removeClipboardDataFor(this);
1149 recentRelations.clear();
1150 }
1151
1152 protected static void removeClipboardDataFor(OsmDataLayer osm) {
1153 Transferable clipboardContents = ClipboardUtils.getClipboardContent();
1154 if (clipboardContents != null && clipboardContents.isDataFlavorSupported(OsmLayerTransferData.OSM_FLAVOR)) {
1155 try {
1156 Object o = clipboardContents.getTransferData(OsmLayerTransferData.OSM_FLAVOR);
1157 if (o instanceof OsmLayerTransferData && osm.equals(((OsmLayerTransferData) o).getLayer())) {
1158 ClipboardUtils.clear();
1159 }
1160 } catch (UnsupportedFlavorException | IOException e) {
1161 Logging.error(e);
1162 }
1163 }
1164 }
1165
1166 @Override
1167 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1168 invalidate();
1169 setRequiresSaveToFile(true);
1170 setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1171 }
1172
1173 @Override
1174 public void selectionChanged(SelectionChangeEvent event) {
1175 invalidate();
1176 }
1177
1178 @Override
1179 public void projectionChanged(Projection oldValue, Projection newValue) {
1180 // No reprojection required. The dataset itself is registered as projection
1181 // change listener and already got notified.
1182 }
1183
1184 @Override
1185 public final boolean isUploadDiscouraged() {
1186 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1187 }
1188
1189 /**
1190 * Sets the "discouraged upload" flag.
1191 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1192 * This feature allows to use "private" data layers.
1193 */
1194 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1195 if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1196 (uploadDiscouraged ^ isUploadDiscouraged())) {
1197 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1198 for (LayerStateChangeListener l : layerStateChangeListeners) {
1199 l.uploadDiscouragedChanged(this, uploadDiscouraged);
1200 }
1201 }
1202 }
1203
1204 @Override
1205 public final boolean isModified() {
1206 return data.isModified();
1207 }
1208
1209 @Override
1210 public boolean isSavable() {
1211 return true; // With OsmExporter
1212 }
1213
1214 @Override
1215 public boolean checkSaveConditions() {
1216 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() ->
1217 new ExtendedDialog(
1218 MainApplication.getMainFrame(),
1219 tr("Empty layer"),
1220 tr("Save anyway"), tr("Cancel"))
1221 .setContent(tr("The layer contains no data."))
1222 .setButtonIcons("save", "cancel")
1223 .showDialog().getValue()
1224 )) {
1225 return false;
1226 }
1227
1228 ConflictCollection conflictsCol = getConflicts();
1229 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1230 new ExtendedDialog(
1231 MainApplication.getMainFrame(),
1232 /* I18N: Display title of the window showing conflicts */
1233 tr("Conflicts"),
1234 tr("Reject Conflicts and Save"), tr("Cancel"))
1235 .setContent(
1236 tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1237 .setButtonIcons("save", "cancel")
1238 .showDialog().getValue()
1239 );
1240 }
1241
1242 /**
1243 * Check the data set if it would be empty on save. It is empty, if it contains
1244 * no objects (after all objects that are created and deleted without being
1245 * transferred to the server have been removed).
1246 *
1247 * @return <code>true</code>, if a save result in an empty data set.
1248 */
1249 private boolean isDataSetEmpty() {
1250 return data == null || data.allNonDeletedPrimitives().stream()
1251 .allMatch(osm -> osm.isDeleted() && osm.isNewOrUndeleted());
1252 }
1253
1254 @Override
1255 public File createAndOpenSaveFileChooser() {
1256 String extension = PROPERTY_SAVE_EXTENSION.get();
1257 File file = getAssociatedFile();
1258 if (file == null && isRenamed()) {
1259 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1260 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1261 filename.append('.').append(extension);
1262 }
1263 file = new File(filename.toString());
1264 }
1265 return new FileChooserManager()
1266 .title(tr("Save OSM file"))
1267 .extension(extension)
1268 .file(file)
1269 .additionalTypes(t -> t != WMSLayerImporter.FILE_FILTER && t != NoteExporter.FILE_FILTER && t != ValidatorErrorExporter.FILE_FILTER)
1270 .getFileForSave();
1271 }
1272
1273 @Override
1274 public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1275 UploadDialog dialog = UploadDialog.getUploadDialog();
1276 return new UploadLayerTask(
1277 dialog.getUploadStrategySpecification(),
1278 this,
1279 monitor,
1280 dialog.getChangeset());
1281 }
1282
1283 @Override
1284 public String getChangesetSourceTag() {
1285 return this.data.getChangeSetTags().getOrDefault("source", null);
1286 }
1287
1288 @Override
1289 public AbstractUploadDialog getUploadDialog() {
1290 UploadDialog dialog = UploadDialog.getUploadDialog();
1291 dialog.setUploadedPrimitives(new APIDataSet(data));
1292 return dialog;
1293 }
1294
1295 @Override
1296 public ProjectionBounds getViewProjectionBounds() {
1297 BoundingXYVisitor v = new BoundingXYVisitor();
1298 v.visit(data.getDataSourceBoundingBox());
1299 if (!v.hasExtend()) {
1300 v.computeBoundingBox(data.getNodes());
1301 }
1302 return v.getBounds();
1303 }
1304
1305 @Override
1306 public void highlightUpdated(HighlightUpdateEvent e) {
1307 invalidate();
1308 }
1309
1310 @Override
1311 public void setName(String name) {
1312 if (data != null) {
1313 data.setName(name);
1314 }
1315 super.setName(name);
1316 }
1317
1318 /**
1319 * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer.
1320 * @since 13434
1321 */
1322 public void setUploadInProgress() {
1323 if (!isUploadInProgress.compareAndSet(false, true)) {
1324 Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName());
1325 }
1326 }
1327
1328 /**
1329 * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer.
1330 * @since 13434
1331 */
1332 public void unsetUploadInProgress() {
1333 if (!isUploadInProgress.compareAndSet(true, false)) {
1334 Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName());
1335 }
1336 }
1337
1338 @Override
1339 public boolean isUploadInProgress() {
1340 return isUploadInProgress.get();
1341 }
1342
1343 @Override
1344 public Data getData() {
1345 return getDataSet();
1346 }
1347
1348 @Override
1349 public boolean autosave(File file) throws IOException {
1350 new OsmExporter().exportData(file, this, true /* no backup with appended ~ */);
1351 return true;
1352 }
1353
1354 /**
1355 * Duplicates this layer with a new name and a copy of this layer dataset.
1356 * @param newName name of new layer
1357 * @return A copy of this layer
1358 * @since 18233
1359 */
1360 public OsmDataLayer duplicate(String newName) {
1361 return new OsmDataLayer(new DataSet(getDataSet()), newName, null);
1362 }
1363}
Note: See TracBrowser for help on using the repository browser.