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