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

Last change on this file since 16187 was 16187, checked in by Don-vip, 4 years ago

fix #18962 - introduce DataSet.update to avoid repetitive begin/endUpdate statements

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