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

Last change on this file since 16438 was 16438, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

  • 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.