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

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

see #24238 - support more EXIF data in image correlation

  • Property svn:eol-style set to native
File size: 61.6 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[444]2package org.openstreetmap.josm.gui.layer;
3
[2381]4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
[786]5import static org.openstreetmap.josm.tools.I18n.marktr;
[444]6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
[1082]9import java.awt.AlphaComposite;
[444]10import java.awt.Color;
[1082]11import java.awt.Composite;
[1078]12import java.awt.Graphics2D;
[444]13import java.awt.GridBagLayout;
[1078]14import java.awt.Rectangle;
15import java.awt.TexturePaint;
[14500]16import java.awt.datatransfer.Transferable;
17import java.awt.datatransfer.UnsupportedFlavorException;
[444]18import java.awt.event.ActionEvent;
[1078]19import java.awt.geom.Area;
[11633]20import java.awt.geom.Path2D;
[10806]21import java.awt.geom.Rectangle2D;
[1078]22import java.awt.image.BufferedImage;
[444]23import java.io.File;
[14500]24import java.io.IOException;
[17166]25import java.time.DateTimeException;
[1523]26import java.util.ArrayList;
[5025]27import java.util.Arrays;
[444]28import java.util.Collection;
[7414]29import java.util.Collections;
[2907]30import java.util.HashMap;
[444]31import java.util.HashSet;
[3669]32import java.util.List;
[2907]33import java.util.Map;
[15496]34import java.util.Map.Entry;
[16438]35import java.util.Objects;
[8338]36import java.util.Set;
[5519]37import java.util.concurrent.CopyOnWriteArrayList;
[13133]38import java.util.concurrent.atomic.AtomicBoolean;
[11033]39import java.util.concurrent.atomic.AtomicInteger;
[8956]40import java.util.regex.Pattern;
[16438]41import java.util.stream.Collectors;
42import java.util.stream.Stream;
[444]43
44import javax.swing.AbstractAction;
[3408]45import javax.swing.Action;
[444]46import javax.swing.Icon;
47import javax.swing.JLabel;
48import javax.swing.JOptionPane;
49import javax.swing.JPanel;
[2500]50import javax.swing.JScrollPane;
[444]51
[19176]52import org.apache.commons.jcs3.access.CacheAccess;
53import org.openstreetmap.gui.jmapviewer.OsmMercator;
[16967]54import org.openstreetmap.josm.actions.AutoScaleAction;
[5025]55import org.openstreetmap.josm.actions.ExpertToggleAction;
[444]56import org.openstreetmap.josm.actions.RenameLayerAction;
[5025]57import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
[7358]58import org.openstreetmap.josm.data.APIDataSet;
[2450]59import org.openstreetmap.josm.data.Bounds;
[16548]60import org.openstreetmap.josm.data.Data;
[19176]61import org.openstreetmap.josm.data.IBounds;
[10371]62import org.openstreetmap.josm.data.ProjectionBounds;
[14134]63import org.openstreetmap.josm.data.UndoRedoHandler;
[19176]64import org.openstreetmap.josm.data.cache.JCSCacheManager;
[1750]65import org.openstreetmap.josm.data.conflict.Conflict;
66import org.openstreetmap.josm.data.conflict.ConflictCollection;
[10806]67import org.openstreetmap.josm.data.coor.EastNorth;
[628]68import org.openstreetmap.josm.data.coor.LatLon;
[7518]69import org.openstreetmap.josm.data.gpx.GpxConstants;
[1523]70import org.openstreetmap.josm.data.gpx.GpxData;
[15496]71import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
[7518]72import org.openstreetmap.josm.data.gpx.GpxLink;
[15496]73import org.openstreetmap.josm.data.gpx.GpxTrack;
74import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
75import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
[1523]76import org.openstreetmap.josm.data.gpx.WayPoint;
[19176]77import org.openstreetmap.josm.data.osm.BBox;
[3965]78import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
[12152]79import org.openstreetmap.josm.data.osm.DataSelectionListener;
[444]80import org.openstreetmap.josm.data.osm.DataSet;
[2434]81import org.openstreetmap.josm.data.osm.DataSetMerger;
[2500]82import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
[13852]83import org.openstreetmap.josm.data.osm.DownloadPolicy;
[12014]84import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
[19176]85import org.openstreetmap.josm.data.osm.INode;
[4534]86import org.openstreetmap.josm.data.osm.IPrimitive;
[19176]87import org.openstreetmap.josm.data.osm.IRelation;
88import org.openstreetmap.josm.data.osm.IWay;
[444]89import org.openstreetmap.josm.data.osm.Node;
90import org.openstreetmap.josm.data.osm.OsmPrimitive;
[8807]91import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
[1523]92import org.openstreetmap.josm.data.osm.Relation;
[14075]93import org.openstreetmap.josm.data.osm.Tagged;
[13852]94import org.openstreetmap.josm.data.osm.UploadPolicy;
[444]95import org.openstreetmap.josm.data.osm.Way;
[3116]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;
[444]99import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
[12809]100import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
[13987]101import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
[19176]102import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache;
[4087]103import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
[19176]104import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
105import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY;
[4623]106import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
[14060]107import org.openstreetmap.josm.data.preferences.BooleanProperty;
[9668]108import org.openstreetmap.josm.data.preferences.IntegerProperty;
[12987]109import org.openstreetmap.josm.data.preferences.NamedColorProperty;
[9721]110import org.openstreetmap.josm.data.preferences.StringProperty;
[4126]111import org.openstreetmap.josm.data.projection.Projection;
[3669]112import org.openstreetmap.josm.data.validation.TestError;
[5459]113import org.openstreetmap.josm.gui.ExtendedDialog;
[12630]114import org.openstreetmap.josm.gui.MainApplication;
115import org.openstreetmap.josm.gui.MapFrame;
[444]116import org.openstreetmap.josm.gui.MapView;
[10806]117import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
[19176]118import org.openstreetmap.josm.gui.NavigatableComponent;
119import org.openstreetmap.josm.gui.PrimitiveHoverListener;
[14500]120import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
121import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
[444]122import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
123import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
[7358]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;
[14668]128import org.openstreetmap.josm.gui.io.importexport.NoteExporter;
[16548]129import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
[12671]130import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
[14668]131import org.openstreetmap.josm.gui.io.importexport.ValidatorErrorExporter;
132import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
[8805]133import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
[14648]134import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
[4684]135import org.openstreetmap.josm.gui.progress.ProgressMonitor;
[12675]136import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
[7204]137import org.openstreetmap.josm.gui.util.GuiHelper;
[16080]138import org.openstreetmap.josm.gui.util.LruCache;
[9670]139import org.openstreetmap.josm.gui.widgets.FileChooserManager;
[5886]140import org.openstreetmap.josm.gui.widgets.JosmTextArea;
[12846]141import org.openstreetmap.josm.spi.preferences.Config;
[11033]142import org.openstreetmap.josm.tools.AlphanumComparator;
[8910]143import org.openstreetmap.josm.tools.CheckParameterUtil;
[444]144import org.openstreetmap.josm.tools.GBC;
[8323]145import org.openstreetmap.josm.tools.ImageOverlay;
[444]146import org.openstreetmap.josm.tools.ImageProvider;
[8323]147import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
[12620]148import org.openstreetmap.josm.tools.Logging;
[14430]149import org.openstreetmap.josm.tools.UncheckedParseException;
[18208]150import org.openstreetmap.josm.tools.Utils;
[7299]151import org.openstreetmap.josm.tools.date.DateUtils;
[444]152
153/**
[5275]154 * A layer that holds OSM data from a specific dataset.
[444]155 * The data can be fully edited.
[1169]156 *
[444]157 * @author imi
[7414]158 * @since 17
[444]159 */
[19176]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;
[10806]164 private static final int HATCHED_SIZE = 15;
[17757]165 // U+2205 EMPTY SET
166 private static final String IS_EMPTY_SYMBOL = "\u2205";
[7414]167 /** Property used to know if this layer has to be uploaded */
[6889]168 public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
[444]169
[8840]170 private boolean requiresSaveToFile;
171 private boolean requiresUploadToServer;
[13434]172 /** Flag used to know if the layer is being uploaded */
173 private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
[19176]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;
[2025]183
[6436]184 /**
185 * List of validation errors in this layer.
186 * @since 3669
187 */
[7005]188 public final List<TestError> validationErrors = new ArrayList<>();
[3669]189
[12115]190 /**
191 * The default number of relations in the recent relations cache.
192 * @see #getRecentRelations()
193 */
[9668]194 public static final int DEFAULT_RECENT_RELATIONS_NUMBER = 20;
[12115]195 /**
196 * The number of relations to use in the recent relations cache.
197 * @see #getRecentRelations()
198 */
[9668]199 public static final IntegerProperty PROPERTY_RECENT_RELATIONS_NUMBER = new IntegerProperty("properties.last-closed-relations-size",
200 DEFAULT_RECENT_RELATIONS_NUMBER);
[12115]201 /**
202 * The extension that should be used when saving the OSM file.
203 */
[9721]204 public static final StringProperty PROPERTY_SAVE_EXTENSION = new StringProperty("save.extension.osm", "osm");
[9668]205
[14060]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
[12987]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);
[9721]213
[9668]214 /** List of recent relations */
[16080]215 private final Map<Relation, Void> recentRelations = new LruCache<>(PROPERTY_RECENT_RELATIONS_NUMBER.get());
[9668]216
217 /**
218 * Returns list of recently closed relations or null if none.
[9679]219 * @return list of recently closed relations or <code>null</code> if none
[12291]220 * @since 12291 (signature)
[9679]221 * @since 9668
[9668]222 */
[12291]223 public List<Relation> getRecentRelations() {
[9971]224 ArrayList<Relation> list = new ArrayList<>(recentRelations.keySet());
[9668]225 Collections.reverse(list);
226 return list;
227 }
228
[9678]229 /**
[9679]230 * Adds recently closed relation.
231 * @param relation new entry for the list of recently closed relations
[12116]232 * @see #PROPERTY_RECENT_RELATIONS_NUMBER
[9679]233 * @since 9668
[9678]234 */
[9668]235 public void setRecentRelation(Relation relation) {
236 recentRelations.put(relation, null);
[12630]237 MapFrame map = MainApplication.getMap();
238 if (map != null && map.relationListDialog != null) {
239 map.relationListDialog.enableRecentRelations();
[10280]240 }
[9668]241 }
242
[9678]243 /**
244 * Remove relation from list of recent relations.
245 * @param relation relation to remove
[9679]246 * @since 9668
[9678]247 */
[9668]248 public void removeRecentRelation(Relation relation) {
249 recentRelations.remove(relation);
[12630]250 MapFrame map = MainApplication.getMap();
251 if (map != null && map.relationListDialog != null) {
252 map.relationListDialog.enableRecentRelations();
[10280]253 }
[9668]254 }
255
[2025]256 protected void setRequiresSaveToFile(boolean newValue) {
257 boolean oldValue = requiresSaveToFile;
258 requiresSaveToFile = newValue;
259 if (oldValue != newValue) {
[13462]260 GuiHelper.runInEDT(() ->
261 propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue)
262 );
[2025]263 }
264 }
265
266 protected void setRequiresUploadToServer(boolean newValue) {
267 boolean oldValue = requiresUploadToServer;
268 requiresUploadToServer = newValue;
269 if (oldValue != newValue) {
[13462]270 GuiHelper.runInEDT(() ->
271 propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue)
272 );
[2025]273 }
274 }
275
[1750]276 /** the global counter for created data layers */
[11033]277 private static final AtomicInteger dataLayerCounter = new AtomicInteger();
[1750]278
279 /**
280 * Replies a new unique name for a data layer
[1843]281 *
[1750]282 * @return a new unique name for a data layer
283 */
[6889]284 public static String createNewName() {
[11033]285 return createLayerName(dataLayerCounter.incrementAndGet());
[1750]286 }
287
[11033]288 static String createLayerName(Object arg) {
289 return tr("Data Layer {0}", arg);
290 }
291
[12115]292 /**
293 * A listener that counts the number of primitives it encounters
294 */
[12809]295 public static final class DataCountVisitor implements OsmPrimitiveVisitor {
[12115]296 /**
297 * Nodes that have been visited
298 */
[2386]299 public int nodes;
[12115]300 /**
301 * Ways that have been visited
302 */
[2386]303 public int ways;
[12115]304 /**
305 * Relations that have been visited
306 */
[2386]307 public int relations;
[12115]308 /**
309 * Deleted nodes that have been visited
310 */
[2386]311 public int deletedNodes;
[12115]312 /**
313 * Deleted ways that have been visited
314 */
[2386]315 public int deletedWays;
[12115]316 /**
317 * Deleted relations that have been visited
318 */
[2386]319 public int deletedRelations;
[15280]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;
[444]332
[6084]333 @Override
[2386]334 public void visit(final Node n) {
335 nodes++;
336 if (n.isDeleted()) {
337 deletedNodes++;
[1670]338 }
[15280]339 if (n.isIncomplete()) {
340 incompleteNodes++;
341 }
[1078]342 }
[444]343
[6084]344 @Override
[2386]345 public void visit(final Way w) {
346 ways++;
347 if (w.isDeleted()) {
348 deletedWays++;
349 }
[15280]350 if (w.isIncomplete()) {
351 incompleteWays++;
352 }
[1078]353 }
[444]354
[6084]355 @Override
[2386]356 public void visit(final Relation r) {
357 relations++;
358 if (r.isDeleted()) {
359 deletedRelations++;
360 }
[15280]361 if (r.isIncomplete()) {
362 incompleteRelations++;
363 }
[1078]364 }
365 }
[444]366
[12115]367 /**
[5519]368 * Listener called when a state of this layer has changed.
[10600]369 * @since 10600 (functional interface)
[5519]370 */
[10600]371 @FunctionalInterface
[5519]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 }
[6070]380
[7005]381 private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
[6070]382
[5519]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 }
[6070]394
[5519]395 /**
[10340]396 * Removes a layer state change listener
[5519]397 *
398 * @param listener the listener. Ignored if null or already registered.
[10340]399 * @since 10340
400 */
401 public void removeLayerStateChangeListener(LayerStateChangeListener listener) {
[5519]402 layerStateChangeListeners.remove(listener);
403 }
[444]404
[1078]405 /**
406 * The data behind this layer.
407 */
408 public final DataSet data;
[16913]409 private final DataSetListenerAdapter dataSetListenerAdapter;
[444]410
[1078]411 /**
[10806]412 * a texture for non-downloaded area
[1078]413 */
[10806]414 private static volatile BufferedImage hatched;
[1169]415
[1082]416 static {
417 createHatchTexture();
418 }
419
[7414]420 /**
421 * Replies background color for downloaded areas.
422 * @return background color for downloaded areas. Black by default
423 */
[3233]424 public static Color getBackgroundColor() {
[10853]425 return PROPERTY_BACKGROUND_COLOR.get();
[3233]426 }
427
[7414]428 /**
429 * Replies background color for non-downloaded areas.
430 * @return background color for non-downloaded areas. Yellow by default
431 */
[3233]432 public static Color getOutsideColor() {
[10853]433 return PROPERTY_OUTSIDE_COLOR.get();
[3233]434 }
435
[1078]436 /**
[1082]437 * Initialize the hatch pattern used to paint the non-downloaded area
[1078]438 */
[1082]439 public static void createHatchTexture() {
[10806]440 BufferedImage bi = new BufferedImage(HATCHED_SIZE, HATCHED_SIZE, BufferedImage.TYPE_INT_ARGB);
[1078]441 Graphics2D big = bi.createGraphics();
[3233]442 big.setColor(getBackgroundColor());
[1082]443 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
444 big.setComposite(comp);
[10806]445 big.fillRect(0, 0, HATCHED_SIZE, HATCHED_SIZE);
[3233]446 big.setColor(getOutsideColor());
[10344]447 big.drawLine(-1, 6, 6, -1);
448 big.drawLine(4, 16, 16, 4);
[10806]449 hatched = bi;
[1078]450 }
[444]451
[1078]452 /**
[7414]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)
[1082]457 */
458 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
459 super(name);
[8910]460 CheckParameterUtil.ensureParameterNotNull(data, "data");
[1082]461 this.data = data;
[12718]462 this.data.setName(name);
[14556]463 this.dataSetListenerAdapter = new DataSetListenerAdapter(this);
[1646]464 this.setAssociatedFile(associatedFile);
[14556]465 data.addDataSetListener(dataSetListenerAdapter);
[4623]466 data.addDataSetListener(MultipolygonCache.getInstance());
[12014]467 data.addHighlightUpdateListener(this);
[12152]468 data.addSelectionListener(this);
[11277]469 if (name != null && name.startsWith(createLayerName("")) && Character.isDigit(
470 (name.substring(createLayerName("").length()) + "XX" /*avoid StringIndexOutOfBoundsException*/).charAt(1))) {
[11033]471 while (AlphanumComparator.getInstance().compare(createLayerName(dataLayerCounter), name) < 0) {
[11261]472 final int i = dataLayerCounter.incrementAndGet();
473 if (i > 1_000_000) {
474 break; // to avoid looping in unforeseen case
475 }
[11033]476 }
477 }
[1082]478 }
479
[8323]480 /**
[13558]481 * Returns the {@link DataSet} behind this layer.
482 * @return the {@link DataSet} behind this layer.
483 * @since 13558
484 */
[13926]485 @Override
[13558]486 public DataSet getDataSet() {
487 return data;
488 }
489
490 /**
[8323]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");
[5025]497 }
[6070]498
[8323]499 @Override
500 public Icon getIcon() {
501 ImageProvider base = getBaseIconProvider().setMaxSize(ImageSizes.LAYER);
[13453]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) {
[8323]506 base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0));
[5025]507 }
[13133]508
[13434]509 if (isUploadInProgress()) {
510 // If the layer is being uploaded then change the default icon to a clock
[13133]511 base = new ImageProvider("clock").setMaxSize(ImageSizes.LAYER);
[13453]512 } else if (isLocked()) {
[13434]513 // If the layer is read only then change the default icon to a lock
514 base = new ImageProvider("lock").setMaxSize(ImageSizes.LAYER);
[13133]515 }
[8323]516 return base.get();
[1078]517 }
[444]518
[1078]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 */
[2450]524 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
[19176]525 if (!hoverListenerAdded) {
526 MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
527 hoverListenerAdded = true;
528 }
[10395]529 boolean active = mv.getLayerManager().getActiveLayer() == this;
[12846]530 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
[1823]531 boolean virtual = !inactive && mv.isVirtualNodesEnabled();
[19176]532 paintHatch(g, mv, active);
533 paintData(g, mv, box, inactive, virtual);
534 }
[1169]535
[19176]536 private void paintHatch(final Graphics2D g, final MapView mv, boolean active) {
[1169]537 // draw the hatched area for non-downloaded region. only draw if we're the active
[1083]538 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
[19176]539 if (active && Boolean.TRUE.equals(DrawingPreference.SOURCE_BOUNDS_PROP.get()) && !data.getDataSources().isEmpty()) {
[1078]540 // initialize area with current viewport
[1823]541 Rectangle b = mv.getBounds();
[1169]542 // on some platforms viewport bounds seem to be offset from the left,
[1083]543 // over-grow it just to be sure
544 b.grow(100, 100);
[11633]545 Path2D p = new Path2D.Double();
[1169]546
[11633]547 // combine successively downloaded areas
[4087]548 for (Bounds bounds : data.getDataSourceBounds()) {
549 if (bounds.isCollapsed()) {
550 continue;
[1078]551 }
[11633]552 p.append(mv.getState().getArea(bounds), false);
[1078]553 }
[11633]554 // subtract combined areas
555 Area a = new Area(b);
556 a.subtract(new Area(p));
[1169]557
[1078]558 // paint remainder
[10806]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);
[14037]562 if (hatched != null) {
563 g.setPaint(new TexturePaint(hatched, anchorRect));
564 }
[14182]565 try {
[14648]566 g.fill(a);
[14182]567 } catch (ArrayIndexOutOfBoundsException e) {
568 // #16686 - AIOOBE in java.awt.TexturePaintContext$Int.setRaster
569 Logging.error(e);
570 }
[1078]571 }
[19176]572 }
[1169]573
[19176]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;
[13987]584 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
[19176]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);
[12630]609 MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
[1078]610 }
[444]611
[1078]612 @Override public String getToolTipText() {
[12153]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;
[2386]620
[12160]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));
[2386]627
[1646]628 File f = getAssociatedFile();
[1670]629 if (f != null) {
[12160]630 tooltip.append("<br>").append(f.getPath());
[1670]631 }
[12153]632 tooltip.append("</html>");
633 return tooltip.toString();
[1078]634 }
[444]635
[1078]636 @Override public void mergeFrom(final Layer from) {
[4684]637 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
638 monitor.setCancelable(false);
[8510]639 if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
[5233]640 setUploadDiscouraged(true);
641 }
[8510]642 mergeFrom(((OsmDataLayer) from).data, monitor);
[4684]643 monitor.close();
[1670]644 }
645
646 /**
647 * merges the primitives in dataset <code>from</code> into the dataset of
648 * this layer
[1677]649 *
[1670]650 * @param from the source data set
651 */
652 public void mergeFrom(final DataSet from) {
[4684]653 mergeFrom(from, null);
654 }
[6070]655
[4684]656 /**
[9674]657 * merges the primitives in dataset <code>from</code> into the dataset of this layer
[4684]658 *
659 * @param from the source data set
[5869]660 * @param progressMonitor the progress monitor, can be {@code null}
[4684]661 */
662 public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
[8510]663 final DataSetMerger visitor = new DataSetMerger(data, from);
[3965]664 try {
[4684]665 visitor.merge(progressMonitor);
[3965]666 } catch (DataIntegrityProblemException e) {
[12620]667 Logging.error(e);
[3965]668 JOptionPane.showMessageDialog(
[14153]669 MainApplication.getMainFrame(),
[5059]670 e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
[3965]671 tr("Error"),
672 JOptionPane.ERROR_MESSAGE
673 );
674 return;
675 }
676
[1750]677 int numNewConflicts = 0;
[2198]678 for (Conflict<?> c : visitor.getConflicts()) {
[12672]679 if (!data.getConflicts().hasConflict(c)) {
[1750]680 numNewConflicts++;
[12672]681 data.getConflicts().add(c);
[1750]682 }
[1670]683 }
[2198]684 // repaint to make sure new data is displayed properly.
[12176]685 invalidate();
[5869]686 // warn about new conflicts
[12630]687 MapFrame map = MainApplication.getMap();
688 if (numNewConflicts > 0 && map != null && map.conflictDialog != null) {
689 map.conflictDialog.warnNumNewConflicts(numNewConflicts);
[5869]690 }
[2198]691 }
692
[8510]693 @Override
694 public boolean isMergable(final Layer other) {
[8855]695 // allow merging between normal layers and discouraged layers with a warning (see #7684)
696 return other instanceof OsmDataLayer;
[1078]697 }
[444]698
[8510]699 @Override
700 public void visitBoundingBox(final BoundingXYVisitor v) {
[2381]701 for (final Node n: data.getNodes()) {
[2120]702 if (n.isUsable()) {
[1078]703 v.visit(n);
[1670]704 }
[2381]705 }
[1078]706 }
[444]707
[1078]708 /**
709 * Clean out the data behind the layer. This means clearing the redo/undo lists,
[1894]710 * really deleting all deleted objects and reset the modified flags. This should
711 * be done after an upload, even after a partial upload.
[1169]712 *
713 * @param processed A list of all objects that were actually uploaded.
[1894]714 * May be <code>null</code>, which means nothing has been uploaded
[1078]715 */
[10205]716 public void cleanupAfterUpload(final Collection<? extends IPrimitive> processed) {
[1078]717 // return immediately if an upload attempt failed
[18208]718 if (Utils.isEmpty(processed))
[1078]719 return;
[1169]720
[17399]721 UndoRedoHandler.getInstance().clean(data);
[444]722
[1078]723 // if uploaded, clean the modified flags as well
[3426]724 data.cleanupDeletedPrimitives();
[16187]725 data.update(() -> {
[9941]726 for (OsmPrimitive p: data.allPrimitives()) {
727 if (processed.contains(p)) {
728 p.setModified(false);
729 }
[3206]730 }
[16187]731 });
[1078]732 }
[444]733
[15280]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
[8510]752 @Override
753 public Object getInfoComponent() {
[1078]754 final DataCountVisitor counter = new DataCountVisitor();
[1670]755 for (final OsmPrimitive osm : data.allPrimitives()) {
[6009]756 osm.accept(counter);
[1670]757 }
[1078]758 final JPanel p = new JPanel(new GridBagLayout());
[2386]759
[15280]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);
[2386]766
[1890]767 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
[8510]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));
[15824]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);
[17757]777 addConditionalInformation(p, IS_EMPTY_SYMBOL + " " + tr("Empty layer"), this.getDataSet().isEmpty());
778 addConditionalInformation(p, IS_DIRTY_SYMBOL + " " + tr("Unsaved changes"), this.isDirty());
[1523]779
[1078]780 return p;
781 }
[444]782
[15824]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() {
[16967]791 List<Action> actions = new ArrayList<>(Arrays.asList(
[3408]792 LayerListDialog.getInstance().createActivateLayerAction(this),
793 LayerListDialog.getInstance().createShowHideLayerAction(),
[16967]794 MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER),
[3408]795 LayerListDialog.getInstance().createDeleteLayerAction(),
796 SeparatorLayerAction.INSTANCE,
797 LayerListDialog.getInstance().createMergeLayerAction(this),
[8728]798 LayerListDialog.getInstance().createDuplicateLayerAction(this),
[3408]799 new LayerSaveAction(this),
[12279]800 new LayerSaveAsAction(this)));
[6039]801 if (ExpertToggleAction.isExpert()) {
[12279]802 actions.addAll(Arrays.asList(
[6039]803 new LayerGpxExportAction(this),
[12279]804 new ConvertToGpxLayerAction()));
[6039]805 }
[12279]806 actions.addAll(Arrays.asList(
[3408]807 SeparatorLayerAction.INSTANCE,
[12279]808 new RenameLayerAction(getAssociatedFile(), this)));
[9873]809 if (ExpertToggleAction.isExpert()) {
[5025]810 actions.add(new ToggleUploadDiscouragedLayerAction(this));
811 }
[12279]812 actions.addAll(Arrays.asList(
[3408]813 new ConsistencyTestAction(),
814 SeparatorLayerAction.INSTANCE,
[12279]815 new LayerListPopup.InfoAction(this)));
[13206]816 return actions.toArray(new Action[0]);
[1078]817 }
[444]818
[7414]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 */
[1508]825 public static GpxData toGpxData(DataSet data, File file) {
[18466]826 GpxData gpxData = new GpxData(true);
[18078]827 fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX);
[18466]828 gpxData.endUpdate();
[18078]829 return gpxData;
830 }
831
832 protected static void fillGpxData(GpxData gpxData, DataSet data, File file, String gpxPrefix) {
[15496]833 if (data.getGPXNamespaces() != null) {
834 gpxData.getNamespaces().addAll(data.getGPXNamespaces());
835 }
[1508]836 gpxData.storageFile = file;
[8338]837 Set<Node> doneNodes = new HashSet<>();
[18078]838 waysToGpxData(data.getWays(), gpxData, doneNodes, gpxPrefix);
839 nodesToGpxData(data.getNodes(), gpxData, doneNodes, gpxPrefix);
[7414]840 }
841
[18078]842 private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
[8807]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 */
[11177]849 ways.stream()
850 .sorted(OsmPrimitiveComparator.comparingUniqueId().reversed())
851 .forEachOrdered(w -> {
[2120]852 if (!w.isUsable()) {
[11177]853 return;
[1670]854 }
[15496]855 List<IGpxTrackSegment> trk = new ArrayList<>();
[7005]856 Map<String, Object> trkAttr = new HashMap<>();
[508]857
[15496]858 GpxExtensionCollection trkExts = new GpxExtensionCollection();
859 GpxExtensionCollection segExts = new GpxExtensionCollection();
860 for (Entry<String, String> e : w.getKeys().entrySet()) {
[18078]861 String k = e.getKey().startsWith(gpxPrefix) ? e.getKey().substring(gpxPrefix.length()) : e.getKey();
[15496]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()))
[18078]869 .map(s -> s.getKey().substring(gpxPrefix.length()))
[15496]870 .findAny()
871 .orElse(k);
872 if (k.startsWith("extension")) {
[16643]873 String[] chain = k.split(":", -1);
[15496]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 }
[1670]882 }
[15496]883 List<WayPoint> trkseg = new ArrayList<>();
[1898]884 for (Node n : w.getNodes()) {
[2120]885 if (!n.isUsable()) {
[15496]886 if (!trkseg.isEmpty()) {
887 trk.add(new GpxTrackSegment(trkseg));
888 trkseg.clear();
889 }
[1078]890 continue;
891 }
[18078]892 if (!n.isTagged() || containsOnlyGpxTags(n, gpxPrefix)) {
[1078]893 doneNodes.add(n);
894 }
[18078]895 trkseg.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
[1078]896 }
[15496]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);
[11177]902 });
[7414]903 }
[1169]904
[18078]905 private static boolean containsOnlyGpxTags(Tagged t, String gpxPrefix) {
[17584]906 return t.keys()
[18078]907 .allMatch(key -> GpxConstants.WPT_KEYS.contains(key) || key.startsWith(gpxPrefix));
[14075]908 }
909
[13210]910 /**
[15419]911 * Reads the Gpx key from the given {@link OsmPrimitive}, with or without &quot;gpx:&quot; prefix
[15422]912 * @param prim OSM primitive
[18078]913 * @param gpxPrefix the GPX prefix
[15422]914 * @param key GPX key without prefix
[15419]915 * @return the value or <code>null</code> if not present
916 */
[18078]917 private static String gpxVal(OsmPrimitive prim, String gpxPrefix, String key) {
918 String val = prim.get(gpxPrefix + key);
[17847]919 return val != null ? val : prim.get(key);
[15419]920 }
921
922 /**
[18078]923 * Converts a node to a waypoint with default {@link GpxConstants#GPX_PREFIX} for tags.
[13210]924 * @param n the {@code Node} to convert
[18078]925 * @param time a timestamp value in milliseconds from the epoch.
[13210]926 * @return {@code WayPoint} object
927 * @since 13210
928 */
[18078]929 public static WayPoint nodeToWayPoint(Node n, long time) {
930 return nodeToWayPoint(n, time, GpxConstants.GPX_PREFIX);
[13210]931 }
932
933 /**
[18078]934 * Converts a node to a waypoint with a configurable GPX prefix for tags.
[13210]935 * @param n the {@code Node} to convert
[14434]936 * @param time a timestamp value in milliseconds from the epoch.
[18078]937 * @param gpxPrefix the GPX prefix for tags
[13210]938 * @return {@code WayPoint} object
[18078]939 * @since 18078
[13210]940 */
[18078]941 public static WayPoint nodeToWayPoint(Node n, long time, String gpxPrefix) {
[8158]942 WayPoint wpt = new WayPoint(n.getCoor());
[7518]943
[8158]944 // Position info
[7518]945
[18078]946 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_ELE, null);
[7518]947
[14430]948 try {
[15419]949 String v;
[14434]950 if (time > Long.MIN_VALUE) {
951 wpt.setTimeInMillis(time);
[18078]952 } else if ((v = gpxVal(n, gpxPrefix, GpxConstants.PT_TIME)) != null) {
[17749]953 wpt.setInstant(DateUtils.parseInstant(v));
[14430]954 } else if (!n.isTimestampEmpty()) {
[17749]955 wpt.setInstant(n.getInstant());
[14430]956 }
[17166]957 } catch (UncheckedParseException | DateTimeException e) {
[14430]958 Logging.error(e);
[8158]959 }
[7518]960
[18078]961 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_MAGVAR, null);
962 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_GEOIDHEIGHT, null);
[7518]963
[8158]964 // Description info
[7518]965
[18078]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");
[7518]970
[16438]971 Collection<GpxLink> links = Stream.of("link", "url", "website", "contact:website")
[18078]972 .map(key -> gpxVal(n, gpxPrefix, key))
[16438]973 .filter(Objects::nonNull)
974 .map(GpxLink::new)
975 .collect(Collectors.toList());
[8158]976 wpt.put(GpxConstants.META_LINKS, links);
[4046]977
[18078]978 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_SYM, "wpt_symbol", null);
979 addStringIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_TYPE, null, null);
[7518]980
[19387]981 // Angle info
982 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_COURSE, "gps:course");
983
[8158]984 // Accuracy info
[18078]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");
[19387]990 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_STD_HDEV, "gps:stdhdev");
991 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_STD_VDEV, "gps:stdvdev");
[18078]992 addDoubleIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
993 addIntegerIfPresent(wpt, n, gpxPrefix, GpxConstants.PT_DGPSID, "gps:dgpsid");
[7518]994
[8158]995 return wpt;
996 }
997
[18078]998 private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes, String gpxPrefix) {
[8158]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 }
[18078]1006 gpxData.waypoints.add(nodeToWayPoint(n, Long.MIN_VALUE, gpxPrefix));
[1078]1007 }
1008 }
[444]1009
[18078]1010 private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
1011 String value = gpxVal(p, gpxPrefix, gpxKey);
[17847]1012 if (value == null && osmKey != null) {
[18078]1013 value = gpxVal(p, gpxPrefix, osmKey);
[17847]1014 }
1015 if (value != null) {
1016 try {
[18753]1017 final int i = Integer.parseInt(value);
[17847]1018 // Sanity checks
1019 if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
[7518]1020 (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
[18753]1021 wpt.put(gpxKey, i);
[7518]1022 }
[17847]1023 } catch (NumberFormatException e) {
1024 Logging.trace(e);
[7518]1025 }
1026 }
1027 }
1028
[18078]1029 private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey) {
1030 String value = gpxVal(p, gpxPrefix, gpxKey);
[17847]1031 if (value == null && osmKey != null) {
[18078]1032 value = gpxVal(p, gpxPrefix, osmKey);
[17847]1033 }
1034 if (value != null) {
1035 try {
[18753]1036 final double d = Double.parseDouble(value);
[17847]1037 // Sanity checks
1038 if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
[18753]1039 wpt.put(gpxKey, d);
[7518]1040 }
[17847]1041 } catch (NumberFormatException e) {
1042 Logging.trace(e);
[7518]1043 }
1044 }
1045 }
1046
[18078]1047 private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxPrefix, String gpxKey, String osmKey, String osmKey2) {
1048 String value = gpxVal(p, gpxPrefix, gpxKey);
[17847]1049 if (value == null && osmKey != null) {
[18078]1050 value = gpxVal(p, gpxPrefix, osmKey);
[17847]1051 }
1052 if (value == null && osmKey2 != null) {
[18078]1053 value = gpxVal(p, gpxPrefix, osmKey2);
[17847]1054 }
1055 // Sanity checks
1056 if (value != null && (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value))) {
1057 wpt.put(gpxKey, value);
1058 }
[7518]1059 }
1060
[7414]1061 /**
1062 * Converts OSM data behind this layer to GPX data.
1063 * @return GPX data
1064 */
[1078]1065 public GpxData toGpxData() {
[1646]1066 return toGpxData(data, getAssociatedFile());
[1078]1067 }
[628]1068
[7414]1069 /**
1070 * Action that converts this OSM layer to a GPX layer.
1071 */
[1078]1072 public class ConvertToGpxLayerAction extends AbstractAction {
[7414]1073 /**
1074 * Constructs a new {@code ConvertToGpxLayerAction}.
1075 */
[1078]1076 public ConvertToGpxLayerAction() {
[13130]1077 super(tr("Convert to GPX layer"));
1078 new ImageProvider("converttogpx").getResource().attachImageIcon(this, true);
[4701]1079 putValue("help", ht("/Action/ConvertToGpxLayer"));
[1078]1080 }
[8510]1081
[6084]1082 @Override
[1078]1083 public void actionPerformed(ActionEvent e) {
[18466]1084 String name = getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
[9674]1085 final GpxData gpxData = toGpxData();
[18466]1086 final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", name), true);
[9481]1087 if (getAssociatedFile() != null) {
[10300]1088 String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
[9481]1089 gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
[18466]1090 gpxLayer.getGpxData().setModified(true);
[9481]1091 }
[13855]1092 MainApplication.getLayerManager().addLayer(gpxLayer, false);
[12846]1093 if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
[13855]1094 MainApplication.getLayerManager().addLayer(
1095 new MarkerLayer(gpxData, tr("Converted from: {0}", getName()), null, gpxLayer), false);
[8805]1096 }
[12636]1097 MainApplication.getLayerManager().removeLayer(OsmDataLayer.this);
[1078]1098 }
1099 }
1100
[7414]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 */
[1523]1106 public boolean containsPoint(LatLon coor) {
[628]1107 // we'll assume that if this has no data sources
1108 // that it also has no borders
[11627]1109 if (this.data.getDataSources().isEmpty())
[628]1110 return true;
1111
[16438]1112 return this.data.getDataSources().stream()
1113 .anyMatch(src -> src.bounds.contains(coor));
[628]1114 }
[1750]1115
1116 /**
[7414]1117 * Replies the set of conflicts currently managed in this layer.
[1823]1118 *
[1750]1119 * @return the set of conflicts currently managed in this layer
1120 */
1121 public ConflictCollection getConflicts() {
[12672]1122 return data.getConflicts();
[1750]1123 }
[2025]1124
[7358]1125 @Override
[13453]1126 public boolean isDownloadable() {
1127 return data.getDownloadPolicy() != DownloadPolicy.BLOCKED && !isLocked();
1128 }
1129
1130 @Override
[9751]1131 public boolean isUploadable() {
[13453]1132 return data.getUploadPolicy() != UploadPolicy.BLOCKED && !isLocked();
[9751]1133 }
1134
1135 @Override
[2025]1136 public boolean requiresUploadToServer() {
[11709]1137 return isUploadable() && requiresUploadToServer;
[2025]1138 }
1139
[7358]1140 @Override
[2025]1141 public boolean requiresSaveToFile() {
1142 return getAssociatedFile() != null && requiresSaveToFile;
1143 }
1144
[5459]1145 @Override
[17626]1146 public String getLabel() {
1147 String label = super.getLabel();
1148 if (this.isDirty()) {
[17757]1149 label += " " + IS_DIRTY_SYMBOL;
[17626]1150 }
1151 if (this.getDataSet().isEmpty()) {
[17757]1152 label += " " + IS_EMPTY_SYMBOL;
[17626]1153 }
1154 return label;
1155 }
1156
1157 @Override
[2025]1158 public void onPostLoadFromFile() {
1159 setRequiresSaveToFile(false);
[18693]1160 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
[10467]1161 invalidate();
[2025]1162 }
1163
[7414]1164 /**
1165 * Actions run after data has been downloaded to this layer.
1166 */
[2434]1167 public void onPostDownloadFromServer() {
1168 setRequiresSaveToFile(true);
[18693]1169 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
[10467]1170 invalidate();
[2434]1171 }
1172
[3116]1173 @Override
[2025]1174 public void onPostSaveToFile() {
1175 setRequiresSaveToFile(false);
[18693]1176 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
[2025]1177 }
1178
[7358]1179 @Override
[2025]1180 public void onPostUploadToServer() {
[18693]1181 setRequiresUploadToServer(getDataSet().requiresUploadToServer());
[2025]1182 // keep requiresSaveToDisk unchanged
1183 }
[2500]1184
1185 private class ConsistencyTestAction extends AbstractAction {
1186
[8836]1187 ConsistencyTestAction() {
[2500]1188 super(tr("Dataset consistency test"));
1189 }
1190
[6084]1191 @Override
[2500]1192 public void actionPerformed(ActionEvent e) {
1193 String result = DatasetConsistencyTest.runTests(data);
[8394]1194 if (result.isEmpty()) {
[14153]1195 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No problems found"));
[2500]1196 } else {
1197 JPanel p = new JPanel(new GridBagLayout());
1198 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
[5886]1199 JosmTextArea info = new JosmTextArea(result, 20, 60);
[2500]1200 info.setCaretPosition(0);
1201 info.setEditable(false);
1202 p.add(new JScrollPane(info), GBC.eop());
1203
[14153]1204 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
[2500]1205 }
1206 }
1207 }
[3116]1208
[3245]1209 @Override
[12028]1210 public synchronized void destroy() {
[10959]1211 super.destroy();
[12152]1212 data.removeSelectionListener(this);
[12014]1213 data.removeHighlightUpdateListener(this);
[14556]1214 data.removeDataSetListener(dataSetListenerAdapter);
1215 data.removeDataSetListener(MultipolygonCache.getInstance());
[17440]1216 data.clearSelection();
1217 validationErrors.clear();
[14500]1218 removeClipboardDataFor(this);
[14556]1219 recentRelations.clear();
[19176]1220 if (hoverListenerAdded) {
1221 hoverListenerAdded = false;
1222 MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
1223 }
[3245]1224 }
1225
[14500]1226 protected static void removeClipboardDataFor(OsmDataLayer osm) {
1227 Transferable clipboardContents = ClipboardUtils.getClipboardContent();
[14504]1228 if (clipboardContents != null && clipboardContents.isDataFlavorSupported(OsmLayerTransferData.OSM_FLAVOR)) {
[14500]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
[6084]1240 @Override
[3116]1241 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
[19176]1242 resetTiles(event.getPrimitives());
[10031]1243 invalidate();
[3225]1244 setRequiresSaveToFile(true);
[13161]1245 setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
[3116]1246 }
1247
[6084]1248 @Override
[12152]1249 public void selectionChanged(SelectionChangeEvent event) {
[19176]1250 Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
1251 primitives.addAll(event.getRemoved());
1252 resetTiles(primitives);
[10031]1253 invalidate();
1254 }
1255
[19176]1256 private void resetTiles(Collection<? extends IPrimitive> primitives) {
[19271]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) {
[19176]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()) {
[19247]1303 if (member instanceof IRelation) {
1304 resetBounds(member.getBBox()); // Avoid recursive relation issues
1305 break;
1306 } else {
1307 resetTiles(member);
1308 }
[19176]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);
[19180]1360 CheckParameterUtil.ensureThat(targetSize > 0, "mappaint.fast_render.tile_size should be > 0 (default 256)");
[19176]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
[10031]1375 @Override
[4126]1376 public void projectionChanged(Projection oldValue, Projection newValue) {
[7414]1377 // No reprojection required. The dataset itself is registered as projection
1378 // change listener and already got notified.
[4126]1379 }
[5025]1380
[7358]1381 @Override
[5025]1382 public final boolean isUploadDiscouraged() {
[11709]1383 return data.getUploadPolicy() == UploadPolicy.DISCOURAGED;
[5025]1384 }
1385
[7414]1386 /**
1387 * Sets the "discouraged upload" flag.
[8540]1388 * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
1389 * This feature allows to use "private" data layers.
[7414]1390 */
[5025]1391 public final void setUploadDiscouraged(boolean uploadDiscouraged) {
[11709]1392 if (data.getUploadPolicy() != UploadPolicy.BLOCKED &&
[11710]1393 (uploadDiscouraged ^ isUploadDiscouraged())) {
[11709]1394 data.setUploadPolicy(uploadDiscouraged ? UploadPolicy.DISCOURAGED : UploadPolicy.NORMAL);
[19116]1395 setRequiresSaveToFile(true);
[5519]1396 for (LayerStateChangeListener l : layerStateChangeListeners) {
1397 l.uploadDiscouragedChanged(this, uploadDiscouraged);
1398 }
1399 }
[5025]1400 }
[5459]1401
1402 @Override
[7358]1403 public final boolean isModified() {
1404 return data.isModified();
1405 }
1406
1407 @Override
[5459]1408 public boolean isSavable() {
1409 return true; // With OsmExporter
1410 }
1411
1412 @Override
1413 public boolean checkSaveConditions() {
[15121]1414 if (isDataSetEmpty() && 1 != GuiHelper.runInEDTAndWaitAndReturn(() ->
1415 new ExtendedDialog(
[14153]1416 MainApplication.getMainFrame(),
[17757]1417 tr("Empty layer"),
[12279]1418 tr("Save anyway"), tr("Cancel"))
[17757]1419 .setContent(tr("The layer contains no data."))
[12279]1420 .setButtonIcons("save", "cancel")
[15121]1421 .showDialog().getValue()
1422 )) {
[9674]1423 return false;
[5459]1424 }
1425
[9674]1426 ConflictCollection conflictsCol = getConflicts();
[12281]1427 return conflictsCol == null || conflictsCol.isEmpty() || 1 == GuiHelper.runInEDTAndWaitAndReturn(() ->
1428 new ExtendedDialog(
[14153]1429 MainApplication.getMainFrame(),
[10611]1430 /* I18N: Display title of the window showing conflicts */
1431 tr("Conflicts"),
[12279]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")
[12281]1436 .showDialog().getValue()
1437 );
[5459]1438 }
[6070]1439
[5459]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() {
[16438]1448 return data == null || data.allNonDeletedPrimitives().stream()
1449 .allMatch(osm -> osm.isDeleted() && osm.isNewOrUndeleted());
[5459]1450 }
1451
1452 @Override
1453 public File createAndOpenSaveFileChooser() {
[9721]1454 String extension = PROPERTY_SAVE_EXTENSION.get();
[9670]1455 File file = getAssociatedFile();
1456 if (file == null && isRenamed()) {
[12846]1457 StringBuilder filename = new StringBuilder(Config.getPref().get("lastDirectory")).append('/').append(getName());
[11649]1458 if (!OsmImporter.FILE_FILTER.acceptName(filename.toString())) {
1459 filename.append('.').append(extension);
1460 }
1461 file = new File(filename.toString());
[9670]1462 }
1463 return new FileChooserManager()
1464 .title(tr("Save OSM file"))
1465 .extension(extension)
1466 .file(file)
[14668]1467 .additionalTypes(t -> t != WMSLayerImporter.FILE_FILTER && t != NoteExporter.FILE_FILTER && t != ValidatorErrorExporter.FILE_FILTER)
[9670]1468 .getFileForSave();
[5459]1469 }
[7358]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
[18457]1482 public String getChangesetSourceTag() {
1483 return this.data.getChangeSetTags().getOrDefault("source", null);
1484 }
1485
1486 @Override
[7358]1487 public AbstractUploadDialog getUploadDialog() {
1488 UploadDialog dialog = UploadDialog.getUploadDialog();
1489 dialog.setUploadedPrimitives(new APIDataSet(data));
1490 return dialog;
1491 }
[10371]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 }
[12014]1502
1503 @Override
1504 public void highlightUpdated(HighlightUpdateEvent e) {
1505 invalidate();
1506 }
[12718]1507
1508 @Override
[19176]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
[12718]1519 public void setName(String name) {
1520 if (data != null) {
1521 data.setName(name);
1522 }
1523 super.setName(name);
1524 }
[13133]1525
[13435]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 */
[13434]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
[13435]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 */
[13434]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 }
[16548]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 }
[18233]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 }
[444]1571}
Note: See TracBrowser for help on using the repository browser.