blob: 00bd8621ac4096567f1c7761afeb54cca488eefc [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.spatial3d;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoShape;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* Add this to a document to index lat/lon or x/y/z point, indexed as a 3D point.
* Multiple values are allowed: just add multiple Geo3DPoint to the document with the
* same field name.
* <p>
* This field defines static factory methods for creating a shape query:
* <ul>
* <li>{@link #newShapeQuery newShapeQuery()} for matching all points inside a specified shape
* </ul>
* @see PointValues
* @lucene.experimental */
public final class Geo3DPoint extends Field {
/** Planet Model for this Geo3DPoint */
protected final PlanetModel planetModel;
/** Indexing {@link FieldType}. */
public static final FieldType TYPE = new FieldType();
static {
TYPE.setDimensions(3, Integer.BYTES);
TYPE.freeze();
}
/**
* Creates a new Geo3DPoint field with the specified latitude, longitude (in degrees), with default WGS84 PlanetModel.
*
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, double lat, double lon) {
this(name, PlanetModel.WGS84, lat, lon);
}
/**
* Creates a new Geo3DPoint field with the specified x,y,z, using default WGS84 planet model.
*
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, double x, double y, double z) {
this(name, PlanetModel.WGS84, x, y, z);
}
/**
* Creates a new Geo3DPoint field with the specified x,y,z, and given planet model.
*
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, PlanetModel planetModel, double x, double y, double z) {
super(name, TYPE);
this.planetModel = planetModel;
fillFieldsData(planetModel, x, y, z);
}
/**
* Creates a new Geo3DPoint field with the specified latitude, longitude (in degrees), given a planet model.
*
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, PlanetModel planetModel, double latitude, double longitude) {
super(name, TYPE);
GeoUtils.checkLatitude(latitude);
GeoUtils.checkLongitude(longitude);
this.planetModel = planetModel;
// Translate latitude/longitude to x,y,z:
final GeoPoint point = new GeoPoint(planetModel, Geo3DUtil.fromDegrees(latitude), Geo3DUtil.fromDegrees(longitude));
fillFieldsData(planetModel, point.x, point.y, point.z);
}
/**
* Create a query for matching points within the specified distance of the supplied location.
* @param field field name. must not be null. Note that if
* {@link PlanetModel#WGS84} is used, the query is approximate and may have up
* to 0.5% error.
*
* @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
* @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
* @param radiusMeters maximum distance from the center in meters: must be non-negative and finite.
* @return query matching points within this distance
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
*/
public static Query newDistanceQuery(final String field, final PlanetModel planetModel, final double latitude, final double longitude, final double radiusMeters) {
final GeoShape shape = Geo3DUtil.fromDistance(planetModel, latitude, longitude, radiusMeters);
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a box.
* <p>
* The box may cross over the dateline.
* @param field field name. must not be null.
* @param minLatitude latitude lower bound: must be within standard +/-90 coordinate bounds.
* @param maxLatitude latitude upper bound: must be within standard +/-90 coordinate bounds.
* @param minLongitude longitude lower bound: must be within standard +/-180 coordinate bounds.
* @param maxLongitude longitude upper bound: must be within standard +/-180 coordinate bounds.
* @return query matching points within this box
* @throws IllegalArgumentException if {@code field} is null, or the box has invalid coordinates.
*/
public static Query newBoxQuery(final String field, final PlanetModel planetModel, final double minLatitude, final double maxLatitude, final double minLongitude, final double maxLongitude) {
final GeoShape shape = Geo3DUtil.fromBox(planetModel, minLatitude, maxLatitude, minLongitude, maxLongitude);
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a polygon. The polygon should have a limited number of edges (less than 100) and be well-defined,
* with well-separated vertices.
* <p>
* The supplied {@code polygons} must be clockwise on the outside level, counterclockwise on the next level in, etc.
* @param field field name. must not be null.
* @param polygons is the list of polygons to use to construct the query; must be at least one.
* @return query matching points within this polygon
*/
public static Query newPolygonQuery(final String field, final PlanetModel planetModel, final Polygon... polygons) {
final GeoShape shape = Geo3DUtil.fromPolygon(planetModel, polygons);
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a large polygon. This differs from the related newPolygonQuery in that it
* does little or no legality checking and is optimized for very large numbers of polygon edges.
* <p>
* The supplied {@code polygons} must be clockwise on the outside level, counterclockwise on the next level in, etc.
* @param field field name. must not be null.
* @param polygons is the list of polygons to use to construct the query; must be at least one.
* @return query matching points within this polygon
*/
public static Query newLargePolygonQuery(final String field, PlanetModel planetModel, final Polygon... polygons) {
final GeoShape shape = Geo3DUtil.fromLargePolygon(planetModel, polygons);
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a path.
* @param field field name. must not be null.
* @param pathLatitudes latitude values for points of the path: must be within standard +/-90 coordinate bounds.
* @param pathLongitudes longitude values for points of the path: must be within standard +/-180 coordinate bounds.
* @param pathWidthMeters width of the path in meters.
* @return query matching points within this polygon
*/
public static Query newPathQuery(final String field, final double[] pathLatitudes, final double[] pathLongitudes, final double pathWidthMeters, PlanetModel planetModel) {
final GeoShape shape = Geo3DUtil.fromPath(planetModel, pathLatitudes, pathLongitudes, pathWidthMeters);
return newShapeQuery(field, shape);
}
private void fillFieldsData(PlanetModel planetModel, double x, double y, double z) {
byte[] bytes = new byte[12];
encodeDimension(x, bytes, 0, planetModel);
encodeDimension(y, bytes, Integer.BYTES, planetModel);
encodeDimension(z, bytes, 2*Integer.BYTES, planetModel);
fieldsData = new BytesRef(bytes);
}
// public helper methods (e.g. for queries)
/** Encode single dimension */
public static void encodeDimension(double value, byte bytes[], int offset, PlanetModel planetModel) {
NumericUtils.intToSortableBytes(planetModel.encodeValue(value), bytes, offset);
}
/** Decode single dimension */
public static double decodeDimension(byte value[], int offset, PlanetModel planetModel) {
return planetModel.decodeValue(NumericUtils.sortableBytesToInt(value, offset));
}
/** Returns a query matching all points inside the provided shape.
*
* @param field field name. must not be {@code null}.
* @param shape Which {@link GeoShape} to match
*/
public static Query newShapeQuery(String field, GeoShape shape) {
return new PointInGeo3DShapeQuery(field, shape);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(getClass().getSimpleName());
result.append(" <");
result.append(name);
result.append(':');
BytesRef bytes = (BytesRef) fieldsData;
result.append(" x=").append(decodeDimension(bytes.bytes, bytes.offset, this.planetModel));
result.append(" y=").append(decodeDimension(bytes.bytes, bytes.offset + Integer.BYTES, this.planetModel));
result.append(" z=").append(decodeDimension(bytes.bytes, bytes.offset + 2 * Integer.BYTES, this.planetModel));
result.append('>');
return result.toString();
}
}