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