package org.osm2world.core.world.modules.common;
import static java.lang.Math.toRadians;
import static java.util.Arrays.asList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.osm2world.core.map_elevation.data.GroundState;
import org.osm2world.core.math.GeometryUtil;
import org.osm2world.core.math.InvalidGeometryException;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.world.creation.WorldModule;
import org.osm2world.core.world.data.WorldObject;
import org.osm2world.core.world.data.WorldObjectWithOutline;
/**
* offers some geometry-related utility functions for {@link WorldModule}s
*/
public final class WorldModuleGeometryUtil {
private WorldModuleGeometryUtil() { }
/**
* creates the vectors for a vertical triangle strip
* at a given elevation above a line of points
*/
public static final List<VectorXYZ> createVerticalTriangleStrip(
List<? extends VectorXYZ> baseLine, float stripLowerYBound, float stripUpperYBound) {
VectorXYZ[] result = new VectorXYZ[baseLine.size() * 2];
for (int i = 0; i < baseLine.size(); i++) {
VectorXYZ basePos = baseLine.get(i);
result[i*2] = new VectorXYZ(
basePos.getX(),
basePos.getY() + stripLowerYBound,
basePos.getZ());
result[i*2+1] = new VectorXYZ(
basePos.getX(),
basePos.getY() + stripUpperYBound,
basePos.getZ());
}
return asList(result);
}
/**
* creates a triangle strip between two outlines with identical number of vectors
*/
public static final List<VectorXYZ> createTriangleStripBetween(
List<VectorXYZ> leftOutline, List<VectorXYZ> rightOutline) {
assert leftOutline.size() == rightOutline.size();
VectorXYZ[] vs = new VectorXYZ[leftOutline.size() * 2];
for (int i = 0; i < leftOutline.size(); i++) {
vs[i*2] = leftOutline.get(i);
vs[i*2+1] = rightOutline.get(i);
}
return asList(vs);
}
/**
* @param ratio 0 is at left outline, 1 at right outline
*/
public static final List<VectorXYZ> createLineBetween(
List<VectorXYZ> leftOutline, List<VectorXYZ> rightOutline, float ratio) {
assert leftOutline.size() == rightOutline.size();
List<VectorXYZ> result = new ArrayList<VectorXYZ>(leftOutline.size());
for (int i = 0; i < leftOutline.size(); i++) {
result.add(GeometryUtil.interpolateBetween(
leftOutline.get(i), rightOutline.get(i), ratio));
}
return result;
}
//TODO: many uses of VisualizationUtil#drawColumn are a special case of this
/**
* creates triangle strip vectors for a shape extruded along a line of coordinates
*
* @param shape shape relative to origin
* @param extrusionPath nodes to extrude the shape along; needs at least 2 nodes
* @param upVectors vector for "up" direction at each extrusion path node.
* You can use {@link Collections#nCopies(int, Object)}
* if you want the same up vector for all nodes.
* @return list of triangle strip vertex lists
*/
public static final List<List<VectorXYZ>> createShapeExtrusionAlong(
List<VectorXYZ> shape,
List<VectorXYZ> extrusionPath,
List<VectorXYZ> upVectors) {
if (extrusionPath.size() < 2) {
throw new IllegalArgumentException("extrusion path needs at least 2 nodes");
} else if (extrusionPath.size() != upVectors.size()) {
throw new IllegalArgumentException("extrusionPath and upVectors must have same size");
}
@SuppressWarnings("unchecked")
List<VectorXYZ>[] shapeVectors = new List[extrusionPath.size()];
/*
* create shape at each node of the extrusion path.
* Special handling for first and last node,
* where calculation of "forward" vector is different.
*/
shapeVectors[0] = transformShape(shape,
extrusionPath.get(0),
extrusionPath.get(1).subtract(extrusionPath.get(0)).normalize(),
upVectors.get(0));
for (int pathI = 1; pathI < extrusionPath.size()-1; pathI ++) {
VectorXYZ forwardVector =
extrusionPath.get(pathI+1).subtract(extrusionPath.get(pathI-1));
forwardVector = forwardVector.normalize();
shapeVectors[pathI] = transformShape(shape,
extrusionPath.get(pathI),
forwardVector,
upVectors.get(pathI));
}
int last = extrusionPath.size()-1;
shapeVectors[last] = transformShape(shape,
extrusionPath.get(last),
extrusionPath.get(last).subtract(extrusionPath.get(last-1)).normalize(),
upVectors.get(last));
/* draw triangle strips */
List<List<VectorXYZ>> triangleStripList =
new ArrayList<List<VectorXYZ>>(shape.size()-1);
for (int i = 0; i+1 < shape.size(); i++) {
VectorXYZ[] triangleStripVectors = new VectorXYZ[2*shapeVectors.length];
for (int j=0; j < shapeVectors.length; j++) {
triangleStripVectors[j*2+1] = shapeVectors[j].get(i);
triangleStripVectors[j*2+0] = shapeVectors[j].get(i+1);
}
triangleStripList.add(asList(triangleStripVectors));
}
return triangleStripList;
}
/**
* creates an rotated version of a list of vectors
* by rotating them by the given angle around the parallel of the x axis
* defined by the given Y and Z coordinates
*
* @param angle rotation angle in degrees
*/
public static final List<VectorXYZ> rotateShapeX(List<VectorXYZ> shape,
double angle, double posY, double posZ) {
VectorXYZ[] result = new VectorXYZ[shape.size()];
for (int i = 0; i < shape.size(); ++i) {
result[i] = shape.get(i).add(0f, -posY, -posZ);
result[i] = result[i].rotateX(toRadians(angle));
result[i] = result[i].add(0f, posY, posZ);
}
return asList(result);
}
/**
* moves a shape that was defined at the origin to a new position.
* This is used by {@link #createShapeExtrusionAlong(List, List, List)}
*
* @param center new center coordinate
* @param forward new forward direction (unit vector)
* @param up new up direction (unit vector)
* @return list of 3d vectors; same length as shape
*/
public static final List<VectorXYZ> transformShape (List<VectorXYZ> shape,
VectorXYZ center, VectorXYZ forward, VectorXYZ up) {
VectorXYZ[] result = new VectorXYZ[shape.size()];
VectorXYZ right = forward.cross(up);
final double[][] m = { //rotation matrix
{right.x, right.y, right.z},
{up.x, up.y, up.z},
{forward.x, forward.y, forward.z}
};
for (int i = 0; i < shape.size(); i++) {
VectorXYZ v = shape.get(i);
v = new VectorXYZ(
m[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z,
m[0][1] * v.x + m[1][1] * v.y + m[2][1] * v.z,
m[0][2] * v.x + m[1][2] * v.y + m[2][2] * v.z
);
v = v.add(center);
result[i] = v;
}
return asList(result);
}
/**
* removes positions from a collection if they are on the area covered by a
* {@link WorldObjectWithOutline} from a collection of {@link WorldObject}s.
*
* This can be used to avoid placing trees, bridge pillars
* and other randomly distributed features on roads, rails
* or other similar places where they don't belong.
*/
public static final void filterWorldObjectCollisions(
Collection<VectorXZ> positions,
Collection<WorldObject> worldObjects) {
//TODO: add support for avoiding a radius around the position, too.
//this is easily possible once "inflating"/"shrinking" polygons is supported [would also be useful for water bodies etc.]
/*
* prepare filter polygons.
* It improves performance to construct the outline polygons only once
* instead of doing this within the loop iterating over positions.
*/
List<SimplePolygonXZ> filterPolygons = new ArrayList<SimplePolygonXZ>();
for (WorldObject worldObject : worldObjects) {
if (worldObject.getGroundState() == GroundState.ON
&& (worldObject instanceof WorldObjectWithOutline)) {
SimplePolygonXZ outline = null;
try {
outline = ((WorldObjectWithOutline)worldObject).
getOutlinePolygonXZ();
} catch (InvalidGeometryException e) {
//ignore this outline
}
if (outline != null) {
filterPolygons.add(outline);
}
}
}
/* perform filtering of positions */
Iterator<VectorXZ> positionIterator = positions.iterator();
while (positionIterator.hasNext()) {
VectorXZ pos = positionIterator.next();
for (SimplePolygonXZ filterPolygon : filterPolygons) {
if (filterPolygon.contains(pos)) {
positionIterator.remove();
break;
}
}
}
}
}