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;
	}

}