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

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

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

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