Event.java

/*
 * Event
 */
package gov.usgs.earthquake.indexer;

import gov.usgs.earthquake.product.ProductId;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * An event is a group of products that are nearby in space and time.
 *
 * Which products appear in an event depend primarily on the
 * ProductIndexQuery.ResultType that is used when retrieving an event from the
 * index. Unless CURRENT is used, you may not get what you expect.
 */
public class Event implements Comparable<Event> {

	/** Origin product type */
	public static final String ORIGIN_PRODUCT_TYPE = "origin";
	/** Associate product type */
	public static final String ASSOCIATE_PRODUCT_TYPE = "associate";
	/** Disassociate product type */
	public static final String DISASSOCIATE_PRODUCT_TYPE = "disassociate";
	/** Property for othereventsource */
	public static final String OTHEREVENTSOURCE_PROPERTY = "othereventsource";
	/** Property for othereventsourcecode */
	public static final String OTHEREVENTSOURCECODE_PROPERTY = "othereventsourcecode";

	/** An ID used by the ProductIndex. */
	private Long indexId = null;

	/** Products nearby in space and time. Keyed by type. */
	private Map<String, List<ProductSummary>> products = new HashMap<String, List<ProductSummary>>();

	/** Cached summary. */
	private EventSummary eventSummary = null;

	/**
	 * Default constructor.
	 *
	 * All fields are set to null, and the list of products is empty.
	 */
	public Event() {
	}

	/**
	 * Construct an event with only an indexId. The products map will be empty.
	 *
	 * @param indexId
	 *            the indexId to set.
	 */
	public Event(final Long indexId) {
		this.setIndexId(indexId);
	}

	/**
	 * Construct and event with an indexId and a list of products.
	 *
	 * @param indexId
	 *            the product index id.
	 * @param products
	 *            the list of products.
	 */
	public Event(final Long indexId,
			final Map<String, List<ProductSummary>> products) {
		this.setIndexId(indexId);
		this.setProducts(products);
	}

	/**
	 * Copy constructor for event.
	 *
	 * The products associated with this event are not cloned, but the list of
	 * products is.
	 *
	 * @param copy
	 *            the event to clone.
	 */
	public Event(final Event copy) {
		this(copy.getIndexId(), copy.getAllProducts());
	}

	/**
	 * Get the index id.
	 *
	 * @return the indexId or null if one hasn't been assigned.
	 */
	public Long getIndexId() {
		return indexId;
	}

	/**
	 * Set the index id.
	 *
	 * @param indexId
	 *            the indexId to set.
	 */
	public void setIndexId(Long indexId) {
		this.indexId = indexId;
	}

	/**
	 * Get all products associated with event, even if they are deleted.
	 *
	 * @return all products associated with event.
	 */
	public Map<String, List<ProductSummary>> getAllProducts() {
		return products;
	}

	/**
	 * Get the event products.
	 *
	 * Only returns products that have not been deleted or superseded. This
	 * method returns a copy of the underlying product map that has been
	 * filtered to remove deleted products.
	 *
	 * @return a map of event products.
	 * @see #getAllProducts()
	 */
	public Map<String, List<ProductSummary>> getProducts() {
		Map<String, List<ProductSummary>> notDeleted = new HashMap<String, List<ProductSummary>>();
		Iterator<String> types = products.keySet().iterator();
		while (types.hasNext()) {
			String type = types.next();
			List<ProductSummary> notDeletedProducts = getProducts(type);
			if (notDeletedProducts.size() > 0) {
				notDeleted.put(type, notDeletedProducts);
			}
		}
		return notDeleted;
	}

	/**
	 * Set products.
	 *
	 * ProductSummaries are not cloned, but lists are.
	 *
	 * @param newProducts
	 *            the products to set.
	 */
	public void setProducts(final Map<String, List<ProductSummary>> newProducts) {
		this.products.clear();
		Iterator<String> iter = new TreeSet<String>(newProducts.keySet())
				.iterator();
		while (iter.hasNext()) {
			String type = iter.next();
			this.products.put(type,
					new ArrayList<ProductSummary>(newProducts.get(type)));
		}
		eventSummary = null;
	}

	/**
	 * A convenience method for adding a product summary to an event object.
	 *
	 * Note: this method does not update any associated product index.
	 *
	 * @param summary
	 *            the summary to add to this event.
	 */
	public void addProduct(final ProductSummary summary) {
		String type = summary.getId().getType();
		List<ProductSummary> list = products.get(type);
		if (list == null) {
			list = new ArrayList<ProductSummary>();
			products.put(type, list);
		}
		if (!list.contains(summary)) {
			list.add(summary);
		}
		eventSummary = null;
	}

	/**
	 * A convenience method for removing a product summary from an event object.
	 *
	 * Note: this method does not update any associated product index.
	 *
	 * @param summary
	 *            the summary to remove from this event.
	 */
	public void removeProduct(final ProductSummary summary) {
		String type = summary.getId().getType();
		// find the list of products of this type
		List<ProductSummary> list = products.get(type);
		if (list != null) {
			// remove the product from the list
			list.remove(summary);
			if (list.size() == 0) {
				// if the list is now empty, remove the list
				products.remove(type);
			}
		}
		eventSummary = null;
	}

	/**
	 * Convenience method to get products of a given type.
	 *
	 * This method always returns a copy of the internal list, and may be empty.
	 * Only returns products that have not been deleted or superseded.
	 *
	 * @param type
	 *            the product type.
	 * @return a list of products of that type, which may be empty.
	 */
	public List<ProductSummary> getProducts(final String type) {
		ArrayList<ProductSummary> typeProducts = new ArrayList<ProductSummary>();

		if (products.containsKey(type)) {
			// only return products that haven't been deleted
			typeProducts.addAll(getWithoutDeleted(getWithoutSuperseded(products
					.get(type))));
		}

		return typeProducts;
	}

	/**
	 * Get all event products (including those that are deleted or superseded).
	 *
	 * @return a list of event products.
	 */
	public List<ProductSummary> getAllProductList() {
		List<ProductSummary> allProductList = new ArrayList<ProductSummary>();
		Map<String, List<ProductSummary>> allProducts = getAllProducts();
		Iterator<String> iter = allProducts.keySet().iterator();
		while (iter.hasNext()) {
			allProductList.addAll(allProducts.get(iter.next()));
		}
		return allProductList;
	}

	/**
	 * Get all event products that have not been deleted or superseded as a
	 * list.
	 *
	 * @return a list of event products.
	 */
	public List<ProductSummary> getProductList() {
		List<ProductSummary> productList = new ArrayList<ProductSummary>();
		Map<String, List<ProductSummary>> notDeletedProducts = getProducts();
		Iterator<String> iter = notDeletedProducts.keySet().iterator();
		while (iter.hasNext()) {
			productList.addAll(notDeletedProducts.get(iter.next()));
		}
		return productList;
	}

	/**
	 * Get preferred products of all types.
	 *
	 * This map will contain one product of each type, chosen by preferred
	 * weight.
	 *
	 * @return a map from product type to the preferred product of that type.
	 */
	public Map<String, ProductSummary> getPreferredProducts() {
		Map<String, ProductSummary> preferredProducts = new HashMap<String, ProductSummary>();

		Map<String, List<ProductSummary>> notDeletedProducts = getProducts();
		Iterator<String> types = notDeletedProducts.keySet().iterator();
		while (types.hasNext()) {
			String type = types.next();
			preferredProducts.put(type,
					getPreferredProduct(notDeletedProducts.get(type)));
		}

		return preferredProducts;
	}

	/**
	 * Get the preferred product of a specific type.
	 *
	 * @param type
	 *            type of product to get.
	 * @return most preferred product of that type, or null if no product of
	 *         that type is associated.
	 */
	public ProductSummary getPreferredProduct(final String type) {
		return getPreferredProduct(getProducts(type));
	}

	/**
	 * Get a map of all event ids associated with this event.
	 *
	 * Same as Event.getEventCodes(this.getAllProductList());
	 *
	 * @deprecated use {@link #getAllEventCodes(boolean)} instead.
	 * @return map of all event ids associated with this event.
	 */
	public Map<String, String> getEventCodes() {
		return getEventCodes(this.getAllProductList());
	}

	/**
	 * Get a map of all event ids associated with this event.
	 *
	 * Map key is eventSource, Map value is eventSourceCode.
	 *
	 * @deprecated use {@link #getAllEventCodes(boolean)} instead.
	 * @param summaries
	 *            the summaries list to extract event codes from.
	 * @return map of all event ids associated with this event.
	 */
	public static Map<String, String> getEventCodes(
			final List<ProductSummary> summaries) {
		Map<String, String> eventIds = new HashMap<String, String>();
		// order most preferred last,
		// to minimize impact of multiple codes from same source
		List<ProductSummary> sorted = getSortedMostPreferredFirst(
				getWithoutSuperseded(summaries));
		Collections.reverse(sorted);
		// done ordering
		Iterator<ProductSummary> iter = sorted.iterator();
		while (iter.hasNext()) {
			ProductSummary product = iter.next();
			String source = product.getEventSource();
			String code = product.getEventSourceCode();
			if (source != null && code != null) {
				eventIds.put(source.toLowerCase(), code.toLowerCase());
			}
		}
		return eventIds;
	}

	/**
	 * Get a map of all event ids associated with this event, recognizing that
	 * one source may have multiple codes (they broke the rules, but it
	 * happens).
	 *
	 * @param includeDeleted
	 *            whether to include ids for sub events whose products have all
	 *            been deleted.
	 * @return Map from source to a list of codes from that source.
	 */
	public Map<String, List<String>> getAllEventCodes(
			final boolean includeDeleted) {
		Map<String, List<String>> allEventCodes = new HashMap<String, List<String>>();

		Map<String, Event> subEvents = getSubEvents();
		Iterator<String> iter = subEvents.keySet().iterator();
		while (iter.hasNext()) {
			Event subEvent = subEvents.get(iter.next());
			if (!includeDeleted && subEvent.isDeleted()) {
				// check for non-deleted products that should
				// keep the event code alive
				List<ProductSummary> nonDeletedProducts = getWithoutDeleted(
						getWithoutSuperseded(subEvent.getAllProductList()));
				if (nonDeletedProducts.size() == 0) {
					// filter deleted events
					continue;
				}
				// otherwise, event has active products;
				// prevent same source associations
			}

			// add code to list for source
			String source = subEvent.getSource();
			String sourceCode = subEvent.getSourceCode();
			List<String> sourceEventCodes = allEventCodes.get(source);
			if (sourceEventCodes == null) {
				// create list for source
				sourceEventCodes = new ArrayList<String>();
				allEventCodes.put(source, sourceEventCodes);
			}
			// keep list distinct
			if (!sourceEventCodes.contains(sourceCode)) {
				sourceEventCodes.add(sourceCode);
			}
		}

		return allEventCodes;
	}

	/**
	 * Get a list of all the preferred products sorted based on their
	 * authoritative weights
	 *
	 * @return sorted list of ProductSummary objects
	 */
	public List<ProductSummary> getPreferredProductsSorted() {
		Map<String, ProductSummary> preferred = getPreferredProducts();

		// Transform the preferred HashMap into a List so we can sort based on
		// preferred weight
		List<ProductSummary> productList = new ArrayList<ProductSummary>(preferred.values());

		// Sort the list, then iterate through it until we find the specified
		// property
		Collections.sort(productList, new MostPreferredFirstComparator());
		return productList;
	}

	/**
	 * Get the event id.
	 *
	 * The event id is the combination of event source and event source code.
	 *
	 * @return the event id, or null if either event source or event source code
	 *         is null.
	 * @see #getSource()
	 * @see #getSourceCode()
	 */
	public String getEventId() {
		ProductSummary product = getEventIdProduct();
		if (product != null) {
			return product.getEventId();
		}
		return null;
	}

	/**
	 * Get the preferred source for this event. If an origin product exists,
	 * it's value is used.
	 *
	 * @return Source from preferred product or null
	 */
	public String getSource() {
		ProductSummary product = getEventIdProduct();
		if (product != null) {
			return product.getEventSource();
		}
		return null;
	}

	/**
	 * Get the preferred source code for this event. If an origin product
	 * exists, it's value is used.
	 *
	 * @return Source code from preferred product or null
	 */
	public String getSourceCode() {
		ProductSummary product = getEventIdProduct();
		if (product != null) {
			return product.getEventSourceCode();
		}
		return null;
	}

	/**
	 * Get the product used for eventsource and eventsourcecode.
	 *
	 * Event ID comes from the preferred origin product.
	 *
	 * @return The most preferred product summary. This summary is used to
	 *         determine the eventsouce and eventsourcecode.
	 * @see #getPreferredOriginProduct()
	 */
	protected ProductSummary getEventIdProduct() {
		ProductSummary product = getPreferredOriginProduct();
		if (product == null) {
			product = getProductWithOriginProperties();
		}
		return product;
	}

	/**
	 * Get the most recent product with origin properties (id, lat, lon, time).
	 *
	 * <strong>NOTE</strong>: this product may have been superseded by a delete.
	 * When an event has not been deleted, this method should be consistent with
	 *  {@link #getPreferredOriginProduct()}.
	 *
	 * Products are checked in the following order, sorted most preferred first
	 * within each group.  The first matching product is returned:
	 * <ol>
	 * <li>"origin" products not superseded or deleted,
	 * 		that have origin properties</li>
	 * <li>"origin" products superseded by a delete,
	 * 		that have origin properties</li>
	 * <li>products not superseded or deleted,
	 * 		that have origin properties</li>
	 * <li>products superseded by a delete,
	 * 		that have origin properties</li>
	 * </ol>
	 *
	 * @return the most recent product with origin properties.
	 * @see #productHasOriginProperties(ProductSummary)
	 */
	public ProductSummary getProductWithOriginProperties() {
		Map<String, List<ProductSummary>> allProducts = getAllProducts();
		List<ProductSummary> productsList = null;
		ProductSummary preferredProduct = null;
		Iterator<ProductSummary> iter = null;

		productsList = allProducts.get(ORIGIN_PRODUCT_TYPE);
		if (productsList != null) {
			// "origin" products not superseded or deleted
			productsList = getSortedMostPreferredFirst(getWithoutDeleted(
					getWithoutSuperseded(allProducts.get(ORIGIN_PRODUCT_TYPE))));
			iter = productsList.iterator();
			while (iter.hasNext()) {
				preferredProduct = iter.next();
				if (productHasOriginProperties(preferredProduct)) {
					return preferredProduct;
				}
			}

			// "origin" products superseded by a delete
			productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
					getWithoutDeleted(allProducts.get(ORIGIN_PRODUCT_TYPE))));
			iter = productsList.iterator();
			while (iter.hasNext()) {
				preferredProduct = iter.next();
				if (productHasOriginProperties(preferredProduct)) {
					return preferredProduct;
				}
			}
		}

		// products not superseded or deleted
		productsList = getSortedMostPreferredFirst(getWithoutDeleted(
				getWithoutSuperseded(productTypeMapToList(allProducts))));
		iter = productsList.iterator();
		while (iter.hasNext()) {
			preferredProduct = iter.next();
			if (productHasOriginProperties(preferredProduct)) {
				return preferredProduct;
			}
		}

		// products superseded by a delete
		productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
				getWithoutDeleted(productTypeMapToList(allProducts))));
		iter = productsList.iterator();
		while (iter.hasNext()) {
			preferredProduct = iter.next();
			if (productHasOriginProperties(preferredProduct)) {
				return preferredProduct;
			}
		}

		return null;
	}

	/**
	 * Get the most preferred origin-like product for this event.
	 *
	 * The event is considered deleted if the returned product is null, deleted,
	 * or does not have origin properties.  Information about the event
	 * may still be available using {@link #getProductWithOriginProperties()}.
	 *
	 * Products are checked in the following order, sorted most preferred first
	 * within each group.  The first matching product is returned:
	 * <ul>
	 * <li>If any "origin" products exist:
	 * 		<ol>
	 * 		<li>"origin" products not superseded or deleted,
	 * 				that have origin properties.</li>
	 * 		<li>"origin" products not superseded,
	 * 				that have an event id.</li>
	 * 		</ol>
	 * </li>
	 * <li>If no "origin" products exist:
	 * 		<ol>
	 * 		<li>products not superseded or deleted,
	 * 				that have origin properties.</li>
	 * 		<li>products not superseded,
	 * 				that have an event id.</li>
	 * 		</ol>
	 * </li>
	 * </ul>
	 *
	 * @return the most recent product with origin properties.
	 * @see #productHasOriginProperties(ProductSummary)
	 */
	public ProductSummary getPreferredOriginProduct() {
		Map<String, List<ProductSummary>> allProducts = getAllProducts();
		List<ProductSummary> productsList = null;
		ProductSummary preferredProduct = null;
		Iterator<ProductSummary> iter = null;

		productsList = allProducts.get(ORIGIN_PRODUCT_TYPE);
		if (productsList != null) {
			// "origin" products not superseded or deleted,
			// that have origin properties
			productsList = getSortedMostPreferredFirst(getWithoutDeleted(
					getWithoutSuperseded(allProducts.get(ORIGIN_PRODUCT_TYPE))));
			iter = productsList.iterator();
			while (iter.hasNext()) {
				preferredProduct = iter.next();
				if (productHasOriginProperties(preferredProduct)) {
					return preferredProduct;
				}
			}

			// "origin" products not superseded,
			// that have event id
			productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
					allProducts.get(ORIGIN_PRODUCT_TYPE)));
			iter = productsList.iterator();
			while (iter.hasNext()) {
				preferredProduct = iter.next();
				if (preferredProduct.getEventSource() != null
						&& preferredProduct.getEventSourceCode() != null) {
					return preferredProduct;
				}
			}

			return null;
		}

		// products not superseded or deleted,
		// that have origin properties
		productsList = getSortedMostPreferredFirst(getWithoutDeleted(
				getWithoutSuperseded(productTypeMapToList(allProducts))));
		iter = productsList.iterator();
		while (iter.hasNext()) {
			preferredProduct = iter.next();
			if (productHasOriginProperties(preferredProduct)) {
				return preferredProduct;
			}
		}

		// products not superseded,
		// that have event id
		productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
				productTypeMapToList(allProducts)));
		iter = productsList.iterator();
		while (iter.hasNext()) {
			preferredProduct = iter.next();
			if (preferredProduct.getEventSource() != null
					&& preferredProduct.getEventSourceCode() != null) {
				return preferredProduct;
			}
		}

		return null;
	}

	/**
	 * Check if a product can define an event (id, lat, lon, time).
	 *
	 * @param product
	 *            product to check.
	 * @return true if product has id, lat, lon, and time properties.
	 */
	public static boolean productHasOriginProperties(
			final ProductSummary product) {
		return (product.getEventSource() != null
				&& product.getEventSourceCode() != null
				&& product.getEventLatitude() != null
				&& product.getEventLongitude() != null && product
					.getEventTime() != null);
	}

	/**
	 * Get the most preferred magnitude product for event.
	 *
	 * Currently calls {@link #getPreferredOriginProduct()}.
	 *
	 * @return the most preferred magnitude product for event.
	 */
	public ProductSummary getPreferredMagnitudeProduct() {
		return getPreferredOriginProduct();
	}

	/**
	 * Get the preferred time for this event. If an origin product exists, it's
	 * value is used.
	 *
	 * @return Time from preferred product or null
	 */
	public Date getTime() {
		ProductSummary preferred = getProductWithOriginProperties();
		if (preferred != null) {
			return preferred.getEventTime();
		}
		return null;
	}

	/**
	 * Get the preferred latitude for this event. If an origin product exists,
	 * it's value is used.
	 *
	 * @return Latitude from preferred product or null
	 */
	public BigDecimal getLatitude() {
		ProductSummary preferred = getProductWithOriginProperties();
		if (preferred != null) {
			return preferred.getEventLatitude();
		}
		return null;

	}

	/**
	 * Get the preferred longitude for this event. If an origin product exists,
	 * it's value is used.
	 *
	 * @return Longitude from preferred product or null
	 */
	public BigDecimal getLongitude() {
		ProductSummary preferred = getProductWithOriginProperties();
		if (preferred != null) {
			return preferred.getEventLongitude();
		}
		return null;

	}

	/**
	 * Event update time is most recent product update time.
	 *
	 * @return the most recent product update time.
	 */
	public Date getUpdateTime() {
		Date updateTime = null;
		Date time = null;
		Iterator<ProductSummary> iter = getAllProductList().iterator();
		while (iter.hasNext()) {
			time = iter.next().getId().getUpdateTime();
			if (updateTime == null || time.after(updateTime)) {
				time = updateTime;
			}
		}
		return updateTime;
	}

	/**
	 * Get the preferred depth for this event. If an origin product exists, it's
	 * value is used.
	 *
	 * @return Depth from preferred product or null
	 */
	public BigDecimal getDepth() {
		ProductSummary preferred = getProductWithOriginProperties();
		if (preferred != null) {
			return preferred.getEventDepth();
		}
		return null;
	}

	/**
	 * Get the preferred magntitude for this event. If an origin product exists, it's
	 * value is used.
	 *
	 * @return magnitude from preferred product or null
	 */
	public BigDecimal getMagnitude() {
		ProductSummary preferred = getPreferredMagnitudeProduct();
		if (preferred != null) {
			return preferred.getEventMagnitude();
		}
		return null;
	}

	/**
	 * @return boolean if the preferred event is deleted
	 */
	public boolean isDeleted() {
		ProductSummary preferred = getPreferredOriginProduct();
		if (preferred != null && !preferred.isDeleted() &&
				Event.productHasOriginProperties(preferred)) {
			// have "origin" type product, that isn't deleted,
			// and has origin properties
			return false;
		}
		// otherwise, deleted
		return true;
	}

	/**
	 * Get the most preferred product from a list of products.
	 *
	 * @param all
	 *            a list of products containing only one type of product.
	 * @return the product with the highest preferred weight, and if tied the
	 *         most recent update time wins.
	 */
	public static ProductSummary getPreferredProduct(
			final List<ProductSummary> all) {
		ProductSummary preferred = null;

		Iterator<ProductSummary> iter = all.iterator();
		while (iter.hasNext()) {
			ProductSummary summary = iter.next();
			if (preferred == null) {
				preferred = summary;
			} else {
				long summaryWeight = summary.getPreferredWeight();
				long preferredWeight = preferred.getPreferredWeight();
				if (summaryWeight > preferredWeight
						|| (summaryWeight == preferredWeight && summary.getId()
								.getUpdateTime()
								.after(preferred.getId().getUpdateTime()))) {
					preferred = summary;
				}
			}
		}
		return preferred;
	}

	/**
	 * Summarize this event into preferred values.
	 *
	 * NOTE: the event summary may include information from an origin product,
	 * even when the preferred origin for the event has been deleted.  Use
	 * getPreferredOriginProduct() to check the preferred origin of the event.
	 *
	 * @return an event summary.
	 */
	public EventSummary getEventSummary() {
		if (eventSummary != null) {
			return eventSummary;
		}

		EventSummary summary = new EventSummary();
		summary.setIndexId(this.getIndexId());
		summary.setDeleted(this.isDeleted());

		ProductSummary eventIdProduct = this.getEventIdProduct();
		if (eventIdProduct != null) {
			summary.setSource(eventIdProduct.getEventSource());
			summary.setSourceCode(eventIdProduct.getEventSourceCode());
		}

		ProductSummary originProduct = this.getProductWithOriginProperties();
		if (originProduct != null) {
			summary.setLatitude(originProduct.getEventLatitude());
			summary.setLongitude(originProduct.getEventLongitude());
			summary.setTime(originProduct.getEventTime());
			summary.setDepth(originProduct.getEventDepth());
		}

		ProductSummary magnitudeProduct = this.getPreferredMagnitudeProduct();
		if (magnitudeProduct != null) {
			summary.setMagnitude(magnitudeProduct.getEventMagnitude());
		}

		// we may be able to avoid implementing this here, since the mapping
		// interface will be driven by the PHP product index.
		summary.getEventCodes().putAll(this.getEventCodes());

		// cache summary
		eventSummary = summary;

		return summary;
	}

	/**
	 * Comparison class that compares two ProductSummary objects based on their
	 * preferred weight and update time.
	 *
	 */
	static class MostPreferredFirstComparator implements
			Comparator<ProductSummary> {

		@Override
		public int compare(ProductSummary p1, ProductSummary p2) {
			if (p1.getPreferredWeight() > p2.getPreferredWeight()) {
				return -1;
			} else if (p1.getPreferredWeight() < p2.getPreferredWeight()) {
				return 1;
			} else {
				Date p1Update = p1.getUpdateTime();
				Date p2Update = p2.getUpdateTime();
				if (p1Update.after(p2Update)) {
					return -1;
				} else if (p2Update.after(p1Update)) {
					return 1;
				} else {
					return 0;
				}
			}
		}
	}

	@Override
	public int compareTo(Event that) {
		int r;

		List<ProductSummary> thisProducts = this.getProductList();
		List<ProductSummary> thatProducts = that.getProductList();
		if ((r = (thatProducts.size() - thisProducts.size())) != 0) {
			return r;
		}

		Iterator<ProductSummary> thisIter = thisProducts.iterator();
		Iterator<ProductSummary> thatIter = thatProducts.iterator();
		while (thisIter.hasNext() && thatIter.hasNext()) {
			// just compare product ids for now
			r = thisIter.next().getId().compareTo(thatIter.next().getId());
			if (r != 0) {
				return r;
			}
		}

		return 0;
	}

	/**
	 * Find the most preferred product.
	 *
	 * If preferredType is not null, products of this type are favored over
	 * those not of this type.
	 *
	 * If preferredNotNullProperty is not null, products that have this property
	 * set are favored over those without this property set.
	 *
	 * @param products
	 *            the list of products to search.
	 * @param preferredType
	 *            the preferred product type, if available.
	 * @param preferredNotNullProperty
	 *            the preferred property name, if available.
	 * @return The most preferred product summary of the given type.
	 */
	public static ProductSummary getMostPreferred(
			final List<ProductSummary> products, final String preferredType,
			final String preferredNotNullProperty) {
		ProductSummary mostPreferred = null;

		Iterator<ProductSummary> iter = products.iterator();
		while (iter.hasNext()) {
			ProductSummary next = iter.next();

			// ignore products that don't have the preferredNotNullProperty
			if (preferredNotNullProperty != null
					&& next.getProperties().get(preferredNotNullProperty) == null) {
				continue;
			}

			if (mostPreferred == null) {
				// first product is most preferred so far
				mostPreferred = next;
				continue;
			}

			if (preferredType != null) {
				if (next.getType().equals(preferredType)) {
					if (!mostPreferred.getType().equals(preferredType)) {
						// prefer products of this type
						mostPreferred = next;
					}
				} else if (mostPreferred.getType().equals(preferredType)) {
					// already have preferred product of preferred type
					continue;
				}
			}

			if (next.getPreferredWeight() > mostPreferred.getPreferredWeight()) {
				// higher preferred weight
				mostPreferred = next;
			} else if (next.getPreferredWeight() == mostPreferred
					.getPreferredWeight()
					&& next.getUpdateTime()
							.after(mostPreferred.getUpdateTime())) {
				// same preferred weight, newer update
				mostPreferred = next;
			}
		}

		return mostPreferred;
	}

	/**
	 * Remove deleted products from the list.
	 *
	 * @param products
	 *            list of products to filter.
	 * @return copy of the products list with deleted products removed.
	 */
	public static List<ProductSummary> getWithoutDeleted(
			final List<ProductSummary> products) {
		List<ProductSummary> withoutDeleted = new ArrayList<ProductSummary>();

		Iterator<ProductSummary> iter = products.iterator();
		while (iter.hasNext()) {
			ProductSummary next = iter.next();
			if (!next.isDeleted()) {
				withoutDeleted.add(next);
			}
		}

		return withoutDeleted;
	}

	/**
	 * Remove deleted products from the list.
	 *
	 * @param products
	 *            list of products to filter.
	 * @return copy of the products list with deleted products removed.
	 */
	public static List<ProductSummary> getWithEventId(
			final List<ProductSummary> products) {
		List<ProductSummary> withEventId = new ArrayList<ProductSummary>();

		Iterator<ProductSummary> iter = products.iterator();
		while (iter.hasNext()) {
			ProductSummary next = iter.next();
			if (next.getEventId() != null) {
				withEventId.add(next);
			}
		}

		return withEventId;
	}

	/**
	 * Remove old versions of products from the list.
	 *
	 * @param products
	 *            list of products to filter.
	 * @return a copy of the products list with products of the same
	 *         source+type+code but with older updateTimes (superseded) removed.
	 */
	public static List<ProductSummary> getWithoutSuperseded(
			final List<ProductSummary> products) {
		// place product into latest, keyed by source+type+code,
		// keeping only most recent update for each key
		Map<String, ProductSummary> latest = new HashMap<String, ProductSummary>();
		Iterator<ProductSummary> iter = products.iterator();
		while (iter.hasNext()) {
			ProductSummary summary = iter.next();
			ProductId id = summary.getId();

			// key is combination of source, type, and code
			// since none of these may contain ":", it is used as a delimiter to
			// prevent collisions.
			String key = new StringBuffer(id.getSource()).append(":").append(
					id.getType()).append(":").append(id.getCode()).toString();
			if (!latest.containsKey(key)) {
				// first product
				latest.put(key, summary);
			} else {
				// keep latest product
				ProductSummary other = latest.get(key);
				if (other.getId().getUpdateTime().before(id.getUpdateTime())) {
					latest.put(key, summary);
				}
			}
		}

		// those that are in the latest map have not been superseded
		return new ArrayList<ProductSummary>(latest.values());
	}

	/**
	 * Sort a list of products, most preferred first.
	 *
	 * @param products
	 *            the list of products to sort.
	 * @return a copy of the list sorted with most preferred first.
	 */
	public static List<ProductSummary> getSortedMostPreferredFirst(
			final List<ProductSummary> products) {
		List<ProductSummary> mostPreferredFirst = new ArrayList<ProductSummary>(
				products);
		Collections
				.sort(mostPreferredFirst, new MostPreferredFirstComparator());
		return mostPreferredFirst;
	}

	static List<ProductSummary> productTypeMapToList(
			final Map<String, List<ProductSummary>> products) {
		List<ProductSummary> list = new ArrayList<ProductSummary>();

		Iterator<String> iter = products.keySet().iterator();
		while (iter.hasNext()) {
			list.addAll(products.get(iter.next()));
		}

		return list;
	}

	static Map<String, List<ProductSummary>> productListToTypeMap(
			final List<ProductSummary> products) {
		Map<String, List<ProductSummary>> typeMap = new HashMap<String, List<ProductSummary>>();

		Iterator<ProductSummary> iter = products.iterator();
		while (iter.hasNext()) {
			ProductSummary product = iter.next();
			List<ProductSummary> typeProducts = typeMap.get(product.getType());
			if (typeProducts == null) {
				typeProducts = new ArrayList<ProductSummary>();
				typeMap.put(product.getType(), typeProducts);
			}
			typeProducts.add(product);
		}

		return typeMap;
	}

	/**
	 * Return a list of sub-events that make up this event.
	 *
	 * Event lines are drawn by eventid. Products that have no eventid are
	 * included with the sub event whose id is considered preferred.
	 *
	 * @return map from eventid to event object with products for that eventid.
	 */
	public Map<String, Event> getSubEvents() {
		// Map of sub-events keyed by product "eventId"
		Map<String, Event> subEvents = new HashMap<String, Event>();

		// Map of events by source_type_code
		Map<String, Event> productEvents = new HashMap<String, Event>();

		// this is the event that will have products without event id...
		String preferredEventId = this.getEventId();
		Event preferredSubEvent = new Event();
		// put a placeholder with no products into the map for this purpose.
		subEvents.put(preferredEventId, preferredSubEvent);

		// List of all products associated to the current event
		List<ProductSummary> allProducts = this.getAllProductList();

		// handle products with a current version
		HashSet<ProductSummary> withoutSuperseded = new HashSet<ProductSummary>(getWithoutSuperseded(allProducts));
		Iterator<ProductSummary> products = withoutSuperseded.iterator();
		while (products.hasNext()) {
			ProductSummary product = products.next();
			Event subEvent = null;

			String subEventId = product.getEventId();
			if (subEventId == null) {
				// maybe try to find another version of product with id?
				subEvent = preferredSubEvent;
			} else {
				subEvent = subEvents.get(subEventId);
				if (subEvent == null) {
					// first product for this sub event
					subEvent = new Event();
					subEvents.put(subEventId, subEvent);
				}
			}
			subEvent.addProduct(product);

			ProductId id = product.getId();
			String key = id.getSource() + "_" + id.getType() + "_" + id.getCode();
			productEvents.put(key, subEvent);
		}

		// handle superseded products
		HashSet<ProductSummary> superseded = new HashSet<ProductSummary>(allProducts);
		superseded.removeAll(withoutSuperseded);
		products = superseded.iterator();
		while (products.hasNext()) {
			ProductSummary next = products.next();
			ProductId id = next.getId();
			String key = id.getSource() + "_" + id.getType() + "_" + id.getCode();
			Event subEvent = productEvents.get(key);
			subEvent.addProduct(next);
		}

		return subEvents;
	}

	/**
	 * Check if this event has an associate product for another given Event.
	 *
	 * @param otherEvent
	 *            the other event.
	 * @return true if there is an associate product, false otherwise.
	 */
	public boolean hasAssociateProduct(final Event otherEvent) {
		if (otherEvent == null) {
			// cannot have an association to a null event...
			return false;
		}

		String otherEventSource = otherEvent.getSource();
		String otherEventSourceCode = otherEvent.getSourceCode();
		if (otherEventSource == null || otherEventSourceCode == null) {
			// same without source+code
			return false;
		}

		// search associate products
		Iterator<ProductSummary> iter = getProducts(ASSOCIATE_PRODUCT_TYPE)
				.iterator();
		while (iter.hasNext()) {
			ProductSummary associate = iter.next();

			if (otherEventSource.equalsIgnoreCase(associate.getProperties()
					.get(OTHEREVENTSOURCE_PROPERTY))
					&& otherEventSourceCode
							.equalsIgnoreCase(associate.getProperties().get(
									OTHEREVENTSOURCECODE_PROPERTY))) {
				// associated
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if this event has an disassociate product for another given Event.
	 *
	 * @param otherEvent
	 *            the other event.
	 * @return true if there is an disassociate product, false otherwise.
	 */
	public boolean hasDisassociateProduct(final Event otherEvent) {
		if (otherEvent == null) {
			// cannot have an disassociation to a null event...
			return false;
		}

		String otherEventSource = otherEvent.getSource();
		String otherEventSourceCode = otherEvent.getSourceCode();
		if (otherEventSource == null || otherEventSourceCode == null) {
			// same without source+code
			return false;
		}

		// search disassociate products
		Iterator<ProductSummary> iter = getProducts(DISASSOCIATE_PRODUCT_TYPE)
				.iterator();
		while (iter.hasNext()) {
			ProductSummary associate = iter.next();

			if (otherEventSource.equalsIgnoreCase(associate.getProperties()
					.get(OTHEREVENTSOURCE_PROPERTY))
					&& otherEventSourceCode
							.equalsIgnoreCase(associate.getProperties().get(
									OTHEREVENTSOURCECODE_PROPERTY))) {
				// disassociated
				return true;
			}
		}

		return false;
	}

	/**
	 * Same as isAssociated(that, new DefaultAssociator());
	 * @param that an event to test
	 * @return boolean true if associated, false otherwise
	 */
	public boolean isAssociated(final Event that) {
		return this.isAssociated(that, new DefaultAssociator());
	}

	/**
	 * Check if an event is associated to this event.
	 *
	 * Reasons events may be considered disassociated:
	 * <ol>
	 * <li>Share a common EVENTSOURCE with different EVENTSOURCECODE.</li>
	 * <li>Either has a disassociate product for the other.</li>
	 * <li>Preferred location in space and time is NOT nearby, and no other
	 * reason to associate.</li>
	 * </ol>
	 *
	 * Reasons events may be considered associated:
	 * <ol>
	 * <li>Share a common EVENTID</li>
	 * <li>Either has an associate product for the other.</li>
	 * <li>Their preferred location in space and time is nearby.</li>
	 * </ol>
	 *
	 * @param that
	 *            candidate event to test.
	 * @param associator
	 *            An associator to compare two events
	 * @return true if associated, false otherwise.
	 */
	public boolean isAssociated(final Event that, final Associator associator) {
		return associator.eventsAssociated(this, that);
	}

	/**
	 * Depending on logger level, takes in summary data and appends to buffer
	 * @param logger logger object
	 */
	public void log(final Logger logger) {
		if (logger.isLoggable(Level.FINE)) {
			EventSummary summary = this.getEventSummary();
			logger.fine(new StringBuffer("Event")
					.append("indexid=").append(summary.getIndexId())
					.append(", eventid=").append(summary.getId())
					.append(", latitude=").append(summary.getLatitude())
					.append(", longitude=").append(summary.getLongitude())
					.append(", time=").append(summary.getTime())
					.append(", deleted=").append(summary.isDeleted()).toString());

			if (logger.isLoggable(Level.FINER)) {
				StringBuffer buf = new StringBuffer("Products in event");
				List<ProductSummary> products = this.getAllProductList();
				Iterator<ProductSummary> iter = products.iterator();
				while (iter.hasNext()) {
					ProductSummary next = iter.next();
					buf.append("\n\tstatus=").append(next.getStatus())
							.append(", id=").append(next.getId().toString())
							.append(", eventid=").append(next.getEventId())
							.append(", latitude=").append(next.getEventLatitude())
							.append(", longitude=").append(next.getEventLongitude())
							.append(", time=").append(next.getEventTime());
				}
				logger.finer(buf.toString());
			}
		}
	}

}