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