package org.openstreetmap.josm.gui.conflict.tags;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.TagCollection;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.WindowGeometry;
/**
* This dialog helps to resolve conflicts occurring when ways are combined or
* nodes are merged.
*
* There is a singleton instance of this dialog which can be retrieved using
* {@see #getInstance()}.
*
* The dialog uses two models: one for resolving tag conflicts, the other
* for resolving conflicts in relation memberships. For both models there are accessors,
* i.e {@see #getTagConflictResolverModel()} and {@see #getRelationMemberConflictResolverModel()}.
*
* Models have to be <strong>populated</strong> before the dialog is launched. Example:
* <pre>
* CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
* dialog.getTagConflictResolverModel().populate(aTagCollection);
* dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection);
* dialog.prepareDefaultDecisions();
* </pre>
*
* You should also set the target primitive which other primitives (ways or nodes) are
* merged to, see {@see #setTargetPrimitive(OsmPrimitive)}.
*
* After the dialog is closed use {@see #isCancelled()} to check whether the user canceled
* the dialog. If it wasn't canceled you may build a collection of {@see Command} objects
* which reflect the conflict resolution decisions the user made in the dialog:
* see {@see #buildResolutionCommands()}
*
*
*/
public class CombinePrimitiveResolverDialog extends JDialog {
/** the unique instance of the dialog */
static private CombinePrimitiveResolverDialog instance;
/**
* Replies the unique instance of the dialog
*
* @return the unique instance of the dialog
*/
public static CombinePrimitiveResolverDialog getInstance() {
if (instance == null) {
instance = new CombinePrimitiveResolverDialog(Main.parent);
}
return instance;
}
private AutoAdjustingSplitPane spTagConflictTypes;
private TagConflictResolver pnlTagConflictResolver;
private RelationMemberConflictResolver pnlRelationMemberConflictResolver;
private boolean cancelled;
private JPanel pnlButtons;
private OsmPrimitive targetPrimitive;
/** the private help action */
private ContextSensitiveHelpAction helpAction;
/** the apply button */
private SideButton btnApply;
/**
* Replies the target primitive the collection of primitives is merged
* or combined to.
*
* @return the target primitive
*/
public OsmPrimitive getTargetPrimitmive() {
return targetPrimitive;
}
/**
* Sets the primitive the collection of primitives is merged or combined
* to.
*
* @param primitive the target primitive
*/
public void setTargetPrimitive(OsmPrimitive primitive) {
this.targetPrimitive = primitive;
updateTitle();
if (primitive instanceof Way) {
pnlRelationMemberConflictResolver.initForWayCombining();
} else if (primitive instanceof Node) {
pnlRelationMemberConflictResolver.initForNodeMerging();
}
}
protected void updateTitle() {
if (targetPrimitive == null) {
setTitle(tr("Conflicts when combining primitives"));
return;
}
if (targetPrimitive instanceof Way) {
setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive
.getDisplayName(DefaultNameFormatter.getInstance())));
helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts"));
getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts"));
} else if (targetPrimitive instanceof Node) {
setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive
.getDisplayName(DefaultNameFormatter.getInstance())));
helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts"));
getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts"));
}
}
protected void build() {
getContentPane().setLayout(new BorderLayout());
updateTitle();
spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT);
spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel());
spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel());
getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH);
addWindowListener(new AdjustDividerLocationAction());
HelpUtil.setHelpContext(getRootPane(), ht("/"));
}
protected JPanel buildTagConflictResolverPanel() {
pnlTagConflictResolver = new TagConflictResolver();
return pnlTagConflictResolver;
}
protected JPanel buildRelationMemberConflictResolverPanel() {
pnlRelationMemberConflictResolver = new RelationMemberConflictResolver();
return pnlRelationMemberConflictResolver;
}
protected JPanel buildButtonPanel() {
JPanel pnl = new JPanel();
pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
// -- apply button
ApplyAction applyAction = new ApplyAction();
pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
btnApply = new SideButton(applyAction);
btnApply.setFocusable(true);
pnl.add(btnApply);
// -- cancel button
CancelAction cancelAction = new CancelAction();
pnl.add(new SideButton(cancelAction));
// -- help button
helpAction = new ContextSensitiveHelpAction();
pnl.add(new SideButton(helpAction));
return pnl;
}
public CombinePrimitiveResolverDialog(Component owner) {
super(JOptionPane.getFrameForComponent(owner), true /* modal */);
build();
}
public TagConflictResolverModel getTagConflictResolverModel() {
return pnlTagConflictResolver.getModel();
}
public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
return pnlRelationMemberConflictResolver.getModel();
}
protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
LinkedList<Command> cmds = new LinkedList<Command>();
for (String key : tc.getKeys()) {
if (tc.hasUniqueEmptyValue(key)) {
if (primitive.get(key) != null) {
cmds.add(new ChangePropertyCommand(primitive, key, null));
}
} else {
String value = tc.getJoinedValues(key);
if (!value.equals(primitive.get(key))) {
cmds.add(new ChangePropertyCommand(primitive, key, value));
}
}
}
return cmds;
}
public List<Command> buildResolutionCommands() {
List<Command> cmds = new LinkedList<Command>();
TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
if (allResolutions.size() > 0) {
cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
}
if (targetPrimitive.get("created_by") != null) {
cmds.add(new ChangePropertyCommand(targetPrimitive, "created_by", null));
}
if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) {
cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive));
}
Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
.getModifiedRelations(targetPrimitive));
if (cmd != null) {
cmds.add(cmd);
}
return cmds;
}
protected void prepareDefaultTagDecisions() {
TagConflictResolverModel model = getTagConflictResolverModel();
for (int i = 0; i < model.getRowCount(); i++) {
MultiValueResolutionDecision decision = model.getDecision(i);
List<String> values = decision.getValues();
values.remove("");
if (values.size() == 1) {
decision.keepOne(values.get(0));
} else {
decision.keepAll();
}
}
model.rebuild();
}
protected void prepareDefaultRelationDecisions() {
RelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel();
Set<Relation> relations = new HashSet<Relation>();
for (int i = 0; i < model.getNumDecisions(); i++) {
RelationMemberConflictDecision decision = model.getDecision(i);
if (!relations.contains(decision.getRelation())) {
decision.decide(RelationMemberConflictDecisionType.KEEP);
relations.add(decision.getRelation());
} else {
decision.decide(RelationMemberConflictDecisionType.REMOVE);
}
}
model.refresh();
}
public void prepareDefaultDecisions() {
prepareDefaultTagDecisions();
prepareDefaultRelationDecisions();
}
protected JPanel buildEmptyConflictsPanel() {
JPanel pnl = new JPanel();
pnl.setLayout(new BorderLayout());
pnl.add(new JLabel(tr("No conflicts to resolve")));
return pnl;
}
protected void prepareGUIBeforeConflictResolutionStarts() {
RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
TagConflictResolverModel tagModel = getTagConflictResolverModel();
getContentPane().removeAll();
if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
// display both, the dialog for resolving relation conflicts and for resolving
// tag conflicts
spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
} else if (relModel.getNumDecisions() > 0) {
// relation conflicts only
//
getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
} else if (tagModel.getNumDecisions() > 0) {
// tag conflicts only
//
getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
} else {
getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
}
getContentPane().add(pnlButtons, BorderLayout.SOUTH);
validate();
int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
if (numTagDecisions > 0 && numRelationDecisions > 0) {
spTagConflictTypes.setDividerLocation(0.5);
}
pnlRelationMemberConflictResolver.prepareForEditing();
}
protected void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public boolean isCancelled() {
return cancelled;
}
@Override
public void setVisible(boolean visible) {
if (visible) {
prepareGUIBeforeConflictResolutionStarts();
new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
new Dimension(600, 400))).applySafe(this);
setCancelled(false);
btnApply.requestFocusInWindow();
} else {
new WindowGeometry(this).remember(getClass().getName() + ".geometry");
}
super.setVisible(visible);
}
class CancelAction extends AbstractAction {
public CancelAction() {
putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
putValue(Action.NAME, tr("Cancel"));
putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
setEnabled(true);
}
public void actionPerformed(ActionEvent arg0) {
setCancelled(true);
setVisible(false);
}
}
class ApplyAction extends AbstractAction implements PropertyChangeListener {
public ApplyAction() {
putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
putValue(Action.NAME, tr("Apply"));
putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
updateEnabledState();
}
public void actionPerformed(ActionEvent arg0) {
setVisible(false);
pnlTagConflictResolver.rememberPreferences();
}
protected void updateEnabledState() {
setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
&& pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
}
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
updateEnabledState();
}
if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
updateEnabledState();
}
}
}
class AdjustDividerLocationAction extends WindowAdapter {
@Override
public void windowOpened(WindowEvent e) {
int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
if (numTagDecisions > 0 && numRelationDecisions > 0) {
spTagConflictTypes.setDividerLocation(0.5);
}
}
}
static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
private double dividerLocation;
public AutoAdjustingSplitPane(int newOrientation) {
super(newOrientation);
addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
addHierarchyBoundsListener(this);
}
public void ancestorResized(HierarchyEvent e) {
setDividerLocation((int) (dividerLocation * getHeight()));
}
public void ancestorMoved(HierarchyEvent e) {
// do nothing
}
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
int newVal = (Integer) evt.getNewValue();
if (getHeight() != 0) {
dividerLocation = (double) newVal / (double) getHeight();
}
}
}
}
}