diff --git a/vtm/src/org/oscim/layers/tile/buildings/BuildingLayer.java b/vtm/src/org/oscim/layers/tile/buildings/BuildingLayer.java index 64c55b41..3f7f8255 100644 --- a/vtm/src/org/oscim/layers/tile/buildings/BuildingLayer.java +++ b/vtm/src/org/oscim/layers/tile/buildings/BuildingLayer.java @@ -41,7 +41,7 @@ import java.util.Set; public class BuildingLayer extends Layer implements TileLoaderThemeHook { - private final static int BUILDING_LEVEL_HEIGHT = 280; // cm + protected final static int BUILDING_LEVEL_HEIGHT = 280; // cm private final static int MIN_ZOOM = 17; private final static int MAX_ZOOM = 17; @@ -52,7 +52,7 @@ public class BuildingLayer extends Layer implements TileLoaderThemeHook { private static final Object BUILDING_DATA = BuildingLayer.class.getName(); // Can be replaced with Multimap in Java 8 - private HashMap> mBuildings = new HashMap<>(); + protected HashMap> mBuildings = new HashMap<>(); class BuildingElement { MapElement element; @@ -93,6 +93,8 @@ public class BuildingLayer extends Layer implements TileLoaderThemeHook { @Override public boolean process(MapTile tile, RenderBuckets buckets, MapElement element, RenderStyle style, int level) { + // FIXME check why some buildings are processed up to 4 times (should avoid overhead) + // FIXME fix artifacts at tile borders if (!(style instanceof ExtrusionStyle)) return false; @@ -128,7 +130,7 @@ public class BuildingLayer extends Layer implements TileLoaderThemeHook { * @param extrusion the style of map element * @param tile the tile which contains map element */ - private void processElement(MapElement element, ExtrusionStyle extrusion, MapTile tile) { + protected void processElement(MapElement element, ExtrusionStyle extrusion, MapTile tile) { int height = 0; // cm int minHeight = 0; // cm @@ -162,7 +164,7 @@ public class BuildingLayer extends Layer implements TileLoaderThemeHook { * * @param tile the tile which contains stored map elements */ - private void processElements(MapTile tile) { + protected void processElements(MapTile tile) { if (!mBuildings.containsKey(tile.hashCode())) return; diff --git a/vtm/src/org/oscim/layers/tile/buildings/S3DBLayer.java b/vtm/src/org/oscim/layers/tile/buildings/S3DBLayer.java new file mode 100644 index 00000000..1d0390ac --- /dev/null +++ b/vtm/src/org/oscim/layers/tile/buildings/S3DBLayer.java @@ -0,0 +1,283 @@ +/* + * Copyright 2018 Gustl22 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.layers.tile.buildings; + +import org.oscim.core.Box; +import org.oscim.core.GeometryBuffer; +import org.oscim.core.MapElement; +import org.oscim.core.Tag; +import org.oscim.core.TagSet; +import org.oscim.core.Tile; +import org.oscim.layers.tile.MapTile; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.map.Map; +import org.oscim.theme.styles.ExtrusionStyle; +import org.oscim.utils.ExtrusionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.oscim.renderer.MapRenderer.COORD_SCALE; + +/** + * An experimental layer to display S3DB elements. + */ +public class S3DBLayer extends BuildingLayer { + + private static final Logger log = LoggerFactory.getLogger(S3DBLayer.class); + + private final float TILE_SCALE = (ExtrusionUtils.REF_TILE_SIZE / (Tile.SIZE * COORD_SCALE)); + + private boolean mColored = true; + + public S3DBLayer(Map map, VectorTileLayer tileLayer) { + super(map, tileLayer, true); + } + + public boolean isColored() { + return mColored; + } + + public void setColored(boolean colored) { + mColored = colored; + } + + @Override + public void complete(MapTile tile, boolean success) { + super.complete(tile, success); + // Do stuff here + } + + @Override + public void processElement(MapElement element, ExtrusionStyle extrusion, MapTile tile) { + float groundScale = tile.getGroundScale(); + + int maxHeight = 0; // cm + int minHeight = 0; // cm + int roofHeight = 0; + + // Get roof height + String v = element.tags.getValue(Tag.KEY_ROOF_HEIGHT); + if (v != null) { + roofHeight = (int) (Float.parseFloat(v) * 100); + } else if ((v = element.tags.getValue(Tag.KEY_ROOF_LEVELS)) != null) { + roofHeight = (int) (Float.parseFloat(v) * BUILDING_LEVEL_HEIGHT); + } else if ((v = element.tags.getValue(Tag.KEY_ROOF_ANGLE)) != null) { + Box bb = null; + for (int k = 0; k < element.index[0]; k += 2) { + float p1 = element.points[k]; + float p2 = element.points[k + 1]; + if (bb == null) + bb = new Box(p1, p2, p1, p2); + else { + bb.add(p1, p2); + } + } + if (bb != null) { + float maxSize = (int) Math.max(bb.getHeight(), bb.getWidth()) * TILE_SCALE; + roofHeight = (int) ((Float.parseFloat(v) / 45.f) * (maxSize * 15)); // Angle is simplified, 15 is some constant, may depend on lat + } + } else if ((v = element.tags.getValue(Tag.KEY_ROOF_SHAPE)) != null && !v.equals(Tag.VALUE_FLAT)) { + roofHeight = (2 * BUILDING_LEVEL_HEIGHT); + } + + // Get building height + v = element.tags.getValue(Tag.KEY_HEIGHT); + if (v != null) { + maxHeight = (int) (Float.parseFloat(v) * 100); + } else { + // #TagFromTheme: generalize level/height tags + if ((v = element.tags.getValue(Tag.KEY_BUILDING_LEVELS)) != null) { + maxHeight = (int) (Float.parseFloat(v) * BUILDING_LEVEL_HEIGHT); + maxHeight += roofHeight; + } + } + if (maxHeight == 0) + maxHeight = extrusion.defaultHeight * 100; + + v = element.tags.getValue(Tag.KEY_MIN_HEIGHT); + if (v != null) + minHeight = (int) (Float.parseFloat(v) * 100); + else { + // #TagFromTheme: level/height tags + if ((v = element.tags.getValue(Tag.KEY_BUILDING_MIN_LEVEL)) != null) + minHeight = (int) (Float.parseFloat(v) * BUILDING_LEVEL_HEIGHT); + } + + // Get building color + Integer bColor = null; + if (mColored) { + if (element.tags.containsKey(Tag.KEY_BUILDING_COLOR)) { + bColor = S3DBUtils.getColor(element.tags.getValue(Tag.KEY_BUILDING_COLOR), false); + } else if (element.tags.containsKey(Tag.KEY_BUILDING_MATERIAL)) { + bColor = S3DBUtils.getMaterialColor(element.tags.getValue(Tag.KEY_BUILDING_MATERIAL), false); + } + } + + if (bColor == null) { + bColor = extrusion.colorTop; + } + + // Scale x, y and z axis + ExtrusionUtils.mapPolyCoordScale(element); + float minHeightS = ExtrusionUtils.mapGroundScale(minHeight, groundScale) * TILE_SCALE; + float maxHeightS = ExtrusionUtils.mapGroundScale(maxHeight, groundScale) * TILE_SCALE; + float minRoofHeightS = ExtrusionUtils.mapGroundScale(maxHeight - roofHeight, groundScale) * TILE_SCALE; + + // Process building and roof + processRoof(element, tile, minRoofHeightS, maxHeightS, bColor); + if (S3DBUtils.calcOutlines(element, minHeightS, minRoofHeightS)) { + get(tile).addMeshElement(element, groundScale, bColor); + } + } + + @Override + protected void processElements(MapTile tile) { + if (!mBuildings.containsKey(tile.hashCode())) + return; + + List tileBuildings = mBuildings.get(tile.hashCode()); + Set rootBuildings = new HashSet<>(); + for (BuildingElement partBuilding : tileBuildings) { + if (!partBuilding.isPart) + continue; + + TagSet partTags = partBuilding.element.tags; + String refId = partTags.getValue(Tag.KEY_REF); // #TagFromTheme + refId = refId == null ? partTags.getValue("root_id") : refId; // Mapzen + if (refId == null) + continue; + + // Search buildings which inherit parts + for (BuildingElement rootBuilding : tileBuildings) { + if (rootBuilding.isPart + || !(refId.equals(rootBuilding.element.tags.getValue(Tag.KEY_ID)))) + continue; + + if (mColored) { + TagSet rootTags = rootBuilding.element.tags; + for (int i = 0; i < rootTags.size(); i++) { + Tag rTag = rootTags.get(i); + if ((rTag.key.equals(Tag.KEY_COLOR) && !partTags.containsKey(Tag.KEY_MATERIAL) + || rTag.key.equals(Tag.KEY_ROOF_COLOR) && !partTags.containsKey(Tag.KEY_ROOF_MATERIAL) + || rTag.key.equals(Tag.KEY_ROOF_SHAPE)) + && !partTags.containsKey(rTag.key)) { + partTags.add(rTag); + } + } + } + rootBuildings.add(rootBuilding); + break; + } + } + + tileBuildings.removeAll(rootBuildings); // root buildings aren't rendered + + for (BuildingElement buildingElement : tileBuildings) { + processElement(buildingElement.element, buildingElement.style, tile); + } + mBuildings.remove(tile.hashCode()); + } + + /** + * Process the roof parts of building. + * + * @param element the MapElement which needs a roof + * @param tile the tile which contains map element + * @param minHeight the height of the underlying building + * @param maxHeight the height of the roof + minHeight (whole building) + * @param buildingColor the color of main building + */ + private void processRoof(MapElement element, MapTile tile, float minHeight, float maxHeight, int buildingColor) { + Integer roofColor = null; + String v; + + if (mColored) { + v = element.tags.getValue(Tag.KEY_ROOF_COLOR); + if (v != null) + roofColor = S3DBUtils.getColor(v, true); + else if ((v = element.tags.getValue(Tag.KEY_ROOF_MATERIAL)) != null) + roofColor = S3DBUtils.getMaterialColor(v, true); + } + + boolean roofOrientationAcross = false; + if ((v = element.tags.getValue(Tag.KEY_ROOF_ORIENTATION)) != null) { + if (v.equals(Tag.VALUE_ACROSS)) { + roofOrientationAcross = true; + } + } + + // Calc roof shape + v = element.tags.getValue(Tag.KEY_ROOF_SHAPE); + if (v == null) { + v = Tag.VALUE_FLAT; + } + + float groundScale = tile.getGroundScale(); + + GeometryBuffer gElement = new GeometryBuffer(element); + GeometryBuffer specialParts = null; + if (roofColor == null) roofColor = buildingColor; + boolean success = false; + + switch (v) { + case Tag.VALUE_DOME: + case Tag.VALUE_ONION: + success = S3DBUtils.calcCircleMesh(gElement, minHeight, maxHeight, v); + break; + case Tag.VALUE_ROUND: + case Tag.VALUE_SALTBOX: + case Tag.VALUE_GABLED: + case Tag.VALUE_GAMBREL: + specialParts = new GeometryBuffer(0, 0); // No data in GeometryBuffer needed + success = S3DBUtils.calcRidgeMesh(gElement, minHeight, maxHeight, roofOrientationAcross, true, specialParts); + break; + case Tag.VALUE_MANSARD: + case Tag.VALUE_HALF_HIPPED: + case Tag.VALUE_HIPPED: + success = S3DBUtils.calcRidgeMesh(gElement, minHeight, maxHeight, roofOrientationAcross, false, null); + break; + case Tag.VALUE_SKILLION: + // ROOF_SLOPE_DIRECTION is not supported yet + String roofDirection = element.tags.getValue(Tag.KEY_ROOF_DIRECTION); + float roofDegree = 0; + if (roofDirection != null) { + roofDegree = Float.parseFloat(roofDirection); + } + specialParts = new GeometryBuffer(element); + success = S3DBUtils.calcSkillionMesh(gElement, minHeight, maxHeight, roofDegree, specialParts); + break; + case Tag.VALUE_PYRAMIDAL: + success = S3DBUtils.calcPyramidalMesh(gElement, minHeight, maxHeight); + break; + case Tag.VALUE_FLAT: + default: + success = S3DBUtils.calcFlatMesh(gElement, minHeight); + break; + } + + if (success) { + get(tile).addMeshElement(gElement, groundScale, roofColor); + if (specialParts != null) { + get(tile).addMeshElement(specialParts, groundScale, buildingColor); + } + } else { + log.debug("Roof calculation failed: " + element.toString()); + } + } +} diff --git a/vtm/src/org/oscim/layers/tile/buildings/S3DBUtils.java b/vtm/src/org/oscim/layers/tile/buildings/S3DBUtils.java index fcdfcbc5..7e82d822 100644 --- a/vtm/src/org/oscim/layers/tile/buildings/S3DBUtils.java +++ b/vtm/src/org/oscim/layers/tile/buildings/S3DBUtils.java @@ -1,6 +1,6 @@ /* * Copyright 2013 Hannes Janetzek - * Copyright 2017 Gustl22 + * Copyright 2017, 2018 Gustl22 * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -18,11 +18,24 @@ package org.oscim.layers.tile.buildings; import org.oscim.backend.canvas.Color; +import org.oscim.core.GeometryBuffer; +import org.oscim.core.Tag; import org.oscim.utils.ColorUtil; import org.oscim.utils.ColorsCSS; +import org.oscim.utils.Tessellator; +import org.oscim.utils.geom.GeometryUtils; +import org.oscim.utils.math.MathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + /** * Provides utils for S3DB layers. */ @@ -33,6 +46,1013 @@ public final class S3DBUtils { private final static double HSV_S = 0.7; private final static double HSV_V = 1.2; + // Toggle this to debug and improve ridge calculation, you can see the faults in map then. + private static final boolean IMPROVE_RIDGE_CALCULATION = false; + private static final int SNAP_THRESHOLD = 70; // Threshold for ridge snap calculation (maybe should depend on map scale) + + /** + * Adds point to ridgePoints and snaps it to a point which is in radius of SNAP_THRESHOLD. + */ + private static void addSnapRidgePoint(int id, float[] point, TreeMap ridgePoints) { + // Simplify ridgePoints + if (point == null) return; + for (float[] ridPoint : ridgePoints.values()) { + if (ridPoint == null) { + log.debug("Ridge point not found!"); + continue; + } + if (GeometryUtils.distance2D(ridPoint, point) < SNAP_THRESHOLD) { + ridgePoints.put(id, ridPoint); + return; + } + } + ridgePoints.put(id, point); + } + + /** + * Calculates a circle mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcCircleMesh(GeometryBuffer element, float minHeight, float maxHeight, String type) { + float[] points = element.points; + int[] index = element.index; + + boolean outerProcessed = false; + for (int i = 0, pointPos = 0; i < index.length && !outerProcessed; i++) { + if (index[i] < 0) { + break; + } + + int numSections = index[i] / 2; + if (numSections < 0) continue; + + outerProcessed = true; + + // Init mesh + GeometryBuffer mesh; + switch (type) { + case Tag.VALUE_ONION: + float[][] onionShape = new float[][]{ + {1, 0, 0}, + {0.2f, 0, 0.01f}, + {0.875f, 0, 0.1875f}, + {1, 0, 0.375f}, + {0.875f, 0, 0.5625f}, + {0.5f, 0, 0.75f}, + {0.2f, 0, 0.8125f}, + {0, 0, 1}}; + mesh = initCircleMesh(onionShape, numSections); + break; + case Tag.VALUE_DOME: + default: + float[][] domeShape = new float[][]{ + {1, 0, 0}, + {0.825f, 0, 0.5f}, + {0.5f, 0, 0.825f}, + {0, 0, 1}}; + mesh = initCircleMesh(domeShape, numSections); + break; + } + + + // Calculate center and load points + float centerX = 0; + float centerY = 0; + float radius = 0; + + List point3Fs = new ArrayList<>(); + + for (int j = 0; j < (numSections * 2); j += 2, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + + centerX += x; + centerY += y; + } + + centerX = centerX / numSections; + centerY = centerY / numSections; + + // Calc max radius + for (float[] point3F : point3Fs) { + float difX = point3F[0] - centerX; + float difY = point3F[1] - centerY; + float tmpR = (float) Math.sqrt(difX * difX + difY * difY); + if (tmpR > radius) { + radius = tmpR; + } + } + + element.points = mesh.points; + + // Calc radius and adjust angle + int numPointsPerSection = (element.points.length / (3 * numSections)); + float heightRange = maxHeight - minHeight; + for (int k = 0, j = 0; k < numSections; k++) { + float px = point3Fs.get(k)[0] - centerX; + float py = point3Fs.get(k)[1] - centerY; + + float phi = (float) Math.atan2(py, px); + int sectionLimit = (numPointsPerSection + numPointsPerSection * k) * 3; + + for (boolean first = true; j < sectionLimit; j = j + 3) { + float r = element.points[j + 0] * radius; // Set radius to initial x (always positive) stretched with individual element radius + px = (float) (r * Math.cos(phi)); + py = (float) (r * Math.sin(phi)); + + if (!first) { + element.points[j + 0] = centerX + px; + element.points[j + 1] = centerY + py; + element.points[j + 2] = minHeight + element.points[j + 2] * heightRange; + } else { + // Set lowest points to outline points. + first = false; + element.points[j + 0] = point3Fs.get(k)[0]; + element.points[j + 1] = point3Fs.get(k)[1]; + element.points[j + 2] = minHeight; + } + } + } + + element.index = mesh.index; + element.pointNextPos = element.points.length; + } + + element.type = GeometryBuffer.GeometryType.TRIS; + return true; + } + + /** + * Calculates a flat mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcFlatMesh(GeometryBuffer element, float maxHeight) { + + if (Tessellator.tessellate(element, element) == 0) return false; + + float[] points = element.points; + List point3Fs = new ArrayList<>(); + + // Calculate center and load points + for (int pointPos = 0; pointPos < points.length; pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + point3Fs.add(new float[]{x, y, maxHeight}); + } + + points = new float[3 * point3Fs.size()]; + for (int i = 0; i < point3Fs.size(); i++) { + int pPos = 3 * i; + float[] point3D = point3Fs.get(i); + points[pPos + 0] = point3D[0]; + points[pPos + 1] = point3D[1]; + points[pPos + 2] = point3D[2]; + } + + element.points = points; + element.pointNextPos = element.points.length; + element.type = GeometryBuffer.GeometryType.TRIS; + return true; + } + + /** + * Calculates a mesh for the outlines of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcOutlines(GeometryBuffer element, float minHeight, float maxHeight) { + float[] points = element.points; + int[] index = element.index; + + element.points = null; + element.index = null; + + for (int i = 0, pointPos = 0; i < index.length; i++) { + if (index[i] < 0) { + break; + } + + int numPoints = index[i] / 2; + if (numPoints < 0) continue; + + List point3Fs = new ArrayList<>(); + + // load points + for (int j = 0; j < numPoints; j++, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + point3Fs.add(new float[]{x, y, maxHeight}); + } + + // Write index: index gives the first point of triangle mesh (divided 3) + int[] meshIndex = new int[numPoints * 6]; // 3 vertices and each side needs 2 triangles + for (int j = 0; j < point3Fs.size(); j = j + 2) { + int pos = 3 * j; // triangle mesh + meshIndex[pos + 2] = j; + meshIndex[pos + 1] = (j + 1) % point3Fs.size(); + meshIndex[pos + 0] = (j + 3) % point3Fs.size(); + + meshIndex[pos + 5] = (j + 3) % point3Fs.size(); + meshIndex[pos + 4] = (j + 2) % point3Fs.size(); + meshIndex[pos + 3] = (j); + } + + // Write points + float[] meshPoints = new float[point3Fs.size() * 3]; + for (int j = 0; j < point3Fs.size(); j++) { + int pos = 3 * j; + meshPoints[pos + 0] = point3Fs.get(j)[0]; + meshPoints[pos + 1] = point3Fs.get(j)[1]; + meshPoints[pos + 2] = point3Fs.get(j)[2]; + } + + // Init points and indices or add more polygons (e.g. inner rings) + if (element.points == null) { + element.points = meshPoints; + } else { + float[] tmpPoints = element.points; + element.points = new float[tmpPoints.length + meshPoints.length]; + System.arraycopy(tmpPoints, 0, element.points, 0, tmpPoints.length); + System.arraycopy(meshPoints, 0, element.points, tmpPoints.length, meshPoints.length); + } + + if (element.index == null) { + element.index = meshIndex; + } else { + int[] tmpIndex = element.index; + element.index = new int[tmpIndex.length + meshIndex.length]; + System.arraycopy(tmpIndex, 0, element.index, 0, tmpIndex.length); + // Shift all indices with previous element.points.length + for (int k = 0; k < meshIndex.length; k++) { + element.index[k + tmpIndex.length] = meshIndex[k] + (element.pointNextPos / 3); + } + } + element.pointNextPos = element.points.length; + } + + if (element.points == null) { + return false; + } + + //element.indexCurrentPos = 0; + element.type = GeometryBuffer.GeometryType.TRIS; + return true; + } + + /** + * Calculates a pyramidal mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcPyramidalMesh(GeometryBuffer element, float minHeight, float maxHeight) { + float[] points = element.points; + int[] index = element.index; + + for (int i = 0, pointPos = 0; i < index.length; i++) { + if (index[i] < 0) { + break; + } + if (i > 0) break; // May add inner rings + + int numPoints = index[i] / 2; + if (numPoints < 0) continue; + + float centerX = 0; + float centerY = 0; + + List point3Fs = new ArrayList<>(); + + // Calculate center and load points + for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + + centerX += x; + centerY += y; + } + + centerX = centerX / numPoints; + centerY = centerY / numPoints; + + // Init top of roof + float[] topPoint = new float[]{centerX, centerY, maxHeight}; + + // Write index: index gives the first point of triangle mesh (divided 3) + int[] meshIndex = new int[numPoints * 3]; + for (int j = 0; j < point3Fs.size(); j++) { + int pos = 3 * j; // triangle mesh + meshIndex[pos + 0] = j; + meshIndex[pos + 1] = (j + 1) % point3Fs.size(); + meshIndex[pos + 2] = point3Fs.size(); + } + + // Write points + point3Fs.add(topPoint); + float[] meshPoints = new float[point3Fs.size() * 3]; + for (int j = 0; j < point3Fs.size(); j++) { + int pos = 3 * j; + float[] point3D = point3Fs.get(j); + meshPoints[pos + 0] = point3D[0]; + meshPoints[pos + 1] = point3D[1]; + meshPoints[pos + 2] = point3D[2]; + } + + element.points = meshPoints; + element.index = meshIndex; + //element.indexCurrentPos = 0; + element.pointNextPos = meshPoints.length; + } + + element.type = GeometryBuffer.GeometryType.TRIS; + return true; + } + + /** + * Calculates a ridge mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @param minHeight the minimum height + * @param maxHeight the maximum height + * @param orientationAcross indicates if ridge is parallel to short side + * @param isGabled indicates if should calculate a gable + * @param specialParts element to add missing parts of underlying element + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcRidgeMesh(GeometryBuffer element, float minHeight, float maxHeight, boolean orientationAcross, boolean isGabled, GeometryBuffer specialParts) { + float[] points = element.points; + int[] index = element.index; + + for (int i = 0, pointPos = 0; i < index.length; i++) { + if (index[i] < 0) { + break; + } + if (i > 0) break; // Handle only first polygon + + int numPoints = index[i] / 2; + if (numPoints < 0) continue; + + if (numPoints < 4 || (!isGabled && orientationAcross)) { + calcPyramidalMesh(element, minHeight, maxHeight); + return true; + } + + List point3Fs = new ArrayList<>(); + + // Calculate center and load points + for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + } + + // Calc vectors + int groundSize = point3Fs.size(); + + List lengths = new ArrayList<>(); + List normVectors = GeometryUtils.normalizedVectors2D(point3Fs, lengths); + + List simpleAngles = getSimpleAngles(normVectors); + + Integer indexStart = getIndexStart(simpleAngles, lengths, orientationAcross); + + int countConcavAngles = 0; + for (Byte simpleAngle : simpleAngles) { + if (simpleAngle > 1) + countConcavAngles++; + } + + // Calc different mesh, if roof has no nearly right angle + if (indexStart == null) { + if (isGabled) + return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts); + else + return calcPyramidalMesh(element, minHeight, maxHeight); + } + + List bisections = getBisections(normVectors); + List intersections = new ArrayList<>(); + + // Calc intersection of bisection + for (int k = 0; k < groundSize; k++) { + int nextTurn = getIndexNextTurn(k, simpleAngles); + float[] pA = point3Fs.get(nextTurn); + float[] pB = point3Fs.get(k); + intersections.add(GeometryUtils.intersectionLines2D(pA, bisections.get(nextTurn), pB, bisections.get(k))); + } + + // Calc ridge points + TreeMap ridgePoints = new TreeMap<>(); + TreeMap ridgeLines = new TreeMap<>(); + HashSet gablePoints = new HashSet<>(); // Only used if gabled + Integer currentRidgeInd = null; + boolean isOdd = false; + for (int k = 0; k < groundSize; k++) { + int shift = (k + indexStart) % groundSize; + byte direction = simpleAngles.get(shift); + if (direction == 0) { + continue; // direction is similar to last one + } else if (direction > 0) { + // If shape turns right + float[] positionRidgeA = null; + float[] positionRidgeB = null; + + // Check two previous corners + Integer indexPrevious = getIndexPreviousLeftTurn(shift, simpleAngles); + Integer indexPrevious2 = getIndexPreviousLeftTurn(indexPrevious == null ? shift - 1 : indexPrevious, simpleAngles); + + if (indexPrevious != null && indexPrevious2 != null) { + // Write two previous + if (!ridgeLines.containsKey(indexPrevious2)) { + ridgeLines.put(indexPrevious2, normVectors.get(indexPrevious)); + } + + positionRidgeA = intersections.get(indexPrevious2); + currentRidgeInd = indexPrevious2; + if (isGabled) { + positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(indexPrevious2), point3Fs.get(indexPrevious2), normVectors.get(indexPrevious2)); + gablePoints.add(indexPrevious2); + } + ridgePoints.put(indexPrevious2, positionRidgeA); + + // Remove previous ridge, if exists + gablePoints.remove(indexPrevious); + ridgePoints.remove(indexPrevious); + ridgeLines.remove(indexPrevious); + } + + // Check two next corners + Integer indexNext = getIndexNextLeftTurn(shift, simpleAngles); + Integer indexNext2 = getIndexNextLeftTurn(indexNext == null ? shift + 1 : indexNext, simpleAngles); + + if (indexNext != null && indexNext2 != null) { + if (ridgePoints.get(indexNext) == null) { + // Write both next + if (!ridgeLines.containsKey(indexNext)) { + ridgeLines.put(indexNext, normVectors.get(indexNext2)); + } + positionRidgeB = intersections.get(indexNext); + + if (isGabled) { + positionRidgeB = GeometryUtils.intersectionLines2D(positionRidgeB, ridgeLines.get(indexNext), point3Fs.get(indexNext), normVectors.get(indexNext)); + gablePoints.add(indexNext); + } + ridgePoints.put(indexNext, positionRidgeB); + } else { + positionRidgeB = ridgePoints.get(indexNext); + } + } + + // Handle multiple concaves + if (positionRidgeA == null || positionRidgeB == null) { + if (positionRidgeA == null && positionRidgeB == null && currentRidgeInd != null) { + positionRidgeA = ridgePoints.get(currentRidgeInd); + } + if (positionRidgeA != null && positionRidgeB == null) { // Next index is concave + positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(currentRidgeInd), point3Fs.get(shift), bisections.get(shift)); + currentRidgeInd = shift; + addSnapRidgePoint(shift, positionRidgeA, ridgePoints); + ridgeLines.put(shift, normVectors.get(shift)); // Add ridgeLine, if concave + isOdd = false; + continue; + } else if (positionRidgeA == null && positionRidgeB != null) { // Previous index is concave + positionRidgeA = GeometryUtils.intersectionLines2D(positionRidgeB, ridgeLines.get(indexNext), point3Fs.get(shift), bisections.get(shift)); + addSnapRidgePoint(shift, positionRidgeA, ridgePoints); + currentRidgeInd = null; + isOdd = false; + continue; + } else { + log.debug("Should never happen, because positionRidge wouldn't be null then"); + currentRidgeInd = null; + continue; + } + } + + // Calc actual concave + if (currentRidgeInd == null || indexNext == null || ridgeLines.get(currentRidgeInd) == null || ridgeLines.get(indexNext) == null) { + log.debug("Concave shape not calculated correctly: " + element.toString()); + currentRidgeInd = null; + continue; + } + + float[] intersection = GeometryUtils.intersectionLines2D(positionRidgeA, ridgeLines.get(currentRidgeInd), positionRidgeB, ridgeLines.get(indexNext)); + addSnapRidgePoint(shift, intersection, ridgePoints); + + // Set opposite ridge, if only one concave corner + if (countConcavAngles == 1) { + Integer opposite = getIndexNextLeftTurn(indexNext2, simpleAngles); + if (opposite != null) { + if (isGabled) + gablePoints.remove(opposite); + ridgePoints.put(opposite, intersection); + } + } + + // Reset ridges + currentRidgeInd = null; + isOdd = false; + continue; + } + // Regular right angle (left turn) + if (isOdd) { + isOdd = false; + continue; + } + if (simpleAngles.get(shift) < -1) { + isOdd = true; + } + if (ridgePoints.containsKey(shift) && ridgeLines.containsKey(shift)) { + currentRidgeInd = shift; + continue; + } + if (currentRidgeInd != null) { + float[] intersection; + // If is gabled, then use the normal line as intersection instead of bisection, but if the angle is not right, this is usually not a gable point + if (isGabled && direction < -1) { + if (ridgePoints.get(currentRidgeInd) == null) { + log.debug("Gabled intersection calc failed"); + currentRidgeInd = null; + continue; + } + intersection = GeometryUtils.intersectionLines2D(ridgePoints.get(currentRidgeInd), ridgeLines.get(currentRidgeInd), point3Fs.get(shift), normVectors.get(shift)); + if (intersection == null) { + log.debug("Gabled intersection calc failed"); + currentRidgeInd = null; + continue; + } + gablePoints.add(shift); + ridgePoints.put(shift, intersection); + } else { + intersection = GeometryUtils.intersectionLines2D(ridgePoints.get(currentRidgeInd), ridgeLines.get(currentRidgeInd), point3Fs.get(shift), bisections.get(shift)); + addSnapRidgePoint(shift, intersection, ridgePoints); + } + if (isOdd) { + currentRidgeInd = null; + } else { + ridgeLines.put(shift, normVectors.get(shift)); + currentRidgeInd = shift; + } + } else { + Integer indexNext = getIndexNextLeftTurn(shift, simpleAngles); + if (indexNext == null) continue; + if (!ridgeLines.containsKey(shift)) { + ridgeLines.put(shift, normVectors.get(indexNext)); + } + currentRidgeInd = shift; + + float[] ridgePos = intersections.get(shift); + if (isGabled) { + ridgePos = GeometryUtils.intersectionLines2D(ridgePos, ridgeLines.get(currentRidgeInd), point3Fs.get(shift), normVectors.get(shift)); + gablePoints.add(shift); + } + addSnapRidgePoint(shift, ridgePos, ridgePoints); + } + } + + if (ridgePoints.isEmpty()) { + calcPyramidalMesh(element, minHeight, maxHeight); + return true; + } + + Iterator> ridgeIt = ridgePoints.entrySet().iterator(); + while (ridgeIt.hasNext()) { + Map.Entry ridgeEntry = ridgeIt.next(); + Integer key = ridgeEntry.getKey(); + if (ridgeEntry.getValue() == null) { + log.debug("Ridge calculation failed at point " + key); + ridgeIt.remove(); + continue; + } + + // Only remove ridgePoint at concave corners + if (!isGabled || simpleAngles.get(key) > 0) { + boolean isIn = GeometryUtils.pointInPoly(ridgeEntry.getValue()[0], ridgeEntry.getValue()[1], points, points.length, 0); + if (!isIn) { + // FIXME can improve shapes with concaves that intersect each other and remove shapes which have ridgepoints outside the outline + if (!IMPROVE_RIDGE_CALCULATION) { + if (isGabled) { + return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts); + } else return calcFlatMesh(element, minHeight); + } + } + } + } + + // Allocate the indices to the points + int ridgePointSize = ridgePoints.size(); + float[] meshPoints = new float[(groundSize + ridgePointSize) * 3]; //(ridgePoints * 3 = 6) + List meshVarIndex = new ArrayList<>(); + + // Add special building parts + List meshPartVarIndex = null; + if (isGabled && specialParts != null) { + meshPartVarIndex = new ArrayList<>(); + } + + for (int k = 0; k < groundSize; k++) { + // Add first face + float[] p = point3Fs.get(k); + int ridgePointIndex1 = k; + while (!ridgePoints.containsKey(ridgePointIndex1)) { + ridgePointIndex1 = (ridgePointIndex1 + groundSize - 1) % groundSize; // Decrease ridgePointIndex + } + int ridgeIndex1 = ridgePoints.headMap(ridgePointIndex1).size(); // set ridgeIndex to shift in ridgePoints + if (meshPartVarIndex != null && gablePoints.contains(ridgePointIndex1) && getIndexNextTurn(ridgePointIndex1, simpleAngles).equals(getIndexNextTurn(k, simpleAngles))) { + meshPartVarIndex.add(k); + meshPartVarIndex.add((k + 1) % groundSize); + meshPartVarIndex.add(ridgeIndex1 + groundSize); + } else { + meshVarIndex.add(k); + meshVarIndex.add((k + 1) % groundSize); + meshVarIndex.add(ridgeIndex1 + groundSize); + } + + // Add second face, if necessary + int ridgePointIndex2 = (k + 1) % groundSize; + while (!ridgePoints.containsKey(ridgePointIndex2)) { + ridgePointIndex2 = (ridgePointIndex2 + groundSize - 1) % groundSize; // Decrease ridgePointIndex + } + + if (ridgePointIndex2 != ridgePointIndex1) { + int ridgeIndex2 = ridgePoints.headMap(ridgePointIndex2).size(); // Set ridgeIndex to position in ridgePoints + meshVarIndex.add(ridgeIndex1 + groundSize); + meshVarIndex.add((k + 1) % groundSize); + meshVarIndex.add(ridgeIndex2 + groundSize); + } + + // Write points + meshPoints[3 * k + 0] = p[0]; + meshPoints[3 * k + 1] = p[1]; + meshPoints[3 * k + 2] = p[2]; + } + + // Tessellate top, if necessary (can be used to improve wrong rendered roofs) + if (ridgePointSize > 2) { + HashSet ridgeSkipFaceIndex = new HashSet<>(); + boolean isTessellateAble = true; + for (int k = 0; k < groundSize; k++) { + if (!isTessellateAble || ridgePoints.get(k) == null) continue; + Integer middle = null; + for (int m = k + 1; m <= k + groundSize; m++) { + int secIndex = m % groundSize; + if (ridgePoints.get(secIndex) == null) continue; + if (middle == null) { + middle = secIndex; + } else { + float isClockwise = GeometryUtils.isTrisClockwise(ridgePoints.get(k), ridgePoints.get(middle), ridgePoints.get(secIndex)); + if (Math.abs(isClockwise) < 0.001) { + ridgeSkipFaceIndex.add(middle); + if (Arrays.equals(ridgePoints.get(k), ridgePoints.get(secIndex))) + ridgeSkipFaceIndex.add(k); + } + if (isClockwise > 0 && IMPROVE_RIDGE_CALCULATION) { + // TODO Improve handling of counter clockwise faces and support multiple faces + isTessellateAble = false; + break; + //return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts); + } + break; + } + } + } + int faceLength = ridgePointSize - ridgeSkipFaceIndex.size(); + if (isTessellateAble && faceLength > 0) { + float[] gbPoints = new float[2 * faceLength]; + int k = 0; + List faceIndex = new ArrayList<>(); // Store used indices + for (int m = 0; m < groundSize; m++) { + float[] point = ridgePoints.get(m); + if (ridgeSkipFaceIndex.contains(m) || point == null) { + continue; + } + faceIndex.add(m); + gbPoints[2 * k] = point[0]; + gbPoints[2 * k + 1] = point[1]; + k++; + } + GeometryBuffer buffer = new GeometryBuffer(gbPoints, new int[]{2 * faceLength}); + if (Tessellator.tessellate(buffer, buffer) != 0) { + for (int ind : buffer.index) { + // Get position in ridgePoints, considering skipped points + meshVarIndex.add(ridgePoints.headMap(faceIndex.get(ind)).size() + groundSize); + } + } else { + // TODO Improve wrong or not tessellated faces + if (!IMPROVE_RIDGE_CALCULATION) { + if (isGabled) { + return calcSimpleGabledMesh(element, minHeight, maxHeight, orientationAcross, specialParts); + } else return calcFlatMesh(element, minHeight); + } + } + } + } + + + // Replace in Java 8 / min API 24 + int[] meshIndex = new int[meshVarIndex.size()]; // new int[(groundSize + ridgePointSize) * 3]; // 3 vertices per point + 6 vertices for left and right roof + for (int k = 0; k < meshIndex.length; k++) + meshIndex[k] = meshVarIndex.get(k); + + for (int k = 0, l = 0; k < groundSize; k++) { + // Add ridge points + float[] tmp = ridgePoints.get(k); + if (tmp != null) { + float[] p = new float[]{tmp[0], tmp[1], maxHeight}; + int ppos = 3 * (l + groundSize); + meshPoints[ppos + 0] = p[0]; + meshPoints[ppos + 1] = p[1]; + meshPoints[ppos + 2] = p[2]; + l++; + } + } + + // Add special parts e.g. for gabled roofs + if (specialParts != null && meshPartVarIndex != null) { + // Replace in Java 8 / min API 24 + int[] meshPartsIndex = new int[meshPartVarIndex.size()]; + for (int k = 0; k < meshPartsIndex.length; k++) + meshPartsIndex[k] = meshPartVarIndex.get(k); + + specialParts.points = meshPoints; + specialParts.index = meshPartsIndex; + specialParts.pointNextPos = meshPoints.length; + specialParts.type = GeometryBuffer.GeometryType.TRIS; + } + + element.points = meshPoints; + element.index = meshIndex; + element.pointNextPos = meshPoints.length; + element.type = GeometryBuffer.GeometryType.TRIS; + } + + return element.isTris(); + } + + /** + * Calculates a simple gabled mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @return true if calculation succeeded, false otherwise + */ + private static boolean calcSimpleGabledMesh(GeometryBuffer element, float minHeight, float maxHeight, boolean orientationAcross, GeometryBuffer specialParts) { + float[] points = element.points; + int[] index = element.index; + + for (int i = 0, pointPos = 0; i < index.length; i++) { + if (index[i] < 0) { + break; + } + if (i > 0) break; // Handle only first polygon + + int numPoints = index[i] / 2; + if (numPoints < 0) continue; + + if (numPoints < 4) { + calcPyramidalMesh(element, minHeight, maxHeight); + return true; + } + + List point3Fs = new ArrayList<>(); + + // Calculate center and load points + for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + } + + // Calc vectors + int groundSize = point3Fs.size(); + + List lengths = new ArrayList<>(); + List normVectors = GeometryUtils.normalizedVectors2D(point3Fs, lengths); + + List simpleAngles = getSimpleAngles(normVectors); + + int indexStart = getIndicesLongestSide(simpleAngles, lengths, null)[0]; + if (orientationAcross) { + Integer tmp = getIndexPreviousLeftTurn(indexStart, simpleAngles); + if (tmp == null) { + tmp = getIndexNextTurn(indexStart, simpleAngles); + } + indexStart = tmp; + } + float[] vL = normVectors.get(indexStart); + float[] pL = point3Fs.get(indexStart); + float[] splitLinePoint = null; + float maxDist = 0; + for (float[] point : point3Fs) { + float curDist = GeometryUtils.distancePointLine2D(point, pL, vL); + if (curDist > maxDist) { + maxDist = curDist; + splitLinePoint = point; // Farthest point from line + } + } + // Scale of normal vec + maxDist = Math.signum(GeometryUtils.isTrisClockwise( + pL, + GeometryUtils.sumVec(pL, vL), + splitLinePoint)) * (maxDist / 2); + + float[] normL = new float[]{-vL[1], vL[0]}; // Normal vec to line + normL = GeometryUtils.scale(normL, (float) (maxDist / Math.sqrt(GeometryUtils.dotProduct(normL, normL)))); // normalize vec + splitLinePoint = GeometryUtils.sumVec(pL, normL); + float degreeNormL = (float) Math.atan2(normL[0], -normL[1]) * MathUtils.radiansToDegrees; + + // Split polygon + int sideChange = 0; + List elementPoints1 = new ArrayList<>(); + List elementPoints2 = new ArrayList<>(); + float[] secSplitPoint = GeometryUtils.sumVec(splitLinePoint, vL); + float sideLastPoint = Math.signum(GeometryUtils.isTrisClockwise(splitLinePoint, secSplitPoint, point3Fs.get(groundSize - 1))); + degreeNormL = sideLastPoint > 0 ? degreeNormL : (degreeNormL + 180f) % 360; // Correct angle + List intersection1 = new ArrayList<>(), intersection2 = new ArrayList<>(); + for (int k = 0; k < groundSize; k++) { + // If point is not on the same side as the previous point, the split line intersect and can calc split point + float sideCurPoint = Math.signum(GeometryUtils.isTrisClockwise(splitLinePoint, secSplitPoint, point3Fs.get(k))); + if (sideCurPoint != sideLastPoint) { + if (sideChange > 2 && !IMPROVE_RIDGE_CALCULATION) + return calcFlatMesh(element, minHeight); // TODO Improve multiple side changes + int indexPrev = (k + groundSize - 1) % groundSize; + float[] intersection = GeometryUtils.intersectionLines2D(splitLinePoint, vL, point3Fs.get(indexPrev), normVectors.get(indexPrev)); + elementPoints1.add(intersection); + elementPoints2.add(intersection); + intersection1.add(elementPoints1.size() - 1); + intersection2.add(elementPoints2.size() - 1); + sideChange++; + } + if (sideChange % 2 == 0) { + elementPoints1.add(point3Fs.get(k)); + } else { + elementPoints2.add(point3Fs.get(k)); + } + sideLastPoint = sideCurPoint; + } + + GeometryBuffer geoEle1 = new GeometryBuffer(elementPoints1.size(), 1); + for (int k = 0; k < elementPoints1.size(); k++) { + geoEle1.points[2 * k] = elementPoints1.get(k)[0]; + geoEle1.points[2 * k + 1] = elementPoints1.get(k)[1]; + } + geoEle1.index[0] = geoEle1.points.length; + geoEle1.pointNextPos = geoEle1.points.length; + + GeometryBuffer geoEle2 = new GeometryBuffer(elementPoints2.size(), 1); + for (int k = 0; k < elementPoints2.size(); k++) { + geoEle2.points[2 * k] = elementPoints2.get(k)[0]; + geoEle2.points[2 * k + 1] = elementPoints2.get(k)[1]; + } + geoEle2.index[0] = geoEle2.points.length; + geoEle2.pointNextPos = geoEle2.points.length; + + GeometryBuffer specialParts1 = new GeometryBuffer(geoEle1); + GeometryBuffer specialParts2 = new GeometryBuffer(geoEle2); + if (!(calcSkillionMesh(geoEle1, minHeight, maxHeight, degreeNormL, specialParts1) + && calcSkillionMesh(geoEle2, minHeight, maxHeight, + degreeNormL + 180, specialParts2))) { + return false; + } + + // Adapt gable intersections to max height + for (Integer integer : intersection1) { + geoEle1.points[integer * 3 + 2] = maxHeight; + specialParts1.points[6 * integer + 5] = maxHeight; + } + for (Integer integer : intersection2) { + geoEle2.points[integer * 3 + 2] = maxHeight; + specialParts2.points[6 * integer + 5] = maxHeight; + } + + // Merge buffers + mergeMeshGeometryBuffer(geoEle1, geoEle2, element); + mergeMeshGeometryBuffer(specialParts1, specialParts2, specialParts); + return true; + } + return false; + } + + /** + * Calculates a skillion mesh of a Poly-GeometryBuffer. + * + * @param element the GeometryBuffer which is used to write the 3D mesh + * @param roofDegree the direction of slope + * @param specialParts the GeometryBuffer which is used to write the additional building parts + * @return true if calculation succeeded, false otherwise + */ + public static boolean calcSkillionMesh(GeometryBuffer element, float minHeight, float maxHeight, float roofDegree, GeometryBuffer specialParts) { + float[] points = element.points; + int[] index = element.index; + + for (int i = 0, pointPos = 0; i < index.length; i++) { + if (index[i] < 0) { + break; + } + if (i > 0) break; // May add inner rings + + int numPoints = index[i] / 2; + if (numPoints < 0) continue; + + List point3Fs = new ArrayList<>(); + for (int j = 0; j < (numPoints * 2); j += 2, pointPos += 2) { + float x = points[pointPos]; + float y = points[pointPos + 1]; + + point3Fs.add(new float[]{x, y, minHeight}); + } + + boolean hasOutlines = calcOutlines(specialParts, minHeight, maxHeight); + + // Use 3 points that match the angle the best and use as plane + float[] min = null, max1 = null, max2 = null; + float minDif = Float.MAX_VALUE, maxDif1 = 0, maxDif2 = 0; + if (calcFlatMesh(element, maxHeight)) { + // To positive radian + roofDegree = ((MathUtils.degreesToRadians * roofDegree) + MathUtils.PI2) % MathUtils.PI2; + float[] vRidge = new float[2]; + vRidge[0] = (float) Math.sin(roofDegree); + vRidge[1] = (float) -Math.cos(roofDegree); + vRidge = GeometryUtils.scale(vRidge, 100000000); // Use very large value, so the distances are nearly parallel + + for (int k = 0; k < point3Fs.size(); k++) { + float[] point = point3Fs.get(k); + float vx = vRidge[0] - point[0]; + float vy = vRidge[1] - point[1]; + float currentDiff = (float) Math.sqrt(vx * vx + vy * vy); + if (max1 == null || currentDiff > maxDif1) { + if (max1 != null) { + max2 = max1; + maxDif2 = maxDif1; + } + max1 = point; + maxDif1 = currentDiff; + } else if (max2 == null || currentDiff > maxDif2) { + max2 = point; + maxDif2 = currentDiff; + } + if (min == null || currentDiff < minDif) { + min = point; + minDif = currentDiff; + } + } + if (min == max1) return false; + + // Calc intersection points of ground points with plane of the three points + float[] zVector = new float[]{0, 0, 1}; // Calc only height intersection + min[2] = minHeight; + max1[2] = max2[2] = maxHeight; + float[] normal = GeometryUtils.normalOfPlane(min, max1, max2); + for (int k = 0; k < point3Fs.size(); k++) { + float[] intersection = GeometryUtils.intersectionLinePlane(point3Fs.get(k), zVector, min, normal); + if (intersection == null) return false; + intersection[2] = intersection[2] > (2 * maxHeight) ? maxHeight : (intersection[2] < minHeight ? minHeight : intersection[2]); + element.points[3 * k + 2] = intersection[2]; + if (hasOutlines) { + specialParts.points[6 * k + 5] = intersection[2]; // Every sixth point is height of k + } + } + + return true; + } + } + + return false; + } + + /** + * @return the bisections of vectors + */ + private static List getBisections(List normVectors) { + int size = normVectors.size(); + List bisections = new ArrayList<>(); + // Calc bisections + for (int k = 0; k < size; k++) { + float[] vBC = normVectors.get((k + size - 1) % size); + float[] vBA = normVectors.get(k); + + // Change direction to get correct angle + vBC = Arrays.copyOf(vBC, vBC.length); + vBC[0] = -vBC[0]; + vBC[1] = -vBC[1]; + + // Calc bisection + bisections.add(GeometryUtils.bisectionNorm2D(vBC, vBA)); + } + return bisections; + } + /** * @param color the color as string (see http://wiki.openstreetmap.org/wiki/Key:colour) * @param roof declare if color is used for roofs @@ -87,6 +1107,140 @@ public final class S3DBUtils { return 0; } + /** + * @return the index of next left turn after specified index + */ + private static Integer getIndexNextLeftTurn(int index, List simpleAngles) { + for (int i = index + 1; i < simpleAngles.size() + index; i++) { + int iMod = i % simpleAngles.size(); + if (simpleAngles.get(iMod) < 0) { + return iMod; + } else if (simpleAngles.get(iMod) > 0) { + return null; + } + } + return (index + 1) % simpleAngles.size(); + } + + /** + * @return the index of next turn after specified index + */ + private static Integer getIndexNextTurn(int index, List simpleAngles) { + for (int i = index + 1; i < simpleAngles.size() + index; i++) { + int iMod = i % simpleAngles.size(); + if (simpleAngles.get(iMod) != 0) { + return iMod; + } + } + return (index + 1) % simpleAngles.size(); + } + + /** + * @return the index of previous left turn at specified index + */ + private static Integer getIndexPreviousLeftTurn(int index, List simpleAngles) { + for (int i = simpleAngles.size() + index - 1; i >= 0; i--) { + int iMod = i % simpleAngles.size(); + if (simpleAngles.get(iMod) < 0) { + return iMod; + } else if (simpleAngles.get(iMod) > 0) { + return null; + } + } + return (simpleAngles.size() + index - 1) % simpleAngles.size(); + } + + /** + * @return the best index to begin a calculation + */ + private static Integer getIndexStart(List simpleAngles, List lengths, boolean directionAcross) { + int size = simpleAngles.size(); + Integer indexStart = null; + Integer concaveStart = null; + for (int i = 0; i < size; i++) { + if (indexStart != null && concaveStart != null) break; + if (indexStart == null && simpleAngles.get(i) < -1) { + // Use first angle as start index; + indexStart = i; + } else if (concaveStart == null && simpleAngles.get(i) > 1) { + // A real concave corner + concaveStart = i; + } + } + + if (indexStart == null) { + return null; + } + + if (concaveStart != null) { + // look for next convex shape (point) + for (int i = concaveStart; i < size + indexStart; i++) { + if (simpleAngles.get(i % size) < 0) { + return i % size; + } + } + } + + // Calculate longest side with right angle next to it. + int[] iLongSide = getIndicesLongestSide(simpleAngles, lengths, indexStart); + if (simpleAngles.get(iLongSide[1]) > -2) { + // If angle is not good to start a ridge use previous + indexStart = getIndexPreviousLeftTurn(iLongSide[0], simpleAngles); + } else { + indexStart = iLongSide[1]; // Get side next to longest one + } + + // Direction across only possible if no concave shapes are present + if (directionAcross) { + return iLongSide[0]; + } + + return indexStart; + } + + /** + * @param indexStart the start index, if already calculated (can be null) + * @return int[0] = start index, int[1] = end index + */ + private static int[] getIndicesLongestSide(List simpleAngles, List lengths, Integer indexStart) { + int[] iLongSide = new int[2]; + int size = simpleAngles.size(); + if (indexStart == null) { + for (int i = 0; i < size; i++) { + if (simpleAngles.get(i) < 0) { + // Use first angle as start index; + indexStart = i; + break; + } + } + } + + float longestSideLength = 0, currentLength = 0; + int indexCurrentSide = indexStart; + int loopSize = size + indexStart; + for (int i = indexStart; i < loopSize; i++) { + if (i >= size) { + i -= size; + loopSize -= size; + } + + if (simpleAngles.get(i) != 0) { + // Right angle + currentLength = lengths.get(i); + indexCurrentSide = i; + } else { + currentLength += lengths.get(i); + } + + if (currentLength > longestSideLength) { + longestSideLength = currentLength; + iLongSide[0] = indexCurrentSide; + iLongSide[1] = (i + 1) % size; + } + } + return iLongSide; + } + /** * @param material the material as string (see http://wiki.openstreetmap.org/wiki/Key:material and following pages) * @param roof declare if material is used for roofs @@ -148,6 +1302,106 @@ public final class S3DBUtils { return 0; } + /** + * @param normVectors the normalized vectors + * @return a list of simple angles: + * 0 straight + * (+/-) 1 (right/left) obtuse angle + * (+/-) 2 (right/left) right angle (or acute angle) + */ + private static List getSimpleAngles(List normVectors) { + int size = normVectors.size(); + // List angles = new ArrayList<>(); + List simpAngls = new ArrayList<>(); + float tmpAnlgeSum = 0; + float threshold = MathUtils.PI / 12; + for (int k = 0; k < size; k++) { + // Check angle between next and this vector + float[] v2 = normVectors.get(k); + float[] v1 = normVectors.get((k - 1 + size) % size); + float val = v1[0] * v2[0] + v1[1] * v2[1]; + float angle = (float) Math.acos(Math.abs(val) > 1 ? Math.signum(val) : val); + // angles.add(angle); + + // Positive is turns right, negative turns left + byte simpAngle = (byte) Math.signum(v1[0] * (-v2[1]) + v1[1] * v2[0]); + if (angle > (MathUtils.PI / 2) - threshold) { + // Right angle + simpAngle *= 2; + tmpAnlgeSum = 0; + } else if (angle < threshold) { + tmpAnlgeSum += simpAngle * angle; // Many small angles, indicate a corner + if (Math.abs(tmpAnlgeSum) > threshold) { + // Can improve sum of concave/convex shapes + simpAngle = (byte) Math.signum(tmpAnlgeSum); + tmpAnlgeSum = 0; + } else + simpAngle = 0; + } else { + // Angle which is not right, and not straight + tmpAnlgeSum = 0; + } + + simpAngls.add(simpAngle); + } + + return simpAngls; + } + + private static GeometryBuffer initCircleMesh(float[][] circleShape, int numSections) { + int indexSize = numSections * (circleShape.length - 1) * 2 * 3; // * 2 faces * 3 vertices + int[] meshIndex = new int[indexSize]; + + int meshSize = numSections * circleShape.length; + float[] meshPoints = new float[meshSize * 3]; + for (int i = 0; i < numSections; i++) { + for (int j = 0; j < circleShape.length; j++) { + // Write point mesh + int pPos = 3 * (i * circleShape.length + j); + meshPoints[pPos + 0] = circleShape[j][0]; + meshPoints[pPos + 1] = circleShape[j][1]; + meshPoints[pPos + 2] = circleShape[j][2]; + + // Write point indices + if (j != circleShape.length - 1) { + int iPos = 6 * (i * (circleShape.length - 1) + j); // 6 = 2 * Mesh * 3PointsPerMesh + pPos = pPos / 3; + meshIndex[iPos + 2] = pPos + 0; + meshIndex[iPos + 1] = pPos + 1; + meshIndex[iPos + 0] = (pPos + circleShape.length) % meshSize; + + // FIXME if is last point, only one tris is needed, if top shape is closed + meshIndex[iPos + 5] = pPos + 1; + meshIndex[iPos + 4] = (pPos + circleShape.length + 1) % meshSize; + meshIndex[iPos + 3] = (pPos + circleShape.length) % meshSize; + } + } + + } + + return new GeometryBuffer(meshPoints, meshIndex); + } + + private static void mergeMeshGeometryBuffer(GeometryBuffer gb1, GeometryBuffer gb2, GeometryBuffer out) { + if (!(gb1.isTris() && gb2.isTris())) return; + int gb1PointSize = gb1.points.length; + float[] mergedPoints = new float[gb1PointSize + gb2.points.length]; + System.arraycopy(gb1.points, 0, mergedPoints, 0, gb1PointSize); + System.arraycopy(gb2.points, 0, mergedPoints, gb1PointSize, gb2.points.length); + out.points = mergedPoints; + out.pointNextPos = mergedPoints.length; + + int gb1IndexSize = gb1.index.length; + int[] mergedIndices = new int[gb1IndexSize + gb2.index.length]; + System.arraycopy(gb1.index, 0, mergedIndices, 0, gb1IndexSize); + gb1PointSize /= 3; // (x,y,z) + for (int k = 0; k < gb2.index.length; k++) { + mergedIndices[gb1IndexSize + k] = gb2.index[k] + gb1PointSize; + } + out.index = mergedIndices; + out.type = gb1.type; + } + private S3DBUtils() { } } diff --git a/vtm/src/org/oscim/renderer/bucket/ExtrusionBucket.java b/vtm/src/org/oscim/renderer/bucket/ExtrusionBucket.java index 3b3e1b93..9f2ef80f 100644 --- a/vtm/src/org/oscim/renderer/bucket/ExtrusionBucket.java +++ b/vtm/src/org/oscim/renderer/bucket/ExtrusionBucket.java @@ -20,6 +20,7 @@ package org.oscim.renderer.bucket; import org.oscim.backend.canvas.Color; import org.oscim.core.GeometryBuffer; import org.oscim.core.Tile; +import org.oscim.utils.ExtrusionUtils; import org.oscim.utils.FastMath; import org.oscim.utils.KeyMap; import org.oscim.utils.KeyMap.HashItem; @@ -361,13 +362,9 @@ public class ExtrusionBucket extends RenderBucket { float[] points = element.points; /* 10 cm steps */ - float sfactor = 1 / 10f; - height *= sfactor; - minHeight *= sfactor; - - /* match height with ground resultion (meter per pixel) */ - height /= mGroundResolution; - minHeight /= mGroundResolution; + /* match height with ground resolution (meter per pixel) */ + height = ExtrusionUtils.mapGroundScale(height, mGroundResolution); + minHeight = ExtrusionUtils.mapGroundScale(minHeight, mGroundResolution); boolean complexOutline = false; boolean simpleOutline = true; diff --git a/vtm/src/org/oscim/utils/ExtrusionUtils.java b/vtm/src/org/oscim/utils/ExtrusionUtils.java new file mode 100644 index 00000000..1e1c74be --- /dev/null +++ b/vtm/src/org/oscim/utils/ExtrusionUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012, 2013 Hannes Janetzek + * Copyright 2018 Gustl22 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.utils; + +import org.oscim.core.GeometryBuffer; +import org.oscim.core.Tile; + +/** + * A utility class with extrusion helper methods. + */ +public final class ExtrusionUtils { + + public static final float REF_TILE_SIZE = 4096.0f; // Standard ref tile size + + /** + * @param value the height value + * @param groundScale the ground scale of tile + * @return the corresponding height in the specific tile + */ + public static float mapGroundScale(float value, float groundScale) { + /* match height with ground resolution (meter per pixel) */ + return value / (groundScale * 10); // 10 cm steps + } + + /** + * Map the raw buffer scale to scale of coordinates + */ + public static void mapPolyCoordScale(GeometryBuffer buffer) { + float tileScale = REF_TILE_SIZE / Tile.SIZE; + float[] points = buffer.points; + for (int pPos = 0; pPos < buffer.pointNextPos; pPos++) { + points[pPos] = points[pPos] * tileScale; + } + } + + private ExtrusionUtils() { + } +} diff --git a/vtm/src/org/oscim/utils/geom/GeometryUtils.java b/vtm/src/org/oscim/utils/geom/GeometryUtils.java index c4cf3f99..0eeb7d37 100644 --- a/vtm/src/org/oscim/utils/geom/GeometryUtils.java +++ b/vtm/src/org/oscim/utils/geom/GeometryUtils.java @@ -20,6 +20,7 @@ package org.oscim.utils.geom; import java.util.ArrayList; import java.util.List; +// TODO Utils can be improved e.g. by avoiding object creations public final class GeometryUtils { private GeometryUtils() { @@ -101,19 +102,19 @@ public final class GeometryUtils { if (bisection[0] == 0 && bisection[1] == 0) { // 90 degree to v1 bisection[0] = v1[1]; - bisection[1] = v1[0]; + bisection[1] = -v1[0]; } return bisection; } /** * @param a first vector - * @param b second vector (same length as a) + * @param b second vector * @return a - b */ public static float[] diffVec(float[] a, float[] b) { - float[] diff = new float[a.length]; - for (int i = 0; i < a.length; i++) { + float[] diff = new float[Math.min(a.length, b.length)]; + for (int i = 0; i < diff.length; i++) { diff[i] = a[i] - b[i]; } return diff; @@ -121,12 +122,12 @@ public final class GeometryUtils { /** * @param a first vector - * @param b second vector (same length as a) + * @param b second vector * @return a + b */ public static float[] sumVec(float[] a, float[] b) { - float[] add = new float[a.length]; - for (int i = 0; i < a.length; i++) { + float[] add = new float[Math.min(a.length, b.length)]; + for (int i = 0; i < add.length; i++) { add[i] = b[i] + a[i]; } return add; @@ -182,6 +183,18 @@ public final class GeometryUtils { return Math.sqrt(dx * dx + dy * dy); } + /** + * @param pP point + * @param pL point of line + * @param vL vector of line + * @return the minimum distance between line and point + */ + public static float distancePointLine2D(float[] pP, float[] pL, float[] vL) { + float[] vPL = diffVec(pL, pP); + float[] vPS = diffVec(vPL, scale(vL, dotProduct(vPL, vL))); + return (float) Math.sqrt(dotProduct(vPS, vPS)); + } + public static double dotProduct(float[] p, int a, int b, int c) { double ux = (p[b] - p[a]); @@ -260,14 +273,11 @@ public final class GeometryUtils { } /** - * @return more than 0 if points of triangle are clockwise, 0 if triangle is a line, - * else less than 0. + * @return a positive value, if pA-pB-pC makes a counter-clockwise turn, + * negative for clockwise turn, and zero if the points are collinear. */ public static float isTrisClockwise(float[] pA, float[] pB, float[] pC) { - float v = (pB[0] - pA[0]) * (pB[1] + pA[1]); - v += (pC[0] - pB[0]) * (pC[1] + pB[1]); - v += (pA[0] - pC[0]) * (pA[1] + pC[1]); - return v; + return (pB[0] - pA[0]) * (pC[1] - pA[1]) - (pB[1] - pA[1]) * (pC[0] - pA[0]); } /**