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

Last change on this file was 19387, checked in by stoecker, 2 months ago

see #24238 - support more EXIF data in image correlation

  • Property svn:eol-style set to native
File size: 61.6 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.GridBagLayout;
14import java.awt.Rectangle;
15import java.awt.TexturePaint;
16import java.awt.datatransfer.Transferable;
17import java.awt.datatransfer.UnsupportedFlavorException;
18import java.awt.event.ActionEvent;
19import java.awt.geom.Area;
20import java.awt.geom.Path2D;
21import java.awt.geom.Rectangle2D;
22import java.awt.image.BufferedImage;
23import java.io.File;
24import java.io.IOException;
25import java.time.DateTimeException;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.Collection;
29import java.util.Collections;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Map;
34import java.util.Map.Entry;
35import java.util.Objects;
36import java.util.Set;
37import java.util.concurrent.CopyOnWriteArrayList;
38import java.util.concurrent.atomic.AtomicBoolean;
39import java.util.concurrent.atomic.AtomicInteger;
40import java.util.regex.Pattern;
41import java.util.stream.Collectors;
42import java.util.stream.Stream;
43
44import javax.swing.AbstractAction;
45import javax.swing.Action;
46import javax.swing.Icon;
47import javax.swing.JLabel;
48import javax.swing.JOptionPane;
49import javax.swing.JPanel;
50import javax.swing.JScrollPane;
51
52import org.apache.commons.jcs3.access.CacheAccess;
53import org.openstreetmap.gui.jmapviewer.OsmMercator;
54import org.openstreetmap.josm.actions.AutoScaleAction;
55import org.openstreetmap.josm.actions.ExpertToggleAction;
56import org.openstreetmap.josm.actions.RenameLayerAction;
57import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
58import org.openstreetmap.josm.data.APIDataSet;
59import org.openstreetmap.josm.data.Bounds;
60import org.openstreetmap.josm.data.Data;
61import org.openstreetmap.josm.data.IBounds;
62import org.openstreetmap.josm.data.ProjectionBounds;
63import org.openstreetmap.josm.data.UndoRedoHandler;
64import org.openstreetmap.josm.data.cache.JCSCacheManager;
65import org.openstreetmap.josm.data.conflict.Conflict;
66import org.openstreetmap.josm.data.conflict.ConflictCollection;
67import org.openstreetmap.josm.data.coor.EastNorth;
68import org.openstreetmap.josm.data.coor.LatLon;
69import org.openstreetmap.josm.data.gpx.GpxConstants;
70import org.openstreetmap.josm.data.gpx.GpxData;
71import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
72import org.openstreetmap.josm.data.gpx.GpxLink;
73import org.openstreetmap.josm.data.gpx.GpxTrack;
74import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
75import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
76import org.openstreetmap.josm.data.gpx.WayPoint;
77import org.openstreetmap.josm.data.osm.BBox;
78import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
79import org.openstreetmap.josm.data.osm.DataSelectionListener;
80import org.openstreetmap.josm.data.osm.DataSet;
81import org.openstreetmap.josm.data.osm.DataSetMerger;
82import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
83import org.openstreetmap.josm.data.osm.DownloadPolicy;
84import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
85import org.openstreetmap.josm.data.osm.INode;
86import org.openstreetmap.josm.data.osm.IPrimitive;
87import org.openstreetmap.josm.data.osm.IRelation;
88import org.openstreetmap.josm.data.osm.IWay;
89import org.openstreetmap.josm.data.osm.Node;
90import org.openstreetmap.josm.data.osm.OsmPrimitive;
91import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
92import org.openstreetmap.josm.data.osm.Relation;
93import org.openstreetmap.josm.data.osm.Tagged;
94import org.openstreetmap.josm.data.osm.UploadPolicy;
95import org.openstreetmap.josm.data.osm.Way;
96import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
97import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
98import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
99import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
100import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
101import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
102import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache;
103import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
104import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
105import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY;
106import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
107import org.openstreetmap.josm.data.preferences.BooleanProperty;
108import org.openstreetmap.josm.data.preferences.IntegerProperty;
109import org.openstreetmap.josm.data.preferences.NamedColorProperty;
110import org.openstreetmap.josm.data.preferences.StringProperty;
111import org.openstreetmap.josm.data.projection.Projection;
112import org.openstreetmap.josm.data.validation.TestError;
113import org.openstreetmap.josm.gui.ExtendedDialog;
114import org.openstreetmap.josm.gui.MainApplication;
115import org.openstreetmap.josm.gui.MapFrame;
116import org.openstreetmap.josm.gui.MapView;
117import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
118import org.openstreetmap.josm.gui.NavigatableComponent;
119import org.openstreetmap.josm.gui.PrimitiveHoverListener;
120import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
121import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
122import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
123import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
124import org.openstreetmap.josm.gui.io.AbstractIOTask;
125import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
126import org.openstreetmap.josm.gui.io.UploadDialog;
127import org.openstreetmap.josm.gui.io.UploadLayerTask;
128import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
129import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
130import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
131import org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter;
132import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
133import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
134import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
135import org.openstreetmap.josm.gui.progress.ProgressMonitor;
136import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
137import org.openstreetmap.josm.gui.util.GuiHelper;
138import org.openstreetmap.josm.gui.util.LruCache;
139import org.openstreetmap.josm.gui.widgets.FileChooserManager;
140import org.openstreetmap.josm.gui.widgets.JosmTextArea;
141import org.openstreetmap.josm.spi.preferences.Config;
142import org.openstreetmap.josm.tools.AlphanumComparator;
143import org.openstreetmap.josm.tools.CheckParameterUtil;
144import org.openstreetmap.josm.tools.GBC;
145import org.openstreetmap.josm.tools.ImageOverlay;
146import org.openstreetmap.josm.tools.ImageProvider;
147import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
148import org.openstreetmap.josm.tools.Logging;
149import org.openstreetmap.josm.tools.UncheckedParseException;
150import org.openstreetmap.josm.tools.Utils;
151import org.openstreetmap.josm.tools.date.DateUtils;
152
153/**
154 * A layer that holds OSM data from a specific dataset.
155 * The data can be fully edited.
156 *
157 * @author imi
158 * @since 17
159 */
160public class OsmDataLayer extends AbstractOsmDataLayer
161 implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener {
162 private static final int MAX_ZOOM = 30;
163 private static final int OVER_ZOOM = 2;
164 private static final int HATCHED_SIZE = 15;
165 // U+2205 EMPTY SET
166 private static final String IS_EMPTY_SYMBOL = "\u2205";
167 /** Property used to know if this layer has to be uploaded */
168 public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
169
170 private boolean requiresSaveToFile;
171 private boolean requiresUploadToServer;
172 /** Flag used to know if the layer is being uploaded */
173 private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
174 /**
175 * A cache used for painting
176 */
177 private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this));
178 /** The map paint index that was painted (used to invalidate {@link #cache}) */
179 private int lastDataIdx;
180 /** The last zoom level (we invalidate all tiles when switching layers) */
181 private int lastZoom;
182 private boolean hoverListenerAdded;
183
184 /**
185 * List of validation errors in this layer.
186 * @since 3669
187 */
188 public final List<TestError> validationErrors = new ArrayList<>();
189
190 /**
191 * The default number of relations in the recent relations cache.
192 * @see #getRecentRelations()
193 */
194 public static final int DEFAULT_RECENT_RELATIONS_NUMBER = 20;
195 /**
196 * The number of relations to use in the recent relations cache.
197 * @see #getRecentRelations()
198 */
199 public static final IntegerProperty PROPERTY_RECENT_RELATIONS_NUMBER = new IntegerProperty("properties.last-closed-relations-size",
200 DEFAULT_RECENT_RELATIONS_NUMBER);
201 /**
202 * The extension that should be used when saving the OSM file.
203 */
204 public static final StringProperty PROPERTY_SAVE_EXTENSION = new StringProperty("save.extension.osm", "osm");
205
206 /**
207 * Property to determine if labels must be hidden while dragging the map.
208 */
209 public static final BooleanProperty PROPERTY_HIDE_LABELS_WHILE_DRAGGING = new BooleanProperty("mappaint.hide.labels.while.dragging", true);
210
211 private static final NamedColorProperty PROPERTY_BACKGROUND_COLOR = new NamedColorProperty(marktr("background"), Color.BLACK);
212 private static final NamedColorProperty PROPERTY_OUTSIDE_COLOR = new NamedColorProperty(marktr("outside downloaded area"), Color.YELLOW);
213
214 /** List of recent relations */
215 private final Map<Relation, Void> recentRelations = new LruCache<>(PROPERTY_RECENT_RELATIONS_NUMBER.get());
216
217 /**
218 * Returns list of recently closed relations or null if none.
219 * @return list of recently closed relations or <code>null</code> if none
220 * @since 12291 (signature)
221 * @since 9668
222 */
223 public List<Relation> getRecentRelations() {
224 ArrayList<Relation> list = new ArrayList<>(recentRelations.keySet());
225 Collections.reverse(list);
226 return list;
227 }
228
229 /**
230 * Adds recently closed relation.
231 * @param relation new entry for the list of recently closed relations
232 * @see #PROPERTY_RECENT_RELATIONS_NUMBER
233 * @since 9668
234 */
235 public void setRecentRelation(Relation relation) {
236 recentRelations.put(relation, null);
237 MapFrame map = MainApplication.getMap();
238 if (map != null && map.relationListDialog != null) {
239 map.relationListDialog.enableRecentRelations();
240 }
241 }
242
243 /**
244 * Remove relation from list of recent relations.
245 * @param relation relation to remove
246 * @since 9668
247 */
248 public void removeRecentRelation(Relation relation) {
249 recentRelations.remove(relation);
250 MapFrame map = MainApplication.getMap();
251 if (map != null && map.relationListDialog != null) {
252 map.relationListDialog.enableRecentRelations();
253 }
254 }
255
256 protected void setRequiresSaveToFile(boolean newValue) {
257 boolean oldValue = requiresSaveToFile;
258 requiresSaveToFile = newValue;
259 if (oldValue != newValue) {
260 GuiHelper.runInEDT(() ->
261 propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue)
262 );
263 }
264 }
265
266 protected void setRequiresUploadToServer(boolean newValue) {
267 boolean oldValue = requiresUploadToServer;
268 requiresUploadToServer = newValue;
269 if (oldValue != newValue) {
270 GuiHelper.runInEDT(() ->
271 propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue)
272 );
273 }
274 }
275
276 /** the global counter for created data layers */
277 private static final AtomicInteger dataLayerCounter = new AtomicInteger();
278
279 /**
280 * Replies a new unique name for a data layer
281 *
282 * @return a new unique name for a data layer
283 */
284 public static String createNewName() {
285 return createLayerName(dataLayerCounter.incrementAndGet());
286 }
287
288 static String createLayerName(Object arg) {
289 return tr("Data Layer {0}", arg);
290 }
291
292 /**
293 * A listener that counts the number of primitives it encounters
294 */
295 public static final class DataCountVisitor implements OsmPrimitiveVisitor {
296 /**
297 * Nodes that have been visited
298 */
299 public int nodes;
300 /**
301 * Ways that have been visited
302 */
303 public int ways;
304 /**
305 * Relations that have been visited
306 */
307 public int relations;
308 /**
309 * Deleted nodes that have been visited
310 */
311 public int deletedNodes;
312 /**
313 * Deleted ways that have been visited
314 */
315 public int deletedWays;
316 /**
317 * Deleted relations that have been visited
318 */
319 public int deletedRelations;
320 /**
321 * Incomplete nodes that have been visited
322 */
323 public int incompleteNodes;
324 /**
325 * Incomplete ways that have been visited
326 */
327 public int incompleteWays;
328 /**
329 * Incomplete relations that have been visited
330 */
331 public int incompleteRelations;
332
333 @Override
334 public void visit(final Node n) {
335 nodes++;
336 if (n.isDeleted()) {
337 deletedNodes++;
338 }
339 if (n.isIncomplete()) {
340 incompleteNodes++;
341 }
342 }
343
344 @Override
345 public void visit(final Way w) {
346 ways++;
347 if (w.isDeleted()) {
348 deletedWays++;
349 }
350 if (w.isIncomplete()) {
351 incompleteWays++;
352 }
353 }
354
355 @Override
356 public void visit(final Relation r) {
357 relations++;
358 if (r.isDeleted()) {
359 deletedRelations++;
360 }
361 if (r.isIncomplete()) {
362 incompleteRelations++;
363 }
364 }
365 }
366
367 /**
368 * Listener called when a state of this layer has changed.
369 * @since 10600 (functional interface)
370 */
371 @FunctionalInterface
372 public interface LayerStateChangeListener {
373 /**
374 * Notifies that the "upload discouraged" (upload=no) state has changed.
375 * @param layer The layer that has been modified
376 * @param newValue The new value of the state
377 */
378 void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue);
379 }
380
381 private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
382
383 /**
384 * Adds a layer state change listener
385 *
386 * @param listener the listener. Ignored if null or already registered.
387 * @since 5519
388 */
389 public void addLayerStateChangeListener(LayerStateChangeListener listener) {
390 if (listener != null) {
391 layerStateChangeListeners.addIfAbsent(listener);
392 }
393 }
394
395 /**
396 * Removes a layer state change listener
397 *
398 * @param listener the listener. Ignored if null or already registered.
399 * @since 10340
400 */
401 public void removeLayerStateChangeListener(LayerStateChangeListener listener) {
402 layerStateChangeListeners.remove(listener);
403 }
404
405 /**
406 * The data behind this layer.
407 */
408 public final DataSet data;
409 private final DataSetListenerAdapter dataSetListenerAdapter;
410
411 /**
412 * a texture for non-downloaded area
413 */
414 private static volatile BufferedImage hatched;
415
416 static {
417 createHatchTexture();
418 }
419
420 /**
421 * Replies background color for downloaded areas.
422 * @return background color for downloaded areas. Black by default
423 */
424 public static Color getBackgroundColor() {
425 return PROPERTY_BACKGROUND_COLOR.get();
426 }
427
428 /**
429 * Replies background color for non-downloaded areas.
430 * @return background color for non-downloaded areas. Yellow by default
431 */
432 public static Color getOutsideColor() {
433 return PROPERTY_OUTSIDE_COLOR.get();
434 }
435
436 /**
437 * Initialize the hatch pattern used to paint the non-downloaded area
438 */
439 public static void createHatchTexture() {
440 BufferedImage bi = new BufferedImage(HATCHED_SIZE, HATCHED_SIZE, BufferedImage.TYPE_INT_ARGB);
441 Graphics2D big = bi.createGraphics();
442 big.setColor(getBackgroundColor());
443 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
444 big.setComposite(comp);
445 big.fillRect(0, 0, HATCHED_SIZE, HATCHED_SIZE);
446 big.setColor(getOutsideColor());
447 big.drawLine(-1, 6, 6, -1);
448 big.drawLine(4, 16, 16, 4);
449 hatched = bi;
450 }
451
452 /**
453 * Construct a new {@code OsmDataLayer}.
454 * @param data OSM data
455 * @param name Layer name
456 * @param associatedFile Associated .osm file (can be null)
457 */
458 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
459 super(name);
460 CheckParameterUtil.ensureParameterNotNull(data, "data");
461 this.data = data;
462 this.data.setName(name);
463 this.dataSetListenerAdapter = new DataSetListenerAdapter(this);
464 this.setAssociatedFile(associatedFile);
465 data.addDataSetListener(dataSetListenerAdapter);
466 data.addDataSetListener(MultipolygonCache.getInstance());
467 data.addHighlightUpdateListener(this);
468 data.addSelectionListener(this);
469 if (name != null && name.startsWith(createLayerName("")) && Character.isDigit(
470 (name.substring(createLayerName("").length()) + "XX" /*avoid StringIndexOutOfBoundsException*/).charAt(1))) {
471 while (AlphanumComparator.getInstance().compare(createLayerName(dataLayerCounter), name) < 0) {
472 final int i = dataLayerCounter.incrementAndGet();
473 if (i > 1_000_000) {
474 break; // to avoid looping in unforeseen case
475 }
476 }
477 }
478 }
479
480 /**
481 * Returns the {@link DataSet} behind this layer.
482 * @return the {@link DataSet} behind this layer.
483 * @since 13558
484 */
485 @Override
486 public DataSet getDataSet() {
487 return data;
488 }
489
490 /**
491 * Return the image provider to get the base icon
492 * @return image provider class which can be modified
493 * @since 8323
494 */
495 protected ImageProvider getBaseIconProvider() {
496 return new ImageProvider("layer", "osmdata_small");
497 }
498
499 @Override
500 public Icon getIcon() {
501 ImageProvider base = getBaseIconProvider().setMaxSize(ImageSizes.LAYER);
502 if (data.getDownloadPolicy() != null && data.getDownloadPolicy() != DownloadPolicy.NORMAL) {
503 base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.0, 1.0, 0.5));
504 }
505 if (data.getUploadPolicy() != null && data.getUploadPolicy() != UploadPolicy.NORMAL) {
506 base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0));
507 }
508
509 if (isUploadInProgress()) {
510 // If the layer is being uploaded then change the default icon to a clock
511 base = new ImageProvider("clock").setMaxSize(ImageSizes.LAYER);
512 } else if (isLocked()) {
513 // If the layer is read only then change the default icon to a lock
514 base = new ImageProvider("lock").setMaxSize(ImageSizes.LAYER);
515 }
516 return base.get();
517 }
518
519 /**
520 * Draw all primitives in this layer but do not draw modified ones (they
521 * are drawn by the edit layer).
522 * Draw nodes last to overlap the ways they belong to.
523 */
524 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
525 if (!hoverListenerAdded) {
526 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
527 hoverListenerAdded = true;
528 }
529 boolean active = mv.getLayerManager().getActiveLayer() == this;
530 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
531 boolean virtual = !inactive && mv.isVirtualNodesEnabled();
532 paintHatch(g, mv, active);
533 paintData(g, mv, box, inactive, virtual);
534 }
535
536 private void paintHatch(final Graphics2D g, final MapView mv, boolean active) {
537 // draw the hatched area for non-downloaded region. only draw if we're the active
538 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
539 if (active && Boolean.TRUE.equals(DrawingPreference.SOURCE_BOUNDS_PROP.get()) && !data.getDataSources().isEmpty()) {
540 // initialize area with current viewport
541 Rectangle b = mv.getBounds();
542 // on some platforms viewport bounds seem to be offset from the left,
543 // over-grow it just to be sure
544 b.grow(100, 100);
545 Path2D p = new Path2D.Double();
546
547 // combine successively downloaded areas
548 for (Bounds bounds : data.getDataSourceBounds()) {
549 if (bounds.isCollapsed()) {
550 continue;
551 }
552 p.append(mv.getState().getArea(bounds), false);
553 }
554 // subtract combined areas
555 Area a = new Area(b);
556 a.subtract(new Area(p));
557
558 // paint remainder
559 MapViewPoint anchor = mv.getState().getPointFor(new EastNorth(0, 0));
560 Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getInView().getX() % HATCHED_SIZE,
561 anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
562 if (hatched != null) {
563 g.setPaint(new TexturePaint(hatched, anchorRect));
564 }
565 try {
566 g.fill(a);
567 } catch (ArrayIndexOutOfBoundsException e) {
568 // #16686 - AIOOBE in java.awt.TexturePaintContext$Int.setRaster
569 Logging.error(e);
570 }
571 }
572 }
573
574 private void paintData(final Graphics2D g, final MapView mv, Bounds box, boolean inactive, boolean virtual) {
575 // Used to invalidate cache
576 int zoom = getZoom(mv);
577 if (zoom != lastZoom) {
578 // We just mark the previous zoom as dirty before moving in.
579 // It means we don't have to traverse up/down z-levels marking tiles as dirty (this can get *very* expensive).
580 this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*")
581 .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty()));
582 }
583 lastZoom = zoom;
584 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
585 if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) {
586 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
587 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
588 } else {
589 StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter;
590 renderer.setCache(box, this.cache, zoom, (tile) -> {
591 /* This causes "bouncing". I'm not certain why.
592 if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState())
593 final Point upperLeft = mv.getPoint(tile);
594 final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1));
595 GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y));
596 }
597 */
598 // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles
599 MainApplication.worker.submit(this::invalidate);
600 });
601
602 if (this.data.getMappaintCacheIndex() != this.lastDataIdx) {
603 this.cache.clear();
604 this.lastDataIdx = this.data.getMappaintCacheIndex();
605 Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName());
606 }
607 }
608 painter.render(this.data, virtual, box);
609 MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
610 }
611
612 @Override public String getToolTipText() {
613 DataCountVisitor counter = new DataCountVisitor();
614 for (final OsmPrimitive osm : data.allPrimitives()) {
615 osm.accept(counter);
616 }
617 int nodes = counter.nodes - counter.deletedNodes;
618 int ways = counter.ways - counter.deletedWays;
619 int rels = counter.relations - counter.deletedRelations;
620
621 StringBuilder tooltip = new StringBuilder("<html>")
622 .append(trn("{0} node", "{0} nodes", nodes, nodes))
623 .append("<br>")
624 .append(trn("{0} way", "{0} ways", ways, ways))
625 .append("<br>")
626 .append(trn("{0} relation", "{0} relations", rels, rels));
627
628 File f = getAssociatedFile();
629 if (f != null) {
630 tooltip.append("<br>").append(f.getPath());
631 }
632 tooltip.append("</html>");
633 return tooltip.toString();
634 }
635
636 @Override public void mergeFrom(final Layer from) {
637 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
638 monitor.setCancelable(false);
639 if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
640 setUploadDiscouraged(true);
641 }
642 mergeFrom(((OsmDataLayer) from).data, monitor);
643 monitor.close();
644 }
645
646 /**
647 * merges the primitives in dataset <code>from</code> into the dataset of
648 * this layer
649 *
650 * @param from the source data set
651 */
652 public void mergeFrom(final DataSet from) {
653 mergeFrom(from, null);
654 }
655
656 /**
657 * merges the primitives in dataset <code>from</code> into the dataset of this layer
658 *
659 * @param from the source data set
660 * @param progressMonitor the progress monitor, can be {@code null}
661 */
662 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
663 final DataSetMerger visitor = new DataSetMerger(data, from);
664 try {
665 visitor.merge(progressMonitor);
666 } catch (DataIntegrityProblemException e) {
667 Logging.error(e);
668 JOptionPane.showMessageDialog(
669 MainApplication.getMainFrame(),
670 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
671 tr("Error"),
672 JOptionPane.ERROR_MESSAGE
673 );
674 return;
675 }
676
677 int numNewConflicts = 0;
678 for (Conflict<?> c : visitor.getConflicts()) {
679 if (!data.getConflicts().hasConflict(c)) {
680 numNewConflicts++;
681 data.getConflicts().add(c);
682 }
683 }
684 // repaint to make sure new data is displayed properly.
685 invalidate();
686 // warn about new conflicts
687 MapFrame map = MainApplication.getMap();
688 if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
689 map.conflictDialog.warnNumNewConflicts(numNewConflicts);
690 }
691 }
692
693 @Override
694 public boolean isMergable(final Layer other) {
695 // allow merging between normal layers and discouraged layers with a warning (see #7684)
696 return other instanceof OsmDataLayer;
697 }
698
699 @Override
700 public void visitBoundingBox(final BoundingXYVisitor v) {
701 for (final Node n: data.getNodes()) {
702 if (n.isUsable()) {
703 v.visit(n);
704 }
705 }
706 }
707
708 /**
709 * Clean out the data behind the layer. This means clearing the redo/undo lists,
710 * really deleting all deleted objects and reset the modified flags. This should
711 * be done after an upload, even after a partial upload.
712 *
713 * @param processed A list of all objects that were actually uploaded.
714 * May be <code>null</code>, which means nothing has been uploaded
715 */
716 public void cleanupAfterUpload(final Collection<? extends IPrimitive> processed) {
717 // return immediately if an upload attempt failed
718 if (Utils.isEmpty(processed))
719 return;
720
721 UndoRedoHandler.getInstance().clean(data);
722
723 // if uploaded, clean the modified flags as well
724 data.cleanupDeletedPrimitives();
725 data.update(() -> {
726 for (OsmPrimitive p: data.allPrimitives()) {
727 if (processed.contains(p)) {
728 p.setModified(false);
729 }
730 }
731 });
732 }
733
734 private static String counterText(String text, int deleted, int incomplete) {
735 StringBuilder sb = new StringBuilder(text);
736 if (deleted > 0 || incomplete > 0) {
737 sb.append(" (");
738 if (deleted > 0) {
739 sb.append(trn("{0} deleted", "{0} deleted", deleted, deleted));
740 }
741 if (deleted > 0 && incomplete > 0) {
742 sb.append(", ");
743 }
744 if (incomplete > 0) {
745 sb.append(trn("{0} incomplete", "{0} incomplete", incomplete, incomplete));
746 }
747 sb.append(')');
748 }
749 return sb.toString();
750 }
751
752 @Override
753 public Object getInfoComponent() {
754 final DataCountVisitor counter = new DataCountVisitor();
755 for (final OsmPrimitive osm : data.allPrimitives()) {
756 osm.accept(counter);
757 }
758 final JPanel p = new JPanel(new GridBagLayout());
759
760 String nodeText = counterText(trn("{0} node", "{0} nodes", counter.nodes, counter.nodes),
761 counter.deletedNodes, counter.incompleteNodes);
762 String wayText = counterText(trn("{0} way", "{0} ways", counter.ways, counter.ways),
763 counter.deletedWays, counter.incompleteWays);
764 String relationText = counterText(trn("{0} relation", "{0} relations", counter.relations, counter.relations),
765 counter.deletedRelations, counter.incompleteRelations);
766
767 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
768 p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
769 p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
770 p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
771 p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))),
772 GBC.eop().insets(15, 0, 0, 0));
773 addConditionalInformation(p, tr("Layer is locked"), isLocked());
774 addConditionalInformation(p, tr("Download is blocked"), data.getDownloadPolicy() == DownloadPolicy.BLOCKED);
775 addConditionalInformation(p, tr("Upload is discouraged"), isUploadDiscouraged());
776 addConditionalInformation(p, tr("Upload is blocked"), data.getUploadPolicy() == UploadPolicy.BLOCKED);
777 addConditionalInformation(p, IS_EMPTY_SYMBOL + " " + tr("Empty layer"), this.getDataSet().isEmpty());
778 addConditionalInformation(p, IS_DIRTY_SYMBOL + " " + tr("Unsaved changes"), this.isDirty());
779
780 return p;
781 }
782
783 private static void addConditionalInformation(JPanel p, String text, boolean condition) {
784 if (condition) {
785 p.add(new JLabel(text), GBC.eop().insets(15, 0, 0, 0));
786 }
787 }
788
789 @Override
790 public Action[] getMenuEntries() {
791 List<Action> actions = new ArrayList<>(Arrays.asList(
792 LayerListDialog.getInstance().createActivateLayerAction(this),
793 LayerListDialog.getInstance().createShowHideLayerAction(),
794 MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER),
795 LayerListDialog.getInstance().createDeleteLayerAction(),
796 SeparatorLayerAction.INSTANCE,
797 LayerListDialog.getInstance().createMergeLayerAction(this),
798 LayerListDialog.getInstance().createDuplicateLayerAction(this),
799 new LayerSaveAction(this),
800 new LayerSaveAsAction(this)));
801 if (ExpertToggleAction.isExpert()) {
802 actions.addAll(Arrays.asList(
803 new LayerGpxExportAction(this),
804 new ConvertToGpxLayerAction()));
805 }
806 actions.addAll(Arrays.asList(
807 SeparatorLayerAction.INSTANCE,
808 new RenameLayerAction(getAssociatedFile(), this)));
809 if (ExpertToggleAction.isExpert()) {
810 actions.add(new ToggleUploadDiscouragedLayerAction(this));
811 }
812 actions.addAll(Arrays.asList(
813 new ConsistencyTestAction(),
814 SeparatorLayerAction.INSTANCE,
815 new LayerListPopup.InfoAction(this)));
816 return actions.toArray(new Action[0]);
817 }
818
819 /**
820 * Converts given OSM dataset to GPX data.
821 * @param data OSM dataset
822 * @param file output .gpx file
823 * @return GPX data
824 */
825 public static GpxData toGpxData(DataSet data, File file) {
826 GpxData gpxData = new GpxData(true);
827 fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX);
828 gpxData.endUpdate();
829 return gpxData;
830 }
831
832 protected static void fillGpxData(GpxData gpxData, DataSet data, File file, String gpxPrefix) {
833 if (data.getGPXNamespaces() != null) {
834 gpxData.getNamespaces().addAll(data.getGPXNamespaces());
835 }
836 gpxData.storageFile = file;
837 Set<Node> doneNodes = new HashSet<>();
838 waysToGpxData(data.getWays(), gpxData, doneNodes, gpxPrefix);
839 nodesToGpxData(data.getNodes(), gpxData, doneNodes, gpxPrefix);
840 }
841
842 private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
843 /* When the dataset has been obtained from a gpx layer and now is being converted back,
844 * the ways have negative ids. The first created way corresponds to the first gpx segment,
845 * and has the highest id (i.e., closest to zero).
846 * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
847 * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
848 */
849 ways.stream()
850 .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
851 .forEachOrdered(w -> {
852 if (!w.isUsable()) {
853 return;
854 }
855 List<IGpxTrackSegment> trk = new ArrayList<>();
856 Map<String, Object> trkAttr = new HashMap<>();
857
858 GpxExtensionCollection trkExts = new GpxExtensionCollection();
859 GpxExtensionCollection segExts = new GpxExtensionCollection();
860 for (Entry<String, String> e : w.getKeys().entrySet()) {
861 String k = e.getKey().startsWith(gpxPrefix) ? e.getKey().substring(gpxPrefix.length()) : e.getKey();
862 String v = e.getValue();
863 if (GpxConstants.RTE_TRK_KEYS.contains(k)) {
864 trkAttr.put(k, v);
865 } else {
866 k = GpxConstants.EXTENSION_ABBREVIATIONS.entrySet()
867 .stream()
868 .filter(s -> s.getValue().equals(e.getKey()))
869 .map(s -> s.getKey().substring(gpxPrefix.length()))
870 .findAny()
871 .orElse(k);
872 if (k.startsWith("extension")) {
873 String[] chain = k.split(":", -1);
874 if (chain.length >= 3 && "segment".equals(chain[2])) {
875 segExts.addFlat(chain, v);
876 } else {
877 trkExts.addFlat(chain, v);
878 }
879 }
880
881 }
882 }
883 List<WayPoint> trkseg = new ArrayList<>();
884 for (Node n : w.getNodes()) {
885 if (!n.isUsable()) {
886 if (!trkseg.isEmpty()) {
887 trk.add(new GpxTrackSegment(trkseg));
888 trkseg.clear();
889 }
890 continue;
891 }
892 if (!n.isTagged() || containsOnlyGpxTags(n, gpxPrefix)) {
893 doneNodes.add(n);
894 }
895 trkseg.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
896 }
897 trk.add(new GpxTrackSegment(trkseg));
898 trk.forEach(gpxseg -> gpxseg.getExtensions().addAll(segExts));
899 GpxTrack gpxtrk = new GpxTrack(trk, trkAttr);
900 gpxtrk.getExtensions().addAll(trkExts);
901 gpxData.addTrack(gpxtrk);
902 });
903 }
904
905 private static boolean containsOnlyGpxTags(Tagged t, String gpxPrefix) {
906 return t.keys()
907 .allMatch(key -> GpxConstants.WPT_KEYS.contains(key) || key.startsWith(gpxPrefix));
908 }
909
910 /**
911 * Reads the Gpx key from the given {@link OsmPrimitive}, with or without &quot;gpx:&quot; prefix
912 * @param prim OSM primitive
913 * @param gpxPrefix the GPX prefix
914 * @param key GPX key without prefix
915 * @return the value or <code>null</code> if not present
916 */
917 private static String gpxVal(OsmPrimitive prim, String gpxPrefix, String key) {
918 String val = prim.get(gpxPrefix + key);
919 return val != null ? val : prim.get(key);
920 }
921
922 /**
923 * Converts a node to a waypoint with default {@link GpxConstants#GPX_PREFIX} for tags.
924 * @param n the {@code Node} to convert
925 * @param time a timestamp value in milliseconds from the epoch.
926 * @return {@code WayPoint} object
927 * @since 13210
928 */
929 public static WayPoint nodeToWayPoint(Node n, long time) {
930 return nodeToWayPoint(n, time, GpxConstants.GPX_PREFIX);
931 }
932
933 /**
934 * Converts a node to a waypoint with a configurable GPX prefix for tags.
935 * @param n the {@code Node} to convert
936 * @param time a timestamp value in milliseconds from the epoch.
937 * @param gpxPrefix the GPX prefix for tags
938 * @return {@code WayPoint} object
939 * @since 18078
940 */
941 public static WayPoint nodeToWayPoint(Node n, long time, String gpxPrefix) {
942 WayPoint wpt = new WayPoint(n.getCoor());
943
944 // Position info
945
946 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_ELE, null);
947
948 try {
949 String v;
950 if (time > Long.MIN_VALUE) {
951 wpt.setTimeInMillis(time);
952 } else if ((v = gpxVal(n, gpxPrefix, GpxConstants.PT_TIME)) != null) {
953 wpt.setInstant(DateUtils.parseInstant(v));
954 } else if (!n.isTimestampEmpty()) {
955 wpt.setInstant(n.getInstant());
956 }
957 } catch (UncheckedParseException | DateTimeException e) {
958 Logging.error(e);
959 }
960
961 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_MAGVAR, null);
962 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_GEOIDHEIGHT, null);
963
964 // Description info
965
966 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_NAME, null, null);
967 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_DESC, "description", null);
968 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_CMT, "comment", null);
969 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.GPX_SRC, "source", "source:position");
970
971 Collection<GpxLink> links = Stream.of("link", "url", "website", "contact:website")
972 .map(key -> gpxVal(n, gpxPrefix, key))
973 .filter(Objects::nonNull)
974 .map(GpxLink::new)
975 .collect(Collectors.toList());
976 wpt.put(GpxConstants.META_LINKS, links);
977
978 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SYM, "wpt_symbol", null);
979 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_TYPE, null, null);
980
981 // Angle info
982 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_COURSE, "gps:course");
983
984 // Accuracy info
985 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_FIX, "gps:fix", null);
986 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SAT, "gps:sat");
987 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_HDOP, "gps:hdop");
988 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_VDOP, "gps:vdop");
989 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_PDOP, "gps:pdop");
990 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_STD_HDEV, "gps:stdhdev");
991 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_STD_VDEV, "gps:stdvdev");
992 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
993 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_DGPSID, "gps:dgpsid");
994
995 return wpt;
996 }
997
998 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
999 List<Node> sortedNodes = new ArrayList<>(nodes);
1000 sortedNodes.removeAll(doneNodes);
1001 Collections.sort(sortedNodes);
1002 for (Node n : sortedNodes) {
1003 if (n.isIncomplete() || n.isDeleted()) {
1004 continue;
1005 }
1006 gpxData.waypoints.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
1007 }
1008 }
1009
1010 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
1011 String value = gpxVal(p, gpxPrefix, gpxKey);
1012 if (value == null && osmKey != null) {
1013 value = gpxVal(p, gpxPrefix, osmKey);
1014 }
1015 if (value != null) {
1016 try {
1017 final int i = Integer.parseInt(value);
1018 // Sanity checks
1019 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
1020 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
1021 wpt.put(gpxKey, i);
1022 }
1023 } catch (NumberFormatException e) {
1024 Logging.trace(e);
1025 }
1026 }
1027 }
1028
1029 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
1030 String value = gpxVal(p, gpxPrefix, gpxKey);
1031 if (value == null && osmKey != null) {
1032 value = gpxVal(p, gpxPrefix, osmKey);
1033 }
1034 if (value != null) {
1035 try {
1036 final double d = Double.parseDouble(value);
1037 // Sanity checks
1038 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
1039 wpt.put(gpxKey, d);
1040 }
1041 } catch (NumberFormatException e) {
1042 Logging.trace(e);
1043 }
1044 }
1045 }
1046
1047 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey, String osmKey2) {
1048 String value = gpxVal(p, gpxPrefix, gpxKey);
1049 if (value == null && osmKey != null) {
1050 value = gpxVal(p, gpxPrefix, osmKey);
1051 }
1052 if (value == null && osmKey2 != null) {
1053 value = gpxVal(p, gpxPrefix, osmKey2);
1054 }
1055 // Sanity checks
1056 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
1057 wpt.put(gpxKey, value);
1058 }
1059 }
1060
1061 /**
1062 * Converts OSM data behind this layer to GPX data.
1063 * @return GPX data
1064 */
1065 public GpxData toGpxData() {
1066 return toGpxData(data, getAssociatedFile());
1067 }
1068
1069 /**
1070 * Action that converts this OSM layer to a GPX layer.
1071 */
1072 public class ConvertToGpxLayerAction extends AbstractAction {
1073 /**
1074 * Constructs a new {@code ConvertToGpxLayerAction}.
1075 */
1076 public ConvertToGpxLayerAction() {
1077 super(tr("Convert to GPX layer"));
1078 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
1079 putValue("help", ht("/Action/ConvertToGpxLayer"));
1080 }
1081
1082 @Override
1083 public void actionPerformed(ActionEvent e) {
1084 String name = getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
1085 final GpxData gpxData = toGpxData();
1086 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", name), true);
1087 if (getAssociatedFile() != null) {
1088 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
1089 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
1090 gpxLayer.getGpxData().setModified(true);
1091 }
1092 MainApplication.getLayerManager().addLayer(gpxLayer, false);
1093 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
1094 MainApplication.getLayerManager().addLayer(
1095 new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false);
1096 }
1097 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
1098 }
1099 }
1100
1101 /**
1102 * Determines if this layer contains data at the given coordinate.
1103 * @param coor the coordinate
1104 * @return {@code true} if data sources bounding boxes contain {@code coor}
1105 */
1106 public boolean containsPoint(LatLon coor) {
1107 // we'll assume that if this has no data sources
1108 // that it also has no borders
1109 if (this.data.getDataSources().isEmpty())
1110 return true;
1111
1112 return this.data.getDataSources().stream()
1113 .anyMatch(src -> src.bounds.contains(coor));
1114 }
1115
1116 /**
1117 * Replies the set of conflicts currently managed in this layer.
1118 *
1119 * @return the set of conflicts currently managed in this layer
1120 */
1121 public ConflictCollection getConflicts() {
1122 return data.getConflicts();
1123 }
1124
1125 @Override
1126 public boolean isDownloadable() {
1127 return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked();
1128 }
1129
1130 @Override
1131 public boolean isUploadable() {
1132 return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked();
1133 }
1134
1135 @Override
1136 public boolean requiresUploadToServer() {
1137 return isUploadable() && requiresUploadToServer;
1138 }
1139
1140 @Override
1141 public boolean requiresSaveToFile() {
1142 return getAssociatedFile() != null && requiresSaveToFile;
1143 }
1144
1145 @Override
1146 public String getLabel() {
1147 String label = super.getLabel();
1148 if (this.isDirty()) {
1149 label += " " + IS_DIRTY_SYMBOL;
1150 }
1151 if (this.getDataSet().isEmpty()) {
1152 label += " " + IS_EMPTY_SYMBOL;
1153 }
1154 return label;
1155 }
1156
1157 @Override
1158 public void onPostLoadFromFile() {
1159 setRequiresSaveToFile(false);
1160 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1161 invalidate();
1162 }
1163
1164 /**
1165 * Actions run after data has been downloaded to this layer.
1166 */
1167 public void onPostDownloadFromServer() {
1168 setRequiresSaveToFile(true);
1169 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1170 invalidate();
1171 }
1172
1173 @Override
1174 public void onPostSaveToFile() {
1175 setRequiresSaveToFile(false);
1176 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1177 }
1178
1179 @Override
1180 public void onPostUploadToServer() {
1181 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
1182 // keep requiresSaveToDisk unchanged
1183 }
1184
1185 private class ConsistencyTestAction extends AbstractAction {
1186
1187 ConsistencyTestAction() {
1188 super(tr("Dataset consistency test"));
1189 }
1190
1191 @Override
1192 public void actionPerformed(ActionEvent e) {
1193 String result = DatasetConsistencyTest.runTests(data);
1194 if (result.isEmpty()) {
1195 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No problems found"));
1196 } else {
1197 JPanel p = new JPanel(new GridBagLayout());
1198 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
1199 JosmTextArea info = new JosmTextArea(result, 20, 60);
1200 info.setCaretPosition(0);
1201 info.setEditable(false);
1202 p.add(new JScrollPane(info), GBC.eop());
1203
1204 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1205 }
1206 }
1207 }
1208
1209 @Override
1210 public synchronized void destroy() {
1211 super.destroy();
1212 data.removeSelectionListener(this);
1213 data.removeHighlightUpdateListener(this);
1214 data.removeDataSetListener(dataSetListenerAdapter);
1215 data.removeDataSetListener(MultipolygonCache.getInstance());
1216 data.clearSelection();
1217 validationErrors.clear();
1218 removeClipboardDataFor(this);
1219 recentRelations.clear();
1220 if (hoverListenerAdded) {
1221 hoverListenerAdded = false;
1222 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
1223 }
1224 }
1225
1226 protected static void removeClipboardDataFor(OsmDataLayer osm) {
1227 Transferable clipboardContents = ClipboardUtils.getClipboardContent();
1228 if (clipboardContents != null && clipboardContents.isDataFlavorSupported(OsmLayerTransferData.OSM_FLAVOR)) {
1229 try {
1230 Object o = clipboardContents.getTransferData(OsmLayerTransferData.OSM_FLAVOR);
1231 if (o instanceof OsmLayerTransferData && osm.equals(((OsmLayerTransferData) o).getLayer())) {
1232 ClipboardUtils.clear();
1233 }
1234 } catch (UnsupportedFlavorException | IOException e) {
1235 Logging.error(e);
1236 }
1237 }
1238 }
1239
1240 @Override
1241 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1242 resetTiles(event.getPrimitives());
1243 invalidate();
1244 setRequiresSaveToFile(true);
1245 setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
1246 }
1247
1248 @Override
1249 public void selectionChanged(SelectionChangeEvent event) {
1250 Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
1251 primitives.addAll(event.getRemoved());
1252 resetTiles(primitives);
1253 invalidate();
1254 }
1255
1256 private void resetTiles(Collection<? extends IPrimitive> primitives) {
1257 // Clear the cache if we aren't using tiles. And return.
1258 if (!MapRendererFactory.getInstance().isMapRendererActive(StyledTiledMapRenderer.class)) {
1259 this.cache.clear();
1260 return;
1261 }
1262 // Don't use anything that uses filtered collections. It becomes slow at large datasets.
1263 if (primitives.size() > 100) {
1264 dirtyAll();
1265 return;
1266 }
1267 if (primitives.size() < 5) {
1268 for (IPrimitive p : primitives) {
1269 resetTiles(p);
1270 }
1271 return;
1272 }
1273 // Most of the time, a selection is going to be a big box.
1274 // So we want to optimize for that case.
1275 BBox box = null;
1276 for (IPrimitive primitive : primitives) {
1277 if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue;
1278 final Collection<? extends IPrimitive> referrers = primitive.getReferrers();
1279 if (box == null) {
1280 box = new BBox(primitive.getBBox());
1281 } else {
1282 box.addPrimitive(primitive, 0);
1283 }
1284 for (IPrimitive referrer : referrers) {
1285 box.addPrimitive(referrer, 0);
1286 }
1287 }
1288 if (box != null) {
1289 resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon());
1290 }
1291 }
1292
1293 private void resetTiles(IPrimitive p) {
1294 if (p instanceof INode) {
1295 resetBounds(getInvalidatedBBox((INode) p, null));
1296 } else if (p instanceof IWay) {
1297 IWay<?> way = (IWay<?>) p;
1298 for (int i = 0; i < way.getNodesCount() - 1; i++) {
1299 resetBounds(getInvalidatedBBox(way.getNode(i), way.getNode(i + 1)));
1300 }
1301 } else if (p instanceof IRelation<?>) {
1302 for (IPrimitive member : ((IRelation<?>) p).getMemberPrimitivesList()) {
1303 if (member instanceof IRelation) {
1304 resetBounds(member.getBBox()); // Avoid recursive relation issues
1305 break;
1306 } else {
1307 resetTiles(member);
1308 }
1309 }
1310 } else {
1311 throw new IllegalArgumentException("Unsupported primitive type: " + p.getClass().getName());
1312 }
1313 }
1314
1315 private BBox getInvalidatedBBox(INode first, INode second) {
1316 final BBox bbox = new BBox(first);
1317 if (second != null) {
1318 bbox.add(second);
1319 }
1320 return bbox;
1321 }
1322
1323 private void resetBounds(IBounds bbox) {
1324 resetBounds(bbox.getMinLat(), bbox.getMinLon(), bbox.getMaxLat(), bbox.getMaxLon());
1325 }
1326
1327 private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) {
1328 // Get the current zoom. Hopefully we aren't painting with a different navigatable component
1329 final int currentZoom = lastZoom;
1330 final AtomicInteger counter = new AtomicInteger();
1331 TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).limit(100).forEach(tile -> {
1332 final ImageCache imageCache = this.cache.get(tile);
1333 if (imageCache != null && !imageCache.isDirty()) {
1334 this.cache.put(tile, imageCache.becomeDirty());
1335 }
1336 counter.incrementAndGet();
1337 });
1338 if (counter.get() > 100) {
1339 dirtyAll();
1340 }
1341 }
1342
1343 private void dirtyAll() {
1344 this.cache.getMatching(".*").forEach((key, value) -> {
1345 this.cache.remove(key);
1346 this.cache.put(key, value.becomeDirty());
1347 });
1348 }
1349
1350 /**
1351 * Get the zoom for a {@link NavigatableComponent}
1352 * @param navigatableComponent The component to get the zoom from
1353 * @return The zoom for the navigatable component
1354 */
1355 private static int getZoom(NavigatableComponent navigatableComponent) {
1356 final double scale = navigatableComponent.getScale();
1357 // We might have to fall back to the old method if user is reprojecting
1358 // 256 is the "target" size, (TODO check HiDPI!)
1359 final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256);
1360 CheckParameterUtil.ensureThat(targetSize > 0, "mappaint.fast_render.tile_size should be > 0 (default 256)");
1361 final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize;
1362 int zoom;
1363 for (zoom = 0; zoom < MAX_ZOOM; zoom++) { // Use something like imagery.{generic|tms}.max_zoom_lvl (20 is a bit too low for our needs)
1364 if (scale > topResolution / Math.pow(2, zoom)) {
1365 zoom = zoom > 0 ? zoom - 1 : zoom;
1366 break;
1367 }
1368 }
1369 // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be
1370 // 64px square).
1371 zoom += OVER_ZOOM;
1372 return zoom;
1373 }
1374
1375 @Override
1376 public void projectionChanged(Projection oldValue, Projection newValue) {
1377 // No reprojection required. The dataset itself is registered as projection
1378 // change listener and already got notified.
1379 }
1380
1381 @Override
1382 public final boolean isUploadDiscouraged() {
1383 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
1384 }
1385
1386 /**
1387 * Sets the "discouraged upload" flag.
1388 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1389 * This feature allows to use "private" data layers.
1390 */
1391 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
1392 if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
1393 (uploadDiscouraged ^ isUploadDiscouraged())) {
1394 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
1395 setRequiresSaveToFile(true);
1396 for (LayerStateChangeListener l : layerStateChangeListeners) {
1397 l.uploadDiscouragedChanged(this, uploadDiscouraged);
1398 }
1399 }
1400 }
1401
1402 @Override
1403 public final boolean isModified() {
1404 return data.isModified();
1405 }
1406
1407 @Override
1408 public boolean isSavable() {
1409 return true; // With OsmExporter
1410 }
1411
1412 @Override
1413 public boolean checkSaveConditions() {
1414 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() ->
1415 new ExtendedDialog(
1416 MainApplication.getMainFrame(),
1417 tr("Empty layer"),
1418 tr("Save anyway"), tr("Cancel"))
1419 .setContent(tr("The layer contains no data."))
1420 .setButtonIcons("save", "cancel")
1421 .showDialog().getValue()
1422 )) {
1423 return false;
1424 }
1425
1426 ConflictCollection conflictsCol = getConflicts();
1427 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1428 new ExtendedDialog(
1429 MainApplication.getMainFrame(),
1430 /* I18N: Display title of the window showing conflicts */
1431 tr("Conflicts"),
1432 tr("Reject Conflicts and Save"), tr("Cancel"))
1433 .setContent(
1434 tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"))
1435 .setButtonIcons("save", "cancel")
1436 .showDialog().getValue()
1437 );
1438 }
1439
1440 /**
1441 * Check the data set if it would be empty on save. It is empty, if it contains
1442 * no objects (after all objects that are created and deleted without being
1443 * transferred to the server have been removed).
1444 *
1445 * @return <code>true</code>, if a save result in an empty data set.
1446 */
1447 private boolean isDataSetEmpty() {
1448 return data == null || data.allNonDeletedPrimitives().stream()
1449 .allMatch(osm -> osm.isDeleted() && osm.isNewOrUndeleted());
1450 }
1451
1452 @Override
1453 public File createAndOpenSaveFileChooser() {
1454 String extension = PROPERTY_SAVE_EXTENSION.get();
1455 File file = getAssociatedFile();
1456 if (file == null && isRenamed()) {
1457 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
1458 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1459 filename.append('.').append(extension);
1460 }
1461 file = new File(filename.toString());
1462 }
1463 return new FileChooserManager()
1464 .title(tr("Save OSM file"))
1465 .extension(extension)
1466 .file(file)
1467 .additionalTypes(t -> t != WMSLayerImporter.FILE_FILTER && t != NoteExporter.FILE_FILTER && t != ValidatorErrorExporter.FILE_FILTER)
1468 .getFileForSave();
1469 }
1470
1471 @Override
1472 public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
1473 UploadDialog dialog = UploadDialog.getUploadDialog();
1474 return new UploadLayerTask(
1475 dialog.getUploadStrategySpecification(),
1476 this,
1477 monitor,
1478 dialog.getChangeset());
1479 }
1480
1481 @Override
1482 public String getChangesetSourceTag() {
1483 return this.data.getChangeSetTags().getOrDefault("source", null);
1484 }
1485
1486 @Override
1487 public AbstractUploadDialog getUploadDialog() {
1488 UploadDialog dialog = UploadDialog.getUploadDialog();
1489 dialog.setUploadedPrimitives(new APIDataSet(data));
1490 return dialog;
1491 }
1492
1493 @Override
1494 public ProjectionBounds getViewProjectionBounds() {
1495 BoundingXYVisitor v = new BoundingXYVisitor();
1496 v.visit(data.getDataSourceBoundingBox());
1497 if (!v.hasExtend()) {
1498 v.computeBoundingBox(data.getNodes());
1499 }
1500 return v.getBounds();
1501 }
1502
1503 @Override
1504 public void highlightUpdated(HighlightUpdateEvent e) {
1505 invalidate();
1506 }
1507
1508 @Override
1509 public void primitiveHovered(PrimitiveHoverEvent e) {
1510 List<IPrimitive> primitives = new ArrayList<>(2);
1511 primitives.add(e.getHoveredPrimitive());
1512 primitives.add(e.getPreviousPrimitive());
1513 primitives.removeIf(Objects::isNull);
1514 resetTiles(primitives);
1515 this.invalidate();
1516 }
1517
1518 @Override
1519 public void setName(String name) {
1520 if (data != null) {
1521 data.setName(name);
1522 }
1523 super.setName(name);
1524 }
1525
1526 /**
1527 * Sets the "upload in progress" flag, which will result in displaying a new icon and forbid to remove the layer.
1528 * @since 13434
1529 */
1530 public void setUploadInProgress() {
1531 if (!isUploadInProgress.compareAndSet(false, true)) {
1532 Logging.warn("Trying to set uploadInProgress flag on layer already being uploaded ", getName());
1533 }
1534 }
1535
1536 /**
1537 * Unsets the "upload in progress" flag, which will result in displaying the standard icon and allow to remove the layer.
1538 * @since 13434
1539 */
1540 public void unsetUploadInProgress() {
1541 if (!isUploadInProgress.compareAndSet(true, false)) {
1542 Logging.warn("Trying to unset uploadInProgress flag on layer not being uploaded ", getName());
1543 }
1544 }
1545
1546 @Override
1547 public boolean isUploadInProgress() {
1548 return isUploadInProgress.get();
1549 }
1550
1551 @Override
1552 public Data getData() {
1553 return getDataSet();
1554 }
1555
1556 @Override
1557 public boolean autosave(File file) throws IOException {
1558 new OsmExporter().exportData(file, this, true /* no backup with appended ~ */);
1559 return true;
1560 }
1561
1562 /**
1563 * Duplicates this layer with a new name and a copy of this layer dataset.
1564 * @param newName name of new layer
1565 * @return A copy of this layer
1566 * @since 18233
1567 */
1568 public OsmDataLayer duplicate(String newName) {
1569 return new OsmDataLayer(new DataSet(getDataSet()), newName, null);
1570 }
1571}
Note: See TracBrowser for help on using the repository browser.