// License: GPL. For details, see LICENSE file.
package relcontext.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JOptionPane;
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.DeleteCommand;
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.tools.Geometry;
import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
/**
* One ring that contains segments forming an outer way of multipolygon.
* This class is used in {@link CreateMultipolygonAction#makeManySimpleMultipolygons(java.util.Collection)}.
*
* @author Zverik
*/
public class TheRing {
private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
private Way source;
private List<RingSegment> segments;
private Relation relation = null;
public TheRing(Way source) {
this.source = source;
segments = new ArrayList<>(1);
segments.add(new RingSegment(source));
}
public static boolean areAllOfThoseRings(Collection<Way> ways) {
List<Way> rings = new ArrayList<>();
for (Way way : ways) {
if (way.isClosed()) {
rings.add(way);
} else
return false;
}
if (rings.isEmpty() || ways.size() == 1)
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;
}
/**
* Creates ALOT of Multipolygons and pets him gently.
* @return list of new relations.
*/
public static List<Relation> makeManySimpleMultipolygons(Collection<Way> selection, List<Command> commands) {
log("---------------------------------------");
List<TheRing> rings = new ArrayList<>(selection.size());
for (Way w : selection) {
rings.add(new TheRing(w));
}
for (int i = 0; i < rings.size() - 1; i++) {
for (int j = i + 1; j < rings.size(); j++) {
rings.get(i).collide(rings.get(j));
}
}
redistributeSegments(rings);
List<Relation> relations = new ArrayList<>();
Map<Relation, Relation> relationCache = new HashMap<>();
for (TheRing r : rings) {
commands.addAll(r.getCommands(relationCache));
relations.add(r.getRelation());
}
updateCommandsWithRelations(commands, relationCache);
return relations;
}
public void collide(TheRing other) {
boolean collideNoted = false;
for (int i = 0; i < segments.size(); i++) {
RingSegment segment1 = segments.get(i);
if (!segment1.isReference()) {
for (int j = 0; j < other.segments.size(); j++) {
RingSegment segment2 = other.segments.get(j);
if (!segment2.isReference()) {
log("Comparing " + segment1 + " and " + segment2);
Node[] split = getSplitNodes(segment1.getNodes(), segment2.getNodes(), segment1.isRing(), segment2.isRing());
if (split != null) {
if (!collideNoted) {
log("Rings for ways " + source.getUniqueId() + " and " + other.source.getUniqueId() + " collide.");
collideNoted = true;
}
RingSegment segment = splitRingAt(i, split[0], split[1]);
RingSegment otherSegment = other.splitRingAt(j, split[2], split[3]);
if (!areSegmentsEqual(segment, otherSegment))
throw new IllegalArgumentException(
"Error: algorithm gave incorrect segments: " + segment + " and " + otherSegment);
segment.makeReference(otherSegment);
}
}
if (segment1.isReference()) {
break;
}
}
}
}
}
/**
* Returns array of {start1, last1, start2, last2} or null if there is no common nodes.
*/
public static Node[] getSplitNodes(List<Node> nodes1, List<Node> nodes2, boolean isRing1, boolean isRing2) {
int pos = 0;
while (pos < nodes1.size() && !nodes2.contains(nodes1.get(pos))) {
pos++;
}
boolean collideFound = pos == nodes1.size();
if (pos == 0 && isRing1) {
// rewind a bit
pos = nodes1.size() - 1;
while (pos > 0 && nodes2.contains(nodes1.get(pos))) {
pos--;
}
if (pos == 0 && nodes1.size() == nodes2.size()) {
JOptionPane.showMessageDialog(Main.parent,
tr("Two rings are equal, and this must not be."), tr("Multipolygon from rings"), JOptionPane.ERROR_MESSAGE);
return null;
}
pos = pos == nodes1.size() - 1 ? 0 : pos + 1;
}
int firstPos = isRing1 ? pos : nodes1.size();
while (!collideFound) {
log("pos=" + pos);
int start1 = pos;
int start2 = nodes2.indexOf(nodes1.get(start1));
int last1 = incrementBy(start1, 1, nodes1.size(), isRing1);
int last2 = start2;
int increment2 = 0;
if (last1 >= 0) {
last2 = incrementBy(start2, -1, nodes2.size(), isRing2);
if (last2 >= 0 && nodes1.get(last1).equals(nodes2.get(last2))) {
increment2 = -1;
} else {
last2 = incrementBy(start2, 1, nodes2.size(), isRing2);
if (last2 >= 0 && nodes1.get(last1).equals(nodes2.get(last2))) {
increment2 = 1;
}
}
}
log("last1=" + last1 + " last2=" + last2 + " increment2=" + increment2);
if (increment2 != 0) {
// find the first nodes
boolean reachedEnd = false;
while (!reachedEnd) {
int newLast1 = incrementBy(last1, 1, nodes1.size(), isRing1);
int newLast2 = incrementBy(last2, increment2, nodes2.size(), isRing2);
if (newLast1 < 0 || newLast2 < 0 || !nodes1.get(newLast1).equals(nodes2.get(newLast2))) {
reachedEnd = true;
} else {
last1 = newLast1;
last2 = newLast2;
}
}
log("last1=" + last1 + " last2=" + last2);
if (increment2 < 0) {
int tmp = start2;
start2 = last2;
last2 = tmp;
}
return new Node[] {nodes1.get(start1), nodes1.get(last1), nodes2.get(start2), nodes2.get(last2)};
} else {
pos = last1;
while (pos != firstPos && pos >= 0 && !nodes2.contains(nodes1.get(pos))) {
pos = incrementBy(pos, 1, nodes1.size(), isRing1);
}
if (pos < 0 || pos == firstPos || !nodes2.contains(nodes1.get(pos))) {
collideFound = true;
}
}
}
return null;
}
private static int incrementBy(int value, int increment, int limit1, boolean isRing) {
int result = value + increment;
if (result < 0)
return isRing ? result + limit1 : -1;
else if (result >= limit1)
return isRing ? result - limit1 : -1;
else
return result;
}
private boolean areSegmentsEqual(RingSegment seg1, RingSegment seg2) {
List<Node> nodes1 = seg1.getNodes();
List<Node> nodes2 = seg2.getNodes();
int size = nodes1.size();
if (size != nodes2.size())
return false;
boolean reverse = size > 1 && !nodes1.get(0).equals(nodes2.get(0));
for (int i = 0; i < size; i++) {
if (!nodes1.get(i).equals(nodes2.get(reverse ? size-1-i : i)))
return false;
}
return true;
}
/**
* Split the segment in this ring at those nodes.
* @return The segment between nodes.
*/
private RingSegment splitRingAt(int segmentIndex, Node n1, Node n2) {
if (n1.equals(n2))
throw new IllegalArgumentException("Both nodes are equal, id=" + n1.getUniqueId());
RingSegment segment = segments.get(segmentIndex);
boolean isRing = segment.isRing();
log("Split segment " + segment + " at nodes " + n1.getUniqueId() + " and " + n2.getUniqueId());
boolean reversed = segment.getNodes().indexOf(n2) < segment.getNodes().indexOf(n1);
if (reversed && !isRing) {
// order nodes
Node tmp = n1;
n1 = n2;
n2 = tmp;
}
RingSegment secondPart = isRing ? segment.split(n1, n2) : segment.split(n1);
// if secondPart == null, then n1 == firstNode
RingSegment thirdPart = isRing ? null : secondPart == null ? segment.split(n2) : secondPart.split(n2);
// if secondPart == null, then thirdPart is between n1 and n2
// otherwise, thirdPart is between n2 and lastNode
// if thirdPart == null, then n2 == lastNode
int pos = segmentIndex + 1;
if (secondPart != null) {
segments.add(pos++, secondPart);
}
if (thirdPart != null) {
segments.add(pos++, thirdPart);
}
return isRing || secondPart == null ? segment : secondPart;
}
/**
* Tries to arrange segments in order for each ring to have at least one.
* Also, sets source way for all rings.
*
* If this method is not called, do not forget to call {@link #putSourceWayFirst()} for all rings.
*/
public static void redistributeSegments(List<TheRing> rings) {
// build segments map
Map<RingSegment, TheRing> segmentMap = new HashMap<>();
for (TheRing ring : rings) {
for (RingSegment seg : ring.segments) {
if (!seg.isReference()) {
segmentMap.put(seg, ring);
}
}
}
// rearrange references
for (int i = 0; i < rings.size(); i++) {
TheRing ring = rings.get(i);
if (ring.countNonReferenceSegments() == 0) {
// need to find one non-reference segment
for (RingSegment seg : ring.segments) {
TheRing otherRing = segmentMap.get(seg.references);
if (otherRing.countNonReferenceSegments() > 1) {
// we could check for >0, but it is prone to deadlocking
seg.swapReference();
}
}
}
}
// initializing source way for each ring
for (TheRing ring : rings) {
ring.putSourceWayFirst();
}
}
private int countNonReferenceSegments() {
int count = 0;
for (RingSegment seg : segments) {
if (!seg.isReference()) {
count++;
}
}
return count;
}
public void putSourceWayFirst() {
for (RingSegment seg : segments) {
if (!seg.isReference()) {
seg.overrideWay(source);
return;
}
}
}
public List<Command> getCommands() {
return getCommands(true, null);
}
public List<Command> getCommands(Map<Relation, Relation> relationChangeMap) {
return getCommands(true, relationChangeMap);
}
/**
* Returns a list of commands to make a new relation and all newly created ways.
* The first way is copied from the source one, ChangeCommand is issued in this case.
*/
public List<Command> getCommands(boolean createMultipolygon, Map<Relation, Relation> relationChangeMap) {
Way sourceCopy = new Way(source);
if (createMultipolygon) {
Collection<String> linearTags = Main.pref.getCollection(PREF_MULTIPOLY + "lineartags", CreateMultipolygonAction.DEFAULT_LINEAR_TAGS);
relation = new Relation();
relation.put("type", "multipolygon");
for (String key : sourceCopy.keySet()) {
if (linearTags.contains(key)) {
continue;
}
if (key.equals("natural") && sourceCopy.get("natural").equals("coastline")) {
continue;
}
relation.put(key, sourceCopy.get(key));
sourceCopy.remove(key);
}
}
// build a map of referencing relations
Map<Relation, Integer> referencingRelations = new HashMap<>();
List<Command> relationCommands = new ArrayList<>();
for (OsmPrimitive p : source.getReferrers()) {
if (p instanceof Relation) {
Relation rel = null;
if (relationChangeMap != null) {
if (relationChangeMap.containsKey(p)) {
rel = relationChangeMap.get(p);
} else {
rel = new Relation((Relation) p);
relationChangeMap.put((Relation) p, rel);
}
} else {
rel = new Relation((Relation) p);
relationCommands.add(new ChangeCommand(p, rel));
}
for (int i = 0; i < rel.getMembersCount(); i++) {
if (rel.getMember(i).getMember().equals(source)) {
referencingRelations.put(rel, Integer.valueOf(i));
}
}
}
}
List<Command> commands = new ArrayList<>();
boolean foundOwnWay = false;
for (RingSegment seg : segments) {
boolean needAdding = !seg.isWayConstructed();
Way w = seg.constructWay(seg.isReference() ? null : sourceCopy);
if (needAdding) {
commands.add(new AddCommand(w));
}
if (w.equals(source)) {
if (createMultipolygon || !seg.getWayNodes().equals(source.getNodes())) {
sourceCopy.setNodes(seg.getWayNodes());
commands.add(new ChangeCommand(source, sourceCopy));
}
foundOwnWay = true;
} else {
for (Relation rel : referencingRelations.keySet()) {
int relIndex = referencingRelations.get(rel);
rel.addMember(new RelationMember(rel.getMember(relIndex).getRole(), w));
}
}
if (createMultipolygon) {
relation.addMember(new RelationMember("outer", w));
}
}
if (!foundOwnWay) {
commands.add(new DeleteCommand(source));
}
commands.addAll(relationCommands);
if (createMultipolygon) {
commands.add(new AddCommand(relation));
}
return commands;
}
public static void updateCommandsWithRelations(List<Command> commands, Map<Relation, Relation> relationCache) {
for (Relation src : relationCache.keySet()) {
commands.add(new ChangeCommand(src, relationCache.get(src)));
}
}
/**
* Returns the relation created in {@link #getCommands()}.
*/
public Relation getRelation() {
return relation;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("TheRing@");
sb.append(this.hashCode()).append('[').append("wayId: ").append(source == null ? "null" : source.getUniqueId()).append("; segments: ");
if (segments.isEmpty()) {
sb.append("empty");
} else {
sb.append(segments.get(0));
for (int i = 1; i < segments.size(); i++) {
sb.append(", ").append(segments.get(i));
}
}
return sb.append(']').toString();
}
private static void log(String s) {
Main.debug(s);
}
private static class RingSegment {
private List<Node> nodes;
private RingSegment references;
private Way resultingWay = null;
private boolean wasTemplateApplied = false;
private boolean isRing;
RingSegment(Way w) {
this(w.getNodes());
}
RingSegment(List<Node> nodes) {
this.nodes = nodes;
isRing = nodes.size() > 1 && nodes.get(0).equals(nodes.get(nodes.size() - 1));
if (isRing) {
nodes.remove(nodes.size() - 1);
}
references = null;
}
/**
* Splits this segment at node n. Retains nodes 0..n and moves
* nodes n..N to a separate segment that is returned.
* @param n node at which to split.
* @return new segment, {@code null} if splitting is unnecessary.
*/
public RingSegment split(Node n) {
if (nodes == null)
throw new IllegalArgumentException("Cannot split segment: it is a reference");
int pos = nodes.indexOf(n);
if (pos <= 0 || pos >= nodes.size() - 1)
return null;
List<Node> newNodes = new ArrayList<>(nodes.subList(pos, nodes.size()));
nodes.subList(pos + 1, nodes.size()).clear();
return new RingSegment(newNodes);
}
/**
* Split this segment as a way at two nodes. If one of them is null or at the end,
* split as an arc. Note: order of nodes is important.
* @return A new segment from n2 to n1.
*/
public RingSegment split(Node n1, Node n2) {
if (nodes == null)
throw new IllegalArgumentException("Cannot split segment: it is a reference");
if (!isRing) {
if (n1 == null || nodes.get(0).equals(n1) || nodes.get(nodes.size() - 1).equals(n1))
return split(n2);
if (n2 == null || nodes.get(0).equals(n2) || nodes.get(nodes.size() - 1).equals(n2))
return split(n1);
throw new IllegalArgumentException("Split for two nodes is called for not-ring: " + this);
}
int pos1 = nodes.indexOf(n1);
int pos2 = nodes.indexOf(n2);
if (pos1 == pos2)
return null;
List<Node> newNodes = new ArrayList<>();
if (pos2 > pos1) {
newNodes.addAll(nodes.subList(pos2, nodes.size()));
newNodes.addAll(nodes.subList(0, pos1 + 1));
if (pos2 + 1 < nodes.size()) {
nodes.subList(pos2 + 1, nodes.size()).clear();
}
if (pos1 > 0) {
nodes.subList(0, pos1).clear();
}
} else {
newNodes.addAll(nodes.subList(pos2, pos1 + 1));
nodes.addAll(new ArrayList<>(nodes.subList(0, pos2 + 1)));
nodes.subList(0, pos1).clear();
}
isRing = false;
return new RingSegment(newNodes);
}
public List<Node> getNodes() {
return nodes == null ? references.nodes : nodes;
}
public List<Node> getWayNodes() {
if (nodes == null)
throw new IllegalArgumentException("Won't give you wayNodes: it is a reference");
List<Node> wayNodes = new ArrayList<>(nodes);
if (isRing) {
wayNodes.add(wayNodes.get(0));
}
return wayNodes;
}
public boolean isReference() {
return nodes == null;
}
public boolean isRing() {
return isRing;
}
public void makeReference(RingSegment segment) {
log(this + " was made a reference to " + segment);
this.nodes = null;
this.references = segment;
}
public void swapReference() {
this.nodes = references.nodes;
references.nodes = null;
references.references = this;
this.references = null;
}
public boolean isWayConstructed() {
return isReference() ? references.isWayConstructed() : resultingWay != null;
}
public Way constructWay(Way template) {
if (isReference())
return references.constructWay(template);
if (resultingWay == null) {
resultingWay = new Way();
resultingWay.setNodes(getWayNodes());
}
if (template != null && !wasTemplateApplied) {
resultingWay.setKeys(template.getKeys());
wasTemplateApplied = true;
}
return resultingWay;
}
public void overrideWay(Way source) {
if (isReference()) {
references.overrideWay(source);
} else {
resultingWay = source;
wasTemplateApplied = true;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("RingSegment@");
sb.append(this.hashCode()).append('[');
if (isReference()) {
sb.append("references ").append(references.hashCode());
} else if (nodes.isEmpty()) {
sb.append("empty");
} else {
if (isRing) {
sb.append("ring:");
}
sb.append(nodes.get(0).getUniqueId());
for (int i = 1; i < nodes.size(); i++) {
sb.append(',').append(nodes.get(i).getUniqueId());
}
}
return sb.append(']').toString();
}
}
}