Ticket #20898: colocated-nodes.patch

File colocated-nodes.patch, 15.2 KB (added by ljdelight, 8 months ago)
  • new file src/org/openstreetmap/josm/gui/colocation/ColocatedNodesResolver.java

    diff --git src/org/openstreetmap/josm/gui/colocation/ColocatedNodesResolver.java src/org/openstreetmap/josm/gui/colocation/ColocatedNodesResolver.java
    new file mode 100644
    index 000000000..4887233ee
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.colocation;
     3
     4import org.openstreetmap.josm.data.coor.LatLon;
     5import org.openstreetmap.josm.gui.ExtendedDialog;
     6import org.openstreetmap.josm.gui.MainApplication;
     7import org.openstreetmap.josm.gui.util.GuiHelper;
     8import org.openstreetmap.josm.spi.preferences.Config;
     9
     10import javax.swing.ButtonGroup;
     11import javax.swing.ButtonModel;
     12import javax.swing.JCheckBox;
     13import javax.swing.JLabel;
     14import javax.swing.JOptionPane;
     15import javax.swing.JPanel;
     16import javax.swing.JRadioButton;
     17import javax.swing.event.ChangeEvent;
     18import java.awt.BorderLayout;
     19import java.awt.FlowLayout;
     20import java.util.HashMap;
     21import java.util.Map;
     22
     23import static org.openstreetmap.josm.tools.I18n.tr;
     24
     25/**
     26 * Co-located node resolver is used to detect nodes that are at the same LatLon point during a geojson import.
     27 *
     28 * @since xxx
     29 */
     30public class ColocatedNodesResolver {
     31    // Resolution choices. Strings are used for easy interoperability with
     32    // preference storage
     33    /**
     34     * Keep only one node found at location (recommended behavior)
     35     */
     36    public static final String RESOLVE_KEEP_ONE = "one";
     37
     38    /**
     39     * Keep all nodes found at location
     40     */
     41    public static final String RESOLVE_KEEP_ALL = "all";
     42
     43    // Future application choices
     44    /**
     45     * Prompt user to manually resolve next future incident
     46     */
     47    public static final int APPLY_PROMPT = 1;
     48
     49    /**
     50     * Resolve detected colocations in chosen manner for all nodes sharing the
     51     * same location as this incident, but prompting user again for new
     52     * locations
     53     */
     54    public static final int APPLY_ALL_AT_LOCATION = 2;
     55
     56    /**
     57     * Resolve detected colocations in chosen manner for all further incidents
     58     * and do not prompt user further
     59     */
     60    public static final int APPLY_ALL = 3;
     61
     62    /**
     63     * The current resolution choice for this resolver (see RESOLVE_* constants)
     64     */
     65    private String currentResolution;
     66
     67    /**
     68     * The current application choice for this resolver (see APPLY_* constants)
     69     */
     70    private int currentApplication;
     71
     72    /**
     73     * Map of locations to resolution decision. An entry exists when the user has chosen to apply a
     74     * resolution to all future detected nodes at that location
     75     */
     76    private final Map<LatLon, String> locationDecisions;
     77
     78    /**
     79     * Constructs a ColocatedNodesResolver with the backward-compatible behavior of
     80     * automatically keeping only one node found at a location
     81     */
     82    public ColocatedNodesResolver() {
     83        this(RESOLVE_KEEP_ONE, APPLY_ALL);
     84    }
     85
     86    public ColocatedNodesResolver(final String defaultResolution, final int defaultApplication) {
     87        this.currentResolution = defaultResolution;
     88        this.currentApplication = defaultApplication;
     89        this.locationDecisions = new HashMap<>();
     90    }
     91
     92    /**
     93     * Resolves detected colocation at a specified location, either using a
     94     * decision made in past that is to apply to future colocations, or else
     95     * prompting the user to make a decision
     96     *
     97     * @param latlon the location to check for a resolution
     98     * @return the resolution
     99     */
     100    public String resolveColocatedNodes(final LatLon latlon) {
     101        // First, is there a decision specific to this location?
     102        if (this.locationDecisions.containsKey(latlon)) {
     103            return this.locationDecisions.get(latlon);
     104        }
     105
     106        // Next, is there a decision to apply to all detected colocations?
     107        if (this.currentApplication == APPLY_ALL) {
     108            return this.currentResolution;
     109        }
     110
     111        // Is there a previously saved choice stored in preferences?
     112        final String preferenceKey = "import.colocated-nodes.keep";
     113        final String resolution = Config.getPref().get(preferenceKey, null);
     114
     115        if (RESOLVE_KEEP_ONE.equals(resolution) || RESOLVE_KEEP_ALL.equals(resolution)) {
     116            return resolution;
     117        }
     118
     119        // Otherwise ask the user how to resolve
     120        GuiHelper.runInEDTAndWait(() -> {
     121            ResolveDialog dialog = new ResolveDialog(latlon, this.currentApplication);
     122            dialog.showDialog();
     123            switch (dialog.getValue()) {
     124                case 1:
     125                    this.currentResolution = RESOLVE_KEEP_ONE;
     126                    break;
     127                case 2:
     128                    this.currentResolution = RESOLVE_KEEP_ALL;
     129                    break;
     130            }
     131            this.currentApplication = dialog.getApplyToValue();
     132
     133            if (this.currentApplication == APPLY_ALL_AT_LOCATION) {
     134                this.locationDecisions.put(latlon, this.currentResolution);
     135            }
     136
     137            if (dialog.shouldSaveChoice()) {
     138                Config.getPref().put(preferenceKey, this.currentResolution);
     139            }
     140        });
     141
     142        return this.currentResolution;
     143    }
     144
     145    /**
     146     * Dialog that prompts user to decide how to treat detected colocated nodes
     147     */
     148    private static class ResolveDialog extends ExtendedDialog {
     149        private final ButtonGroup applyOptionsGroup;
     150        private final JCheckBox rememberCheckbox;
     151
     152        ResolveDialog(final LatLon latlon, final int currentApplication) {
     153            super(MainApplication.getMainFrame(),
     154                    tr("Resolve Co-located Nodes"),
     155                    tr("Keep One Node (recommended)"), tr("Keep All Nodes"));
     156
     157            setIcon(JOptionPane.WARNING_MESSAGE);
     158            JPanel dialogPanel = new JPanel(new BorderLayout());
     159            JPanel rememberChoicePanel = new JPanel(new BorderLayout());
     160            dialogPanel.add(new JLabel("<html>"
     161                            + tr("Import contains multiple nodes positioned at ")
     162                            + latlon.toDisplayString()
     163                            + ".<br/>"
     164                            + tr("How would you like to proceed with these nodes?")
     165                            + "<br/><br/>"
     166                            + "</html>"),
     167                    BorderLayout.NORTH);
     168
     169            // Options for applying chosen resolution to future colocated nodes
     170            JPanel applyOptionsPanel = new JPanel(new FlowLayout());
     171            JPanel buttonGroupPanel = new JPanel(new FlowLayout());
     172            this.applyOptionsGroup = new ButtonGroup();
     173
     174            JRadioButton locationOption = new JRadioButton(tr("This location"));
     175            locationOption.setActionCommand("location");
     176            this.applyOptionsGroup.add(locationOption);
     177            locationOption.setSelected(currentApplication == APPLY_ALL_AT_LOCATION);
     178            buttonGroupPanel.add(locationOption);
     179
     180            JRadioButton allOption = new JRadioButton(tr("All locations"));
     181            allOption.setActionCommand("all");
     182            this.applyOptionsGroup.add(allOption);
     183            allOption.setSelected(currentApplication != APPLY_ALL_AT_LOCATION);
     184
     185            // Listen for changes to the applies-to-all radio button, as we
     186            // only want to offer the option to remember choice if the choice
     187            // is being applied to all nodes rather than a single location, as
     188            // we naturally have to prompt for each new location if user wants
     189            // per-location option
     190            allOption.addChangeListener((ChangeEvent e) -> {
     191                if (allOption.isSelected()) {
     192                    if (!dialogPanel.isAncestorOf(rememberChoicePanel)) {
     193                        dialogPanel.add(rememberChoicePanel, BorderLayout.SOUTH);
     194                    }
     195                } else {
     196                    dialogPanel.remove(rememberChoicePanel);
     197                }
     198
     199                this.revalidate();
     200                this.pack();
     201                this.repaint();
     202            });
     203            buttonGroupPanel.add(allOption);
     204
     205            applyOptionsPanel.add(new JLabel(tr("Apply this choice to:")));
     206            applyOptionsPanel.add(buttonGroupPanel);
     207            dialogPanel.add(applyOptionsPanel, BorderLayout.CENTER);
     208
     209            // Remember this decision? Only available for all-locations choice,
     210            // as we naturally have to prompt for each new location if user
     211            // wants per-location option
     212            this.rememberCheckbox = new JCheckBox(tr("Don''t ask me again"));
     213            rememberChoicePanel.add(this.rememberCheckbox, BorderLayout.SOUTH);
     214            if (currentApplication != APPLY_ALL_AT_LOCATION) {
     215                dialogPanel.add(rememberChoicePanel, BorderLayout.SOUTH);
     216            }
     217
     218            setContent(dialogPanel);
     219        }
     220
     221        public int getApplyToValue() {
     222            final ButtonModel selected = this.applyOptionsGroup.getSelection();
     223            if (selected != null && "all".equals(selected.getActionCommand())) {
     224                return APPLY_ALL;
     225            } else {
     226                return APPLY_ALL_AT_LOCATION;
     227            }
     228        }
     229
     230        public boolean shouldSaveChoice() {
     231            // Only allow apply-to-all choices to be saved
     232            return this.getApplyToValue() == APPLY_ALL && this.rememberCheckbox.isSelected();
     233        }
     234    }
     235}
  • src/org/openstreetmap/josm/gui/io/importexport/GeoJSONImporter.java

    diff --git src/org/openstreetmap/josm/gui/io/importexport/GeoJSONImporter.java src/org/openstreetmap/josm/gui/io/importexport/GeoJSONImporter.java
    index c1e59d67e..94800c0b4 100644
    import org.openstreetmap.josm.gui.MainApplication; 
    1616import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    1717import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    1818import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     19import org.openstreetmap.josm.gui.colocation.ColocatedNodesResolver;
    1920import org.openstreetmap.josm.gui.util.GuiHelper;
    2021import org.openstreetmap.josm.io.CachedFile;
    2122import org.openstreetmap.josm.io.Compression;
    public class GeoJSONImporter extends FileImporter { 
    4849        progressMonitor.setTicksCount(2);
    4950        Logging.info("Parsing GeoJSON: {0}", file.getAbsolutePath());
    5051        try (InputStream fileInputStream = Compression.getUncompressedFileInputStream(file)) {
    51             DataSet data = GeoJSONReader.parseDataSet(fileInputStream, progressMonitor);
     52            ColocatedNodesResolver resolver = new ColocatedNodesResolver(
     53                    ColocatedNodesResolver.RESOLVE_KEEP_ONE, ColocatedNodesResolver.APPLY_PROMPT);
     54            DataSet data = GeoJSONReader.parseDataSet(fileInputStream, progressMonitor, resolver);
    5255            progressMonitor.worked(1);
    5356            MainApplication.getLayerManager().addLayer(new OsmDataLayer(data, file.getName(), file));
    5457        } catch (IOException | IllegalArgumentException | IllegalDataException e) {
  • src/org/openstreetmap/josm/io/GeoJSONReader.java

    diff --git src/org/openstreetmap/josm/io/GeoJSONReader.java src/org/openstreetmap/josm/io/GeoJSONReader.java
    index f389688a7..f8f004364 100644
    import org.openstreetmap.josm.data.validation.TestError; 
    4444import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
    4545import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    4646import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     47import org.openstreetmap.josm.gui.colocation.ColocatedNodesResolver;
    4748import org.openstreetmap.josm.tools.CheckParameterUtil;
    4849import org.openstreetmap.josm.tools.Logging;
    4950import org.openstreetmap.josm.tools.Utils;
    public class GeoJSONReader extends AbstractReader { 
    6566    /** The record separator is 0x1E per RFC 7464 */
    6667    private static final byte RECORD_SEPARATOR_BYTE = 0x1E;
    6768    private Projection projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
     69    private final ColocatedNodesResolver resolver;
    6870
    6971    GeoJSONReader() {
    7072        // Restricts visibility
     73        this.resolver = new ColocatedNodesResolver();
     74    }
     75
     76    GeoJSONReader(final ColocatedNodesResolver resolver) {
     77        this.resolver = resolver;
    7178    }
    7279
    7380    private void parse(final JsonParser parser) throws IllegalDataException {
    public class GeoJSONReader extends AbstractReader { 
    279286    private Node createNode(final LatLon latlon) {
    280287        final List<Node> existingNodes = getDataSet().searchNodes(new BBox(latlon, latlon));
    281288        if (!existingNodes.isEmpty()) {
    282             // reuse existing node, avoid multiple nodes on top of each other
    283             return existingNodes.get(0);
     289            if (ColocatedNodesResolver.RESOLVE_KEEP_ONE.equals(this.resolver.resolveColocatedNodes(latlon))) {
     290                // reuse existing node
     291                return existingNodes.get(0);
     292            }
    284293        }
    285294        final Node node = new Node(latlon);
    286295        getDataSet().addPrimitive(node);
    public class GeoJSONReader extends AbstractReader { 
    300309        final boolean doAutoclose;
    301310        if (size > 1) {
    302311            if (latlons.get(0).equals(latlons.get(size - 1))) {
    303                 doAutoclose = false; // already closed
     312                // Auto-close to avoid creating a dup final node
     313                latlons.remove(size - 1);
     314                doAutoclose = true;
    304315            } else {
    305316                doAutoclose = autoClose;
    306317            }
    public class GeoJSONReader extends AbstractReader { 
    464475    }
    465476
    466477    /**
    467      * Parse the given input source and return the dataset.
     478     * Parse the given input source and return the dataset, using default
     479     * node-colocation resolution rules
    468480     *
    469481     * @param source          the source input stream. Must not be null.
    470482     * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed
    public class GeoJSONReader extends AbstractReader { 
    473485     * @throws IllegalArgumentException if source is null
    474486     */
    475487    public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
    476         return new GeoJSONReader().doParseDataSet(source, progressMonitor);
     488        return GeoJSONReader.parseDataSet(source, progressMonitor, new ColocatedNodesResolver());
     489    }
     490
     491    /**
     492     * Parse the given input source and return the dataset.
     493     *
     494     * @param source          the source input stream. Must not be null.
     495     * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed
     496     * @param resolver        the resolver for determining outcome of nodes colocated in same location
     497     * @return the dataset with the parsed data
     498     * @throws IllegalDataException     if an error was found while parsing the data from the source
     499     * @throws IllegalArgumentException if source is null
     500     */
     501    public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor, ColocatedNodesResolver resolver)
     502            throws IllegalDataException {
     503        return new GeoJSONReader(resolver).doParseDataSet(source, progressMonitor);
    477504    }
    478505}