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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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