All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensearch.geometry.utils.Geohash Maven / Gradle / Ivy

/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.
 */
/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.geometry.utils;

import org.opensearch.geometry.Point;
import org.opensearch.geometry.Rectangle;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Utilities for converting to/from the GeoHash standard
 * 

* The geohash long format is represented as lon/lat (x/y) interleaved with the 4 least significant bits * representing the level (1-12) [xyxy...xyxyllll] *

* This differs from a morton encoded value which interleaves lat/lon (y/x). *

* NOTE: this will replace {@code org.opensearch.common.geo.GeoHashUtils} */ public class Geohash { private static final char[] BASE_32 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; private static final String BASE_32_STRING = new String(BASE_32); /** maximum precision for geohash strings */ public static final int PRECISION = 12; /** number of bits used for quantizing latitude and longitude values */ private static final short BITS = 32; private static final double LAT_SCALE = (0x1L << (BITS - 1)) / 180.0D; private static final double LAT_DECODE = 180.0D / (0x1L << BITS); private static final double LON_SCALE = (0x1L << (BITS - 1)) / 360.0D; private static final double LON_DECODE = 360.0D / (0x1L << BITS); private static final short MORTON_OFFSET = (BITS << 1) - (PRECISION * 5); /** Bit encoded representation of the latitude of north pole */ private static final long MAX_LAT_BITS = (0x1L << (PRECISION * 5 / 2)) - 1; // Below code is adapted from the spatial4j library (GeohashUtils.java) Apache 2.0 Licensed private static final double[] precisionToLatHeight, precisionToLonWidth; static { precisionToLatHeight = new double[PRECISION + 1]; precisionToLonWidth = new double[PRECISION + 1]; precisionToLatHeight[0] = 90 * 2; precisionToLonWidth[0] = 180 * 2; boolean even = false; for (int i = 1; i <= PRECISION; i++) { precisionToLatHeight[i] = precisionToLatHeight[i - 1] / (even ? 8 : 4); precisionToLonWidth[i] = precisionToLonWidth[i - 1] / (even ? 4 : 8); even = !even; } } // no instance: private Geohash() {} /** Returns a {@link Point} instance from a geohash string */ public static Point toPoint(final String geohash) throws IllegalArgumentException { final long hash = mortonEncode(geohash); return new Point(decodeLongitude(hash), decodeLatitude(hash)); } /** * Computes the bounding box coordinates from a given geohash * * @param geohash Geohash of the defined cell * @return GeoRect rectangle defining the bounding box */ public static Rectangle toBoundingBox(final String geohash) { // bottom left is the coordinate Point bottomLeft = toPoint(geohash); int len = Math.min(12, geohash.length()); long ghLong = longEncode(geohash, len); // shift away the level ghLong >>>= 4; // deinterleave long lon = BitUtil.deinterleave(ghLong >>> 1); long lat = BitUtil.deinterleave(ghLong); final int shift = (12 - len) * 5 + 2; if (lat < MAX_LAT_BITS) { // add 1 to lat and lon to get topRight ghLong = BitUtil.interleave((int) (lat + 1), (int) (lon + 1)) << 4 | len; final long mortonHash = BitUtil.flipFlop((ghLong >>> 4) << shift); Point topRight = new Point(decodeLongitude(mortonHash), decodeLatitude(mortonHash)); return new Rectangle(bottomLeft.getX(), topRight.getX(), topRight.getY(), bottomLeft.getY()); } else { // We cannot go north of north pole, so just using 90 degrees instead of calculating it using // add 1 to lon to get lon of topRight, we are going to use 90 for lat ghLong = BitUtil.interleave((int) lat, (int) (lon + 1)) << 4 | len; final long mortonHash = BitUtil.flipFlop((ghLong >>> 4) << shift); Point topRight = new Point(decodeLongitude(mortonHash), decodeLatitude(mortonHash)); return new Rectangle(bottomLeft.getX(), topRight.getX(), 90D, bottomLeft.getY()); } } /** Array of geohashes one level below the baseGeohash. Sorted. */ public static String[] getSubGeohashes(String baseGeohash) { String[] hashes = new String[BASE_32.length]; for (int i = 0; i < BASE_32.length; i++) {// note: already sorted char c = BASE_32[i]; hashes[i] = baseGeohash + c; } return hashes; } /** * Calculate all neighbors of a given geohash cell. * * @param geohash Geohash of the defined cell * @return geohashes of all neighbor cells */ public static Collection getNeighbors(String geohash) { return addNeighborsAtLevel(geohash, geohash.length(), new ArrayList(8)); } /** * Add all geohashes of the cells next to a given geohash to a list. * * @param geohash Geohash of a specified cell * @param neighbors list to add the neighbors to * @return the given list */ public static final > E addNeighbors(String geohash, E neighbors) { return addNeighborsAtLevel(geohash, geohash.length(), neighbors); } /** * Add all geohashes of the cells next to a given geohash to a list. * * @param geohash Geohash of a specified cell * @param level level of the given geohash * @param neighbors list to add the neighbors to * @return the given list */ public static final > E addNeighborsAtLevel(String geohash, int level, E neighbors) { String south = getNeighbor(geohash, level, 0, -1); String north = getNeighbor(geohash, level, 0, +1); if (north != null) { neighbors.add(getNeighbor(north, level, -1, 0)); neighbors.add(north); neighbors.add(getNeighbor(north, level, +1, 0)); } neighbors.add(getNeighbor(geohash, level, -1, 0)); neighbors.add(getNeighbor(geohash, level, +1, 0)); if (south != null) { neighbors.add(getNeighbor(south, level, -1, 0)); neighbors.add(south); neighbors.add(getNeighbor(south, level, +1, 0)); } return neighbors; } /** * Calculate the geohash of a neighbor of a geohash * * @param geohash the geohash of a cell * @param level level of the geohash * @param dx delta of the first grid coordinate (must be -1, 0 or +1) * @param dy delta of the second grid coordinate (must be -1, 0 or +1) * @return geohash of the defined cell */ public static final String getNeighbor(String geohash, int level, int dx, int dy) { int cell = BASE_32_STRING.indexOf(geohash.charAt(level - 1)); // Decoding the Geohash bit pattern to determine grid coordinates int x0 = cell & 1; // first bit of x int y0 = cell & 2; // first bit of y int x1 = cell & 4; // second bit of x int y1 = cell & 8; // second bit of y int x2 = cell & 16; // third bit of x // combine the bitpattern to grid coordinates. // note that the semantics of x and y are swapping // on each level int x = x0 + (x1 / 2) + (x2 / 4); int y = (y0 / 2) + (y1 / 4); if (level == 1) { // Root cells at north (namely "bcfguvyz") or at // south (namely "0145hjnp") do not have neighbors // in north/south direction if ((dy < 0 && y == 0) || (dy > 0 && y == 3)) { return null; } else { return Character.toString(encodeBase32(x + dx, y + dy)); } } else { // define grid coordinates for next level final int nx = ((level % 2) == 1) ? (x + dx) : (x + dy); final int ny = ((level % 2) == 1) ? (y + dy) : (y + dx); // if the defined neighbor has the same parent a the current cell // encode the cell directly. Otherwise find the cell next to this // cell recursively. Since encoding wraps around within a cell // it can be encoded here. // xLimit and YLimit must always be respectively 7 and 3 // since x and y semantics are swapping on each level. if (nx >= 0 && nx <= 7 && ny >= 0 && ny <= 3) { return geohash.substring(0, level - 1) + encodeBase32(nx, ny); } else { String neighbor = getNeighbor(geohash, level - 1, dx, dy); return (neighbor != null) ? neighbor + encodeBase32(nx, ny) : neighbor; } } } /** * Encode a string geohash to the geohash based long format (lon/lat interleaved, 4 least significant bits = level) */ public static final long longEncode(String hash) { return longEncode(hash, hash.length()); } /** * Encode lon/lat to the geohash based long format (lon/lat interleaved, 4 least significant bits = level) */ public static final long longEncode(final double lon, final double lat, final int level) { // shift to appropriate level final short msf = (short) (((12 - level) * 5) + (MORTON_OFFSET - 2)); return ((encodeLatLon(lat, lon) >>> msf) << 4) | level; } /** * Encode to a geohash string from full resolution longitude, latitude) */ public static final String stringEncode(final double lon, final double lat) { return stringEncode(lon, lat, 12); } /** * Encode to a level specific geohash string from full resolution longitude, latitude */ public static final String stringEncode(final double lon, final double lat, final int level) { // convert to geohashlong long interleaved = encodeLatLon(lat, lon); interleaved >>>= (((PRECISION - level) * 5) + (MORTON_OFFSET - 2)); final long geohash = (interleaved << 4) | level; return stringEncode(geohash); } /** * Encode to a geohash string from the geohash based long format */ public static final String stringEncode(long geoHashLong) { int level = (int) geoHashLong & 15; geoHashLong >>>= 4; char[] chars = new char[level]; do { chars[--level] = BASE_32[(int) (geoHashLong & 31L)]; geoHashLong >>>= 5; } while (level > 0); return new String(chars); } /** base32 encode at the given grid coordinate */ private static char encodeBase32(int x, int y) { return BASE_32[((x & 1) + ((y & 1) * 2) + ((x & 2) * 2) + ((y & 2) * 4) + ((x & 4) * 4)) % 32]; } /** * Encode from geohash string to the geohash based long format (lon/lat interleaved, 4 least significant bits = level) */ private static long longEncode(final String hash, int length) { int level = length - 1; long b; long l = 0L; for (char c : hash.toCharArray()) { b = (long) (BASE_32_STRING.indexOf(c)); l |= (b << (level-- * 5)); if (level < 0) { // We cannot handle more than 12 levels break; } } return (l << 4) | length; } /** * Encode to a morton long value from a given geohash string */ public static long mortonEncode(final String hash) { if (hash.isEmpty()) { throw new IllegalArgumentException("empty geohash"); } int level = 11; long b; long l = 0L; for (char c : hash.toCharArray()) { b = (long) (BASE_32_STRING.indexOf(c)); if (b < 0) { throw new IllegalArgumentException("unsupported symbol [" + c + "] in geohash [" + hash + "]"); } l |= (b << ((level-- * 5) + (MORTON_OFFSET - 2))); if (level < 0) { // We cannot handle more than 12 levels break; } } return BitUtil.flipFlop(l); } /** approximate width of geohash tile for a specific precision in degrees */ public static double lonWidthInDegrees(int precision) { return precisionToLonWidth[precision]; } /** approximate height of geohash tile for a specific precision in degrees */ public static double latHeightInDegrees(int precision) { return precisionToLatHeight[precision]; } private static long encodeLatLon(final double lat, final double lon) { // encode lat/lon flipping the sign bit so negative ints sort before positive ints final int latEnc = encodeLatitude(lat) ^ 0x80000000; final int lonEnc = encodeLongitude(lon) ^ 0x80000000; return BitUtil.interleave(latEnc, lonEnc) >>> 2; } /** encode latitude to integer */ public static int encodeLatitude(double latitude) { // the maximum possible value cannot be encoded without overflow if (latitude == 90.0D) { latitude = Math.nextDown(latitude); } return (int) Math.floor(latitude / LAT_DECODE); } /** encode longitude to integer */ public static int encodeLongitude(double longitude) { // the maximum possible value cannot be encoded without overflow if (longitude == 180.0D) { longitude = Math.nextDown(longitude); } return (int) Math.floor(longitude / LON_DECODE); } /** returns the latitude value from the string based geohash */ public static final double decodeLatitude(final String geohash) { return decodeLatitude(Geohash.mortonEncode(geohash)); } /** returns the latitude value from the string based geohash */ public static final double decodeLongitude(final String geohash) { return decodeLongitude(Geohash.mortonEncode(geohash)); } /** decode longitude value from morton encoded geo point */ public static double decodeLongitude(final long hash) { return unscaleLon(BitUtil.deinterleave(hash)); } /** decode latitude value from morton encoded geo point */ public static double decodeLatitude(final long hash) { return unscaleLat(BitUtil.deinterleave(hash >>> 1)); } private static double unscaleLon(final long val) { return (val / LON_SCALE) - 180; } private static double unscaleLat(final long val) { return (val / LAT_SCALE) - 90; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy