001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.AlphaComposite; 010import java.awt.Color; 011import java.awt.Composite; 012import java.awt.Graphics2D; 013import java.awt.GridBagLayout; 014import java.awt.Rectangle; 015import java.awt.TexturePaint; 016import java.awt.datatransfer.Transferable; 017import java.awt.datatransfer.UnsupportedFlavorException; 018import java.awt.event.ActionEvent; 019import java.awt.geom.Area; 020import java.awt.geom.Path2D; 021import java.awt.geom.Rectangle2D; 022import java.awt.image.BufferedImage; 023import java.io.File; 024import java.io.IOException; 025import java.time.DateTimeException; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.Objects; 036import java.util.Set; 037import java.util.concurrent.CopyOnWriteArrayList; 038import java.util.concurrent.atomic.AtomicBoolean; 039import java.util.concurrent.atomic.AtomicInteger; 040import java.util.regex.Pattern; 041import java.util.stream.Collectors; 042import java.util.stream.Stream; 043 044import javax.swing.AbstractAction; 045import javax.swing.Action; 046import javax.swing.Icon; 047import javax.swing.JLabel; 048import javax.swing.JOptionPane; 049import javax.swing.JPanel; 050import javax.swing.JScrollPane; 051 052import org.openstreetmap.josm.actions.AutoScaleAction; 053import org.openstreetmap.josm.actions.ExpertToggleAction; 054import org.openstreetmap.josm.actions.RenameLayerAction; 055import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction; 056import org.openstreetmap.josm.data.APIDataSet; 057import org.openstreetmap.josm.data.Bounds; 058import org.openstreetmap.josm.data.Data; 059import org.openstreetmap.josm.data.ProjectionBounds; 060import org.openstreetmap.josm.data.UndoRedoHandler; 061import org.openstreetmap.josm.data.conflict.Conflict; 062import org.openstreetmap.josm.data.conflict.ConflictCollection; 063import org.openstreetmap.josm.data.coor.EastNorth; 064import org.openstreetmap.josm.data.coor.LatLon; 065import org.openstreetmap.josm.data.gpx.GpxConstants; 066import org.openstreetmap.josm.data.gpx.GpxData; 067import org.openstreetmap.josm.data.gpx.GpxExtensionCollection; 068import org.openstreetmap.josm.data.gpx.GpxLink; 069import org.openstreetmap.josm.data.gpx.GpxTrack; 070import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 071import org.openstreetmap.josm.data.gpx.IGpxTrackSegment; 072import org.openstreetmap.josm.data.gpx.WayPoint; 073import org.openstreetmap.josm.data.osm.DataIntegrityProblemException; 074import org.openstreetmap.josm.data.osm.DataSelectionListener; 075import org.openstreetmap.josm.data.osm.DataSet; 076import org.openstreetmap.josm.data.osm.DataSetMerger; 077import org.openstreetmap.josm.data.osm.DatasetConsistencyTest; 078import org.openstreetmap.josm.data.osm.DownloadPolicy; 079import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 080import org.openstreetmap.josm.data.osm.IPrimitive; 081import org.openstreetmap.josm.data.osm.Node; 082import org.openstreetmap.josm.data.osm.OsmPrimitive; 083import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator; 084import org.openstreetmap.josm.data.osm.Relation; 085import org.openstreetmap.josm.data.osm.Tagged; 086import org.openstreetmap.josm.data.osm.UploadPolicy; 087import org.openstreetmap.josm.data.osm.Way; 088import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 089import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; 090import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; 091import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 092import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 093import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 094import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 095import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 096import org.openstreetmap.josm.data.preferences.BooleanProperty; 097import org.openstreetmap.josm.data.preferences.IntegerProperty; 098import org.openstreetmap.josm.data.preferences.NamedColorProperty; 099import 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(); 762 fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX); 763 return gpxData; 764 } 765 766 protected static void fillGpxData(GpxData gpxData, DataSet data, File file, String gpxPrefix) { 767 if (data.getGPXNamespaces() != null) { 768 gpxData.getNamespaces().addAll(data.getGPXNamespaces()); 769 } 770 gpxData.storageFile = file; 771 Set<Node> doneNodes = new HashSet<>(); 772 waysToGpxData(data.getWays(), gpxData, doneNodes, gpxPrefix); 773 nodesToGpxData(data.getNodes(), gpxData, doneNodes, gpxPrefix); 774 } 775 776 private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) { 777 /* When the dataset has been obtained from a gpx layer and now is being converted back, 778 * the ways have negative ids. The first created way corresponds to the first gpx segment, 779 * and has the highest id (i.e., closest to zero). 780 * Thus, sorting by OsmPrimitive#getUniqueId gives the original order. 781 * (Only works if the data layer has not been saved to and been loaded from an osm file before.) 782 */ 783 ways.stream() 784 .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed()) 785 .forEachOrdered(w -> { 786 if (!w.isUsable()) { 787 return; 788 } 789 List<IGpxTrackSegment> trk = new ArrayList<>(); 790 Map<String, Object> trkAttr = new HashMap<>(); 791 792 GpxExtensionCollection trkExts = new GpxExtensionCollection(); 793 GpxExtensionCollection segExts = new GpxExtensionCollection(); 794 for (Entry<String, String> e : w.getKeys().entrySet()) { 795 String k = e.getKey().startsWith(gpxPrefix) ? e.getKey().substring(gpxPrefix.length()) : e.getKey(); 796 String v = e.getValue(); 797 if (GpxConstants.RTE_TRK_KEYS.contains(k)) { 798 trkAttr.put(k, v); 799 } else { 800 k = GpxConstants.EXTENSION_ABBREVIATIONS.entrySet() 801 .stream() 802 .filter(s -> s.getValue().equals(e.getKey())) 803 .map(s -> s.getKey().substring(gpxPrefix.length())) 804 .findAny() 805 .orElse(k); 806 if (k.startsWith("extension")) { 807 String[] chain = k.split(":", -1); 808 if (chain.length >= 3 && "segment".equals(chain[2])) { 809 segExts.addFlat(chain, v); 810 } else { 811 trkExts.addFlat(chain, v); 812 } 813 } 814 815 } 816 } 817 List<WayPoint> trkseg = new ArrayList<>(); 818 for (Node n : w.getNodes()) { 819 if (!n.isUsable()) { 820 if (!trkseg.isEmpty()) { 821 trk.add(new GpxTrackSegment(trkseg)); 822 trkseg.clear(); 823 } 824 continue; 825 } 826 if (!n.isTagged() || containsOnlyGpxTags(n, gpxPrefix)) { 827 doneNodes.add(n); 828 } 829 trkseg.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix)); 830 } 831 trk.add(new GpxTrackSegment(trkseg)); 832 trk.forEach(gpxseg -> gpxseg.getExtensions().addAll(segExts)); 833 GpxTrack gpxtrk = new GpxTrack(trk, trkAttr); 834 gpxtrk.getExtensions().addAll(trkExts); 835 gpxData.addTrack(gpxtrk); 836 }); 837 } 838 839 private static boolean containsOnlyGpxTags(Tagged t, String gpxPrefix) { 840 return t.keys() 841 .allMatch(key -> GpxConstants.WPT_KEYS.contains(key) || key.startsWith(gpxPrefix)); 842 } 843 844 /** 845 * Reads the Gpx key from the given {@link OsmPrimitive}, with or without "gpx:" prefix 846 * @param prim OSM primitive 847 * @param gpxPrefix the GPX prefix 848 * @param key GPX key without prefix 849 * @return the value or <code>null</code> if not present 850 */ 851 private static String gpxVal(OsmPrimitive prim, String gpxPrefix, String key) { 852 String val = prim.get(gpxPrefix + key); 853 return val != null ? val : prim.get(key); 854 } 855 856 /** 857 * Converts a node to a waypoint with default {@link GpxConstants#GPX_PREFIX} for tags. 858 * @param n the {@code Node} to convert 859 * @param time a timestamp value in milliseconds from the epoch. 860 * @return {@code WayPoint} object 861 * @since 13210 862 */ 863 public static WayPoint nodeToWayPoint(Node n, long time) { 864 return nodeToWayPoint(n, time, GpxConstants.GPX_PREFIX); 865 } 866 867 /** 868 * Converts a node to a waypoint with a configurable GPX prefix for tags. 869 * @param n the {@code Node} to convert 870 * @param time a timestamp value in milliseconds from the epoch. 871 * @param gpxPrefix the GPX prefix for tags 872 * @return {@code WayPoint} object 873 * @since 18078 874 */ 875 public static WayPoint nodeToWayPoint(Node n, long time, String gpxPrefix) { 876 WayPoint wpt = new WayPoint(n.getCoor()); 877 878 // Position info 879 880 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_ELE, null); 881 882 try { 883 String v; 884 if (time > Long.MIN_VALUE) { 885 wpt.setTimeInMillis(time); 886 } else if ((v = gpxVal(n, gpxPrefix, GpxConstants.PT_TIME)) != null) { 887 wpt.setInstant(DateUtils.parseInstant(v)); 888 } else if (!n.isTimestampEmpty()) { 889 wpt.setInstant(n.getInstant()); 890 } 891 } catch (UncheckedParseException | DateTimeException e) { 892 Logging.error(e); 893 } 894 895 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_MAGVAR, null); 896 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_GEOIDHEIGHT, null); 897 898 // Description info 899 900 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_NAME, null, null); 901 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_DESC, "description", null); 902 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_CMT, "comment", null); 903 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_SRC, "source", "source:position"); 904 905 Collection<GpxLink> links = Stream.of("link", "url", "website", "contact:website") 906 .map(key -> gpxVal(n, gpxPrefix, key)) 907 .filter(Objects::nonNull) 908 .map(GpxLink::new) 909 .collect(Collectors.toList()); 910 wpt.put(GpxConstants.META_LINKS, links); 911 912 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SYM, "wpt_symbol", null); 913 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_TYPE, null, null); 914 915 // Accuracy info 916 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_FIX, "gps:fix", null); 917 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SAT, "gps:sat"); 918 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_HDOP, "gps:hdop"); 919 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_VDOP, "gps:vdop"); 920 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_PDOP, "gps:pdop"); 921 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata"); 922 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_DGPSID, "gps:dgpsid"); 923 924 return wpt; 925 } 926 927 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) { 928 List<Node> sortedNodes = new ArrayList<>(nodes); 929 sortedNodes.removeAll(doneNodes); 930 Collections.sort(sortedNodes); 931 for (Node n : sortedNodes) { 932 if (n.isIncomplete() || n.isDeleted()) { 933 continue; 934 } 935 gpxData.waypoints.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix)); 936 } 937 } 938 939 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) { 940 String value = gpxVal(p, gpxPrefix, gpxKey); 941 if (value == null && osmKey != null) { 942 value = gpxVal(p, gpxPrefix, osmKey); 943 } 944 if (value != null) { 945 try { 946 int i = Integer.parseInt(value); 947 // Sanity checks 948 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) && 949 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) { 950 wpt.put(gpxKey, value); 951 } 952 } catch (NumberFormatException e) { 953 Logging.trace(e); 954 } 955 } 956 } 957 958 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) { 959 String value = gpxVal(p, gpxPrefix, gpxKey); 960 if (value == null && osmKey != null) { 961 value = gpxVal(p, gpxPrefix, osmKey); 962 } 963 if (value != null) { 964 try { 965 double d = Double.parseDouble(value); 966 // Sanity checks 967 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) { 968 wpt.put(gpxKey, value); 969 } 970 } catch (NumberFormatException e) { 971 Logging.trace(e); 972 } 973 } 974 } 975 976 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey, String osmKey2) { 977 String value = gpxVal(p, gpxPrefix, gpxKey); 978 if (value == null && osmKey != null) { 979 value = gpxVal(p, gpxPrefix, osmKey); 980 } 981 if (value == null && osmKey2 != null) { 982 value = gpxVal(p, gpxPrefix, osmKey2); 983 } 984 // Sanity checks 985 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) { 986 wpt.put(gpxKey, value); 987 } 988 } 989 990 /** 991 * Converts OSM data behind this layer to GPX data. 992 * @return GPX data 993 */ 994 public GpxData toGpxData() { 995 return toGpxData(data, getAssociatedFile()); 996 } 997 998 /** 999 * Action that converts this OSM layer to a GPX layer. 1000 */ 1001 public class ConvertToGpxLayerAction extends AbstractAction { 1002 /** 1003 * Constructs a new {@code ConvertToGpxLayerAction}. 1004 */ 1005 public ConvertToGpxLayerAction() { 1006 super(tr("Convert to GPX layer")); 1007 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true); 1008 putValue("help", ht("/Action/ConvertToGpxLayer")); 1009 } 1010 1011 @Override 1012 public void actionPerformed(ActionEvent e) { 1013 final GpxData gpxData = toGpxData(); 1014 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName())); 1015 if (getAssociatedFile() != null) { 1016 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx"; 1017 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename)); 1018 } 1019 MainApplication.getLayerManager().addLayer(gpxLayer, false); 1020 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) { 1021 MainApplication.getLayerManager().addLayer( 1022 new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false); 1023 } 1024 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this); 1025 } 1026 } 1027 1028 /** 1029 * Determines if this layer contains data at the given coordinate. 1030 * @param coor the coordinate 1031 * @return {@code true} if data sources bounding boxes contain {@code coor} 1032 */ 1033 public boolean containsPoint(LatLon coor) { 1034 // we'll assume that if this has no data sources 1035 // that it also has no borders 1036 if (this.data.getDataSources().isEmpty()) 1037 return true; 1038 1039 return this.data.getDataSources().stream() 1040 .anyMatch(src -> src.bounds.contains(coor)); 1041 } 1042 1043 /** 1044 * Replies the set of conflicts currently managed in this layer. 1045 * 1046 * @return the set of conflicts currently managed in this layer 1047 */ 1048 public ConflictCollection getConflicts() { 1049 return data.getConflicts(); 1050 } 1051 1052 @Override 1053 public boolean isDownloadable() { 1054 return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked(); 1055 } 1056 1057 @Override 1058 public boolean isUploadable() { 1059 return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked(); 1060 } 1061 1062 @Override 1063 public boolean requiresUploadToServer() { 1064 return isUploadable() && requiresUploadToServer; 1065 } 1066 1067 @Override 1068 public boolean requiresSaveToFile() { 1069 return getAssociatedFile() != null && requiresSaveToFile; 1070 } 1071 1072 @Override 1073 public String getLabel() { 1074 String label = super.getLabel(); 1075 if (this.isDirty()) { 1076 label += " " + IS_DIRTY_SYMBOL; 1077 } 1078 if (this.getDataSet().isEmpty()) { 1079 label += " " + IS_EMPTY_SYMBOL; 1080 } 1081 return label; 1082 } 1083 1084 @Override 1085 public void onPostLoadFromFile() { 1086 setRequiresSaveToFile(false); 1087 setRequiresUploadToServer(isModified()); 1088 invalidate(); 1089 } 1090 1091 /** 1092 * Actions run after data has been downloaded to this layer. 1093 */ 1094 public void onPostDownloadFromServer() { 1095 setRequiresSaveToFile(true); 1096 setRequiresUploadToServer(isModified()); 1097 invalidate(); 1098 } 1099 1100 @Override 1101 public void onPostSaveToFile() { 1102 setRequiresSaveToFile(false); 1103 setRequiresUploadToServer(isModified()); 1104 } 1105 1106 @Override 1107 public void onPostUploadToServer() { 1108 setRequiresUploadToServer(isModified()); 1109 // keep requiresSaveToDisk unchanged 1110 } 1111 1112 private class ConsistencyTestAction extends AbstractAction { 1113 1114 ConsistencyTestAction() { 1115 super(tr("Dataset consistency test")); 1116 } 1117 1118 @Override 1119 public void actionPerformed(ActionEvent e) { 1120 String result = DatasetConsistencyTest.runTests(data); 1121 if (result.isEmpty()) { 1122 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No problems found")); 1123 } else { 1124 JPanel p = new JPanel(new GridBagLayout()); 1125 p.add(new JLabel(tr("Following problems found:")), GBC.eol()); 1126 JosmTextArea info = new JosmTextArea(result, 20, 60); 1127 info.setCaretPosition(0); 1128 info.setEditable(false); 1129 p.add(new JScrollPane(info), GBC.eop()); 1130 1131 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), p, tr("Warning"), JOptionPane.WARNING_MESSAGE); 1132 } 1133 } 1134 } 1135 1136 @Override 1137 public synchronized void destroy() { 1138 super.destroy(); 1139 data.removeSelectionListener(this); 1140 data.removeHighlightUpdateListener(this); 1141 data.removeDataSetListener(dataSetListenerAdapter); 1142 data.removeDataSetListener(MultipolygonCache.getInstance()); 1143 data.clearSelection(); 1144 validationErrors.clear(); 1145 removeClipboardDataFor(this); 1146 recentRelations.clear(); 1147 } 1148 1149 protected static void removeClipboardDataFor(OsmDataLayer osm) { 1150 Transferable clipboardContents = ClipboardUtils.getClipboardContent(); 1151 if (clipboardContents != null && clipboardContents.isDataFlavorSupported(OsmLayerTransferData.OSM_FLAVOR)) { 1152 try { 1153 Object o = clipboardContents.getTransferData(OsmLayerTransferData.OSM_FLAVOR); 1154 if (o instanceof OsmLayerTransferData && osm.equals(((OsmLayerTransferData) o).getLayer())) { 1155 ClipboardUtils.clear(); 1156 } 1157 } catch (UnsupportedFlavorException | IOException e) { 1158 Logging.error(e); 1159 } 1160 } 1161 } 1162 1163 @Override 1164 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 1165 invalidate(); 1166 setRequiresSaveToFile(true); 1167 setRequiresUploadToServer(event.getDataset().requiresUploadToServer()); 1168 } 1169 1170 @Override 1171 public void selectionChanged(SelectionChangeEvent event) { 1172 invalidate(); 1173 } 1174 1175 @Override 1176 public void projectionChanged(Projection oldValue, Projection newValue) { 1177 // No reprojection required. The dataset itself is registered as projection 1178 // change listener and already got notified. 1179 } 1180 1181 @Override 1182 public final boolean isUploadDiscouraged() { 1183 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED; 1184 } 1185 1186 /** 1187 * Sets the "discouraged upload" flag. 1188 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged. 1189 * This feature allows to use "private" data layers. 1190 */ 1191 public final void setUploadDiscouraged(boolean uploadDiscouraged) { 1192 if (data.getUploadPolicy() != UploadPolicy.BLOCKED && 1193 (uploadDiscouraged ^ isUploadDiscouraged())) { 1194 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL); 1195 for (LayerStateChangeListener l : layerStateChangeListeners) { 1196 l.uploadDiscouragedChanged(this, uploadDiscouraged); 1197 } 1198 } 1199 } 1200 1201 @Override 1202 public final boolean isModified() { 1203 return data.isModified(); 1204 } 1205 1206 @Override 1207 public boolean isSavable() { 1208 return true; // With OsmExporter 1209 } 1210 1211 @Override 1212 public boolean checkSaveConditions() { 1213 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() -> 1214 new ExtendedDialog( 1215 MainApplication.getMainFrame(), 1216 tr("Empty layer"), 1217 tr("Save anyway"), tr("Cancel")) 1218 .setContent(tr("The layer contains no data.")) 1219 .setButtonIcons("save", "cancel") 1220 .showDialog().getValue() 1221 )) { 1222 return false; 1223 } 1224 1225 ConflictCollection conflictsCol = getConflicts(); 1226 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() -> 1227 new ExtendedDialog( 1228 MainApplication.getMainFrame(), 1229 /* I18N: Display title of the window showing conflicts */ 1230 tr("Conflicts"), 1231 tr("Reject Conflicts and Save"), tr("Cancel")) 1232 .setContent( 1233 tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?")) 1234 .setButtonIcons("save", "cancel") 1235 .showDialog().getValue() 1236 ); 1237 } 1238 1239 /** 1240 * Check the data set if it would be empty on save. It is empty, if it contains 1241 * no objects (after all objects that are created and deleted without being 1242 * transferred to the server have been removed). 1243 * 1244 * @return <code>true</code>, if a save result in an empty data set. 1245 */ 1246 private boolean isDataSetEmpty() { 1247 return data == null || data.allNonDeletedPrimitives().stream() 1248 .allMatch(osm -> osm.isDeleted() && osm.isNewOrUndeleted()); 1249 } 1250 1251 @Override 1252 public File createAndOpenSaveFileChooser() { 1253 String extension = PROPERTY_SAVE_EXTENSION.get(); 1254 File file = getAssociatedFile(); 1255 if (file == null && isRenamed()) { 1256 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName()); 1257 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) { 1258 filename.append('.').append(extension); 1259 } 1260 file = new File(filename.toString()); 1261 } 1262 return new FileChooserManager() 1263 .title(tr("Save OSM file")) 1264 .extension(extension) 1265 .file(file) 1266 .additionalTypes(t -> t != WMSLayerImporter.FILE_FILTER && t != NoteExporter.FILE_FILTER && t != ValidatorErrorExporter.FILE_FILTER) 1267 .getFileForSave(); 1268 } 1269 1270 @Override 1271 public AbstractIOTask createUploadTask(final ProgressMonitor monitor) { 1272 UploadDialog dialog = UploadDialog.getUploadDialog(); 1273 return new UploadLayerTask( 1274 dialog.getUploadStrategySpecification(), 1275 this, 1276 monitor, 1277 dialog.getChangeset()); 1278 } 1279 1280 @Override 1281 public AbstractUploadDialog getUploadDialog() { 1282 UploadDialog dialog = UploadDialog.getUploadDialog(); 1283 dialog.setUploadedPrimitives(new APIDataSet(data)); 1284 return dialog; 1285 } 1286 1287 @Override 1288 public ProjectionBounds getViewProjectionBounds() { 1289 BoundingXYVisitor v = new BoundingXYVisitor(); 1290 v.visit(data.getDataSourceBoundingBox()); 1291 if (!v.hasExtend()) { 1292 v.computeBoundingBox(data.getNodes()); 1293 } 1294 return v.getBounds(); 1295 } 1296 1297 @Override 1298 public void highlightUpdated(HighlightUpdateEvent e) { 1299 invalidate(); 1300 } 1301 1302 @Override 1303 public void setName(String name) { 1304 if (data != null) { 1305 data.setName(name); 1306 } 1307 super.setName(name); 1308 } 1309 1310 /** 1311 * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer. 1312 * @since 13434 1313 */ 1314 public void setUploadInProgress() { 1315 if (!isUploadInProgress.compareAndSet(false, true)) { 1316 Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName()); 1317 } 1318 } 1319 1320 /** 1321 * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer. 1322 * @since 13434 1323 */ 1324 public void unsetUploadInProgress() { 1325 if (!isUploadInProgress.compareAndSet(true, false)) { 1326 Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName()); 1327 } 1328 } 1329 1330 @Override 1331 public boolean isUploadInProgress() { 1332 return isUploadInProgress.get(); 1333 } 1334 1335 @Override 1336 public Data getData() { 1337 return getDataSet(); 1338 } 1339 1340 @Override 1341 public boolean autosave(File file) throws IOException { 1342 new OsmExporter().exportData(file, this, true /* no backup with appended ~ */); 1343 return true; 1344 } 1345 1346 /** 1347 * Duplicates this layer with a new name and a copy of this layer dataset. 1348 * @param newName name of new layer 1349 * @return A copy of this layer 1350 * @since 18233 1351 */ 1352 public OsmDataLayer duplicate(String newName) { 1353 return new OsmDataLayer(new DataSet(getDataSet()), newName, null); 1354 } 1355}