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

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

fix #16855 - OsmDataLayerTest: fix for non-headless mode (patch by ris)

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