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

Last change on this file since 13133 was 13133, checked in by bastiK, 6 years ago

applied #8509 - Background uploading (patch by udit, minor changes)

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