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.InputMapUtils; 044import org.openstreetmap.josm.tools.WindowGeometry; 045 046public class PasteTagsConflictResolverDialog extends JDialog implements PropertyChangeListener { 047 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 enum Mode { 056 RESOLVING_ONE_TAGCOLLECTION_ONLY, 057 RESOLVING_TYPED_TAGCOLLECTIONS 058 } 059 060 private final TagConflictResolver allPrimitivesResolver = new TagConflictResolver(); 061 private final transient Map<OsmPrimitiveType, TagConflictResolver> resolvers = new EnumMap<>(OsmPrimitiveType.class); 062 private final JTabbedPane tpResolvers = new JTabbedPane(); 063 private Mode mode; 064 private boolean canceled; 065 066 private final ImageIcon iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved"); 067 private final ImageIcon iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved"); 068 private final StatisticsTableModel statisticsModel = new StatisticsTableModel(); 069 private final JPanel pnlTagResolver = new JPanel(new BorderLayout()); 070 071 /** 072 * Constructs a new {@code PasteTagsConflictResolverDialog}. 073 * @param owner parent component 074 */ 075 public PasteTagsConflictResolverDialog(Component owner) { 076 super(GuiHelper.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL); 077 build(); 078 } 079 080 protected final void build() { 081 setTitle(tr("Conflicts in pasted tags")); 082 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 083 resolvers.put(type, new TagConflictResolver()); 084 resolvers.get(type).getModel().addPropertyChangeListener(this); 085 } 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, 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 InputMapUtils.addEscapeAction(getRootPane(), new CancelAction()); 108 109 } 110 111 protected JPanel buildButtonPanel() { 112 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 113 114 // -- apply button 115 ApplyAction applyAction = new ApplyAction(); 116 allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction); 117 for (TagConflictResolver r : resolvers.values()) { 118 r.getModel().addPropertyChangeListener(applyAction); 119 } 120 pnl.add(new SideButton(applyAction)); 121 122 // -- cancel button 123 CancelAction cancelAction = new CancelAction(); 124 pnl.add(new SideButton(cancelAction)); 125 126 return pnl; 127 } 128 129 protected JPanel buildSourceAndTargetInfoPanel() { 130 JPanel pnl = new JPanel(new BorderLayout()); 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, 158 Map<OsmPrimitiveType, Integer> targetStatistics) { 159 mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY; 160 tagsForAllPrimitives = tagsForAllPrimitives == null ? new TagCollection() : tagsForAllPrimitives; 161 sourceStatistics = sourceStatistics == null ? new HashMap<>() : sourceStatistics; 162 targetStatistics = targetStatistics == null ? new HashMap<>() : 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.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, 200 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.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 final class CancelAction extends AbstractAction { 266 267 private CancelAction() { 268 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 269 putValue(Action.NAME, tr("Cancel")); 270 new ImageProvider("cancel").getResource().attachImageIcon(this); 271 setEnabled(true); 272 } 273 274 @Override 275 public void actionPerformed(ActionEvent arg0) { 276 setVisible(false); 277 setCanceled(true); 278 } 279 } 280 281 final class ApplyAction extends AbstractAction implements PropertyChangeListener { 282 283 private ApplyAction() { 284 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 285 putValue(Action.NAME, tr("Apply")); 286 new ImageProvider("ok").getResource().attachImageIcon(this); 287 updateEnabledState(); 288 } 289 290 @Override 291 public void actionPerformed(ActionEvent arg0) { 292 setVisible(false); 293 } 294 295 void updateEnabledState() { 296 if (mode == null) { 297 setEnabled(false); 298 } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) { 299 setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely()); 300 } else { 301 setEnabled(resolvers.values().stream().allMatch(val -> val.getModel().isResolvedCompletely())); 302 } 303 } 304 305 @Override 306 public void propertyChange(PropertyChangeEvent evt) { 307 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 308 updateEnabledState(); 309 } 310 } 311 } 312 313 @Override 314 public void setVisible(boolean visible) { 315 if (visible) { 316 new WindowGeometry( 317 getClass().getName() + ".geometry", 318 WindowGeometry.centerOnScreen(new Dimension(600, 400)) 319 ).applySafe(this); 320 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 321 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 322 } 323 super.setVisible(visible); 324 } 325 326 /** 327 * Returns conflict resolution. 328 * @return conflict resolution 329 */ 330 public TagCollection getResolution() { 331 return allPrimitivesResolver.getModel().getResolution(); 332 } 333 334 public TagCollection getResolution(OsmPrimitiveType type) { 335 if (type == null) return null; 336 return resolvers.get(type).getModel().getResolution(); 337 } 338 339 @Override 340 public void propertyChange(PropertyChangeEvent evt) { 341 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 342 TagConflictResolverModel model = (TagConflictResolverModel) evt.getSource(); 343 for (int i = 0; i < tpResolvers.getTabCount(); i++) { 344 TagConflictResolver resolver = (TagConflictResolver) tpResolvers.getComponentAt(i); 345 if (model == resolver.getModel()) { 346 tpResolvers.setIconAt(i, 347 (Integer) evt.getNewValue() == 0 ? iconResolved : iconUnresolved 348 ); 349 } 350 } 351 } 352 } 353 354 static final class StatisticsInfo { 355 int numTags; 356 final Map<OsmPrimitiveType, Integer> sourceInfo; 357 final Map<OsmPrimitiveType, Integer> targetInfo; 358 359 StatisticsInfo() { 360 sourceInfo = new EnumMap<>(OsmPrimitiveType.class); 361 targetInfo = new EnumMap<>(OsmPrimitiveType.class); 362 } 363 } 364 365 static final class StatisticsTableModel extends DefaultTableModel { 366 private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") }; 367 private final transient List<StatisticsInfo> data = new ArrayList<>(); 368 369 @Override 370 public Object getValueAt(int row, int column) { 371 if (row == 0) 372 return HEADERS[column]; 373 else if (row -1 < data.size()) 374 return data.get(row -1); 375 else 376 return null; 377 } 378 379 @Override 380 public boolean isCellEditable(int row, int column) { 381 return false; 382 } 383 384 @Override 385 public int getRowCount() { 386 return data == null ? 1 : data.size() + 1; 387 } 388 389 void reset() { 390 data.clear(); 391 } 392 393 void append(StatisticsInfo info) { 394 data.add(info); 395 fireTableDataChanged(); 396 } 397 } 398 399 static final class StatisticsInfoRenderer extends JLabel implements TableCellRenderer { 400 private void reset() { 401 setIcon(null); 402 setText(""); 403 setFont(UIManager.getFont("Table.font")); 404 } 405 406 private void renderNumTags(StatisticsInfo info) { 407 if (info == null) return; 408 setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags)); 409 } 410 411 private void renderStatistics(Map<OsmPrimitiveType, Integer> stat) { 412 if (stat == null) return; 413 if (stat.isEmpty()) return; 414 if (stat.size() == 1) { 415 setIcon(ImageProvider.get(stat.keySet().iterator().next())); 416 } else { 417 setIcon(ImageProvider.get("data", "object")); 418 } 419 StringBuilder text = new StringBuilder(); 420 for (Entry<OsmPrimitiveType, Integer> entry: stat.entrySet()) { 421 OsmPrimitiveType type = entry.getKey(); 422 int numPrimitives = entry.getValue() == null ? 0 : entry.getValue(); 423 if (numPrimitives == 0) { 424 continue; 425 } 426 String msg; 427 switch(type) { 428 case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives, numPrimitives); break; 429 case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break; 430 case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break; 431 default: throw new AssertionError(); 432 } 433 if (text.length() > 0) { 434 text.append(", "); 435 } 436 text.append(msg); 437 } 438 setText(text.toString()); 439 } 440 441 private void renderFrom(StatisticsInfo info) { 442 renderStatistics(info.sourceInfo); 443 } 444 445 private void renderTo(StatisticsInfo info) { 446 renderStatistics(info.targetInfo); 447 } 448 449 @Override 450 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, 451 boolean hasFocus, int row, int column) { 452 reset(); 453 if (value == null) 454 return this; 455 456 if (row == 0) { 457 setFont(getFont().deriveFont(Font.BOLD)); 458 setText((String) value); 459 } else { 460 StatisticsInfo info = (StatisticsInfo) value; 461 462 switch(column) { 463 case 0: renderNumTags(info); break; 464 case 1: renderFrom(info); break; 465 case 2: renderTo(info); break; 466 default: // Do nothing 467 } 468 } 469 return this; 470 } 471 } 472 473 static final class StatisticsInfoTable extends JPanel { 474 475 StatisticsInfoTable(StatisticsTableModel model) { 476 JTable infoTable = new JTable(model, 477 new TagTableColumnModelBuilder(new StatisticsInfoRenderer(), tr("Paste ..."), tr("From ..."), tr("To ...")).build()); 478 infoTable.setShowHorizontalLines(true); 479 infoTable.setShowVerticalLines(false); 480 infoTable.setEnabled(false); 481 setLayout(new BorderLayout()); 482 add(infoTable, BorderLayout.CENTER); 483 } 484 485 @Override 486 public Insets getInsets() { 487 Insets insets = super.getInsets(); 488 insets.bottom = 20; 489 return insets; 490 } 491 } 492}