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

org.apache.geode.redis.internal.GeoCoder Maven / Gradle / Ivy

/*
 * 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.geode.redis.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import com.github.davidmoten.geo.GeoHash;
import com.github.davidmoten.geo.LatLong;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;

public class GeoCoder {

  /**
   * Earth radius for distance calculations.
   */
  private static final double EARTH_RADIUS_IN_METERS = 6372797.560856;

  public static ByteBuf getBulkStringGeoCoordinateArrayResponse(ByteBufAllocator alloc,
      Collection items)
      throws CoderException {
    ByteBuf response = alloc.buffer();
    response.writeByte(Coder.ARRAY_ID);
    ByteBuf tmp = alloc.buffer();
    int size = 0;
    try {
      for (LatLong next : items) {
        if (next == null) {
          tmp.writeBytes(Coder.bNIL);
        } else {
          tmp.writeBytes(Coder.getBulkStringArrayResponse(alloc,
              Arrays.asList(
                  Double.toString(next.getLon()),
                  Double.toString(next.getLat()))));
        }
        size++;
      }

      response.writeBytes(Coder.intToBytes(size));
      response.writeBytes(Coder.CRLFar);
      response.writeBytes(tmp);
    } finally {
      tmp.release();
    }

    return response;
  }

  public static ByteBuf geoRadiusResponse(ByteBufAllocator alloc,
      Collection list)
      throws CoderException {
    if (list.isEmpty())
      return Coder.getEmptyArrayResponse(alloc);

    List responseElements = new ArrayList<>();
    for (GeoRadiusResponseElement element : list) {
      String name = element.getName();

      String distStr = "";
      if (element.isShowDist()) {
        distStr = element.getDistFromCenter().toString();
      }

      List coord = new ArrayList<>();
      if (element.getCoord().isPresent()) {
        coord.add(Double.toString(element.getCoord().get().getLon()));
        coord.add(Double.toString(element.getCoord().get().getLat()));
      }

      String hash = "";
      if (element.getHash().isPresent()) {
        hash = element.getHash().get();
      }

      if (!Objects.equals(distStr, "") || !coord.isEmpty() || !Objects.equals(hash, "")) {
        List elementData = new ArrayList<>();
        elementData.add(name);
        if (!Objects.equals(distStr, "")) {
          elementData.add(distStr);
        }
        if (!coord.isEmpty()) {
          elementData.add(coord);
        }
        if (!Objects.equals(hash, "")) {
          elementData.add(hash);
        }
        responseElements.add(elementData);
      } else {
        responseElements.add(name);
      }
    }

    return Coder.getBulkStringArrayResponse(alloc, responseElements);
  }

  /**
   * Converts geohash to lat/long.
   *
   * @param hash geohash as base32
   * @return a LatLong object containing the coordinates
   */
  public static LatLong geoPos(String hash) {
    return GeoHash.decodeHash(hash);
  }

  /**
   * Calculates distance between two points.
   *
   * @param hash1 geohash of first point
   * @param hash2 geohash of second point
   * @return distance in meters
   */
  public static double geoDist(String hash1, String hash2) {
    LatLong coord1 = geoPos(hash1);
    LatLong coord2 = geoPos(hash2);

    double lat1 = Math.toRadians(coord1.getLat());
    double long1 = Math.toRadians(coord1.getLon());
    double lat2 = Math.toRadians(coord2.getLat());
    double long2 = Math.toRadians(coord2.getLon());

    return dist(long1, lat1, long2, lat2);
  }

  /**
   * Calculates geohash given latitude and longitude as byte arrays encoding decimals.
   *
   * @param lon byte array encoding longitude as decimal
   * @param lat byte array encoding latitude as decimal
   * @return geohash as base32
   */
  public static String geohash(byte[] lon, byte[] lat) throws IllegalArgumentException {
    double longitude = Coder.bytesToDouble(lon);
    double latitude = Coder.bytesToDouble(lat);
    return GeoHash.encodeHash(latitude, longitude);
  }

  public static Set geohashSearchAreas(double longitude, double latitude,
      double radiusMeters) {
    HashArea boundingBox = boundingBox(longitude, latitude, radiusMeters);
    int steps =
        Math.max(1, GeoHash.hashLengthToCoverBoundingBox(boundingBox.maxlat, boundingBox.maxlon,
            boundingBox.minlat, boundingBox.minlon));

    List extra = new ArrayList<>();
    // Large distance boundary condition
    if (steps == 1) {
      extra.addAll(GeoHash.neighbours(GeoHash.encodeHash(latitude, longitude, steps)));
    }

    Set areas = GeoHash.coverBoundingBox(boundingBox.maxlat, boundingBox.maxlon,
        boundingBox.minlat, boundingBox.minlon, steps).getHashes();
    if (!extra.isEmpty()) {
      extra.forEach(ex -> areas.add(ex));
    }

    return areas;
  }

  public static HashArea boundingBox(double longitude, double latitude,
      double radiusMeters) {
    double minlon = longitude - Math
        .toDegrees((radiusMeters / EARTH_RADIUS_IN_METERS) * Math.cos(Math.toRadians(latitude)));
    double maxlon = longitude + Math
        .toDegrees((radiusMeters / EARTH_RADIUS_IN_METERS) * Math.cos(Math.toRadians(latitude)));
    double minlat = latitude - Math.toDegrees(radiusMeters / EARTH_RADIUS_IN_METERS);
    double maxlat = latitude + Math.toDegrees(radiusMeters / EARTH_RADIUS_IN_METERS);

    return new HashArea(minlon, maxlon, minlat, maxlat);
  }

  public static double dist(double long1, double lat1, double long2, double lat2) {
    double hav =
        haversine(lat2 - lat1) + (Math.cos(lat1) * Math.cos(lat2) * haversine(long2 - long1));
    double distAngle = Math.acos(1 - (2 * hav));

    return EARTH_RADIUS_IN_METERS * distAngle;
  }

  public static double haversine(double rad) {
    return 0.5 * (1 - Math.cos(rad));
  }

  public static double parseUnitScale(String unit) throws IllegalArgumentException {
    switch (unit) {
      case "km":
        return 0.001;
      case "m":
        return 1.0;
      case "ft":
        return 3.28084;
      case "mi":
        return 0.000621371;
      default:
        throw new IllegalArgumentException();
    }
  }
}