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

Last change on this file since 16548 was 16548, checked in by simon04, 4 years ago

fix #18801 - Allow layers to determine autosave functionality (patch by taylor.smock, modified)

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