blob: 071c77517b6f3b5eca04f76505d95b6cdfc16e21 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial.spatial4j;
import java.util.ArrayList;
import java.util.List;
import com.google.common.geometry.S2Cell;
import com.google.common.geometry.S2CellId;
import com.google.common.geometry.S2Point;
import org.apache.lucene.spatial.prefix.tree.S2ShapeFactory;
import org.apache.lucene.spatial3d.geom.GeoBBox;
import org.apache.lucene.spatial3d.geom.GeoBBoxFactory;
import org.apache.lucene.spatial3d.geom.GeoCircle;
import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
import org.apache.lucene.spatial3d.geom.GeoCompositeAreaShape;
import org.apache.lucene.spatial3d.geom.GeoPath;
import org.apache.lucene.spatial3d.geom.GeoPathFactory;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoPointShape;
import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory;
import org.apache.lucene.spatial3d.geom.GeoPolygon;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoS2ShapeFactory;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeCollection;
/**
* Geo3d implementation of {@link S2ShapeFactory}
*
* @lucene.experimental
*/
public class Geo3dShapeFactory implements S2ShapeFactory {
private final boolean normWrapLongitude;
private SpatialContext context;
private PlanetModel planetModel;
/**
* Default accuracy for circles when not using the unit sphere.
* It is equivalent to ~10m on the surface of the earth.
*/
private static final double DEFAULT_CIRCLE_ACCURACY = 1e-4;
private double circleAccuracy = DEFAULT_CIRCLE_ACCURACY;
@SuppressWarnings("unchecked")
public Geo3dShapeFactory(SpatialContext context, SpatialContextFactory factory) {
this.context = context;
this.planetModel = ((Geo3dSpatialContextFactory) factory).planetModel;
this.normWrapLongitude = context.isGeo() && factory.normWrapLongitude;
}
@Override
public SpatialContext getSpatialContext() {
return context;
}
/**
* Set the accuracy for circles in decimal degrees. Note that accuracy has no effect
* when the planet model is a sphere. In that case, circles are always fully precise.
*
* @param circleAccuracy the provided accuracy in decimal degrees.
*/
public void setCircleAccuracy(double circleAccuracy) {
this.circleAccuracy = circleAccuracy;
}
@Override
public boolean isNormWrapLongitude() {
return normWrapLongitude;
}
@Override
public double normX(double x) {
if (this.normWrapLongitude) {
x = DistanceUtils.normLonDEG(x);
}
return x;
}
@Override
public double normY(double y) {
return y;
}
@Override
public double normZ(double z) {
return z;
}
@Override
public double normDist(double distance) {
return distance;
}
@Override
public void verifyX(double x) {
Rectangle bounds = this.context.getWorldBounds();
if (x < bounds.getMinX() || x > bounds.getMaxX()) {
throw new InvalidShapeException("Bad X value " + x + " is not in boundary " + bounds);
}
}
@Override
public void verifyY(double y) {
Rectangle bounds = this.context.getWorldBounds();
if (y < bounds.getMinY() || y > bounds.getMaxY()) {
throw new InvalidShapeException("Bad Y value " + y + " is not in boundary " + bounds);
}
}
@Override
public void verifyZ(double v) {
}
@Override
public Point pointXY(double x, double y) {
GeoPointShape point = GeoPointShapeFactory.makeGeoPointShape(planetModel,
y * DistanceUtils.DEGREES_TO_RADIANS,
x * DistanceUtils.DEGREES_TO_RADIANS);
return new Geo3dPointShape(point, context);
}
@Override
public Point pointXYZ(double x, double y, double z) {
GeoPoint point = new GeoPoint(x, y, z);
GeoPointShape pointShape = GeoPointShapeFactory.makeGeoPointShape(planetModel,
point.getLatitude(),
point.getLongitude());
return new Geo3dPointShape(pointShape, context);
//throw new UnsupportedOperationException();
}
@Override
public Rectangle rect(Point point, Point point1) {
return rect(point.getX(), point1.getX(), point.getY(), point1.getY());
}
@Override
public Rectangle rect(double minX, double maxX, double minY, double maxY) {
GeoBBox bBox = GeoBBoxFactory.makeGeoBBox(planetModel,
maxY * DistanceUtils.DEGREES_TO_RADIANS,
minY * DistanceUtils.DEGREES_TO_RADIANS,
minX * DistanceUtils.DEGREES_TO_RADIANS,
maxX * DistanceUtils.DEGREES_TO_RADIANS);
return new Geo3dRectangleShape(bBox, context, minX, maxX, minY, maxY);
}
@Override
public Circle circle(double x, double y, double distance) {
GeoCircle circle;
if (planetModel.isSphere()) {
circle = GeoCircleFactory.makeGeoCircle(planetModel,
y * DistanceUtils.DEGREES_TO_RADIANS,
x * DistanceUtils.DEGREES_TO_RADIANS,
distance * DistanceUtils.DEGREES_TO_RADIANS);
}
else {
//accuracy is defined as a linear distance in this class. At tiny distances, linear distance
//can be approximated to surface distance in radians.
circle = GeoCircleFactory.makeExactGeoCircle(planetModel,
y * DistanceUtils.DEGREES_TO_RADIANS,
x * DistanceUtils.DEGREES_TO_RADIANS,
distance * DistanceUtils.DEGREES_TO_RADIANS,
circleAccuracy * DistanceUtils.DEGREES_TO_RADIANS);
}
return new Geo3dCircleShape(circle, context);
}
@Override
public Circle circle(Point point, double distance) {
return circle(point.getX(), point.getY(), distance);
}
@Override
public Shape lineString(List<Point> list, double distance) {
LineStringBuilder builder = lineString();
for (Point point : list) {
builder.pointXY(point.getX(), point.getY());
}
builder.buffer(distance);
return builder.build();
}
@Override
public <S extends Shape> ShapeCollection<S> multiShape(List<S> list) {
throw new UnsupportedOperationException();
}
@Override
public LineStringBuilder lineString() {
return new Geo3dLineStringBuilder();
}
@Override
public PolygonBuilder polygon() {
return new Geo3dPolygonBuilder();
}
@Override
public <T extends Shape> MultiShapeBuilder<T> multiShape(Class<T> aClass) {
return new Geo3dMultiShapeBuilder<>();
}
@Override
public MultiPointBuilder multiPoint() {
return new Geo3dMultiPointBuilder();
}
@Override
public MultiLineStringBuilder multiLineString() {
return new Geo3dMultiLineBuilder();
}
@Override
public MultiPolygonBuilder multiPolygon() {
return new Geo3dMultiPolygonBuilder();
}
@Override
public Shape getS2CellShape(S2CellId cellId) {
S2Cell cell = new S2Cell(cellId);
GeoPoint point1 = getGeoPoint(cell.getVertexRaw(0));
GeoPoint point2 = getGeoPoint(cell.getVertexRaw(1));
GeoPoint point3 = getGeoPoint(cell.getVertexRaw(2));
GeoPoint point4 = getGeoPoint(cell.getVertexRaw(3));
return new Geo3dShape<>(GeoS2ShapeFactory.makeGeoS2Shape(planetModel, point1, point2, point3, point4), context);
}
private GeoPoint getGeoPoint(S2Point point) {
return planetModel.createSurfacePoint(point.get(0), point.get(1), point.get(2));
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.PointsBuilder} interface to
* generate {@link GeoPoint}.
*
* @param <T> is normally this object
*/
private class Geo3dPointBuilder<T> implements PointsBuilder<T> {
List<GeoPoint> points = new ArrayList<>();
@SuppressWarnings("unchecked")
@Override
public T pointXY(double x, double y) {
GeoPoint point = new GeoPoint(planetModel, y * DistanceUtils.DEGREES_TO_RADIANS, x * DistanceUtils.DEGREES_TO_RADIANS);
points.add(point);
return (T) this;
}
@SuppressWarnings("unchecked")
@Override
public T pointXYZ(double x, double y, double z) {
GeoPoint point = new GeoPoint(x, y, z);
if (!points.contains(point)) {
points.add(point);
}
return (T) this;
}
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.LineStringBuilder} to generate
* line strings.
*/
private class Geo3dLineStringBuilder extends Geo3dPointBuilder<LineStringBuilder> implements LineStringBuilder {
double distance = 0;
@Override
public LineStringBuilder buffer(double distance) {
this.distance = distance;
return this;
}
@Override
public Shape build() {
GeoPath path = GeoPathFactory.makeGeoPath(planetModel, distance, points.toArray(new GeoPoint[points.size()]));
return new Geo3dShape<>(path, context);
}
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.PolygonBuilder} to generate
* polygons.
*/
private class Geo3dPolygonBuilder extends Geo3dPointBuilder<PolygonBuilder> implements PolygonBuilder {
List<GeoPolygonFactory.PolygonDescription> polyHoles = new ArrayList<>();
@Override
public HoleBuilder hole() {
return new Geo3dHoleBuilder();
}
class Geo3dHoleBuilder extends Geo3dPointBuilder<PolygonBuilder.HoleBuilder> implements PolygonBuilder.HoleBuilder {
@Override
public PolygonBuilder endHole() {
polyHoles.add(new GeoPolygonFactory.PolygonDescription(points));
return Geo3dPolygonBuilder.this;
}
}
@SuppressWarnings("unchecked")
@Override
public Shape build() {
GeoPolygonFactory.PolygonDescription description = new GeoPolygonFactory.PolygonDescription(points, polyHoles);
GeoPolygon polygon = GeoPolygonFactory.makeGeoPolygon(planetModel, description);
if (polygon == null) {
throw new InvalidShapeException("Invalid polygon, all points are coplanar");
}
return new Geo3dShape<>(polygon, context);
}
@Override
public Shape buildOrRect() {
return build();
}
}
private class Geo3dMultiPointBuilder extends Geo3dPointBuilder<MultiPointBuilder> implements MultiPointBuilder {
@Override
public Shape build() {
GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
for (GeoPoint point : points) {
GeoPointShape pointShape = GeoPointShapeFactory.makeGeoPointShape(planetModel, point.getLatitude(), point.getLongitude());
areaShape.addShape(pointShape);
}
return new Geo3dShape<>(areaShape, context);
}
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiLineStringBuilder} to generate
* multi-lines
*/
private class Geo3dMultiLineBuilder implements MultiLineStringBuilder {
List<LineStringBuilder> builders = new ArrayList<>();
@Override
public LineStringBuilder lineString() {
return new Geo3dLineStringBuilder();
}
@Override
public MultiLineStringBuilder add(LineStringBuilder lineStringBuilder) {
builders.add(lineStringBuilder);
return this;
}
@SuppressWarnings("unchecked")
@Override
public Shape build() {
GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
for (LineStringBuilder builder : builders) {
Geo3dShape<GeoPolygon> shape = (Geo3dShape<GeoPolygon>) builder.build();
areaShape.addShape(shape.shape);
}
return new Geo3dShape<>(areaShape, context);
}
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiPolygonBuilder} to generate
* multi-polygons. We have chosen to use a composite shape but
* it might be possible to use GeoComplexPolygon.
*/
private class Geo3dMultiPolygonBuilder implements MultiPolygonBuilder {
List<PolygonBuilder> builders = new ArrayList<>();
@Override
public PolygonBuilder polygon() {
return new Geo3dPolygonBuilder();
}
@Override
public MultiPolygonBuilder add(PolygonBuilder polygonBuilder) {
builders.add(polygonBuilder);
return this;
}
@SuppressWarnings("unchecked")
@Override
public Shape build() {
GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
for (PolygonBuilder builder : builders) {
Geo3dShape<GeoPolygon> shape = (Geo3dShape<GeoPolygon>) builder.build();
areaShape.addShape(shape.shape);
}
return new Geo3dShape<>(areaShape, context);
}
}
/**
* Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiShapeBuilder} to generate
* geometry collections.
*
* @param <T> is the type of shapes.
*/
private class Geo3dMultiShapeBuilder<T extends Shape> implements MultiShapeBuilder<T> {
GeoCompositeAreaShape composite = new GeoCompositeAreaShape(planetModel);
@Override
public MultiShapeBuilder<T> add(T shape) {
Geo3dShape<?> areaShape = (Geo3dShape<?>) shape;
composite.addShape(areaShape.shape);
return this;
}
@Override
public Shape build() {
return new Geo3dShape<>(composite, context);
}
}
}