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

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

fix #17979 - display number of incomplete objects in OSM data layer information

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