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

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

see #16128 - add new advanced property mappaint.hide.labels.while.dragging

  • Property svn:eol-style set to native
File size: 45.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.AlphaComposite;
10import java.awt.Color;
11import java.awt.Composite;
12import java.awt.Graphics2D;
13import java.awt.GraphicsEnvironment;
14import java.awt.GridBagLayout;
15import java.awt.Rectangle;
16import java.awt.TexturePaint;
17import java.awt.event.ActionEvent;
18import java.awt.geom.Area;
19import java.awt.geom.Path2D;
20import java.awt.geom.Rectangle2D;
21import java.awt.image.BufferedImage;
22import java.io.File;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.LinkedHashMap;
30import java.util.List;
31import java.util.Map;
32import java.util.Set;
33import java.util.concurrent.CopyOnWriteArrayList;
34import java.util.concurrent.atomic.AtomicBoolean;
35import java.util.concurrent.atomic.AtomicInteger;
36import java.util.regex.Pattern;
37
38import javax.swing.AbstractAction;
39import javax.swing.Action;
40import javax.swing.Icon;
41import javax.swing.JLabel;
42import javax.swing.JOptionPane;
43import javax.swing.JPanel;
44import javax.swing.JScrollPane;
45
46import org.openstreetmap.josm.Main;
47import org.openstreetmap.josm.actions.ExpertToggleAction;
48import org.openstreetmap.josm.actions.RenameLayerAction;
49import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
50import org.openstreetmap.josm.data.APIDataSet;
51import org.openstreetmap.josm.data.Bounds;
52import org.openstreetmap.josm.data.DataSource;
53import org.openstreetmap.josm.data.ProjectionBounds;
54import org.openstreetmap.josm.data.conflict.Conflict;
55import org.openstreetmap.josm.data.conflict.ConflictCollection;
56import org.openstreetmap.josm.data.coor.EastNorth;
57import org.openstreetmap.josm.data.coor.LatLon;
58import org.openstreetmap.josm.data.gpx.GpxConstants;
59import org.openstreetmap.josm.data.gpx.GpxData;
60import org.openstreetmap.josm.data.gpx.GpxLink;
61import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
62import org.openstreetmap.josm.data.gpx.WayPoint;
63import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
64import org.openstreetmap.josm.data.osm.DataSelectionListener;
65import org.openstreetmap.josm.data.osm.DataSet;
66import org.openstreetmap.josm.data.osm.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.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 g.fill(a);
501 }
502
503 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
504 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
505 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
506 painter.render(data, virtual, box);
507 MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
508 }
509
510 @Override public String getToolTipText() {
511 DataCountVisitor counter = new DataCountVisitor();
512 for (final OsmPrimitive osm : data.allPrimitives()) {
513 osm.accept(counter);
514 }
515 int nodes = counter.nodes - counter.deletedNodes;
516 int ways = counter.ways - counter.deletedWays;
517 int rels = counter.relations - counter.deletedRelations;
518
519 StringBuilder tooltip = new StringBuilder("<html>")
520 .append(trn("{0} node", "{0} nodes", nodes, nodes))
521 .append("<br>")
522 .append(trn("{0} way", "{0} ways", ways, ways))
523 .append("<br>")
524 .append(trn("{0} relation", "{0} relations", rels, rels));
525
526 File f = getAssociatedFile();
527 if (f != null) {
528 tooltip.append("<br>").append(f.getPath());
529 }
530 tooltip.append("</html>");
531 return tooltip.toString();
532 }
533
534 @Override public void mergeFrom(final Layer from) {
535 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
536 monitor.setCancelable(false);
537 if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
538 setUploadDiscouraged(true);
539 }
540 mergeFrom(((OsmDataLayer) from).data, monitor);
541 monitor.close();
542 }
543
544 /**
545 * merges the primitives in dataset <code>from</code> into the dataset of
546 * this layer
547 *
548 * @param from the source data set
549 */
550 public void mergeFrom(final DataSet from) {
551 mergeFrom(from, null);
552 }
553
554 /**
555 * merges the primitives in dataset <code>from</code> into the dataset of this layer
556 *
557 * @param from the source data set
558 * @param progressMonitor the progress monitor, can be {@code null}
559 */
560 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
561 final DataSetMerger visitor = new DataSetMerger(data, from);
562 try {
563 visitor.merge(progressMonitor);
564 } catch (DataIntegrityProblemException e) {
565 Logging.error(e);
566 JOptionPane.showMessageDialog(
567 Main.parent,
568 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
569 tr("Error"),
570 JOptionPane.ERROR_MESSAGE
571 );
572 return;
573 }
574
575 int numNewConflicts = 0;
576 for (Conflict<?> c : visitor.getConflicts()) {
577 if (!data.getConflicts().hasConflict(c)) {
578 numNewConflicts++;
579 data.getConflicts().add(c);
580 }
581 }
582 // repaint to make sure new data is displayed properly.
583 invalidate();
584 // warn about new conflicts
585 MapFrame map = MainApplication.getMap();
586 if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
587 map.conflictDialog.warnNumNewConflicts(numNewConflicts);
588 }
589 }
590
591 @Override
592 public boolean isMergable(final Layer other) {
593 // allow merging between normal layers and discouraged layers with a warning (see #7684)
594 return other instanceof OsmDataLayer;
595 }
596
597 @Override
598 public void visitBoundingBox(final BoundingXYVisitor v) {
599 for (final Node n: data.getNodes()) {
600 if (n.isUsable()) {
601 v.visit(n);
602 }
603 }
604 }
605
606 /**
607 * Clean out the data behind the layer. This means clearing the redo/undo lists,
608 * really deleting all deleted objects and reset the modified flags. This should
609 * be done after an upload, even after a partial upload.
610 *
611 * @param processed A list of all objects that were actually uploaded.
612 * May be <code>null</code>, which means nothing has been uploaded
613 */
614 public void cleanupAfterUpload(final Collection<? extends IPrimitive> processed) {
615 // return immediately if an upload attempt failed
616 if (processed == null || processed.isEmpty())
617 return;
618
619 MainApplication.undoRedo.clean(data);
620
621 // if uploaded, clean the modified flags as well
622 data.cleanupDeletedPrimitives();
623 data.beginUpdate();
624 try {
625 for (OsmPrimitive p: data.allPrimitives()) {
626 if (processed.contains(p)) {
627 p.setModified(false);
628 }
629 }
630 } finally {
631 data.endUpdate();
632 }
633 }
634
635 @Override
636 public Object getInfoComponent() {
637 final DataCountVisitor counter = new DataCountVisitor();
638 for (final OsmPrimitive osm : data.allPrimitives()) {
639 osm.accept(counter);
640 }
641 final JPanel p = new JPanel(new GridBagLayout());
642
643 String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
644 if (counter.deletedNodes > 0) {
645 nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+')';
646 }
647
648 String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
649 if (counter.deletedWays > 0) {
650 wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+')';
651 }
652
653 String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
654 if (counter.deletedRelations > 0) {
655 relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+')';
656 }
657
658 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
659 p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
660 p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
661 p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
662 p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))),
663 GBC.eop().insets(15, 0, 0, 0));
664 if (isUploadDiscouraged()) {
665 p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15, 0, 0, 0));
666 }
667 if (data.getUploadPolicy() == UploadPolicy.BLOCKED) {
668 p.add(new JLabel(tr("Upload is blocked")), GBC.eop().insets(15, 0, 0, 0));
669 }
670
671 return p;
672 }
673
674 @Override public Action[] getMenuEntries() {
675 List<Action> actions = new ArrayList<>();
676 actions.addAll(Arrays.asList(
677 LayerListDialog.getInstance().createActivateLayerAction(this),
678 LayerListDialog.getInstance().createShowHideLayerAction(),
679 LayerListDialog.getInstance().createDeleteLayerAction(),
680 SeparatorLayerAction.INSTANCE,
681 LayerListDialog.getInstance().createMergeLayerAction(this),
682 LayerListDialog.getInstance().createDuplicateLayerAction(this),
683 new LayerSaveAction(this),
684 new LayerSaveAsAction(this)));
685 if (ExpertToggleAction.isExpert()) {
686 actions.addAll(Arrays.asList(
687 new LayerGpxExportAction(this),
688 new ConvertToGpxLayerAction()));
689 }
690 actions.addAll(Arrays.asList(
691 SeparatorLayerAction.INSTANCE,
692 new RenameLayerAction(getAssociatedFile(), this)));
693 if (ExpertToggleAction.isExpert()) {
694 actions.add(new ToggleUploadDiscouragedLayerAction(this));
695 }
696 actions.addAll(Arrays.asList(
697 new ConsistencyTestAction(),
698 SeparatorLayerAction.INSTANCE,
699 new LayerListPopup.InfoAction(this)));
700 return actions.toArray(new Action[0]);
701 }
702
703 /**
704 * Converts given OSM dataset to GPX data.
705 * @param data OSM dataset
706 * @param file output .gpx file
707 * @return GPX data
708 */
709 public static GpxData toGpxData(DataSet data, File file) {
710 GpxData gpxData = new GpxData();
711 gpxData.storageFile = file;
712 Set<Node> doneNodes = new HashSet<>();
713 waysToGpxData(data.getWays(), gpxData, doneNodes);
714 nodesToGpxData(data.getNodes(), gpxData, doneNodes);
715 return gpxData;
716 }
717
718 private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes) {
719 /* When the dataset has been obtained from a gpx layer and now is being converted back,
720 * the ways have negative ids. The first created way corresponds to the first gpx segment,
721 * and has the highest id (i.e., closest to zero).
722 * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
723 * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
724 */
725 ways.stream()
726 .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
727 .forEachOrdered(w -> {
728 if (!w.isUsable()) {
729 return;
730 }
731 Collection<Collection<WayPoint>> trk = new ArrayList<>();
732 Map<String, Object> trkAttr = new HashMap<>();
733
734 String name = w.get("name");
735 if (name != null) {
736 trkAttr.put("name", name);
737 }
738
739 List<WayPoint> trkseg = null;
740 for (Node n : w.getNodes()) {
741 if (!n.isUsable()) {
742 trkseg = null;
743 continue;
744 }
745 if (trkseg == null) {
746 trkseg = new ArrayList<>();
747 trk.add(trkseg);
748 }
749 if (!n.isTagged()) {
750 doneNodes.add(n);
751 }
752 trkseg.add(nodeToWayPoint(n));
753 }
754
755 gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
756 });
757 }
758
759 /**
760 * @param n the {@code Node} to convert
761 * @return {@code WayPoint} object
762 * @since 13210
763 */
764 public static WayPoint nodeToWayPoint(Node n) {
765 return nodeToWayPoint(n, 0);
766 }
767
768 /**
769 * @param n the {@code Node} to convert
770 * @param time a time value in milliseconds from the epoch.
771 * @return {@code WayPoint} object
772 * @since 13210
773 */
774 public static WayPoint nodeToWayPoint(Node n, long time) {
775 WayPoint wpt = new WayPoint(n.getCoor());
776
777 // Position info
778
779 addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
780
781 if (time > 0) {
782 wpt.put(GpxConstants.PT_TIME, DateUtils.fromTimestamp(time));
783 wpt.setTime(time);
784 } else if (n.hasKey(GpxConstants.PT_TIME)) {
785 wpt.put(GpxConstants.PT_TIME, DateUtils.fromString(n.get(GpxConstants.PT_TIME)));
786 wpt.setTime();
787 } else if (!n.isTimestampEmpty()) {
788 wpt.put(GpxConstants.PT_TIME, DateUtils.fromTimestamp(n.getRawTimestamp()));
789 wpt.setTime();
790 }
791
792 addDoubleIfPresent(wpt, n, GpxConstants.PT_MAGVAR);
793 addDoubleIfPresent(wpt, n, GpxConstants.PT_GEOIDHEIGHT);
794
795 // Description info
796
797 addStringIfPresent(wpt, n, GpxConstants.GPX_NAME);
798 addStringIfPresent(wpt, n, GpxConstants.GPX_DESC, "description");
799 addStringIfPresent(wpt, n, GpxConstants.GPX_CMT, "comment");
800 addStringIfPresent(wpt, n, GpxConstants.GPX_SRC, "source", "source:position");
801
802 Collection<GpxLink> links = new ArrayList<>();
803 for (String key : new String[]{"link", "url", "website", "contact:website"}) {
804 String value = n.get(key);
805 if (value != null) {
806 links.add(new GpxLink(value));
807 }
808 }
809 wpt.put(GpxConstants.META_LINKS, links);
810
811 addStringIfPresent(wpt, n, GpxConstants.PT_SYM, "wpt_symbol");
812 addStringIfPresent(wpt, n, GpxConstants.PT_TYPE);
813
814 // Accuracy info
815 addStringIfPresent(wpt, n, GpxConstants.PT_FIX, "gps:fix");
816 addIntegerIfPresent(wpt, n, GpxConstants.PT_SAT, "gps:sat");
817 addDoubleIfPresent(wpt, n, GpxConstants.PT_HDOP, "gps:hdop");
818 addDoubleIfPresent(wpt, n, GpxConstants.PT_VDOP, "gps:vdop");
819 addDoubleIfPresent(wpt, n, GpxConstants.PT_PDOP, "gps:pdop");
820 addDoubleIfPresent(wpt, n, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
821 addIntegerIfPresent(wpt, n, GpxConstants.PT_DGPSID, "gps:dgpsid");
822
823 return wpt;
824 }
825
826 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes) {
827 List<Node> sortedNodes = new ArrayList<>(nodes);
828 sortedNodes.removeAll(doneNodes);
829 Collections.sort(sortedNodes);
830 for (Node n : sortedNodes) {
831 if (n.isIncomplete() || n.isDeleted()) {
832 continue;
833 }
834 gpxData.waypoints.add(nodeToWayPoint(n));
835 }
836 }
837
838 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
839 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
840 possibleKeys.add(0, gpxKey);
841 for (String key : possibleKeys) {
842 String value = p.get(key);
843 if (value != null) {
844 try {
845 int i = Integer.parseInt(value);
846 // Sanity checks
847 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
848 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
849 wpt.put(gpxKey, value);
850 break;
851 }
852 } catch (NumberFormatException e) {
853 Logging.trace(e);
854 }
855 }
856 }
857 }
858
859 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
860 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
861 possibleKeys.add(0, gpxKey);
862 for (String key : possibleKeys) {
863 String value = p.get(key);
864 if (value != null) {
865 try {
866 double d = Double.parseDouble(value);
867 // Sanity checks
868 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
869 wpt.put(gpxKey, value);
870 break;
871 }
872 } catch (NumberFormatException e) {
873 Logging.trace(e);
874 }
875 }
876 }
877 }
878
879 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
880 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
881 possibleKeys.add(0, gpxKey);
882 for (String key : possibleKeys) {
883 String value = p.get(key);
884 // Sanity checks
885 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
886 wpt.put(gpxKey, value);
887 break;
888 }
889 }
890 }
891
892 /**
893 * Converts OSM data behind this layer to GPX data.
894 * @return GPX data
895 */
896 public GpxData toGpxData() {
897 return toGpxData(data, getAssociatedFile());
898 }
899
900 /**
901 * Action that converts this OSM layer to a GPX layer.
902 */
903 public class ConvertToGpxLayerAction extends AbstractAction {
904 /**
905 * Constructs a new {@code ConvertToGpxLayerAction}.
906 */
907 public ConvertToGpxLayerAction() {
908 super(tr("Convert to GPX layer"));
909 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
910 putValue("help", ht("/Action/ConvertToGpxLayer"));
911 }
912
913 @Override
914 public void actionPerformed(ActionEvent e) {
915 final GpxData gpxData = toGpxData();
916 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
917 if (getAssociatedFile() != null) {
918 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
919 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
920 }
921 MainApplication.getLayerManager().addLayer(gpxLayer, false);
922 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
923 MainApplication.getLayerManager().addLayer(
924 new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false);
925 }
926 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
927 }
928 }
929
930 /**
931 * Determines if this layer contains data at the given coordinate.
932 * @param coor the coordinate
933 * @return {@code true} if data sources bounding boxes contain {@code coor}
934 */
935 public boolean containsPoint(LatLon coor) {
936 // we'll assume that if this has no data sources
937 // that it also has no borders
938 if (this.data.getDataSources().isEmpty())
939 return true;
940
941 boolean layerBoundsPoint = false;
942 for (DataSource src : this.data.getDataSources()) {
943 if (src.bounds.contains(coor)) {
944 layerBoundsPoint = true;
945 break;
946 }
947 }
948 return layerBoundsPoint;
949 }
950
951 /**
952 * Replies the set of conflicts currently managed in this layer.
953 *
954 * @return the set of conflicts currently managed in this layer
955 */
956 public ConflictCollection getConflicts() {
957 return data.getConflicts();
958 }
959
960 @Override
961 public boolean isDownloadable() {
962 return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked();
963 }
964
965 @Override
966 public boolean isUploadable() {
967 return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked();
968 }
969
970 @Override
971 public boolean requiresUploadToServer() {
972 return isUploadable() && requiresUploadToServer;
973 }
974
975 @Override
976 public boolean requiresSaveToFile() {
977 return getAssociatedFile() != null && requiresSaveToFile;
978 }
979
980 @Override
981 public void onPostLoadFromFile() {
982 setRequiresSaveToFile(false);
983 setRequiresUploadToServer(isModified());
984 invalidate();
985 }
986
987 /**
988 * Actions run after data has been downloaded to this layer.
989 */
990 public void onPostDownloadFromServer() {
991 setRequiresSaveToFile(true);
992 setRequiresUploadToServer(isModified());
993 invalidate();
994 }
995
996 @Override
997 public void onPostSaveToFile() {
998 setRequiresSaveToFile(false);
999 setRequiresUploadToServer(isModified());
1000 }
1001
1002 @Override
1003 public void onPostUploadToServer() {
1004 setRequiresUploadToServer(isModified());
1005 // keep requiresSaveToDisk unchanged
1006 }
1007
1008 private class ConsistencyTestAction extends AbstractAction {
1009
1010 ConsistencyTestAction() {
1011 super(tr("Dataset consistency test"));
1012 }
1013
1014 @Override
1015 public void actionPerformed(ActionEvent e) {
1016 String result = DatasetConsistencyTest.runTests(data);
1017 if (result.isEmpty()) {
1018 JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
1019 } else {
1020 JPanel p = new JPanel(new GridBagLayout());
1021 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
1022 JosmTextArea info = new JosmTextArea(result, 20, 60);
1023 info.setCaretPosition(0);
1024 info.setEditable(false);
1025 p.add(new JScrollPane(info), GBC.eop());
1026
1027 JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1028 }
1029 }
1030 }
1031
1032 @Override
1033 public synchronized void destroy() {
1034 super.destroy();
1035 data.removeSelectionListener(this);
1036 data.removeHighlightUpdateListener(this);
1037 }
1038
1039 @Override
1040 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1041 invalidate();
1042 setRequiresSaveToFile(true);
1043 setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1044 }
1045
1046 @Override
1047 public void selectionChanged(SelectionChangeEvent event) {
1048 invalidate();
1049 }
1050
1051 @Override
1052 public void projectionChanged(Projection oldValue, Projection newValue) {
1053 // No reprojection required. The dataset itself is registered as projection
1054 // change listener and already got notified.
1055 }
1056
1057 @Override
1058 public final boolean isUploadDiscouraged() {
1059 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1060 }
1061
1062 /**
1063 * Sets the "discouraged upload" flag.
1064 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1065 * This feature allows to use "private" data layers.
1066 */
1067 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1068 if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1069 (uploadDiscouraged ^ isUploadDiscouraged())) {
1070 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1071 for (LayerStateChangeListener l : layerStateChangeListeners) {
1072 l.uploadDiscouragedChanged(this, uploadDiscouraged);
1073 }
1074 }
1075 }
1076
1077 @Override
1078 public final boolean isModified() {
1079 return data.isModified();
1080 }
1081
1082 @Override
1083 public boolean isSavable() {
1084 return true; // With OsmExporter
1085 }
1086
1087 @Override
1088 public boolean checkSaveConditions() {
1089 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() -> {
1090 if (GraphicsEnvironment.isHeadless()) {
1091 return 2;
1092 }
1093 return new ExtendedDialog(
1094 Main.parent,
1095 tr("Empty document"),
1096 tr("Save anyway"), tr("Cancel"))
1097 .setContent(tr("The document contains no data."))
1098 .setButtonIcons("save", "cancel")
1099 .showDialog().getValue();
1100 })) {
1101 return false;
1102 }
1103
1104 ConflictCollection conflictsCol = getConflicts();
1105 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1106 new ExtendedDialog(
1107 Main.parent,
1108 /* I18N: Display title of the window showing conflicts */
1109 tr("Conflicts"),
1110 tr("Reject Conflicts and Save"), tr("Cancel"))
1111 .setContent(
1112 tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1113 .setButtonIcons("save", "cancel")
1114 .showDialog().getValue()
1115 );
1116 }
1117
1118 /**
1119 * Check the data set if it would be empty on save. It is empty, if it contains
1120 * no objects (after all objects that are created and deleted without being
1121 * transferred to the server have been removed).
1122 *
1123 * @return <code>true</code>, if a save result in an empty data set.
1124 */
1125 private boolean isDataSetEmpty() {
1126 if (data != null) {
1127 for (OsmPrimitive osm : data.allNonDeletedPrimitives()) {
1128 if (!osm.isDeleted() || !osm.isNewOrUndeleted())
1129 return false;
1130 }
1131 }
1132 return true;
1133 }
1134
1135 @Override
1136 public File createAndOpenSaveFileChooser() {
1137 String extension = PROPERTY_SAVE_EXTENSION.get();
1138 File file = getAssociatedFile();
1139 if (file == null && isRenamed()) {
1140 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1141 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1142 filename.append('.').append(extension);
1143 }
1144 file = new File(filename.toString());
1145 }
1146 return new FileChooserManager()
1147 .title(tr("Save OSM file"))
1148 .extension(extension)
1149 .file(file)
1150 .allTypes(true)
1151 .getFileForSave();
1152 }
1153
1154 @Override
1155 public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1156 UploadDialog dialog = UploadDialog.getUploadDialog();
1157 return new UploadLayerTask(
1158 dialog.getUploadStrategySpecification(),
1159 this,
1160 monitor,
1161 dialog.getChangeset());
1162 }
1163
1164 @Override
1165 public AbstractUploadDialog getUploadDialog() {
1166 UploadDialog dialog = UploadDialog.getUploadDialog();
1167 dialog.setUploadedPrimitives(new APIDataSet(data));
1168 return dialog;
1169 }
1170
1171 @Override
1172 public ProjectionBounds getViewProjectionBounds() {
1173 BoundingXYVisitor v = new BoundingXYVisitor();
1174 v.visit(data.getDataSourceBoundingBox());
1175 if (!v.hasExtend()) {
1176 v.computeBoundingBox(data.getNodes());
1177 }
1178 return v.getBounds();
1179 }
1180
1181 @Override
1182 public void highlightUpdated(HighlightUpdateEvent e) {
1183 invalidate();
1184 }
1185
1186 @Override
1187 public void setName(String name) {
1188 if (data != null) {
1189 data.setName(name);
1190 }
1191 super.setName(name);
1192 }
1193
1194 /**
1195 * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer.
1196 * @since 13434
1197 */
1198 public void setUploadInProgress() {
1199 if (!isUploadInProgress.compareAndSet(false, true)) {
1200 Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName());
1201 }
1202 }
1203
1204 /**
1205 * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer.
1206 * @since 13434
1207 */
1208 public void unsetUploadInProgress() {
1209 if (!isUploadInProgress.compareAndSet(true, false)) {
1210 Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName());
1211 }
1212 }
1213
1214 @Override
1215 public boolean isUploadInProgress() {
1216 return isUploadInProgress.get();
1217 }
1218}
Note: See TracBrowser for help on using the repository browser.