001// License: GPL. See LICENSE file for details. 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.Image; 015import java.awt.Point; 016import java.awt.Rectangle; 017import java.awt.TexturePaint; 018import java.awt.event.ActionEvent; 019import java.awt.geom.Area; 020import java.awt.image.BufferedImage; 021import java.io.File; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.concurrent.Callable; 030import java.util.concurrent.CopyOnWriteArrayList; 031 032import javax.swing.AbstractAction; 033import javax.swing.Action; 034import javax.swing.Icon; 035import javax.swing.ImageIcon; 036import javax.swing.JLabel; 037import javax.swing.JOptionPane; 038import javax.swing.JPanel; 039import javax.swing.JScrollPane; 040 041import org.openstreetmap.josm.Main; 042import org.openstreetmap.josm.actions.ExpertToggleAction; 043import org.openstreetmap.josm.actions.RenameLayerAction; 044import org.openstreetmap.josm.actions.SaveActionBase; 045import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction; 046import org.openstreetmap.josm.data.Bounds; 047import org.openstreetmap.josm.data.SelectionChangedListener; 048import org.openstreetmap.josm.data.conflict.Conflict; 049import org.openstreetmap.josm.data.conflict.ConflictCollection; 050import org.openstreetmap.josm.data.coor.LatLon; 051import org.openstreetmap.josm.data.gpx.GpxData; 052import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 053import org.openstreetmap.josm.data.gpx.WayPoint; 054import org.openstreetmap.josm.data.osm.DataIntegrityProblemException; 055import org.openstreetmap.josm.data.osm.DataSet; 056import org.openstreetmap.josm.data.osm.DataSetMerger; 057import org.openstreetmap.josm.data.osm.DataSource; 058import org.openstreetmap.josm.data.osm.DatasetConsistencyTest; 059import org.openstreetmap.josm.data.osm.IPrimitive; 060import org.openstreetmap.josm.data.osm.Node; 061import org.openstreetmap.josm.data.osm.OsmPrimitive; 062import org.openstreetmap.josm.data.osm.Relation; 063import org.openstreetmap.josm.data.osm.Way; 064import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 065import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; 066import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; 067import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 068import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 069import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 070import org.openstreetmap.josm.data.osm.visitor.paint.Rendering; 071import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 072import org.openstreetmap.josm.data.projection.Projection; 073import org.openstreetmap.josm.data.validation.TestError; 074import org.openstreetmap.josm.gui.ExtendedDialog; 075import org.openstreetmap.josm.gui.MapView; 076import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 077import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 078import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 079import org.openstreetmap.josm.gui.progress.ProgressMonitor; 080import org.openstreetmap.josm.gui.util.GuiHelper; 081import org.openstreetmap.josm.gui.widgets.JosmTextArea; 082import org.openstreetmap.josm.tools.FilteredCollection; 083import org.openstreetmap.josm.tools.GBC; 084import org.openstreetmap.josm.tools.ImageProvider; 085import org.openstreetmap.josm.tools.date.DateUtils; 086 087/** 088 * A layer that holds OSM data from a specific dataset. 089 * The data can be fully edited. 090 * 091 * @author imi 092 */ 093public class OsmDataLayer extends Layer implements Listener, SelectionChangedListener { 094 public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk"; 095 public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer"; 096 097 private boolean requiresSaveToFile = false; 098 private boolean requiresUploadToServer = false; 099 private boolean isChanged = true; 100 private int highlightUpdateCount; 101 102 /** 103 * List of validation errors in this layer. 104 * @since 3669 105 */ 106 public final List<TestError> validationErrors = new ArrayList<>(); 107 108 protected void setRequiresSaveToFile(boolean newValue) { 109 boolean oldValue = requiresSaveToFile; 110 requiresSaveToFile = newValue; 111 if (oldValue != newValue) { 112 propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue); 113 } 114 } 115 116 protected void setRequiresUploadToServer(boolean newValue) { 117 boolean oldValue = requiresUploadToServer; 118 requiresUploadToServer = newValue; 119 if (oldValue != newValue) { 120 propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue); 121 } 122 } 123 124 /** the global counter for created data layers */ 125 private static int dataLayerCounter = 0; 126 127 /** 128 * Replies a new unique name for a data layer 129 * 130 * @return a new unique name for a data layer 131 */ 132 public static String createNewName() { 133 dataLayerCounter++; 134 return tr("Data Layer {0}", dataLayerCounter); 135 } 136 137 public static final class DataCountVisitor extends AbstractVisitor { 138 public int nodes; 139 public int ways; 140 public int relations; 141 public int deletedNodes; 142 public int deletedWays; 143 public int deletedRelations; 144 145 @Override 146 public void visit(final Node n) { 147 nodes++; 148 if (n.isDeleted()) { 149 deletedNodes++; 150 } 151 } 152 153 @Override 154 public void visit(final Way w) { 155 ways++; 156 if (w.isDeleted()) { 157 deletedWays++; 158 } 159 } 160 161 @Override 162 public void visit(final Relation r) { 163 relations++; 164 if (r.isDeleted()) { 165 deletedRelations++; 166 } 167 } 168 } 169 170 public interface CommandQueueListener { 171 void commandChanged(int queueSize, int redoSize); 172 } 173 174 /** 175 * Listener called when a state of this layer has changed. 176 */ 177 public interface LayerStateChangeListener { 178 /** 179 * Notifies that the "upload discouraged" (upload=no) state has changed. 180 * @param layer The layer that has been modified 181 * @param newValue The new value of the state 182 */ 183 void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue); 184 } 185 186 private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>(); 187 188 /** 189 * Adds a layer state change listener 190 * 191 * @param listener the listener. Ignored if null or already registered. 192 * @since 5519 193 */ 194 public void addLayerStateChangeListener(LayerStateChangeListener listener) { 195 if (listener != null) { 196 layerStateChangeListeners.addIfAbsent(listener); 197 } 198 } 199 200 /** 201 * Removes a layer property change listener 202 * 203 * @param listener the listener. Ignored if null or already registered. 204 * @since 5519 205 */ 206 public void removeLayerPropertyChangeListener(LayerStateChangeListener listener) { 207 layerStateChangeListeners.remove(listener); 208 } 209 210 /** 211 * The data behind this layer. 212 */ 213 public final DataSet data; 214 215 /** 216 * the collection of conflicts detected in this layer 217 */ 218 private ConflictCollection conflicts; 219 220 /** 221 * a paint texture for non-downloaded area 222 */ 223 private static TexturePaint hatched; 224 225 static { 226 createHatchTexture(); 227 } 228 229 public static Color getBackgroundColor() { 230 return Main.pref.getColor(marktr("background"), Color.BLACK); 231 } 232 233 public static Color getOutsideColor() { 234 return Main.pref.getColor(marktr("outside downloaded area"), Color.YELLOW); 235 } 236 237 /** 238 * Initialize the hatch pattern used to paint the non-downloaded area 239 */ 240 public static void createHatchTexture() { 241 BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB); 242 Graphics2D big = bi.createGraphics(); 243 big.setColor(getBackgroundColor()); 244 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f); 245 big.setComposite(comp); 246 big.fillRect(0,0,15,15); 247 big.setColor(getOutsideColor()); 248 big.drawLine(0,15,15,0); 249 Rectangle r = new Rectangle(0, 0, 15,15); 250 hatched = new TexturePaint(bi, r); 251 } 252 253 /** 254 * Construct a OsmDataLayer. 255 */ 256 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) { 257 super(name); 258 this.data = data; 259 this.setAssociatedFile(associatedFile); 260 conflicts = new ConflictCollection(); 261 data.addDataSetListener(new DataSetListenerAdapter(this)); 262 data.addDataSetListener(MultipolygonCache.getInstance()); 263 DataSet.addSelectionListener(this); 264 } 265 266 protected Icon getBaseIcon() { 267 return ImageProvider.get("layer", "osmdata_small"); 268 } 269 270 /** 271 * TODO: @return Return a dynamic drawn icon of the map data. The icon is 272 * updated by a background thread to not disturb the running programm. 273 */ 274 @Override public Icon getIcon() { 275 Icon baseIcon = getBaseIcon(); 276 if (isUploadDiscouraged()) { 277 return ImageProvider.overlay(baseIcon, 278 new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(8, 8, Image.SCALE_SMOOTH)), 279 ImageProvider.OverlayPosition.SOUTHEAST); 280 } else { 281 return baseIcon; 282 } 283 } 284 285 /** 286 * Draw all primitives in this layer but do not draw modified ones (they 287 * are drawn by the edit layer). 288 * Draw nodes last to overlap the ways they belong to. 289 */ 290 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) { 291 isChanged = false; 292 highlightUpdateCount = data.getHighlightUpdateCount(); 293 294 boolean active = mv.getActiveLayer() == this; 295 boolean inactive = !active && Main.pref.getBoolean("draw.data.inactive_color", true); 296 boolean virtual = !inactive && mv.isVirtualNodesEnabled(); 297 298 // draw the hatched area for non-downloaded region. only draw if we're the active 299 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc 300 if (active && Main.pref.getBoolean("draw.data.downloaded_area", true) && !data.dataSources.isEmpty()) { 301 // initialize area with current viewport 302 Rectangle b = mv.getBounds(); 303 // on some platforms viewport bounds seem to be offset from the left, 304 // over-grow it just to be sure 305 b.grow(100, 100); 306 Area a = new Area(b); 307 308 // now successively subtract downloaded areas 309 for (Bounds bounds : data.getDataSourceBounds()) { 310 if (bounds.isCollapsed()) { 311 continue; 312 } 313 Point p1 = mv.getPoint(bounds.getMin()); 314 Point p2 = mv.getPoint(bounds.getMax()); 315 Rectangle r = new Rectangle(Math.min(p1.x, p2.x),Math.min(p1.y, p2.y),Math.abs(p2.x-p1.x),Math.abs(p2.y-p1.y)); 316 a.subtract(new Area(r)); 317 } 318 319 // paint remainder 320 g.setPaint(hatched); 321 g.fill(a); 322 } 323 324 Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive); 325 painter.render(data, virtual, box); 326 Main.map.conflictDialog.paintConflicts(g, mv); 327 } 328 329 @Override public String getToolTipText() { 330 int nodes = new FilteredCollection<>(data.getNodes(), OsmPrimitive.nonDeletedPredicate).size(); 331 int ways = new FilteredCollection<>(data.getWays(), OsmPrimitive.nonDeletedPredicate).size(); 332 333 String tool = trn("{0} node", "{0} nodes", nodes, nodes)+", "; 334 tool += trn("{0} way", "{0} ways", ways, ways); 335 336 if (data.getVersion() != null) { 337 tool += ", " + tr("version {0}", data.getVersion()); 338 } 339 File f = getAssociatedFile(); 340 if (f != null) { 341 tool = "<html>"+tool+"<br>"+f.getPath()+"</html>"; 342 } 343 return tool; 344 } 345 346 @Override public void mergeFrom(final Layer from) { 347 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers")); 348 monitor.setCancelable(false); 349 if (from instanceof OsmDataLayer && ((OsmDataLayer)from).isUploadDiscouraged()) { 350 setUploadDiscouraged(true); 351 } 352 mergeFrom(((OsmDataLayer)from).data, monitor); 353 monitor.close(); 354 } 355 356 /** 357 * merges the primitives in dataset <code>from</code> into the dataset of 358 * this layer 359 * 360 * @param from the source data set 361 */ 362 public void mergeFrom(final DataSet from) { 363 mergeFrom(from, null); 364 } 365 366 /** 367 * merges the primitives in dataset <code>from</code> into the dataset of 368 * this layer 369 * 370 * @param from the source data set 371 * @param progressMonitor the progress monitor, can be {@code null} 372 */ 373 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) { 374 final DataSetMerger visitor = new DataSetMerger(data,from); 375 try { 376 visitor.merge(progressMonitor); 377 } catch (DataIntegrityProblemException e) { 378 JOptionPane.showMessageDialog( 379 Main.parent, 380 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(), 381 tr("Error"), 382 JOptionPane.ERROR_MESSAGE 383 ); 384 return; 385 386 } 387 388 Area a = data.getDataSourceArea(); 389 390 // copy the merged layer's data source info. 391 // only add source rectangles if they are not contained in the layer already. 392 for (DataSource src : from.dataSources) { 393 if (a == null || !a.contains(src.bounds.asRect())) { 394 data.dataSources.add(src); 395 } 396 } 397 398 // copy the merged layer's API version 399 if (data.getVersion() == null) { 400 data.setVersion(from.getVersion()); 401 } 402 403 int numNewConflicts = 0; 404 for (Conflict<?> c : visitor.getConflicts()) { 405 if (!conflicts.hasConflict(c)) { 406 numNewConflicts++; 407 conflicts.add(c); 408 } 409 } 410 // repaint to make sure new data is displayed properly. 411 if (Main.isDisplayingMapView()) { 412 Main.map.mapView.repaint(); 413 } 414 // warn about new conflicts 415 if (numNewConflicts > 0 && Main.map != null && Main.map.conflictDialog != null) { 416 Main.map.conflictDialog.warnNumNewConflicts(numNewConflicts); 417 } 418 } 419 420 @Override public boolean isMergable(final Layer other) { 421 // isUploadDiscouraged commented to allow merging between normal layers and discouraged layers with a warning (see #7684) 422 return other instanceof OsmDataLayer;// && (isUploadDiscouraged() == ((OsmDataLayer)other).isUploadDiscouraged()); 423 } 424 425 @Override public void visitBoundingBox(final BoundingXYVisitor v) { 426 for (final Node n: data.getNodes()) { 427 if (n.isUsable()) { 428 v.visit(n); 429 } 430 } 431 } 432 433 /** 434 * Clean out the data behind the layer. This means clearing the redo/undo lists, 435 * really deleting all deleted objects and reset the modified flags. This should 436 * be done after an upload, even after a partial upload. 437 * 438 * @param processed A list of all objects that were actually uploaded. 439 * May be <code>null</code>, which means nothing has been uploaded 440 */ 441 public void cleanupAfterUpload(final Collection<IPrimitive> processed) { 442 // return immediately if an upload attempt failed 443 if (processed == null || processed.isEmpty()) 444 return; 445 446 Main.main.undoRedo.clean(this); 447 448 // if uploaded, clean the modified flags as well 449 data.cleanupDeletedPrimitives(); 450 for (OsmPrimitive p: data.allPrimitives()) { 451 if (processed.contains(p)) { 452 p.setModified(false); 453 } 454 } 455 } 456 457 458 @Override public Object getInfoComponent() { 459 final DataCountVisitor counter = new DataCountVisitor(); 460 for (final OsmPrimitive osm : data.allPrimitives()) { 461 osm.accept(counter); 462 } 463 final JPanel p = new JPanel(new GridBagLayout()); 464 465 String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes); 466 if (counter.deletedNodes > 0) { 467 nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+")"; 468 } 469 470 String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways); 471 if (counter.deletedWays > 0) { 472 wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+")"; 473 } 474 475 String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations); 476 if (counter.deletedRelations > 0) { 477 relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+")"; 478 } 479 480 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol()); 481 p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0)); 482 p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0)); 483 p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0)); 484 p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))), GBC.eop().insets(15,0,0,0)); 485 if (isUploadDiscouraged()) { 486 p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15,0,0,0)); 487 } 488 489 return p; 490 } 491 492 @Override public Action[] getMenuEntries() { 493 List<Action> actions = new ArrayList<>(); 494 actions.addAll(Arrays.asList(new Action[]{ 495 LayerListDialog.getInstance().createActivateLayerAction(this), 496 LayerListDialog.getInstance().createShowHideLayerAction(), 497 LayerListDialog.getInstance().createDeleteLayerAction(), 498 SeparatorLayerAction.INSTANCE, 499 LayerListDialog.getInstance().createMergeLayerAction(this), 500 new LayerSaveAction(this), 501 new LayerSaveAsAction(this), 502 })); 503 if (ExpertToggleAction.isExpert()) { 504 actions.addAll(Arrays.asList(new Action[]{ 505 new LayerGpxExportAction(this), 506 new ConvertToGpxLayerAction()})); 507 } 508 actions.addAll(Arrays.asList(new Action[]{ 509 SeparatorLayerAction.INSTANCE, 510 new RenameLayerAction(getAssociatedFile(), this)})); 511 if (ExpertToggleAction.isExpert() && Main.pref.getBoolean("data.layer.upload_discouragement.menu_item", false)) { 512 actions.add(new ToggleUploadDiscouragedLayerAction(this)); 513 } 514 actions.addAll(Arrays.asList(new Action[]{ 515 new ConsistencyTestAction(), 516 SeparatorLayerAction.INSTANCE, 517 new LayerListPopup.InfoAction(this)})); 518 return actions.toArray(new Action[actions.size()]); 519 } 520 521 public static GpxData toGpxData(DataSet data, File file) { 522 GpxData gpxData = new GpxData(); 523 gpxData.storageFile = file; 524 HashSet<Node> doneNodes = new HashSet<>(); 525 for (Way w : data.getWays()) { 526 if (!w.isUsable()) { 527 continue; 528 } 529 Collection<Collection<WayPoint>> trk = new ArrayList<>(); 530 Map<String, Object> trkAttr = new HashMap<>(); 531 532 if (w.get("name") != null) { 533 trkAttr.put("name", w.get("name")); 534 } 535 536 List<WayPoint> trkseg = null; 537 for (Node n : w.getNodes()) { 538 if (!n.isUsable()) { 539 trkseg = null; 540 continue; 541 } 542 if (trkseg == null) { 543 trkseg = new ArrayList<>(); 544 trk.add(trkseg); 545 } 546 if (!n.isTagged()) { 547 doneNodes.add(n); 548 } 549 WayPoint wpt = new WayPoint(n.getCoor()); 550 if (!n.isTimestampEmpty()) { 551 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp())); 552 wpt.setTime(); 553 } 554 trkseg.add(wpt); 555 } 556 557 gpxData.tracks.add(new ImmutableGpxTrack(trk, trkAttr)); 558 } 559 560 for (Node n : data.getNodes()) { 561 if (n.isIncomplete() || n.isDeleted() || doneNodes.contains(n)) { 562 continue; 563 } 564 WayPoint wpt = new WayPoint(n.getCoor()); 565 String name = n.get("name"); 566 if (name != null) { 567 wpt.attr.put("name", name); 568 } 569 if (!n.isTimestampEmpty()) { 570 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp())); 571 wpt.setTime(); 572 } 573 String desc = n.get("description"); 574 if (desc != null) { 575 wpt.attr.put("desc", desc); 576 } 577 578 gpxData.waypoints.add(wpt); 579 } 580 return gpxData; 581 } 582 583 public GpxData toGpxData() { 584 return toGpxData(data, getAssociatedFile()); 585 } 586 587 public class ConvertToGpxLayerAction extends AbstractAction { 588 public ConvertToGpxLayerAction() { 589 super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx")); 590 putValue("help", ht("/Action/ConvertToGpxLayer")); 591 } 592 @Override 593 public void actionPerformed(ActionEvent e) { 594 Main.main.addLayer(new GpxLayer(toGpxData(), tr("Converted from: {0}", getName()))); 595 Main.main.removeLayer(OsmDataLayer.this); 596 } 597 } 598 599 public boolean containsPoint(LatLon coor) { 600 // we'll assume that if this has no data sources 601 // that it also has no borders 602 if (this.data.dataSources.isEmpty()) 603 return true; 604 605 boolean layer_bounds_point = false; 606 for (DataSource src : this.data.dataSources) { 607 if (src.bounds.contains(coor)) { 608 layer_bounds_point = true; 609 break; 610 } 611 } 612 return layer_bounds_point; 613 } 614 615 /** 616 * replies the set of conflicts currently managed in this layer 617 * 618 * @return the set of conflicts currently managed in this layer 619 */ 620 public ConflictCollection getConflicts() { 621 return conflicts; 622 } 623 624 /** 625 * Replies true if the data managed by this layer needs to be uploaded to 626 * the server because it contains at least one modified primitive. 627 * 628 * @return true if the data managed by this layer needs to be uploaded to 629 * the server because it contains at least one modified primitive; false, 630 * otherwise 631 */ 632 public boolean requiresUploadToServer() { 633 return requiresUploadToServer; 634 } 635 636 /** 637 * Replies true if the data managed by this layer needs to be saved to 638 * a file. Only replies true if a file is assigned to this layer and 639 * if the data managed by this layer has been modified since the last 640 * save operation to the file. 641 * 642 * @return true if the data managed by this layer needs to be saved to 643 * a file 644 */ 645 public boolean requiresSaveToFile() { 646 return getAssociatedFile() != null && requiresSaveToFile; 647 } 648 649 @Override 650 public void onPostLoadFromFile() { 651 setRequiresSaveToFile(false); 652 setRequiresUploadToServer(data.isModified()); 653 } 654 655 public void onPostDownloadFromServer() { 656 setRequiresSaveToFile(true); 657 setRequiresUploadToServer(data.isModified()); 658 } 659 660 @Override 661 public boolean isChanged() { 662 return isChanged || highlightUpdateCount != data.getHighlightUpdateCount(); 663 } 664 665 /** 666 * Initializes the layer after a successful save of OSM data to a file 667 * 668 */ 669 public void onPostSaveToFile() { 670 setRequiresSaveToFile(false); 671 setRequiresUploadToServer(data.isModified()); 672 } 673 674 /** 675 * Initializes the layer after a successful upload to the server 676 * 677 */ 678 public void onPostUploadToServer() { 679 setRequiresUploadToServer(data.isModified()); 680 // keep requiresSaveToDisk unchanged 681 } 682 683 private class ConsistencyTestAction extends AbstractAction { 684 685 public ConsistencyTestAction() { 686 super(tr("Dataset consistency test")); 687 } 688 689 @Override 690 public void actionPerformed(ActionEvent e) { 691 String result = DatasetConsistencyTest.runTests(data); 692 if (result.length() == 0) { 693 JOptionPane.showMessageDialog(Main.parent, tr("No problems found")); 694 } else { 695 JPanel p = new JPanel(new GridBagLayout()); 696 p.add(new JLabel(tr("Following problems found:")), GBC.eol()); 697 JosmTextArea info = new JosmTextArea(result, 20, 60); 698 info.setCaretPosition(0); 699 info.setEditable(false); 700 p.add(new JScrollPane(info), GBC.eop()); 701 702 JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE); 703 } 704 } 705 } 706 707 @Override 708 public void destroy() { 709 DataSet.removeSelectionListener(this); 710 } 711 712 @Override 713 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 714 isChanged = true; 715 setRequiresSaveToFile(true); 716 setRequiresUploadToServer(true); 717 } 718 719 @Override 720 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 721 isChanged = true; 722 } 723 724 @Override 725 public void projectionChanged(Projection oldValue, Projection newValue) { 726 /* 727 * No reprojection required. The dataset itself is registered as projection 728 * change listener and already got notified. 729 */ 730 } 731 732 public final boolean isUploadDiscouraged() { 733 return data.isUploadDiscouraged(); 734 } 735 736 public final void setUploadDiscouraged(boolean uploadDiscouraged) { 737 if (uploadDiscouraged ^ isUploadDiscouraged()) { 738 data.setUploadDiscouraged(uploadDiscouraged); 739 for (LayerStateChangeListener l : layerStateChangeListeners) { 740 l.uploadDiscouragedChanged(this, uploadDiscouraged); 741 } 742 } 743 } 744 745 @Override 746 public boolean isSavable() { 747 return true; // With OsmExporter 748 } 749 750 @Override 751 public boolean checkSaveConditions() { 752 if (isDataSetEmpty()) { 753 if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() { 754 @Override 755 public Integer call() { 756 ExtendedDialog dialog = new ExtendedDialog( 757 Main.parent, 758 tr("Empty document"), 759 new String[] {tr("Save anyway"), tr("Cancel")} 760 ); 761 dialog.setContent(tr("The document contains no data.")); 762 dialog.setButtonIcons(new String[] {"save.png", "cancel.png"}); 763 return dialog.showDialog().getValue(); 764 } 765 })) { 766 return false; 767 } 768 } 769 770 ConflictCollection conflicts = getConflicts(); 771 if (conflicts != null && !conflicts.isEmpty()) { 772 if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() { 773 @Override 774 public Integer call() { 775 ExtendedDialog dialog = new ExtendedDialog( 776 Main.parent, 777 /* I18N: Display title of the window showing conflicts */ 778 tr("Conflicts"), 779 new String[] {tr("Reject Conflicts and Save"), tr("Cancel")} 780 ); 781 dialog.setContent(tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?")); 782 dialog.setButtonIcons(new String[] {"save.png", "cancel.png"}); 783 return dialog.showDialog().getValue(); 784 } 785 })) { 786 return false; 787 } 788 } 789 return true; 790 } 791 792 /** 793 * Check the data set if it would be empty on save. It is empty, if it contains 794 * no objects (after all objects that are created and deleted without being 795 * transferred to the server have been removed). 796 * 797 * @return <code>true</code>, if a save result in an empty data set. 798 */ 799 private boolean isDataSetEmpty() { 800 if (data != null) { 801 for (OsmPrimitive osm : data.allNonDeletedPrimitives()) 802 if (!osm.isDeleted() || !osm.isNewOrUndeleted()) 803 return false; 804 } 805 return true; 806 } 807 808 @Override 809 public File createAndOpenSaveFileChooser() { 810 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save OSM file"), "osm"); 811 } 812}