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

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

fix #16686 - add robustness when drawing hatched area for non-downloaded region

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