ShakeMapIndexerModule.java

package gov.usgs.earthquake.shakemap;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;

import gov.usgs.earthquake.indexer.DefaultIndexerModule;
import gov.usgs.earthquake.indexer.IndexerModule;
import gov.usgs.earthquake.indexer.ProductSummary;
import gov.usgs.earthquake.product.Content;
import gov.usgs.earthquake.product.Product;
import gov.usgs.util.StreamUtils;

/**
 * ShakeMap Indexer Module.
 *
 * Provides a higher and more specific level of support for ShakeMap products,
 * including reading additional product information out of the ShakeMap content
 * files provided with the Product and placing it into the ProductSummary for
 * the Product itself.
 */
public class ShakeMapIndexerModule extends DefaultIndexerModule {

  private static final Logger LOGGER = Logger.getLogger(ShakeMapIndexerModule.class.getName());

  /** Path to overlay img */
  public static final String OVERLAY_IMAGE_PATH = "download/ii_overlay.png";
  /** Property for overlay width */
  public static final String OVERLAY_WIDTH_PROPERTY = "overlayWidth";
  /** Property for overlay height */
  public static final String OVERLAY_HEIGHT_PROPERTY = "overlayHeight";

  /** CONTAINS_EPICENTER_WEIGHT */
  public static final int CONTAINS_EPICENTER_WEIGHT = 50;
  /** CENTERED_ON_EPICENTER_WEIGHT */
  public static final int CENTERED_ON_EPICENTER_WEIGHT = 25;
  /**
   * Number of degrees at which no additional weight will be assigned based on the
   * proximity of the map center to the epicenter.
   */
  public static final double MAX_DELTA_DEGREES = 2.0;

  /** ShakeMap atlas is the most preferred ShakeMap contributor */
  public static final String SHAKEMAP_ATLAS_SOURCE = "atlas";
  /** Atlas weight */
  public static final int SHAKEMAP_ATLAS_WEIGHT = 200;

  @Override
  public int getSupportLevel(Product product) {
    int supportLevel = IndexerModule.LEVEL_UNSUPPORTED;
    String type = getBaseProductType(product.getId().getType());
    // Support only ShakeMap products that contain grid.xml
    if (type.equals("shakemap") && product.getContents().containsKey(ShakeMap.GRID_XML_ATTACHMENT))
      supportLevel = IndexerModule.LEVEL_SUPPORTED;
    return supportLevel;
  }

  @Override
  public ProductSummary getProductSummary(Product product) throws Exception {
    // Load additional properties into the ProductSummary by loading these
    // properties specifically through a ShakeMap product
    ProductSummary summary = super.getProductSummary(new ShakeMap(product));

    Content overlayImage = product.getContents().get(OVERLAY_IMAGE_PATH);
    if (overlayImage != null) {
      InputStream overlayInputStream = null;
      try {
        overlayInputStream = overlayImage.getInputStream();
        BufferedImage info = ImageIO.read(overlayInputStream);
        summary.getProperties().put(OVERLAY_WIDTH_PROPERTY, Integer.toString(info.getWidth()));
        summary.getProperties().put(OVERLAY_HEIGHT_PROPERTY, Integer.toString(info.getHeight()));
        LOGGER.finest("overlay width=" + info.getWidth() + ", overlay height=" + info.getHeight());
      } catch (IOException e) {
        LOGGER.log(Level.WARNING, "exception reading " + OVERLAY_IMAGE_PATH + " width/height", e);
      } finally {
        StreamUtils.closeStream(overlayInputStream);
      }
    }

    return summary;
  }

  @Override
  protected long getPreferredWeight(ProductSummary summary) throws Exception {
    // Get the default preferred weight value from the parent class
    long weight = super.getPreferredWeight(summary);

    if (SHAKEMAP_ATLAS_SOURCE.equals(summary.getSource())) {
      weight += SHAKEMAP_ATLAS_WEIGHT;
    }

    // check that shakemap has event properties and map extents
    Map<String, String> properties = summary.getProperties();
    if (summary.getEventLatitude() == null || summary.getEventLongitude() == null
        || properties.get(ShakeMap.MINIMUM_LATITUDE_PROPERTY) == null
        || properties.get(ShakeMap.MAXIMUM_LATITUDE_PROPERTY) == null
        || properties.get(ShakeMap.MINIMUM_LONGITUDE_PROPERTY) == null
        || properties.get(ShakeMap.MAXIMUM_LONGITUDE_PROPERTY) == null) {
      return weight;
    }

    // Get properties for comparison to alter authoritative weight
    BigDecimal eventLat = summary.getEventLatitude();
    BigDecimal eventLon = summary.getEventLongitude();
    BigDecimal minLat = new BigDecimal(properties.get(ShakeMap.MINIMUM_LATITUDE_PROPERTY));
    BigDecimal maxLat = new BigDecimal(properties.get(ShakeMap.MAXIMUM_LATITUDE_PROPERTY));
    BigDecimal minLon = new BigDecimal(properties.get(ShakeMap.MINIMUM_LONGITUDE_PROPERTY));
    BigDecimal maxLon = new BigDecimal(properties.get(ShakeMap.MAXIMUM_LONGITUDE_PROPERTY));
    BigDecimal centerLat = minLat.add(maxLat).divide(new BigDecimal(2));
    BigDecimal centerLon = minLon.add(maxLon).divide(new BigDecimal(2));

    // Calculate delta in degrees between map center and event epicenter
    double latDelta = Math.abs(centerLat.doubleValue() - eventLat.doubleValue());
    double lonDelta = Math.abs(centerLon.doubleValue() - eventLon.doubleValue());
    double locationDelta = (double) Math.sqrt(Math.pow(latDelta, 2) + Math.pow(lonDelta, 2));

    // Increase weight dynamically if the map center is within
    // MAX_DELTA_DEGREES of the event epicenter
    if (locationDelta <= MAX_DELTA_DEGREES) {
      // Add more weight based on the map center being closer to
      // the event epicenter
      weight += Math.round((1 - (locationDelta / MAX_DELTA_DEGREES)) * CENTERED_ON_EPICENTER_WEIGHT);
    }

    // Increase weight further if the map contains the epicenter within
    // its boundaries.
    if (eventLat.longValue() < maxLat.longValue() && eventLat.longValue() > minLat.longValue()
        && eventLon.longValue() < maxLon.longValue() && eventLon.longValue() > minLon.longValue()) {
      weight += CONTAINS_EPICENTER_WEIGHT;
    }

    return weight;
  }

}