2021-12-23 10:42:24 +00:00
|
|
|
package com.onthegomap.planetiler.reader;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
|
|
|
import com.onthegomap.planetiler.geo.GeometryException;
|
|
|
|
import com.onthegomap.planetiler.reader.osm.OsmReader;
|
|
|
|
import com.onthegomap.planetiler.reader.osm.OsmRelationInfo;
|
2021-05-31 10:21:53 +00:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
2021-04-23 11:26:02 +00:00
|
|
|
import java.util.Map;
|
2021-04-10 09:25:42 +00:00
|
|
|
import org.locationtech.jts.geom.Geometry;
|
2021-09-10 00:46:20 +00:00
|
|
|
import org.locationtech.jts.geom.LineString;
|
2021-05-25 10:05:41 +00:00
|
|
|
import org.locationtech.jts.geom.Lineal;
|
2021-09-10 00:46:20 +00:00
|
|
|
import org.locationtech.jts.geom.MultiLineString;
|
|
|
|
import org.locationtech.jts.geom.MultiPolygon;
|
2021-07-17 00:48:02 +00:00
|
|
|
import org.locationtech.jts.geom.Polygon;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Base class for input features read from a data source.
|
|
|
|
* <p>
|
2022-03-09 02:08:03 +00:00
|
|
|
* Provides cached convenience methods with lazy initialization for geometric attributes derived from
|
|
|
|
* {@link #latLonGeometry()} and {@link #worldGeometry()} to avoid computing them if not needed, and recomputing them if
|
|
|
|
* needed by multiple features.
|
2021-09-10 00:46:20 +00:00
|
|
|
* <p>
|
2022-03-09 02:08:03 +00:00
|
|
|
* All geometries except for {@link #latLonGeometry()} return elements in world web mercator coordinates where (0,0) is
|
2021-09-10 00:46:20 +00:00
|
|
|
* the northwest corner and (1,1) is the southeast corner of the planet.
|
|
|
|
*/
|
2022-09-23 10:49:09 +00:00
|
|
|
public abstract class SourceFeature implements WithTags, WithGeometryType {
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-08-14 09:55:00 +00:00
|
|
|
private final Map<String, Object> tags;
|
2021-05-25 11:47:34 +00:00
|
|
|
private final String source;
|
|
|
|
private final String sourceLayer;
|
2021-09-10 00:46:20 +00:00
|
|
|
private final List<OsmReader.RelationMember<OsmRelationInfo>> relationInfos;
|
2021-06-07 11:46:03 +00:00
|
|
|
private final long id;
|
2021-09-10 00:46:20 +00:00
|
|
|
private Geometry centroid = null;
|
|
|
|
private Geometry pointOnSurface = null;
|
|
|
|
private Geometry centroidIfConvex = null;
|
|
|
|
private Geometry linearGeometry = null;
|
|
|
|
private Geometry polygonGeometry = null;
|
|
|
|
private Geometry validPolygon = null;
|
|
|
|
private double area = Double.NaN;
|
|
|
|
private double length = Double.NaN;
|
2021-04-23 11:26:02 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Constructs a new input feature.
|
|
|
|
*
|
|
|
|
* @param tags string key/value pairs associated with this element
|
2022-03-09 02:08:03 +00:00
|
|
|
* @param source source name that profile can use to distinguish between elements from different data sources
|
2021-09-16 09:15:43 +00:00
|
|
|
* @param sourceLayer layer name within {@code source} that profile can use to distinguish between different kinds
|
2021-09-10 00:46:20 +00:00
|
|
|
* of elements in a given source.
|
|
|
|
* @param relationInfos relations that this element is contained within
|
|
|
|
* @param id numeric ID of this feature within this source (i.e. an OSM element ID)
|
|
|
|
*/
|
2021-08-14 09:55:00 +00:00
|
|
|
protected SourceFeature(Map<String, Object> tags, String source, String sourceLayer,
|
2021-12-23 10:42:24 +00:00
|
|
|
List<OsmReader.RelationMember<OsmRelationInfo>> relationInfos, long id) {
|
2021-08-14 09:55:00 +00:00
|
|
|
this.tags = tags;
|
2021-05-25 11:47:34 +00:00
|
|
|
this.source = source;
|
|
|
|
this.sourceLayer = sourceLayer;
|
2021-05-31 10:21:53 +00:00
|
|
|
this.relationInfos = relationInfos;
|
2021-06-07 11:46:03 +00:00
|
|
|
this.id = id;
|
2021-04-23 11:26:02 +00:00
|
|
|
}
|
|
|
|
|
2021-09-16 09:15:43 +00:00
|
|
|
// slight optimization: replace default implementation with direct access to the tags
|
|
|
|
// map to get slightly improved performance when matching elements against expressions
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Object getTag(String key) {
|
|
|
|
return tags.get(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean hasTag(String key) {
|
|
|
|
return tags.containsKey(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Object getTag(String key, Object defaultValue) {
|
|
|
|
Object val = tags.get(key);
|
|
|
|
if (val == null) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
2021-08-14 09:55:00 +00:00
|
|
|
@Override
|
|
|
|
public Map<String, Object> tags() {
|
|
|
|
return tags;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-25 10:05:41 +00:00
|
|
|
public abstract Geometry latLonGeometry() throws GeometryException;
|
2021-04-23 11:26:02 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-25 10:05:41 +00:00
|
|
|
public abstract Geometry worldGeometry() throws GeometryException;
|
2021-05-13 10:25:06 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns and caches {@link Geometry#getCentroid()} of this geometry in world web mercator coordinates. */
|
|
|
|
public final Geometry centroid() throws GeometryException {
|
2021-05-25 10:05:41 +00:00
|
|
|
return centroid != null ? centroid : (centroid =
|
2021-12-23 10:42:24 +00:00
|
|
|
canBePolygon() ? polygon().getCentroid() :
|
|
|
|
canBeLine() ? line().getCentroid() :
|
2022-03-09 02:08:03 +00:00
|
|
|
worldGeometry().getCentroid());
|
2021-05-25 10:05:41 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns and caches {@link Geometry#getInteriorPoint()} of this geometry in world web mercator coordinates. */
|
|
|
|
public final Geometry pointOnSurface() throws GeometryException {
|
2021-05-25 10:05:41 +00:00
|
|
|
return pointOnSurface != null ? pointOnSurface : (pointOnSurface =
|
2021-12-23 10:42:24 +00:00
|
|
|
canBePolygon() ? polygon().getInteriorPoint() :
|
|
|
|
canBeLine() ? line().getInteriorPoint() :
|
2022-03-09 02:08:03 +00:00
|
|
|
worldGeometry().getInteriorPoint());
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-07-17 00:48:02 +00:00
|
|
|
private Geometry computeCentroidIfConvex() throws GeometryException {
|
|
|
|
if (!canBePolygon()) {
|
|
|
|
return centroid();
|
2023-10-28 00:29:26 +00:00
|
|
|
} else if (polygon() instanceof Polygon poly &&
|
2021-12-23 10:42:24 +00:00
|
|
|
poly.getNumInteriorRing() == 0 &&
|
|
|
|
GeoUtils.isConvex(poly.getExteriorRing())) {
|
2021-07-17 00:48:02 +00:00
|
|
|
return centroid();
|
2021-09-10 00:46:20 +00:00
|
|
|
} else { // multipolygon, polygon with holes, or concave polygon
|
2021-07-17 00:48:02 +00:00
|
|
|
return pointOnSurface();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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 {
|
2021-07-17 00:48:02 +00:00
|
|
|
return centroidIfConvex != null ? centroidIfConvex : (centroidIfConvex = computeCentroidIfConvex());
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-28 10:08:13 +00:00
|
|
|
protected Geometry computeLine() throws GeometryException {
|
|
|
|
Geometry world = worldGeometry();
|
|
|
|
return world instanceof Lineal ? world : GeoUtils.polygonToLineString(world);
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-28 10:08:13 +00:00
|
|
|
public final Geometry line() throws GeometryException {
|
2021-05-25 10:05:41 +00:00
|
|
|
if (!canBeLine()) {
|
2023-03-20 20:41:18 +00:00
|
|
|
throw new GeometryException("feature_not_line", "cannot be line", true);
|
2021-05-25 10:05:41 +00:00
|
|
|
}
|
|
|
|
if (linearGeometry == null) {
|
2021-05-28 10:08:13 +00:00
|
|
|
linearGeometry = computeLine();
|
2021-05-25 10:05:41 +00:00
|
|
|
}
|
|
|
|
return linearGeometry;
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-28 10:08:13 +00:00
|
|
|
protected Geometry computePolygon() throws GeometryException {
|
|
|
|
return worldGeometry();
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-05-28 10:08:13 +00:00
|
|
|
public final Geometry polygon() throws GeometryException {
|
2021-05-25 10:05:41 +00:00
|
|
|
if (!canBePolygon()) {
|
2023-03-20 20:41:18 +00:00
|
|
|
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
|
2021-05-25 10:05:41 +00:00
|
|
|
}
|
2021-05-28 10:08:13 +00:00
|
|
|
return polygonGeometry != null ? polygonGeometry : (polygonGeometry = computePolygon());
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-05-30 11:42:06 +00:00
|
|
|
private Geometry computeValidPolygon() throws GeometryException {
|
|
|
|
Geometry polygon = polygon();
|
|
|
|
if (!polygon.isValid()) {
|
|
|
|
polygon = GeoUtils.fixPolygon(polygon);
|
|
|
|
}
|
|
|
|
return polygon;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Returns this feature as a valid {@link Polygon} or {@link MultiPolygon} in world web mercator coordinates.
|
|
|
|
* <p>
|
2022-03-09 02:08:03 +00:00
|
|
|
* Validating and fixing invalid polygons can be expensive, so use only if necessary. Invalid polygons will also be
|
2021-09-10 00:46:20 +00:00
|
|
|
* fixed at render-time.
|
|
|
|
*
|
|
|
|
* @throws GeometryException if an error occurs constructing the geometry, or of this feature should not be
|
|
|
|
* interpreted as a line
|
|
|
|
*/
|
2021-05-30 11:42:06 +00:00
|
|
|
public final Geometry validatedPolygon() throws GeometryException {
|
|
|
|
if (!canBePolygon()) {
|
2023-03-20 20:41:18 +00:00
|
|
|
throw new GeometryException("feature_not_polygon", "cannot be polygon", true);
|
2021-05-30 11:42:06 +00:00
|
|
|
}
|
|
|
|
return validPolygon != null ? validPolygon : (validPolygon = computeValidPolygon());
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-05-25 10:05:41 +00:00
|
|
|
public double area() throws GeometryException {
|
|
|
|
return Double.isNaN(area) ? (area = canBePolygon() ? polygon().getArea() : 0) : area;
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-05-25 10:05:41 +00:00
|
|
|
public double length() throws GeometryException {
|
2021-05-28 10:08:13 +00:00
|
|
|
return Double.isNaN(length) ? (length =
|
2021-12-23 10:42:24 +00:00
|
|
|
(isPoint() || canBePolygon() || canBeLine()) ? worldGeometry().getLength() : 0) : length;
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the ID of the source that this feature came from. */
|
2021-05-25 10:05:41 +00:00
|
|
|
public String getSource() {
|
|
|
|
return source;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the layer ID within a source that this feature comes from. */
|
2021-05-25 10:05:41 +00:00
|
|
|
public String getSourceLayer() {
|
|
|
|
return sourceLayer;
|
|
|
|
}
|
2021-05-31 10:21:53 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a list of OSM relations that this element belongs to.
|
|
|
|
*
|
|
|
|
* @param relationInfoClass class of the processed relation data
|
|
|
|
* @param <T> type of {@code relationInfoClass}
|
|
|
|
* @return A list containing the OSM relation info along with the role that this element is tagged with in that
|
2022-03-09 02:08:03 +00:00
|
|
|
* relation
|
2021-09-10 00:46:20 +00:00
|
|
|
*/
|
|
|
|
// TODO this should be in a specialized OSM subclass, not the generic superclass
|
|
|
|
public <T extends OsmRelationInfo> List<OsmReader.RelationMember<T>> relationInfo(
|
2021-12-23 10:42:24 +00:00
|
|
|
Class<T> relationInfoClass) {
|
2021-08-14 09:55:00 +00:00
|
|
|
List<OsmReader.RelationMember<T>> result = null;
|
2021-05-31 10:21:53 +00:00
|
|
|
if (relationInfos != null) {
|
2021-08-14 09:55:00 +00:00
|
|
|
for (OsmReader.RelationMember<?> info : relationInfos) {
|
2021-06-18 11:21:43 +00:00
|
|
|
if (relationInfoClass.isInstance(info.relation())) {
|
2021-05-31 10:21:53 +00:00
|
|
|
if (result == null) {
|
|
|
|
result = new ArrayList<>();
|
|
|
|
}
|
2022-03-09 02:08:03 +00:00
|
|
|
@SuppressWarnings("unchecked") OsmReader.RelationMember<T> casted = (OsmReader.RelationMember<T>) info;
|
2021-06-18 11:21:43 +00:00
|
|
|
result.add(casted);
|
2021-05-31 10:21:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result == null ? List.of() : result;
|
|
|
|
}
|
2021-06-07 11:46:03 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Returns the ID for this element from the input data source (i.e. OSM element ID). */
|
2021-06-07 11:46:03 +00:00
|
|
|
public final long id() {
|
|
|
|
return id;
|
|
|
|
}
|
2021-06-11 01:15:27 +00:00
|
|
|
|
2022-09-23 10:49:09 +00:00
|
|
|
|
2021-09-16 09:15:43 +00:00
|
|
|
/** Returns true if this element has any OSM relation info. */
|
|
|
|
public boolean hasRelationInfo() {
|
|
|
|
return relationInfos != null && !relationInfos.isEmpty();
|
|
|
|
}
|
2021-04-10 09:25:42 +00:00
|
|
|
}
|