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