package br.com.citframework.util.geo; import java.util.regex.Matcher; import java.util.regex.Pattern; import br.com.citframework.util.Assert; /** * Utilit�rios para trabalho com atributos de geolocaliza��o * * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @since 22/09/2014 * */ public final class GeoUtils { public static final double MIN_LAT = Math.toRadians(-90d); // -PI/2 public static final double MAX_LAT = Math.toRadians(90d); // PI/2 public static final double MIN_LON = Math.toRadians(-180d); // -PI public static final double MAX_LON = Math.toRadians(180d); // PI public static final double EARTH_RADIUS = 6371.01; private static final String LONGITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY = "Longitude field name must not be null or empty"; private static final String LATITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY = "Latitude field name must not be null or empty"; private static final String STR_COMMON_PATTERN = "^([-+])?(?:%s(?:(?:\\.0{1,15})?)|(?:[0-9]|[1-%s][0-9]%s)(?:(?:\\.[0-9]{1,15})?))$"; private static final String STR_LATITUDE_PATTERN = String.format(STR_COMMON_PATTERN, 90, 8, ""); private static final String STR_LONGITUDE_PATTERN = String.format(STR_COMMON_PATTERN, 180, 9, "{1,}"); private static final Pattern LATITUDE_PATTERN = Pattern.compile(STR_LATITUDE_PATTERN); private static final Pattern LONGITUDE_PATTERN = Pattern.compile(STR_LONGITUDE_PATTERN); /** * Valida a latitude informada como argumento * * @param latitude * latitude a ser validada * @return {@code true}, caso a latitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validLatitude(String) * @since 22/09/2014 */ public static Boolean validLatitude(final Double latitude) { if (latitude == null) { throw new IllegalArgumentException("Latitude must not be null."); } final Matcher matcherLatitude = LATITUDE_PATTERN.matcher(latitude.toString()); return matcherLatitude.matches(); } /** * Valida a latitude informada como argumento * * @param latitude * latitude a ser validada * @return {@code true}, caso a latitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validLatitude(Double) * @since 22/09/2014 */ public static Boolean validLatitude(final String latitude) { return validLatitude(Double.valueOf(latitude)); } /** * Valida a longitude informada como argumento * * @param longitude * longitude a ser validada * @return {@code true}, caso a longitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validLongitude(String) * @since 22/09/2014 */ public static Boolean validLongitude(final Double longitude) { if (longitude == null) { throw new IllegalArgumentException("Longitude must not be null."); } final Matcher matcherLongitude = LONGITUDE_PATTERN.matcher(longitude.toString()); return matcherLongitude.matches(); } /** * Valida a longitude informada como argumento * * @param longitude * longitude a ser validada * @return {@code true}, caso a longitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validLongitude(Double) * @since 22/09/2014 */ public static Boolean validLongitude(final String longitude) { return validLongitude(Double.valueOf(longitude)); } /** * Valida a latitude e a longitude informadas como argumento * * @param latitude * latitude a ser validada * @param longitude * longitude a ser validada * @return {@code true}, caso latitude e longitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validCoordinates(String, String) * @since 22/09/2014 */ public static Boolean validCoordinates(final Double latitude, final Double longitude) { return validLatitude(latitude) && validLongitude(longitude); } /** * Valida a latitude e a longitude informadas como argumento * * @param latitude * latitude a ser validada * @param longitude * longitude a ser validada * @return {@code true}, caso latitude e longitude seja v�lida. {@code false}, caso contr�rio * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @see GeoUtils#validCoordinates(Double, Double) * @since 22/09/2014 */ public static Boolean validCoordinates(final String latitude, final String longitude) { return validCoordinates(Double.valueOf(latitude), Double.valueOf(longitude)); } private static final String DISTANCE_WHERE_QUERY_WITHOUT_INDEX = " acos(sin(?) * sin(radians(%s)) + cos(?) * cos(radians(%s)) * cos(radians(%s) - (?))) * 6371 <= ? "; private static final String DISTANCE_WHERE_QUERY_WITH_INDEX = " (radians(%s) >= ? AND radians(%s) <= ?) AND (radians(%s) >= ? %s radians(%s) <= ?) AND acos(sin(?) * sin(radians(%s)) + cos(?) * cos(radians(%s)) * cos(radians(%s) - (?))) <= ? "; private static final String DISTANCE_WHERE_QUERY_WITH_INDEX_FOR_MYSQL_MSSQLSERVER = " (%s >= ? AND %s <= ?) AND (%s >= ? %s %s <= ?) AND acos(sin(?) * sin(%s) + cos(?) * cos(%s) * cos(%s - (?))) <= ? "; /** * Recupera um trecho de query SQL, a ser adicionado � cl�usula {@code WHERE} final a ser executada * * <p> * Exemplo de como seria o resultado final: * * <pre> * acos(sin(-0.306154991) * sin(radians(latitude)) + cos(-0.306154991) * cos(radians(latitude)) * cos(radians(longitude) - (-0.811136922))) * 6371 <= 10000; * </pre> * * </p> * * Em que: * <ul> * <li><b>latitude</b>: nome da coluna que cont�m o valor da latitude na base de dados</li> * <li><b>longitude</b>: nome da coluna que cont�m o valor da longitude na base de dados</li> * <li><b>-0.306154991</b>: latitude do ponto central de onde ser� calculado o raio</li> * <li><b>-0.811136922</b>: longitude do ponto central de onde ser� calculado o raio</li> * <li><b>6371</b>: raio da terra</li> * <li><b>10000</b>: dist�ncia em KM a partir do ponto</li> * </ul> * </p> * * <p> * Exemplo de uso do trecho de query gerado: * * <pre> * * </pre> * * </p> * * OBSERVA��O: a query retornada n�o permite indexa��o por parte do SGBD, mais perfom�tica para menor quantidade de dados * * @param latitudeColumnName * nome da coluna que armazena a informa��o de latitude do ponto geogr�fico * @param longitudeColumnName * nome da coluna que armazena a informa��o de longitude do ponto geogr�fico * @return query a ser adicionada � cl�usula query, com os statements em seus devidos lugares * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @since 23/10/2014 */ public static String getSQLWherePieceForDistanceWithoutIndex(final String latitudeColumnName, final String longitudeColumnName) { Assert.notNullAndNotEmpty(latitudeColumnName, LATITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); Assert.notNullAndNotEmpty(longitudeColumnName, LONGITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); return String.format(DISTANCE_WHERE_QUERY_WITHOUT_INDEX, latitudeColumnName, latitudeColumnName, longitudeColumnName); } /** * Recupera um trecho de query SQL, a ser adicionado � cl�usula {@code WHERE} final a ser executada. A query filtra por faixas, ent�o os limites devem ser calculados antes. * Veja {@link GeoLocation#boundingCoordinates(double, double)} * * <p> * Exemplo de como seria o resultado final: * * <pre> * (radians(latitude) >= -0.3078396187175743 AND radians(latitude) <= -0.30470039903341123) * AND * (radians(longitude) >= -0.8128445354586982 AND radians(longitude) <= -0.8095521016055605) * AND * acos(sin(-0.30627000887549277) * sin(radians(latitude)) + cos(-0.30627000887549277) * cos(radians(latitude)) * cos(radians(longitude) - (-0.8111983185321293))) <= 0.0015696098420815538 * </pre> * * </p> * * Em que: * <ul> * <li><b>latitude</b>: nome da coluna que cont�m o valor da latitude na base de dados</li> * <li><b>longitude</b>: nome da coluna que cont�m o valor da longitude na base de dados</li> * <li><b>-0.3078396187175743</b>: latitude minima para filtro</li> * <li><b>-0.30470039903341123</b>: latitude m�xima para filtro</li> * <li><b>-0.8128445354586982</b>: longitude minima para filtro</li> * <li><b>-0.8095521016055605</b>: longitude m�xima para filtro</li> * <li><b>-0.30627000887549277</b>: latitude do ponto de refer�ncia</li> * <li><b>-0.8111983185321293</b>: longitude do ponto de refer�ncia</li> * <li><b>0.0015696098420815538</b>: rela��o dist�ncia raio (10 / 6371.01)</li> * </ul> * </p> * * <p> * Exemplo de uso do trecho de query gerado: * * <pre> * final double distance = 10; // in kilometers * final double radius = GeoUtils.EARTH_RADIUS; // in kilometers * final GeoLocation referencePoint = GeoLocation.fromDegrees(-17.547978900000000, -46.478240000000000); * final GeoLocation[] bounds = location.boundingCoordinates(distance, radius); * * final boolean meridian180WithinDistance = bounds[0].getLongitudeInRadians() > bounds[1].getLongitudeInRadians(); * * final java.lang.StringBuilder query = new java.lang.StringBuilder("select * from endereco where "); * query.append(GeoUtils.getSQLWherePieceForDistanceWithIndex("latitude", "longitude", meridian180WithinDistance)); * java.sql.PreparedStatement ps = connection.prepareStatement(query.toString()); * ps.setDouble(1, bounds[0].getLatitudeInRadians()); * ps.setDouble(2, bounds[1].getLatitudeInRadians()); * ps.setDouble(3, bounds[0].getLongitudeInRadians()); * ps.setDouble(4, bounds[1].getLongitudeInRadians()); * ps.setDouble(5, referencePoint.getLatitudeInRadians()); * ps.setDouble(6, referencePoint.getLatitudeInRadians()); * ps.setDouble(7, referencePoint.getLongitudeInRadians()); * ps.setDouble(8, distance / radius); * return ps.executeQuery(); * </pre> * * </p> * * @param latitudeColumnName * nome da coluna que armazena a informa��o de latitude do ponto geogr�fico * @param longitudeColumnName * nome da coluna que armazena a informa��o de longitude do ponto geogr�fico * @param meridian180WithinDistance * @return query a ser adicionada � cl�usula query, com os statements em seus devidos lugares * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @since 23/10/2014 * @see GeoLocation#boundingCoordinates(double, double) para calcular os limites */ public static String getSQLWherePieceForDistanceWithIndex(final String latitudeColumnName, final String longitudeColumnName, final boolean meridian180WithinDistance) { Assert.notNullAndNotEmpty(latitudeColumnName, LATITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); Assert.notNullAndNotEmpty(longitudeColumnName, LONGITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); return String.format(DISTANCE_WHERE_QUERY_WITH_INDEX, latitudeColumnName, latitudeColumnName, longitudeColumnName, meridian180WithinDistance ? "OR" : "AND", longitudeColumnName, latitudeColumnName, latitudeColumnName, longitudeColumnName); } /** * Recupera um trecho de query SQL para MySQL e MS SQL Server, a ser adicionado � cl�usula {@code WHERE} final a ser executada. A query filtra por faixas, ent�o os limites * devem ser calculados antes. * Veja {@link GeoLocation#boundingCoordinates(double, double)} * * <p> * Exemplo de como seria o resultado final: * * <pre> * (latitude_radians >= -0.3078396187175743 AND latitude_radians <= -0.30470039903341123) * AND * (longitude_radians >= -0.8128445354586982 AND longitude_radians <= -0.8095521016055605) * AND * acos(sin(-0.30627000887549277) * sin(latitude_radians) + cos(-0.30627000887549277) * cos(latitude_radians) * cos(longitude_radians - (-0.8111983185321293))) <= 0.0015696098420815538 * </pre> * * </p> * * Em que: * <ul> * <li><b>latitude</b>: nome da coluna que cont�m o valor da latitude na base de dados</li> * <li><b>longitude</b>: nome da coluna que cont�m o valor da longitude na base de dados</li> * <li><b>-0.3078396187175743</b>: latitude minima para filtro</li> * <li><b>-0.30470039903341123</b>: latitude m�xima para filtro</li> * <li><b>-0.8128445354586982</b>: longitude minima para filtro</li> * <li><b>-0.8095521016055605</b>: longitude m�xima para filtro</li> * <li><b>-0.30627000887549277</b>: latitude do ponto de refer�ncia</li> * <li><b>-0.8111983185321293</b>: longitude do ponto de refer�ncia</li> * <li><b>0.0015696098420815538</b>: rela��o dist�ncia raio (10 / 6371.01)</li> * </ul> * </p> * * <p> * Exemplo de uso do trecho de query gerado: * * <pre> * final double distance = 10; // in kilometers * final double radius = GeoUtils.EARTH_RADIUS; // in kilometers * final GeoLocation referencePoint = GeoLocation.fromDegrees(-17.547978900000000, -46.478240000000000); * final GeoLocation[] bounds = location.boundingCoordinates(distance, radius); * * final boolean meridian180WithinDistance = bounds[0].getLongitudeInRadians() > bounds[1].getLongitudeInRadians(); * * final java.lang.StringBuilder query = new java.lang.StringBuilder("select * from endereco where "); * query.append(GeoUtils.getSQLWherePieceForDistanceWithIndexForMySQLAndMSSQLServer("latitude", "longitude", meridian180WithinDistance)); * java.sql.PreparedStatement ps = connection.prepareStatement(query.toString()); * ps.setDouble(1, bounds[0].getLatitudeInRadians()); * ps.setDouble(2, bounds[1].getLatitudeInRadians()); * ps.setDouble(3, bounds[0].getLongitudeInRadians()); * ps.setDouble(4, bounds[1].getLongitudeInRadians()); * ps.setDouble(5, referencePoint.getLatitudeInRadians()); * ps.setDouble(6, referencePoint.getLatitudeInRadians()); * ps.setDouble(7, referencePoint.getLongitudeInRadians()); * ps.setDouble(8, distance / radius); * return ps.executeQuery(); * </pre> * * </p> * * @param latitudeColumnName * nome da coluna que armazena a informa��o de latitude do ponto geogr�fico * @param longitudeColumnName * nome da coluna que armazena a informa��o de longitude do ponto geogr�fico * @param meridian180WithinDistance * @return query a ser adicionada � cl�usula query, com os statements em seus devidos lugares * @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a> * @since 27/10/2014 * @see GeoLocation#boundingCoordinates(double, double) para calcular os limites */ public static String getSQLWherePieceForDistanceWithIndexForMySQLAndMSSQLServer(final String latitudeColumnName, final String longitudeColumnName, final boolean meridian180WithinDistance) { Assert.notNullAndNotEmpty(latitudeColumnName, LATITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); Assert.notNullAndNotEmpty(longitudeColumnName, LONGITUDE_FIELD_NAME_MUST_NOT_BE_NULL_OR_EMPTY); return String.format(DISTANCE_WHERE_QUERY_WITH_INDEX_FOR_MYSQL_MSSQLSERVER, latitudeColumnName, latitudeColumnName, longitudeColumnName, meridian180WithinDistance ? "OR" : "AND", longitudeColumnName, latitudeColumnName, latitudeColumnName, longitudeColumnName); } }