Add length/area units and CEL expression geometry accessors (#1084)

pull/1088/head
Michael Barry 2024-11-04 18:06:29 -05:00 zatwierdzone przez GitHub
rodzic 6e7de64645
commit 2c4062db1a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
12 zmienionych plików z 1041 dodań i 275 usunięć

Wyświetl plik

@ -0,0 +1,214 @@
package com.onthegomap.planetiler.geo;
import com.onthegomap.planetiler.util.ToDoubleFunctionThatThrows;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import systems.uom.common.USCustomary;
/** Units of length and area measurement based off of constants defined in {@link USCustomary}. */
public interface Unit {
Pattern EXTRA_CHARS = Pattern.compile("[^a-z]+");
Pattern TRAILING_S = Pattern.compile("s$");
private static <T extends Unit> Map<String, T> index(T[] values) {
return Arrays.stream(values)
.flatMap(unit -> Stream.concat(unit.symbols().stream(), Stream.of(unit.unitName(), unit.toString()))
.map(label -> Map.entry(normalize(label), unit))
.distinct())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static String normalize(String unit) {
String result = EXTRA_CHARS.matcher(unit.toLowerCase()).replaceAll("");
return TRAILING_S.matcher(result).replaceAll("");
}
/** The {@link Base} measurement this unit is based off of. */
Base base();
/** Computes the size of {@code geometry} in this unit. */
double of(WithGeometry geometry);
/** The aliases for this unit. */
List<String> symbols();
/** The full name for this unit. */
String unitName();
/** Converts a measurement in {@link Base} units to this unit. */
double fromBaseUnit(double base);
/** The base units that all other units are derived from. */
enum Base {
/** Size of a feature in "z0 tiles" where 1=the length/width/area entire world. */
Z0_TILE(
WithGeometry::length,
WithGeometry::area),
/** Size of a feature in meters. */
METER(
WithGeometry::lengthMeters,
WithGeometry::areaMeters);
private final ToDoubleFunctionThatThrows<WithGeometry> area;
private final ToDoubleFunctionThatThrows<WithGeometry> length;
Base(ToDoubleFunctionThatThrows<WithGeometry> length, ToDoubleFunctionThatThrows<WithGeometry> area) {
this.length = length;
this.area = area;
}
public double area(WithGeometry geometry) {
return area.applyAndWrapException(geometry);
}
public double length(WithGeometry geometry) {
return length.applyAndWrapException(geometry);
}
}
/** Units to measure line length. */
enum Length implements Unit {
METER(USCustomary.METER, "m"),
FOOT(USCustomary.FOOT, "ft", "feet"),
YARD(USCustomary.YARD, "yd"),
NAUTICAL_MILE(USCustomary.NAUTICAL_MILE, "nm"),
MILE(USCustomary.MILE, "mi", "miles"),
KILOMETER(Base.METER, 1e-3, List.of("km"), "Kilometer"),
Z0_PIXEL(Base.Z0_TILE, 1d / 256, List.of("z0_px"), "Z0 Pixel"),
Z0_TILE(Base.Z0_TILE, 1d, List.of("z0_ti"), "Z0 Tile");
private static final Map<String, Length> NAMES = index(values());
private final Base base;
private final double multiplier;
private final List<String> symbols;
private final String name;
Length(Base base, double multiplier, List<String> symbols, String name) {
this.base = base;
this.multiplier = multiplier;
this.symbols = Stream.concat(symbols.stream(), Stream.of(name, name())).distinct().toList();
this.name = name;
}
Length(javax.measure.Unit<javax.measure.quantity.Length> from, String... alias) {
this(Base.METER, USCustomary.METER.getConverterTo(from).convert(1d), List.of(alias), from.getName());
}
public static Length from(String label) {
Length unit = NAMES.get(normalize(label));
if (unit == null) {
throw new IllegalArgumentException("Could not find area unit for '%s'".formatted(label));
}
return unit;
}
@Override
public double fromBaseUnit(double i) {
return i * multiplier;
}
@Override
public Base base() {
return base;
}
@Override
public double of(WithGeometry geometry) {
return fromBaseUnit(base.length.applyAndWrapException(geometry));
}
@Override
public List<String> symbols() {
return symbols;
}
@Override
public String unitName() {
return name;
}
}
/** Units to measure polygon areas. */
enum Area implements Unit {
SQUARE_METER(Length.METER),
SQUARE_FOOT(Length.FOOT),
SQUARE_YARD(Length.YARD),
SQUARE_NAUTICAL_MILE(Length.NAUTICAL_MILE),
SQUARE_MILE(Length.MILE),
SQUARE_KILOMETER(Length.KILOMETER),
SQUARE_Z0_PIXEL(Length.Z0_PIXEL),
SQUARE_Z0_TILE(Length.Z0_TILE),
ARE(USCustomary.ARE, "a"),
HECTARE(USCustomary.HECTARE, "ha"),
ACRE(USCustomary.ACRE, "ac");
private static final Map<String, Area> NAMES = index(values());
private final Base base;
private final double multiplier;
private final List<String> symbols;
private final String name;
Area(Base base, double multiplier, List<String> symbols, String name) {
this.base = base;
this.multiplier = multiplier;
this.symbols = symbols;
this.name = name;
}
Area(Length length) {
this(length.base, length.multiplier * length.multiplier,
length.symbols().stream().flatMap(symbol -> Stream.of(
"s" + symbol,
"square " + symbol,
"sq " + symbol,
symbol + "2"
)).toList(),
"Square " + length.name);
}
Area(javax.measure.Unit<javax.measure.quantity.Area> area, String... symbols) {
this(Base.METER, USCustomary.ARE.getConverterTo(area).convert(0.01d), List.of(symbols), area.getName());
}
public static Area from(String label) {
Area unit = NAMES.get(normalize(label));
if (unit == null) {
throw new IllegalArgumentException("Could not find area unit for '%s'".formatted(label));
}
return unit;
}
@Override
public double fromBaseUnit(double base) {
return base * multiplier;
}
@Override
public Base base() {
return base;
}
@Override
public double of(WithGeometry geometry) {
return fromBaseUnit(base.area.applyAndWrapException(geometry));
}
@Override
public List<String> symbols() {
return symbols;
}
@Override
public String unitName() {
return name;
}
}
}

Wyświetl plik

@ -0,0 +1,344 @@
package com.onthegomap.planetiler.geo;
import com.onthegomap.planetiler.reader.WithGeometryType;
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Lineal;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.Polygonal;
import org.locationtech.jts.geom.Puntal;
/**
* Wraps a geometry and provides cached accessor methods for applying transformations and transforming to lat/lon.
* <p>
* All geometries except for {@link #latLonGeometry()} return elements in world web mercator coordinates where (0,0) is
* the northwest corner and (1,1) is the southeast corner of the planet.
*/
public abstract class WithGeometry implements WithGeometryType {
private Geometry centroid = null;
private Geometry pointOnSurface = null;
private Geometry centroidIfConvex = null;
private double innermostPointTolerance = Double.NaN;
private Geometry innermostPoint = null;
private Geometry linearGeometry = null;
private Geometry polygonGeometry = null;
private Geometry validPolygon = null;
private double area = Double.NaN;
private double length = Double.NaN;
private double areaMeters = Double.NaN;
private double lengthMeters = Double.NaN;
private LineSplitter lineSplitter;
/**
* Returns a geometry in world web mercator coordinates.
*
* @return the geometry in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
public abstract Geometry worldGeometry() throws GeometryException;
/**
* Returns this geometry in latitude/longitude degree coordinates.
*
* @return the latitude/longitude geometry
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
public abstract Geometry latLonGeometry() throws GeometryException;
/**
* Returns and caches the result of {@link Geometry#getArea()} of this feature in world web mercator coordinates where
* {@code 1} means the area of the entire planet.
*/
public double area() throws GeometryException {
return Double.isNaN(area) ? (area = canBePolygon() ? Math.abs(polygon().getArea()) : 0) : area;
}
/**
* Returns and caches the result of {@link Geometry#getLength()} of this feature in world web mercator coordinates
* where {@code 1} means the circumference of the entire planet or the distance from 85 degrees north to 85 degrees
* south.
*/
public double length() throws GeometryException {
return Double.isNaN(length) ? (length =
(isPoint() || canBePolygon() || canBeLine()) ? worldGeometry().getLength() : 0) : length;
}
/**
* Returns the sqrt of {@link #area()} if polygon or {@link #length()} if a line string.
*/
public double size() throws GeometryException {
return canBePolygon() ? Math.sqrt(Math.abs(area())) : canBeLine() ? length() : 0;
}
/** Returns the approximate area of the geometry in square meters. */
public double areaMeters() throws GeometryException {
return Double.isNaN(areaMeters) ? (areaMeters =
(isPoint() || canBePolygon() || canBeLine()) ? GeoUtils.areaInMeters(latLonGeometry()) : 0) : areaMeters;
}
/** Returns the approximate length of the geometry in meters. */
public double lengthMeters() throws GeometryException {
return Double.isNaN(lengthMeters) ? (lengthMeters =
(isPoint() || canBePolygon() || canBeLine()) ? GeoUtils.lengthInMeters(latLonGeometry()) : 0) : lengthMeters;
}
/** Returns the sqrt of {@link #areaMeters()} if polygon or {@link #lengthMeters()} if a line string. */
public double sizeMeters() throws GeometryException {
return canBePolygon() ? Math.sqrt(Math.abs(areaMeters())) : canBeLine() ? lengthMeters() : 0;
}
/** Returns the length of this geometry in units of {@link Unit.Length}. */
public double length(Unit.Length length) {
return length.of(this);
}
/**
* Returns the length of this geometry if it is a line or the square root of the area if it is a polygon in units of
* {@link Unit.Length}.
*/
public double size(Unit.Length length) {
return canBePolygon() ? Math.sqrt(length.base().area(this)) : length.base().length(this);
}
/** Returns the area of this geometry in units of {@link Unit.Area}. */
public double area(Unit.Area area) {
return area.of(this);
}
/** Returns and caches {@link Geometry#getCentroid()} of this geometry in world web mercator coordinates. */
public final Geometry centroid() throws GeometryException {
return centroid != null ? centroid : (centroid =
canBePolygon() ? polygon().getCentroid() :
canBeLine() ? line().getCentroid() :
worldGeometry().getCentroid());
}
/** Returns and caches {@link Geometry#getInteriorPoint()} of this geometry in world web mercator coordinates. */
public final Geometry pointOnSurface() throws GeometryException {
return pointOnSurface != null ? pointOnSurface : (pointOnSurface =
canBePolygon() ? polygon().getInteriorPoint() :
canBeLine() ? line().getInteriorPoint() :
worldGeometry().getInteriorPoint());
}
/**
* Returns {@link MaximumInscribedCircle#getCenter()} of this geometry in world web mercator coordinates.
*
* @param tolerance precision for calculating maximum inscribed circle. 0.01 means 1% of the square root of the area.
* Smaller values for a more precise tolerance become very expensive to compute. Values between
* 0.05-0.1 are a good compromise of performance vs. precision.
*/
public final Geometry innermostPoint(double tolerance) throws GeometryException {
if (canBePolygon()) {
// cache as long as the tolerance hasn't changed
if (tolerance != innermostPointTolerance || innermostPoint == null) {
innermostPoint = MaximumInscribedCircle.getCenter(polygon(), Math.sqrt(area()) * tolerance);
innermostPointTolerance = tolerance;
}
return innermostPoint;
} else if (canBeLine()) {
return lineMidpoint();
} else {
return pointOnSurface();
}
}
/**
* Returns the midpoint of this line, or the longest segment if it is a multilinestring.
*/
public final Geometry lineMidpoint() throws GeometryException {
if (innermostPoint == null) {
innermostPoint = pointAlongLine(0.5);
}
return innermostPoint;
}
/**
* Returns along this line where {@code ratio=0} is the start {@code ratio=1} is the end and {@code ratio=0.5} is the
* midpoint.
* <p>
* When this is a multilinestring, the longest segment is used.
*/
public final Geometry pointAlongLine(double ratio) throws GeometryException {
if (lineSplitter == null) {
var line = line();
lineSplitter = new LineSplitter(line instanceof MultiLineString multi ? GeoUtils.getLongestLine(multi) : line);
}
return lineSplitter.get(ratio);
}
private Geometry computeCentroidIfConvex() throws GeometryException {
if (!canBePolygon()) {
return centroid();
} else if (polygon() instanceof Polygon poly &&
poly.getNumInteriorRing() == 0 &&
GeoUtils.isConvex(poly.getExteriorRing())) {
return centroid();
} else { // multipolygon, polygon with holes, or concave polygon
return pointOnSurface();
}
}
/**
* Returns and caches a point inside the geometry in world web mercator coordinates.
* <p>
* If the geometry is convex, uses the faster {@link Geometry#getCentroid()} but otherwise falls back to the slower
* {@link Geometry#getInteriorPoint()}.
*/
public final Geometry centroidIfConvex() throws GeometryException {
return centroidIfConvex != null ? centroidIfConvex : (centroidIfConvex = computeCentroidIfConvex());
}
/**
* Computes this feature as a {@link LineString} or {@link MultiLineString} in world web mercator coordinates.
*
* @return the linestring in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
protected Geometry computeLine() throws GeometryException {
Geometry world = worldGeometry();
return world instanceof Lineal ? world : GeoUtils.polygonToLineString(world);
}
/**
* Returns this feature as a {@link LineString} or {@link MultiLineString} in world web mercator coordinates.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry line() throws GeometryException {
if (!canBeLine()) {
throw new GeometryException("feature_not_line", "cannot be line", true);
}
if (linearGeometry == null) {
linearGeometry = computeLine();
}
return linearGeometry;
}
/**
* Returns a partial line string from {@code start} to {@code end} where 0 is the beginning of the line and 1 is the
* end of the line.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a single line (multilinestrings are not allowed).
*/
public final Geometry partialLine(double start, double end) throws GeometryException {
Geometry line = line();
if (start <= 0 && end >= 1) {
return line;
} else if (line instanceof LineString lineString) {
if (this.lineSplitter == null) {
this.lineSplitter = new LineSplitter(lineString);
}
return lineSplitter.get(start, end);
} else {
throw new GeometryException("partial_multilinestring", "cannot get partial of a multiline", true);
}
}
/**
* Computes this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
*
* @return the polygon in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
protected Geometry computePolygon() throws GeometryException {
return worldGeometry();
}
/**
* Returns this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry polygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return polygonGeometry != null ? polygonGeometry : (polygonGeometry = computePolygon());
}
private Geometry computeValidPolygon() throws GeometryException {
Geometry polygon = polygon();
if (!polygon.isValid()) {
polygon = GeoUtils.fixPolygon(polygon);
}
return polygon;
}
/**
* Returns this feature as a valid {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
* <p>
* Validating and fixing invalid polygons can be expensive, so use only if necessary. Invalid polygons will also be
* fixed at render-time.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry validatedPolygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return validPolygon != null ? validPolygon : (validPolygon = computeValidPolygon());
}
/** Wraps a world web mercator geometry. */
public static WithGeometry fromWorldGeometry(Geometry worldGeometry) {
return new FromWorld(worldGeometry);
}
private static class FromWorld extends WithGeometry {
private final Geometry worldGeometry;
private Geometry latLonGeometry;
FromWorld(Geometry worldGeometry) {
this.worldGeometry = worldGeometry;
}
@Override
public Geometry worldGeometry() {
return worldGeometry;
}
@Override
public Geometry latLonGeometry() {
return latLonGeometry != null ? latLonGeometry : (latLonGeometry = GeoUtils.worldToLatLonCoords(worldGeometry));
}
@Override
public boolean isPoint() {
return worldGeometry instanceof Puntal;
}
@Override
public boolean canBePolygon() {
return worldGeometry instanceof Polygonal;
}
@Override
public boolean canBeLine() {
return worldGeometry instanceof Lineal;
}
}
}

Wyświetl plik

@ -1,20 +1,11 @@
package com.onthegomap.planetiler.reader;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.LineSplitter;
import com.onthegomap.planetiler.geo.WithGeometry;
import com.onthegomap.planetiler.reader.osm.OsmReader;
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Lineal;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
/**
* Base class for input features read from a data source.
@ -26,26 +17,14 @@ import org.locationtech.jts.geom.Polygon;
* All geometries except for {@link #latLonGeometry()} return elements in world web mercator coordinates where (0,0) is
* the northwest corner and (1,1) is the southeast corner of the planet.
*/
public abstract class SourceFeature implements WithTags, WithGeometryType, WithSource, WithSourceLayer {
public abstract class SourceFeature extends WithGeometry
implements WithTags, WithSource, WithSourceLayer {
private final Map<String, Object> tags;
private final String source;
private final String sourceLayer;
private final List<OsmReader.RelationMember<OsmRelationInfo>> relationInfos;
private final long id;
private Geometry centroid = null;
private Geometry pointOnSurface = null;
private Geometry centroidIfConvex = null;
private double innermostPointTolerance = Double.NaN;
private Geometry innermostPoint = null;
private Geometry linearGeometry = null;
private Geometry polygonGeometry = null;
private Geometry validPolygon = null;
private double area = Double.NaN;
private double length = Double.NaN;
private double areaMeters = Double.NaN;
private double lengthMeters = Double.NaN;
private LineSplitter lineSplitter;
/**
* Constructs a new input feature.
@ -72,255 +51,6 @@ public abstract class SourceFeature implements WithTags, WithGeometryType, WithS
return tags;
}
/**
* Returns this feature's geometry in latitude/longitude degree coordinates.
*
* @return the latitude/longitude geometry
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
public abstract Geometry latLonGeometry() throws GeometryException;
/**
* Returns this feature's geometry in world web mercator coordinates.
*
* @return the geometry in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
public abstract Geometry worldGeometry() throws GeometryException;
/** Returns and caches {@link Geometry#getCentroid()} of this geometry in world web mercator coordinates. */
public final Geometry centroid() throws GeometryException {
return centroid != null ? centroid : (centroid =
canBePolygon() ? polygon().getCentroid() :
canBeLine() ? line().getCentroid() :
worldGeometry().getCentroid());
}
/** Returns and caches {@link Geometry#getInteriorPoint()} of this geometry in world web mercator coordinates. */
public final Geometry pointOnSurface() throws GeometryException {
return pointOnSurface != null ? pointOnSurface : (pointOnSurface =
canBePolygon() ? polygon().getInteriorPoint() :
canBeLine() ? line().getInteriorPoint() :
worldGeometry().getInteriorPoint());
}
/**
* Returns {@link MaximumInscribedCircle#getCenter()} of this geometry in world web mercator coordinates.
*
* @param tolerance precision for calculating maximum inscribed circle. 0.01 means 1% of the square root of the area.
* Smaller values for a more precise tolerance become very expensive to compute. Values between
* 0.05-0.1 are a good compromise of performance vs. precision.
*/
public final Geometry innermostPoint(double tolerance) throws GeometryException {
if (canBePolygon()) {
// cache as long as the tolerance hasn't changed
if (tolerance != innermostPointTolerance || innermostPoint == null) {
innermostPoint = MaximumInscribedCircle.getCenter(polygon(), Math.sqrt(area()) * tolerance);
innermostPointTolerance = tolerance;
}
return innermostPoint;
} else if (canBeLine()) {
return lineMidpoint();
} else {
return pointOnSurface();
}
}
/**
* Returns the midpoint of this line, or the longest segment if it is a multilinestring.
*/
public final Geometry lineMidpoint() throws GeometryException {
if (innermostPoint == null) {
innermostPoint = pointAlongLine(0.5);
}
return innermostPoint;
}
/**
* Returns along this line where {@code ratio=0} is the start {@code ratio=1} is the end and {@code ratio=0.5} is the
* midpoint.
* <p>
* When this is a multilinestring, the longest segment is used.
*/
public final Geometry pointAlongLine(double ratio) throws GeometryException {
if (lineSplitter == null) {
var line = line();
lineSplitter = new LineSplitter(line instanceof MultiLineString multi ? GeoUtils.getLongestLine(multi) : line);
}
return lineSplitter.get(ratio);
}
private Geometry computeCentroidIfConvex() throws GeometryException {
if (!canBePolygon()) {
return centroid();
} else if (polygon() instanceof Polygon poly &&
poly.getNumInteriorRing() == 0 &&
GeoUtils.isConvex(poly.getExteriorRing())) {
return centroid();
} else { // multipolygon, polygon with holes, or concave polygon
return pointOnSurface();
}
}
/**
* Returns and caches a point inside the geometry in world web mercator coordinates.
* <p>
* If the geometry is convex, uses the faster {@link Geometry#getCentroid()} but otherwise falls back to the slower
* {@link Geometry#getInteriorPoint()}.
*/
public final Geometry centroidIfConvex() throws GeometryException {
return centroidIfConvex != null ? centroidIfConvex : (centroidIfConvex = computeCentroidIfConvex());
}
/**
* Computes this feature as a {@link LineString} or {@link MultiLineString} in world web mercator coordinates.
*
* @return the linestring in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
protected Geometry computeLine() throws GeometryException {
Geometry world = worldGeometry();
return world instanceof Lineal ? world : GeoUtils.polygonToLineString(world);
}
/**
* Returns this feature as a {@link LineString} or {@link MultiLineString} in world web mercator coordinates.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry line() throws GeometryException {
if (!canBeLine()) {
throw new GeometryException("feature_not_line", "cannot be line", true);
}
if (linearGeometry == null) {
linearGeometry = computeLine();
}
return linearGeometry;
}
/**
* Returns a partial line string from {@code start} to {@code end} where 0 is the beginning of the line and 1 is the
* end of the line.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a single line (multilinestrings are not allowed).
*/
public final Geometry partialLine(double start, double end) throws GeometryException {
Geometry line = line();
if (start <= 0 && end >= 1) {
return line;
} else if (line instanceof LineString lineString) {
if (this.lineSplitter == null) {
this.lineSplitter = new LineSplitter(lineString);
}
return lineSplitter.get(start, end);
} else {
throw new GeometryException("partial_multilinestring", "cannot get partial of a multiline", true);
}
}
/**
* Computes this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
*
* @return the polygon in web mercator coordinates
* @throws GeometryException if an unexpected but recoverable error occurs creating this geometry that should
* be logged for debugging
* @throws GeometryException.Verbose if an expected error occurs creating this geometry that will be logged at a lower
* log level
*/
protected Geometry computePolygon() throws GeometryException {
return worldGeometry();
}
/**
* Returns this feature as a {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry polygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return polygonGeometry != null ? polygonGeometry : (polygonGeometry = computePolygon());
}
private Geometry computeValidPolygon() throws GeometryException {
Geometry polygon = polygon();
if (!polygon.isValid()) {
polygon = GeoUtils.fixPolygon(polygon);
}
return polygon;
}
/**
* Returns this feature as a valid {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
* <p>
* Validating and fixing invalid polygons can be expensive, so use only if necessary. Invalid polygons will also be
* fixed at render-time.
*
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
* interpreted as a line
*/
public final Geometry validatedPolygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
}
return validPolygon != null ? validPolygon : (validPolygon = computeValidPolygon());
}
/**
* Returns and caches the result of {@link Geometry#getArea()} of this feature in world web mercator coordinates where
* {@code 1} means the area of the entire planet.
*/
public double area() throws GeometryException {
return Double.isNaN(area) ? (area = canBePolygon() ? Math.abs(polygon().getArea()) : 0) : area;
}
/**
* Returns and caches the result of {@link Geometry#getLength()} of this feature in world web mercator coordinates
* where {@code 1} means the circumference of the entire planet or the distance from 85 degrees north to 85 degrees
* south.
*/
public double length() throws GeometryException {
return Double.isNaN(length) ? (length =
(isPoint() || canBePolygon() || canBeLine()) ? worldGeometry().getLength() : 0) : length;
}
/**
* Returns the sqrt of {@link #area()} if polygon or {@link #length()} if a line string.
*/
public double size() throws GeometryException {
return canBePolygon() ? Math.sqrt(Math.abs(area())) : canBeLine() ? length() : 0;
}
/** Returns and caches the approximate area of the geometry in square meters. */
public double areaMeters() throws GeometryException {
return Double.isNaN(areaMeters) ? (areaMeters =
(isPoint() || canBePolygon() || canBeLine()) ? GeoUtils.areaInMeters(latLonGeometry()) : 0) : areaMeters;
}
/** Returns and caches the approximate length of the geometry in meters. */
public double lengthMeters() throws GeometryException {
return Double.isNaN(lengthMeters) ? (lengthMeters =
(isPoint() || canBePolygon() || canBeLine()) ? GeoUtils.lengthInMeters(latLonGeometry()) : 0) : lengthMeters;
}
/** Returns the sqrt of {@link #areaMeters()} if polygon or {@link #lengthMeters()} if a line string. */
public double sizeMeters() throws GeometryException {
return canBePolygon() ? Math.sqrt(Math.abs(areaMeters())) : canBeLine() ? lengthMeters() : 0;
}
/** Returns the ID of the source that this feature came from. */
@Override
public String getSource() {

Wyświetl plik

@ -0,0 +1,18 @@
package com.onthegomap.planetiler.util;
import static com.onthegomap.planetiler.util.Exceptions.throwFatalException;
@FunctionalInterface
public interface ToDoubleFunctionThatThrows<I> {
@SuppressWarnings("java:S112")
double applyAsDouble(I value) throws Exception;
default double applyAndWrapException(I value) {
try {
return applyAsDouble(value);
} catch (Exception e) {
return throwFatalException(e);
}
}
}

Wyświetl plik

@ -0,0 +1,50 @@
package com.onthegomap.planetiler.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class UnitTest {
@ParameterizedTest
@CsvSource({
"METER, 1, m; meters; metre; metres",
"KILOMETER, 0.001, km; kilometers",
"FOOT, 3.28084, ft; feet; foot",
"MILE, 0.000621371, mi; mile; miles",
"NAUTICAL_MILE, 0.000539957, nm; nautical miles",
"YARD, 1.0936136964129, yd; yards; yds",
"Z0_PIXEL, 0.00390625, z0px; z0 pixels; z0 pixel",
"Z0_TILE, 1, z0ti; z0tile; z0 tile; z0_tiles; z0_tiles; z0 tiles",
})
void testLengthAndDerivedArea(String name, double expected, String aliases) {
Unit.Length length = Unit.Length.from(name);
Unit.Area area = Unit.Area.from("SQUARE_" + name);
assertEquals(expected, length.fromBaseUnit(1), expected / 1e5);
double expectedArea = expected * expected;
assertEquals(expected * expected, area.fromBaseUnit(1), expectedArea / 1e5);
for (String alias : aliases.split(";")) {
assertEquals(length, Unit.Length.from(alias), alias);
assertEquals(area, Unit.Area.from("s" + alias), "s" + alias);
assertEquals(area, Unit.Area.from("sq " + alias), "sq " + alias);
assertEquals(area, Unit.Area.from("square " + alias), "square " + alias);
assertEquals(area, Unit.Area.from(alias + "2"), alias + "2");
}
}
@ParameterizedTest
@CsvSource({
"ARE, 0.01, a; ares",
"HECTARE, 0.0001, ha; hectares",
"ACRE, 0.000247105, ac; acres",
})
void testCustomArea(String name, double expected, String aliases) {
Unit.Area area = Unit.Area.valueOf(name);
assertEquals(expected, area.fromBaseUnit(1), expected / 1e5);
for (String alias : aliases.split(";")) {
assertEquals(area, Unit.Area.from(alias));
}
}
}

Wyświetl plik

@ -506,6 +506,38 @@ Additional variables, on top of the root context:
- `feature.osm_user_name` - optional name of the OSM user that last modified this feature
- `feature.osm_type` - type of the OSM element as a string: `"node"`, `"way"`, or `"relation"`
On the original feature or any accessor that returns a geometry, you can also use:
- `feature.length("unit")` - length of the feature if it is a line, 0 otherwise. Allowed units: "meters"/"m", "feet"
/"ft", "yards"/"yd", "nautical miles"/"nm", "kilometer"/"km" for units relative to the size in meters, or "z0 tiles"/"
z0 ti", "z0 pixels"/"z0 px" for sizes relative to the size of the geometry when projected into a z0 web mercator tile
containing the entire world.
- `feature.area("unit")` - area of the feature if it is a polygon, 0 otherwise. Allowed units: any length unit like "
km2", "mi2", or "z0 px2" or also "acres"/"ac", "hectares"/"ha", or "ares"/"a".
- `feature.min_lat` / `feature.min_lon` / `feature.max_lat` / `feature.max_lon` - returns coordinates from the bounding
box of this geometry
- `feature.lat` / `feature.lon` - returns the coordinate of an arbitrary point on this shape (useful to get the lat/lon
of a point)
- `feature.bbox` - returns the rectangle bounding box that contains this entire shape
- `feature.centroid` - returns the weighted center point of the geometry, which may fall outside the the shape
- `feature.point_on_surface` - returns a point that is within the shape (on the line, or inside the polygon)
- `feature.validated_polygon` - if this is a polygon, fixes any self-intersections and returns the result
- `feature.centroid_if_convex` - returns point_on_surface if this is a concave polygon, or centroid if convex
- `feature.line_midpoint` - returns midpoint of this feature if it is a line
- `feature.point_along_line(amount)` - when amount=0 returns the start of the line, when amount=1 returns the end,
otherwise a point at a certain ratio along the line
- `feature.partial_line(start, end)` - returns a partial line segment from start to end where 0=the beginning of the
line and 1=the end
- `feature.innermost_point` / `feature.innermost_point(tolerance)` - returns the midpoint of a line, or
the [pole of inaccessibility](https://en.wikipedia.org/wiki/Pole_of_inaccessibility) if it is a polygon
For example:
```yaml
key: bbox_area_km2
value: ${ feature.bbox.area('km2') }
```
##### 3. Post-Match Context
Context available after a feature has matched, for example computing an attribute value.

Wyświetl plik

@ -10,6 +10,7 @@ import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.custommap.expression.ParseException;
import com.onthegomap.planetiler.custommap.expression.ScriptContext;
import com.onthegomap.planetiler.custommap.expression.ScriptEnvironment;
import com.onthegomap.planetiler.custommap.expression.stdlib.GeometryVal;
import com.onthegomap.planetiler.expression.DataType;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.reader.WithGeometryType;
@ -362,6 +363,7 @@ public class Contexts {
private static final String FEATURE_OSM_USER_ID = "feature.osm_user_id";
private static final String FEATURE_OSM_USER_NAME = "feature.osm_user_name";
private static final String FEATURE_OSM_TYPE = "feature.osm_type";
private static final String FEATURE_GEOMETRY = "feature";
public static ScriptEnvironment<ProcessFeature> description(Root root) {
return root.description()
@ -376,7 +378,8 @@ public class Contexts {
Decls.newVar(FEATURE_OSM_TIMESTAMP, Decls.Int),
Decls.newVar(FEATURE_OSM_USER_ID, Decls.Int),
Decls.newVar(FEATURE_OSM_USER_NAME, Decls.String),
Decls.newVar(FEATURE_OSM_TYPE, Decls.String)
Decls.newVar(FEATURE_OSM_TYPE, Decls.String),
Decls.newVar(FEATURE_GEOMETRY, GeometryVal.PROTO_TYPE)
);
}
@ -388,6 +391,7 @@ public class Contexts {
case FEATURE_ID -> feature.id();
case FEATURE_SOURCE -> feature.getSource();
case FEATURE_SOURCE_LAYER -> wrapNullable(feature.getSourceLayer());
case FEATURE_GEOMETRY -> new GeometryVal(feature);
default -> {
OsmElement elem = feature instanceof OsmSourceFeature osm ? osm.originalElement() : null;
if (FEATURE_OSM_TYPE.equals(key)) {

Wyświetl plik

@ -1,7 +1,9 @@
package com.onthegomap.planetiler.custommap.expression;
import com.onthegomap.planetiler.custommap.TypeConversion;
import com.onthegomap.planetiler.custommap.expression.stdlib.GeometryVal;
import com.onthegomap.planetiler.custommap.expression.stdlib.PlanetilerStdLib;
import com.onthegomap.planetiler.custommap.expression.stdlib.PlanetilerTypeRegistry;
import com.onthegomap.planetiler.util.Memoized;
import com.onthegomap.planetiler.util.Try;
import java.util.Objects;
@ -98,7 +100,8 @@ public class ConfigExpressionScript<I extends ScriptContext, O> implements Confi
*/
public static <I extends ScriptContext, O> ConfigExpressionScript<I, O> parse(String string,
ScriptEnvironment<I> description, Class<O> expected) {
ScriptHost scriptHost = ScriptHost.newBuilder().build();
var scriptHost = ScriptHost.newBuilder().registry(new PlanetilerTypeRegistry())
.build();
try {
var scriptBuilder = scriptHost.buildScript(string).withLibraries(
new StringsLib(),
@ -107,6 +110,7 @@ public class ConfigExpressionScript<I extends ScriptContext, O> implements Confi
if (!description.declarations().isEmpty()) {
scriptBuilder.withDeclarations(description.declarations());
}
scriptBuilder.withTypes(GeometryVal.PROTO_TYPE);
var script = scriptBuilder.build();
return new ConfigExpressionScript<>(string, script, description, expected);

Wyświetl plik

@ -0,0 +1,130 @@
package com.onthegomap.planetiler.custommap.expression.stdlib;
import static org.projectnessie.cel.common.types.Err.newTypeConversionError;
import static org.projectnessie.cel.common.types.Err.noSuchOverload;
import static org.projectnessie.cel.common.types.Types.boolOf;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.WithGeometry;
import com.onthegomap.planetiler.util.FunctionThatThrows;
import com.onthegomap.planetiler.util.ToDoubleFunctionThatThrows;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.locationtech.jts.geom.Geometry;
import org.projectnessie.cel.checker.Decls;
import org.projectnessie.cel.common.types.DoubleT;
import org.projectnessie.cel.common.types.Err;
import org.projectnessie.cel.common.types.StringT;
import org.projectnessie.cel.common.types.TypeT;
import org.projectnessie.cel.common.types.ref.BaseVal;
import org.projectnessie.cel.common.types.ref.Type;
import org.projectnessie.cel.common.types.ref.Val;
import org.projectnessie.cel.common.types.traits.FieldTester;
import org.projectnessie.cel.common.types.traits.Indexer;
/** Wrapper for a geometry that exposes utility functions to CEL expressions. */
public class GeometryVal extends BaseVal implements Indexer, FieldTester {
public static final String NAME = "geometry";
public static final com.google.api.expr.v1alpha1.Type PROTO_TYPE = Decls.newObjectType(NAME);
private static final Type TYPE = TypeT.newObjectTypeValue(NAME);
private final WithGeometry geometry;
private static final Map<String, Field> FIELDS = Stream.of(
doubleField("lat", geom -> GeoUtils.getWorldLat(geom.worldGeometry().getCoordinate().getY())),
doubleField("lon", geom -> GeoUtils.getWorldLon(geom.worldGeometry().getCoordinate().getX())),
doubleField("min_lat", geom -> geom.latLonGeometry().getEnvelopeInternal().getMinY()),
doubleField("max_lat", geom -> geom.latLonGeometry().getEnvelopeInternal().getMaxY()),
doubleField("min_lon", geom -> geom.latLonGeometry().getEnvelopeInternal().getMinX()),
doubleField("max_lon", geom -> geom.latLonGeometry().getEnvelopeInternal().getMaxX()),
geometryField("bbox", geom -> geom.worldGeometry().getEnvelope()),
geometryField("centroid", WithGeometry::centroid),
geometryField("centroid_if_convex", WithGeometry::centroidIfConvex),
geometryField("validated_polygon", WithGeometry::validatedPolygon),
geometryField("point_on_surface", WithGeometry::pointOnSurface),
geometryField("line_midpoint", WithGeometry::lineMidpoint),
geometryField("innermost_point", geom -> geom.innermostPoint(0.1))
).collect(Collectors.toMap(field -> field.name, Function.identity()));
public static GeometryVal fromWorldGeom(Geometry geometry) {
return new GeometryVal(WithGeometry.fromWorldGeometry(geometry));
}
record Field(String name, com.google.api.expr.v1alpha1.Type type, FunctionThatThrows<WithGeometry, Val> getter) {}
private static Field doubleField(String name, ToDoubleFunctionThatThrows<WithGeometry> getter) {
return new Field(name, Decls.Double, geom -> DoubleT.doubleOf(getter.applyAsDouble(geom)));
}
private static Field geometryField(String name, FunctionThatThrows<WithGeometry, Geometry> getter) {
return new Field(name, PROTO_TYPE, geom -> new GeometryVal(WithGeometry.fromWorldGeometry(getter.apply(geom))));
}
public GeometryVal(WithGeometry geometry) {
this.geometry = geometry;
}
public static com.google.api.expr.v1alpha1.Type fieldType(String fieldName) {
var field = FIELDS.get(fieldName);
return field == null ? null : field.type;
}
@Override
public <T> T convertToNative(Class<T> typeDesc) {
return typeDesc.isInstance(geometry) ? typeDesc.cast(geometry) : null;
}
@Override
public Val convertToType(Type typeValue) {
return newTypeConversionError(TYPE, typeValue);
}
@Override
public Val equal(Val other) {
return boolOf(other instanceof GeometryVal val && Objects.equals(val.geometry, geometry));
}
@Override
public Type type() {
return TYPE;
}
@Override
public Object value() {
return geometry;
}
@Override
public Val isSet(Val field) {
if (!(field instanceof StringT)) {
return noSuchOverload(this, "isSet", field);
}
String fieldName = (String) field.value();
return boolOf(FIELDS.containsKey(fieldName));
}
@Override
public Val get(Val index) {
if (!(index instanceof StringT)) {
return noSuchOverload(this, "get", index);
}
String fieldName = (String) index.value();
try {
var field = FIELDS.get(fieldName);
return field.getter.apply(geometry);
} catch (Exception err) {
return Err.newErr(err, "Error getting %s", fieldName);
}
}
@Override
public final boolean equals(Object o) {
return this == o || (o instanceof GeometryVal val && val.geometry.equals(geometry));
}
@Override
public int hashCode() {
return geometry.hashCode();
}
}

Wyświetl plik

@ -3,6 +3,9 @@ package com.onthegomap.planetiler.custommap.expression.stdlib;
import static org.projectnessie.cel.checker.Decls.newOverload;
import com.google.api.expr.v1alpha1.Type;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.Unit;
import com.onthegomap.planetiler.geo.WithGeometry;
import java.util.List;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
@ -162,6 +165,83 @@ public class PlanetilerStdLib extends PlanetilerLib {
Decls.newOverload("max_double", List.of(Decls.newListType(Decls.Double)), Decls.Double)
),
Overload.unary("max", list -> reduceNumeric(list, Math::max, Math::max))
),
new BuiltInFunction(
Decls.newFunction("area",
Decls.newInstanceOverload("area", List.of(GeometryVal.PROTO_TYPE, Decls.String), Decls.Double)
),
Overload.binary("area",
(a, b) -> DoubleT
.doubleOf(a.convertToNative(WithGeometry.class).area(Unit.Area.from(b.convertToNative(String.class)))))
),
new BuiltInFunction(
Decls.newFunction("length",
Decls.newInstanceOverload("length", List.of(GeometryVal.PROTO_TYPE, Decls.String), Decls.Double)
),
Overload.binary("length",
(a, b) -> DoubleT
.doubleOf(a.convertToNative(WithGeometry.class).length(Unit.Length.from(b.convertToNative(String.class)))))
),
new BuiltInFunction(
Decls.newFunction("point_along_line",
Decls.newInstanceOverload("point_along_line_double", List.of(GeometryVal.PROTO_TYPE, Decls.Double),
GeometryVal.PROTO_TYPE),
Decls.newInstanceOverload("point_along_line_int", List.of(GeometryVal.PROTO_TYPE, Decls.Int),
GeometryVal.PROTO_TYPE)
),
Overload.binary("point_along_line",
(a, b) -> {
try {
return GeometryVal.fromWorldGeom(a.convertToNative(WithGeometry.class).pointAlongLine(b.doubleValue()));
} catch (GeometryException e) {
return Err.newErr(e, "Unable to compute point_along_line(%d)", b.doubleValue());
}
})
),
new BuiltInFunction(
Decls.newFunction("innermost_point",
Decls.newInstanceOverload("innermost_point_double", List.of(GeometryVal.PROTO_TYPE, Decls.Double),
GeometryVal.PROTO_TYPE),
Decls.newInstanceOverload("innermost_point_int", List.of(GeometryVal.PROTO_TYPE, Decls.Int),
GeometryVal.PROTO_TYPE)
),
Overload.binary("innermost_point",
(a, b) -> {
try {
return GeometryVal.fromWorldGeom(a.convertToNative(WithGeometry.class).innermostPoint(b.doubleValue()));
} catch (GeometryException e) {
return Err.newErr(e, "Unable to compute innermost_point(%d)", b.doubleValue());
}
})
),
new BuiltInFunction(
Decls.newFunction("partial_line",
Decls.newInstanceOverload("partial_line_double_double",
List.of(GeometryVal.PROTO_TYPE, Decls.Double, Decls.Double),
GeometryVal.PROTO_TYPE),
Decls.newInstanceOverload("partial_line_double_int", List.of(GeometryVal.PROTO_TYPE, Decls.Double, Decls.Int),
GeometryVal.PROTO_TYPE),
Decls.newInstanceOverload("partial_line_int_double", List.of(GeometryVal.PROTO_TYPE, Decls.Int, Decls.Double),
GeometryVal.PROTO_TYPE),
Decls.newInstanceOverload("partial_line_int_int", List.of(GeometryVal.PROTO_TYPE, Decls.Int, Decls.Int),
GeometryVal.PROTO_TYPE)
),
Overload.function("partial_line",
(Val[] args) -> {
Val a = args[0];
double b = args[1].doubleValue(), c = args[2].doubleValue();
try {
return GeometryVal
.fromWorldGeom(a.convertToNative(WithGeometry.class).partialLine(b, c));
} catch (GeometryException e) {
return Err.newErr(e, "Unable to compute partial_line(%d, %d)", b, c);
}
})
)
));
}

Wyświetl plik

@ -0,0 +1,76 @@
package com.onthegomap.planetiler.custommap.expression.stdlib;
import static org.projectnessie.cel.common.types.Err.newErr;
import static org.projectnessie.cel.common.types.Err.unsupportedRefValConversionErr;
import com.onthegomap.planetiler.reader.SourceFeature;
import java.util.Map;
import org.projectnessie.cel.common.types.pb.Db;
import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter;
import org.projectnessie.cel.common.types.ref.FieldType;
import org.projectnessie.cel.common.types.ref.Type;
import org.projectnessie.cel.common.types.ref.TypeRegistry;
import org.projectnessie.cel.common.types.ref.Val;
/** Registers any types that are available to CEL expressions in planetiler configs. */
public final class PlanetilerTypeRegistry implements TypeRegistry {
@Override
public TypeRegistry copy() {
return new PlanetilerTypeRegistry();
}
@Override
public void register(Object t) {
// types are defined statically
}
@Override
public void registerType(Type... types) {
// types are defined statically
}
@Override
public Val nativeToValue(Object value) {
return switch (value) {
case Val val -> val;
case SourceFeature sourceFeature -> new GeometryVal(sourceFeature);
case null, default -> {
Val val = DefaultTypeAdapter.nativeToValue(Db.defaultDb, this, value);
if (val != null) {
yield val;
}
yield unsupportedRefValConversionErr(value);
}
};
}
@Override
public Val enumValue(String enumName) {
return newErr("unknown enum name '%s'", enumName);
}
@Override
public Val findIdent(String identName) {
return null;
}
@Override
public com.google.api.expr.v1alpha1.Type findType(String typeName) {
return typeName.equals(GeometryVal.NAME) ? GeometryVal.PROTO_TYPE : null;
}
@Override
public FieldType findFieldType(String messageType, String fieldName) {
com.google.api.expr.v1alpha1.Type type = switch (messageType) {
case GeometryVal.NAME -> GeometryVal.fieldType(fieldName);
case null, default -> null;
};
return type == null ? null : new FieldType(type, any -> false, any -> null);
}
@Override
public Val newValue(String typeName, Map<String, Val> fields) {
return null;
}
}

Wyświetl plik

@ -1518,4 +1518,88 @@ class ConfiguredFeatureTest {
testFeature(config, sfNoMatch, any -> {
}, 1);
}
@ParameterizedTest
@CsvSource(value = {
"feature.length('z0 px'); 3.0712E-5",
"feature.length('z0 tiles'); 0.007862",
"feature.length('m'); 314283",
"feature.length('km'); 314.28",
"feature.length('nm'); 169.7",
"feature.length('ft'); 1031114",
"feature.length('yd'); 343704",
"feature.length('mi'); 195.287",
"feature.bbox.area('mi2'); 19068",
"feature.centroid.lat; 3",
"feature.centroid.lon; 2",
"feature.innermost_point.lat; 3",
"feature.innermost_point(0.01).lat; 3",
"feature.line_midpoint.lat; 3",
"feature.point_along_line(0).lat; 2",
"feature.point_along_line(1.0).lat; 4",
"feature.partial_line(0.0, 0.1).centroid.lat; 2.1",
}, delimiter = ';')
void testGeometryAttributesLine(String expression, double expected) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
attributes:
- key: attr
value: ${%s}
""".formatted(expression);
var sfMatch =
SimpleFeature.createFakeOsmFeature(newLineString(1, 2, 3, 4), Map.of(), "osm", "layer", 1, emptyList(),
new OsmElement.Info(2, 3, 4, 5, "user"));
testFeature(config, sfMatch,
any -> assertEquals(expected, (Double) any.getAttrsAtZoom(14).get("attr"), expected / 1e3), 1);
}
@ParameterizedTest
@CsvSource(value = {
"feature.area('z0 px2'); 1.17743E-10",
"feature.area('z0 tiles'); 7.7164E-6",
"feature.area('sm'); 1.2364E10",
"feature.area('km2'); 12363",
"feature.area('ft2'); 1.3308E11",
"feature.area('a'); 1.23637E8",
"feature.area('ac'); 3055141",
"feature.area('acres'); 3055141",
"feature.area('ha'); 1236371",
"feature.area('mi2'); 4773.7",
"feature.bbox.area('mi2'); 4773.7",
"feature.centroid.lat; 0.5",
"feature.centroid.lon; 0.5",
"feature.centroid_if_convex.lon; 0.5",
"feature.point_on_surface.lat; 0.5",
"feature.innermost_point.lat; 0.5",
"feature.validated_polygon.area('mi2'); 4773.7",
}, delimiter = ';')
void testGeometryAttributesArea(String expression, double expected) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
attributes:
- key: attr
value: ${%s}
""".formatted(expression);
var sfMatch =
SimpleFeature.createFakeOsmFeature(rectangle(0, 1), Map.of(), "osm", "layer", 1, emptyList(),
new OsmElement.Info(2, 3, 4, 5, "user"));
testFeature(config, sfMatch,
any -> assertEquals(expected, (Double) any.getAttrsAtZoom(14).get("attr"), expected / 1e3), 1);
}
}