OriginIndexerModule.java

package gov.usgs.earthquake.origin;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.json.JsonObject;

import gov.usgs.earthquake.geoserve.GeoservePlacesService;
import gov.usgs.earthquake.geoserve.GeoserveRegionsService;
import gov.usgs.earthquake.indexer.DefaultIndexerModule;
import gov.usgs.earthquake.indexer.IndexerModule;
import gov.usgs.earthquake.indexer.ProductSummary;
import gov.usgs.earthquake.product.Product;

import gov.usgs.util.Config;
import gov.usgs.util.StringUtils;

/**
 * Class for summarizing "origin" type products during the indexing process.
 * Specifically this implementation uses a GeoservePlacesService to augment the
 * properties on the product to include a "title" property if one is not already
 * present.
 *
 * This module may be configured with the following properties: `endpointUrl`
 * `connectTimeout`, and `readTimeout`.
 */
public class OriginIndexerModule extends DefaultIndexerModule {
  private static final Logger LOGGER = Logger.getLogger(OriginIndexerModule.class.getName());

  private GeoservePlacesService geoservePlaces;
  private GeoserveRegionsService geoserveRegions;


  /** Property for places endpoint url */
  public static final String PLACES_ENDPOINT_URL_PROPERTY = "placesEndpointUrl";
  /** property for regions endpoint url */
  public static final String REGIONS_ENDPOINT_URL_PROPERTY = "regionsEndpointUrl";
  /** property for connectTimeout */
  public static final String CONNECT_TIMEOUT_PROPERTY = "connectTimeout";
  /** Properties for readTimeout */
  public static final String READ_TIMEOUT_PROPERTY = "readTimeout";

  /** Property for Geoserve distance threshold */
  public static final String GEOSERVE_DISTANCE_THRESHOLD_PROPERTY = "geoserveDistanceThreshold";

  /**
   * Distance threshold (in km), determines whether to use fe region
   * or nearest place in the event title
   */
  public static final int DEFAULT_GEOSERVE_DISTANCE_THRESHOLD = 300;

  private int distanceThreshold;

  /**
   * Empty constructor
   * Do nothing, must be configured through bootstrapping before use
   */
  public OriginIndexerModule() {
  }

  /**
   * Constructor
   * @param geoservePlaces GeoservePlacesService
   * @param geoserveRegions GeoserveRegionsService
   */
  public OriginIndexerModule(
      final GeoservePlacesService geoservePlaces,
      final GeoserveRegionsService geoserveRegions
  ) {
    this.setPlacesService(geoservePlaces);
    this.setRegionsService(geoserveRegions);
  }

  /**
   * @return The places service currently being used to return nearby places
   */
  public GeoservePlacesService getPlacesService() {
    return this.geoservePlaces;
  }

  /**
   * @return The regions service currently being used to return fe regions
   */
  public GeoserveRegionsService getRegionsService() {
    return this.geoserveRegions;
  }

  /**
   * @return The distance threshold currently being used to default to FE region
   */
  public int getDistanceThreshold() {
    return this.distanceThreshold;
  }

  @Override
  public ProductSummary getProductSummary(Product product) throws Exception {
    ProductSummary summary = super.getProductSummary(product);
    BigDecimal latitude = summary.getEventLatitude();
    BigDecimal longitude = summary.getEventLongitude();

    // Defer to existing title property if set...
    Map<String, String> summaryProperties = summary.getProperties();
    String title = summaryProperties.get("title");

    if (title == null && latitude != null && longitude != null) {
      try {
        title = this.getEventTitle(latitude, longitude);
        summaryProperties.put("title", StringUtils.encodeAsUtf8(title));
      } catch (Exception ex) {
        LOGGER
            .warning(String.format("[%s] %s for product %s", this.getName(), ex.getMessage(), product.getId().toString()));
        // Do nothing, value-added failed. Move on.
      }
    }

    return summary;
  }

  @Override
  public int getSupportLevel(Product product) {
    int supportLevel = IndexerModule.LEVEL_UNSUPPORTED;
    String type = getBaseProductType(product.getId().getType());

    if ("origin".equals(type) && !"DELETE".equalsIgnoreCase(product.getStatus())) {
      supportLevel = IndexerModule.LEVEL_SUPPORTED;
    }

    return supportLevel;
  }

  /**
   * Set the GeoservePlacesService to be used for subsequent calls to GeoServe places
   * endpoint.
   *
   * @param geoservePlaces The GeoservePlacesService to use
   */
  public void setPlacesService(GeoservePlacesService geoservePlaces) {
    this.geoservePlaces = geoservePlaces;
  }

  /**
   * Set the geoserveRegions to be used for subsequent calls to GeoServe regions
   * endpoint.
   *
   * @param geoserveRegions The GeoserveRegions to use
   */
  public void setRegionsService(GeoserveRegionsService geoserveRegions) {
    this.geoserveRegions = geoserveRegions;
  }

  /**
   * Set the distance threshold to prefer fe region over nearst place
   * in the event title
   *
   * @param threshold The distance threshold to use
   */
  public void setDistanceThreshold(int threshold) {
    this.distanceThreshold = threshold;
  }

  @Override
  public void configure(Config config) throws Exception {
    // Distance threshold (in km)
    this.distanceThreshold = Integer.parseInt(
        config.getProperty(
            GEOSERVE_DISTANCE_THRESHOLD_PROPERTY,
            Integer.toString(DEFAULT_GEOSERVE_DISTANCE_THRESHOLD)
        )
    );

    // Geoserve Places Endpoint configuration
    String placesEndpointUrl = config.getProperty(
        PLACES_ENDPOINT_URL_PROPERTY,
        GeoservePlacesService.DEFAULT_ENDPOINT_URL
    );
    int placesEndpointConnectTimeout = Integer.parseInt(
        config.getProperty(
            CONNECT_TIMEOUT_PROPERTY,
            Integer.toString(GeoservePlacesService.DEFAULT_CONNECT_TIMEOUT)
        )
    );
    int placesEndpointReadTimeout = Integer.parseInt(
        config.getProperty(
            READ_TIMEOUT_PROPERTY,
            Integer.toString(GeoservePlacesService.DEFAULT_READ_TIMEOUT)
        )
    );
    LOGGER.config(
        String.format("[%s] GeoservePlacesService(%s, %d, %d)",
          this.getName(),
          placesEndpointUrl,
          placesEndpointConnectTimeout,
          placesEndpointReadTimeout
        )
    );
    this.setPlacesService(
        new GeoservePlacesService(
          placesEndpointUrl,
          placesEndpointConnectTimeout,
          placesEndpointReadTimeout
        )
    );

    // Geoserve Regions Endpoint configuration
    String regionsEndpointUrl = config.getProperty(
        REGIONS_ENDPOINT_URL_PROPERTY,
        GeoserveRegionsService.DEFAULT_ENDPOINT_URL
    );
    int regionsEndpointConnectTimeout = Integer.parseInt(
        config.getProperty(
            CONNECT_TIMEOUT_PROPERTY,
            Integer.toString(GeoserveRegionsService.DEFAULT_CONNECT_TIMEOUT)
        )
    );
    int regionsEndpointReadTimeout = Integer.parseInt(
        config.getProperty(
            READ_TIMEOUT_PROPERTY,
            Integer.toString(GeoserveRegionsService.DEFAULT_READ_TIMEOUT)
        )
    );
    LOGGER.config(
        String.format("[%s] GeoserveRegionsService(%s, %d, %d)",
            this.getName(),
            regionsEndpointUrl,
            regionsEndpointConnectTimeout,
            regionsEndpointReadTimeout
        )
    );
    this.setRegionsService(
        new GeoserveRegionsService(
            regionsEndpointUrl,
            regionsEndpointConnectTimeout,
            regionsEndpointReadTimeout
        )
    );
  }

  /**
   * Get the event title based on the name and location of the nearest
   * place, or if the nearest place is outside of the distance threshold
   * return the fe region name
   *
   * @param latitude event latitude in degrees
   * @param longitude event longitude in degrees
   *
   * @return {String} event name
   *
   * @throws IOException if IO error occurs
   */
  public String getEventTitle(BigDecimal latitude, BigDecimal longitude) throws Exception, IOException {
    StringBuffer messages = new StringBuffer();
    String message = null;

    try {
      final JsonObject feature = this.geoservePlaces.getNearestPlace(
          latitude,
          longitude,
          this.distanceThreshold
      );

      if (feature != null) {
        return this.formatEventTitle(feature);
      } else {
        message = "Places service returned no places within distance threshold";
        messages.append(message + ". ");
        LOGGER.log(Level.INFO, "[" + this.getName() + "] " + message);
      }
    } catch (Exception e) {
      message = "Failed to get nearest place from geoserve places service";
      messages.append(message + ". ");
      messages.append(e.getMessage() + ". ");
      LOGGER.log(Level.INFO, "[" + this.getName() + "] " + message);
    }

    try {
      return this.geoserveRegions.getFeRegionName(latitude, longitude);
    } catch (Exception e) {
      message = "Failed to get FE region name";
      messages.append(message + ". ");
      messages.append(e.getMessage() + ". ");
      LOGGER.log(Level.INFO, "[" + this.getName() + "] .");
    }

    // If we get this far, things failed spectacularly, report the error
    Exception e = new Exception(messages.toString());
    e.fillInStackTrace();
    throw e;
  }

  /**
   * Takes properties from feature and formats them into a string
   * @param feature feature to format
   * @return string with distance, direction, name, and admin
   */
  public String formatEventTitle(JsonObject feature) {
    JsonObject properties = feature.getJsonObject("properties");

    String name = properties.getString("name");
    String country = properties.getString("country_code").toLowerCase();
    String admin = properties.getString("country_name");
    int distance = properties.getInt("distance");
    double azimuth = properties.getJsonNumber("azimuth").doubleValue();
    String direction = azimuthToDirection(azimuth);

    if ("us".equals(country)) {
      admin = properties.getString("admin1_name");
    }

    return String.format("%d km %s of %s, %s", distance, direction, name, admin);
  }

  /**
   * Converts a decimal degree azimuth to a canonical compass direction
   *
   * @param azimuth The degrees azimuth to be converted
   *
   * @return {String} The canonical compass direction for the given input azimuth
   */
  public String azimuthToDirection(double azimuth) {
    double fullwind = 22.5;
    String[] directions = { "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW",
        "NNW", "N" };

    // Invert azimuth for proper directivity
    // Maybe not needed in the future.
    azimuth += 180.0;

    // adjust azimuth if negative
    while (azimuth < 0.0) {
      azimuth = azimuth + 360.0;
    }

    return directions[(int) Math.round((azimuth % 360.0) / fullwind)];
  }

  public static void main(String[] args) throws Exception {
    BigDecimal latitude = new BigDecimal("0.0");
    BigDecimal longitude = new BigDecimal("0.0");
    int maxradiuskm = DEFAULT_GEOSERVE_DISTANCE_THRESHOLD;
    final OriginIndexerModule module = new OriginIndexerModule(
      new GeoservePlacesService(),
      new GeoserveRegionsService()
    );
    module.setName("TestModule");

    for (String arg : args) {
      if (arg.startsWith("--latitude=")) {
        latitude = new BigDecimal(arg.replace("--latitude=", ""));
      } else if (arg.startsWith("--longitude=")) {
        longitude = new BigDecimal(arg.replace("--longitude=", ""));
      } else if (arg.startsWith("--maxradiuskm=")) {
        maxradiuskm = Integer.parseInt(arg.replace("--maxradiuskm=", ""));
      }
    }

    module.setDistanceThreshold(maxradiuskm);

    System.out.printf("Title[%s, %s] = `%s`\n",
        latitude.doubleValue(),
        longitude.doubleValue(),
        module.getEventTitle(latitude, longitude));
  }
}