Event.java

  1. /*
  2.  * Event
  3.  */
  4. package gov.usgs.earthquake.indexer;

  5. import gov.usgs.earthquake.product.ProductId;

  6. import java.math.BigDecimal;
  7. import java.util.ArrayList;
  8. import java.util.Date;
  9. import java.util.Collections;
  10. import java.util.Comparator;
  11. import java.util.HashMap;
  12. import java.util.HashSet;
  13. import java.util.Iterator;
  14. import java.util.List;
  15. import java.util.Map;
  16. import java.util.TreeSet;
  17. import java.util.logging.Level;
  18. import java.util.logging.Logger;

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

  27.     /** Origin product type */
  28.     public static final String ORIGIN_PRODUCT_TYPE = "origin";
  29.     /** Associate product type */
  30.     public static final String ASSOCIATE_PRODUCT_TYPE = "associate";
  31.     /** Disassociate product type */
  32.     public static final String DISASSOCIATE_PRODUCT_TYPE = "disassociate";
  33.     /** Property for othereventsource */
  34.     public static final String OTHEREVENTSOURCE_PROPERTY = "othereventsource";
  35.     /** Property for othereventsourcecode */
  36.     public static final String OTHEREVENTSOURCECODE_PROPERTY = "othereventsourcecode";

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

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

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

  43.     /**
  44.      * Default constructor.
  45.      *
  46.      * All fields are set to null, and the list of products is empty.
  47.      */
  48.     public Event() {
  49.     }

  50.     /**
  51.      * Construct an event with only an indexId. The products map will be empty.
  52.      *
  53.      * @param indexId
  54.      *            the indexId to set.
  55.      */
  56.     public Event(final Long indexId) {
  57.         this.setIndexId(indexId);
  58.     }

  59.     /**
  60.      * Construct and event with an indexId and a list of products.
  61.      *
  62.      * @param indexId
  63.      *            the product index id.
  64.      * @param products
  65.      *            the list of products.
  66.      */
  67.     public Event(final Long indexId,
  68.             final Map<String, List<ProductSummary>> products) {
  69.         this.setIndexId(indexId);
  70.         this.setProducts(products);
  71.     }

  72.     /**
  73.      * Copy constructor for event.
  74.      *
  75.      * The products associated with this event are not cloned, but the list of
  76.      * products is.
  77.      *
  78.      * @param copy
  79.      *            the event to clone.
  80.      */
  81.     public Event(final Event copy) {
  82.         this(copy.getIndexId(), copy.getAllProducts());
  83.     }

  84.     /**
  85.      * Get the index id.
  86.      *
  87.      * @return the indexId or null if one hasn't been assigned.
  88.      */
  89.     public Long getIndexId() {
  90.         return indexId;
  91.     }

  92.     /**
  93.      * Set the index id.
  94.      *
  95.      * @param indexId
  96.      *            the indexId to set.
  97.      */
  98.     public void setIndexId(Long indexId) {
  99.         this.indexId = indexId;
  100.     }

  101.     /**
  102.      * Get all products associated with event, even if they are deleted.
  103.      *
  104.      * @return all products associated with event.
  105.      */
  106.     public Map<String, List<ProductSummary>> getAllProducts() {
  107.         return products;
  108.     }

  109.     /**
  110.      * Get the event products.
  111.      *
  112.      * Only returns products that have not been deleted or superseded. This
  113.      * method returns a copy of the underlying product map that has been
  114.      * filtered to remove deleted products.
  115.      *
  116.      * @return a map of event products.
  117.      * @see #getAllProducts()
  118.      */
  119.     public Map<String, List<ProductSummary>> getProducts() {
  120.         Map<String, List<ProductSummary>> notDeleted = new HashMap<String, List<ProductSummary>>();
  121.         Iterator<String> types = products.keySet().iterator();
  122.         while (types.hasNext()) {
  123.             String type = types.next();
  124.             List<ProductSummary> notDeletedProducts = getProducts(type);
  125.             if (notDeletedProducts.size() > 0) {
  126.                 notDeleted.put(type, notDeletedProducts);
  127.             }
  128.         }
  129.         return notDeleted;
  130.     }

  131.     /**
  132.      * Set products.
  133.      *
  134.      * ProductSummaries are not cloned, but lists are.
  135.      *
  136.      * @param newProducts
  137.      *            the products to set.
  138.      */
  139.     public void setProducts(final Map<String, List<ProductSummary>> newProducts) {
  140.         this.products.clear();
  141.         Iterator<String> iter = new TreeSet<String>(newProducts.keySet())
  142.                 .iterator();
  143.         while (iter.hasNext()) {
  144.             String type = iter.next();
  145.             this.products.put(type,
  146.                     new ArrayList<ProductSummary>(newProducts.get(type)));
  147.         }
  148.         eventSummary = null;
  149.     }

  150.     /**
  151.      * A convenience method for adding a product summary to an event object.
  152.      *
  153.      * Note: this method does not update any associated product index.
  154.      *
  155.      * @param summary
  156.      *            the summary to add to this event.
  157.      */
  158.     public void addProduct(final ProductSummary summary) {
  159.         String type = summary.getId().getType();
  160.         List<ProductSummary> list = products.get(type);
  161.         if (list == null) {
  162.             list = new ArrayList<ProductSummary>();
  163.             products.put(type, list);
  164.         }
  165.         if (!list.contains(summary)) {
  166.             list.add(summary);
  167.         }
  168.         eventSummary = null;
  169.     }

  170.     /**
  171.      * A convenience method for removing a product summary from an event object.
  172.      *
  173.      * Note: this method does not update any associated product index.
  174.      *
  175.      * @param summary
  176.      *            the summary to remove from this event.
  177.      */
  178.     public void removeProduct(final ProductSummary summary) {
  179.         String type = summary.getId().getType();
  180.         // find the list of products of this type
  181.         List<ProductSummary> list = products.get(type);
  182.         if (list != null) {
  183.             // remove the product from the list
  184.             list.remove(summary);
  185.             if (list.size() == 0) {
  186.                 // if the list is now empty, remove the list
  187.                 products.remove(type);
  188.             }
  189.         }
  190.         eventSummary = null;
  191.     }

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

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

  209.         return typeProducts;
  210.     }

  211.     /**
  212.      * Get all event products (including those that are deleted or superseded).
  213.      *
  214.      * @return a list of event products.
  215.      */
  216.     public List<ProductSummary> getAllProductList() {
  217.         List<ProductSummary> allProductList = new ArrayList<ProductSummary>();
  218.         Map<String, List<ProductSummary>> allProducts = getAllProducts();
  219.         Iterator<String> iter = allProducts.keySet().iterator();
  220.         while (iter.hasNext()) {
  221.             allProductList.addAll(allProducts.get(iter.next()));
  222.         }
  223.         return allProductList;
  224.     }

  225.     /**
  226.      * Get all event products that have not been deleted or superseded as a
  227.      * list.
  228.      *
  229.      * @return a list of event products.
  230.      */
  231.     public List<ProductSummary> getProductList() {
  232.         List<ProductSummary> productList = new ArrayList<ProductSummary>();
  233.         Map<String, List<ProductSummary>> notDeletedProducts = getProducts();
  234.         Iterator<String> iter = notDeletedProducts.keySet().iterator();
  235.         while (iter.hasNext()) {
  236.             productList.addAll(notDeletedProducts.get(iter.next()));
  237.         }
  238.         return productList;
  239.     }

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

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

  257.         return preferredProducts;
  258.     }

  259.     /**
  260.      * Get the preferred product of a specific type.
  261.      *
  262.      * @param type
  263.      *            type of product to get.
  264.      * @return most preferred product of that type, or null if no product of
  265.      *         that type is associated.
  266.      */
  267.     public ProductSummary getPreferredProduct(final String type) {
  268.         return getPreferredProduct(getProducts(type));
  269.     }

  270.     /**
  271.      * Get a map of all event ids associated with this event.
  272.      *
  273.      * Same as Event.getEventCodes(this.getAllProductList());
  274.      *
  275.      * @deprecated use {@link #getAllEventCodes(boolean)} instead.
  276.      * @return map of all event ids associated with this event.
  277.      */
  278.     public Map<String, String> getEventCodes() {
  279.         return getEventCodes(this.getAllProductList());
  280.     }

  281.     /**
  282.      * Get a map of all event ids associated with this event.
  283.      *
  284.      * Map key is eventSource, Map value is eventSourceCode.
  285.      *
  286.      * @deprecated use {@link #getAllEventCodes(boolean)} instead.
  287.      * @param summaries
  288.      *            the summaries list to extract event codes from.
  289.      * @return map of all event ids associated with this event.
  290.      */
  291.     public static Map<String, String> getEventCodes(
  292.             final List<ProductSummary> summaries) {
  293.         Map<String, String> eventIds = new HashMap<String, String>();
  294.         // order most preferred last,
  295.         // to minimize impact of multiple codes from same source
  296.         List<ProductSummary> sorted = getSortedMostPreferredFirst(
  297.                 getWithoutSuperseded(summaries));
  298.         Collections.reverse(sorted);
  299.         // done ordering
  300.         Iterator<ProductSummary> iter = sorted.iterator();
  301.         while (iter.hasNext()) {
  302.             ProductSummary product = iter.next();
  303.             String source = product.getEventSource();
  304.             String code = product.getEventSourceCode();
  305.             if (source != null && code != null) {
  306.                 eventIds.put(source.toLowerCase(), code.toLowerCase());
  307.             }
  308.         }
  309.         return eventIds;
  310.     }

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

  324.         Map<String, Event> subEvents = getSubEvents();
  325.         Iterator<String> iter = subEvents.keySet().iterator();
  326.         while (iter.hasNext()) {
  327.             Event subEvent = subEvents.get(iter.next());
  328.             if (!includeDeleted && subEvent.isDeleted()) {
  329.                 // check for non-deleted products that should
  330.                 // keep the event code alive
  331.                 List<ProductSummary> nonDeletedProducts = getWithoutDeleted(
  332.                         getWithoutSuperseded(subEvent.getAllProductList()));
  333.                 if (nonDeletedProducts.size() == 0) {
  334.                     // filter deleted events
  335.                     continue;
  336.                 }
  337.                 // otherwise, event has active products;
  338.                 // prevent same source associations
  339.             }

  340.             // add code to list for source
  341.             String source = subEvent.getSource();
  342.             String sourceCode = subEvent.getSourceCode();
  343.             List<String> sourceEventCodes = allEventCodes.get(source);
  344.             if (sourceEventCodes == null) {
  345.                 // create list for source
  346.                 sourceEventCodes = new ArrayList<String>();
  347.                 allEventCodes.put(source, sourceEventCodes);
  348.             }
  349.             // keep list distinct
  350.             if (!sourceEventCodes.contains(sourceCode)) {
  351.                 sourceEventCodes.add(sourceCode);
  352.             }
  353.         }

  354.         return allEventCodes;
  355.     }

  356.     /**
  357.      * Get a list of all the preferred products sorted based on their
  358.      * authoritative weights
  359.      *
  360.      * @return sorted list of ProductSummary objects
  361.      */
  362.     public List<ProductSummary> getPreferredProductsSorted() {
  363.         Map<String, ProductSummary> preferred = getPreferredProducts();

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

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

  372.     /**
  373.      * Get the event id.
  374.      *
  375.      * The event id is the combination of event source and event source code.
  376.      *
  377.      * @return the event id, or null if either event source or event source code
  378.      *         is null.
  379.      * @see #getSource()
  380.      * @see #getSourceCode()
  381.      */
  382.     public String getEventId() {
  383.         ProductSummary product = getEventIdProduct();
  384.         if (product != null) {
  385.             return product.getEventId();
  386.         }
  387.         return null;
  388.     }

  389.     /**
  390.      * Get the preferred source for this event. If an origin product exists,
  391.      * it's value is used.
  392.      *
  393.      * @return Source from preferred product or null
  394.      */
  395.     public String getSource() {
  396.         ProductSummary product = getEventIdProduct();
  397.         if (product != null) {
  398.             return product.getEventSource();
  399.         }
  400.         return null;
  401.     }

  402.     /**
  403.      * Get the preferred source code for this event. If an origin product
  404.      * exists, it's value is used.
  405.      *
  406.      * @return Source code from preferred product or null
  407.      */
  408.     public String getSourceCode() {
  409.         ProductSummary product = getEventIdProduct();
  410.         if (product != null) {
  411.             return product.getEventSourceCode();
  412.         }
  413.         return null;
  414.     }

  415.     /**
  416.      * Get the product used for eventsource and eventsourcecode.
  417.      *
  418.      * Event ID comes from the preferred origin product.
  419.      *
  420.      * @return The most preferred product summary. This summary is used to
  421.      *         determine the eventsouce and eventsourcecode.
  422.      * @see #getPreferredOriginProduct()
  423.      */
  424.     protected ProductSummary getEventIdProduct() {
  425.         ProductSummary product = getPreferredOriginProduct();
  426.         if (product == null) {
  427.             product = getProductWithOriginProperties();
  428.         }
  429.         return product;
  430.     }

  431.     /**
  432.      * Get the most recent product with origin properties (id, lat, lon, time).
  433.      *
  434.      * <strong>NOTE</strong>: this product may have been superseded by a delete.
  435.      * When an event has not been deleted, this method should be consistent with
  436.      *  {@link #getPreferredOriginProduct()}.
  437.      *
  438.      * Products are checked in the following order, sorted most preferred first
  439.      * within each group.  The first matching product is returned:
  440.      * <ol>
  441.      * <li>"origin" products not superseded or deleted,
  442.      *      that have origin properties</li>
  443.      * <li>"origin" products superseded by a delete,
  444.      *      that have origin properties</li>
  445.      * <li>products not superseded or deleted,
  446.      *      that have origin properties</li>
  447.      * <li>products superseded by a delete,
  448.      *      that have origin properties</li>
  449.      * </ol>
  450.      *
  451.      * @return the most recent product with origin properties.
  452.      * @see #productHasOriginProperties(ProductSummary)
  453.      */
  454.     public ProductSummary getProductWithOriginProperties() {
  455.         Map<String, List<ProductSummary>> allProducts = getAllProducts();
  456.         List<ProductSummary> productsList = null;
  457.         ProductSummary preferredProduct = null;
  458.         Iterator<ProductSummary> iter = null;

  459.         productsList = allProducts.get(ORIGIN_PRODUCT_TYPE);
  460.         if (productsList != null) {
  461.             // "origin" products not superseded or deleted
  462.             productsList = getSortedMostPreferredFirst(getWithoutDeleted(
  463.                     getWithoutSuperseded(allProducts.get(ORIGIN_PRODUCT_TYPE))));
  464.             iter = productsList.iterator();
  465.             while (iter.hasNext()) {
  466.                 preferredProduct = iter.next();
  467.                 if (productHasOriginProperties(preferredProduct)) {
  468.                     return preferredProduct;
  469.                 }
  470.             }

  471.             // "origin" products superseded by a delete
  472.             productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
  473.                     getWithoutDeleted(allProducts.get(ORIGIN_PRODUCT_TYPE))));
  474.             iter = productsList.iterator();
  475.             while (iter.hasNext()) {
  476.                 preferredProduct = iter.next();
  477.                 if (productHasOriginProperties(preferredProduct)) {
  478.                     return preferredProduct;
  479.                 }
  480.             }
  481.         }

  482.         // products not superseded or deleted
  483.         productsList = getSortedMostPreferredFirst(getWithoutDeleted(
  484.                 getWithoutSuperseded(productTypeMapToList(allProducts))));
  485.         iter = productsList.iterator();
  486.         while (iter.hasNext()) {
  487.             preferredProduct = iter.next();
  488.             if (productHasOriginProperties(preferredProduct)) {
  489.                 return preferredProduct;
  490.             }
  491.         }

  492.         // products superseded by a delete
  493.         productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
  494.                 getWithoutDeleted(productTypeMapToList(allProducts))));
  495.         iter = productsList.iterator();
  496.         while (iter.hasNext()) {
  497.             preferredProduct = iter.next();
  498.             if (productHasOriginProperties(preferredProduct)) {
  499.                 return preferredProduct;
  500.             }
  501.         }

  502.         return null;
  503.     }

  504.     /**
  505.      * Get the most preferred origin-like product for this event.
  506.      *
  507.      * The event is considered deleted if the returned product is null, deleted,
  508.      * or does not have origin properties.  Information about the event
  509.      * may still be available using {@link #getProductWithOriginProperties()}.
  510.      *
  511.      * Products are checked in the following order, sorted most preferred first
  512.      * within each group.  The first matching product is returned:
  513.      * <ul>
  514.      * <li>If any "origin" products exist:
  515.      *      <ol>
  516.      *      <li>"origin" products not superseded or deleted,
  517.      *              that have origin properties.</li>
  518.      *      <li>"origin" products not superseded,
  519.      *              that have an event id.</li>
  520.      *      </ol>
  521.      * </li>
  522.      * <li>If no "origin" products exist:
  523.      *      <ol>
  524.      *      <li>products not superseded or deleted,
  525.      *              that have origin properties.</li>
  526.      *      <li>products not superseded,
  527.      *              that have an event id.</li>
  528.      *      </ol>
  529.      * </li>
  530.      * </ul>
  531.      *
  532.      * @return the most recent product with origin properties.
  533.      * @see #productHasOriginProperties(ProductSummary)
  534.      */
  535.     public ProductSummary getPreferredOriginProduct() {
  536.         Map<String, List<ProductSummary>> allProducts = getAllProducts();
  537.         List<ProductSummary> productsList = null;
  538.         ProductSummary preferredProduct = null;
  539.         Iterator<ProductSummary> iter = null;

  540.         productsList = allProducts.get(ORIGIN_PRODUCT_TYPE);
  541.         if (productsList != null) {
  542.             // "origin" products not superseded or deleted,
  543.             // that have origin properties
  544.             productsList = getSortedMostPreferredFirst(getWithoutDeleted(
  545.                     getWithoutSuperseded(allProducts.get(ORIGIN_PRODUCT_TYPE))));
  546.             iter = productsList.iterator();
  547.             while (iter.hasNext()) {
  548.                 preferredProduct = iter.next();
  549.                 if (productHasOriginProperties(preferredProduct)) {
  550.                     return preferredProduct;
  551.                 }
  552.             }

  553.             // "origin" products not superseded,
  554.             // that have event id
  555.             productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
  556.                     allProducts.get(ORIGIN_PRODUCT_TYPE)));
  557.             iter = productsList.iterator();
  558.             while (iter.hasNext()) {
  559.                 preferredProduct = iter.next();
  560.                 if (preferredProduct.getEventSource() != null
  561.                         && preferredProduct.getEventSourceCode() != null) {
  562.                     return preferredProduct;
  563.                 }
  564.             }

  565.             return null;
  566.         }

  567.         // products not superseded or deleted,
  568.         // that have origin properties
  569.         productsList = getSortedMostPreferredFirst(getWithoutDeleted(
  570.                 getWithoutSuperseded(productTypeMapToList(allProducts))));
  571.         iter = productsList.iterator();
  572.         while (iter.hasNext()) {
  573.             preferredProduct = iter.next();
  574.             if (productHasOriginProperties(preferredProduct)) {
  575.                 return preferredProduct;
  576.             }
  577.         }

  578.         // products not superseded,
  579.         // that have event id
  580.         productsList = getSortedMostPreferredFirst(getWithoutSuperseded(
  581.                 productTypeMapToList(allProducts)));
  582.         iter = productsList.iterator();
  583.         while (iter.hasNext()) {
  584.             preferredProduct = iter.next();
  585.             if (preferredProduct.getEventSource() != null
  586.                     && preferredProduct.getEventSourceCode() != null) {
  587.                 return preferredProduct;
  588.             }
  589.         }

  590.         return null;
  591.     }

  592.     /**
  593.      * Check if a product can define an event (id, lat, lon, time).
  594.      *
  595.      * @param product
  596.      *            product to check.
  597.      * @return true if product has id, lat, lon, and time properties.
  598.      */
  599.     public static boolean productHasOriginProperties(
  600.             final ProductSummary product) {
  601.         return (product.getEventSource() != null
  602.                 && product.getEventSourceCode() != null
  603.                 && product.getEventLatitude() != null
  604.                 && product.getEventLongitude() != null && product
  605.                     .getEventTime() != null);
  606.     }

  607.     /**
  608.      * Get the most preferred magnitude product for event.
  609.      *
  610.      * Currently calls {@link #getPreferredOriginProduct()}.
  611.      *
  612.      * @return the most preferred magnitude product for event.
  613.      */
  614.     public ProductSummary getPreferredMagnitudeProduct() {
  615.         return getPreferredOriginProduct();
  616.     }

  617.     /**
  618.      * Get the preferred time for this event. If an origin product exists, it's
  619.      * value is used.
  620.      *
  621.      * @return Time from preferred product or null
  622.      */
  623.     public Date getTime() {
  624.         ProductSummary preferred = getProductWithOriginProperties();
  625.         if (preferred != null) {
  626.             return preferred.getEventTime();
  627.         }
  628.         return null;
  629.     }

  630.     /**
  631.      * Get the preferred latitude for this event. If an origin product exists,
  632.      * it's value is used.
  633.      *
  634.      * @return Latitude from preferred product or null
  635.      */
  636.     public BigDecimal getLatitude() {
  637.         ProductSummary preferred = getProductWithOriginProperties();
  638.         if (preferred != null) {
  639.             return preferred.getEventLatitude();
  640.         }
  641.         return null;

  642.     }

  643.     /**
  644.      * Get the preferred longitude for this event. If an origin product exists,
  645.      * it's value is used.
  646.      *
  647.      * @return Longitude from preferred product or null
  648.      */
  649.     public BigDecimal getLongitude() {
  650.         ProductSummary preferred = getProductWithOriginProperties();
  651.         if (preferred != null) {
  652.             return preferred.getEventLongitude();
  653.         }
  654.         return null;

  655.     }

  656.     /**
  657.      * Event update time is most recent product update time.
  658.      *
  659.      * @return the most recent product update time.
  660.      */
  661.     public Date getUpdateTime() {
  662.         Date updateTime = null;
  663.         Date time = null;
  664.         Iterator<ProductSummary> iter = getAllProductList().iterator();
  665.         while (iter.hasNext()) {
  666.             time = iter.next().getId().getUpdateTime();
  667.             if (updateTime == null || time.after(updateTime)) {
  668.                 time = updateTime;
  669.             }
  670.         }
  671.         return updateTime;
  672.     }

  673.     /**
  674.      * Get the preferred depth for this event. If an origin product exists, it's
  675.      * value is used.
  676.      *
  677.      * @return Depth from preferred product or null
  678.      */
  679.     public BigDecimal getDepth() {
  680.         ProductSummary preferred = getProductWithOriginProperties();
  681.         if (preferred != null) {
  682.             return preferred.getEventDepth();
  683.         }
  684.         return null;
  685.     }

  686.     /**
  687.      * Get the preferred magntitude for this event. If an origin product exists, it's
  688.      * value is used.
  689.      *
  690.      * @return magnitude from preferred product or null
  691.      */
  692.     public BigDecimal getMagnitude() {
  693.         ProductSummary preferred = getPreferredMagnitudeProduct();
  694.         if (preferred != null) {
  695.             return preferred.getEventMagnitude();
  696.         }
  697.         return null;
  698.     }

  699.     /**
  700.      * @return boolean if the preferred event is deleted
  701.      */
  702.     public boolean isDeleted() {
  703.         ProductSummary preferred = getPreferredOriginProduct();
  704.         if (preferred != null && !preferred.isDeleted() &&
  705.                 Event.productHasOriginProperties(preferred)) {
  706.             // have "origin" type product, that isn't deleted,
  707.             // and has origin properties
  708.             return false;
  709.         }
  710.         // otherwise, deleted
  711.         return true;
  712.     }

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

  724.         Iterator<ProductSummary> iter = all.iterator();
  725.         while (iter.hasNext()) {
  726.             ProductSummary summary = iter.next();
  727.             if (preferred == null) {
  728.                 preferred = summary;
  729.             } else {
  730.                 long summaryWeight = summary.getPreferredWeight();
  731.                 long preferredWeight = preferred.getPreferredWeight();
  732.                 if (summaryWeight > preferredWeight
  733.                         || (summaryWeight == preferredWeight && summary.getId()
  734.                                 .getUpdateTime()
  735.                                 .after(preferred.getId().getUpdateTime()))) {
  736.                     preferred = summary;
  737.                 }
  738.             }
  739.         }
  740.         return preferred;
  741.     }

  742.     /**
  743.      * Summarize this event into preferred values.
  744.      *
  745.      * NOTE: the event summary may include information from an origin product,
  746.      * even when the preferred origin for the event has been deleted.  Use
  747.      * getPreferredOriginProduct() to check the preferred origin of the event.
  748.      *
  749.      * @return an event summary.
  750.      */
  751.     public EventSummary getEventSummary() {
  752.         if (eventSummary != null) {
  753.             return eventSummary;
  754.         }

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

  758.         ProductSummary eventIdProduct = this.getEventIdProduct();
  759.         if (eventIdProduct != null) {
  760.             summary.setSource(eventIdProduct.getEventSource());
  761.             summary.setSourceCode(eventIdProduct.getEventSourceCode());
  762.         }

  763.         ProductSummary originProduct = this.getProductWithOriginProperties();
  764.         if (originProduct != null) {
  765.             summary.setLatitude(originProduct.getEventLatitude());
  766.             summary.setLongitude(originProduct.getEventLongitude());
  767.             summary.setTime(originProduct.getEventTime());
  768.             summary.setDepth(originProduct.getEventDepth());
  769.         }

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

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

  777.         // cache summary
  778.         eventSummary = summary;

  779.         return summary;
  780.     }

  781.     /**
  782.      * Comparison class that compares two ProductSummary objects based on their
  783.      * preferred weight and update time.
  784.      *
  785.      */
  786.     static class MostPreferredFirstComparator implements
  787.             Comparator<ProductSummary> {

  788.         @Override
  789.         public int compare(ProductSummary p1, ProductSummary p2) {
  790.             if (p1.getPreferredWeight() > p2.getPreferredWeight()) {
  791.                 return -1;
  792.             } else if (p1.getPreferredWeight() < p2.getPreferredWeight()) {
  793.                 return 1;
  794.             } else {
  795.                 Date p1Update = p1.getUpdateTime();
  796.                 Date p2Update = p2.getUpdateTime();
  797.                 if (p1Update.after(p2Update)) {
  798.                     return -1;
  799.                 } else if (p2Update.after(p1Update)) {
  800.                     return 1;
  801.                 } else {
  802.                     return 0;
  803.                 }
  804.             }
  805.         }
  806.     }

  807.     @Override
  808.     public int compareTo(Event that) {
  809.         int r;

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

  815.         Iterator<ProductSummary> thisIter = thisProducts.iterator();
  816.         Iterator<ProductSummary> thatIter = thatProducts.iterator();
  817.         while (thisIter.hasNext() && thatIter.hasNext()) {
  818.             // just compare product ids for now
  819.             r = thisIter.next().getId().compareTo(thatIter.next().getId());
  820.             if (r != 0) {
  821.                 return r;
  822.             }
  823.         }

  824.         return 0;
  825.     }

  826.     /**
  827.      * Find the most preferred product.
  828.      *
  829.      * If preferredType is not null, products of this type are favored over
  830.      * those not of this type.
  831.      *
  832.      * If preferredNotNullProperty is not null, products that have this property
  833.      * set are favored over those without this property set.
  834.      *
  835.      * @param products
  836.      *            the list of products to search.
  837.      * @param preferredType
  838.      *            the preferred product type, if available.
  839.      * @param preferredNotNullProperty
  840.      *            the preferred property name, if available.
  841.      * @return The most preferred product summary of the given type.
  842.      */
  843.     public static ProductSummary getMostPreferred(
  844.             final List<ProductSummary> products, final String preferredType,
  845.             final String preferredNotNullProperty) {
  846.         ProductSummary mostPreferred = null;

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

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

  855.             if (mostPreferred == null) {
  856.                 // first product is most preferred so far
  857.                 mostPreferred = next;
  858.                 continue;
  859.             }

  860.             if (preferredType != null) {
  861.                 if (next.getType().equals(preferredType)) {
  862.                     if (!mostPreferred.getType().equals(preferredType)) {
  863.                         // prefer products of this type
  864.                         mostPreferred = next;
  865.                     }
  866.                 } else if (mostPreferred.getType().equals(preferredType)) {
  867.                     // already have preferred product of preferred type
  868.                     continue;
  869.                 }
  870.             }

  871.             if (next.getPreferredWeight() > mostPreferred.getPreferredWeight()) {
  872.                 // higher preferred weight
  873.                 mostPreferred = next;
  874.             } else if (next.getPreferredWeight() == mostPreferred
  875.                     .getPreferredWeight()
  876.                     && next.getUpdateTime()
  877.                             .after(mostPreferred.getUpdateTime())) {
  878.                 // same preferred weight, newer update
  879.                 mostPreferred = next;
  880.             }
  881.         }

  882.         return mostPreferred;
  883.     }

  884.     /**
  885.      * Remove deleted products from the list.
  886.      *
  887.      * @param products
  888.      *            list of products to filter.
  889.      * @return copy of the products list with deleted products removed.
  890.      */
  891.     public static List<ProductSummary> getWithoutDeleted(
  892.             final List<ProductSummary> products) {
  893.         List<ProductSummary> withoutDeleted = new ArrayList<ProductSummary>();

  894.         Iterator<ProductSummary> iter = products.iterator();
  895.         while (iter.hasNext()) {
  896.             ProductSummary next = iter.next();
  897.             if (!next.isDeleted()) {
  898.                 withoutDeleted.add(next);
  899.             }
  900.         }

  901.         return withoutDeleted;
  902.     }

  903.     /**
  904.      * Remove deleted products from the list.
  905.      *
  906.      * @param products
  907.      *            list of products to filter.
  908.      * @return copy of the products list with deleted products removed.
  909.      */
  910.     public static List<ProductSummary> getWithEventId(
  911.             final List<ProductSummary> products) {
  912.         List<ProductSummary> withEventId = new ArrayList<ProductSummary>();

  913.         Iterator<ProductSummary> iter = products.iterator();
  914.         while (iter.hasNext()) {
  915.             ProductSummary next = iter.next();
  916.             if (next.getEventId() != null) {
  917.                 withEventId.add(next);
  918.             }
  919.         }

  920.         return withEventId;
  921.     }

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

  939.             // key is combination of source, type, and code
  940.             // since none of these may contain ":", it is used as a delimiter to
  941.             // prevent collisions.
  942.             String key = new StringBuffer(id.getSource()).append(":").append(
  943.                     id.getType()).append(":").append(id.getCode()).toString();
  944.             if (!latest.containsKey(key)) {
  945.                 // first product
  946.                 latest.put(key, summary);
  947.             } else {
  948.                 // keep latest product
  949.                 ProductSummary other = latest.get(key);
  950.                 if (other.getId().getUpdateTime().before(id.getUpdateTime())) {
  951.                     latest.put(key, summary);
  952.                 }
  953.             }
  954.         }

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

  958.     /**
  959.      * Sort a list of products, most preferred first.
  960.      *
  961.      * @param products
  962.      *            the list of products to sort.
  963.      * @return a copy of the list sorted with most preferred first.
  964.      */
  965.     public static List<ProductSummary> getSortedMostPreferredFirst(
  966.             final List<ProductSummary> products) {
  967.         List<ProductSummary> mostPreferredFirst = new ArrayList<ProductSummary>(
  968.                 products);
  969.         Collections
  970.                 .sort(mostPreferredFirst, new MostPreferredFirstComparator());
  971.         return mostPreferredFirst;
  972.     }

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

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

  980.         return list;
  981.     }

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

  985.         Iterator<ProductSummary> iter = products.iterator();
  986.         while (iter.hasNext()) {
  987.             ProductSummary product = iter.next();
  988.             List<ProductSummary> typeProducts = typeMap.get(product.getType());
  989.             if (typeProducts == null) {
  990.                 typeProducts = new ArrayList<ProductSummary>();
  991.                 typeMap.put(product.getType(), typeProducts);
  992.             }
  993.             typeProducts.add(product);
  994.         }

  995.         return typeMap;
  996.     }

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

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

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

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

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

  1023.             String subEventId = product.getEventId();
  1024.             if (subEventId == null) {
  1025.                 // maybe try to find another version of product with id?
  1026.                 subEvent = preferredSubEvent;
  1027.             } else {
  1028.                 subEvent = subEvents.get(subEventId);
  1029.                 if (subEvent == null) {
  1030.                     // first product for this sub event
  1031.                     subEvent = new Event();
  1032.                     subEvents.put(subEventId, subEvent);
  1033.                 }
  1034.             }
  1035.             subEvent.addProduct(product);

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

  1040.         // handle superseded products
  1041.         HashSet<ProductSummary> superseded = new HashSet<ProductSummary>(allProducts);
  1042.         superseded.removeAll(withoutSuperseded);
  1043.         products = superseded.iterator();
  1044.         while (products.hasNext()) {
  1045.             ProductSummary next = products.next();
  1046.             ProductId id = next.getId();
  1047.             String key = id.getSource() + "_" + id.getType() + "_" + id.getCode();
  1048.             Event subEvent = productEvents.get(key);
  1049.             subEvent.addProduct(next);
  1050.         }

  1051.         return subEvents;
  1052.     }

  1053.     /**
  1054.      * Check if this event has an associate product for another given Event.
  1055.      *
  1056.      * @param otherEvent
  1057.      *            the other event.
  1058.      * @return true if there is an associate product, false otherwise.
  1059.      */
  1060.     public boolean hasAssociateProduct(final Event otherEvent) {
  1061.         if (otherEvent == null) {
  1062.             // cannot have an association to a null event...
  1063.             return false;
  1064.         }

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

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

  1076.             if (otherEventSource.equalsIgnoreCase(associate.getProperties()
  1077.                     .get(OTHEREVENTSOURCE_PROPERTY))
  1078.                     && otherEventSourceCode
  1079.                             .equalsIgnoreCase(associate.getProperties().get(
  1080.                                     OTHEREVENTSOURCECODE_PROPERTY))) {
  1081.                 // associated
  1082.                 return true;
  1083.             }
  1084.         }

  1085.         return false;
  1086.     }

  1087.     /**
  1088.      * Check if this event has an disassociate product for another given Event.
  1089.      *
  1090.      * @param otherEvent
  1091.      *            the other event.
  1092.      * @return true if there is an disassociate product, false otherwise.
  1093.      */
  1094.     public boolean hasDisassociateProduct(final Event otherEvent) {
  1095.         if (otherEvent == null) {
  1096.             // cannot have an disassociation to a null event...
  1097.             return false;
  1098.         }

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

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

  1110.             if (otherEventSource.equalsIgnoreCase(associate.getProperties()
  1111.                     .get(OTHEREVENTSOURCE_PROPERTY))
  1112.                     && otherEventSourceCode
  1113.                             .equalsIgnoreCase(associate.getProperties().get(
  1114.                                     OTHEREVENTSOURCECODE_PROPERTY))) {
  1115.                 // disassociated
  1116.                 return true;
  1117.             }
  1118.         }

  1119.         return false;
  1120.     }

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

  1129.     /**
  1130.      * Check if an event is associated to this event.
  1131.      *
  1132.      * Reasons events may be considered disassociated:
  1133.      * <ol>
  1134.      * <li>Share a common EVENTSOURCE with different EVENTSOURCECODE.</li>
  1135.      * <li>Either has a disassociate product for the other.</li>
  1136.      * <li>Preferred location in space and time is NOT nearby, and no other
  1137.      * reason to associate.</li>
  1138.      * </ol>
  1139.      *
  1140.      * Reasons events may be considered associated:
  1141.      * <ol>
  1142.      * <li>Share a common EVENTID</li>
  1143.      * <li>Either has an associate product for the other.</li>
  1144.      * <li>Their preferred location in space and time is nearby.</li>
  1145.      * </ol>
  1146.      *
  1147.      * @param that
  1148.      *            candidate event to test.
  1149.      * @param associator
  1150.      *            An associator to compare two events
  1151.      * @return true if associated, false otherwise.
  1152.      */
  1153.     public boolean isAssociated(final Event that, final Associator associator) {
  1154.         return associator.eventsAssociated(this, that);
  1155.     }

  1156.     /**
  1157.      * Depending on logger level, takes in summary data and appends to buffer
  1158.      * @param logger logger object
  1159.      */
  1160.     public void log(final Logger logger) {
  1161.         if (logger.isLoggable(Level.FINE)) {
  1162.             EventSummary summary = this.getEventSummary();
  1163.             logger.fine(new StringBuffer("Event")
  1164.                     .append("indexid=").append(summary.getIndexId())
  1165.                     .append(", eventid=").append(summary.getId())
  1166.                     .append(", latitude=").append(summary.getLatitude())
  1167.                     .append(", longitude=").append(summary.getLongitude())
  1168.                     .append(", time=").append(summary.getTime())
  1169.                     .append(", deleted=").append(summary.isDeleted()).toString());

  1170.             if (logger.isLoggable(Level.FINER)) {
  1171.                 StringBuffer buf = new StringBuffer("Products in event");
  1172.                 List<ProductSummary> products = this.getAllProductList();
  1173.                 Iterator<ProductSummary> iter = products.iterator();
  1174.                 while (iter.hasNext()) {
  1175.                     ProductSummary next = iter.next();
  1176.                     buf.append("\n\tstatus=").append(next.getStatus())
  1177.                             .append(", id=").append(next.getId().toString())
  1178.                             .append(", eventid=").append(next.getEventId())
  1179.                             .append(", latitude=").append(next.getEventLatitude())
  1180.                             .append(", longitude=").append(next.getEventLongitude())
  1181.                             .append(", time=").append(next.getEventTime());
  1182.                 }
  1183.                 logger.finer(buf.toString());
  1184.             }
  1185.         }
  1186.     }

  1187. }