package org.osm2world.core.world.modules;
import static java.util.Collections.*;
import static org.osm2world.core.map_data.creation.EmptyTerrainBuilder.EMPTY_SURFACE_TAG;
import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.MIN;
import static org.osm2world.core.map_elevation.data.GroundState.*;
import static org.osm2world.core.target.common.material.NamedTexCoordFunction.GLOBAL_X_Z;
import static org.osm2world.core.target.common.material.TexCoordUtil.triangleTexCoordLists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openstreetmap.josm.plugins.graphview.core.data.Tag;
import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;
import org.osm2world.core.map_data.creation.EmptyTerrainBuilder;
import org.osm2world.core.map_data.data.MapArea;
import org.osm2world.core.map_data.data.overlaps.MapOverlap;
import org.osm2world.core.map_data.data.overlaps.MapOverlapType;
import org.osm2world.core.map_elevation.creation.EleConstraintEnforcer;
import org.osm2world.core.map_elevation.data.EleConnector;
import org.osm2world.core.map_elevation.data.GroundState;
import org.osm2world.core.math.LineSegmentXZ;
import org.osm2world.core.math.PolygonWithHolesXZ;
import org.osm2world.core.math.PolygonXYZ;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.TriangleXYZ;
import org.osm2world.core.math.TriangleXZ;
import org.osm2world.core.math.VectorGridXZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.math.algorithms.CAGUtil;
import org.osm2world.core.math.algorithms.JTSTriangulationUtil;
import org.osm2world.core.math.algorithms.Poly2TriTriangulationUtil;
import org.osm2world.core.target.RenderableToAllTargets;
import org.osm2world.core.target.Target;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.target.common.material.Materials;
import org.osm2world.core.util.exception.TriangulationException;
import org.osm2world.core.world.data.AbstractAreaWorldObject;
import org.osm2world.core.world.data.TerrainBoundaryWorldObject;
import org.osm2world.core.world.data.WorldObject;
import org.osm2world.core.world.data.WorldObjectWithOutline;
import org.osm2world.core.world.modules.common.AbstractModule;
/**
* adds generic areas with surface information to the world.
* Is based on surface information on otherwise unknown/unspecified areas.
*/
public class SurfaceAreaModule extends AbstractModule {
/** assumptions about default surfaces for certain tags */
private static final Map<Tag, String> defaultSurfaceMap
= new HashMap<Tag, String>();
static {
defaultSurfaceMap.put(new Tag("leisure", "pitch"), "ground");
defaultSurfaceMap.put(new Tag("landuse", "construction"), "ground");
defaultSurfaceMap.put(new Tag("golf", "bunker"), "sand");
defaultSurfaceMap.put(new Tag("golf", "green"), "grass");
defaultSurfaceMap.put(new Tag("natural", "sand"), "sand");
defaultSurfaceMap.put(new Tag("natural", "beach"), "sand");
defaultSurfaceMap.put(new Tag("landuse", "meadow"), "grass");
defaultSurfaceMap.put(new Tag("landuse", "grass"), "grass");
}
@Override
protected void applyToArea(MapArea area) {
if (!area.getRepresentations().isEmpty()) return;
TagGroup tags = area.getTags();
if (tags.containsKey("surface")) {
area.addRepresentation(new SurfaceArea(area, tags.getValue("surface")));
} else {
for (Tag tagWithDefault : defaultSurfaceMap.keySet()) {
if (tags.contains(tagWithDefault)) {
area.addRepresentation(new SurfaceArea(
area, defaultSurfaceMap.get(tagWithDefault)));
}
}
}
}
public static class SurfaceArea extends AbstractAreaWorldObject
implements RenderableToAllTargets, TerrainBoundaryWorldObject {
private final String surface;
private Collection<TriangleXZ> triangulationXZ;
public SurfaceArea(MapArea area, String surface) {
super(area);
this.surface = surface;
}
@Override
public void renderTo(Target<?> target) {
Material material = null;
if (surface.equals(EMPTY_SURFACE_TAG.value)) {
material = Materials.TERRAIN_DEFAULT;
} else {
material = Materials.getSurfaceMaterial(surface);
}
if (material != null) {
Collection<TriangleXYZ> triangles = getTriangulation();
target.drawTriangles(material, triangles,
triangleTexCoordLists(triangles, material, GLOBAL_X_Z));
}
}
/**
* calculates the true ground footprint of this area by removing
* area covered by other overlapping features, then triangulates it
* into counterclockwise triangles.
*/
@Override
protected Collection<TriangleXZ> getTriangulationXZ() {
if (triangulationXZ != null) {
return triangulationXZ;
}
boolean isEmptyTerrain = surface.equals(EMPTY_SURFACE_TAG.value);
/* collect the outlines of overlapping ground polygons and other polygons,
* and EleConnectors within the area */
List<SimplePolygonXZ> subtractPolys = new ArrayList<SimplePolygonXZ>();
List<SimplePolygonXZ> allPolys = new ArrayList<SimplePolygonXZ>();
List<VectorXZ> eleConnectorPoints = new ArrayList<VectorXZ>();
for (MapOverlap<?, ?> overlap : area.getOverlaps()) {
for (WorldObject otherWO : overlap.getOther(area).getRepresentations()) {
if (otherWO instanceof TerrainBoundaryWorldObject
&& otherWO.getGroundState() == GroundState.ON) {
if (otherWO instanceof SurfaceArea && !isEmptyTerrain) {
// empty terrain has lowest priority
continue;
}
if (overlap.type == MapOverlapType.CONTAIN
&& overlap.e1 == this.area) {
// completely within other element, no ground area left
return emptyList();
}
TerrainBoundaryWorldObject terrainBoundary =
(TerrainBoundaryWorldObject)otherWO;
SimplePolygonXZ outlinePolygon = terrainBoundary.getOutlinePolygonXZ();
if (outlinePolygon != null) {
subtractPolys.add(outlinePolygon);
allPolys.add(outlinePolygon);
for (EleConnector eleConnector : otherWO.getEleConnectors()) {
if (!outlinePolygon.getVertexCollection().contains(eleConnector.pos)) {
eleConnectorPoints.add(eleConnector.pos);
}
}
}
} else {
for (EleConnector eleConnector : otherWO.getEleConnectors()) {
if (eleConnector.reference == null) {
/* workaround to avoid using connectors at intersections,
* which might fall on area segments
* //TODO cleaner solution
*/
continue;
}
eleConnectorPoints.add(eleConnector.pos);
}
if (otherWO instanceof WorldObjectWithOutline) {
SimplePolygonXZ outlinePolygon =
((WorldObjectWithOutline)otherWO).getOutlinePolygonXZ();
if (outlinePolygon != null) {
allPolys.add(outlinePolygon);
}
}
}
}
}
/* add a grid of points within the area for smoother surface shapes */
VectorGridXZ pointGrid = new VectorGridXZ(
area.getAxisAlignedBoundingBoxXZ(),
EmptyTerrainBuilder.POINT_GRID_DIST);
for (VectorXZ point : pointGrid) {
//don't insert if it is e.g. on top of a tunnel;
//otherwise there would be no minimum vertical distance
boolean safe = true;
for (SimplePolygonXZ polygon : allPolys) {
if (polygon.contains(point)) {
safe = false;
break;
}
}
if (safe) {
eleConnectorPoints.add(point);
}
}
/* create "leftover" polygons by subtracting the existing ones */
Collection<PolygonWithHolesXZ> polygons;
if (subtractPolys.isEmpty()) {
/* SUGGEST (performance) handle the common "empty terrain cell"
* special case more efficiently, also regarding point raster? */
polygons = singleton(area.getPolygon());
} else {
polygons = CAGUtil.subtractPolygons(
area.getOuterPolygon(), subtractPolys);
}
/* triangulate, using elevation information from all participants */
triangulationXZ = new ArrayList<TriangleXZ>();
for (PolygonWithHolesXZ polygon : polygons) {
List<VectorXZ> points = new ArrayList<VectorXZ>();
for (VectorXZ point : eleConnectorPoints) {
if (polygon.contains(point)) {
points.add(point);
}
}
try {
triangulationXZ.addAll(Poly2TriTriangulationUtil.triangulate(
polygon.getOuter(),
polygon.getHoles(),
Collections.<LineSegmentXZ>emptyList(),
points));
} catch (TriangulationException e) {
System.err.println("Poly2Tri exception for " + this + ":");
e.printStackTrace();
System.err.println("... falling back to JTS triangulation.");
triangulationXZ.addAll(JTSTriangulationUtil.triangulate(
polygon.getOuter(),
polygon.getHoles(),
Collections.<LineSegmentXZ>emptyList(),
points));
}
}
return triangulationXZ;
}
@Override
public void defineEleConstraints(EleConstraintEnforcer enforcer) {
super.defineEleConstraints(enforcer);
/** add vertical distance to connectors above and below */
for (MapOverlap<?, ?> overlap : area.getOverlaps()) {
for (WorldObject otherWO : overlap.getOther(area).getRepresentations()) {
for (EleConnector eleConnector : otherWO.getEleConnectors()) {
EleConnector ownConnector = getEleConnectors().getConnector(eleConnector.pos);
if (ownConnector == null) continue;
if (eleConnector.groundState == ABOVE) {
enforcer.requireVerticalDistance(
MIN, 1,
eleConnector, ownConnector); //TODO actual clearing
} else if (eleConnector.groundState == BELOW) {
enforcer.requireVerticalDistance(
MIN, 10,
ownConnector, eleConnector); //TODO actual clearing
}
}
}
}
}
@Override
public PolygonXYZ getOutlinePolygon() {
if (surface.equals(EMPTY_SURFACE_TAG.value)) {
// avoid interfering with e.g. tree placement
return null;
} else {
return super.getOutlinePolygon();
}
}
@Override
public SimplePolygonXZ getOutlinePolygonXZ() {
if (surface.equals(EMPTY_SURFACE_TAG.value)) {
// avoid interfering with e.g. tree placement
return null;
} else {
return super.getOutlinePolygonXZ();
}
}
@Override
public GroundState getGroundState() {
if (BridgeModule.isBridge(area.getTags())) {
return GroundState.ABOVE;
} else if (TunnelModule.isTunnel(area.getTags())) {
return GroundState.BELOW;
} else {
return GroundState.ON;
}
}
}
}