source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java@ 19050

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 18.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.tags;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.FlowLayout;
11import java.awt.Font;
12import java.awt.GridBagConstraints;
13import java.awt.GridBagLayout;
14import java.awt.Insets;
15import java.awt.event.ActionEvent;
16import java.beans.PropertyChangeEvent;
17import java.beans.PropertyChangeListener;
18import java.util.ArrayList;
19import java.util.EnumMap;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23import java.util.Map.Entry;
24import java.util.StringJoiner;
25import java.util.stream.IntStream;
26
27import javax.swing.AbstractAction;
28import javax.swing.Action;
29import javax.swing.ImageIcon;
30import javax.swing.JButton;
31import javax.swing.JDialog;
32import javax.swing.JLabel;
33import javax.swing.JPanel;
34import javax.swing.JTabbedPane;
35import javax.swing.JTable;
36import javax.swing.UIManager;
37import javax.swing.table.DefaultTableModel;
38import javax.swing.table.TableCellRenderer;
39
40import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
41import org.openstreetmap.josm.data.osm.TagCollection;
42import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder;
43import org.openstreetmap.josm.gui.util.GuiHelper;
44import org.openstreetmap.josm.gui.util.WindowGeometry;
45import org.openstreetmap.josm.tools.ImageProvider;
46import org.openstreetmap.josm.tools.InputMapUtils;
47
48/**
49 * This conflict resolution dialog is used when tags are pasted from the clipboard that conflict with the existing ones.
50 */
51public class PasteTagsConflictResolverDialog extends JDialog implements PropertyChangeListener {
52 static final Map<OsmPrimitiveType, String> PANE_TITLES;
53 static {
54 PANE_TITLES = new EnumMap<>(OsmPrimitiveType.class);
55 PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes"));
56 PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways"));
57 PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations"));
58 }
59
60 enum Mode {
61 RESOLVING_ONE_TAGCOLLECTION_ONLY,
62 RESOLVING_TYPED_TAGCOLLECTIONS
63 }
64
65 private final TagConflictResolverModel model = new TagConflictResolverModel();
66 private final transient Map<OsmPrimitiveType, TagConflictResolver> resolvers = new EnumMap<>(OsmPrimitiveType.class);
67 private final JTabbedPane tpResolvers = new JTabbedPane();
68 private Mode mode;
69 private boolean canceled;
70
71 private final ImageIcon iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
72 private final ImageIcon iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
73 private final StatisticsTableModel statisticsModel = new StatisticsTableModel();
74 private final JPanel pnlTagResolver = new JPanel(new BorderLayout());
75
76 /**
77 * Constructs a new {@code PasteTagsConflictResolverDialog}.
78 * @param owner parent component
79 */
80 public PasteTagsConflictResolverDialog(Component owner) {
81 super(GuiHelper.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
82 build();
83 }
84
85 protected final void build() {
86 setTitle(tr("Conflicts in pasted tags"));
87 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
88 TagConflictResolverModel tagModel = new TagConflictResolverModel();
89 resolvers.put(type, new TagConflictResolver(tagModel));
90 tagModel.addPropertyChangeListener(this);
91 }
92 getContentPane().setLayout(new GridBagLayout());
93 mode = null;
94 GridBagConstraints gc = new GridBagConstraints();
95 gc.gridx = 0;
96 gc.gridy = 0;
97 gc.fill = GridBagConstraints.HORIZONTAL;
98 gc.weightx = 1.0;
99 gc.weighty = 0.0;
100 getContentPane().add(buildSourceAndTargetInfoPanel(), gc);
101 gc.gridx = 0;
102 gc.gridy = 1;
103 gc.fill = GridBagConstraints.BOTH;
104 gc.weightx = 1.0;
105 gc.weighty = 1.0;
106 getContentPane().add(pnlTagResolver, gc);
107 gc.gridx = 0;
108 gc.gridy = 2;
109 gc.fill = GridBagConstraints.HORIZONTAL;
110 gc.weightx = 1.0;
111 gc.weighty = 0.0;
112 getContentPane().add(buildButtonPanel(), gc);
113 InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
114 }
115
116 protected JPanel buildButtonPanel() {
117 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
118
119 // -- apply button
120 ApplyAction applyAction = new ApplyAction();
121 model.addPropertyChangeListener(applyAction);
122 for (TagConflictResolver r : resolvers.values()) {
123 r.getModel().addPropertyChangeListener(applyAction);
124 }
125 pnl.add(new JButton(applyAction));
126
127 // -- cancel button
128 CancelAction cancelAction = new CancelAction();
129 pnl.add(new JButton(cancelAction));
130
131 return pnl;
132 }
133
134 protected JPanel buildSourceAndTargetInfoPanel() {
135 JPanel pnl = new JPanel(new BorderLayout());
136 pnl.add(new StatisticsInfoTable(statisticsModel), BorderLayout.CENTER);
137 return pnl;
138 }
139
140 /**
141 * Initializes the conflict resolver for a specific type of primitives
142 *
143 * @param type the type of primitives
144 * @param tc the tags belonging to this type of primitives
145 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
146 */
147 protected void initResolver(OsmPrimitiveType type, TagCollection tc, Map<OsmPrimitiveType, Integer> targetStatistics) {
148 TagConflictResolver resolver = resolvers.get(type);
149 resolver.getModel().populate(tc, tc.getKeysWithMultipleValues());
150 resolver.getModel().prepareDefaultTagDecisions();
151 if (!tc.isEmpty() && targetStatistics.get(type) != null && targetStatistics.get(type) > 0) {
152 tpResolvers.add(PANE_TITLES.get(type), resolver);
153 }
154 }
155
156 /**
157 * Populates the conflict resolver with one tag collection
158 *
159 * @param tagsForAllPrimitives the tag collection
160 * @param sourceStatistics histogram of tag source, number of primitives of each type in the source
161 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
162 */
163 public void populate(TagCollection tagsForAllPrimitives, Map<OsmPrimitiveType, Integer> sourceStatistics,
164 Map<OsmPrimitiveType, Integer> targetStatistics) {
165 mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY;
166 tagsForAllPrimitives = tagsForAllPrimitives == null ? new TagCollection() : tagsForAllPrimitives;
167 sourceStatistics = sourceStatistics == null ? new HashMap<>() : sourceStatistics;
168 targetStatistics = targetStatistics == null ? new HashMap<>() : targetStatistics;
169
170 // init the resolver
171 //
172 model.populate(tagsForAllPrimitives, tagsForAllPrimitives.getKeysWithMultipleValues());
173 model.prepareDefaultTagDecisions();
174
175 // prepare the dialog with one tag resolver
176 pnlTagResolver.removeAll();
177 pnlTagResolver.add(new TagConflictResolver(model), BorderLayout.CENTER);
178
179 statisticsModel.reset();
180 StatisticsInfo info = new StatisticsInfo();
181 info.numTags = tagsForAllPrimitives.getKeys().size();
182 info.sourceInfo.putAll(sourceStatistics);
183 info.targetInfo.putAll(targetStatistics);
184 statisticsModel.append(info);
185 validate();
186 }
187
188 protected int getNumResolverTabs() {
189 return tpResolvers.getTabCount();
190 }
191
192 protected TagConflictResolver getResolver(int idx) {
193 return (TagConflictResolver) tpResolvers.getComponentAt(idx);
194 }
195
196 /**
197 * Populate the tag conflict resolver with tags for each type of primitives
198 *
199 * @param tagsForNodes the tags belonging to nodes in the paste source
200 * @param tagsForWays the tags belonging to way in the paste source
201 * @param tagsForRelations the tags belonging to relations in the paste source
202 * @param sourceStatistics histogram of tag source, number of primitives of each type in the source
203 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
204 */
205 public void populate(TagCollection tagsForNodes, TagCollection tagsForWays, TagCollection tagsForRelations,
206 Map<OsmPrimitiveType, Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) {
207 tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes;
208 tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays;
209 tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations;
210 if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) {
211 populate(null, null, null);
212 return;
213 }
214 tpResolvers.removeAll();
215 initResolver(OsmPrimitiveType.NODE, tagsForNodes, targetStatistics);
216 initResolver(OsmPrimitiveType.WAY, tagsForWays, targetStatistics);
217 initResolver(OsmPrimitiveType.RELATION, tagsForRelations, targetStatistics);
218
219 pnlTagResolver.removeAll();
220 pnlTagResolver.add(tpResolvers, BorderLayout.CENTER);
221 mode = Mode.RESOLVING_TYPED_TAGCOLLECTIONS;
222 validate();
223 statisticsModel.reset();
224 if (!tagsForNodes.isEmpty()) {
225 StatisticsInfo info = new StatisticsInfo();
226 info.numTags = tagsForNodes.getKeys().size();
227 int numTargets = targetStatistics.get(OsmPrimitiveType.NODE) == null ? 0 : targetStatistics.get(OsmPrimitiveType.NODE);
228 if (numTargets > 0) {
229 info.sourceInfo.put(OsmPrimitiveType.NODE, sourceStatistics.get(OsmPrimitiveType.NODE));
230 info.targetInfo.put(OsmPrimitiveType.NODE, numTargets);
231 statisticsModel.append(info);
232 }
233 }
234 if (!tagsForWays.isEmpty()) {
235 StatisticsInfo info = new StatisticsInfo();
236 info.numTags = tagsForWays.getKeys().size();
237 int numTargets = targetStatistics.get(OsmPrimitiveType.WAY) == null ? 0 : targetStatistics.get(OsmPrimitiveType.WAY);
238 if (numTargets > 0) {
239 info.sourceInfo.put(OsmPrimitiveType.WAY, sourceStatistics.get(OsmPrimitiveType.WAY));
240 info.targetInfo.put(OsmPrimitiveType.WAY, numTargets);
241 statisticsModel.append(info);
242 }
243 }
244 if (!tagsForRelations.isEmpty()) {
245 StatisticsInfo info = new StatisticsInfo();
246 info.numTags = tagsForRelations.getKeys().size();
247 int numTargets = targetStatistics.get(OsmPrimitiveType.RELATION) == null ? 0 : targetStatistics.get(OsmPrimitiveType.RELATION);
248 if (numTargets > 0) {
249 info.sourceInfo.put(OsmPrimitiveType.RELATION, sourceStatistics.get(OsmPrimitiveType.RELATION));
250 info.targetInfo.put(OsmPrimitiveType.RELATION, numTargets);
251 statisticsModel.append(info);
252 }
253 }
254
255 IntStream.range(0, getNumResolverTabs())
256 .filter(i -> !getResolver(i).getModel().isResolvedCompletely())
257 .findFirst()
258 .ifPresent(tpResolvers::setSelectedIndex);
259 }
260
261 protected void setCanceled(boolean canceled) {
262 this.canceled = canceled;
263 }
264
265 public boolean isCanceled() {
266 return this.canceled;
267 }
268
269 final class CancelAction extends AbstractAction {
270
271 private CancelAction() {
272 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
273 putValue(Action.NAME, tr("Cancel"));
274 new ImageProvider("cancel").getResource().attachImageIcon(this);
275 setEnabled(true);
276 }
277
278 @Override
279 public void actionPerformed(ActionEvent arg0) {
280 setVisible(false);
281 setCanceled(true);
282 }
283 }
284
285 final class ApplyAction extends AbstractAction implements PropertyChangeListener {
286
287 private ApplyAction() {
288 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
289 putValue(Action.NAME, tr("Apply"));
290 new ImageProvider("ok").getResource().attachImageIcon(this);
291 updateEnabledState();
292 }
293
294 @Override
295 public void actionPerformed(ActionEvent arg0) {
296 setVisible(false);
297 }
298
299 void updateEnabledState() {
300 if (mode == null) {
301 setEnabled(false);
302 } else if (mode == Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY) {
303 setEnabled(model.isResolvedCompletely());
304 } else {
305 setEnabled(resolvers.values().stream().allMatch(val -> val.getModel().isResolvedCompletely()));
306 }
307 }
308
309 @Override
310 public void propertyChange(PropertyChangeEvent evt) {
311 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
312 updateEnabledState();
313 }
314 }
315 }
316
317 @Override
318 public void setVisible(boolean visible) {
319 if (visible) {
320 new WindowGeometry(
321 getClass().getName() + ".geometry",
322 WindowGeometry.centerOnScreen(new Dimension(600, 400))
323 ).applySafe(this);
324 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
325 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
326 }
327 super.setVisible(visible);
328 }
329
330 /**
331 * Returns conflict resolution.
332 * @return conflict resolution
333 */
334 public TagCollection getResolution() {
335 return model.getResolution();
336 }
337
338 public TagCollection getResolution(OsmPrimitiveType type) {
339 if (type == null) return null;
340 return resolvers.get(type).getModel().getResolution();
341 }
342
343 @Override
344 public void propertyChange(PropertyChangeEvent evt) {
345 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
346 TagConflictResolverModel tagModel = (TagConflictResolverModel) evt.getSource();
347 for (int i = 0; i < tpResolvers.getTabCount(); i++) {
348 TagConflictResolver resolver = (TagConflictResolver) tpResolvers.getComponentAt(i);
349 if (tagModel == resolver.getModel()) {
350 tpResolvers.setIconAt(i,
351 (Integer) evt.getNewValue() == 0 ? iconResolved : iconUnresolved
352 );
353 }
354 }
355 }
356 }
357
358 static final class StatisticsInfo {
359 int numTags;
360 final Map<OsmPrimitiveType, Integer> sourceInfo;
361 final Map<OsmPrimitiveType, Integer> targetInfo;
362
363 StatisticsInfo() {
364 sourceInfo = new EnumMap<>(OsmPrimitiveType.class);
365 targetInfo = new EnumMap<>(OsmPrimitiveType.class);
366 }
367 }
368
369 static final class StatisticsTableModel extends DefaultTableModel {
370 private static final String[] HEADERS = {tr("Paste ..."), tr("From ..."), tr("To ...") };
371 private final transient List<StatisticsInfo> data = new ArrayList<>();
372
373 @Override
374 public Object getValueAt(int row, int column) {
375 if (row == 0)
376 return HEADERS[column];
377 else if (row -1 < data.size())
378 return data.get(row -1);
379 else
380 return null;
381 }
382
383 @Override
384 public boolean isCellEditable(int row, int column) {
385 return false;
386 }
387
388 @Override
389 public int getRowCount() {
390 return data == null ? 1 : data.size() + 1;
391 }
392
393 void reset() {
394 data.clear();
395 }
396
397 void append(StatisticsInfo info) {
398 data.add(info);
399 fireTableDataChanged();
400 }
401 }
402
403 static final class StatisticsInfoRenderer extends JLabel implements TableCellRenderer {
404 private void reset() {
405 setIcon(null);
406 setText("");
407 setFont(UIManager.getFont("Table.font"));
408 }
409
410 private void renderNumTags(StatisticsInfo info) {
411 if (info == null) return;
412 setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags));
413 }
414
415 private void renderStatistics(Map<OsmPrimitiveType, Integer> stat) {
416 if (stat == null) return;
417 if (stat.isEmpty()) return;
418 if (stat.size() == 1) {
419 setIcon(ImageProvider.get(stat.keySet().iterator().next()));
420 } else {
421 setIcon(ImageProvider.get("data", "object"));
422 }
423 StringJoiner text = new StringJoiner(", ");
424 for (Entry<OsmPrimitiveType, Integer> entry: stat.entrySet()) {
425 OsmPrimitiveType type = entry.getKey();
426 int numPrimitives = entry.getValue() == null ? 0 : entry.getValue();
427 if (numPrimitives == 0) {
428 continue;
429 }
430 String msg;
431 switch (type) {
432 case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives, numPrimitives); break;
433 case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break;
434 case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break;
435 default: throw new AssertionError();
436 }
437 text.add(msg);
438 }
439 setText(text.toString());
440 }
441
442 private void renderFrom(StatisticsInfo info) {
443 renderStatistics(info.sourceInfo);
444 }
445
446 private void renderTo(StatisticsInfo info) {
447 renderStatistics(info.targetInfo);
448 }
449
450 @Override
451 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
452 boolean hasFocus, int row, int column) {
453 reset();
454 if (value == null)
455 return this;
456
457 if (row == 0) {
458 setFont(getFont().deriveFont(Font.BOLD));
459 setText((String) value);
460 } else {
461 StatisticsInfo info = (StatisticsInfo) value;
462
463 switch (column) {
464 case 0: renderNumTags(info); break;
465 case 1: renderFrom(info); break;
466 case 2: renderTo(info); break;
467 default: // Do nothing
468 }
469 }
470 return this;
471 }
472 }
473
474 static final class StatisticsInfoTable extends JPanel {
475
476 StatisticsInfoTable(StatisticsTableModel model) {
477 JTable infoTable = new JTable(model,
478 new TagTableColumnModelBuilder(new StatisticsInfoRenderer(), tr("Paste ..."), tr("From ..."), tr("To ...")).build());
479 infoTable.setShowHorizontalLines(true);
480 infoTable.setShowVerticalLines(false);
481 infoTable.setEnabled(false);
482 setLayout(new BorderLayout());
483 add(infoTable, BorderLayout.CENTER);
484 }
485
486 @Override
487 public Insets getInsets() {
488 Insets insets = super.getInsets();
489 insets.bottom = 20;
490 return insets;
491 }
492 }
493}
Note: See TracBrowser for help on using the repository browser.