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

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

see #15182 - deprecate Main.map and Main.isDisplayingMapView(). Replacements: gui.MainApplication.getMap() / gui.MainApplication.isDisplayingMapView()

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