source: josm/trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java@ 6499

Last change on this file since 6499 was 6499, checked in by simon04, 10 years ago

Add checkbox to main menu to toggle hatched background rendering of areas outside of the downloaded areas

This is useful, e.g, for mapping public transport relations when one
downloads a relation modify by id and missing stops by bbox.

  • Property svn:eol-style set to native
File size: 29.7 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.marktr;
7import static org.openstreetmap.josm.tools.I18n.tr;
8import static org.openstreetmap.josm.tools.I18n.trn;
9
10import java.awt.AlphaComposite;
11import java.awt.Color;
12import java.awt.Composite;
13import java.awt.Graphics2D;
14import java.awt.GridBagLayout;
15import java.awt.Image;
16import java.awt.Point;
17import java.awt.Rectangle;
18import java.awt.TexturePaint;
19import java.awt.event.ActionEvent;
20import java.awt.geom.Area;
21import java.awt.image.BufferedImage;
22import java.io.File;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.List;
29import java.util.Map;
30import java.util.concurrent.CopyOnWriteArrayList;
31
32import javax.swing.AbstractAction;
33import javax.swing.Action;
34import javax.swing.Icon;
35import javax.swing.ImageIcon;
36import javax.swing.JLabel;
37import javax.swing.JOptionPane;
38import javax.swing.JPanel;
39import javax.swing.JScrollPane;
40
41import org.openstreetmap.josm.Main;
42import org.openstreetmap.josm.actions.ExpertToggleAction;
43import org.openstreetmap.josm.actions.HatchAreaOutsideDownloadAction;
44import org.openstreetmap.josm.actions.RenameLayerAction;
45import org.openstreetmap.josm.actions.SaveActionBase;
46import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
47import org.openstreetmap.josm.data.Bounds;
48import org.openstreetmap.josm.data.SelectionChangedListener;
49import org.openstreetmap.josm.data.conflict.Conflict;
50import org.openstreetmap.josm.data.conflict.ConflictCollection;
51import org.openstreetmap.josm.data.coor.LatLon;
52import org.openstreetmap.josm.data.gpx.GpxData;
53import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
54import org.openstreetmap.josm.data.gpx.WayPoint;
55import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
56import org.openstreetmap.josm.data.osm.DataSet;
57import org.openstreetmap.josm.data.osm.DataSetMerger;
58import org.openstreetmap.josm.data.osm.DataSource;
59import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
60import org.openstreetmap.josm.data.osm.IPrimitive;
61import org.openstreetmap.josm.data.osm.Node;
62import org.openstreetmap.josm.data.osm.OsmPrimitive;
63import org.openstreetmap.josm.data.osm.Relation;
64import org.openstreetmap.josm.data.osm.Way;
65import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
66import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
67import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
68import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
69import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
70import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
71import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
72import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
73import org.openstreetmap.josm.data.projection.Projection;
74import org.openstreetmap.josm.data.validation.TestError;
75import org.openstreetmap.josm.gui.ExtendedDialog;
76import org.openstreetmap.josm.gui.MapView;
77import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
78import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
79import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
80import org.openstreetmap.josm.gui.progress.ProgressMonitor;
81import org.openstreetmap.josm.gui.widgets.JosmTextArea;
82import org.openstreetmap.josm.tools.DateUtils;
83import org.openstreetmap.josm.tools.FilteredCollection;
84import org.openstreetmap.josm.tools.GBC;
85import org.openstreetmap.josm.tools.ImageProvider;
86
87/**
88 * A layer that holds OSM data from a specific dataset.
89 * The data can be fully edited.
90 *
91 * @author imi
92 */
93public class OsmDataLayer extends Layer implements Listener, SelectionChangedListener {
94 static public final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
95 static public final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
96
97 private boolean requiresSaveToFile = false;
98 private boolean requiresUploadToServer = false;
99 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<TestError>();
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 static private 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 static public String createNewName() {
133 dataLayerCounter++;
134 return tr("Data Layer {0}", dataLayerCounter);
135 }
136
137 public final static 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<LayerStateChangeListener>();
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 if (HatchAreaOutsideDownloadAction.isHatchEnabled()) {
321 g.setPaint(hatched);
322 g.fill(a);
323 }
324 }
325
326 Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
327 painter.render(data, virtual, box);
328 Main.map.conflictDialog.paintConflicts(g, mv);
329 }
330
331 @Override public String getToolTipText() {
332 int nodes = new FilteredCollection<Node>(data.getNodes(), OsmPrimitive.nonDeletedPredicate).size();
333 int ways = new FilteredCollection<Way>(data.getWays(), OsmPrimitive.nonDeletedPredicate).size();
334
335 String tool = trn("{0} node", "{0} nodes", nodes, nodes)+", ";
336 tool += trn("{0} way", "{0} ways", ways, ways);
337
338 if (data.getVersion() != null) {
339 tool += ", " + tr("version {0}", data.getVersion());
340 }
341 File f = getAssociatedFile();
342 if (f != null) {
343 tool = "<html>"+tool+"<br>"+f.getPath()+"</html>";
344 }
345 return tool;
346 }
347
348 @Override public void mergeFrom(final Layer from) {
349 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
350 monitor.setCancelable(false);
351 if (from instanceof OsmDataLayer && ((OsmDataLayer)from).isUploadDiscouraged()) {
352 setUploadDiscouraged(true);
353 }
354 mergeFrom(((OsmDataLayer)from).data, monitor);
355 monitor.close();
356 }
357
358 /**
359 * merges the primitives in dataset <code>from</code> into the dataset of
360 * this layer
361 *
362 * @param from the source data set
363 */
364 public void mergeFrom(final DataSet from) {
365 mergeFrom(from, null);
366 }
367
368 /**
369 * merges the primitives in dataset <code>from</code> into the dataset of
370 * this layer
371 *
372 * @param from the source data set
373 * @param progressMonitor the progress monitor, can be {@code null}
374 */
375 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
376 final DataSetMerger visitor = new DataSetMerger(data,from);
377 try {
378 visitor.merge(progressMonitor);
379 } catch (DataIntegrityProblemException e) {
380 JOptionPane.showMessageDialog(
381 Main.parent,
382 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
383 tr("Error"),
384 JOptionPane.ERROR_MESSAGE
385 );
386 return;
387
388 }
389
390 Area a = data.getDataSourceArea();
391
392 // copy the merged layer's data source info.
393 // only add source rectangles if they are not contained in the layer already.
394 for (DataSource src : from.dataSources) {
395 if (a == null || !a.contains(src.bounds.asRect())) {
396 data.dataSources.add(src);
397 }
398 }
399
400 // copy the merged layer's API version, downgrade if required
401 if (data.getVersion() == null) {
402 data.setVersion(from.getVersion());
403 } else if ("0.5".equals(data.getVersion()) ^ "0.5".equals(from.getVersion())) {
404 Main.warn(tr("Mixing 0.6 and 0.5 data results in version 0.5"));
405 data.setVersion("0.5");
406 }
407
408 int numNewConflicts = 0;
409 for (Conflict<?> c : visitor.getConflicts()) {
410 if (!conflicts.hasConflict(c)) {
411 numNewConflicts++;
412 conflicts.add(c);
413 }
414 }
415 // repaint to make sure new data is displayed properly.
416 if (Main.isDisplayingMapView()) {
417 Main.map.mapView.repaint();
418 }
419 // warn about new conflicts
420 if (numNewConflicts > 0 && Main.map != null && Main.map.conflictDialog != null) {
421 Main.map.conflictDialog.warnNumNewConflicts(numNewConflicts);
422 }
423 }
424
425 @Override public boolean isMergable(final Layer other) {
426 // isUploadDiscouraged commented to allow merging between normal layers and discouraged layers with a warning (see #7684)
427 return other instanceof OsmDataLayer;// && (isUploadDiscouraged() == ((OsmDataLayer)other).isUploadDiscouraged());
428 }
429
430 @Override public void visitBoundingBox(final BoundingXYVisitor v) {
431 for (final Node n: data.getNodes()) {
432 if (n.isUsable()) {
433 v.visit(n);
434 }
435 }
436 }
437
438 /**
439 * Clean out the data behind the layer. This means clearing the redo/undo lists,
440 * really deleting all deleted objects and reset the modified flags. This should
441 * be done after an upload, even after a partial upload.
442 *
443 * @param processed A list of all objects that were actually uploaded.
444 * May be <code>null</code>, which means nothing has been uploaded
445 */
446 public void cleanupAfterUpload(final Collection<IPrimitive> processed) {
447 // return immediately if an upload attempt failed
448 if (processed == null || processed.isEmpty())
449 return;
450
451 Main.main.undoRedo.clean(this);
452
453 // if uploaded, clean the modified flags as well
454 data.cleanupDeletedPrimitives();
455 for (OsmPrimitive p: data.allPrimitives()) {
456 if (processed.contains(p)) {
457 p.setModified(false);
458 }
459 }
460 }
461
462
463 @Override public Object getInfoComponent() {
464 final DataCountVisitor counter = new DataCountVisitor();
465 for (final OsmPrimitive osm : data.allPrimitives()) {
466 osm.accept(counter);
467 }
468 final JPanel p = new JPanel(new GridBagLayout());
469
470 String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
471 if (counter.deletedNodes > 0) {
472 nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+")";
473 }
474
475 String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
476 if (counter.deletedWays > 0) {
477 wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+")";
478 }
479
480 String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
481 if (counter.deletedRelations > 0) {
482 relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+")";
483 }
484
485 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
486 p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
487 p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
488 p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
489 p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))), GBC.eop().insets(15,0,0,0));
490 if (isUploadDiscouraged()) {
491 p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15,0,0,0));
492 }
493
494 return p;
495 }
496
497 @Override public Action[] getMenuEntries() {
498 if (Main.applet)
499 return new Action[]{
500 LayerListDialog.getInstance().createActivateLayerAction(this),
501 LayerListDialog.getInstance().createShowHideLayerAction(),
502 LayerListDialog.getInstance().createDeleteLayerAction(),
503 SeparatorLayerAction.INSTANCE,
504 LayerListDialog.getInstance().createMergeLayerAction(this),
505 SeparatorLayerAction.INSTANCE,
506 new RenameLayerAction(getAssociatedFile(), this),
507 new ConsistencyTestAction(),
508 SeparatorLayerAction.INSTANCE,
509 new LayerListPopup.InfoAction(this)};
510 List<Action> actions = new ArrayList<Action>();
511 actions.addAll(Arrays.asList(new Action[]{
512 LayerListDialog.getInstance().createActivateLayerAction(this),
513 LayerListDialog.getInstance().createShowHideLayerAction(),
514 LayerListDialog.getInstance().createDeleteLayerAction(),
515 SeparatorLayerAction.INSTANCE,
516 LayerListDialog.getInstance().createMergeLayerAction(this),
517 new LayerSaveAction(this),
518 new LayerSaveAsAction(this),
519 }));
520 if (ExpertToggleAction.isExpert()) {
521 actions.addAll(Arrays.asList(new Action[]{
522 new LayerGpxExportAction(this),
523 new ConvertToGpxLayerAction()}));
524 }
525 actions.addAll(Arrays.asList(new Action[]{
526 SeparatorLayerAction.INSTANCE,
527 new RenameLayerAction(getAssociatedFile(), this)}));
528 if (ExpertToggleAction.isExpert() && Main.pref.getBoolean("data.layer.upload_discouragement.menu_item", false)) {
529 actions.add(new ToggleUploadDiscouragedLayerAction(this));
530 }
531 actions.addAll(Arrays.asList(new Action[]{
532 new ConsistencyTestAction(),
533 SeparatorLayerAction.INSTANCE,
534 new LayerListPopup.InfoAction(this)}));
535 return actions.toArray(new Action[actions.size()]);
536 }
537
538 public static GpxData toGpxData(DataSet data, File file) {
539 GpxData gpxData = new GpxData();
540 gpxData.storageFile = file;
541 HashSet<Node> doneNodes = new HashSet<Node>();
542 for (Way w : data.getWays()) {
543 if (!w.isUsable()) {
544 continue;
545 }
546 Collection<Collection<WayPoint>> trk = new ArrayList<Collection<WayPoint>>();
547 Map<String, Object> trkAttr = new HashMap<String, Object>();
548
549 if (w.get("name") != null) {
550 trkAttr.put("name", w.get("name"));
551 }
552
553 List<WayPoint> trkseg = null;
554 for (Node n : w.getNodes()) {
555 if (!n.isUsable()) {
556 trkseg = null;
557 continue;
558 }
559 if (trkseg == null) {
560 trkseg = new ArrayList<WayPoint>();
561 trk.add(trkseg);
562 }
563 if (!n.isTagged()) {
564 doneNodes.add(n);
565 }
566 WayPoint wpt = new WayPoint(n.getCoor());
567 if (!n.isTimestampEmpty()) {
568 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
569 wpt.setTime();
570 }
571 trkseg.add(wpt);
572 }
573
574 gpxData.tracks.add(new ImmutableGpxTrack(trk, trkAttr));
575 }
576
577 for (Node n : data.getNodes()) {
578 if (n.isIncomplete() || n.isDeleted() || doneNodes.contains(n)) {
579 continue;
580 }
581 WayPoint wpt = new WayPoint(n.getCoor());
582 String name = n.get("name");
583 if (name != null) {
584 wpt.attr.put("name", name);
585 }
586 if (!n.isTimestampEmpty()) {
587 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
588 wpt.setTime();
589 }
590 String desc = n.get("description");
591 if (desc != null) {
592 wpt.attr.put("desc", desc);
593 }
594
595 gpxData.waypoints.add(wpt);
596 }
597 return gpxData;
598 }
599
600 public GpxData toGpxData() {
601 return toGpxData(data, getAssociatedFile());
602 }
603
604 public class ConvertToGpxLayerAction extends AbstractAction {
605 public ConvertToGpxLayerAction() {
606 super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx"));
607 putValue("help", ht("/Action/ConvertToGpxLayer"));
608 }
609 @Override
610 public void actionPerformed(ActionEvent e) {
611 Main.main.addLayer(new GpxLayer(toGpxData(), tr("Converted from: {0}", getName())));
612 Main.main.removeLayer(OsmDataLayer.this);
613 }
614 }
615
616 public boolean containsPoint(LatLon coor) {
617 // we'll assume that if this has no data sources
618 // that it also has no borders
619 if (this.data.dataSources.isEmpty())
620 return true;
621
622 boolean layer_bounds_point = false;
623 for (DataSource src : this.data.dataSources) {
624 if (src.bounds.contains(coor)) {
625 layer_bounds_point = true;
626 break;
627 }
628 }
629 return layer_bounds_point;
630 }
631
632 /**
633 * replies the set of conflicts currently managed in this layer
634 *
635 * @return the set of conflicts currently managed in this layer
636 */
637 public ConflictCollection getConflicts() {
638 return conflicts;
639 }
640
641 /**
642 * Replies true if the data managed by this layer needs to be uploaded to
643 * the server because it contains at least one modified primitive.
644 *
645 * @return true if the data managed by this layer needs to be uploaded to
646 * the server because it contains at least one modified primitive; false,
647 * otherwise
648 */
649 public boolean requiresUploadToServer() {
650 return requiresUploadToServer;
651 }
652
653 /**
654 * Replies true if the data managed by this layer needs to be saved to
655 * a file. Only replies true if a file is assigned to this layer and
656 * if the data managed by this layer has been modified since the last
657 * save operation to the file.
658 *
659 * @return true if the data managed by this layer needs to be saved to
660 * a file
661 */
662 public boolean requiresSaveToFile() {
663 return getAssociatedFile() != null && requiresSaveToFile;
664 }
665
666 @Override
667 public void onPostLoadFromFile() {
668 setRequiresSaveToFile(false);
669 setRequiresUploadToServer(data.isModified());
670 }
671
672 public void onPostDownloadFromServer() {
673 setRequiresSaveToFile(true);
674 setRequiresUploadToServer(data.isModified());
675 }
676
677 @Override
678 public boolean isChanged() {
679 return isChanged || highlightUpdateCount != data.getHighlightUpdateCount();
680 }
681
682 /**
683 * Initializes the layer after a successful save of OSM data to a file
684 *
685 */
686 public void onPostSaveToFile() {
687 setRequiresSaveToFile(false);
688 setRequiresUploadToServer(data.isModified());
689 }
690
691 /**
692 * Initializes the layer after a successful upload to the server
693 *
694 */
695 public void onPostUploadToServer() {
696 setRequiresUploadToServer(data.isModified());
697 // keep requiresSaveToDisk unchanged
698 }
699
700 private class ConsistencyTestAction extends AbstractAction {
701
702 public ConsistencyTestAction() {
703 super(tr("Dataset consistency test"));
704 }
705
706 @Override
707 public void actionPerformed(ActionEvent e) {
708 String result = DatasetConsistencyTest.runTests(data);
709 if (result.length() == 0) {
710 JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
711 } else {
712 JPanel p = new JPanel(new GridBagLayout());
713 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
714 JosmTextArea info = new JosmTextArea(result, 20, 60);
715 info.setCaretPosition(0);
716 info.setEditable(false);
717 p.add(new JScrollPane(info), GBC.eop());
718
719 JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
720 }
721 }
722 }
723
724 @Override
725 public void destroy() {
726 DataSet.removeSelectionListener(this);
727 }
728
729 @Override
730 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
731 isChanged = true;
732 setRequiresSaveToFile(true);
733 setRequiresUploadToServer(true);
734 }
735
736 @Override
737 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
738 isChanged = true;
739 }
740
741 @Override
742 public void projectionChanged(Projection oldValue, Projection newValue) {
743 /*
744 * No reprojection required. The dataset itself is registered as projection
745 * change listener and already got notified.
746 */
747 }
748
749 public final boolean isUploadDiscouraged() {
750 return data.isUploadDiscouraged();
751 }
752
753 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
754 if (uploadDiscouraged ^ isUploadDiscouraged()) {
755 data.setUploadDiscouraged(uploadDiscouraged);
756 for (LayerStateChangeListener l : layerStateChangeListeners) {
757 l.uploadDiscouragedChanged(this, uploadDiscouraged);
758 }
759 }
760 }
761
762 @Override
763 public boolean isSavable() {
764 return true; // With OsmExporter
765 }
766
767 @Override
768 public boolean checkSaveConditions() {
769 if (isDataSetEmpty()) {
770 ExtendedDialog dialog = new ExtendedDialog(
771 Main.parent,
772 tr("Empty document"),
773 new String[] {tr("Save anyway"), tr("Cancel")}
774 );
775 dialog.setContent(tr("The document contains no data."));
776 dialog.setButtonIcons(new String[] {"save.png", "cancel.png"});
777 dialog.showDialog();
778 if (dialog.getValue() != 1) return false;
779 }
780
781 ConflictCollection conflicts = getConflicts();
782 if (conflicts != null && !conflicts.isEmpty()) {
783 ExtendedDialog dialog = new ExtendedDialog(
784 Main.parent,
785 /* I18N: Display title of the window showing conflicts */
786 tr("Conflicts"),
787 new String[] {tr("Reject Conflicts and Save"), tr("Cancel")}
788 );
789 dialog.setContent(tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"));
790 dialog.setButtonIcons(new String[] {"save.png", "cancel.png"});
791 dialog.showDialog();
792 if (dialog.getValue() != 1) return false;
793 }
794 return true;
795 }
796
797 /**
798 * Check the data set if it would be empty on save. It is empty, if it contains
799 * no objects (after all objects that are created and deleted without being
800 * transferred to the server have been removed).
801 *
802 * @return <code>true</code>, if a save result in an empty data set.
803 */
804 private boolean isDataSetEmpty() {
805 if (data != null) {
806 for (OsmPrimitive osm : data.allNonDeletedPrimitives())
807 if (!osm.isDeleted() || !osm.isNewOrUndeleted())
808 return false;
809 }
810 return true;
811 }
812
813 @Override
814 public File createAndOpenSaveFileChooser() {
815 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save OSM file"), "osm");
816 }
817}
Note: See TracBrowser for help on using the repository browser.