/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.spatial.impl; import java.util.ArrayList; import java.util.List; import org.hibernate.search.exception.AssertionFailure; import org.hibernate.search.spatial.Coordinates; /** * Spatial fields, ids generator and geometric calculation methods for use in SpatialFieldBridge * * @author Nicolas Helleringer * @author Mathieu Perez * @see org.hibernate.search.spatial.SpatialFieldBridgeByHash * @see org.hibernate.search.spatial.SpatialFieldBridgeByRange */ public abstract class SpatialHelper { private static final double LOG2 = Math.log( 2 ); private static final String SPATIAL_FIELD_SUFFIX = "_HSSI_"; private static final String SPATIAL_LATITUDE_SUFFIX = SPATIAL_FIELD_SUFFIX + "Latitude"; private static final String SPATIAL_LONGITUDE_SUFFIX = SPATIAL_FIELD_SUFFIX + "Longitude"; /** * Private constructor locking down utility class */ private SpatialHelper() { }; /** * Generate a Cell Index on one axis * * @param coordinate position to compute the Index for * @param range range of the axis {@literal (-pi,pi)/(-90,90) => 2*pi/180} e.g * @param spatialHashLevel Hox many time the range has been split in two * @return the cell index on the axis */ public static int getCellIndex(double coordinate, double range, int spatialHashLevel) { return (int) Math.floor( Math.pow( 2, spatialHashLevel ) * coordinate / range ); } /** * Generate a spatial hash cell id (with both cell index on both dimension in it) for a position * * @param point position to compute the spatial hash cell id for * @param spatialHashLevel Hox many time the dimensions have been split in two * @return the cell id for the point at the given spatial hash level */ public static String getSpatialHashCellId(Point point, int spatialHashLevel) { double[] indexablesCoordinates = projectToIndexSpace( point ); int longitudeCellIndex = getCellIndex( indexablesCoordinates[0], GeometricConstants.PROJECTED_LONGITUDE_RANGE, spatialHashLevel ); int latitudeCellIndex = getCellIndex( indexablesCoordinates[1], GeometricConstants.PROJECTED_LATITUDE_RANGE, spatialHashLevel ); return formatSpatialHashCellId( longitudeCellIndex, latitudeCellIndex ); } /** * Generate a spatial hash cell ids list covered by a bounding box * * @param lowerLeft lower left corner of the bounding box * @param upperRight upper right corner of the bounding box * @param spatialHashLevel spatial hash level of the wanted cell ids * @return List of ids of the cells containing the point */ public static List<String> getSpatialHashCellsIds(Point lowerLeft, Point upperRight, int spatialHashLevel) { double[] projectedLowerLeft = projectToIndexSpace( lowerLeft ); int lowerLeftXIndex = getCellIndex( projectedLowerLeft[0], GeometricConstants.PROJECTED_LONGITUDE_RANGE, spatialHashLevel ); int lowerLeftYIndex = getCellIndex( projectedLowerLeft[1], GeometricConstants.PROJECTED_LATITUDE_RANGE, spatialHashLevel ); double[] projectedUpperRight = projectToIndexSpace( upperRight ); int upperRightXIndex = getCellIndex( projectedUpperRight[0], GeometricConstants.PROJECTED_LONGITUDE_RANGE, spatialHashLevel ); int upperRightYIndex = getCellIndex( projectedUpperRight[1], GeometricConstants.PROJECTED_LATITUDE_RANGE, spatialHashLevel ); double[] projectedLowerRight = projectToIndexSpace( Point.fromDegrees( lowerLeft.getLatitude(), upperRight.getLongitude() ) ); int lowerRightXIndex = getCellIndex( projectedLowerRight[0], GeometricConstants.PROJECTED_LONGITUDE_RANGE, spatialHashLevel ); int lowerRightYIndex = getCellIndex( projectedLowerRight[1], GeometricConstants.PROJECTED_LATITUDE_RANGE, spatialHashLevel ); double[] projectedUpperLeft = projectToIndexSpace( Point.fromDegrees( upperRight.getLatitude(), lowerLeft.getLongitude() ) ); int upperLeftXIndex = getCellIndex( projectedUpperLeft[0], GeometricConstants.PROJECTED_LONGITUDE_RANGE, spatialHashLevel ); int upperLeftYIndex = getCellIndex( projectedUpperLeft[1], GeometricConstants.PROJECTED_LATITUDE_RANGE, spatialHashLevel ); final int startX = Math.min( Math.min( Math.min( lowerLeftXIndex, upperLeftXIndex ), upperRightXIndex ), lowerRightXIndex ); final int endX = Math.max( Math.max( Math.max( lowerLeftXIndex, upperLeftXIndex ), upperRightXIndex ), lowerRightXIndex ); final int startY = Math.min( Math.min( Math.min( lowerLeftYIndex, upperLeftYIndex ), upperRightYIndex ), lowerRightYIndex ); final int endY = Math.max( Math.max( Math.max( lowerLeftYIndex, upperLeftYIndex ), upperRightYIndex ), lowerRightYIndex ); List<String> spatialHashCellsIds = new ArrayList<String>( ( endX + 1 - startX ) * ( endY + 1 - startY ) ); for ( int xIndex = startX; xIndex <= endX; xIndex++ ) { for ( int yIndex = startY; yIndex <= endY; yIndex++ ) { spatialHashCellsIds.add( formatSpatialHashCellId( xIndex, yIndex ) ); } } return spatialHashCellsIds; } /** * Generate a spatial hash cell ids list for the bounding box of a circular search area * * @param center center of the search area * @param radius radius of the search area * @param spatialHashLevel spatial hash level of the wanted cell ids * @return List of the ids of the cells covering the bounding box of the given search discus */ public static List<String> getSpatialHashCellsIds(Coordinates center, double radius, int spatialHashLevel) { Rectangle boundingBox = Rectangle.fromBoundingCircle( center, radius ); double lowerLeftLatitude = boundingBox.getLowerLeft().getLatitude(); double lowerLeftLongitude = boundingBox.getLowerLeft().getLongitude(); double upperRightLatitude = boundingBox.getUpperRight().getLatitude(); double upperRightLongitude = boundingBox.getUpperRight().getLongitude(); if ( upperRightLongitude < lowerLeftLongitude ) { // Box cross the 180 meridian final List<String> spatialHashCellsIds; spatialHashCellsIds = getSpatialHashCellsIds( Point.fromDegreesInclusive( lowerLeftLatitude, lowerLeftLongitude ), Point.fromDegreesInclusive( upperRightLatitude, GeometricConstants.LONGITUDE_DEGREE_RANGE / 2 ), spatialHashLevel ); spatialHashCellsIds.addAll( getSpatialHashCellsIds( Point.fromDegreesInclusive( lowerLeftLatitude, -GeometricConstants.LONGITUDE_DEGREE_RANGE / 2 ), Point.fromDegreesInclusive( upperRightLatitude, upperRightLongitude ), spatialHashLevel ) ); return spatialHashCellsIds; } else { return getSpatialHashCellsIds( Point.fromDegreesInclusive( lowerLeftLatitude, lowerLeftLongitude ), Point.fromDegreesInclusive( upperRightLatitude, upperRightLongitude ), spatialHashLevel ); } } /** * If point are searched at d distance from a point, a certain spatial hash cell level will problem spatial hash * cell that are big enough to contain the search area but the smallest possible. By returning this level we ensure * 4 spatial hash cell maximum will be needed to cover the search area (2 max on each axis because of search area * crossing fixed bonds of the spatial hash cells) * * @param searchRange * search range to be covered by the spatial hash cells * @return Return the best spatial hash level for a given search radius. */ public static int findBestSpatialHashLevelForSearchRange(double searchRange) { double iterations = GeometricConstants.EARTH_EQUATOR_CIRCUMFERENCE_KM / ( 2.0d * searchRange ); return (int) Math.max( 0, Math.ceil( Math.log( iterations ) / LOG2 ) ); } /** * Project a degree latitude/longitude point into a sinusoidal projection planar space for spatial hash cell ids * computation * * @param point * point to be projected * @return array of projected coordinates */ public static double[] projectToIndexSpace(Point point) { double[] projectedCoordinates = new double[2]; projectedCoordinates[0] = point.getLongitudeRad() * Math.cos( point.getLatitudeRad() ); projectedCoordinates[1] = point.getLatitudeRad(); return projectedCoordinates; } public static String formatFieldName(final int spatialHashLevel, final String fieldName) { return fieldName + SPATIAL_FIELD_SUFFIX + spatialHashLevel; } public static String formatLatitude(final String fieldName) { return fieldName + SPATIAL_LATITUDE_SUFFIX; } public static String formatLongitude(final String fieldName) { return fieldName + SPATIAL_LONGITUDE_SUFFIX; } public static boolean isSpatialField(String fieldName) { return fieldName.contains( SPATIAL_FIELD_SUFFIX ); } public static boolean isSpatialFieldLatitude(String fieldName) { return fieldName.endsWith( SPATIAL_LATITUDE_SUFFIX ); } public static boolean isSpatialFieldLongitude(String fieldName) { return fieldName.endsWith( SPATIAL_LONGITUDE_SUFFIX ); } public static String stripSpatialFieldSuffix(String fieldName) { if ( !isSpatialField( fieldName ) ) { throw new AssertionFailure( "The field " + fieldName + " is not a spatial field." ); } return fieldName.substring( 0, fieldName.indexOf( SPATIAL_FIELD_SUFFIX ) ); } public static String formatSpatialHashCellId(final int xIndex, final int yIndex) { return xIndex + "|" + yIndex; } }