001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Shape; 007import java.awt.geom.Area; 008import java.awt.geom.Ellipse2D; 009import java.awt.geom.Path2D; 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.List; 014 015/** 016 * A class to generate geometry for a vector tile 017 * @author Taylor Smock 018 * @since 17862 019 */ 020public class Geometry { 021 final Collection<Shape> shapes = new ArrayList<>(); 022 023 /** 024 * Create a {@link Geometry} for a {@link Feature} 025 * @param geometryType The type of geometry 026 * @param commands The commands used to create the geometry 027 * @throws IllegalArgumentException if arguments are not understood or if the shoelace formula returns 0 for a polygon ring. 028 */ 029 public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) { 030 if (geometryType == GeometryTypes.POINT) { 031 for (CommandInteger command : commands) { 032 final short[] operations = command.getOperations(); 033 // Each MoveTo command is a new point 034 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 035 for (int i = 0; i < operations.length / 2; i++) { 036 // Just using Ellipse2D since it extends Shape 037 shapes.add(new Ellipse2D.Float(operations[2 * i], operations[2 * i + 1], 0, 0)); 038 } 039 } else { 040 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 041 } 042 } 043 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 044 Path2D.Float line = null; 045 Area area = null; 046 // MVT uses delta encoding. Each feature starts at (0, 0). 047 int x = 0; 048 int y = 0; 049 // Area is used to determine the inner/outer of a polygon 050 final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath) 051 .mapToInt(command -> command.getOperations().length).sum(); 052 final List<Integer> xArray = new ArrayList<>(maxArraySize); 053 final List<Integer> yArray = new ArrayList<>(maxArraySize); 054 for (CommandInteger command : commands) { 055 final short[] operations = command.getOperations(); 056 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 057 if (command.getType() == Command.MoveTo && operations.length == 2) { 058 x += operations[0]; 059 y += operations[1]; 060 line = new Path2D.Float(); 061 line.moveTo(x, y); 062 xArray.add(x); 063 yArray.add(y); 064 shapes.add(line); 065 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 066 for (int i = 0; i < operations.length / 2; i++) { 067 x += operations[2 * i]; 068 y += operations[2 * i + 1]; 069 xArray.add(x); 070 yArray.add(y); 071 line.lineTo(x, y); 072 } 073 // ClosePath should only be used with Polygon geometry 074 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 075 shapes.remove(line); 076 // new Area() closes the line if it isn't already closed 077 if (area == null) { 078 area = new Area(); 079 shapes.add(area); 080 } 081 082 final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), 083 yArray.stream().mapToInt(i -> i).toArray()); 084 Area nArea = new Area(line); 085 // SonarLint thinks that this is never > 0. It can be. 086 if (areaAreaSq > 0) { 087 area.add(nArea); 088 } else if (areaAreaSq < 0) { 089 area.exclusiveOr(nArea); 090 } else { 091 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 092 } 093 xArray.clear(); 094 yArray.clear(); 095 } else { 096 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 097 } 098 } 099 } 100 } 101 102 /** 103 * This is also known as the "shoelace formula". 104 * @param xArray The array of x coordinates 105 * @param yArray The array of y coordinates 106 * @return The area of the object 107 * @throws IllegalArgumentException if the array lengths are not equal 108 */ 109 static double calculateSurveyorsArea(int[] xArray, int[] yArray) { 110 if (xArray.length != yArray.length) { 111 throw new IllegalArgumentException("Cannot calculate areas when arrays are uneven"); 112 } 113 // Lines have no area 114 if (xArray.length < 3) { 115 return 0; 116 } 117 int area = 0; 118 // Do the non-special stuff first (x0 * y1 - x1 * y0) 119 for (int i = 0; i < xArray.length - 1; i++) { 120 area += xArray[i] * yArray[i + 1] - xArray[i + 1] * yArray[i]; 121 } 122 // Now calculate the edges (xn * y0 - x0 * yn) 123 area += xArray[xArray.length - 1] * yArray[0] - xArray[0] * yArray[yArray.length - 1]; 124 return area / 2d; 125 } 126 127 /** 128 * Get the shapes to draw this geometry with 129 * @return A collection of shapes 130 */ 131 public Collection<Shape> getShapes() { 132 return Collections.unmodifiableCollection(this.shapes); 133 } 134}