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