// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnlanes.model;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.AbstractPrimitive;
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.plugins.turnlanes.CollectionUtils;
import org.openstreetmap.josm.plugins.turnlanes.model.Route.Segment;
import org.openstreetmap.josm.tools.Pair;
public class Road {
public final class End {
private final boolean from;
private final Junction junction;
private final Relation lengthsLeft;
private final Relation lengthsRight;
private final double extraLengthLeft;
private final double extraLengthRight;
private final List<Lane> lanes;
private Set<Turn> turns;
private End(boolean from, Junction junction, Relation lengthsLeft, Relation lengthsRight) {
this.from = from;
this.junction = junction;
this.lengthsLeft = lengthsLeft;
this.lengthsRight = lengthsRight;
this.extraLengthLeft = lengthsLeft == null ? 0 : Route.load(lengthsLeft).getLengthFrom(getWay());
this.extraLengthRight = lengthsRight == null ? 0 : Route.load(lengthsRight).getLengthFrom(getWay());
this.lanes = Lane.load(this);
junction.addRoad(getWay());
}
private End(boolean from, Junction junction) {
this.from = from;
this.junction = junction;
this.lengthsLeft = null;
this.lengthsRight = null;
this.extraLengthLeft = 0;
this.extraLengthRight = 0;
this.lanes = Lane.load(this);
junction.addRoad(getWay());
}
public Road getRoad() {
return Road.this;
}
public Way getWay() {
return isFromEnd() ? getRoute().getFirstSegment().getWay() : getRoute().getLastSegment().getWay();
}
public Junction getJunction() {
return junction;
}
public boolean isFromEnd() {
return from;
}
public boolean isToEnd() {
return !isFromEnd();
}
public End getOppositeEnd() {
return isFromEnd() ? toEnd : fromEnd;
}
/**
* @return the turns <em>onto</em> this road at this end
*/
public Set<Turn> getTurns() {
return turns;
}
public void addLane(Lane.Kind kind) {
if (kind == Lane.Kind.REGULAR) {
throw new IllegalArgumentException("Only extra lanes can be added.");
}
double length = Double.POSITIVE_INFINITY;
for (Lane l : lanes) {
if (l.getKind() == kind) {
length = Math.max(0, Math.min(length, l.getLength() - 1));
}
}
if (Double.isInfinite(length)) {
length = Math.min(20, 3 * getLength() / 4);
}
addLane(kind, length);
}
private void addLane(Lane.Kind kind, double length) {
assert kind == Lane.Kind.EXTRA_LEFT || kind == Lane.Kind.EXTRA_RIGHT;
final GenericCommand cmd = new GenericCommand(getJunction().getNode().getDataSet(), "Add lane");
final boolean left = kind == Lane.Kind.EXTRA_LEFT;
final Relation rel = left ? lengthsLeft : lengthsRight;
final Relation other = left ? lengthsRight : lengthsLeft;
final Node n = getJunction().getNode();
final String lengthStr = toLengthString(length);
final Relation target;
if (rel == null) {
if (other == null || !Utils.getMemberNode(other, "end").equals(n)) {
target = createLengthsRelation();
} else {
target = other;
}
} else {
target = rel;
}
final String key = left ? Constants.LENGTHS_KEY_LENGTHS_LEFT : Constants.LENGTHS_KEY_LENGTHS_RIGHT;
final String old = target.get(key);
if (old == null) {
cmd.backup(target).put(key, lengthStr);
} else {
cmd.backup(target).put(key, old + Constants.SEPARATOR + lengthStr);
}
Main.main.undoRedo.add(cmd);
}
private Relation createLengthsRelation() {
final Node n = getJunction().getNode();
final Relation r = new Relation();
r.put("type", Constants.TYPE_LENGTHS);
r.addMember(new RelationMember(Constants.LENGTHS_ROLE_END, n));
for (Route.Segment s : isFromEnd() ? route.getSegments() : CollectionUtils.reverse(route.getSegments())) {
r.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, s.getWay()));
}
n.getDataSet().addPrimitive(r);
return r;
}
void updateLengths() {
final GenericCommand cmd = new GenericCommand(getJunction().getNode().getDataSet(), "Change lane length");
for (final boolean left : Arrays.asList(true, false)) {
final Lane.Kind kind = left ? Lane.Kind.EXTRA_LEFT : Lane.Kind.EXTRA_RIGHT;
final Relation r = left ? lengthsLeft : lengthsRight;
final double extra = left ? extraLengthLeft : extraLengthRight;
if (r == null) {
continue;
}
final StringBuilder lengths = new StringBuilder(32);
for (Lane l : left ? CollectionUtils.reverse(lanes) : lanes) {
if (l.getKind() == kind) {
lengths.append(toLengthString(extra + l.getLength())).append(Constants.SEPARATOR);
}
}
lengths.setLength(lengths.length() - Constants.SEPARATOR.length());
cmd.backup(r).put(left ? Constants.LENGTHS_KEY_LENGTHS_LEFT : Constants.LENGTHS_KEY_LENGTHS_RIGHT,
lengths.toString());
}
Main.main.undoRedo.add(cmd);
}
public List<Lane> getLanes() {
return lanes;
}
public Lane getLane(Lane.Kind kind, int index) {
for (Lane l : lanes) {
if (l.getKind() == kind && l.getIndex() == index) {
return l;
}
}
throw new IllegalArgumentException("No such lane.");
}
public Lane getExtraLane(int index) {
return index < 0 ? getLane(Lane.Kind.EXTRA_LEFT, index) : getLane(Lane.Kind.EXTRA_RIGHT, index);
}
public boolean isExtendable() {
final End o = getOppositeEnd();
return (lengthsLeft == null && lengthsRight == null) && (o.lengthsLeft != null || o.lengthsRight != null);
}
public void extend(Way way) {
if (!isExtendable()) {
throw new IllegalStateException();
}
final End o = getOppositeEnd();
if (o.lengthsLeft != null) {
o.lengthsLeft.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, way));
}
if (o.lengthsRight != null) {
o.lengthsRight.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, way));
}
}
public List<Double> getLengths(Lane.Kind kind) {
switch (kind) {
case EXTRA_LEFT:
return Lane.loadLengths(lengthsLeft, Constants.LENGTHS_KEY_LENGTHS_LEFT, extraLengthLeft);
case EXTRA_RIGHT:
return Lane.loadLengths(lengthsRight, Constants.LENGTHS_KEY_LENGTHS_RIGHT, extraLengthRight);
default:
throw new IllegalArgumentException(String.valueOf(kind));
}
}
void removeLane(GenericCommand cmd, Lane lane) {
assert lane.getKind() == Lane.Kind.EXTRA_LEFT || lane.getKind() == Lane.Kind.EXTRA_RIGHT;
final boolean left = lane.getKind() == Lane.Kind.EXTRA_LEFT;
final Relation rel = left ? lengthsLeft : lengthsRight;
for (Turn t : Turn.load(getContainer(), Constants.TURN_ROLE_FROM, getWay())) {
t.fixReferences(cmd, left, lane.getIndex());
}
final double extraLength = left ? extraLengthLeft : extraLengthRight;
final List<Double> newLengths = new ArrayList<>();
int i = Math.abs(lane.getIndex());
final String key = left ? Constants.LENGTHS_KEY_LENGTHS_LEFT : Constants.LENGTHS_KEY_LENGTHS_RIGHT;
for (double l : Lane.loadLengths(rel, key, 0)) {
if (l < extraLength) {
newLengths.add(l);
} else if (--i != 0) {
newLengths.add(l);
}
}
final AbstractPrimitive bRel = cmd.backup(rel);
bRel.put(key, join(newLengths));
if (bRel.get(Constants.LENGTHS_KEY_LENGTHS_LEFT) == null
&& bRel.get(Constants.LENGTHS_KEY_LENGTHS_RIGHT) == null) {
bRel.setDeleted(true);
}
}
void initialize() {
this.turns = Collections.unmodifiableSet(Turn.load(getContainer(), Constants.TURN_ROLE_TO, getWay()));
for (Lane l : lanes) {
l.initialize();
}
}
}
private static Pair<Relation, Relation> getLengthRelations(Way w, Node n) {
final List<Relation> left = new ArrayList<>();
final List<Relation> right = new ArrayList<>();
for (OsmPrimitive p : w.getReferrers()) {
if (p.getType() != OsmPrimitiveType.RELATION) {
continue;
}
Relation r = (Relation) p;
if (Constants.TYPE_LENGTHS.equals(r.get("type")) && isRightDirection(r, w, n)) {
if (r.get(Constants.LENGTHS_KEY_LENGTHS_LEFT) != null) {
left.add(r);
}
if (r.get(Constants.LENGTHS_KEY_LENGTHS_RIGHT) != null) {
right.add(r);
}
}
}
if (left.size() > 1) {
throw new IllegalArgumentException("Way is in " + left.size()
+ " lengths relations for given direction, both specifying left lane lengths.");
}
if (right.size() > 1) {
throw new IllegalArgumentException("Way is in " + right.size()
+ " lengths relations for given direction, both specifying right lane lengths.");
}
return new Pair<>(
left.isEmpty() ? null : left.get(0),
right.isEmpty() ? null : right.get(0)
);
}
/**
* @param r
* lengths relation
* @param w
* the way to check for
* @param n
* first or last node of w, determines the direction
* @return whether the turn lane goes into the direction of n
*/
private static boolean isRightDirection(Relation r, Way w, Node n) {
for (Segment s : Route.load(r).getSegments()) {
if (w.equals(s.getWay())) {
return n.equals(s.getEnd());
}
}
return false;
}
private final ModelContainer container;
private final Route route;
private final End fromEnd;
private final End toEnd;
Road(ModelContainer container, Way w, Junction j) {
final Node n = j.getNode();
if (!w.isFirstLastNode(n)) {
throw new IllegalArgumentException("Way must start or end in given node.");
}
final Pair<Relation, Relation> lengthsRelations = getLengthRelations(w, n);
this.container = container;
this.route = lengthsRelations.a == null && lengthsRelations.b == null ? Route.create(Arrays.asList(w), n)
: Route.load(lengthsRelations.a, lengthsRelations.b, w);
this.fromEnd = new End(true, container.getOrCreateJunction(route.getFirstSegment().getStart()));
this.toEnd = new End(false, j, lengthsRelations.a, lengthsRelations.b);
}
Road(ModelContainer container, Route route) {
this.container = container;
this.route = route;
this.fromEnd = new End(true, container.getJunction(route.getStart()));
this.toEnd = new End(false, container.getJunction(route.getEnd()));
}
public End getFromEnd() {
return fromEnd;
}
public End getToEnd() {
return toEnd;
}
public Route getRoute() {
return route;
}
public double getLength() {
return route.getLength();
}
private static String join(List<Double> list) {
if (list.isEmpty()) {
return null;
}
final StringBuilder builder = new StringBuilder(list.size() * (4 + Constants.SEPARATOR.length()));
for (double e : list) {
builder.append(toLengthString(e)).append(Constants.SEPARATOR);
}
builder.setLength(builder.length() - Constants.SEPARATOR.length());
return builder.toString();
}
private static String toLengthString(double length) {
final DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance();
dfs.setDecimalSeparator('.');
final DecimalFormat nf = new DecimalFormat("0.0", dfs);
nf.setRoundingMode(RoundingMode.HALF_UP);
return nf.format(length);
}
public ModelContainer getContainer() {
return container;
}
public boolean isPrimary() {
return getContainer().isPrimary(this);
}
void initialize() {
fromEnd.initialize();
toEnd.initialize();
}
}