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

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

fix #17003 - catch UncheckedParseException when converting OSM data to GPX

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