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

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

see #8039, see #10456 - fix regressions and code style issues

  • Property svn:eol-style set to native
File size: 44.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.AlphaComposite;
10import java.awt.Color;
11import java.awt.Composite;
12import java.awt.Graphics2D;
13import java.awt.GraphicsEnvironment;
14import java.awt.GridBagLayout;
15import java.awt.Rectangle;
16import java.awt.TexturePaint;
17import java.awt.event.ActionEvent;
18import java.awt.geom.Area;
19import java.awt.geom.Path2D;
20import java.awt.geom.Rectangle2D;
21import java.awt.image.BufferedImage;
22import java.io.File;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.LinkedHashMap;
30import java.util.List;
31import java.util.Map;
32import java.util.Set;
33import java.util.concurrent.CopyOnWriteArrayList;
34import java.util.concurrent.atomic.AtomicBoolean;
35import java.util.concurrent.atomic.AtomicInteger;
36import java.util.regex.Pattern;
37
38import javax.swing.AbstractAction;
39import javax.swing.Action;
40import javax.swing.Icon;
41import javax.swing.JLabel;
42import javax.swing.JOptionPane;
43import javax.swing.JPanel;
44import javax.swing.JScrollPane;
45
46import org.openstreetmap.josm.Main;
47import org.openstreetmap.josm.actions.ExpertToggleAction;
48import org.openstreetmap.josm.actions.RenameLayerAction;
49import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
50import org.openstreetmap.josm.data.APIDataSet;
51import org.openstreetmap.josm.data.Bounds;
52import org.openstreetmap.josm.data.DataSource;
53import org.openstreetmap.josm.data.ProjectionBounds;
54import org.openstreetmap.josm.data.conflict.Conflict;
55import org.openstreetmap.josm.data.conflict.ConflictCollection;
56import org.openstreetmap.josm.data.coor.EastNorth;
57import org.openstreetmap.josm.data.coor.LatLon;
58import org.openstreetmap.josm.data.gpx.GpxConstants;
59import org.openstreetmap.josm.data.gpx.GpxData;
60import org.openstreetmap.josm.data.gpx.GpxLink;
61import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
62import org.openstreetmap.josm.data.gpx.WayPoint;
63import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
64import org.openstreetmap.josm.data.osm.DataSelectionListener;
65import org.openstreetmap.josm.data.osm.DataSet;
66import org.openstreetmap.josm.data.osm.DataSet.UploadPolicy;
67import org.openstreetmap.josm.data.osm.DataSetMerger;
68import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
69import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
70import org.openstreetmap.josm.data.osm.IPrimitive;
71import org.openstreetmap.josm.data.osm.Node;
72import org.openstreetmap.josm.data.osm.OsmPrimitive;
73import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
74import org.openstreetmap.josm.data.osm.Relation;
75import org.openstreetmap.josm.data.osm.Way;
76import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
77import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
78import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
79import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
80import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
81import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
82import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
83import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
84import org.openstreetmap.josm.data.preferences.IntegerProperty;
85import org.openstreetmap.josm.data.preferences.NamedColorProperty;
86import org.openstreetmap.josm.data.preferences.StringProperty;
87import org.openstreetmap.josm.data.projection.Projection;
88import org.openstreetmap.josm.data.validation.TestError;
89import org.openstreetmap.josm.gui.ExtendedDialog;
90import org.openstreetmap.josm.gui.MainApplication;
91import org.openstreetmap.josm.gui.MapFrame;
92import org.openstreetmap.josm.gui.MapView;
93import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
94import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
95import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
96import org.openstreetmap.josm.gui.io.AbstractIOTask;
97import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
98import org.openstreetmap.josm.gui.io.UploadDialog;
99import org.openstreetmap.josm.gui.io.UploadLayerTask;
100import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
101import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
102import org.openstreetmap.josm.gui.progress.ProgressMonitor;
103import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
104import org.openstreetmap.josm.gui.util.GuiHelper;
105import org.openstreetmap.josm.gui.widgets.FileChooserManager;
106import org.openstreetmap.josm.gui.widgets.JosmTextArea;
107import org.openstreetmap.josm.spi.preferences.Config;
108import org.openstreetmap.josm.tools.AlphanumComparator;
109import org.openstreetmap.josm.tools.CheckParameterUtil;
110import org.openstreetmap.josm.tools.GBC;
111import org.openstreetmap.josm.tools.ImageOverlay;
112import org.openstreetmap.josm.tools.ImageProvider;
113import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
114import org.openstreetmap.josm.tools.Logging;
115import org.openstreetmap.josm.tools.date.DateUtils;
116
117/**
118 * A layer that holds OSM data from a specific dataset.
119 * The data can be fully edited.
120 *
121 * @author imi
122 * @since 17
123 */
124public class OsmDataLayer extends AbstractModifiableLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
125 private static final int HATCHED_SIZE = 15;
126 /** Property used to know if this layer has to be saved on disk */
127 public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
128 /** Property used to know if this layer has to be uploaded */
129 public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
130
131 private boolean requiresSaveToFile;
132 private boolean requiresUploadToServer;
133 /** Flag used to know if the layer is being uploaded */
134 private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
135
136 /**
137 * List of validation errors in this layer.
138 * @since 3669
139 */
140 public final List<TestError> validationErrors = new ArrayList<>();
141
142 /**
143 * The default number of relations in the recent relations cache.
144 * @see #getRecentRelations()
145 */
146 public static final int DEFAULT_RECENT_RELATIONS_NUMBER = 20;
147 /**
148 * The number of relations to use in the recent relations cache.
149 * @see #getRecentRelations()
150 */
151 public static final IntegerProperty PROPERTY_RECENT_RELATIONS_NUMBER = new IntegerProperty("properties.last-closed-relations-size",
152 DEFAULT_RECENT_RELATIONS_NUMBER);
153 /**
154 * The extension that should be used when saving the OSM file.
155 */
156 public static final StringProperty PROPERTY_SAVE_EXTENSION = new StringProperty("save.extension.osm", "osm");
157
158 private static final NamedColorProperty PROPERTY_BACKGROUND_COLOR = new NamedColorProperty(marktr("background"), Color.BLACK);
159 private static final NamedColorProperty PROPERTY_OUTSIDE_COLOR = new NamedColorProperty(marktr("outside downloaded area"), Color.YELLOW);
160
161 /** List of recent relations */
162 private final Map<Relation, Void> recentRelations = new LruCache(PROPERTY_RECENT_RELATIONS_NUMBER.get()+1);
163
164 /**
165 * Returns list of recently closed relations or null if none.
166 * @return list of recently closed relations or <code>null</code> if none
167 * @since 12291 (signature)
168 * @since 9668
169 */
170 public List<Relation> getRecentRelations() {
171 ArrayList<Relation> list = new ArrayList<>(recentRelations.keySet());
172 Collections.reverse(list);
173 return list;
174 }
175
176 /**
177 * Adds recently closed relation.
178 * @param relation new entry for the list of recently closed relations
179 * @see #PROPERTY_RECENT_RELATIONS_NUMBER
180 * @since 9668
181 */
182 public void setRecentRelation(Relation relation) {
183 recentRelations.put(relation, null);
184 MapFrame map = MainApplication.getMap();
185 if (map != null && map.relationListDialog != null) {
186 map.relationListDialog.enableRecentRelations();
187 }
188 }
189
190 /**
191 * Remove relation from list of recent relations.
192 * @param relation relation to remove
193 * @since 9668
194 */
195 public void removeRecentRelation(Relation relation) {
196 recentRelations.remove(relation);
197 MapFrame map = MainApplication.getMap();
198 if (map != null && map.relationListDialog != null) {
199 map.relationListDialog.enableRecentRelations();
200 }
201 }
202
203 protected void setRequiresSaveToFile(boolean newValue) {
204 boolean oldValue = requiresSaveToFile;
205 requiresSaveToFile = newValue;
206 if (oldValue != newValue) {
207 propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue);
208 }
209 }
210
211 protected void setRequiresUploadToServer(boolean newValue) {
212 boolean oldValue = requiresUploadToServer;
213 requiresUploadToServer = newValue;
214 if (oldValue != newValue) {
215 propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue);
216 }
217 }
218
219 /** the global counter for created data layers */
220 private static final AtomicInteger dataLayerCounter = new AtomicInteger();
221
222 /**
223 * Replies a new unique name for a data layer
224 *
225 * @return a new unique name for a data layer
226 */
227 public static String createNewName() {
228 return createLayerName(dataLayerCounter.incrementAndGet());
229 }
230
231 static String createLayerName(Object arg) {
232 return tr("Data Layer {0}", arg);
233 }
234
235 static final class LruCache extends LinkedHashMap<Relation, Void> {
236 LruCache(int initialCapacity) {
237 super(initialCapacity, 1.1f, true);
238 }
239
240 @Override
241 protected boolean removeEldestEntry(Map.Entry<Relation, Void> eldest) {
242 return size() > PROPERTY_RECENT_RELATIONS_NUMBER.get();
243 }
244 }
245
246 /**
247 * A listener that counts the number of primitives it encounters
248 */
249 public static final class DataCountVisitor implements OsmPrimitiveVisitor {
250 /**
251 * Nodes that have been visited
252 */
253 public int nodes;
254 /**
255 * Ways that have been visited
256 */
257 public int ways;
258 /**
259 * Relations that have been visited
260 */
261 public int relations;
262 /**
263 * Deleted nodes that have been visited
264 */
265 public int deletedNodes;
266 /**
267 * Deleted ways that have been visited
268 */
269 public int deletedWays;
270 /**
271 * Deleted relations that have been visited
272 */
273 public int deletedRelations;
274
275 @Override
276 public void visit(final Node n) {
277 nodes++;
278 if (n.isDeleted()) {
279 deletedNodes++;
280 }
281 }
282
283 @Override
284 public void visit(final Way w) {
285 ways++;
286 if (w.isDeleted()) {
287 deletedWays++;
288 }
289 }
290
291 @Override
292 public void visit(final Relation r) {
293 relations++;
294 if (r.isDeleted()) {
295 deletedRelations++;
296 }
297 }
298 }
299
300 /**
301 * Listener called when a state of this layer has changed.
302 * @since 10600 (functional interface)
303 */
304 @FunctionalInterface
305 public interface LayerStateChangeListener {
306 /**
307 * Notifies that the "upload discouraged" (upload=no) state has changed.
308 * @param layer The layer that has been modified
309 * @param newValue The new value of the state
310 */
311 void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue);
312 }
313
314 private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
315
316 /**
317 * Adds a layer state change listener
318 *
319 * @param listener the listener. Ignored if null or already registered.
320 * @since 5519
321 */
322 public void addLayerStateChangeListener(LayerStateChangeListener listener) {
323 if (listener != null) {
324 layerStateChangeListeners.addIfAbsent(listener);
325 }
326 }
327
328 /**
329 * Removes a layer state change listener
330 *
331 * @param listener the listener. Ignored if null or already registered.
332 * @since 10340
333 */
334 public void removeLayerStateChangeListener(LayerStateChangeListener listener) {
335 layerStateChangeListeners.remove(listener);
336 }
337
338 /**
339 * The data behind this layer.
340 */
341 public final DataSet data;
342
343 /**
344 * a texture for non-downloaded area
345 */
346 private static volatile BufferedImage hatched;
347
348 static {
349 createHatchTexture();
350 }
351
352 /**
353 * Replies background color for downloaded areas.
354 * @return background color for downloaded areas. Black by default
355 */
356 public static Color getBackgroundColor() {
357 return PROPERTY_BACKGROUND_COLOR.get();
358 }
359
360 /**
361 * Replies background color for non-downloaded areas.
362 * @return background color for non-downloaded areas. Yellow by default
363 */
364 public static Color getOutsideColor() {
365 return PROPERTY_OUTSIDE_COLOR.get();
366 }
367
368 /**
369 * Initialize the hatch pattern used to paint the non-downloaded area
370 */
371 public static void createHatchTexture() {
372 BufferedImage bi = new BufferedImage(HATCHED_SIZE, HATCHED_SIZE, BufferedImage.TYPE_INT_ARGB);
373 Graphics2D big = bi.createGraphics();
374 big.setColor(getBackgroundColor());
375 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
376 big.setComposite(comp);
377 big.fillRect(0, 0, HATCHED_SIZE, HATCHED_SIZE);
378 big.setColor(getOutsideColor());
379 big.drawLine(-1, 6, 6, -1);
380 big.drawLine(4, 16, 16, 4);
381 hatched = bi;
382 }
383
384 /**
385 * Construct a new {@code OsmDataLayer}.
386 * @param data OSM data
387 * @param name Layer name
388 * @param associatedFile Associated .osm file (can be null)
389 */
390 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
391 super(name);
392 CheckParameterUtil.ensureParameterNotNull(data, "data");
393 this.data = data;
394 this.data.setName(name);
395 this.setAssociatedFile(associatedFile);
396 data.addDataSetListener(new DataSetListenerAdapter(this));
397 data.addDataSetListener(MultipolygonCache.getInstance());
398 data.addHighlightUpdateListener(this);
399 data.addSelectionListener(this);
400 if (name != null && name.startsWith(createLayerName("")) && Character.isDigit(
401 (name.substring(createLayerName("").length()) + "XX" /*avoid StringIndexOutOfBoundsException*/).charAt(1))) {
402 while (AlphanumComparator.getInstance().compare(createLayerName(dataLayerCounter), name) < 0) {
403 final int i = dataLayerCounter.incrementAndGet();
404 if (i > 1_000_000) {
405 break; // to avoid looping in unforeseen case
406 }
407 }
408 }
409 }
410
411 /**
412 * Return the image provider to get the base icon
413 * @return image provider class which can be modified
414 * @since 8323
415 */
416 protected ImageProvider getBaseIconProvider() {
417 return new ImageProvider("layer", "osmdata_small");
418 }
419
420 @Override
421 public Icon getIcon() {
422 ImageProvider base = getBaseIconProvider().setMaxSize(ImageSizes.LAYER);
423 if (isUploadDiscouraged() || data.getUploadPolicy() == UploadPolicy.BLOCKED) {
424 base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0));
425 }
426
427 if (isUploadInProgress()) {
428 // If the layer is being uploaded then change the default icon to a clock
429 base = new ImageProvider("clock").setMaxSize(ImageSizes.LAYER);
430 } else if (isReadOnly()) {
431 // If the layer is read only then change the default icon to a lock
432 base = new ImageProvider("lock").setMaxSize(ImageSizes.LAYER);
433 }
434 return base.get();
435 }
436
437 /**
438 * Draw all primitives in this layer but do not draw modified ones (they
439 * are drawn by the edit layer).
440 * Draw nodes last to overlap the ways they belong to.
441 */
442 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
443 boolean active = mv.getLayerManager().getActiveLayer() == this;
444 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
445 boolean virtual = !inactive && mv.isVirtualNodesEnabled();
446
447 // draw the hatched area for non-downloaded region. only draw if we're the active
448 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
449 if (active && Config.getPref().getBoolean("draw.data.downloaded_area", true) && !data.getDataSources().isEmpty()) {
450 // initialize area with current viewport
451 Rectangle b = mv.getBounds();
452 // on some platforms viewport bounds seem to be offset from the left,
453 // over-grow it just to be sure
454 b.grow(100, 100);
455 Path2D p = new Path2D.Double();
456
457 // combine successively downloaded areas
458 for (Bounds bounds : data.getDataSourceBounds()) {
459 if (bounds.isCollapsed()) {
460 continue;
461 }
462 p.append(mv.getState().getArea(bounds), false);
463 }
464 // subtract combined areas
465 Area a = new Area(b);
466 a.subtract(new Area(p));
467
468 // paint remainder
469 MapViewPoint anchor = mv.getState().getPointFor(new EastNorth(0, 0));
470 Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getInView().getX() % HATCHED_SIZE,
471 anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
472 g.setPaint(new TexturePaint(hatched, anchorRect));
473 g.fill(a);
474 }
475
476 Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
477 painter.render(data, virtual, box);
478 MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
479 }
480
481 @Override public String getToolTipText() {
482 DataCountVisitor counter = new DataCountVisitor();
483 for (final OsmPrimitive osm : data.allPrimitives()) {
484 osm.accept(counter);
485 }
486 int nodes = counter.nodes - counter.deletedNodes;
487 int ways = counter.ways - counter.deletedWays;
488 int rels = counter.relations - counter.deletedRelations;
489
490 StringBuilder tooltip = new StringBuilder("<html>")
491 .append(trn("{0} node", "{0} nodes", nodes, nodes))
492 .append("<br>")
493 .append(trn("{0} way", "{0} ways", ways, ways))
494 .append("<br>")
495 .append(trn("{0} relation", "{0} relations", rels, rels));
496
497 File f = getAssociatedFile();
498 if (f != null) {
499 tooltip.append("<br>").append(f.getPath());
500 }
501 tooltip.append("</html>");
502 return tooltip.toString();
503 }
504
505 @Override public void mergeFrom(final Layer from) {
506 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
507 monitor.setCancelable(false);
508 if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
509 setUploadDiscouraged(true);
510 }
511 mergeFrom(((OsmDataLayer) from).data, monitor);
512 monitor.close();
513 }
514
515 /**
516 * merges the primitives in dataset <code>from</code> into the dataset of
517 * this layer
518 *
519 * @param from the source data set
520 */
521 public void mergeFrom(final DataSet from) {
522 mergeFrom(from, null);
523 }
524
525 /**
526 * merges the primitives in dataset <code>from</code> into the dataset of this layer
527 *
528 * @param from the source data set
529 * @param progressMonitor the progress monitor, can be {@code null}
530 */
531 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
532 final DataSetMerger visitor = new DataSetMerger(data, from);
533 try {
534 visitor.merge(progressMonitor);
535 } catch (DataIntegrityProblemException e) {
536 Logging.error(e);
537 JOptionPane.showMessageDialog(
538 Main.parent,
539 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
540 tr("Error"),
541 JOptionPane.ERROR_MESSAGE
542 );
543 return;
544 }
545
546 Area a = data.getDataSourceArea();
547
548 // copy the merged layer's data source info.
549 // only add source rectangles if they are not contained in the layer already.
550 for (DataSource src : from.getDataSources()) {
551 if (a == null || !a.contains(src.bounds.asRect())) {
552 data.addDataSource(src);
553 }
554 }
555
556 // copy the merged layer's API version
557 if (data.getVersion() == null) {
558 data.setVersion(from.getVersion());
559 }
560
561 int numNewConflicts = 0;
562 for (Conflict<?> c : visitor.getConflicts()) {
563 if (!data.getConflicts().hasConflict(c)) {
564 numNewConflicts++;
565 data.getConflicts().add(c);
566 }
567 }
568 // repaint to make sure new data is displayed properly.
569 invalidate();
570 // warn about new conflicts
571 MapFrame map = MainApplication.getMap();
572 if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
573 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 MainApplication.undoRedo.clean(data);
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[0]);
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 /**
746 * @param n the {@code Node} to convert
747 * @return {@code WayPoint} object
748 * @since 13210
749 */
750 public static WayPoint nodeToWayPoint(Node n) {
751 return nodeToWayPoint(n, 0);
752 }
753
754 /**
755 * @param n the {@code Node} to convert
756 * @param time a time value in milliseconds from the epoch.
757 * @return {@code WayPoint} object
758 * @since 13210
759 */
760 public static WayPoint nodeToWayPoint(Node n, long time) {
761 WayPoint wpt = new WayPoint(n.getCoor());
762
763 // Position info
764
765 addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
766
767 if (time > 0) {
768 wpt.setTime(time);
769 } else if (!n.isTimestampEmpty()) {
770 wpt.put("time", DateUtils.fromTimestamp(n.getRawTimestamp()));
771 wpt.setTime();
772 }
773
774 addDoubleIfPresent(wpt, n, GpxConstants.PT_MAGVAR);
775 addDoubleIfPresent(wpt, n, GpxConstants.PT_GEOIDHEIGHT);
776
777 // Description info
778
779 addStringIfPresent(wpt, n, GpxConstants.GPX_NAME);
780 addStringIfPresent(wpt, n, GpxConstants.GPX_DESC, "description");
781 addStringIfPresent(wpt, n, GpxConstants.GPX_CMT, "comment");
782 addStringIfPresent(wpt, n, GpxConstants.GPX_SRC, "source", "source:position");
783
784 Collection<GpxLink> links = new ArrayList<>();
785 for (String key : new String[]{"link", "url", "website", "contact:website"}) {
786 String value = n.get(key);
787 if (value != null) {
788 links.add(new GpxLink(value));
789 }
790 }
791 wpt.put(GpxConstants.META_LINKS, links);
792
793 addStringIfPresent(wpt, n, GpxConstants.PT_SYM, "wpt_symbol");
794 addStringIfPresent(wpt, n, GpxConstants.PT_TYPE);
795
796 // Accuracy info
797 addStringIfPresent(wpt, n, GpxConstants.PT_FIX, "gps:fix");
798 addIntegerIfPresent(wpt, n, GpxConstants.PT_SAT, "gps:sat");
799 addDoubleIfPresent(wpt, n, GpxConstants.PT_HDOP, "gps:hdop");
800 addDoubleIfPresent(wpt, n, GpxConstants.PT_VDOP, "gps:vdop");
801 addDoubleIfPresent(wpt, n, GpxConstants.PT_PDOP, "gps:pdop");
802 addDoubleIfPresent(wpt, n, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
803 addIntegerIfPresent(wpt, n, GpxConstants.PT_DGPSID, "gps:dgpsid");
804
805 return wpt;
806 }
807
808 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes) {
809 List<Node> sortedNodes = new ArrayList<>(nodes);
810 sortedNodes.removeAll(doneNodes);
811 Collections.sort(sortedNodes);
812 for (Node n : sortedNodes) {
813 if (n.isIncomplete() || n.isDeleted()) {
814 continue;
815 }
816 gpxData.waypoints.add(nodeToWayPoint(n));
817 }
818 }
819
820 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
821 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
822 possibleKeys.add(0, gpxKey);
823 for (String key : possibleKeys) {
824 String value = p.get(key);
825 if (value != null) {
826 try {
827 int i = Integer.parseInt(value);
828 // Sanity checks
829 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
830 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
831 wpt.put(gpxKey, value);
832 break;
833 }
834 } catch (NumberFormatException e) {
835 Logging.trace(e);
836 }
837 }
838 }
839 }
840
841 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
842 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
843 possibleKeys.add(0, gpxKey);
844 for (String key : possibleKeys) {
845 String value = p.get(key);
846 if (value != null) {
847 try {
848 double d = Double.parseDouble(value);
849 // Sanity checks
850 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
851 wpt.put(gpxKey, value);
852 break;
853 }
854 } catch (NumberFormatException e) {
855 Logging.trace(e);
856 }
857 }
858 }
859 }
860
861 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String... osmKeys) {
862 List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
863 possibleKeys.add(0, gpxKey);
864 for (String key : possibleKeys) {
865 String value = p.get(key);
866 // Sanity checks
867 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
868 wpt.put(gpxKey, value);
869 break;
870 }
871 }
872 }
873
874 /**
875 * Converts OSM data behind this layer to GPX data.
876 * @return GPX data
877 */
878 public GpxData toGpxData() {
879 return toGpxData(data, getAssociatedFile());
880 }
881
882 /**
883 * Action that converts this OSM layer to a GPX layer.
884 */
885 public class ConvertToGpxLayerAction extends AbstractAction {
886 /**
887 * Constructs a new {@code ConvertToGpxLayerAction}.
888 */
889 public ConvertToGpxLayerAction() {
890 super(tr("Convert to GPX layer"));
891 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
892 putValue("help", ht("/Action/ConvertToGpxLayer"));
893 }
894
895 @Override
896 public void actionPerformed(ActionEvent e) {
897 final GpxData gpxData = toGpxData();
898 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
899 if (getAssociatedFile() != null) {
900 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
901 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
902 }
903 MainApplication.getLayerManager().addLayer(gpxLayer);
904 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
905 MainApplication.getLayerManager().addLayer(new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer));
906 }
907 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
908 }
909 }
910
911 /**
912 * Determines if this layer contains data at the given coordinate.
913 * @param coor the coordinate
914 * @return {@code true} if data sources bounding boxes contain {@code coor}
915 */
916 public boolean containsPoint(LatLon coor) {
917 // we'll assume that if this has no data sources
918 // that it also has no borders
919 if (this.data.getDataSources().isEmpty())
920 return true;
921
922 boolean layerBoundsPoint = false;
923 for (DataSource src : this.data.getDataSources()) {
924 if (src.bounds.contains(coor)) {
925 layerBoundsPoint = true;
926 break;
927 }
928 }
929 return layerBoundsPoint;
930 }
931
932 /**
933 * Replies the set of conflicts currently managed in this layer.
934 *
935 * @return the set of conflicts currently managed in this layer
936 */
937 public ConflictCollection getConflicts() {
938 return data.getConflicts();
939 }
940
941 @Override
942 public boolean isUploadable() {
943 return data.getUploadPolicy() != UploadPolicy.BLOCKED;
944 }
945
946 @Override
947 public boolean requiresUploadToServer() {
948 return isUploadable() && requiresUploadToServer;
949 }
950
951 @Override
952 public boolean requiresSaveToFile() {
953 return getAssociatedFile() != null && requiresSaveToFile;
954 }
955
956 @Override
957 public void onPostLoadFromFile() {
958 setRequiresSaveToFile(false);
959 setRequiresUploadToServer(isModified());
960 invalidate();
961 }
962
963 /**
964 * Actions run after data has been downloaded to this layer.
965 */
966 public void onPostDownloadFromServer() {
967 setRequiresSaveToFile(true);
968 setRequiresUploadToServer(isModified());
969 invalidate();
970 }
971
972 @Override
973 public void onPostSaveToFile() {
974 setRequiresSaveToFile(false);
975 setRequiresUploadToServer(isModified());
976 }
977
978 @Override
979 public void onPostUploadToServer() {
980 setRequiresUploadToServer(isModified());
981 // keep requiresSaveToDisk unchanged
982 }
983
984 private class ConsistencyTestAction extends AbstractAction {
985
986 ConsistencyTestAction() {
987 super(tr("Dataset consistency test"));
988 }
989
990 @Override
991 public void actionPerformed(ActionEvent e) {
992 String result = DatasetConsistencyTest.runTests(data);
993 if (result.isEmpty()) {
994 JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
995 } else {
996 JPanel p = new JPanel(new GridBagLayout());
997 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
998 JosmTextArea info = new JosmTextArea(result, 20, 60);
999 info.setCaretPosition(0);
1000 info.setEditable(false);
1001 p.add(new JScrollPane(info), GBC.eop());
1002
1003 JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1004 }
1005 }
1006 }
1007
1008 @Override
1009 public synchronized void destroy() {
1010 super.destroy();
1011 data.removeSelectionListener(this);
1012 data.removeHighlightUpdateListener(this);
1013 }
1014
1015 @Override
1016 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1017 invalidate();
1018 setRequiresSaveToFile(true);
1019 setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1020 }
1021
1022 @Override
1023 public void selectionChanged(SelectionChangeEvent event) {
1024 invalidate();
1025 }
1026
1027 @Override
1028 public void projectionChanged(Projection oldValue, Projection newValue) {
1029 // No reprojection required. The dataset itself is registered as projection
1030 // change listener and already got notified.
1031 }
1032
1033 /**
1034 * Determines if upload is being discouraged.
1035 * (i.e. this dataset contains private data which should not be uploaded)
1036 * @return {@code true} if upload is being discouraged, {@code false} otherwise
1037 */
1038 @Override
1039 public final boolean isUploadDiscouraged() {
1040 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1041 }
1042
1043 /**
1044 * Sets the "discouraged upload" flag.
1045 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1046 * This feature allows to use "private" data layers.
1047 */
1048 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1049 if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1050 (uploadDiscouraged ^ isUploadDiscouraged())) {
1051 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1052 for (LayerStateChangeListener l : layerStateChangeListeners) {
1053 l.uploadDiscouragedChanged(this, uploadDiscouraged);
1054 }
1055 }
1056 }
1057
1058 @Override
1059 public final boolean isModified() {
1060 return data.isModified();
1061 }
1062
1063 @Override
1064 public boolean isSavable() {
1065 return true; // With OsmExporter
1066 }
1067
1068 @Override
1069 public boolean checkSaveConditions() {
1070 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() -> {
1071 if (GraphicsEnvironment.isHeadless()) {
1072 return 2;
1073 }
1074 return new ExtendedDialog(
1075 Main.parent,
1076 tr("Empty document"),
1077 tr("Save anyway"), tr("Cancel"))
1078 .setContent(tr("The document contains no data."))
1079 .setButtonIcons("save", "cancel")
1080 .showDialog().getValue();
1081 })) {
1082 return false;
1083 }
1084
1085 ConflictCollection conflictsCol = getConflicts();
1086 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1087 new ExtendedDialog(
1088 Main.parent,
1089 /* I18N: Display title of the window showing conflicts */
1090 tr("Conflicts"),
1091 tr("Reject Conflicts and Save"), tr("Cancel"))
1092 .setContent(
1093 tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1094 .setButtonIcons("save", "cancel")
1095 .showDialog().getValue()
1096 );
1097 }
1098
1099 /**
1100 * Check the data set if it would be empty on save. It is empty, if it contains
1101 * no objects (after all objects that are created and deleted without being
1102 * transferred to the server have been removed).
1103 *
1104 * @return <code>true</code>, if a save result in an empty data set.
1105 */
1106 private boolean isDataSetEmpty() {
1107 if (data != null) {
1108 for (OsmPrimitive osm : data.allNonDeletedPrimitives()) {
1109 if (!osm.isDeleted() || !osm.isNewOrUndeleted())
1110 return false;
1111 }
1112 }
1113 return true;
1114 }
1115
1116 @Override
1117 public File createAndOpenSaveFileChooser() {
1118 String extension = PROPERTY_SAVE_EXTENSION.get();
1119 File file = getAssociatedFile();
1120 if (file == null && isRenamed()) {
1121 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1122 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1123 filename.append('.').append(extension);
1124 }
1125 file = new File(filename.toString());
1126 }
1127 return new FileChooserManager()
1128 .title(tr("Save OSM file"))
1129 .extension(extension)
1130 .file(file)
1131 .allTypes(true)
1132 .getFileForSave();
1133 }
1134
1135 @Override
1136 public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1137 UploadDialog dialog = UploadDialog.getUploadDialog();
1138 return new UploadLayerTask(
1139 dialog.getUploadStrategySpecification(),
1140 this,
1141 monitor,
1142 dialog.getChangeset());
1143 }
1144
1145 @Override
1146 public AbstractUploadDialog getUploadDialog() {
1147 UploadDialog dialog = UploadDialog.getUploadDialog();
1148 dialog.setUploadedPrimitives(new APIDataSet(data));
1149 return dialog;
1150 }
1151
1152 @Override
1153 public ProjectionBounds getViewProjectionBounds() {
1154 BoundingXYVisitor v = new BoundingXYVisitor();
1155 v.visit(data.getDataSourceBoundingBox());
1156 if (!v.hasExtend()) {
1157 v.computeBoundingBox(data.getNodes());
1158 }
1159 return v.getBounds();
1160 }
1161
1162 @Override
1163 public void highlightUpdated(HighlightUpdateEvent e) {
1164 invalidate();
1165 }
1166
1167 @Override
1168 public void setName(String name) {
1169 if (data != null) {
1170 data.setName(name);
1171 }
1172 super.setName(name);
1173 }
1174
1175 @Override
1176 public void setReadOnly() {
1177 data.setReadOnly();
1178 }
1179
1180 @Override
1181 public void unsetReadOnly() {
1182 data.unsetReadOnly();
1183 }
1184
1185 @Override
1186 public boolean isReadOnly() {
1187 return data.isReadOnly();
1188 }
1189
1190 /**
1191 * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer.
1192 * @since 13434
1193 */
1194 public void setUploadInProgress() {
1195 if (!isUploadInProgress.compareAndSet(false, true)) {
1196 Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName());
1197 }
1198 }
1199
1200 /**
1201 * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer.
1202 * @since 13434
1203 */
1204 public void unsetUploadInProgress() {
1205 if (!isUploadInProgress.compareAndSet(true, false)) {
1206 Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName());
1207 }
1208 }
1209
1210 @Override
1211 public boolean isUploadInProgress() {
1212 return isUploadInProgress.get();
1213 }
1214}
Note: See TracBrowser for help on using the repository browser.