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

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

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

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