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

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

fix #15847 - fires OsmDataLayer property change events in EDT to avoid deadlock in AutoSaveTask

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