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

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

sonar - squid:S1319 - Declarations should use Java collection interfaces such as "List" rather than specific implementation classes such as "ArrayList"

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