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