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

Last change on this file since 14537 was 14537, checked in by GerdP, 5 years ago

see #17040 call Dataset.clear() in OsmDataLayer.destroy()

Without this call sometimes the complete Dataset instance is not GCed, esp. when dDataset contains a large number of objects. The call to clear() eases work of GC.

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