// License: GPL. For details, see LICENSE file.
package relcontext.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
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.gui.DefaultNameFormatter;
import org.openstreetmap.josm.tools.Geometry;
import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
/**
*
* @author Zverik
*/
public final class SplittingMultipolygons {
private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
private SplittingMultipolygons() {
// Hide default constructor for utilities classes
}
public static boolean canProcess(Collection<Way> ways) {
List<Way> rings = new ArrayList<>();
List<Way> arcs = new ArrayList<>();
Area a = Main.getLayerManager().getEditDataSet().getDataSourceArea();
for (Way way : ways) {
if (way.isDeleted())
return false;
for (Node n : way.getNodes()) {
LatLon ll = n.getCoor();
if (n.isIncomplete() || (a != null && !a.contains(ll.getX(), ll.getY())))
return false;
}
if (way.isClosed()) {
rings.add(way);
} else {
arcs.add(way);
}
}
// If there are more that one segment, check that they touch rings
if (arcs.size() > 1) {
for (Way segment : arcs) {
boolean found = false;
for (Way ring : rings) {
if (ring.containsNode(segment.firstNode()) && ring.containsNode(segment.lastNode())) {
found = true;
}
}
if (!found)
return false;
}
}
if (rings.isEmpty() && arcs.isEmpty())
return false;
// check for non-containment of rings
for (int i = 0; i < rings.size() - 1; i++) {
for (int j = i + 1; j < rings.size(); j++) {
PolygonIntersection intersection = Geometry.polygonIntersection(rings.get(i).getNodes(), rings.get(j).getNodes());
if (intersection == PolygonIntersection.FIRST_INSIDE_SECOND || intersection == PolygonIntersection.SECOND_INSIDE_FIRST)
return false;
}
}
return true;
}
public static List<Relation> process(Collection<Way> selectedWays) {
List<Relation> result = new ArrayList<>();
List<Way> rings = new ArrayList<>();
List<Way> arcs = new ArrayList<>();
for (Way way : selectedWays) {
if (way.isClosed()) {
rings.add(way);
} else {
arcs.add(way);
}
}
for (Way ring : rings) {
List<Command> commands = new ArrayList<>();
Relation newRelation = SplittingMultipolygons.attachRingToNeighbours(ring, commands);
if (newRelation != null && !commands.isEmpty()) {
Main.main.undoRedo.add(commands.get(0));
result.add(newRelation);
}
}
for (Way arc : arcs) {
List<Command> commands = new ArrayList<>();
Relation newRelation = SplittingMultipolygons.tryToCloseOneWay(arc, commands);
if (newRelation != null && !commands.isEmpty()) {
Main.main.undoRedo.add(commands.get(0));
result.add(newRelation);
}
}
return result;
}
/**
* Appends "append" to "base" so the closed polygon forms.
*/
private static void closePolygon(List<Node> base, List<Node> append) {
if (append.get(0).equals(base.get(0)) && append.get(append.size() - 1).equals(base.get(base.size() - 1))) {
List<Node> ap2 = new ArrayList<>(append);
Collections.reverse(ap2);
append = ap2;
}
base.remove(base.size() - 1);
base.addAll(append);
}
/**
* Checks if a middle point between two nodes is inside a polygon. Useful to check if the way is inside.
*/
private static boolean segmentInsidePolygon(Node n1, Node n2, List<Node> polygon) {
EastNorth en1 = n1.getEastNorth();
EastNorth en2 = n2.getEastNorth();
Node testNode = new Node(new EastNorth((en1.east() + en2.east()) / 2.0, (en1.north() + en2.north()) / 2.0));
return Geometry.nodeInsidePolygon(testNode, polygon);
}
/**
* Splits a way with regard to containing relations. This modifies the way and the relation, be prepared.
* @param w The way.
* @param n The node to split at.
* @param commands List of commands to add way/relation changing to. If null, never mind.
* @return Newly created ways. <b>Warning:</b> if commands is no not, newWays contains {@code w}.
*/
public static List<Way> splitWay(Way w, Node n1, Node n2, List<Command> commands) {
List<Node> nodes = new ArrayList<>(w.getNodes());
if (w.isClosed()) {
nodes.remove(nodes.size() - 1);
}
int index1 = nodes.indexOf(n1);
int index2 = n2 == null ? -1 : nodes.indexOf(n2);
if (index1 > index2) {
int tmp = index1;
index1 = index2;
index2 = tmp;
}
// right now index2 >= index1
if (index2 < 1 || index1 >= w.getNodesCount() - 1 || index2 >= w.getNodesCount())
return Collections.emptyList();
if (w.isClosed() && (index1 < 0 || index1 == index2 || index1 + w.getNodesCount() == index2))
return Collections.emptyList();
// todo: download parent relations!
// make a list of segments
List<List<Node>> chunks = new ArrayList<>(2);
List<Node> chunk = new ArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
chunk.add(nodes.get(i));
if ((w.isClosed() || chunk.size() > 1) && (i == index1 || i == index2)) {
chunks.add(chunk);
chunk = new ArrayList<>();
chunk.add(nodes.get(i));
}
}
chunks.add(chunk);
// for closed way ignore the way boundary
if (w.isClosed()) {
chunks.get(chunks.size() - 1).addAll(chunks.get(0));
chunks.remove(0);
} else if (chunks.get(chunks.size() - 1).size() < 2) {
chunks.remove(chunks.size() - 1);
}
// build a map of referencing relations
Map<Relation, Integer> references = new HashMap<>();
List<Command> relationCommands = new ArrayList<>();
for (OsmPrimitive p : w.getReferrers()) {
if (p instanceof Relation) {
Relation rel = commands == null ? (Relation) p : new Relation((Relation) p);
if (commands != null) {
relationCommands.add(new ChangeCommand(p, rel));
}
for (int i = 0; i < rel.getMembersCount(); i++) {
if (rel.getMember(i).getMember().equals(w)) {
references.put(rel, Integer.valueOf(i));
}
}
}
}
// build ways
List<Way> result = new ArrayList<>();
Way updatedWay = commands == null ? w : new Way(w);
updatedWay.setNodes(chunks.get(0));
if (commands != null) {
commands.add(new ChangeCommand(w, updatedWay));
result.add(updatedWay);
}
for (int i = 1; i < chunks.size(); i++) {
List<Node> achunk = chunks.get(i);
Way newWay = new Way();
newWay.setKeys(w.getKeys());
result.add(newWay);
for (Relation rel : references.keySet()) {
int relIndex = references.get(rel);
rel.addMember(relIndex + 1, new RelationMember(rel.getMember(relIndex).getRole(), newWay));
}
newWay.setNodes(achunk);
if (commands != null) {
commands.add(new AddCommand(newWay));
}
}
if (commands != null) {
commands.addAll(relationCommands);
}
return result;
}
public static List<Way> splitWay(Way w, Node n1, Node n2) {
return splitWay(w, n1, n2, null);
}
/**
* Find a way the tips of a segment, ensure it's in a multipolygon and try to close the relation.
*/
public static Relation tryToCloseOneWay(Way segment, List<Command> resultingCommands) {
if (segment.isClosed() || segment.isIncomplete())
return null;
List<Way> ways = intersection(
OsmPrimitive.getFilteredList(segment.firstNode().getReferrers(), Way.class),
OsmPrimitive.getFilteredList(segment.lastNode().getReferrers(), Way.class));
ways.remove(segment);
for (Iterator<Way> iter = ways.iterator(); iter.hasNext();) {
boolean save = false;
for (OsmPrimitive ref : iter.next().getReferrers()) {
if (ref instanceof Relation && ((Relation) ref).isMultipolygon() && !ref.isDeleted()) {
save = true;
}
}
if (!save) {
iter.remove();
}
}
if (ways.isEmpty())
return null; // well...
Way target = ways.get(0);
// time to create a new multipolygon relation and a command stack
List<Command> commands = new ArrayList<>();
Relation newRelation = new Relation();
newRelation.put("type", "multipolygon");
newRelation.addMember(new RelationMember("outer", segment));
Collection<String> linearTags = Main.pref.getCollection(PREF_MULTIPOLY + "lineartags", CreateMultipolygonAction.DEFAULT_LINEAR_TAGS);
Way segmentCopy = new Way(segment);
boolean changed = false;
for (String key : segmentCopy.keySet()) {
if (!linearTags.contains(key)) {
newRelation.put(key, segmentCopy.get(key));
segmentCopy.remove(key);
changed = true;
}
}
if (changed) {
commands.add(new ChangeCommand(segment, segmentCopy));
}
// now split the way, at last
List<Way> newWays = new ArrayList<>(splitWay(target, segment.firstNode(), segment.lastNode(), commands));
Way addingWay = null;
if (target.isClosed()) {
Way utarget = newWays.get(1);
Way alternate = newWays.get(0);
List<Node> testRing = new ArrayList<>(segment.getNodes());
closePolygon(testRing, utarget.getNodes());
addingWay = segmentInsidePolygon(alternate.getNode(0), alternate.getNode(1), testRing) ? alternate : utarget;
} else {
for (Way w : newWays) {
if ((w.firstNode().equals(segment.firstNode()) && w.lastNode().equals(segment.lastNode()))
|| (w.firstNode().equals(segment.lastNode()) && w.lastNode().equals(segment.firstNode()))) {
addingWay = w;
break;
}
}
}
newRelation.addMember(new RelationMember("outer", addingWay.getUniqueId() == target.getUniqueId() ? target : addingWay));
commands.add(new AddCommand(newRelation));
resultingCommands.add(new SequenceCommand(tr("Complete multipolygon for way {0}",
DefaultNameFormatter.getInstance().format(segment)), commands));
return newRelation;
}
/**
* Returns all elements from {@code list1} that are in {@code list2}.
*/
private static <T> List<T> intersection(Collection<T> list1, Collection<T> list2) {
List<T> result = new ArrayList<>();
for (T item : list1) {
if (list2.contains(item)) {
result.add(item);
}
}
return result;
}
/**
* Make a multipolygon out of the ring, but split it to attach to neighboring multipolygons.
*/
public static Relation attachRingToNeighbours(Way ring, List<Command> resultingCommands) {
if (!ring.isClosed() || ring.isIncomplete())
return null;
Map<Way, Boolean> touchingWays = new HashMap<>();
for (Node n : ring.getNodes()) {
for (OsmPrimitive p : n.getReferrers()) {
if (p instanceof Way && !p.equals(ring)) {
for (OsmPrimitive r : p.getReferrers()) {
if (r instanceof Relation && ((Relation) r).hasKey("type") && ((Relation) r).get("type").equals("multipolygon")) {
if (touchingWays.containsKey(p)) {
touchingWays.put((Way) p, Boolean.TRUE);
} else {
touchingWays.put((Way) p, Boolean.FALSE);
}
break;
}
}
}
}
}
List<TheRing> otherWays = new ArrayList<>();
for (Way w : touchingWays.keySet()) {
if (touchingWays.get(w)) {
otherWays.add(new TheRing(w));
}
}
// now touchingWays has only ways that touch the ring twice
List<Command> commands = new ArrayList<>();
TheRing theRing = new TheRing(ring); // this is actually useful
for (TheRing otherRing : otherWays) {
theRing.collide(otherRing);
}
theRing.putSourceWayFirst();
for (TheRing otherRing : otherWays) {
otherRing.putSourceWayFirst();
}
Map<Relation, Relation> relationCache = new HashMap<>();
for (TheRing otherRing : otherWays) {
commands.addAll(otherRing.getCommands(false, relationCache));
}
commands.addAll(theRing.getCommands(relationCache));
TheRing.updateCommandsWithRelations(commands, relationCache);
resultingCommands.add(new SequenceCommand(tr("Complete multipolygon for way {0}",
DefaultNameFormatter.getInstance().format(ring)), commands));
return theRing.getRelation();
}
}