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

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

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

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