[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[444] | 2 | package org.openstreetmap.josm.gui.layer;
|
---|
| 3 |
|
---|
[2381] | 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
[786] | 5 | import static org.openstreetmap.josm.tools.I18n.marktr;
|
---|
[444] | 6 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 7 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
| 8 |
|
---|
[1082] | 9 | import java.awt.AlphaComposite;
|
---|
[444] | 10 | import java.awt.Color;
|
---|
[1082] | 11 | import java.awt.Composite;
|
---|
[1078] | 12 | import java.awt.Graphics2D;
|
---|
[444] | 13 | import java.awt.GridBagLayout;
|
---|
| 14 | import java.awt.Point;
|
---|
[1078] | 15 | import java.awt.Rectangle;
|
---|
| 16 | import java.awt.TexturePaint;
|
---|
[444] | 17 | import java.awt.event.ActionEvent;
|
---|
[1078] | 18 | import java.awt.geom.Area;
|
---|
| 19 | import java.awt.image.BufferedImage;
|
---|
[444] | 20 | import java.io.File;
|
---|
[1523] | 21 | import java.util.ArrayList;
|
---|
[5025] | 22 | import java.util.Arrays;
|
---|
[444] | 23 | import java.util.Collection;
|
---|
[7414] | 24 | import java.util.Collections;
|
---|
[2907] | 25 | import java.util.HashMap;
|
---|
[444] | 26 | import java.util.HashSet;
|
---|
[3669] | 27 | import java.util.List;
|
---|
[2907] | 28 | import java.util.Map;
|
---|
[8338] | 29 | import java.util.Set;
|
---|
[7204] | 30 | import java.util.concurrent.Callable;
|
---|
[5519] | 31 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
[8956] | 32 | import java.util.regex.Pattern;
|
---|
[444] | 33 |
|
---|
| 34 | import javax.swing.AbstractAction;
|
---|
[3408] | 35 | import javax.swing.Action;
|
---|
[444] | 36 | import javax.swing.Icon;
|
---|
| 37 | import javax.swing.JLabel;
|
---|
| 38 | import javax.swing.JOptionPane;
|
---|
| 39 | import javax.swing.JPanel;
|
---|
[2500] | 40 | import javax.swing.JScrollPane;
|
---|
[444] | 41 |
|
---|
| 42 | import org.openstreetmap.josm.Main;
|
---|
[5025] | 43 | import org.openstreetmap.josm.actions.ExpertToggleAction;
|
---|
[444] | 44 | import org.openstreetmap.josm.actions.RenameLayerAction;
|
---|
[5459] | 45 | import org.openstreetmap.josm.actions.SaveActionBase;
|
---|
[5025] | 46 | import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
|
---|
[7358] | 47 | import org.openstreetmap.josm.data.APIDataSet;
|
---|
[2450] | 48 | import org.openstreetmap.josm.data.Bounds;
|
---|
[7575] | 49 | import org.openstreetmap.josm.data.DataSource;
|
---|
[3116] | 50 | import org.openstreetmap.josm.data.SelectionChangedListener;
|
---|
[1750] | 51 | import org.openstreetmap.josm.data.conflict.Conflict;
|
---|
| 52 | import org.openstreetmap.josm.data.conflict.ConflictCollection;
|
---|
[628] | 53 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[7518] | 54 | import org.openstreetmap.josm.data.gpx.GpxConstants;
|
---|
[1523] | 55 | import org.openstreetmap.josm.data.gpx.GpxData;
|
---|
[7518] | 56 | import org.openstreetmap.josm.data.gpx.GpxLink;
|
---|
[2907] | 57 | import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
|
---|
[1523] | 58 | import org.openstreetmap.josm.data.gpx.WayPoint;
|
---|
[3965] | 59 | import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
|
---|
[444] | 60 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[2434] | 61 | import org.openstreetmap.josm.data.osm.DataSetMerger;
|
---|
[2500] | 62 | import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
|
---|
[4534] | 63 | import org.openstreetmap.josm.data.osm.IPrimitive;
|
---|
[444] | 64 | import org.openstreetmap.josm.data.osm.Node;
|
---|
| 65 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
[8807] | 66 | import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
|
---|
[1523] | 67 | import org.openstreetmap.josm.data.osm.Relation;
|
---|
[444] | 68 | import org.openstreetmap.josm.data.osm.Way;
|
---|
[3116] | 69 | import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
|
---|
| 70 | import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
|
---|
| 71 | import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
|
---|
[1523] | 72 | import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
|
---|
[444] | 73 | import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
|
---|
[4087] | 74 | import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
|
---|
| 75 | import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
|
---|
[4623] | 76 | import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
|
---|
[4126] | 77 | import org.openstreetmap.josm.data.projection.Projection;
|
---|
[3669] | 78 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
[5459] | 79 | import org.openstreetmap.josm.gui.ExtendedDialog;
|
---|
[444] | 80 | import org.openstreetmap.josm.gui.MapView;
|
---|
| 81 | import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
|
---|
| 82 | import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
|
---|
[7358] | 83 | import org.openstreetmap.josm.gui.io.AbstractIOTask;
|
---|
| 84 | import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
|
---|
| 85 | import org.openstreetmap.josm.gui.io.UploadDialog;
|
---|
| 86 | import org.openstreetmap.josm.gui.io.UploadLayerTask;
|
---|
[8805] | 87 | import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
|
---|
[4684] | 88 | import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
|
---|
| 89 | import org.openstreetmap.josm.gui.progress.ProgressMonitor;
|
---|
[7204] | 90 | import org.openstreetmap.josm.gui.util.GuiHelper;
|
---|
[5886] | 91 | import org.openstreetmap.josm.gui.widgets.JosmTextArea;
|
---|
[8910] | 92 | import org.openstreetmap.josm.tools.CheckParameterUtil;
|
---|
[3801] | 93 | import org.openstreetmap.josm.tools.FilteredCollection;
|
---|
[444] | 94 | import org.openstreetmap.josm.tools.GBC;
|
---|
[8323] | 95 | import org.openstreetmap.josm.tools.ImageOverlay;
|
---|
[444] | 96 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
[8323] | 97 | import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
|
---|
[7299] | 98 | import org.openstreetmap.josm.tools.date.DateUtils;
|
---|
[444] | 99 |
|
---|
| 100 | /**
|
---|
[5275] | 101 | * A layer that holds OSM data from a specific dataset.
|
---|
[444] | 102 | * The data can be fully edited.
|
---|
[1169] | 103 | *
|
---|
[444] | 104 | * @author imi
|
---|
[7414] | 105 | * @since 17
|
---|
[444] | 106 | */
|
---|
[7402] | 107 | public class OsmDataLayer extends AbstractModifiableLayer implements Listener, SelectionChangedListener {
|
---|
[7414] | 108 | /** Property used to know if this layer has to be saved on disk */
|
---|
[6889] | 109 | public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
|
---|
[7414] | 110 | /** Property used to know if this layer has to be uploaded */
|
---|
[6889] | 111 | public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
|
---|
[444] | 112 |
|
---|
[8840] | 113 | private boolean requiresSaveToFile;
|
---|
| 114 | private boolean requiresUploadToServer;
|
---|
[3116] | 115 | private boolean isChanged = true;
|
---|
| 116 | private int highlightUpdateCount;
|
---|
[2025] | 117 |
|
---|
[6436] | 118 | /**
|
---|
| 119 | * List of validation errors in this layer.
|
---|
| 120 | * @since 3669
|
---|
| 121 | */
|
---|
[7005] | 122 | public final List<TestError> validationErrors = new ArrayList<>();
|
---|
[3669] | 123 |
|
---|
[2025] | 124 | protected void setRequiresSaveToFile(boolean newValue) {
|
---|
| 125 | boolean oldValue = requiresSaveToFile;
|
---|
| 126 | requiresSaveToFile = newValue;
|
---|
| 127 | if (oldValue != newValue) {
|
---|
| 128 | propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue);
|
---|
| 129 | }
|
---|
| 130 | }
|
---|
| 131 |
|
---|
| 132 | protected void setRequiresUploadToServer(boolean newValue) {
|
---|
| 133 | boolean oldValue = requiresUploadToServer;
|
---|
| 134 | requiresUploadToServer = newValue;
|
---|
| 135 | if (oldValue != newValue) {
|
---|
| 136 | propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue);
|
---|
| 137 | }
|
---|
| 138 | }
|
---|
| 139 |
|
---|
[1750] | 140 | /** the global counter for created data layers */
|
---|
[8840] | 141 | private static int dataLayerCounter;
|
---|
[1750] | 142 |
|
---|
| 143 | /**
|
---|
| 144 | * Replies a new unique name for a data layer
|
---|
[1843] | 145 | *
|
---|
[1750] | 146 | * @return a new unique name for a data layer
|
---|
| 147 | */
|
---|
[6889] | 148 | public static String createNewName() {
|
---|
[1750] | 149 | dataLayerCounter++;
|
---|
| 150 | return tr("Data Layer {0}", dataLayerCounter);
|
---|
| 151 | }
|
---|
| 152 |
|
---|
[6889] | 153 | public static final class DataCountVisitor extends AbstractVisitor {
|
---|
[2386] | 154 | public int nodes;
|
---|
| 155 | public int ways;
|
---|
| 156 | public int relations;
|
---|
| 157 | public int deletedNodes;
|
---|
| 158 | public int deletedWays;
|
---|
| 159 | public int deletedRelations;
|
---|
[444] | 160 |
|
---|
[6084] | 161 | @Override
|
---|
[2386] | 162 | public void visit(final Node n) {
|
---|
| 163 | nodes++;
|
---|
| 164 | if (n.isDeleted()) {
|
---|
| 165 | deletedNodes++;
|
---|
[1670] | 166 | }
|
---|
[1078] | 167 | }
|
---|
[444] | 168 |
|
---|
[6084] | 169 | @Override
|
---|
[2386] | 170 | public void visit(final Way w) {
|
---|
| 171 | ways++;
|
---|
| 172 | if (w.isDeleted()) {
|
---|
| 173 | deletedWays++;
|
---|
| 174 | }
|
---|
[1078] | 175 | }
|
---|
[444] | 176 |
|
---|
[6084] | 177 | @Override
|
---|
[2386] | 178 | public void visit(final Relation r) {
|
---|
| 179 | relations++;
|
---|
| 180 | if (r.isDeleted()) {
|
---|
| 181 | deletedRelations++;
|
---|
| 182 | }
|
---|
[1078] | 183 | }
|
---|
| 184 | }
|
---|
[444] | 185 |
|
---|
[1078] | 186 | public interface CommandQueueListener {
|
---|
| 187 | void commandChanged(int queueSize, int redoSize);
|
---|
| 188 | }
|
---|
[6070] | 189 |
|
---|
[5519] | 190 | /**
|
---|
| 191 | * Listener called when a state of this layer has changed.
|
---|
| 192 | */
|
---|
| 193 | public interface LayerStateChangeListener {
|
---|
| 194 | /**
|
---|
| 195 | * Notifies that the "upload discouraged" (upload=no) state has changed.
|
---|
| 196 | * @param layer The layer that has been modified
|
---|
| 197 | * @param newValue The new value of the state
|
---|
| 198 | */
|
---|
| 199 | void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue);
|
---|
| 200 | }
|
---|
[6070] | 201 |
|
---|
[7005] | 202 | private final CopyOnWriteArrayList<LayerStateChangeListener> layerStateChangeListeners = new CopyOnWriteArrayList<>();
|
---|
[6070] | 203 |
|
---|
[5519] | 204 | /**
|
---|
| 205 | * Adds a layer state change listener
|
---|
| 206 | *
|
---|
| 207 | * @param listener the listener. Ignored if null or already registered.
|
---|
| 208 | * @since 5519
|
---|
| 209 | */
|
---|
| 210 | public void addLayerStateChangeListener(LayerStateChangeListener listener) {
|
---|
| 211 | if (listener != null) {
|
---|
| 212 | layerStateChangeListeners.addIfAbsent(listener);
|
---|
| 213 | }
|
---|
| 214 | }
|
---|
[6070] | 215 |
|
---|
[5519] | 216 | /**
|
---|
| 217 | * Removes a layer property change listener
|
---|
| 218 | *
|
---|
| 219 | * @param listener the listener. Ignored if null or already registered.
|
---|
| 220 | * @since 5519
|
---|
| 221 | */
|
---|
| 222 | public void removeLayerPropertyChangeListener(LayerStateChangeListener listener) {
|
---|
| 223 | layerStateChangeListeners.remove(listener);
|
---|
| 224 | }
|
---|
[444] | 225 |
|
---|
[1078] | 226 | /**
|
---|
| 227 | * The data behind this layer.
|
---|
| 228 | */
|
---|
| 229 | public final DataSet data;
|
---|
[444] | 230 |
|
---|
[1078] | 231 | /**
|
---|
[1750] | 232 | * the collection of conflicts detected in this layer
|
---|
| 233 | */
|
---|
[9078] | 234 | private final ConflictCollection conflicts;
|
---|
[1750] | 235 |
|
---|
[2985] | 236 | /**
|
---|
[1078] | 237 | * a paint texture for non-downloaded area
|
---|
| 238 | */
|
---|
[8126] | 239 | private static volatile TexturePaint hatched;
|
---|
[1169] | 240 |
|
---|
[1082] | 241 | static {
|
---|
| 242 | createHatchTexture();
|
---|
| 243 | }
|
---|
| 244 |
|
---|
[7414] | 245 | /**
|
---|
| 246 | * Replies background color for downloaded areas.
|
---|
| 247 | * @return background color for downloaded areas. Black by default
|
---|
| 248 | */
|
---|
[3233] | 249 | public static Color getBackgroundColor() {
|
---|
[8910] | 250 | return Main.pref != null ? Main.pref.getColor(marktr("background"), Color.BLACK) : Color.BLACK;
|
---|
[3233] | 251 | }
|
---|
| 252 |
|
---|
[7414] | 253 | /**
|
---|
| 254 | * Replies background color for non-downloaded areas.
|
---|
| 255 | * @return background color for non-downloaded areas. Yellow by default
|
---|
| 256 | */
|
---|
[3233] | 257 | public static Color getOutsideColor() {
|
---|
[8910] | 258 | return Main.pref != null ? Main.pref.getColor(marktr("outside downloaded area"), Color.YELLOW) : Color.YELLOW;
|
---|
[3233] | 259 | }
|
---|
| 260 |
|
---|
[1078] | 261 | /**
|
---|
[1082] | 262 | * Initialize the hatch pattern used to paint the non-downloaded area
|
---|
[1078] | 263 | */
|
---|
[1082] | 264 | public static void createHatchTexture() {
|
---|
| 265 | BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
|
---|
[1078] | 266 | Graphics2D big = bi.createGraphics();
|
---|
[3233] | 267 | big.setColor(getBackgroundColor());
|
---|
[1082] | 268 | Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
|
---|
| 269 | big.setComposite(comp);
|
---|
[8510] | 270 | big.fillRect(0, 0, 15, 15);
|
---|
[3233] | 271 | big.setColor(getOutsideColor());
|
---|
[8510] | 272 | big.drawLine(0, 15, 15, 0);
|
---|
| 273 | Rectangle r = new Rectangle(0, 0, 15, 15);
|
---|
[1078] | 274 | hatched = new TexturePaint(bi, r);
|
---|
| 275 | }
|
---|
[444] | 276 |
|
---|
[1078] | 277 | /**
|
---|
[7414] | 278 | * Construct a new {@code OsmDataLayer}.
|
---|
| 279 | * @param data OSM data
|
---|
| 280 | * @param name Layer name
|
---|
| 281 | * @param associatedFile Associated .osm file (can be null)
|
---|
[1082] | 282 | */
|
---|
| 283 | public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
|
---|
| 284 | super(name);
|
---|
[8910] | 285 | CheckParameterUtil.ensureParameterNotNull(data, "data");
|
---|
[1082] | 286 | this.data = data;
|
---|
[1646] | 287 | this.setAssociatedFile(associatedFile);
|
---|
[1750] | 288 | conflicts = new ConflictCollection();
|
---|
[3116] | 289 | data.addDataSetListener(new DataSetListenerAdapter(this));
|
---|
[4623] | 290 | data.addDataSetListener(MultipolygonCache.getInstance());
|
---|
[3416] | 291 | DataSet.addSelectionListener(this);
|
---|
[1082] | 292 | }
|
---|
| 293 |
|
---|
[8323] | 294 | /**
|
---|
| 295 | * Return the image provider to get the base icon
|
---|
| 296 | * @return image provider class which can be modified
|
---|
| 297 | * @since 8323
|
---|
| 298 | */
|
---|
| 299 | protected ImageProvider getBaseIconProvider() {
|
---|
| 300 | return new ImageProvider("layer", "osmdata_small");
|
---|
[5025] | 301 | }
|
---|
[6070] | 302 |
|
---|
[8323] | 303 | @Override
|
---|
| 304 | public Icon getIcon() {
|
---|
| 305 | ImageProvider base = getBaseIconProvider().setMaxSize(ImageSizes.LAYER);
|
---|
[5025] | 306 | if (isUploadDiscouraged()) {
|
---|
[8323] | 307 | base.addOverlay(new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0));
|
---|
[5025] | 308 | }
|
---|
[8323] | 309 | return base.get();
|
---|
[1078] | 310 | }
|
---|
[444] | 311 |
|
---|
[1078] | 312 | /**
|
---|
| 313 | * Draw all primitives in this layer but do not draw modified ones (they
|
---|
| 314 | * are drawn by the edit layer).
|
---|
| 315 | * Draw nodes last to overlap the ways they belong to.
|
---|
| 316 | */
|
---|
[2450] | 317 | @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
|
---|
[3116] | 318 | isChanged = false;
|
---|
| 319 | highlightUpdateCount = data.getHighlightUpdateCount();
|
---|
| 320 |
|
---|
[1823] | 321 | boolean active = mv.getActiveLayer() == this;
|
---|
[1083] | 322 | boolean inactive = !active && Main.pref.getBoolean("draw.data.inactive_color", true);
|
---|
[1823] | 323 | boolean virtual = !inactive && mv.isVirtualNodesEnabled();
|
---|
[1169] | 324 |
|
---|
| 325 | // draw the hatched area for non-downloaded region. only draw if we're the active
|
---|
[1083] | 326 | // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
|
---|
| 327 | if (active && Main.pref.getBoolean("draw.data.downloaded_area", true) && !data.dataSources.isEmpty()) {
|
---|
[1078] | 328 | // initialize area with current viewport
|
---|
[1823] | 329 | Rectangle b = mv.getBounds();
|
---|
[1169] | 330 | // on some platforms viewport bounds seem to be offset from the left,
|
---|
[1083] | 331 | // over-grow it just to be sure
|
---|
| 332 | b.grow(100, 100);
|
---|
| 333 | Area a = new Area(b);
|
---|
[1169] | 334 |
|
---|
[4087] | 335 | // now successively subtract downloaded areas
|
---|
| 336 | for (Bounds bounds : data.getDataSourceBounds()) {
|
---|
| 337 | if (bounds.isCollapsed()) {
|
---|
| 338 | continue;
|
---|
[1078] | 339 | }
|
---|
[4087] | 340 | Point p1 = mv.getPoint(bounds.getMin());
|
---|
| 341 | Point p2 = mv.getPoint(bounds.getMax());
|
---|
[8510] | 342 | Rectangle r = new Rectangle(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y), Math.abs(p2.x-p1.x), Math.abs(p2.y-p1.y));
|
---|
[4087] | 343 | a.subtract(new Area(r));
|
---|
[1078] | 344 | }
|
---|
[1169] | 345 |
|
---|
[1078] | 346 | // paint remainder
|
---|
[6501] | 347 | g.setPaint(hatched);
|
---|
| 348 | g.fill(a);
|
---|
[1078] | 349 | }
|
---|
[1169] | 350 |
|
---|
[4087] | 351 | Rendering painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
|
---|
| 352 | painter.render(data, virtual, box);
|
---|
[1078] | 353 | Main.map.conflictDialog.paintConflicts(g, mv);
|
---|
| 354 | }
|
---|
[444] | 355 |
|
---|
[1078] | 356 | @Override public String getToolTipText() {
|
---|
[7005] | 357 | int nodes = new FilteredCollection<>(data.getNodes(), OsmPrimitive.nonDeletedPredicate).size();
|
---|
| 358 | int ways = new FilteredCollection<>(data.getWays(), OsmPrimitive.nonDeletedPredicate).size();
|
---|
[7684] | 359 | int rels = new FilteredCollection<>(data.getRelations(), OsmPrimitive.nonDeletedPredicate).size();
|
---|
[2386] | 360 |
|
---|
| 361 | String tool = trn("{0} node", "{0} nodes", nodes, nodes)+", ";
|
---|
[7684] | 362 | tool += trn("{0} way", "{0} ways", ways, ways)+", ";
|
---|
| 363 | tool += trn("{0} relation", "{0} relations", rels, rels);
|
---|
[2386] | 364 |
|
---|
[1646] | 365 | File f = getAssociatedFile();
|
---|
[1670] | 366 | if (f != null) {
|
---|
[1646] | 367 | tool = "<html>"+tool+"<br>"+f.getPath()+"</html>";
|
---|
[1670] | 368 | }
|
---|
[1078] | 369 | return tool;
|
---|
| 370 | }
|
---|
[444] | 371 |
|
---|
[1078] | 372 | @Override public void mergeFrom(final Layer from) {
|
---|
[4684] | 373 | final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
|
---|
| 374 | monitor.setCancelable(false);
|
---|
[8510] | 375 | if (from instanceof OsmDataLayer && ((OsmDataLayer) from).isUploadDiscouraged()) {
|
---|
[5233] | 376 | setUploadDiscouraged(true);
|
---|
| 377 | }
|
---|
[8510] | 378 | mergeFrom(((OsmDataLayer) from).data, monitor);
|
---|
[4684] | 379 | monitor.close();
|
---|
[1670] | 380 | }
|
---|
| 381 |
|
---|
| 382 | /**
|
---|
| 383 | * merges the primitives in dataset <code>from</code> into the dataset of
|
---|
| 384 | * this layer
|
---|
[1677] | 385 | *
|
---|
[1670] | 386 | * @param from the source data set
|
---|
| 387 | */
|
---|
| 388 | public void mergeFrom(final DataSet from) {
|
---|
[4684] | 389 | mergeFrom(from, null);
|
---|
| 390 | }
|
---|
[6070] | 391 |
|
---|
[4684] | 392 | /**
|
---|
| 393 | * merges the primitives in dataset <code>from</code> into the dataset of
|
---|
| 394 | * this layer
|
---|
| 395 | *
|
---|
| 396 | * @param from the source data set
|
---|
[5869] | 397 | * @param progressMonitor the progress monitor, can be {@code null}
|
---|
[4684] | 398 | */
|
---|
| 399 | public void mergeFrom(final DataSet from, ProgressMonitor progressMonitor) {
|
---|
[8510] | 400 | final DataSetMerger visitor = new DataSetMerger(data, from);
|
---|
[3965] | 401 | try {
|
---|
[4684] | 402 | visitor.merge(progressMonitor);
|
---|
[3965] | 403 | } catch (DataIntegrityProblemException e) {
|
---|
| 404 | JOptionPane.showMessageDialog(
|
---|
| 405 | Main.parent,
|
---|
[5059] | 406 | e.getHtmlMessage() != null ? e.getHtmlMessage() : e.getMessage(),
|
---|
[3965] | 407 | tr("Error"),
|
---|
| 408 | JOptionPane.ERROR_MESSAGE
|
---|
| 409 | );
|
---|
| 410 | return;
|
---|
| 411 | }
|
---|
| 412 |
|
---|
[1558] | 413 | Area a = data.getDataSourceArea();
|
---|
[1670] | 414 |
|
---|
[6296] | 415 | // copy the merged layer's data source info.
|
---|
| 416 | // only add source rectangles if they are not contained in the layer already.
|
---|
[1670] | 417 | for (DataSource src : from.dataSources) {
|
---|
| 418 | if (a == null || !a.contains(src.bounds.asRect())) {
|
---|
[1558] | 419 | data.dataSources.add(src);
|
---|
[1670] | 420 | }
|
---|
[1558] | 421 | }
|
---|
[1670] | 422 |
|
---|
[7013] | 423 | // copy the merged layer's API version
|
---|
[2396] | 424 | if (data.getVersion() == null) {
|
---|
| 425 | data.setVersion(from.getVersion());
|
---|
[1523] | 426 | }
|
---|
[1169] | 427 |
|
---|
[1750] | 428 | int numNewConflicts = 0;
|
---|
[2198] | 429 | for (Conflict<?> c : visitor.getConflicts()) {
|
---|
[1750] | 430 | if (!conflicts.hasConflict(c)) {
|
---|
| 431 | numNewConflicts++;
|
---|
| 432 | conflicts.add(c);
|
---|
| 433 | }
|
---|
[1670] | 434 | }
|
---|
[2198] | 435 | // repaint to make sure new data is displayed properly.
|
---|
[6333] | 436 | if (Main.isDisplayingMapView()) {
|
---|
[5758] | 437 | Main.map.mapView.repaint();
|
---|
| 438 | }
|
---|
[5869] | 439 | // warn about new conflicts
|
---|
| 440 | if (numNewConflicts > 0 && Main.map != null && Main.map.conflictDialog != null) {
|
---|
| 441 | Main.map.conflictDialog.warnNumNewConflicts(numNewConflicts);
|
---|
| 442 | }
|
---|
[2198] | 443 | }
|
---|
| 444 |
|
---|
[8510] | 445 | @Override
|
---|
| 446 | public boolean isMergable(final Layer other) {
|
---|
[8855] | 447 | // allow merging between normal layers and discouraged layers with a warning (see #7684)
|
---|
| 448 | return other instanceof OsmDataLayer;
|
---|
[1078] | 449 | }
|
---|
[444] | 450 |
|
---|
[8510] | 451 | @Override
|
---|
| 452 | public void visitBoundingBox(final BoundingXYVisitor v) {
|
---|
[2381] | 453 | for (final Node n: data.getNodes()) {
|
---|
[2120] | 454 | if (n.isUsable()) {
|
---|
[1078] | 455 | v.visit(n);
|
---|
[1670] | 456 | }
|
---|
[2381] | 457 | }
|
---|
[1078] | 458 | }
|
---|
[444] | 459 |
|
---|
[1078] | 460 | /**
|
---|
| 461 | * Clean out the data behind the layer. This means clearing the redo/undo lists,
|
---|
[1894] | 462 | * really deleting all deleted objects and reset the modified flags. This should
|
---|
| 463 | * be done after an upload, even after a partial upload.
|
---|
[1169] | 464 | *
|
---|
| 465 | * @param processed A list of all objects that were actually uploaded.
|
---|
[1894] | 466 | * May be <code>null</code>, which means nothing has been uploaded
|
---|
[1078] | 467 | */
|
---|
[4534] | 468 | public void cleanupAfterUpload(final Collection<IPrimitive> processed) {
|
---|
[1078] | 469 | // return immediately if an upload attempt failed
|
---|
[1894] | 470 | if (processed == null || processed.isEmpty())
|
---|
[1078] | 471 | return;
|
---|
[1169] | 472 |
|
---|
[1894] | 473 | Main.main.undoRedo.clean(this);
|
---|
[444] | 474 |
|
---|
[1078] | 475 | // if uploaded, clean the modified flags as well
|
---|
[3426] | 476 | data.cleanupDeletedPrimitives();
|
---|
[3206] | 477 | for (OsmPrimitive p: data.allPrimitives()) {
|
---|
| 478 | if (processed.contains(p)) {
|
---|
| 479 | p.setModified(false);
|
---|
| 480 | }
|
---|
[1078] | 481 | }
|
---|
| 482 | }
|
---|
[444] | 483 |
|
---|
[8510] | 484 | @Override
|
---|
| 485 | public Object getInfoComponent() {
|
---|
[1078] | 486 | final DataCountVisitor counter = new DataCountVisitor();
|
---|
[1670] | 487 | for (final OsmPrimitive osm : data.allPrimitives()) {
|
---|
[6009] | 488 | osm.accept(counter);
|
---|
[1670] | 489 | }
|
---|
[1078] | 490 | final JPanel p = new JPanel(new GridBagLayout());
|
---|
[2386] | 491 |
|
---|
| 492 | String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
|
---|
[2388] | 493 | if (counter.deletedNodes > 0) {
|
---|
[8846] | 494 | nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+')';
|
---|
[2388] | 495 | }
|
---|
[2386] | 496 |
|
---|
| 497 | String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
|
---|
[2388] | 498 | if (counter.deletedWays > 0) {
|
---|
[8846] | 499 | wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+')';
|
---|
[2388] | 500 | }
|
---|
[2386] | 501 |
|
---|
| 502 | String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
|
---|
[2388] | 503 | if (counter.deletedRelations > 0) {
|
---|
[8846] | 504 | relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+')';
|
---|
[2388] | 505 | }
|
---|
[2386] | 506 |
|
---|
[1890] | 507 | p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
|
---|
[8510] | 508 | p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
|
---|
| 509 | p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
|
---|
| 510 | p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15, 0, 0, 0));
|
---|
| 511 | p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))),
|
---|
| 512 | GBC.eop().insets(15, 0, 0, 0));
|
---|
[5025] | 513 | if (isUploadDiscouraged()) {
|
---|
[8510] | 514 | p.add(new JLabel(tr("Upload is discouraged")), GBC.eop().insets(15, 0, 0, 0));
|
---|
[5025] | 515 | }
|
---|
[1523] | 516 |
|
---|
[1078] | 517 | return p;
|
---|
| 518 | }
|
---|
[444] | 519 |
|
---|
[3408] | 520 | @Override public Action[] getMenuEntries() {
|
---|
[7005] | 521 | List<Action> actions = new ArrayList<>();
|
---|
[5025] | 522 | actions.addAll(Arrays.asList(new Action[]{
|
---|
[3408] | 523 | LayerListDialog.getInstance().createActivateLayerAction(this),
|
---|
| 524 | LayerListDialog.getInstance().createShowHideLayerAction(),
|
---|
| 525 | LayerListDialog.getInstance().createDeleteLayerAction(),
|
---|
| 526 | SeparatorLayerAction.INSTANCE,
|
---|
| 527 | LayerListDialog.getInstance().createMergeLayerAction(this),
|
---|
[8728] | 528 | LayerListDialog.getInstance().createDuplicateLayerAction(this),
|
---|
[3408] | 529 | new LayerSaveAction(this),
|
---|
| 530 | new LayerSaveAsAction(this),
|
---|
[6039] | 531 | }));
|
---|
| 532 | if (ExpertToggleAction.isExpert()) {
|
---|
| 533 | actions.addAll(Arrays.asList(new Action[]{
|
---|
| 534 | new LayerGpxExportAction(this),
|
---|
| 535 | new ConvertToGpxLayerAction()}));
|
---|
| 536 | }
|
---|
| 537 | actions.addAll(Arrays.asList(new Action[]{
|
---|
[3408] | 538 | SeparatorLayerAction.INSTANCE,
|
---|
[5025] | 539 | new RenameLayerAction(getAssociatedFile(), this)}));
|
---|
| 540 | if (ExpertToggleAction.isExpert() && Main.pref.getBoolean("data.layer.upload_discouragement.menu_item", false)) {
|
---|
| 541 | actions.add(new ToggleUploadDiscouragedLayerAction(this));
|
---|
| 542 | }
|
---|
| 543 | actions.addAll(Arrays.asList(new Action[]{
|
---|
[3408] | 544 | new ConsistencyTestAction(),
|
---|
| 545 | SeparatorLayerAction.INSTANCE,
|
---|
[5025] | 546 | new LayerListPopup.InfoAction(this)}));
|
---|
[6083] | 547 | return actions.toArray(new Action[actions.size()]);
|
---|
[1078] | 548 | }
|
---|
[444] | 549 |
|
---|
[7414] | 550 | /**
|
---|
| 551 | * Converts given OSM dataset to GPX data.
|
---|
| 552 | * @param data OSM dataset
|
---|
| 553 | * @param file output .gpx file
|
---|
| 554 | * @return GPX data
|
---|
| 555 | */
|
---|
[1508] | 556 | public static GpxData toGpxData(DataSet data, File file) {
|
---|
[1078] | 557 | GpxData gpxData = new GpxData();
|
---|
[1508] | 558 | gpxData.storageFile = file;
|
---|
[8338] | 559 | Set<Node> doneNodes = new HashSet<>();
|
---|
[7414] | 560 | waysToGpxData(data.getWays(), gpxData, doneNodes);
|
---|
| 561 | nodesToGpxData(data.getNodes(), gpxData, doneNodes);
|
---|
| 562 | return gpxData;
|
---|
| 563 | }
|
---|
| 564 |
|
---|
[8338] | 565 | private static void waysToGpxData(Collection<Way> ways, GpxData gpxData, Set<Node> doneNodes) {
|
---|
[8807] | 566 | /* When the dataset has been obtained from a gpx layer and now is being converted back,
|
---|
| 567 | * the ways have negative ids. The first created way corresponds to the first gpx segment,
|
---|
| 568 | * and has the highest id (i.e., closest to zero).
|
---|
| 569 | * Thus, sorting by OsmPrimitive#getUniqueId gives the original order.
|
---|
| 570 | * (Only works if the data layer has not been saved to and been loaded from an osm file before.)
|
---|
| 571 | */
|
---|
| 572 | final List<Way> sortedWays = new ArrayList<>(ways);
|
---|
| 573 | Collections.sort(sortedWays, new OsmPrimitiveComparator(true, false)); // sort by OsmPrimitive#getUniqueId ascending
|
---|
| 574 | Collections.reverse(sortedWays); // sort by OsmPrimitive#getUniqueId descending
|
---|
| 575 | for (Way w : sortedWays) {
|
---|
[2120] | 576 | if (!w.isUsable()) {
|
---|
[1670] | 577 | continue;
|
---|
| 578 | }
|
---|
[7005] | 579 | Collection<Collection<WayPoint>> trk = new ArrayList<>();
|
---|
| 580 | Map<String, Object> trkAttr = new HashMap<>();
|
---|
[508] | 581 |
|
---|
[1670] | 582 | if (w.get("name") != null) {
|
---|
[2907] | 583 | trkAttr.put("name", w.get("name"));
|
---|
[1670] | 584 | }
|
---|
[444] | 585 |
|
---|
[3669] | 586 | List<WayPoint> trkseg = null;
|
---|
[1898] | 587 | for (Node n : w.getNodes()) {
|
---|
[2120] | 588 | if (!n.isUsable()) {
|
---|
[1078] | 589 | trkseg = null;
|
---|
| 590 | continue;
|
---|
| 591 | }
|
---|
| 592 | if (trkseg == null) {
|
---|
[7005] | 593 | trkseg = new ArrayList<>();
|
---|
[2907] | 594 | trk.add(trkseg);
|
---|
[1078] | 595 | }
|
---|
[1499] | 596 | if (!n.isTagged()) {
|
---|
[1078] | 597 | doneNodes.add(n);
|
---|
| 598 | }
|
---|
[8158] | 599 | trkseg.add(nodeToWayPoint(n));
|
---|
[1078] | 600 | }
|
---|
[2907] | 601 |
|
---|
| 602 | gpxData.tracks.add(new ImmutableGpxTrack(trk, trkAttr));
|
---|
[1078] | 603 | }
|
---|
[7414] | 604 | }
|
---|
[1169] | 605 |
|
---|
[8158] | 606 | private static WayPoint nodeToWayPoint(Node n) {
|
---|
| 607 | WayPoint wpt = new WayPoint(n.getCoor());
|
---|
[7518] | 608 |
|
---|
[8158] | 609 | // Position info
|
---|
[7518] | 610 |
|
---|
[8158] | 611 | addDoubleIfPresent(wpt, n, GpxConstants.PT_ELE);
|
---|
[7518] | 612 |
|
---|
[8158] | 613 | if (!n.isTimestampEmpty()) {
|
---|
[8565] | 614 | wpt.put("time", DateUtils.fromTimestamp(n.getRawTimestamp()));
|
---|
[8158] | 615 | wpt.setTime();
|
---|
| 616 | }
|
---|
[7518] | 617 |
|
---|
[8158] | 618 | addDoubleIfPresent(wpt, n, GpxConstants.PT_MAGVAR);
|
---|
| 619 | addDoubleIfPresent(wpt, n, GpxConstants.PT_GEOIDHEIGHT);
|
---|
[7518] | 620 |
|
---|
[8158] | 621 | // Description info
|
---|
[7518] | 622 |
|
---|
[8158] | 623 | addStringIfPresent(wpt, n, GpxConstants.GPX_NAME);
|
---|
| 624 | addStringIfPresent(wpt, n, GpxConstants.GPX_DESC, "description");
|
---|
| 625 | addStringIfPresent(wpt, n, GpxConstants.GPX_CMT, "comment");
|
---|
| 626 | addStringIfPresent(wpt, n, GpxConstants.GPX_SRC, "source", "source:position");
|
---|
[7518] | 627 |
|
---|
[8158] | 628 | Collection<GpxLink> links = new ArrayList<>();
|
---|
| 629 | for (String key : new String[]{"link", "url", "website", "contact:website"}) {
|
---|
| 630 | String value = n.get(key);
|
---|
| 631 | if (value != null) {
|
---|
| 632 | links.add(new GpxLink(value));
|
---|
[4087] | 633 | }
|
---|
[8158] | 634 | }
|
---|
| 635 | wpt.put(GpxConstants.META_LINKS, links);
|
---|
[4046] | 636 |
|
---|
[8158] | 637 | addStringIfPresent(wpt, n, GpxConstants.PT_SYM, "wpt_symbol");
|
---|
| 638 | addStringIfPresent(wpt, n, GpxConstants.PT_TYPE);
|
---|
[7518] | 639 |
|
---|
[8158] | 640 | // Accuracy info
|
---|
| 641 | addStringIfPresent(wpt, n, GpxConstants.PT_FIX, "gps:fix");
|
---|
| 642 | addIntegerIfPresent(wpt, n, GpxConstants.PT_SAT, "gps:sat");
|
---|
| 643 | addDoubleIfPresent(wpt, n, GpxConstants.PT_HDOP, "gps:hdop");
|
---|
| 644 | addDoubleIfPresent(wpt, n, GpxConstants.PT_VDOP, "gps:vdop");
|
---|
| 645 | addDoubleIfPresent(wpt, n, GpxConstants.PT_PDOP, "gps:pdop");
|
---|
| 646 | addDoubleIfPresent(wpt, n, GpxConstants.PT_AGEOFDGPSDATA, "gps:ageofdgpsdata");
|
---|
| 647 | addIntegerIfPresent(wpt, n, GpxConstants.PT_DGPSID, "gps:dgpsid");
|
---|
[7518] | 648 |
|
---|
[8158] | 649 | return wpt;
|
---|
| 650 | }
|
---|
| 651 |
|
---|
[8338] | 652 | private static void nodesToGpxData(Collection<Node> nodes, GpxData gpxData, Set<Node> doneNodes) {
|
---|
[8158] | 653 | List<Node> sortedNodes = new ArrayList<>(nodes);
|
---|
| 654 | sortedNodes.removeAll(doneNodes);
|
---|
| 655 | Collections.sort(sortedNodes);
|
---|
| 656 | for (Node n : sortedNodes) {
|
---|
| 657 | if (n.isIncomplete() || n.isDeleted()) {
|
---|
| 658 | continue;
|
---|
| 659 | }
|
---|
| 660 | gpxData.waypoints.add(nodeToWayPoint(n));
|
---|
[1078] | 661 | }
|
---|
| 662 | }
|
---|
[444] | 663 |
|
---|
[7518] | 664 | private static void addIntegerIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String ... osmKeys) {
|
---|
[8338] | 665 | List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
|
---|
[7518] | 666 | possibleKeys.add(0, gpxKey);
|
---|
| 667 | for (String key : possibleKeys) {
|
---|
| 668 | String value = p.get(key);
|
---|
| 669 | if (value != null) {
|
---|
| 670 | try {
|
---|
| 671 | int i = Integer.parseInt(value);
|
---|
| 672 | // Sanity checks
|
---|
| 673 | if ((!GpxConstants.PT_SAT.equals(gpxKey) || i >= 0) &&
|
---|
| 674 | (!GpxConstants.PT_DGPSID.equals(gpxKey) || (0 <= i && i <= 1023))) {
|
---|
| 675 | wpt.put(gpxKey, value);
|
---|
| 676 | break;
|
---|
| 677 | }
|
---|
| 678 | } catch (NumberFormatException e) {
|
---|
| 679 | if (Main.isTraceEnabled()) {
|
---|
| 680 | Main.trace(e.getMessage());
|
---|
| 681 | }
|
---|
| 682 | }
|
---|
| 683 | }
|
---|
| 684 | }
|
---|
| 685 | }
|
---|
| 686 |
|
---|
| 687 | private static void addDoubleIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String ... osmKeys) {
|
---|
[8338] | 688 | List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
|
---|
[7518] | 689 | possibleKeys.add(0, gpxKey);
|
---|
| 690 | for (String key : possibleKeys) {
|
---|
| 691 | String value = p.get(key);
|
---|
| 692 | if (value != null) {
|
---|
| 693 | try {
|
---|
| 694 | double d = Double.parseDouble(value);
|
---|
| 695 | // Sanity checks
|
---|
| 696 | if (!GpxConstants.PT_MAGVAR.equals(gpxKey) || (0.0 <= d && d < 360.0)) {
|
---|
| 697 | wpt.put(gpxKey, value);
|
---|
| 698 | break;
|
---|
| 699 | }
|
---|
| 700 | } catch (NumberFormatException e) {
|
---|
| 701 | if (Main.isTraceEnabled()) {
|
---|
| 702 | Main.trace(e.getMessage());
|
---|
| 703 | }
|
---|
| 704 | }
|
---|
| 705 | }
|
---|
| 706 | }
|
---|
| 707 | }
|
---|
| 708 |
|
---|
| 709 | private static void addStringIfPresent(WayPoint wpt, OsmPrimitive p, String gpxKey, String ... osmKeys) {
|
---|
[8338] | 710 | List<String> possibleKeys = new ArrayList<>(Arrays.asList(osmKeys));
|
---|
[7518] | 711 | possibleKeys.add(0, gpxKey);
|
---|
| 712 | for (String key : possibleKeys) {
|
---|
| 713 | String value = p.get(key);
|
---|
| 714 | if (value != null) {
|
---|
| 715 | // Sanity checks
|
---|
| 716 | if (!GpxConstants.PT_FIX.equals(gpxKey) || GpxConstants.FIX_VALUES.contains(value)) {
|
---|
| 717 | wpt.put(gpxKey, value);
|
---|
| 718 | break;
|
---|
| 719 | }
|
---|
| 720 | }
|
---|
| 721 | }
|
---|
| 722 | }
|
---|
| 723 |
|
---|
[7414] | 724 | /**
|
---|
| 725 | * Converts OSM data behind this layer to GPX data.
|
---|
| 726 | * @return GPX data
|
---|
| 727 | */
|
---|
[1078] | 728 | public GpxData toGpxData() {
|
---|
[1646] | 729 | return toGpxData(data, getAssociatedFile());
|
---|
[1078] | 730 | }
|
---|
[628] | 731 |
|
---|
[7414] | 732 | /**
|
---|
| 733 | * Action that converts this OSM layer to a GPX layer.
|
---|
| 734 | */
|
---|
[1078] | 735 | public class ConvertToGpxLayerAction extends AbstractAction {
|
---|
[7414] | 736 | /**
|
---|
| 737 | * Constructs a new {@code ConvertToGpxLayerAction}.
|
---|
| 738 | */
|
---|
[1078] | 739 | public ConvertToGpxLayerAction() {
|
---|
| 740 | super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx"));
|
---|
[4701] | 741 | putValue("help", ht("/Action/ConvertToGpxLayer"));
|
---|
[1078] | 742 | }
|
---|
[8510] | 743 |
|
---|
[6084] | 744 | @Override
|
---|
[1078] | 745 | public void actionPerformed(ActionEvent e) {
|
---|
[8805] | 746 | final GpxData data = toGpxData();
|
---|
| 747 | final GpxLayer gpxLayer = new GpxLayer(data, tr("Converted from: {0}", getName()));
|
---|
[9481] | 748 | if (getAssociatedFile() != null) {
|
---|
| 749 | final String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + "$", "") + ".gpx";
|
---|
| 750 | gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
|
---|
| 751 | }
|
---|
[8805] | 752 | Main.main.addLayer(gpxLayer);
|
---|
| 753 | if (Main.pref.getBoolean("marker.makeautomarkers", true) && !data.waypoints.isEmpty()) {
|
---|
| 754 | Main.main.addLayer(new MarkerLayer(data, tr("Converted from: {0}", getName()), null, gpxLayer));
|
---|
| 755 | }
|
---|
[1078] | 756 | Main.main.removeLayer(OsmDataLayer.this);
|
---|
| 757 | }
|
---|
| 758 | }
|
---|
| 759 |
|
---|
[7414] | 760 | /**
|
---|
| 761 | * Determines if this layer contains data at the given coordinate.
|
---|
| 762 | * @param coor the coordinate
|
---|
| 763 | * @return {@code true} if data sources bounding boxes contain {@code coor}
|
---|
| 764 | */
|
---|
[1523] | 765 | public boolean containsPoint(LatLon coor) {
|
---|
[628] | 766 | // we'll assume that if this has no data sources
|
---|
| 767 | // that it also has no borders
|
---|
| 768 | if (this.data.dataSources.isEmpty())
|
---|
| 769 | return true;
|
---|
| 770 |
|
---|
| 771 | boolean layer_bounds_point = false;
|
---|
| 772 | for (DataSource src : this.data.dataSources) {
|
---|
| 773 | if (src.bounds.contains(coor)) {
|
---|
| 774 | layer_bounds_point = true;
|
---|
| 775 | break;
|
---|
| 776 | }
|
---|
| 777 | }
|
---|
| 778 | return layer_bounds_point;
|
---|
| 779 | }
|
---|
[1750] | 780 |
|
---|
| 781 | /**
|
---|
[7414] | 782 | * Replies the set of conflicts currently managed in this layer.
|
---|
[1823] | 783 | *
|
---|
[1750] | 784 | * @return the set of conflicts currently managed in this layer
|
---|
| 785 | */
|
---|
| 786 | public ConflictCollection getConflicts() {
|
---|
| 787 | return conflicts;
|
---|
| 788 | }
|
---|
[2025] | 789 |
|
---|
[7358] | 790 | @Override
|
---|
[2025] | 791 | public boolean requiresUploadToServer() {
|
---|
| 792 | return requiresUploadToServer;
|
---|
| 793 | }
|
---|
| 794 |
|
---|
[7358] | 795 | @Override
|
---|
[2025] | 796 | public boolean requiresSaveToFile() {
|
---|
| 797 | return getAssociatedFile() != null && requiresSaveToFile;
|
---|
| 798 | }
|
---|
| 799 |
|
---|
[5459] | 800 | @Override
|
---|
[2025] | 801 | public void onPostLoadFromFile() {
|
---|
| 802 | setRequiresSaveToFile(false);
|
---|
[7358] | 803 | setRequiresUploadToServer(isModified());
|
---|
[2025] | 804 | }
|
---|
| 805 |
|
---|
[7414] | 806 | /**
|
---|
| 807 | * Actions run after data has been downloaded to this layer.
|
---|
| 808 | */
|
---|
[2434] | 809 | public void onPostDownloadFromServer() {
|
---|
| 810 | setRequiresSaveToFile(true);
|
---|
[7358] | 811 | setRequiresUploadToServer(isModified());
|
---|
[2434] | 812 | }
|
---|
| 813 |
|
---|
[3116] | 814 | @Override
|
---|
| 815 | public boolean isChanged() {
|
---|
[3118] | 816 | return isChanged || highlightUpdateCount != data.getHighlightUpdateCount();
|
---|
[3116] | 817 | }
|
---|
| 818 |
|
---|
[7358] | 819 | @Override
|
---|
[2025] | 820 | public void onPostSaveToFile() {
|
---|
| 821 | setRequiresSaveToFile(false);
|
---|
[7358] | 822 | setRequiresUploadToServer(isModified());
|
---|
[2025] | 823 | }
|
---|
| 824 |
|
---|
[7358] | 825 | @Override
|
---|
[2025] | 826 | public void onPostUploadToServer() {
|
---|
[7358] | 827 | setRequiresUploadToServer(isModified());
|
---|
[2025] | 828 | // keep requiresSaveToDisk unchanged
|
---|
| 829 | }
|
---|
[2500] | 830 |
|
---|
| 831 | private class ConsistencyTestAction extends AbstractAction {
|
---|
| 832 |
|
---|
[8836] | 833 | ConsistencyTestAction() {
|
---|
[2500] | 834 | super(tr("Dataset consistency test"));
|
---|
| 835 | }
|
---|
| 836 |
|
---|
[6084] | 837 | @Override
|
---|
[2500] | 838 | public void actionPerformed(ActionEvent e) {
|
---|
| 839 | String result = DatasetConsistencyTest.runTests(data);
|
---|
[8394] | 840 | if (result.isEmpty()) {
|
---|
[2500] | 841 | JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
|
---|
| 842 | } else {
|
---|
| 843 | JPanel p = new JPanel(new GridBagLayout());
|
---|
| 844 | p.add(new JLabel(tr("Following problems found:")), GBC.eol());
|
---|
[5886] | 845 | JosmTextArea info = new JosmTextArea(result, 20, 60);
|
---|
[2500] | 846 | info.setCaretPosition(0);
|
---|
| 847 | info.setEditable(false);
|
---|
| 848 | p.add(new JScrollPane(info), GBC.eop());
|
---|
| 849 |
|
---|
| 850 | JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
|
---|
| 851 | }
|
---|
| 852 | }
|
---|
| 853 | }
|
---|
[3116] | 854 |
|
---|
[3245] | 855 | @Override
|
---|
| 856 | public void destroy() {
|
---|
[3416] | 857 | DataSet.removeSelectionListener(this);
|
---|
[3245] | 858 | }
|
---|
| 859 |
|
---|
[6084] | 860 | @Override
|
---|
[3116] | 861 | public void processDatasetEvent(AbstractDatasetChangedEvent event) {
|
---|
| 862 | isChanged = true;
|
---|
[3225] | 863 | setRequiresSaveToFile(true);
|
---|
| 864 | setRequiresUploadToServer(true);
|
---|
[3116] | 865 | }
|
---|
| 866 |
|
---|
[6084] | 867 | @Override
|
---|
[3116] | 868 | public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
|
---|
| 869 | isChanged = true;
|
---|
| 870 | }
|
---|
[4126] | 871 |
|
---|
| 872 | @Override
|
---|
| 873 | public void projectionChanged(Projection oldValue, Projection newValue) {
|
---|
[7414] | 874 | // No reprojection required. The dataset itself is registered as projection
|
---|
| 875 | // change listener and already got notified.
|
---|
[4126] | 876 | }
|
---|
[5025] | 877 |
|
---|
[7358] | 878 | @Override
|
---|
[5025] | 879 | public final boolean isUploadDiscouraged() {
|
---|
| 880 | return data.isUploadDiscouraged();
|
---|
| 881 | }
|
---|
| 882 |
|
---|
[7414] | 883 | /**
|
---|
| 884 | * Sets the "discouraged upload" flag.
|
---|
[8540] | 885 | * @param uploadDiscouraged {@code true} if upload of data managed by this layer is discouraged.
|
---|
| 886 | * This feature allows to use "private" data layers.
|
---|
[7414] | 887 | */
|
---|
[5025] | 888 | public final void setUploadDiscouraged(boolean uploadDiscouraged) {
|
---|
[5519] | 889 | if (uploadDiscouraged ^ isUploadDiscouraged()) {
|
---|
| 890 | data.setUploadDiscouraged(uploadDiscouraged);
|
---|
| 891 | for (LayerStateChangeListener l : layerStateChangeListeners) {
|
---|
| 892 | l.uploadDiscouragedChanged(this, uploadDiscouraged);
|
---|
| 893 | }
|
---|
| 894 | }
|
---|
[5025] | 895 | }
|
---|
[5459] | 896 |
|
---|
| 897 | @Override
|
---|
[7358] | 898 | public final boolean isModified() {
|
---|
| 899 | return data.isModified();
|
---|
| 900 | }
|
---|
| 901 |
|
---|
| 902 | @Override
|
---|
[5459] | 903 | public boolean isSavable() {
|
---|
| 904 | return true; // With OsmExporter
|
---|
| 905 | }
|
---|
| 906 |
|
---|
| 907 | @Override
|
---|
| 908 | public boolean checkSaveConditions() {
|
---|
| 909 | if (isDataSetEmpty()) {
|
---|
[7204] | 910 | if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() {
|
---|
| 911 | @Override
|
---|
| 912 | public Integer call() {
|
---|
| 913 | ExtendedDialog dialog = new ExtendedDialog(
|
---|
| 914 | Main.parent,
|
---|
| 915 | tr("Empty document"),
|
---|
| 916 | new String[] {tr("Save anyway"), tr("Cancel")}
|
---|
| 917 | );
|
---|
| 918 | dialog.setContent(tr("The document contains no data."));
|
---|
[8061] | 919 | dialog.setButtonIcons(new String[] {"save", "cancel"});
|
---|
[7204] | 920 | return dialog.showDialog().getValue();
|
---|
| 921 | }
|
---|
| 922 | })) {
|
---|
| 923 | return false;
|
---|
| 924 | }
|
---|
[5459] | 925 | }
|
---|
| 926 |
|
---|
| 927 | ConflictCollection conflicts = getConflicts();
|
---|
| 928 | if (conflicts != null && !conflicts.isEmpty()) {
|
---|
[7204] | 929 | if (1 != GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() {
|
---|
| 930 | @Override
|
---|
| 931 | public Integer call() {
|
---|
| 932 | ExtendedDialog dialog = new ExtendedDialog(
|
---|
| 933 | Main.parent,
|
---|
| 934 | /* I18N: Display title of the window showing conflicts */
|
---|
| 935 | tr("Conflicts"),
|
---|
| 936 | new String[] {tr("Reject Conflicts and Save"), tr("Cancel")}
|
---|
| 937 | );
|
---|
[8509] | 938 | dialog.setContent(
|
---|
| 939 | tr("There are unresolved conflicts. Conflicts will not be saved and handled as if you rejected all. Continue?"));
|
---|
[8061] | 940 | dialog.setButtonIcons(new String[] {"save", "cancel"});
|
---|
[7204] | 941 | return dialog.showDialog().getValue();
|
---|
| 942 | }
|
---|
| 943 | })) {
|
---|
| 944 | return false;
|
---|
| 945 | }
|
---|
[5459] | 946 | }
|
---|
| 947 | return true;
|
---|
| 948 | }
|
---|
[6070] | 949 |
|
---|
[5459] | 950 | /**
|
---|
| 951 | * Check the data set if it would be empty on save. It is empty, if it contains
|
---|
| 952 | * no objects (after all objects that are created and deleted without being
|
---|
| 953 | * transferred to the server have been removed).
|
---|
| 954 | *
|
---|
| 955 | * @return <code>true</code>, if a save result in an empty data set.
|
---|
| 956 | */
|
---|
| 957 | private boolean isDataSetEmpty() {
|
---|
| 958 | if (data != null) {
|
---|
[8513] | 959 | for (OsmPrimitive osm : data.allNonDeletedPrimitives()) {
|
---|
[5459] | 960 | if (!osm.isDeleted() || !osm.isNewOrUndeleted())
|
---|
| 961 | return false;
|
---|
[8513] | 962 | }
|
---|
[5459] | 963 | }
|
---|
| 964 | return true;
|
---|
| 965 | }
|
---|
| 966 |
|
---|
| 967 | @Override
|
---|
| 968 | public File createAndOpenSaveFileChooser() {
|
---|
[9461] | 969 | String extension = Main.pref.get("save.extension.osm", "osm");
|
---|
| 970 | return SaveActionBase.createAndOpenSaveFileChooser(tr("Save OSM file"), extension);
|
---|
[5459] | 971 | }
|
---|
[7358] | 972 |
|
---|
| 973 | @Override
|
---|
| 974 | public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
|
---|
| 975 | UploadDialog dialog = UploadDialog.getUploadDialog();
|
---|
| 976 | return new UploadLayerTask(
|
---|
| 977 | dialog.getUploadStrategySpecification(),
|
---|
| 978 | this,
|
---|
| 979 | monitor,
|
---|
| 980 | dialog.getChangeset());
|
---|
| 981 | }
|
---|
| 982 |
|
---|
| 983 | @Override
|
---|
| 984 | public AbstractUploadDialog getUploadDialog() {
|
---|
| 985 | UploadDialog dialog = UploadDialog.getUploadDialog();
|
---|
| 986 | dialog.setUploadedPrimitives(new APIDataSet(data));
|
---|
| 987 | return dialog;
|
---|
| 988 | }
|
---|
[444] | 989 | }
|
---|