package org.osm2world.core.target.common;
import static java.lang.Math.abs;
import static java.util.Collections.nCopies;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.osm2world.core.math.TriangleXYZ;
import org.osm2world.core.math.TriangleXYZWithNormals;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.target.Renderable;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.world.data.WorldObject;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
/**
* a target that relies on faces to represent geometry.
* The faces used by this target are polygons
* with three or more coplanar vertices.
*
* TODO: this currently produces faces that are not convex
*/
public abstract class FaceTarget<R extends Renderable>
extends AbstractTarget<R> {
abstract public void drawFace(Material material, List<VectorXYZ> vs,
List<VectorXYZ> normals, List<List<VectorXZ>> texCoordLists);
/**
* decides whether faces should be reconstructed from triangulations
* and other primitives.
*/
abstract public boolean reconstructFaces();
/**
* prevents triangles from before the call to be connected with triangles
* after this call when faces are reconstructed.
*
* This is automatically done at the beginning of each new object.
* It only has any effect if {@link #reconstructFaces()} is enabled.
*
* Calling this method at appropriate times can also help to speed up
* performance by lowering the number of candidates for merging.
*/
public void flushReconstructedFaces() {
drawAndClearCurrentFaces();
}
/**
* mutable representation of a face
*/
protected final static class Face {
public final List<VectorXYZ> vs;
public final List<List<VectorXZ>> texCoordLists;
public final VectorXYZ normal;
public Face(List<VectorXYZ> vs,
List<List<VectorXZ>> texCoordLists, VectorXYZ normal) {
this.vs = vs;
this.texCoordLists = texCoordLists;
this.normal = normal;
}
/**
* @return true if the triangle has been successfully inserted
*/
public boolean tryInsert(IsolatedTriangle t) {
for (int i = 0; i < vs.size(); i++) {
int j = (i+1) % vs.size();
int k = (i+2) % vs.size();
if (vs.get(i).equals(t.triangle.v3)
&& vs.get(j).equals(t.triangle.v2)) { /* TODO tex coords equal */
if (vs.get(k).equals(t.triangle.v1)) {
removeVertex(j);
} else {
insertVertex(j, t.triangle.v1,
texCoordLists, t.texCoordOffset + 0);
}
return true;
}
if (vs.get(i).equals(t.triangle.v1)
&& vs.get(j).equals(t.triangle.v3)) { /* TODO tex coords equal */
if (vs.get(k).equals(t.triangle.v2)) {
removeVertex(j);
} else {
insertVertex(j, t.triangle.v2,
texCoordLists, t.texCoordOffset + 1);
}
return true;
}
if (vs.get(i).equals(t.triangle.v2)
&& vs.get(j).equals(t.triangle.v1)) { /* TODO tex coords equal */
if (vs.get(k).equals(t.triangle.v3)) {
removeVertex(j);
} else {
insertVertex(j, t.triangle.v3,
texCoordLists, t.texCoordOffset + 2);
}
return true;
}
}
return false;
}
public void removeDuplicateEdges() {
boolean repeat = true;
while (repeat) {
repeat = false;
assert vs.size() >= 3;
for (int i = 0; i < vs.size(); i++) {
int j = (i+1) % vs.size();
int k = (i+2) % vs.size();
//TODO: what about tex coords?
if (vs.get(i).equals(vs.get(k))) {
if (k > j) {
removeVertex(k);
removeVertex(j);
} else {
removeVertex(j);
removeVertex(k);
}
repeat = true;
break;
}
}
}
}
private void insertVertex(int i, VectorXYZ vertex,
List<List<VectorXZ>> insTexCoordLists, int texCoordPos) {
this.vs.add(i, vertex);
for (int list = 0; list < texCoordLists.size(); list++) {
this.texCoordLists.get(list).add(i,
insTexCoordLists.get(list).get(texCoordPos));
}
}
private void removeVertex(int i) {
this.vs.remove(i);
for (int list = 0; list < texCoordLists.size(); list++) {
this.texCoordLists.get(list).remove(i);
}
}
@Override
public String toString() {
return vs.toString();
}
}
protected final static class IsolatedTriangle {
public final TriangleXYZ triangle;
public final VectorXYZ normal;
public final int texCoordOffset;
public final List<List<VectorXZ>> texCoordLists;
public IsolatedTriangle(TriangleXYZ triangle, VectorXYZ normal,
int texCoordOffset, List<List<VectorXZ>> texCoordLists) {
this.triangle = triangle;
this.normal = normal;
this.texCoordOffset = texCoordOffset;
this.texCoordLists = texCoordLists;
}
@Override
public String toString() {
return triangle.toString();
}
}
private final Multimap<Material, IsolatedTriangle> isolatedTriangles =
HashMultimap.create();
@Override
public void drawTriangles(Material material,
Collection<? extends TriangleXYZ> triangles,
List<List<VectorXZ>> texCoordLists) {
int i = 0;
for (TriangleXYZ triangle : triangles) {
if (reconstructFaces()) {
VectorXYZ n = triangle.getNormal();
if (Double.isNaN(n.x) || Double.isNaN(n.y) || Double.isNaN(n.z)) {
continue; //TODO log
}
isolatedTriangles.put(material,
new IsolatedTriangle(triangle, n, i*3, texCoordLists));
} else {
List<List<VectorXZ>> subLists = new ArrayList<List<VectorXZ>>();
for (List<VectorXZ> list : texCoordLists) {
subLists.add(list.subList(3*i, 3*(i+1)));
}
drawFace(material, triangle.getVertices(), null, subLists);
}
i++;
}
}
@Override
public void drawTrianglesWithNormals(Material material,
Collection<? extends TriangleXYZWithNormals> triangles,
List<List<VectorXZ>> texCoordLists) {
drawTriangles(material, triangles, texCoordLists);
//TODO keep normals information
}
@Override
public void drawConvexPolygon(Material material, List<VectorXYZ> vs,
List<List<VectorXZ>> texCoordLists) {
if (reconstructFaces()) {
super.drawConvexPolygon(material, vs, texCoordLists);
} else {
drawFace(material, vs, null, texCoordLists);
}
}
@Override
public void beginObject(WorldObject object) {
drawAndClearCurrentFaces();
}
@Override
public void finish() {
drawAndClearCurrentFaces();
}
/**
* integrates all {@link #isolatedTriangles} into faces,
* then draws all faces and clears the collection.
*/
private void drawAndClearCurrentFaces() {
for (Material material : isolatedTriangles.keySet()) {
Collection<Face> faces = combineTrianglesToFaces(isolatedTriangles.get(material));
/* draw faces */
for (Face face : faces) {
drawFace(material, face.vs,
nCopies(face.vs.size(), face.normal),
face.texCoordLists);
}
}
isolatedTriangles.clear();
}
/**
* @param isolatedTriangles non-empty collection of triangles
*/
protected static Collection<Face> combineTrianglesToFaces(
Collection<IsolatedTriangle> isolatedTriangles) {
List<IsolatedTriangle> triangles =
new LinkedList<IsolatedTriangle>(isolatedTriangles);
Collection<Face> faces = new ArrayList<Face>();
/* turn one triangle into a face */
faces.add(createFaceFromTriangle(triangles.remove(0)));
/* turn remaining triangles into faces or insert them into existing ones */
trianglesToFacesLoop: while (!triangles.isEmpty()) {
/* try to insert triangles into existing faces */
for (IsolatedTriangle triangle : triangles) {
for (Face face : faces) {
if (normalAlmostEquals(face.normal, triangle.normal)) {
boolean inserted = face.tryInsert(triangle);
if (inserted) {
triangles.remove(triangle);
continue trianglesToFacesLoop;
}
}
}
}
/* could not extend existing faces, start a new face instead */
faces.add(createFaceFromTriangle(triangles.remove(0)));
}
/* eliminate duplicate edges */
for (Face face : faces) {
face.removeDuplicateEdges();
}
return faces;
}
protected static boolean normalAlmostEquals(VectorXYZ n1, VectorXYZ n2) {
return abs(n1.x - n2.x) <= 0.01
&& abs(n1.y - n2.y) <= 0.01
&& abs(n1.z - n2.z) <= 0.01;
}
protected static Face createFaceFromTriangle(IsolatedTriangle t) {
List<VectorXYZ> newFaceVs =
new ArrayList<VectorXYZ>(t.triangle.getVertices());
List<List<VectorXZ>> newFaceTCLists = new ArrayList<List<VectorXZ>>();
for (int list = 0; list < t.texCoordLists.size(); list++) {
newFaceTCLists.add(new ArrayList<VectorXZ>(
t.texCoordLists.get(list).subList(
t.texCoordOffset, t.texCoordOffset + 3)));
}
return new Face(newFaceVs, newFaceTCLists, t.normal);
}
}