package org.osm2world.core.world.network;
import static java.lang.Double.*;
import static java.util.Collections.emptyList;
import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseIncline;
import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*;
import static org.osm2world.core.map_elevation.data.GroundState.*;
import static org.osm2world.core.math.VectorXZ.distanceSquared;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.osm2world.core.map_data.data.MapNode;
import org.osm2world.core.map_data.data.MapWaySegment;
import org.osm2world.core.map_data.data.overlaps.MapIntersectionWW;
import org.osm2world.core.map_data.data.overlaps.MapOverlap;
import org.osm2world.core.map_data.data.overlaps.MapOverlapType;
import org.osm2world.core.map_data.data.overlaps.MapOverlapWA;
import org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;
import org.osm2world.core.map_elevation.data.EleConnector;
import org.osm2world.core.map_elevation.data.EleConnectorGroup;
import org.osm2world.core.map_elevation.data.GroundState;
import org.osm2world.core.map_elevation.data.WaySegmentElevationProfile;
import org.osm2world.core.math.AxisAlignedBoundingBoxXZ;
import org.osm2world.core.math.GeometryUtil;
import org.osm2world.core.math.InvalidGeometryException;
import org.osm2world.core.math.PolygonXYZ;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.math.datastructures.IntersectionTestObject;
import org.osm2world.core.world.data.AbstractAreaWorldObject;
import org.osm2world.core.world.data.WaySegmentWorldObject;
import org.osm2world.core.world.data.WorldObject;
import org.osm2world.core.world.data.WorldObjectWithOutline;
import org.osm2world.core.world.modules.BridgeModule;
import org.osm2world.core.world.modules.TunnelModule;
public abstract class AbstractNetworkWaySegmentWorldObject
implements NetworkWaySegmentWorldObject, WaySegmentWorldObject,
IntersectionTestObject, WorldObjectWithOutline {
public final MapWaySegment segment;
private VectorXZ startCutVector = null;
private VectorXZ endCutVector = null;
private VectorXZ startOffset = VectorXZ.NULL_VECTOR;
private VectorXZ endOffset = VectorXZ.NULL_VECTOR;
protected EleConnectorGroup connectors;
private List<VectorXZ> centerlineXZ = null;
private List<VectorXZ> leftOutlineXZ = null;
private List<VectorXZ> rightOutlineXZ = null;
private SimplePolygonXZ outlinePolygonXZ = null;
private Boolean broken = null;
protected AbstractNetworkWaySegmentWorldObject(MapWaySegment segment) {
this.segment = segment;
}
@Override
public final MapWaySegment getPrimaryMapElement() {
return segment;
}
@Override
public void setStartCutVector(VectorXZ cutVector) {
this.startCutVector = cutVector;
}
@Override
public void setEndCutVector(VectorXZ cutVector) {
this.endCutVector = cutVector;
}
@Override
public VectorXZ getStartCutVector() {
return startCutVector;
}
@Override
public VectorXZ getEndCutVector() {
return endCutVector;
}
public VectorXZ getCutVectorAt(MapNode node) {
if (node == segment.getStartNode()) {
return getStartCutVector();
} else if (node == segment.getEndNode()) {
return getEndCutVector();
} else {
throw new IllegalArgumentException("node is not part of the line");
}
}
@Override
public void setStartOffset(VectorXZ offsetVector) {
this.startOffset = offsetVector;
}
@Override
public void setEndOffset(VectorXZ offsetVector) {
this.endOffset = offsetVector;
}
protected VectorXZ getStartWithOffset() {
return segment.getStartNode().getPos().add(startOffset); //SUGGEST (performance): cache? [also getEnd*]
}
protected VectorXZ getEndWithOffset() {
return segment.getEndNode().getPos().add(endOffset);
}
/**
* calculates centerline and outlines, along with their connectors
* TODO: perform before construction, to simplify the object and avoid {@link #broken}
*/
private void calculateXZGeometry() {
if (startCutVector == null || endCutVector == null) {
throw new IllegalStateException("cannot calculate outlines before cut vectors");
}
connectors = new EleConnectorGroup();
{ /* calculate centerline */
centerlineXZ = new ArrayList<VectorXZ>();
final VectorXZ start = getStartWithOffset();
final VectorXZ end = getEndWithOffset();
centerlineXZ.add(start);
connectors.add(new EleConnector(start,
segment.getStartNode(), getGroundState(segment.getStartNode())));
// add intersections along the centerline
for (MapOverlap<?,?> overlap : segment.getOverlaps()) {
if (overlap.getOther(segment).getPrimaryRepresentation() == null)
continue;
if (overlap instanceof MapIntersectionWW) {
MapIntersectionWW intersection = (MapIntersectionWW) overlap;
if (GeometryUtil.isBetween(intersection.pos, start, end)) {
centerlineXZ.add(intersection.pos);
connectors.add(new EleConnector(intersection.pos,
null, getGroundState()));
}
} else if (overlap instanceof MapOverlapWA
&& overlap.type == MapOverlapType.INTERSECT) {
if (!(overlap.getOther(segment).getPrimaryRepresentation()
instanceof AbstractAreaWorldObject)) continue;
MapOverlapWA overlapWA = (MapOverlapWA) overlap;
for (int i = 0; i < overlapWA.getIntersectionPositions().size(); i++) {
VectorXZ pos = overlapWA.getIntersectionPositions().get(i);
if (GeometryUtil.isBetween(pos, start, end)) {
centerlineXZ.add(pos);
connectors.add(new EleConnector(pos,
null, getGroundState()));
}
}
}
}
// finish the centerline
centerlineXZ.add(end);
connectors.add(new EleConnector(end,
segment.getEndNode(), getGroundState(segment.getEndNode())));
if (centerlineXZ.size() > 3) {
// sort by distance from start
Collections.sort(centerlineXZ, new Comparator<VectorXZ>() {
@Override
public int compare(VectorXZ v1, VectorXZ v2) {
return Double.compare(
distanceSquared(v1, start),
distanceSquared(v2, start));
}
});
}
}
{ /* calculate left and right outlines */
leftOutlineXZ = new ArrayList<VectorXZ>(centerlineXZ.size());
rightOutlineXZ = new ArrayList<VectorXZ>(centerlineXZ.size());
assert centerlineXZ.size() >= 2;
double halfWidth = getWidth() * 0.5f;
VectorXZ centerStart = centerlineXZ.get(0);
leftOutlineXZ.add(centerStart.add(startCutVector.mult(-halfWidth)));
rightOutlineXZ.add(centerStart.add(startCutVector.mult(halfWidth)));
connectors.add(new EleConnector(leftOutlineXZ.get(0),
segment.getStartNode(), getGroundState(segment.getStartNode())));
connectors.add(new EleConnector(rightOutlineXZ.get(0),
segment.getStartNode(), getGroundState(segment.getStartNode())));
for (int i = 1; i < centerlineXZ.size() - 1; i++) {
leftOutlineXZ.add(centerlineXZ.get(i).add(segment.getRightNormal().mult(-halfWidth)));
rightOutlineXZ.add(centerlineXZ.get(i).add(segment.getRightNormal().mult(halfWidth)));
connectors.add(new EleConnector(leftOutlineXZ.get(i),
null, getGroundState()));
connectors.add(new EleConnector(rightOutlineXZ.get(i),
null, getGroundState()));
}
VectorXZ centerEnd = centerlineXZ.get(centerlineXZ.size() - 1);
leftOutlineXZ.add(centerEnd.add(endCutVector.mult(-halfWidth)));
rightOutlineXZ.add(centerEnd.add(endCutVector.mult(halfWidth)));
connectors.add(new EleConnector(leftOutlineXZ.get(leftOutlineXZ.size() - 1),
segment.getEndNode(), getGroundState(segment.getEndNode())));
connectors.add(new EleConnector(rightOutlineXZ.get(rightOutlineXZ.size() - 1),
segment.getEndNode(), getGroundState(segment.getEndNode())));
}
{ /* calculate the outline loop */
List<VectorXZ> outlineLoopXZ =
new ArrayList<VectorXZ>(centerlineXZ.size() * 2 + 1);
outlineLoopXZ.addAll(rightOutlineXZ);
List<VectorXZ> left = new ArrayList<VectorXZ>(leftOutlineXZ);
Collections.reverse(left);
outlineLoopXZ.addAll(left);
outlineLoopXZ.add(outlineLoopXZ.get(0));
// check for brokenness
try {
outlinePolygonXZ = new SimplePolygonXZ(outlineLoopXZ);
broken = outlinePolygonXZ.isClockwise();
} catch (InvalidGeometryException e) {
broken = true;
connectors = EleConnectorGroup.EMPTY;
}
}
}
/**
* determines whether the node is connected to the terrain based on the
* segments connected to it
*
* @param node one of the nodes of {@link #segment}
*/
private GroundState getGroundState(MapNode node) {
WorldObject primaryWO = node.getPrimaryRepresentation();
if (primaryWO != null) {
return primaryWO.getGroundState();
} else if (this.getGroundState() == ON) {
return ON;
} else {
boolean allAbove = true;
boolean allBelow = true;
for (MapWaySegment segment : node.getConnectedWaySegments()) {
if (segment.getPrimaryRepresentation() != null) {
switch (segment.getPrimaryRepresentation().getGroundState()) {
case ABOVE: allBelow = false; break;
case BELOW: allAbove = false; break;
case ON: return ON;
}
}
}
if (allAbove) {
return ABOVE;
} else if (allBelow) {
return BELOW;
} else {
return ON;
}
}
}
/**
* implementation of {@link WorldObject#getGroundState()}.
* This version checks for bridge and tunnel tags to make the decision.
* If that is not desired, subclasses may override the method.
*/
@Override
public GroundState getGroundState() {
if (BridgeModule.isBridge(segment.getTags())) {
return GroundState.ABOVE;
} else if (TunnelModule.isTunnel(segment.getTags())) {
return GroundState.BELOW;
} else {
return GroundState.ON;
}
}
@Override
public EleConnectorGroup getEleConnectors() {
if (connectors == null) {
calculateXZGeometry();
}
return connectors;
}
@Override
public void defineEleConstraints(EleConstraintEnforcer enforcer) {
if (isBroken()) return;
//TODO: maybe save connectors separately right away
List<EleConnector> center = getCenterlineEleConnectors();
List<EleConnector> left = connectors.getConnectors(getOutlineXZ(false));
List<EleConnector> right = connectors.getConnectors(getOutlineXZ(true));
/* left and right connectors have the same ele as their center conn. */
for (int i = 0; i < center.size(); i++) {
enforcer.requireSameEle(center.get(i), left.get(i));
enforcer.requireSameEle(center.get(i), right.get(i));
}
/* incline should be honored */
String inclineValue = segment.getTags().getValue("incline");
if (inclineValue != null) {
double minIncline = NaN;
double maxIncline = NaN;
if ("up".equals(inclineValue)) {
minIncline = 0.1;
} else if ("down".equals(inclineValue)) {
maxIncline = -0.1;
} else {
Float incline = parseIncline(inclineValue);
if (incline != null) {
if (incline > 0) {
minIncline = incline / 100.0 * 0.5;
maxIncline = incline / 100.0 * 1.1;
} else if (incline < 0) {
maxIncline = incline / 100.0 * 0.5;
minIncline = incline / 100.0 * 1.1;
} else {
enforcer.requireSameEle(center);
}
}
}
if (!isNaN(minIncline)) {
enforcer.requireIncline(MIN, minIncline, center);
}
if (!isNaN(maxIncline)) {
enforcer.requireIncline(MAX, maxIncline, center);
}
}
//TODO sensible maximum incline for road and rail; and waterway down-incline
// ... take incline differences, steps etc. into account => move into Road, Rail separately
/* ensure a smooth transition from previous segment */
//TODO this might be more elegant with an "Invisible Connector WO"
List<MapWaySegment> connectedSegments =
segment.getStartNode().getConnectedWaySegments();
if (connectedSegments.size() == 2) {
MapWaySegment previousSegment = null;
for (MapWaySegment connectedSegment : connectedSegments) {
if (connectedSegment != this.segment) {
previousSegment = connectedSegment;
}
}
WorldObject previousWO = previousSegment.getPrimaryRepresentation();
if (previousWO instanceof AbstractNetworkWaySegmentWorldObject) {
AbstractNetworkWaySegmentWorldObject previous =
(AbstractNetworkWaySegmentWorldObject)previousWO;
if (!previous.isBroken()) {
List<EleConnector> previousCenter =
previous.getCenterlineEleConnectors();
enforcer.requireSmoothness(
previousCenter.get(previousCenter.size() - 2),
center.get(0),
center.get(1));
}
}
}
/* ensure smooth transitions within the way itself */
for (int i = 0; i + 2 < center.size(); i++) {
enforcer.requireSmoothness(
center.get(i),
center.get(i+1),
center.get(i+2));
}
}
protected List<EleConnector> getCenterlineEleConnectors() {
if (isBroken()) return emptyList();
return connectors.getConnectors(getCenterlineXZ());
}
/**
* returns a sequence of node running along the center of the
* line from start to end (each with offset).
* Uses the {@link WaySegmentElevationProfile} for adding
* elevation information.
*/
public List<VectorXZ> getCenterlineXZ() {
if (centerlineXZ == null) {
calculateXZGeometry();
}
return centerlineXZ;
}
/**
* 3d version of {@link #getCenterlineXZ()}.
* Only available after elevation calculation.
*/
public List<VectorXYZ> getCenterline() {
return connectors.getPosXYZ(getCenterlineXZ());
}
/**
* Variant of {@link #getOutline(boolean)}.
* This one is already available before elevation calculation.
*/
public List<VectorXZ> getOutlineXZ(boolean right) {
if (right) {
if (rightOutlineXZ == null) {
calculateXZGeometry();
}
return rightOutlineXZ;
} else { //left
if (leftOutlineXZ == null) {
calculateXZGeometry();
}
return leftOutlineXZ;
}
}
/**
* provides the left or right border (a line at an appropriate distance
* from the center line), taking into account cut vectors, offsets and
* elevation information.
* Available after cut vectors, offsets and elevation information
* have been calculated.
*
* Left and right border have the same number of nodes as the elevation
* profile's {@link WaySegmentElevationProfile#getPointsWithEle()}.
* //TODO: compatible with future offset/clearing influences?
*/
public List<VectorXYZ> getOutline(boolean right) {
return connectors.getPosXYZ(getOutlineXZ(right));
}
@Override
public SimplePolygonXZ getOutlinePolygonXZ() {
if (outlinePolygonXZ == null) {
calculateXZGeometry();
}
if (isBroken()) {
return null;
} else {
return outlinePolygonXZ;
}
}
@Override
public PolygonXYZ getOutlinePolygon() {
if (isBroken()) {
return null;
} else {
return connectors.getPosXYZ(outlinePolygonXZ);
}
}
/**
* checks whether this segment has a broken outline.
* That can happen e.g. if it lies between two junctions that are too close
* together.
*/
public boolean isBroken() {
if (broken == null) {
calculateXZGeometry();
}
//TODO filter out broken objects during creation in the world module
return broken;
}
/**
* returns a point on the start or end cut line.
*
* @param start point is on the start cut if true, on the end cut if false
* @param relativePosFromLeft 0 is the leftmost point, 1 the rightmost.
* Values in between are for interpolation.
*/
public VectorXYZ getPointOnCut(boolean start, double relativePosFromLeft) {
assert 0 <= relativePosFromLeft && relativePosFromLeft <= 1;
VectorXZ position = start ? getStartWithOffset() : getEndWithOffset();
VectorXZ cutVector = start ? getStartCutVector() : getEndCutVector();
return connectors.getPosXYZ(position.add(cutVector.mult(
(-0.5 + relativePosFromLeft) * getWidth())));
}
@Override
public VectorXZ getStartOffset() {
return startOffset;
}
@Override
public VectorXZ getEndOffset() {
return endOffset;
}
@Override
public VectorXZ getStartPosition() {
return segment.getStartNode().getPos().add(getStartOffset());
}
@Override
public VectorXZ getEndPosition() {
return segment.getEndNode().getPos().add(getEndOffset());
}
@Override
public AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {
if (isBroken() || getOutlinePolygonXZ() == null) {
return null;
} else {
return new AxisAlignedBoundingBoxXZ(
getOutlinePolygonXZ().getVertexCollection());
}
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "(" + segment + ")";
}
}