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; 024 025import javax.swing.AbstractAction; 026import javax.swing.Action; 027import javax.swing.ImageIcon; 028import javax.swing.JDialog; 029import javax.swing.JLabel; 030import javax.swing.JOptionPane; 031import javax.swing.JPanel; 032import javax.swing.JTabbedPane; 033import javax.swing.JTable; 034import javax.swing.UIManager; 035import javax.swing.table.DefaultTableColumnModel; 036import javax.swing.table.DefaultTableModel; 037import javax.swing.table.TableCellRenderer; 038import javax.swing.table.TableColumn; 039 040import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 041import org.openstreetmap.josm.data.osm.TagCollection; 042import org.openstreetmap.josm.gui.SideButton; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.WindowGeometry; 045 046public class PasteTagsConflictResolverDialog extends JDialog implements PropertyChangeListener { 047 private static final Map<OsmPrimitiveType, String> PANE_TITLES; 048 static { 049 PANE_TITLES = new EnumMap<>(OsmPrimitiveType.class); 050 PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes")); 051 PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways")); 052 PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations")); 053 } 054 055 private enum Mode { 056 RESOLVING_ONE_TAGCOLLECTION_ONLY, 057 RESOLVING_TYPED_TAGCOLLECTIONS 058 } 059 060 private TagConflictResolver allPrimitivesResolver; 061 private transient Map<OsmPrimitiveType, TagConflictResolver> resolvers; 062 private JTabbedPane tpResolvers; 063 private Mode mode; 064 private boolean canceled; 065 066 private final ImageIcon iconResolved; 067 private final ImageIcon iconUnresolved; 068 private StatisticsTableModel statisticsModel; 069 private JPanel pnlTagResolver; 070 071 /** 072 * Constructs a new {@code PasteTagsConflictResolverDialog}. 073 * @param owner parent component 074 */ 075 public PasteTagsConflictResolverDialog(Component owner) { 076 super(JOptionPane.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL); 077 build(); 078 iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved"); 079 iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved"); 080 } 081 082 protected final void build() { 083 setTitle(tr("Conflicts in pasted tags")); 084 allPrimitivesResolver = new TagConflictResolver(); 085 resolvers = new EnumMap<>(OsmPrimitiveType.class); 086 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 087 resolvers.put(type, new TagConflictResolver()); 088 resolvers.get(type).getModel().addPropertyChangeListener(this); 089 } 090 tpResolvers = new JTabbedPane(); 091 getContentPane().setLayout(new GridBagLayout()); 092 mode = null; 093 GridBagConstraints gc = new GridBagConstraints(); 094 gc.gridx = 0; 095 gc.gridy = 0; 096 gc.fill = GridBagConstraints.HORIZONTAL; 097 gc.weightx = 1.0; 098 gc.weighty = 0.0; 099 getContentPane().add(buildSourceAndTargetInfoPanel(), gc); 100 gc.gridx = 0; 101 gc.gridy = 1; 102 gc.fill = GridBagConstraints.BOTH; 103 gc.weightx = 1.0; 104 gc.weighty = 1.0; 105 getContentPane().add(pnlTagResolver = new JPanel(), gc); 106 gc.gridx = 0; 107 gc.gridy = 2; 108 gc.fill = GridBagConstraints.HORIZONTAL; 109 gc.weightx = 1.0; 110 gc.weighty = 0.0; 111 getContentPane().add(buildButtonPanel(), gc); 112 } 113 114 protected JPanel buildButtonPanel() { 115 JPanel pnl = new JPanel(); 116 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 117 118 // -- apply button 119 ApplyAction applyAction = new ApplyAction(); 120 allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction); 121 for (TagConflictResolver r : resolvers.values()) { 122 r.getModel().addPropertyChangeListener(applyAction); 123 } 124 pnl.add(new SideButton(applyAction)); 125 126 // -- cancel button 127 CancelAction cancelAction = new CancelAction(); 128 pnl.add(new SideButton(cancelAction)); 129 130 return pnl; 131 } 132 133 protected JPanel buildSourceAndTargetInfoPanel() { 134 JPanel pnl = new JPanel(); 135 pnl.setLayout(new BorderLayout()); 136 statisticsModel = new StatisticsTableModel(); 137 pnl.add(new StatisticsInfoTable(statisticsModel), BorderLayout.CENTER); 138 return pnl; 139 } 140 141 /** 142 * Initializes the conflict resolver for a specific type of primitives 143 * 144 * @param type the type of primitives 145 * @param tc the tags belonging to this type of primitives 146 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target 147 */ 148 protected void initResolver(OsmPrimitiveType type, TagCollection tc, Map<OsmPrimitiveType, Integer> targetStatistics) { 149 resolvers.get(type).getModel().populate(tc, tc.getKeysWithMultipleValues()); 150 resolvers.get(type).getModel().prepareDefaultTagDecisions(); 151 if (!tc.isEmpty() && targetStatistics.get(type) != null && targetStatistics.get(type) > 0) { 152 tpResolvers.add(PANE_TITLES.get(type), resolvers.get(type)); 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<OsmPrimitiveType, Integer>() : sourceStatistics; 168 targetStatistics = targetStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() : targetStatistics; 169 170 // init the resolver 171 // 172 allPrimitivesResolver.getModel().populate(tagsForAllPrimitives, tagsForAllPrimitives.getKeysWithMultipleValues()); 173 allPrimitivesResolver.getModel().prepareDefaultTagDecisions(); 174 175 // prepare the dialog with one tag resolver 176 pnlTagResolver.setLayout(new BorderLayout()); 177 pnlTagResolver.removeAll(); 178 pnlTagResolver.add(allPrimitivesResolver, BorderLayout.CENTER); 179 180 statisticsModel.reset(); 181 StatisticsInfo info = new StatisticsInfo(); 182 info.numTags = tagsForAllPrimitives.getKeys().size(); 183 info.sourceInfo.putAll(sourceStatistics); 184 info.targetInfo.putAll(targetStatistics); 185 statisticsModel.append(info); 186 validate(); 187 } 188 189 protected int getNumResolverTabs() { 190 return tpResolvers.getTabCount(); 191 } 192 193 protected TagConflictResolver getResolver(int idx) { 194 return (TagConflictResolver) tpResolvers.getComponentAt(idx); 195 } 196 197 /** 198 * Populate the tag conflict resolver with tags for each type of primitives 199 * 200 * @param tagsForNodes the tags belonging to nodes in the paste source 201 * @param tagsForWays the tags belonging to way in the paste source 202 * @param tagsForRelations the tags belonging to relations in the paste source 203 * @param sourceStatistics histogram of tag source, number of primitives of each type in the source 204 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target 205 */ 206 public void populate(TagCollection tagsForNodes, TagCollection tagsForWays, TagCollection tagsForRelations, 207 Map<OsmPrimitiveType, Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) { 208 tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes; 209 tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays; 210 tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations; 211 if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) { 212 populate(null, null, null); 213 return; 214 } 215 tpResolvers.removeAll(); 216 initResolver(OsmPrimitiveType.NODE, tagsForNodes, targetStatistics); 217 initResolver(OsmPrimitiveType.WAY, tagsForWays, targetStatistics); 218 initResolver(OsmPrimitiveType.RELATION, tagsForRelations, targetStatistics); 219 220 pnlTagResolver.setLayout(new BorderLayout()); 221 pnlTagResolver.removeAll(); 222 pnlTagResolver.add(tpResolvers, BorderLayout.CENTER); 223 mode = Mode.RESOLVING_TYPED_TAGCOLLECTIONS; 224 validate(); 225 statisticsModel.reset(); 226 if (!tagsForNodes.isEmpty()) { 227 StatisticsInfo info = new StatisticsInfo(); 228 info.numTags = tagsForNodes.getKeys().size(); 229 int numTargets = targetStatistics.get(OsmPrimitiveType.NODE) == null ? 0 : targetStatistics.get(OsmPrimitiveType.NODE); 230 if (numTargets > 0) { 231 info.sourceInfo.put(OsmPrimitiveType.NODE, sourceStatistics.get(OsmPrimitiveType.NODE)); 232 info.targetInfo.put(OsmPrimitiveType.NODE, numTargets); 233 statisticsModel.append(info); 234 } 235 } 236 if (!tagsForWays.isEmpty()) { 237 StatisticsInfo info = new StatisticsInfo(); 238 info.numTags = tagsForWays.getKeys().size(); 239 int numTargets = targetStatistics.get(OsmPrimitiveType.WAY) == null ? 0 : targetStatistics.get(OsmPrimitiveType.WAY); 240 if (numTargets > 0) { 241 info.sourceInfo.put(OsmPrimitiveType.WAY, sourceStatistics.get(OsmPrimitiveType.WAY)); 242 info.targetInfo.put(OsmPrimitiveType.WAY, numTargets); 243 statisticsModel.append(info); 244 } 245 } 246 if (!tagsForRelations.isEmpty()) { 247 StatisticsInfo info = new StatisticsInfo(); 248 info.numTags = tagsForRelations.getKeys().size(); 249 int numTargets = targetStatistics.get(OsmPrimitiveType.RELATION) == null ? 0 : targetStatistics.get(OsmPrimitiveType.RELATION); 250 if (numTargets > 0) { 251 info.sourceInfo.put(OsmPrimitiveType.RELATION, sourceStatistics.get(OsmPrimitiveType.RELATION)); 252 info.targetInfo.put(OsmPrimitiveType.RELATION, numTargets); 253 statisticsModel.append(info); 254 } 255 } 256 257 for (int i = 0; i < getNumResolverTabs(); i++) { 258 if (!getResolver(i).getModel().isResolvedCompletely()) { 259 tpResolvers.setSelectedIndex(i); 260 break; 261 } 262 } 263 } 264 265 protected void setCanceled(boolean canceled) { 266 this.canceled = canceled; 267 } 268 269 public boolean isCanceled() { 270 return this.canceled; 271 } 272 273 final class CancelAction extends AbstractAction { 274 275 private CancelAction() { 276 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 277 putValue(Action.NAME, tr("Cancel")); 278 putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel")); 279 setEnabled(true); 280 } 281 282 @Override 283 public void actionPerformed(ActionEvent arg0) { 284 setVisible(false); 285 setCanceled(true); 286 } 287 } 288 289 final class ApplyAction extends AbstractAction implements PropertyChangeListener { 290 291 private ApplyAction() { 292 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 293 putValue(Action.NAME, tr("Apply")); 294 putValue(Action.SMALL_ICON, ImageProvider.get("ok")); 295 updateEnabledState(); 296 } 297 298 @Override 299 public void actionPerformed(ActionEvent arg0) { 300 setVisible(false); 301 } 302 303 protected void updateEnabledState() { 304 if (mode == null) { 305 setEnabled(false); 306 } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) { 307 setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely()); 308 } else { 309 boolean enabled = true; 310 for (TagConflictResolver val: resolvers.values()) { 311 enabled &= val.getModel().isResolvedCompletely(); 312 } 313 setEnabled(enabled); 314 } 315 } 316 317 @Override 318 public void propertyChange(PropertyChangeEvent evt) { 319 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 320 updateEnabledState(); 321 } 322 } 323 } 324 325 @Override 326 public void setVisible(boolean visible) { 327 if (visible) { 328 new WindowGeometry( 329 getClass().getName() + ".geometry", 330 WindowGeometry.centerOnScreen(new Dimension(600, 400)) 331 ).applySafe(this); 332 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 333 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 334 } 335 super.setVisible(visible); 336 } 337 338 public TagCollection getResolution() { 339 return allPrimitivesResolver.getModel().getResolution(); 340 } 341 342 public TagCollection getResolution(OsmPrimitiveType type) { 343 if (type == null) return null; 344 return resolvers.get(type).getModel().getResolution(); 345 } 346 347 @Override 348 public void propertyChange(PropertyChangeEvent evt) { 349 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 350 TagConflictResolverModel model = (TagConflictResolverModel) evt.getSource(); 351 for (int i = 0; i < tpResolvers.getTabCount(); i++) { 352 TagConflictResolver resolver = (TagConflictResolver) tpResolvers.getComponentAt(i); 353 if (model == resolver.getModel()) { 354 tpResolvers.setIconAt(i, 355 (Boolean) evt.getNewValue() ? iconResolved : iconUnresolved 356 357 ); 358 } 359 } 360 } 361 } 362 363 private static final class StatisticsInfo { 364 public int numTags; 365 public final Map<OsmPrimitiveType, Integer> sourceInfo; 366 public final Map<OsmPrimitiveType, Integer> targetInfo; 367 368 private StatisticsInfo() { 369 sourceInfo = new EnumMap<>(OsmPrimitiveType.class); 370 targetInfo = new EnumMap<>(OsmPrimitiveType.class); 371 } 372 } 373 374 private static final class StatisticsTableColumnModel extends DefaultTableColumnModel { 375 private StatisticsTableColumnModel() { 376 TableCellRenderer renderer = new StatisticsInfoRenderer(); 377 TableColumn col = null; 378 379 // column 0 - Paste 380 col = new TableColumn(0); 381 col.setHeaderValue(tr("Paste ...")); 382 col.setResizable(true); 383 col.setCellRenderer(renderer); 384 addColumn(col); 385 386 // column 1 - From 387 col = new TableColumn(1); 388 col.setHeaderValue(tr("From ...")); 389 col.setResizable(true); 390 col.setCellRenderer(renderer); 391 addColumn(col); 392 393 // column 2 - To 394 col = new TableColumn(2); 395 col.setHeaderValue(tr("To ...")); 396 col.setResizable(true); 397 col.setCellRenderer(renderer); 398 addColumn(col); 399 } 400 } 401 402 private static final class StatisticsTableModel extends DefaultTableModel { 403 private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") }; 404 private final transient List<StatisticsInfo> data; 405 406 private StatisticsTableModel() { 407 data = new ArrayList<>(); 408 } 409 410 @Override 411 public Object getValueAt(int row, int column) { 412 if (row == 0) 413 return HEADERS[column]; 414 else if (row -1 < data.size()) 415 return data.get(row -1); 416 else 417 return null; 418 } 419 420 @Override 421 public boolean isCellEditable(int row, int column) { 422 return false; 423 } 424 425 @Override 426 public int getRowCount() { 427 if (data == null) return 1; 428 return data.size() + 1; 429 } 430 431 public void reset() { 432 data.clear(); 433 } 434 435 public void append(StatisticsInfo info) { 436 data.add(info); 437 fireTableDataChanged(); 438 } 439 } 440 441 private static class StatisticsInfoRenderer extends JLabel implements TableCellRenderer { 442 protected void reset() { 443 setIcon(null); 444 setText(""); 445 setFont(UIManager.getFont("Table.font")); 446 } 447 448 protected void renderNumTags(StatisticsInfo info) { 449 if (info == null) return; 450 setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags)); 451 } 452 453 protected void renderStatistics(Map<OsmPrimitiveType, Integer> stat) { 454 if (stat == null) return; 455 if (stat.isEmpty()) return; 456 if (stat.size() == 1) { 457 setIcon(ImageProvider.get(stat.keySet().iterator().next())); 458 } else { 459 setIcon(ImageProvider.get("data", "object")); 460 } 461 StringBuilder text = new StringBuilder(); 462 for (Entry<OsmPrimitiveType, Integer> entry: stat.entrySet()) { 463 OsmPrimitiveType type = entry.getKey(); 464 int numPrimitives = entry.getValue() == null ? 0 : entry.getValue(); 465 if (numPrimitives == 0) { 466 continue; 467 } 468 String msg = ""; 469 switch(type) { 470 case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives, numPrimitives); break; 471 case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break; 472 case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break; 473 } 474 if (text.length() > 0) { 475 text.append(", "); 476 } 477 text.append(msg); 478 } 479 setText(text.toString()); 480 } 481 482 protected void renderFrom(StatisticsInfo info) { 483 renderStatistics(info.sourceInfo); 484 } 485 486 protected void renderTo(StatisticsInfo info) { 487 renderStatistics(info.targetInfo); 488 } 489 490 @Override 491 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 492 boolean hasFocus, int row, int column) { 493 reset(); 494 if (value == null) 495 return this; 496 497 if (row == 0) { 498 setFont(getFont().deriveFont(Font.BOLD)); 499 setText((String) value); 500 } else { 501 StatisticsInfo info = (StatisticsInfo) value; 502 503 switch(column) { 504 case 0: renderNumTags(info); break; 505 case 1: renderFrom(info); break; 506 case 2: renderTo(info); break; 507 } 508 } 509 return this; 510 } 511 } 512 513 private static final class StatisticsInfoTable extends JPanel { 514 515 private StatisticsInfoTable(StatisticsTableModel model) { 516 JTable infoTable = new JTable(model, new StatisticsTableColumnModel()); 517 infoTable.setShowHorizontalLines(true); 518 infoTable.setShowVerticalLines(false); 519 infoTable.setEnabled(false); 520 setLayout(new BorderLayout()); 521 add(infoTable, BorderLayout.CENTER); 522 } 523 524 @Override 525 public Insets getInsets() { 526 Insets insets = super.getInsets(); 527 insets.bottom = 20; 528 return insets; 529 } 530 } 531}