package org.osm2world.core.world.modules; import static java.util.Arrays.asList; import static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.filterWorldObjectCollisions; import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.parseHeight; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.apache.commons.configuration.Configuration; import org.openstreetmap.josm.plugins.graphview.core.data.Tag; import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup; import org.osm2world.core.map_data.data.MapArea; import org.osm2world.core.map_data.data.MapData; import org.osm2world.core.map_data.data.MapElement; import org.osm2world.core.map_data.data.MapNode; import org.osm2world.core.map_data.data.MapWaySegment; import org.osm2world.core.map_data.data.overlaps.MapOverlap; 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.AxisAlignedBoundingBoxXZ; import org.osm2world.core.math.GeometryUtil; import org.osm2world.core.math.VectorXYZ; import org.osm2world.core.math.VectorXZ; import org.osm2world.core.target.RenderableToAllTargets; import org.osm2world.core.target.Target; import org.osm2world.core.target.common.FaceTarget; import org.osm2world.core.target.common.RenderableToFaceTarget; import org.osm2world.core.target.common.material.Material; import org.osm2world.core.target.common.material.Materials; import org.osm2world.core.target.povray.POVRayTarget; import org.osm2world.core.target.povray.RenderableToPOVRay; import org.osm2world.core.world.data.AreaWorldObject; import org.osm2world.core.world.data.NoOutlineNodeWorldObject; import org.osm2world.core.world.data.WaySegmentWorldObject; import org.osm2world.core.world.data.WorldObject; import org.osm2world.core.world.modules.common.ConfigurableWorldModule; import org.osm2world.core.world.modules.common.WorldModuleBillboardUtil; /** * adds trees, tree rows, tree groups and forests to the world */ public class TreeModule extends ConfigurableWorldModule { private static final List<String> LEAF_TYPE_KEYS = asList("leaf_type", "wood", "type"); private static enum LeafType { BROADLEAVED("broadleaved", "broad_leaved", "broad_leafed", "deciduous"), NEEDLELEAVED("needleleaved", "coniferous", "conifer"); private final List<String> values; private LeafType(String... values) { this.values = asList(values); } public static LeafType getValue(TagGroup tags) { for (LeafType type : values()) { if (tags.containsAny(LEAF_TYPE_KEYS, type.values)) { return type; } } return null; } } private static final List<String> LEAF_CYCLE_KEYS = asList("leaf_cycle", "wood", "type"); private static enum LeafCycle { EVERGREEN("evergreen"), DECIDUOUS("deciduous", "broad_leaved"), SEMI_EVERGREEN("semi_evergreen"), SEMI_DECIDUOUS("semi_deciduous"); private final List<String> values; private LeafCycle(String... values) { this.values = asList(values); } public static LeafCycle getValue(TagGroup tags) { for (LeafCycle type : values()) { if (tags.containsAny(LEAF_CYCLE_KEYS, type.values)) { return type; } } return null; } } private static enum TreeSpecies { APPLE_TREE("malus"); private final String value; private TreeSpecies(String value) { this.value = value; } public static TreeSpecies getValue(TagGroup tags) { String speciesString = tags.getValue("species"); if (speciesString != null) { for (TreeSpecies species : values()) { if (speciesString.contains(species.value)) { return species; } } } // default to apple trees for orchards if (tags.contains("landuse", "orchard")) { return APPLE_TREE; } else { return null; } } } private boolean useBillboards = false; private double defaultTreeHeight = 10; private double defaultTreeHeightForest = 20; @Override public void setConfiguration(Configuration config) { super.setConfiguration(config); useBillboards = config.getBoolean("useBillboards", false); defaultTreeHeight = config.getDouble("defaultTreeHeight", 10); defaultTreeHeightForest = config.getDouble("defaultTreeHeightForest", 20); } @Override public final void applyTo(MapData mapData) { for (MapNode node : mapData.getMapNodes()) { if (node.getTags().contains("natural", "tree")) { node.addRepresentation(new Tree(node)); } } for (MapWaySegment segment : mapData.getMapWaySegments()) { if (segment.getTags().contains(new Tag("natural", "tree_row"))) { segment.addRepresentation(new TreeRow(segment)); } } for (MapArea area : mapData.getMapAreas()) { if (area.getTags().contains("natural", "wood") || area.getTags().contains("landuse", "forest") || area.getTags().containsKey("wood") || area.getTags().contains("landuse", "orchard")) { area.addRepresentation(new Forest(area, mapData)); } } } private static final float TREE_RADIUS_PER_HEIGHT = 0.2f; private void renderTree(Target<?> target, MapElement element, VectorXYZ pos, LeafType leafType, LeafCycle leafCycle, TreeSpecies species) { // if leaf type is unknown, make "random" decision based on x coord if (leafType == null) { if ((long)(pos.getX()) % 2 == 0) { leafType = LeafType.NEEDLELEAVED; } else { leafType = LeafType.BROADLEAVED; } } double height = getTreeHeight(element, leafType == LeafType.NEEDLELEAVED, species != null); if (useBillboards) { //"random" decision based on x coord boolean mirrored = (long)(pos.getX()) % 2 == 0; Material material = species == TreeSpecies.APPLE_TREE ? Materials.TREE_BILLBOARD_BROAD_LEAVED_FRUIT : leafType == LeafType.NEEDLELEAVED ? Materials.TREE_BILLBOARD_CONIFEROUS : Materials.TREE_BILLBOARD_BROAD_LEAVED; WorldModuleBillboardUtil.renderCrosstree(target, material, pos, (species != null ? 1.0 : 0.5 ) * height, height, mirrored); } else { renderTreeGeometry(target, pos, leafType, height); } } private static void renderTreeGeometry(Target<?> target, VectorXYZ posXYZ, LeafType leafType, double height) { boolean coniferous = (leafType == LeafType.NEEDLELEAVED); double stemRatio = coniferous?0.3:0.5; double radius = height*TREE_RADIUS_PER_HEIGHT; target.drawColumn(Materials.TREE_TRUNK, null, posXYZ, height*stemRatio, radius / 4, radius / 5, false, true); target.drawColumn(Materials.TREE_CROWN, null, posXYZ.y(posXYZ.y+height*stemRatio), height*(1-stemRatio), radius, coniferous ? 0 : radius, true, true); } /** * parse height (for forests, add some random factor) */ private double getTreeHeight(MapElement element, boolean isConiferousTree, boolean isFruitTree) { float heightFactor = 1; if (element instanceof MapArea) { heightFactor = 0.5f + 0.75f * (float)Math.random(); } double defaultHeight = defaultTreeHeight; if (element instanceof MapArea && !isFruitTree) { defaultHeight = defaultTreeHeightForest; } return heightFactor * parseHeight(element.getTags(), (float)defaultHeight); } private POVRayTarget previousDeclarationTarget = null; private void addTreeDeclarationsTo(POVRayTarget target) { if (target != previousDeclarationTarget) { //TODO support any combination of leaf type and leaf cycle previousDeclarationTarget = target; target.append("#ifndef (broad_leaved_tree)\n"); target.append("#declare broad_leaved_tree = object { union {\n"); renderTreeGeometry(target, VectorXYZ.NULL_VECTOR, LeafType.BROADLEAVED, 1); target.append("} }\n#end\n\n"); target.append("#ifndef (coniferous_tree)\n"); target.append("#declare coniferous_tree = object { union {\n"); renderTreeGeometry(target, VectorXYZ.NULL_VECTOR, LeafType.NEEDLELEAVED, 1); target.append("} }\n#end\n\n"); } } private void renderTree(POVRayTarget target, MapElement element, VectorXYZ pos, LeafType leafType, LeafCycle leafCycle, TreeSpecies species) { boolean isConiferousTree = (leafType == LeafType.NEEDLELEAVED); double height = getTreeHeight(element, isConiferousTree, false); //rotate randomly for variation float yRotation = (float) Math.random() * 360; //add union of stem and leaves if (isConiferousTree) { target.append("object { coniferous_tree rotate "); } else { target.append("object { broad_leaved_tree rotate "); } target.append(Float.toString(yRotation)); target.append("*y scale "); target.append(height); target.append(" translate "); target.appendVector(pos.x, 0, pos.z); target.append(" }\n"); } public class Tree extends NoOutlineNodeWorldObject implements RenderableToAllTargets, RenderableToPOVRay { private final LeafType leafType; private final LeafCycle leafCycle; private final TreeSpecies species; public Tree(MapNode node) { super(node); leafType = LeafType.getValue(node.getTags()); leafCycle = LeafCycle.getValue(node.getTags()); species = TreeSpecies.getValue(node.getTags()); } @Override public GroundState getGroundState() { return GroundState.ON; } @Override public AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() { return new AxisAlignedBoundingBoxXZ(Collections.singleton(node.getPos())); } @Override public void renderTo(Target<?> target) { renderTree(target, node, getBase(), leafType, leafCycle, species); } @Override public void addDeclarationsTo(POVRayTarget target) { addTreeDeclarationsTo(target); } @Override public void renderTo(POVRayTarget target) { renderTree(target, node, getBase(), leafType, leafCycle, species); } } public class TreeRow implements WaySegmentWorldObject, RenderableToPOVRay, RenderableToFaceTarget, RenderableToAllTargets { private final MapWaySegment segment; private final List<EleConnector> treeConnectors; private final LeafType leafType; private final LeafCycle leafCycle; private final TreeSpecies species; public TreeRow(MapWaySegment segment) { this.segment = segment; /* determine details about the trees in the row */ leafType = LeafType.getValue(segment.getTags()); leafCycle = LeafCycle.getValue(segment.getTags()); species = TreeSpecies.getValue(segment.getTags()); /* add connectors for the trees' positions */ //TODO: spread along a full way List<VectorXZ> treePositions = GeometryUtil.equallyDistributePointsAlong( 4 /* TODO: derive from tree count */ , false /* TODO: should be true once a full way is covered */, segment.getStartNode().getPos(), segment.getEndNode().getPos()); treeConnectors = new ArrayList<EleConnector>(treePositions.size()); for (VectorXZ treePosition : treePositions) { treeConnectors.add( new EleConnector(treePosition, null, getGroundState())); } } @Override public MapWaySegment getPrimaryMapElement() { return segment; } @Override public Iterable<EleConnector> getEleConnectors() { return treeConnectors; } @Override public void defineEleConstraints(EleConstraintEnforcer enforcer) {} @Override public VectorXZ getEndPosition() { return segment.getEndNode().getPos(); } @Override public VectorXZ getStartPosition() { return segment.getStartNode().getPos(); } @Override public GroundState getGroundState() { return GroundState.ON; } @Override public void renderTo(POVRayTarget target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, segment, treeConnector.getPosXYZ(), leafType, leafCycle, species); } } @Override public void addDeclarationsTo(POVRayTarget target) { addTreeDeclarationsTo(target); } @Override public void renderTo(FaceTarget<?> target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, segment, treeConnector.getPosXYZ(), leafType, leafCycle, species); target.flushReconstructedFaces(); } } @Override public void renderTo(Target<?> target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, segment, treeConnector.getPosXYZ(), leafType, leafCycle, species); } } //TODO: there is significant code duplication with Forest... } public class Forest implements AreaWorldObject, RenderableToPOVRay, RenderableToFaceTarget, RenderableToAllTargets { private final MapArea area; private final MapData mapData; private Collection<EleConnector> treeConnectors = null; private final LeafType leafType; private final LeafCycle leafCycle; private final TreeSpecies species; public Forest(MapArea area, MapData mapData) { this.area = area; this.mapData = mapData; leafType = LeafType.getValue(area.getTags()); leafCycle = LeafCycle.getValue(area.getTags()); species = TreeSpecies.getValue(area.getTags()); } private void createTreeConnectors(double density) { /* collect other objects that the trees should not be placed on */ Collection<WorldObject> avoidedObjects = new ArrayList<WorldObject>(); for (MapOverlap<?, ?> overlap : area.getOverlaps()) { for (WorldObject otherRep : overlap.getOther(area).getRepresentations()) { if (otherRep.getGroundState() == GroundState.ON) { avoidedObjects.add(otherRep); } } } /* place the trees */ List<VectorXZ> treePositions = GeometryUtil.distributePointsOn(area.getOsmObject().id, area.getPolygon(), mapData.getBoundary(), density, 0.3f); filterWorldObjectCollisions(treePositions, avoidedObjects); /* create a terrain connector for each tree */ treeConnectors = new ArrayList<EleConnector>(treePositions.size()); for (VectorXZ treePosition : treePositions) { treeConnectors.add(new EleConnector( treePosition, null, getGroundState())); } } @Override public MapArea getPrimaryMapElement() { return area; } @Override public Iterable<EleConnector> getEleConnectors() { if (treeConnectors == null) { createTreeConnectors(config.getDouble("treesPerSquareMeter", 0.01f)); } return treeConnectors; } @Override public void defineEleConstraints(EleConstraintEnforcer enforcer) {} @Override public GroundState getGroundState() { return GroundState.ON; } @Override public void renderTo(POVRayTarget target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, area, treeConnector.getPosXYZ(), leafType, leafCycle, species); } } @Override public void addDeclarationsTo(POVRayTarget target) { addTreeDeclarationsTo(target); } @Override public void renderTo(FaceTarget<?> target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, area, treeConnector.getPosXYZ(), leafType, leafCycle, species); target.flushReconstructedFaces(); } } @Override public void renderTo(Target<?> target) { for (EleConnector treeConnector : treeConnectors) { renderTree(target, area, treeConnector.getPosXYZ(), leafType, leafCycle, species); } } } }