// License: GPL. See LICENSE file for details.
package org.openstreetmap.josm.gui.dialogs;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.marktr;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.AbstractAction;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.SelectionChangedListener;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.conflict.ConflictCollection;
import org.openstreetmap.josm.data.conflict.IConflictListener;
import org.openstreetmap.josm.data.osm.DataSet;
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.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
import org.openstreetmap.josm.gui.PopupMenuHandler;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
/**
* This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle
* dialog on the right of the main frame.
*
*/
public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener{
/**
* Replies the color used to paint conflicts.
*
* @return the color used to paint conflicts
* @since 1221
* @see #paintConflicts
*/
public static Color getColor() {
return Main.pref.getColor(marktr("conflict"), Color.gray);
}
/** the collection of conflicts displayed by this conflict dialog */
private ConflictCollection conflicts;
/** the model for the list of conflicts */
private ConflictListModel model;
/** the list widget for the list of conflicts */
private JList<OsmPrimitive> lstConflicts;
private final JPopupMenu popupMenu = new JPopupMenu();
private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
private ResolveAction actResolve;
private SelectAction actSelect;
/**
* builds the GUI
*/
protected void build() {
model = new ConflictListModel();
lstConflicts = new JList<>(model);
lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
lstConflicts.setCellRenderer(new OsmPrimitivRenderer());
lstConflicts.addMouseListener(new MouseEventHandler());
addListSelectionListener(new ListSelectionListener(){
@Override
public void valueChanged(ListSelectionEvent e) {
Main.map.mapView.repaint();
}
});
SideButton btnResolve = new SideButton(actResolve = new ResolveAction());
addListSelectionListener(actResolve);
SideButton btnSelect = new SideButton(actSelect = new SelectAction());
addListSelectionListener(actSelect);
createLayout(lstConflicts, true, Arrays.asList(new SideButton[] {
btnResolve, btnSelect
}));
popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("conflict"));
}
/**
* constructor
*/
public ConflictDialog() {
super(tr("Conflict"), "conflict", tr("Resolve conflicts."),
Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")),
KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100);
build();
refreshView();
}
@Override
public void showNotify() {
DataSet.addSelectionListener(this);
MapView.addEditLayerChangeListener(this, true);
refreshView();
}
@Override
public void hideNotify() {
MapView.removeEditLayerChangeListener(this);
DataSet.removeSelectionListener(this);
}
/**
* Add a list selection listener to the conflicts list.
* @param listener the ListSelectionListener
* @since 5958
*/
public void addListSelectionListener(ListSelectionListener listener) {
lstConflicts.getSelectionModel().addListSelectionListener(listener);
}
/**
* Remove the given list selection listener from the conflicts list.
* @param listener the ListSelectionListener
* @since 5958
*/
public void removeListSelectionListener(ListSelectionListener listener) {
lstConflicts.getSelectionModel().removeListSelectionListener(listener);
}
/**
* Replies the popup menu handler.
* @return The popup menu handler
* @since 5958
*/
public PopupMenuHandler getPopupMenuHandler() {
return popupMenuHandler;
}
/**
* Launches a conflict resolution dialog for the first selected conflict
*
*/
private final void resolve() {
if (conflicts == null || model.getSize() == 0) return;
int index = lstConflicts.getSelectedIndex();
if (index < 0) {
index = 0;
}
Conflict<? extends OsmPrimitive> c = conflicts.get(index);
ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent);
dialog.getConflictResolver().populate(c);
dialog.setVisible(true);
lstConflicts.setSelectedIndex(index);
Main.map.mapView.repaint();
}
/**
* refreshes the view of this dialog
*/
public final void refreshView() {
OsmDataLayer editLayer = Main.main.getEditLayer();
conflicts = (editLayer == null ? new ConflictCollection() : editLayer.getConflicts());
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
model.fireContentChanged();
updateTitle();
}
});
}
private void updateTitle() {
int conflictsCount = conflicts.size();
if (conflictsCount > 0) {
setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) +
" ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}",
conflicts.getRelationConflicts().size(),
conflicts.getWayConflicts().size(),
conflicts.getNodeConflicts().size())+")");
} else {
setTitle(tr("Conflict"));
}
}
/**
* Paints all conflicts that can be expressed on the main window.
*
* @param g The {@code Graphics} used to paint
* @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
* @since 86
*/
public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
Color preferencesColor = getColor();
if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black)))
return;
g.setColor(preferencesColor);
Visitor conflictPainter = new AbstractVisitor() {
// Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
private final Set<Relation> visited = new HashSet<>();
@Override
public void visit(Node n) {
Point p = nc.getPoint(n);
g.drawRect(p.x-1, p.y-1, 2, 2);
}
public void visit(Node n1, Node n2) {
Point p1 = nc.getPoint(n1);
Point p2 = nc.getPoint(n2);
g.drawLine(p1.x, p1.y, p2.x, p2.y);
}
@Override
public void visit(Way w) {
Node lastN = null;
for (Node n : w.getNodes()) {
if (lastN == null) {
lastN = n;
continue;
}
visit(lastN, n);
lastN = n;
}
}
@Override
public void visit(Relation e) {
if (!visited.contains(e)) {
visited.add(e);
try {
for (RelationMember em : e.getMembers()) {
em.getMember().accept(this);
}
} finally {
visited.remove(e);
}
}
}
};
for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
if (conflicts == null || !conflicts.hasConflictForMy(o)) {
continue;
}
conflicts.getConflictForMy(o).getTheir().accept(conflictPainter);
}
}
@Override
public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
if (oldLayer != null) {
oldLayer.getConflicts().removeConflictListener(this);
}
if (newLayer != null) {
newLayer.getConflicts().addConflictListener(this);
}
refreshView();
}
/**
* replies the conflict collection currently held by this dialog; may be null
*
* @return the conflict collection currently held by this dialog; may be null
*/
public ConflictCollection getConflicts() {
return conflicts;
}
/**
* returns the first selected item of the conflicts list
*
* @return Conflict
*/
public Conflict<? extends OsmPrimitive> getSelectedConflict() {
if (conflicts == null || model.getSize() == 0) return null;
int index = lstConflicts.getSelectedIndex();
if (index < 0) return null;
return conflicts.get(index);
}
@Override
public void onConflictsAdded(ConflictCollection conflicts) {
refreshView();
}
@Override
public void onConflictsRemoved(ConflictCollection conflicts) {
Main.info("1 conflict has been resolved.");
refreshView();
}
@Override
public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
lstConflicts.clearSelection();
for (OsmPrimitive osm : newSelection) {
if (conflicts != null && conflicts.hasConflictForMy(osm)) {
int pos = model.indexOf(osm);
if (pos >= 0) {
lstConflicts.addSelectionInterval(pos, pos);
}
}
}
}
@Override
public String helpTopic() {
return ht("/Dialog/ConflictList");
}
class MouseEventHandler extends PopupMenuLauncher {
public MouseEventHandler() {
super(popupMenu);
}
@Override public void mouseClicked(MouseEvent e) {
if (isDoubleClick(e)) {
resolve();
}
}
}
/**
* The {@link ListModel} for conflicts
*
*/
class ConflictListModel implements ListModel<OsmPrimitive> {
private CopyOnWriteArrayList<ListDataListener> listeners;
public ConflictListModel() {
listeners = new CopyOnWriteArrayList<>();
}
@Override
public void addListDataListener(ListDataListener l) {
if (l != null) {
listeners.addIfAbsent(l);
}
}
@Override
public void removeListDataListener(ListDataListener l) {
listeners.remove(l);
}
protected void fireContentChanged() {
ListDataEvent evt = new ListDataEvent(
this,
ListDataEvent.CONTENTS_CHANGED,
0,
getSize()
);
for (ListDataListener listener : listeners) {
listener.contentsChanged(evt);
}
}
@Override
public OsmPrimitive getElementAt(int index) {
if (index < 0) return null;
if (index >= getSize()) return null;
return conflicts.get(index).getMy();
}
@Override
public int getSize() {
if (conflicts == null) return 0;
return conflicts.size();
}
public int indexOf(OsmPrimitive my) {
if (conflicts == null) return -1;
for (int i=0; i < conflicts.size();i++) {
if (conflicts.get(i).isMatchingMy(my))
return i;
}
return -1;
}
public OsmPrimitive get(int idx) {
if (conflicts == null) return null;
return conflicts.get(idx).getMy();
}
}
class ResolveAction extends AbstractAction implements ListSelectionListener {
public ResolveAction() {
putValue(NAME, tr("Resolve"));
putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above."));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict"));
putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
}
@Override
public void actionPerformed(ActionEvent e) {
resolve();
}
@Override
public void valueChanged(ListSelectionEvent e) {
ListSelectionModel model = (ListSelectionModel)e.getSource();
boolean enabled = model.getMinSelectionIndex() >= 0
&& model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
setEnabled(enabled);
}
}
class SelectAction extends AbstractAction implements ListSelectionListener {
public SelectAction() {
putValue(NAME, tr("Select"));
putValue(SHORT_DESCRIPTION, tr("Set the selected elements on the map to the selected items in the list above."));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
putValue("help", ht("/Dialog/ConflictList#SelectAction"));
}
@Override
public void actionPerformed(ActionEvent e) {
Collection<OsmPrimitive> sel = new LinkedList<>();
for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
sel.add(o);
}
DataSet ds = Main.main.getCurrentDataSet();
if (ds != null) { // Can't see how it is possible but it happened in #7942
ds.setSelected(sel);
}
}
@Override
public void valueChanged(ListSelectionEvent e) {
ListSelectionModel model = (ListSelectionModel)e.getSource();
boolean enabled = model.getMinSelectionIndex() >= 0
&& model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
setEnabled(enabled);
}
}
/**
* Warns the user about the number of detected conflicts
*
* @param numNewConflicts the number of detected conflicts
* @since 5775
*/
public void warnNumNewConflicts(int numNewConflicts) {
if (numNewConflicts == 0) return;
String msg1 = trn(
"There was {0} conflict detected.",
"There were {0} conflicts detected.",
numNewConflicts,
numNewConflicts
);
final StringBuilder sb = new StringBuilder();
sb.append("<html>").append(msg1).append("</html>");
if (numNewConflicts > 0) {
final ButtonSpec[] options = new ButtonSpec[] {
new ButtonSpec(
tr("OK"),
ImageProvider.get("ok"),
tr("Click to close this dialog and continue editing"),
null /* no specific help */
)
};
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
HelpAwareOptionPane.showOptionDialog(
Main.parent,
sb.toString(),
tr("Conflicts detected"),
JOptionPane.WARNING_MESSAGE,
null, /* no icon */
options,
options[0],
ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
);
unfurlDialog();
Main.map.repaint();
}
});
}
}
}