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.JPanel; 031import javax.swing.JTabbedPane; 032import javax.swing.JTable; 033import javax.swing.UIManager; 034import javax.swing.table.DefaultTableModel; 035import javax.swing.table.TableCellRenderer; 036 037import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 038import org.openstreetmap.josm.data.osm.TagCollection; 039import org.openstreetmap.josm.gui.SideButton; 040import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder; 041import org.openstreetmap.josm.gui.util.GuiHelper; 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 final TagConflictResolver allPrimitivesResolver = new TagConflictResolver(); 060 private final transient Map<OsmPrimitiveType, TagConflictResolver> resolvers = new EnumMap<>(OsmPrimitiveType.class); 061 private final JTabbedPane tpResolvers = new JTabbedPane(); 062 private Mode mode; 063 private boolean canceled; 064 065 private final ImageIcon iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved"); 066 private final ImageIcon iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved"); 067 private final StatisticsTableModel statisticsModel = new StatisticsTableModel(); 068 private final JPanel pnlTagResolver = new JPanel(new BorderLayout()); 069 070 /** 071 * Constructs a new {@code PasteTagsConflictResolverDialog}. 072 * @param owner parent component 073 */ 074 public PasteTagsConflictResolverDialog(Component owner) { 075 super(GuiHelper.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL); 076 build(); 077 } 078 079 protected final void build() { 080 setTitle(tr("Conflicts in pasted tags")); 081 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 082 resolvers.put(type, new TagConflictResolver()); 083 resolvers.get(type).getModel().addPropertyChangeListener(this); 084 } 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, 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(new FlowLayout(FlowLayout.CENTER)); 110 111 // -- apply button 112 ApplyAction applyAction = new ApplyAction(); 113 allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction); 114 for (TagConflictResolver r : resolvers.values()) { 115 r.getModel().addPropertyChangeListener(applyAction); 116 } 117 pnl.add(new SideButton(applyAction)); 118 119 // -- cancel button 120 CancelAction cancelAction = new CancelAction(); 121 pnl.add(new SideButton(cancelAction)); 122 123 return pnl; 124 } 125 126 protected JPanel buildSourceAndTargetInfoPanel() { 127 JPanel pnl = new JPanel(new BorderLayout()); 128 pnl.add(new StatisticsInfoTable(statisticsModel), BorderLayout.CENTER); 129 return pnl; 130 } 131 132 /** 133 * Initializes the conflict resolver for a specific type of primitives 134 * 135 * @param type the type of primitives 136 * @param tc the tags belonging to this type of primitives 137 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target 138 */ 139 protected void initResolver(OsmPrimitiveType type, TagCollection tc, Map<OsmPrimitiveType, Integer> targetStatistics) { 140 resolvers.get(type).getModel().populate(tc, tc.getKeysWithMultipleValues()); 141 resolvers.get(type).getModel().prepareDefaultTagDecisions(); 142 if (!tc.isEmpty() && targetStatistics.get(type) != null && targetStatistics.get(type) > 0) { 143 tpResolvers.add(PANE_TITLES.get(type), resolvers.get(type)); 144 } 145 } 146 147 /** 148 * Populates the conflict resolver with one tag collection 149 * 150 * @param tagsForAllPrimitives the tag collection 151 * @param sourceStatistics histogram of tag source, number of primitives of each type in the source 152 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target 153 */ 154 public void populate(TagCollection tagsForAllPrimitives, Map<OsmPrimitiveType, Integer> sourceStatistics, 155 Map<OsmPrimitiveType, Integer> targetStatistics) { 156 mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY; 157 tagsForAllPrimitives = tagsForAllPrimitives == null ? new TagCollection() : tagsForAllPrimitives; 158 sourceStatistics = sourceStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() : sourceStatistics; 159 targetStatistics = targetStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() : targetStatistics; 160 161 // init the resolver 162 // 163 allPrimitivesResolver.getModel().populate(tagsForAllPrimitives, tagsForAllPrimitives.getKeysWithMultipleValues()); 164 allPrimitivesResolver.getModel().prepareDefaultTagDecisions(); 165 166 // prepare the dialog with one tag resolver 167 pnlTagResolver.removeAll(); 168 pnlTagResolver.add(allPrimitivesResolver, BorderLayout.CENTER); 169 170 statisticsModel.reset(); 171 StatisticsInfo info = new StatisticsInfo(); 172 info.numTags = tagsForAllPrimitives.getKeys().size(); 173 info.sourceInfo.putAll(sourceStatistics); 174 info.targetInfo.putAll(targetStatistics); 175 statisticsModel.append(info); 176 validate(); 177 } 178 179 protected int getNumResolverTabs() { 180 return tpResolvers.getTabCount(); 181 } 182 183 protected TagConflictResolver getResolver(int idx) { 184 return (TagConflictResolver) tpResolvers.getComponentAt(idx); 185 } 186 187 /** 188 * Populate the tag conflict resolver with tags for each type of primitives 189 * 190 * @param tagsForNodes the tags belonging to nodes in the paste source 191 * @param tagsForWays the tags belonging to way in the paste source 192 * @param tagsForRelations the tags belonging to relations in the paste source 193 * @param sourceStatistics histogram of tag source, number of primitives of each type in the source 194 * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target 195 */ 196 public void populate(TagCollection tagsForNodes, TagCollection tagsForWays, TagCollection tagsForRelations, 197 Map<OsmPrimitiveType, Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) { 198 tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes; 199 tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays; 200 tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations; 201 if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) { 202 populate(null, null, null); 203 return; 204 } 205 tpResolvers.removeAll(); 206 initResolver(OsmPrimitiveType.NODE, tagsForNodes, targetStatistics); 207 initResolver(OsmPrimitiveType.WAY, tagsForWays, targetStatistics); 208 initResolver(OsmPrimitiveType.RELATION, tagsForRelations, targetStatistics); 209 210 pnlTagResolver.removeAll(); 211 pnlTagResolver.add(tpResolvers, BorderLayout.CENTER); 212 mode = Mode.RESOLVING_TYPED_TAGCOLLECTIONS; 213 validate(); 214 statisticsModel.reset(); 215 if (!tagsForNodes.isEmpty()) { 216 StatisticsInfo info = new StatisticsInfo(); 217 info.numTags = tagsForNodes.getKeys().size(); 218 int numTargets = targetStatistics.get(OsmPrimitiveType.NODE) == null ? 0 : targetStatistics.get(OsmPrimitiveType.NODE); 219 if (numTargets > 0) { 220 info.sourceInfo.put(OsmPrimitiveType.NODE, sourceStatistics.get(OsmPrimitiveType.NODE)); 221 info.targetInfo.put(OsmPrimitiveType.NODE, numTargets); 222 statisticsModel.append(info); 223 } 224 } 225 if (!tagsForWays.isEmpty()) { 226 StatisticsInfo info = new StatisticsInfo(); 227 info.numTags = tagsForWays.getKeys().size(); 228 int numTargets = targetStatistics.get(OsmPrimitiveType.WAY) == null ? 0 : targetStatistics.get(OsmPrimitiveType.WAY); 229 if (numTargets > 0) { 230 info.sourceInfo.put(OsmPrimitiveType.WAY, sourceStatistics.get(OsmPrimitiveType.WAY)); 231 info.targetInfo.put(OsmPrimitiveType.WAY, numTargets); 232 statisticsModel.append(info); 233 } 234 } 235 if (!tagsForRelations.isEmpty()) { 236 StatisticsInfo info = new StatisticsInfo(); 237 info.numTags = tagsForRelations.getKeys().size(); 238 int numTargets = targetStatistics.get(OsmPrimitiveType.RELATION) == null ? 0 : targetStatistics.get(OsmPrimitiveType.RELATION); 239 if (numTargets > 0) { 240 info.sourceInfo.put(OsmPrimitiveType.RELATION, sourceStatistics.get(OsmPrimitiveType.RELATION)); 241 info.targetInfo.put(OsmPrimitiveType.RELATION, numTargets); 242 statisticsModel.append(info); 243 } 244 } 245 246 for (int i = 0; i < getNumResolverTabs(); i++) { 247 if (!getResolver(i).getModel().isResolvedCompletely()) { 248 tpResolvers.setSelectedIndex(i); 249 break; 250 } 251 } 252 } 253 254 protected void setCanceled(boolean canceled) { 255 this.canceled = canceled; 256 } 257 258 public boolean isCanceled() { 259 return this.canceled; 260 } 261 262 final class CancelAction extends AbstractAction { 263 264 private CancelAction() { 265 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 266 putValue(Action.NAME, tr("Cancel")); 267 new ImageProvider("cancel").getResource().attachImageIcon(this); 268 setEnabled(true); 269 } 270 271 @Override 272 public void actionPerformed(ActionEvent arg0) { 273 setVisible(false); 274 setCanceled(true); 275 } 276 } 277 278 final class ApplyAction extends AbstractAction implements PropertyChangeListener { 279 280 private ApplyAction() { 281 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 282 putValue(Action.NAME, tr("Apply")); 283 new ImageProvider("ok").getResource().attachImageIcon(this); 284 updateEnabledState(); 285 } 286 287 @Override 288 public void actionPerformed(ActionEvent arg0) { 289 setVisible(false); 290 } 291 292 protected void updateEnabledState() { 293 if (mode == null) { 294 setEnabled(false); 295 } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) { 296 setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely()); 297 } else { 298 boolean enabled = true; 299 for (TagConflictResolver val: resolvers.values()) { 300 enabled &= val.getModel().isResolvedCompletely(); 301 } 302 setEnabled(enabled); 303 } 304 } 305 306 @Override 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(600, 400)) 320 ).applySafe(this); 321 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 322 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 323 } 324 super.setVisible(visible); 325 } 326 327 /** 328 * Returns conflict resolution. 329 * @return conflict resolution 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 final class StatisticsInfo { 357 int numTags; 358 final Map<OsmPrimitiveType, Integer> sourceInfo; 359 final Map<OsmPrimitiveType, Integer> targetInfo; 360 361 StatisticsInfo() { 362 sourceInfo = new EnumMap<>(OsmPrimitiveType.class); 363 targetInfo = new EnumMap<>(OsmPrimitiveType.class); 364 } 365 } 366 367 static final class StatisticsTableModel extends DefaultTableModel { 368 private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") }; 369 private final transient List<StatisticsInfo> data = new ArrayList<>(); 370 371 @Override 372 public Object getValueAt(int row, int column) { 373 if (row == 0) 374 return HEADERS[column]; 375 else if (row -1 < data.size()) 376 return data.get(row -1); 377 else 378 return null; 379 } 380 381 @Override 382 public boolean isCellEditable(int row, int column) { 383 return false; 384 } 385 386 @Override 387 public int getRowCount() { 388 return data == null ? 1 : data.size() + 1; 389 } 390 391 void reset() { 392 data.clear(); 393 } 394 395 void append(StatisticsInfo info) { 396 data.add(info); 397 fireTableDataChanged(); 398 } 399 } 400 401 static final class StatisticsInfoRenderer extends JLabel implements TableCellRenderer { 402 private void reset() { 403 setIcon(null); 404 setText(""); 405 setFont(UIManager.getFont("Table.font")); 406 } 407 408 private void renderNumTags(StatisticsInfo info) { 409 if (info == null) return; 410 setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags)); 411 } 412 413 private void renderStatistics(Map<OsmPrimitiveType, Integer> stat) { 414 if (stat == null) return; 415 if (stat.isEmpty()) return; 416 if (stat.size() == 1) { 417 setIcon(ImageProvider.get(stat.keySet().iterator().next())); 418 } else { 419 setIcon(ImageProvider.get("data", "object")); 420 } 421 StringBuilder text = new StringBuilder(); 422 for (Entry<OsmPrimitiveType, Integer> entry: stat.entrySet()) { 423 OsmPrimitiveType type = entry.getKey(); 424 int numPrimitives = entry.getValue() == null ? 0 : entry.getValue(); 425 if (numPrimitives == 0) { 426 continue; 427 } 428 String msg; 429 switch(type) { 430 case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives, numPrimitives); break; 431 case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break; 432 case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break; 433 default: throw new AssertionError(); 434 } 435 if (text.length() > 0) { 436 text.append(", "); 437 } 438 text.append(msg); 439 } 440 setText(text.toString()); 441 } 442 443 private void renderFrom(StatisticsInfo info) { 444 renderStatistics(info.sourceInfo); 445 } 446 447 private void renderTo(StatisticsInfo info) { 448 renderStatistics(info.targetInfo); 449 } 450 451 @Override 452 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 453 boolean hasFocus, int row, int column) { 454 reset(); 455 if (value == null) 456 return this; 457 458 if (row == 0) { 459 setFont(getFont().deriveFont(Font.BOLD)); 460 setText((String) value); 461 } else { 462 StatisticsInfo info = (StatisticsInfo) value; 463 464 switch(column) { 465 case 0: renderNumTags(info); break; 466 case 1: renderFrom(info); break; 467 case 2: renderTo(info); break; 468 default: // Do nothing 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}