DefaultAssociator.java
/*
* DefaultAssociator
*/
package gov.usgs.earthquake.indexer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utilities for associating events.
*
* Based on the QDM EQEventsUtils class.
*/
public class DefaultAssociator implements Associator {
private static final Logger LOGGER = Logger
.getLogger(DefaultAssociator.class.getName());
// time
/** Distance between related events in time, in milliseconds. */
public static final long TIME_DIFF_MILLISECONDS = 16 * 1000;
// space
/** Distance between related events in space, in kilometers. */
public static final BigDecimal LOCATION_DIFF_KILOMETER = new BigDecimal(100);
/** Number of kilometers in a degree at the equator. */
public static final BigDecimal KILOMETERS_PER_DEGREE = new BigDecimal("111.12");
/**
* Distance between related events latitude, in degrees.
*
* This is based on the max number of kilometers per degree, and provides
* the maximum latitude separation (assuming events share a longitude).
*
* Used as a pre-filter before more expensive checks.
*/
public static final BigDecimal LOCATION_DIFF_DEGREES = new BigDecimal(
LOCATION_DIFF_KILOMETER.doubleValue()
/ KILOMETERS_PER_DEGREE.doubleValue());
/**
* Build an index search that searches for associated products. Products are
* considered associated if the eventid matches or their location is within
* a certain distance.
*/
public SearchRequest getSearchRequest(ProductSummary summary) {
SearchRequest request = new SearchRequest();
// Order is important here. The eventId query must be added first
ProductIndexQuery eventIdQuery = getEventIdQuery(
summary.getEventSource(), summary.getEventSourceCode());
if (eventIdQuery != null) {
request.addQuery(new EventDetailQuery(eventIdQuery));
}
// Now a query that looks for location
ProductIndexQuery locationQuery = getLocationQuery(
summary.getEventTime(), summary.getEventLatitude(),
summary.getEventLongitude());
if (locationQuery != null) {
request.addQuery(new EventDetailQuery(locationQuery));
}
return request;
}
/**
* Choose and return the most closely associated event.
*
* @param events
* a list of candidate events.
* @param summary
* the summary being associated.
* @return the best match event from the list of events.
*/
public Event chooseEvent(final List<Event> events,
final ProductSummary summary) {
List<Event> filteredEvents = new LinkedList<Event>();
// remove events that are from the same source with a different code
String summarySource = summary.getEventSource();
String summaryCode = summary.getEventSourceCode();
if (summarySource == null || summaryCode == null) {
// can't check if same source with different code
filteredEvents = events;
} else {
// try to associate by event id
Iterator<Event> iter = events.iterator();
while (iter.hasNext()) {
Event event = iter.next();
boolean sameSourceDifferentCode = false;
Iterator<ProductSummary> summaryIter;
if (event.isDeleted()) {
// ignore delete products before checking
summaryIter = Event.getWithoutSuperseded(
Event.getWithoutDeleted(event.getAllProductList())).iterator();
} else {
summaryIter = event.getProductList()
.iterator();
}
while (summaryIter.hasNext()) {
ProductSummary nextSummary = summaryIter.next();
if (summarySource.equalsIgnoreCase(nextSummary
.getEventSource())) {
if (summaryCode.equalsIgnoreCase(nextSummary
.getEventSourceCode())) {
// this is the event we are looking for! so stop
// already
return event;
} else {
// different event code from same source, probably a
// different event. Don't give up yet, because
// associate may force multiple codes from same
// source in same event.
sameSourceDifferentCode = true;
}
}
}
if (!sameSourceDifferentCode) {
filteredEvents.add(event);
}
}
}
// no events found
if (filteredEvents.size() == 0) {
return null;
}
// more than one event found
else if (filteredEvents.size() > 1) {
ArrayList<String> matches = new ArrayList<String>();
Iterator<Event> iter = filteredEvents.iterator();
while (iter.hasNext()) {
Event match = iter.next();
matches.add(match.getEventId());
}
LOGGER.log(Level.WARNING, "Potential merge, product id="
+ summary.getId().toString() + ", nearby events: "
+ matches.toString());
// Return the "closest" event
Event mostSimilar = chooseMostSimilar(summary, filteredEvents);
if (mostSimilar != null) {
LOGGER.log(Level.FINE, "Associated product id="
+ summary.getId().toString() + ", to event id="
+ mostSimilar.getEventId());
}
return mostSimilar;
}
// one event found
else {
return filteredEvents.get(0);
}
}
/**
* For the given list of events, find the one that is "closest" to the given
* product. Similarity is calculated by first subtracting the event
* parameter from the product parameter, normalizing between 1 and -1, then
* calculating the Euclidean distance in the 3D space composed of the
* normalized lat, lon, and time vectors.
*
* @param summary ProductSummary to compare events with
* @param events List of events
* @return Event with lowest distance
*/
protected Event chooseMostSimilar(ProductSummary summary, List<Event> events) {
double lowest = Double.POSITIVE_INFINITY;
Event bestMatch = null;
if (summary.getEventLatitude() == null
|| summary.getEventLongitude() == null
|| summary.getEventTime() == null) {
// cannot choose most similar
if (events.size() > 0) {
// choose first
return events.get(0);
} else {
return null;
}
}
// find "closest" event
Iterator<Event> iter = events.iterator();
while (iter.hasNext()) {
Event event = iter.next();
try {
EventSummary eventSummary = event.getEventSummary();
// First get the difference between the lat, lon, and time
double deltaLat = summary.getEventLatitude()
.subtract(eventSummary.getLatitude()).doubleValue();
double deltaLon = summary.getEventLongitude()
.subtract(eventSummary.getLongitude()).doubleValue();
double deltaTime = summary.getEventTime().getTime()
- eventSummary.getTime().getTime();
// Each of the deltas will now be between the range
// -TIME_DIFF_MILLISECONDS to +TIME_DIFF_MILLISECONDS (or
// whatever
// the units are). To normalize, between -1 and 1, we just need
// to
// divide by TIME_DIFF_MILLISECONDS
deltaLat = deltaLat / LOCATION_DIFF_DEGREES.doubleValue();
deltaLon = deltaLon / LOCATION_DIFF_DEGREES.doubleValue();
deltaTime = deltaTime / TIME_DIFF_MILLISECONDS;
// Calculate the Euclidean distance between the summary and the
// vector representing this event
double distance = Math.sqrt(deltaLat * deltaLat + deltaLon
* deltaLon + deltaTime * deltaTime);
if (distance < lowest) {
lowest = distance;
bestMatch = event;
}
} catch (Exception e) {
LOGGER.log(Level.WARNING,
"Exception checking for most similar event", e);
// only log, but continue processing
if (bestMatch == null) {
// pick an event, but don't update "lowest"
bestMatch = event;
}
}
}
return bestMatch;
}
/**
* Check if two events are associated to each other.
*
* 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 event1
* candidate event to test.
* @param event2
* candidate event to test.
* @return true if associated, false otherwise.
*/
@Override
public boolean eventsAssociated(Event event1, Event event2) {
// ---------------------------------------------------------//
// -- Is there an explicit association or disassocation? -- //
// ---------------------------------------------------------//
// check disassociation first
if (event1.hasDisassociateProduct(event2)
|| event2.hasDisassociateProduct(event1)) {
// explicitly disassociated
return false;
}
// associate overrides usual event source rules.
if (event1.hasAssociateProduct(event2)
|| event2.hasAssociateProduct(event1)) {
// explicitly associated
return true;
}
EventSummary event1Summary = event1.getEventSummary();
EventSummary event2Summary = event2.getEventSummary();
// ---------------------------------- //
// -- Do events share an eventid ? -- //
// ---------------------------------- //
// this check happens after associate and disassociate to allow two
// events from the same source to be forced to associate
// (bad network, bad)
// THIS CHECKS PREFERRED EVENT ID
// if source is same, check code
String event1Source = event1Summary.getSource();
String event2Source = event2Summary.getSource();
if (event1Source != null && event2Source != null
&& event1Source.equalsIgnoreCase(event2Source)) {
String event1Code = event1Summary.getSourceCode();
String event2Code = event2Summary.getSourceCode();
// this is somewhat implied, (preferred source+code are
// combination) but be safe anyways
if (event1Code != null && event2Code != null) {
if (event1Code.equalsIgnoreCase(event2Code)) {
// same event id
return true;
} else {
// different event id from same source
return false;
}
}
}
// THIS CHECKS NON-PREFERRED EVENT IDS Map<String, String>
// ignore deleted sub events for this comparison
Map<String, List<String>> event1Codes = event1
.getAllEventCodes(false);
Map<String, List<String>> event2Codes = event2
.getAllEventCodes(false);
Set<String> commonSources = event1Codes.keySet();
commonSources.retainAll(event2Codes.keySet());
Iterator<String> eventSourceIter = commonSources.iterator();
while (eventSourceIter.hasNext()) {
String source = eventSourceIter.next();
List<String> event1SourceCodes = event1Codes.get(source);
List<String> event2SourceCodes = event2Codes.get(source);
Iterator<String> iter = event1SourceCodes.iterator();
while (iter.hasNext()) {
if (!event2SourceCodes.contains(iter.next())) {
return false;
}
}
iter = event1SourceCodes.iterator();
while (iter.hasNext()) {
if (!event1SourceCodes.contains(iter.next())) {
return false;
}
}
}
// --------------------------------------------------- //
// -- Are event locations (lat/lon/time) "nearby" ? -- //
// --------------------------------------------------- //
if (queryContainsLocation(
getLocationQuery(event1Summary.getTime(), event1Summary.getLatitude(),
event1Summary.getLongitude()), event2Summary.getTime(),
event2Summary.getLatitude(), event2Summary.getLongitude())) {
// location matches
return true;
}
return false;
}
/**
* Build a ProductIndexQuery that searches based on event id.
*
* @param eventSource
* the eventSource to search
* @param eventCode
* the eventCode to search
* @return null if eventSource or eventCode are null, otherwise a
* ProductIndexQuery. A returned ProductIndexQuery will have
* EventSearchType SEARCH_EVENT_PREFERRED and ResultType
* RESULT_TYPE_ALL.
*/
@Override
public ProductIndexQuery getEventIdQuery(final String eventSource,
final String eventCode) {
ProductIndexQuery query = null;
if (eventSource != null && eventCode != null) {
query = new ProductIndexQuery();
// search all products, not just preferred (in case the preferred is
// a delete)
query.setEventSearchType(ProductIndexQuery.SEARCH_EVENT_PRODUCTS);
query.setResultType(ProductIndexQuery.RESULT_TYPE_ALL);
query.setEventSource(eventSource);
query.setEventSourceCode(eventCode);
query.log(LOGGER);
}
return query;
}
/**
* Build a ProductIndexQuery that searches based on location.
*
*
* @param time
* the time to search around.
* @param latitude
* the latitude to search around.
* @param longitude
* the longitude to search around.
* @return null if time, latitude, or longitude are null, otherwise a
* ProductIndexQuery. A returned ProductIndexQuery will have
* EventSearchType SEARCH_EVENT_PREFERRED and ResultType
* RESULT_TYPE_ALL.
*/
@Override
public ProductIndexQuery getLocationQuery(final Date time,
final BigDecimal latitude, final BigDecimal longitude) {
ProductIndexQuery query = null;
if (time != null && latitude != null && longitude != null) {
query = new ProductIndexQuery();
// search all products, not just preferred (in case the preferred is
// a delete)
query.setEventSearchType(ProductIndexQuery.SEARCH_EVENT_PREFERRED);
query.setResultType(ProductIndexQuery.RESULT_TYPE_ALL);
// time
query.setMinEventTime(new Date(time.getTime()
- TIME_DIFF_MILLISECONDS));
query.setMaxEventTime(new Date(time.getTime()
+ TIME_DIFF_MILLISECONDS));
// latitude
query.setMinEventLatitude(latitude.subtract(LOCATION_DIFF_DEGREES));
query.setMaxEventLatitude(latitude.add(LOCATION_DIFF_DEGREES));
// longitude
double lat = latitude.abs().doubleValue();
if (lat < 89.0) {
// only restrict longitude when not close to a pole...
BigDecimal adjustedLongitudeDiff = new BigDecimal(
LOCATION_DIFF_DEGREES.doubleValue()
/ Math.cos(Math.toRadians(lat)));
query.setMinEventLongitude(longitude
.subtract(adjustedLongitudeDiff));
query.setMaxEventLongitude(longitude.add(adjustedLongitudeDiff));
/* make sure to compare across date/time line */
JDBCProductIndex jdbcProductIndex = null;
try {
jdbcProductIndex = new JDBCProductIndex();
} catch (Exception e) {
e.printStackTrace();
}
BigDecimal minLon = query.getMinEventLongitude();
BigDecimal maxLon = query.getMaxEventLongitude();
// Normalize the longitudes between -180 and 180
query.setMinEventLongitude(jdbcProductIndex
.normalizeLongitude(minLon));
query.setMaxEventLongitude(jdbcProductIndex
.normalizeLongitude(maxLon));
}
query.log(LOGGER);
}
return query;
}
/**
* Check if a location would be matched by a ProductIndexQuery.
*
* @param query
* location query
* @param time
* time to check
* @param latitude
* latitude to check
* @param longitude
* longitude to check
* @return false if query, time, latitude, or longitude are null, or if
* min/max time, latitude, longitude are set and do not match time,
* latitude, or longitude. otherwise, true.
*/
protected boolean queryContainsLocation(final ProductIndexQuery query,
final Date time, final BigDecimal latitude,
final BigDecimal longitude) {
if (query == null || time == null || latitude == null
|| longitude == null) {
// no query or location? no contains
return false;
}
if (query.getMinEventTime() != null
&& query.getMinEventTime().after(time)) {
// time too early
return false;
}
if (query.getMaxEventTime() != null
&& query.getMaxEventTime().before(time)) {
// time too late
return false;
}
if (query.getMinEventLatitude() != null
&& query.getMinEventLatitude().compareTo(latitude) > 0) {
// latitude too small
return false;
}
if (query.getMaxEventLatitude() != null
&& query.getMaxEventLatitude().compareTo(latitude) < 0) {
// latitude too large
return false;
}
if (query.getMinEventLongitude() != null
&& query.getMaxEventLongitude() != null) {
/*
* longitude range check for min & max longitude when the
* locationQuery spans the date line
*/
if (query.getMinEventLongitude().compareTo(
query.getMaxEventLongitude()) > 0) {
boolean inBounds = false;
// MAX:: getMaxLongitude < longitude <= -180
if (longitude.compareTo(query.getMaxEventLongitude()) < 0
&& longitude.compareTo(new BigDecimal("-180")) >= 0) {
inBounds = true;
}
// MIN:: 180 >= longitude > getMinEventLongitude
if (longitude.compareTo(query.getMinEventLongitude()) > 0
&& longitude.compareTo(new BigDecimal("180")) <= 0) {
inBounds = true;
}
if (!inBounds) {
return false;
}
} else {
if (query.getMinEventLongitude().compareTo(longitude) > 0) {
// longitude too small
return false;
}
if (query.getMaxEventLongitude().compareTo(longitude) < 0) {
// longitude too large
return false;
}
}
}
// must contain location
return true;
}
}