| | 1 | // License: GPL. For details, see LICENSE file. |
| | 2 | package org.openstreetmap.josm.gui.colocation; |
| | 3 | |
| | 4 | import org.openstreetmap.josm.data.coor.LatLon; |
| | 5 | import org.openstreetmap.josm.gui.ExtendedDialog; |
| | 6 | import org.openstreetmap.josm.gui.MainApplication; |
| | 7 | import org.openstreetmap.josm.gui.util.GuiHelper; |
| | 8 | import org.openstreetmap.josm.spi.preferences.Config; |
| | 9 | |
| | 10 | import javax.swing.ButtonGroup; |
| | 11 | import javax.swing.ButtonModel; |
| | 12 | import javax.swing.JCheckBox; |
| | 13 | import javax.swing.JLabel; |
| | 14 | import javax.swing.JOptionPane; |
| | 15 | import javax.swing.JPanel; |
| | 16 | import javax.swing.JRadioButton; |
| | 17 | import javax.swing.event.ChangeEvent; |
| | 18 | import java.awt.BorderLayout; |
| | 19 | import java.awt.FlowLayout; |
| | 20 | import java.util.HashMap; |
| | 21 | import java.util.Map; |
| | 22 | |
| | 23 | import 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 | */ |
| | 30 | public 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 | } |