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