// License: GPL. For details, see LICENSE file.
package relcontext.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Dialog.ModalityType;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.Box;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
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.tools.GBC;
import org.openstreetmap.josm.tools.Shortcut;
import relcontext.ChosenRelation;
/**
* Creates new multipolygon from selected ways.
* Choose relation afterwards.
*
* @author Zverik
*/
public class CreateMultipolygonAction extends JosmAction {
private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
protected ChosenRelation chRel;
public CreateMultipolygonAction(ChosenRelation chRel) {
super("Multi", "data/multipolygon", tr("Create a multipolygon from selected objects"),
Shortcut.registerShortcut("reltoolbox:multipolygon", tr("Relation Toolbox: {0}", tr("Create multipolygon")),
KeyEvent.VK_A, Shortcut.ALT_CTRL), false);
this.chRel = chRel;
updateEnabledState();
}
public CreateMultipolygonAction() {
this(null);
}
public static boolean getDefaultPropertyValue(String property) {
if (property.equals("boundary"))
return false;
else if (property.equals("boundaryways"))
return true;
else if (property.equals("tags"))
return true;
else if (property.equals("alltags"))
return false;
else if (property.equals("single"))
return true;
else if (property.equals("allowsplit"))
return false;
throw new IllegalArgumentException(property);
}
private boolean getPref(String property) {
return Main.pref.getBoolean(PREF_MULTIPOLY + property, getDefaultPropertyValue(property));
}
@Override
public void actionPerformed(ActionEvent e) {
boolean isBoundary = getPref("boundary");
DataSet ds = getLayerManager().getEditDataSet();
Collection<Way> selectedWays = ds.getSelectedWays();
if (!isBoundary && getPref("tags")) {
List<Relation> rels = null;
if (getPref("allowsplit") || selectedWays.size() == 1) {
if (SplittingMultipolygons.canProcess(selectedWays)) {
rels = SplittingMultipolygons.process(ds.getSelectedWays());
}
} else {
if (TheRing.areAllOfThoseRings(selectedWays)) {
List<Command> commands = new ArrayList<>();
rels = TheRing.makeManySimpleMultipolygons(ds.getSelectedWays(), commands);
if (!commands.isEmpty()) {
Main.main.undoRedo.add(new SequenceCommand(tr("Create multipolygons from rings"), commands));
}
}
}
if (rels != null && !rels.isEmpty()) {
if (chRel != null) {
chRel.set(rels.size() == 1 ? rels.get(0) : null);
}
if (rels.size() == 1) {
ds.setSelected(rels);
} else {
ds.clearSelection();
}
return;
}
}
// for now, just copying standard action
MultipolygonBuilder mpc = new MultipolygonBuilder();
String error = mpc.makeFromWays(getLayerManager().getEditDataSet().getSelectedWays());
if (error != null) {
JOptionPane.showMessageDialog(Main.parent, error);
return;
}
Relation rel = new Relation();
if (isBoundary) {
rel.put("type", "boundary");
rel.put("boundary", "administrative");
} else {
rel.put("type", "multipolygon");
}
for (MultipolygonBuilder.JoinedPolygon poly : mpc.outerWays) {
for (Way w : poly.ways) {
rel.addMember(new RelationMember("outer", w));
}
}
for (MultipolygonBuilder.JoinedPolygon poly : mpc.innerWays) {
for (Way w : poly.ways) {
rel.addMember(new RelationMember("inner", w));
}
}
List<Command> list = removeTagsFromInnerWays(rel);
if (!list.isEmpty() && isBoundary) {
Main.main.undoRedo.add(new SequenceCommand(tr("Move tags from ways to relation"), list));
list = new ArrayList<>();
}
if (isBoundary) {
if (!askForAdminLevelAndName(rel))
return;
addBoundaryMembers(rel);
if (getPref("boundaryways")) {
list.addAll(fixWayTagsForBoundary(rel));
}
}
list.add(new AddCommand(rel));
Main.main.undoRedo.add(new SequenceCommand(tr("Create multipolygon"), list));
if (chRel != null) {
chRel.set(rel);
}
getLayerManager().getEditDataSet().setSelected(rel);
}
@Override
protected void updateEnabledState() {
if (getLayerManager().getEditDataSet() == null) {
setEnabled(false);
} else {
updateEnabledState(getLayerManager().getEditDataSet().getSelected());
}
}
@Override
protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
boolean isEnabled = true;
if (selection == null || selection.isEmpty()) {
isEnabled = false;
} else {
if (!getPref("boundary")) {
for (OsmPrimitive p : selection) {
if (!(p instanceof Way)) {
isEnabled = false;
break;
}
}
}
}
setEnabled(isEnabled);
}
/**
* Add selected nodes and relations with corresponding roles.
*/
private void addBoundaryMembers(Relation rel) {
for (OsmPrimitive p : getLayerManager().getEditDataSet().getSelected()) {
String role = null;
if (p.getType().equals(OsmPrimitiveType.RELATION)) {
role = "subarea";
} else if (p.getType().equals(OsmPrimitiveType.NODE)) {
Node n = (Node) p;
if (!n.isIncomplete()) {
if (n.hasKey("place")) {
role = "admin_centre";
} else {
role = "label";
}
}
}
if (role != null) {
rel.addMember(new RelationMember(role, p));
}
}
}
/**
* For all untagged ways in relation, add tags boundary and admin_level.
*/
private List<Command> fixWayTagsForBoundary(Relation rel) {
List<Command> commands = new ArrayList<>();
if (!rel.hasKey("boundary") || !rel.hasKey("admin_level"))
return commands;
String adminLevelStr = rel.get("admin_level");
int adminLevel = 0;
try {
adminLevel = Integer.parseInt(adminLevelStr);
} catch (NumberFormatException e) {
return commands;
}
Set<OsmPrimitive> waysBoundary = new HashSet<>();
Set<OsmPrimitive> waysAdminLevel = new HashSet<>();
for (OsmPrimitive p : rel.getMemberPrimitives()) {
if (p instanceof Way) {
int count = 0;
if (p.hasKey("boundary") && p.get("boundary").equals("administrative")) {
count++;
}
if (p.hasKey("admin_level")) {
count++;
}
if (p.keySet().size() - count == 0) {
if (!p.hasKey("boundary")) {
waysBoundary.add(p);
}
if (!p.hasKey("admin_level")) {
waysAdminLevel.add(p);
} else {
try {
int oldAdminLevel = Integer.parseInt(p.get("admin_level"));
if (oldAdminLevel > adminLevel) {
waysAdminLevel.add(p);
}
} catch (NumberFormatException e) {
waysAdminLevel.add(p); // some garbage, replace it
}
}
}
}
}
if (!waysBoundary.isEmpty()) {
commands.add(new ChangePropertyCommand(waysBoundary, "boundary", "administrative"));
}
if (!waysAdminLevel.isEmpty()) {
commands.add(new ChangePropertyCommand(waysAdminLevel, "admin_level", adminLevelStr));
}
return commands;
}
public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList(new String[] {"barrier", "source"});
private static final Set<String> REMOVE_FROM_BOUNDARY_TAGS = new TreeSet<>(Arrays.asList(new String[] {
"boundary", "boundary_type", "type", "admin_level"
}));
/**
* This method removes tags/value pairs from inner ways that are present in relation or outer ways.
* It was copypasted from the standard {@link org.openstreetmap.josm.actions.CreateMultipolygonAction}.
* Todo: rewrite it.
*/
private List<Command> removeTagsFromInnerWays(Relation relation) {
Map<String, String> values = new HashMap<>();
if (relation.hasKeys()) {
for (String key : relation.keySet()) {
values.put(key, relation.get(key));
}
}
List<Way> innerWays = new ArrayList<>();
List<Way> outerWays = new ArrayList<>();
Set<String> conflictingKeys = new TreeSet<>();
for (RelationMember m : relation.getMembers()) {
if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
innerWays.add(m.getWay());
}
if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
Way way = m.getWay();
outerWays.add(way);
for (String key : way.keySet()) {
if (!values.containsKey(key)) { //relation values take precedence
values.put(key, way.get(key));
} else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) {
conflictingKeys.add(key);
}
}
}
}
// filter out empty key conflicts - we need second iteration
boolean isBoundary = getPref("boundary");
if (isBoundary || !getPref("alltags")) {
for (RelationMember m : relation.getMembers()) {
if (m.hasRole() && m.getRole().equals("outer") && m.isWay()) {
for (String key : values.keySet()) {
if (!m.getWay().hasKey(key) && !relation.hasKey(key)) {
conflictingKeys.add(key);
}
}
}
}
}
for (String key : conflictingKeys) {
values.remove(key);
}
for (String linearTag : Main.pref.getCollection(PREF_MULTIPOLY + "lineartags", DEFAULT_LINEAR_TAGS)) {
values.remove(linearTag);
}
if (values.containsKey("natural") && values.get("natural").equals("coastline")) {
values.remove("natural");
}
String name = values.get("name");
if (isBoundary) {
Set<String> keySet = new TreeSet<>(values.keySet());
for (String key : keySet) {
if (!REMOVE_FROM_BOUNDARY_TAGS.contains(key)) {
values.remove(key);
}
}
}
values.put("area", "yes");
List<Command> commands = new ArrayList<>();
boolean moveTags = getPref("tags");
for (String key : values.keySet()) {
List<OsmPrimitive> affectedWays = new ArrayList<>();
String value = values.get(key);
for (Way way : innerWays) {
if (way.hasKey(key) && (isBoundary || value.equals(way.get(key)))) {
affectedWays.add(way);
}
}
if (moveTags) {
// remove duplicated tags from outer ways
for (Way way : outerWays) {
if (way.hasKey(key)) {
affectedWays.add(way);
}
}
}
if (affectedWays.size() > 0) {
commands.add(new ChangePropertyCommand(affectedWays, key, null));
}
}
if (moveTags) {
// add those tag values to the relation
if (isBoundary) {
values.put("name", name);
}
boolean fixed = false;
Relation r2 = new Relation(relation);
for (String key : values.keySet()) {
if (!r2.hasKey(key) && !key.equals("area")
&& (!isBoundary || key.equals("admin_level") || key.equals("name"))) {
if (relation.isNew()) {
relation.put(key, values.get(key));
} else {
r2.put(key, values.get(key));
}
fixed = true;
}
}
if (fixed && !relation.isNew()) {
commands.add(new ChangeCommand(relation, r2));
}
}
return commands;
}
/**
*
* @param rel relation
* @return false if user pressed "cancel".
*/
private boolean askForAdminLevelAndName(Relation rel) {
String relAL = rel.get("admin_level");
String relName = rel.get("name");
if (relAL != null && relName != null)
return true;
JPanel panel = new JPanel(new GridBagLayout());
panel.add(new JLabel(tr("Enter admin level and name for the border relation:")), GBC.eol().insets(0, 0, 0, 5));
final JTextField admin = new JTextField();
admin.setText(relAL != null ? relAL : Main.pref.get(PREF_MULTIPOLY + "lastadmin", ""));
panel.add(new JLabel(tr("Admin level")), GBC.std());
panel.add(Box.createHorizontalStrut(10), GBC.std());
panel.add(admin, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
final JTextField name = new JTextField();
if (relName != null) {
name.setText(relName);
}
panel.add(new JLabel(tr("Name")), GBC.std());
panel.add(Box.createHorizontalStrut(10), GBC.std());
panel.add(name, GBC.eol().fill(GBC.HORIZONTAL));
final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) {
@Override
public void selectInitialValue() {
admin.requestFocusInWindow();
admin.selectAll();
}
};
final JDialog dlg = optionPane.createDialog(Main.parent, tr("Create a new relation"));
dlg.setModalityType(ModalityType.DOCUMENT_MODAL);
name.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dlg.setVisible(false);
optionPane.setValue(JOptionPane.OK_OPTION);
}
});
dlg.setVisible(true);
Object answer = optionPane.getValue();
if (answer == null || answer == JOptionPane.UNINITIALIZED_VALUE
|| (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))
return false;
String admin_level = admin.getText().trim();
String new_name = name.getText().trim();
if (admin_level.equals("10") || (admin_level.length() == 1 && Character.isDigit(admin_level.charAt(0)))) {
rel.put("admin_level", admin_level);
Main.pref.put(PREF_MULTIPOLY + "lastadmin", admin_level);
}
if (new_name.length() > 0) {
rel.put("name", new_name);
}
return true;
}
}