001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.FlowLayout; 011import java.awt.Font; 012import java.awt.GridBagConstraints; 013import java.awt.GridBagLayout; 014import java.awt.Insets; 015import java.awt.event.ActionEvent; 016import java.beans.PropertyChangeEvent; 017import java.beans.PropertyChangeListener; 018import java.util.ArrayList; 019import java.util.EnumMap; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.StringJoiner; 025import java.util.stream.IntStream; 026 027import javax.swing.AbstractAction; 028import javax.swing.Action; 029import javax.swing.ImageIcon; 030import javax.swing.JButton; 031import javax.swing.JDialog; 032import javax.swing.JLabel; 033import javax.swing.JPanel; 034import javax.swing.JTabbedPane; 035import javax.swing.JTable; 036import javax.swing.UIManager; 037import javax.swing.table.DefaultTableModel; 038import javax.swing.table.TableCellRenderer; 039 040import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 041import org.openstreetmap.josm.data.osm.TagCollection; 042import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder; 043import org.openstreetmap.josm.gui.util.GuiHelper; 044import org.openstreetmap.josm.gui.util.WindowGeometry; 045import org.openstreetmap.josm.tools.ImageProvider; 046import org.openstreetmap.josm.tools.InputMapUtils; 047 048/** 049 * This conflict resolution dialog is used when tags are pasted from the clipboard that conflict with the existing ones. 050 */ 051public class PasteTagsConflictResolverDialog extends JDialog implements PropertyChangeListener { 052 static final Map<OsmPrimitiveType, String> PANE_TITLES; 053 static { 054 PANE_TITLES = new EnumMap<>(OsmPrimitiveType.class); 055 PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes")); 056 PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways")); 057 PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations")); 058 } 059 060 enum Mode { 061 RESOLVING_ONE_TAGCOLLECTION_ONLY, 062 RESOLVING_TYPED_TAGCOLLECTIONS 063 } 064 065 private final TagConflictResolverModel model = new TagConflictResolverModel(); 066 private final transient Map<OsmPrimitiveType, TagConflictResolver> resolvers = new EnumMap<>(OsmPrimitiveType.class); 067 private final JTabbedPane tpResolvers = new JTabbedPane(); 068 private Mode mode; 069 private boolean canceled; 070 071 private final ImageIcon iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved"); 072 private final ImageIcon iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved"); 073 private final StatisticsTableModel statisticsModel = new StatisticsTableModel(); 074 private final JPanel pnlTagResolver = new JPanel(new BorderLayout()); 075 076 /** 077 * Constructs a new {@code PasteTagsConflictResolverDialog}. 078 * @param owner parent component 079 */ 080 public PasteTagsConflictResolverDialog(Component owner) { 081 super(GuiHelper.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL); 082 build(); 083 } 084 085 protected final void build() { 086 setTitle(tr("Conflicts in pasted tags")); 087 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 088 TagConflictResolverModel tagModel = new TagConflictResolverModel(); 089 resolvers.put(type, new TagConflictResolver(tagModel)); 090 tagModel.addPropertyChangeListener(this); 091 } 092 getContentPane().setLayout(new GridBagLayout()); 093 mode = null; 094 GridBagConstraints gc = new GridBagConstraints(); 095 gc.gridx = 0; 096 gc.gridy = 0; 097 gc.fill = GridBagConstraints.HORIZONTAL; 098 gc.weightx = 1.0; 099 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}