simplified derivation of device mappings from mapping events

This commit is contained in:
Fredrik Teschke committed 2014-04-24 01:12:55 +02:00
1 parent 7508b7ccee
commit 83610c20bb
6 files changed
+17 -175

No files matched your search

@@ -3,9 +3,6 @@ package com.sap.sailing.domain.racelog.tracking.analyzing.test;
import static org.junit.Assert.assertEquals;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.UUID;
@@ -14,9 +11,7 @@ import junit.framework.Assert;
import org.junit.Test;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.impl.CompetitorImpl;
import com.sap.sailing.domain.base.impl.MarkImpl;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.racelog.RaceLogEvent;
@@ -29,7 +24,6 @@ import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifie
public class DeviceMappingFinderTest extends AbstractRaceLogTrackingTest {
private final Competitor competitor = new CompetitorImpl("comp", "Comp", null, null, null);
private final Competitor competitor2 = new CompetitorImpl("comp2", "Comp2", null, null, null);
private final Mark mark = new MarkImpl("mark");
private final DeviceIdentifier device = new SmartphoneImeiIdentifier("imei");
private int time = 0;
@@ -53,13 +47,6 @@ public class DeviceMappingFinderTest extends AbstractRaceLogTrackingTest {
return mapping.getId();
}
private Serializable addMapping(RaceLogEventAuthor author, DeviceIdentifier device, Long from, Long to, Mark item) {
RaceLogEvent mapping = factory.createDeviceMarkMappingEvent(t(), author, t(), UUID.randomUUID(), device, item, 0,
t(from), t(to));
log.add(mapping);
return mapping.getId();
}
private void closeMapping(RaceLogEventAuthor author, DeviceIdentifier device, Serializable mappingId, long millis) {
RaceLogEvent mapping = factory.createCloseOpenEndedDeviceMappingEvent(t(), author, t(), UUID.randomUUID(), 0, mappingId,
new MillisecondsTimePoint(millis));
@@ -75,30 +62,9 @@ public class DeviceMappingFinderTest extends AbstractRaceLogTrackingTest {
addMapping(author, device, 50L, 90L, competitor);
Assert.assertEquals(2, getMappings().size());
//are mappings merged? (mark mapping should not "distract")
addMapping(author, device, 5L, 100L, competitor);
addMapping(author, device, 15L, 90L, mark);
Assert.assertEquals(1, getMappings().size());
//overlap with same device -> should be merged
addMapping(author, device, 30L, 101L, competitor);
Assert.assertEquals(1, getMappings().size());
//non-overlap with new device
addMapping(author, new SmartphoneImeiIdentifier("imei2"), 110L, 150L, competitor);
Assert.assertEquals(2, getMappings().size());
//different device at start with overlap
addMapping(author, new SmartphoneImeiIdentifier("imei3"), 0L, 50L, competitor);
Assert.assertEquals(3, getMappings().size());
//different device in middle -> split
addMapping(author, new SmartphoneImeiIdentifier("imei3"), 60L, 70L, competitor);
Assert.assertEquals(5, getMappings().size());
//different device at end with overlap
addMapping(author, new SmartphoneImeiIdentifier("imei4"), 140L, 160L, competitor);
Assert.assertEquals(6, getMappings().size());
}
@Test
@@ -126,39 +92,5 @@ public class DeviceMappingFinderTest extends AbstractRaceLogTrackingTest {
//open-ended at end, not closed
addMapping(author, device, 30L, null, competitor);
assertEquals(3, getMappings().size());
//open-ended at beginning, eats others up
addMapping(author, device, -10L, null, competitor);
assertEquals(1, getMappings().size());
}
@Test
public void doesLaterMappingThatIncludesOtherOverwrite() throws ParseException {
DeviceIdentifier cde = new SmartphoneImeiIdentifier("cde");
DeviceIdentifier abc = new SmartphoneImeiIdentifier("abc");
DateFormat df = new SimpleDateFormat("mm:ss");
long m1600 = df.parse("16:00").getTime();
long m1550 = df.parse("15:50").getTime();
long m1615 = df.parse("16:15").getTime();
long m1610 = df.parse("16:10").getTime();
addMapping(author, abc, m1600, m1610, competitor);
addMapping(author, cde, m1550, m1615, competitor);
assertEquals(1, getMappings().size());
}
@Test
public void doesLaterMappingThatLiesWithinBreakOtherIntoPieces() throws ParseException {
DeviceIdentifier cde = new SmartphoneImeiIdentifier("cde");
DeviceIdentifier abc = new SmartphoneImeiIdentifier("abc");
DateFormat df = new SimpleDateFormat("mm:ss");
long m1600 = df.parse("16:00").getTime();
long m1550 = df.parse("15:50").getTime();
long m1615 = df.parse("16:15").getTime();
long m1610 = df.parse("16:10").getTime();
addMapping(author, cde, m1550, m1615, competitor);
addMapping(author, abc, m1600, m1610, competitor);
assertEquals(3, getMappings().size());
}
}
@@ -104,6 +104,7 @@ public interface RaceLog extends Track<RaceLogEvent>, WithID {
/**
* Search for the event by its {@link RaceLogEvent#getId() id}.
* Caller needs to hold the read lock.
*/
RaceLogEvent getEventById(Serializable id);
@@ -6,9 +6,10 @@ import java.util.List;
import com.sap.sailing.domain.base.Timed;
import com.sap.sailing.domain.common.TimeRange;
import com.sap.sailing.domain.common.WithID;
import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceMappingFinder;
/**
* Refer to the documentation of {@link DeviceMappingEvent} for details on how have-open ranges are resolved.
* @see DeviceMappingFinder {@code DeviceMappings} are derived from {@code DeviceMappingEvents}.
* @author Fredrik Teschke
*/
public interface DeviceMapping<ItemType extends WithID> extends Timed {
@@ -6,6 +6,7 @@ import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WithID;
import com.sap.sailing.domain.racelog.RaceLogEvent;
import com.sap.sailing.domain.racelog.Revokable;
import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DefinedMarkFinder;
/**
* Event type for mapping {@link DeviceIdentifier devices} to {@code items} ({@link Competitor competitors} or {@link Mark marks}).
@@ -15,6 +16,8 @@ import com.sap.sailing.domain.racelog.Revokable;
* <p>
* If one end of the time range enclosed by {@link #getFrom()} and {@link #getTo()} is open, this can be closed
* by a {@link CloseOpenEndedDeviceMappingEvent}.
*
* @see DefinedMarkFinder for rules by which {@link DeviceMapping}s are derived from these events.
* @author Fredrik Teschke
*/
public interface DeviceMappingEvent<ItemType extends WithID> extends RaceLogEvent, Revokable {
@@ -14,7 +14,6 @@ import com.sap.sailing.domain.common.impl.TimeRangeImpl;
import com.sap.sailing.domain.racelog.RaceLog;
import com.sap.sailing.domain.racelog.RaceLogEvent;
import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer;
import com.sap.sailing.domain.racelog.impl.RaceLogEventComparator;
import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent;
import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier;
import com.sap.sailing.domain.racelog.tracking.DeviceMapping;
@@ -24,25 +23,15 @@ import com.sap.sailing.domain.racelog.tracking.impl.DeviceMappingImpl;
/**
* Extracts the {@link DeviceMapping}s with the appropriate {@link TimeRange}s from the {@link RaceLog}.
* Based on the {@link DeviceMappingEvent}s found in the {@code RaceLog} for the tracked {@code items}
* ({@code Competitor}s and {@code Marks}), the actual mappings are created by removing conflicting
* overlaps of these mappings by the rules defined below.
* ({@code Competitor}s and {@code Marks}), the actual mappings are created.
* <p>
* Mappings can be defined with an open-ended time range (e.g. track this from now on), which should then be
* closed by a corresponding {@link CloseOpenEndedDeviceMappingEvent}.
* <p>
* The events in the {@code RaceLog} are ordered by their priority through the {@link RaceLogEventComparator}.
* Therefore, the rules for resolving conflicts are applied by inspecting the events in order of increasing
* priority one by one. Each inspected {@code DeviceMappingEvent} is resolved against the set of mappings
* deducted so far from the events with lower priority. The resulting set of mappings from this conflict
* resolution is then used as the new mapping basis against which the next higher-priority event is resolved.
* <ol>
* <li>Mappings that have no overlap in time are never in conflict.</li>
* <li>Mappings for different items are never in conflict.</li>
* <li>Mappings for both the same device and item with overlapping {@code TimeRanges} can be merged.</li>
* <li>Mappings for the same item, but different devices, are in conflict if they have overlapping {@code TimeRanges}.
* The higher-priority mapping of the two takes precedence, and overwrites the lower-priority mapping for the
* duration of the overlap.</li>
* </ol>
* The process of removing conflicts between mappings to ensure that only one mapping was active for
* one tracked item has been removed. Instead, every {@code DeviceMappingEvent} now results directly in one
* {@code DeviceMapping}, the only changes being introduced by closing open time ranges in cases where an
* according {@code CloseOpenEndedDeviceMappingEvent} exists.
*/
public class DeviceMappingFinder<ItemT extends WithID> extends RaceLogAnalyzer<Map<ItemT, List<DeviceMapping<ItemT>>>> {
public DeviceMappingFinder(RaceLog raceLog) {
@@ -53,24 +42,10 @@ public class DeviceMappingFinder<ItemT extends WithID> extends RaceLogAnalyzer<M
return mapping instanceof DeviceMappingEvent;
}
protected DeviceMapping<ItemT> getMapping(DeviceMappingEvent<ItemT> event) {
return new DeviceMappingImpl<ItemT>(event.getMappedTo(), event.getDevice(),
new TimeRangeImpl(event.getFrom(), event.getTo()), event.getId());
}
protected DeviceMapping<ItemT> getMapping(DeviceIdentifier device, ItemT item,
TimePoint from, TimePoint to, List<Serializable> originalEventIds) {
return new DeviceMappingImpl<ItemT>(item, device, new TimeRangeImpl(from, to), originalEventIds);
}
protected DeviceMapping<ItemT> getMapping(DeviceIdentifier device, ItemT item,
TimePoint from, TimePoint to, Serializable originalEventId) {
return getMapping(device, item, from, to, Collections.singletonList(originalEventId));
}
protected DeviceMapping<ItemT> getMapping(DeviceIdentifier device, ItemT item,
TimeRange range, List<Serializable> originalEventIds) {
return new DeviceMappingImpl<ItemT>(item, device, range, originalEventIds);
return new DeviceMappingImpl<ItemT>(item, device, new TimeRangeImpl(from, to),
Collections.singletonList(originalEventId));
}
private <T, U> List<U> getItemSet(Map<T, List<U>> map, T item) {
@@ -81,72 +56,6 @@ public class DeviceMappingFinder<ItemT extends WithID> extends RaceLogAnalyzer<M
}
return list;
}
/**
* Resolves conflicts between an existing set of mappings for one {@code item}, and a new, higher-priority mapping for the same {@code item}.
*/
private List<DeviceMapping<ItemT>> getOverlapFreeMappings(List<DeviceMapping<ItemT>> initial,
DeviceMapping<ItemT> toBeAdded) {
TimeRange timeRange = toBeAdded.getTimeRange();
ItemT item = toBeAdded.getMappedTo();
DeviceIdentifier device = toBeAdded.getDevice();
List<DeviceMapping<ItemT>> result = new ArrayList<DeviceMapping<ItemT>>();
List<Serializable> originalEventIds = new ArrayList<Serializable>();
originalEventIds.addAll(toBeAdded.getOriginalRaceLogEventIds());
for (DeviceMapping<ItemT> otherMapping : initial) {
TimeRange otherTimeRange = otherMapping.getTimeRange();
//time ranges intersect? further investigate
if (otherTimeRange.intersects(timeRange)) {
//same device
if (otherMapping.getDevice().equals(device)) {
//two open ranges that close each other
if ((otherTimeRange.openEnd() && timeRange.openBeginning() && otherTimeRange.from().before(timeRange.to())) ||
(timeRange.openEnd() && otherTimeRange.openBeginning() && timeRange.from().before(otherTimeRange.to()))) {
//shouldn't happen here any more -> dealt with in combineOpenRanges
//timeRange = timeRange.intersection(otherTimeRange);
//otherwise merge
} else {
timeRange = timeRange.union(otherTimeRange);
originalEventIds.addAll(otherMapping.getOriginalRaceLogEventIds());
}
//different device: higher prio overwrites other
} else {
if (otherTimeRange.liesWithin(timeRange)) {
//just ignore the other mapping, as it is completely overwritten by the new one
} else {
if (otherTimeRange.startsBefore(timeRange)) {
//add the part of the lower-prio mapping, that lies before the higher-prio mapping
result.add(getMapping(otherMapping.getDevice(), item, otherTimeRange.from(), timeRange.from().minus(1),
otherMapping.getOriginalRaceLogEventIds()));
}
if (otherTimeRange.endsAfter(timeRange)) {
//add the part of the lower-prio mapping, that lies after the higher-prio mapping
result.add(getMapping(otherMapping.getDevice(), item, timeRange.to().plus(1), otherTimeRange.to(),
otherMapping.getOriginalRaceLogEventIds()));
}
}
}
//no intersection -> add both
} else {
result.add(otherMapping);
}
}
//other mappings have been checked for conflicts, now add toBeAdded (time range may have grown through merges)
result.add(getMapping(device, item, timeRange, toBeAdded.getOriginalRaceLogEventIds()));
return result;
}
@Override
protected Map<ItemT, List<DeviceMapping<ItemT>>> performAnalysis() {
Map<ItemT, List<DeviceMappingEvent<ItemT>>> events = new HashMap<ItemT, List<DeviceMappingEvent<ItemT>>>();
@@ -159,20 +68,14 @@ public class DeviceMappingFinder<ItemT extends WithID> extends RaceLogAnalyzer<M
DeviceMappingEvent<ItemT> mappingEvent = (DeviceMappingEvent<ItemT>) e;
getItemSet(events, mappingEvent.getMappedTo()).add(mappingEvent);
} else if (e instanceof CloseOpenEndedDeviceMappingEvent) {
//a higher priority closing events for the same mapping event overwrites the lower priority one
CloseOpenEndedDeviceMappingEvent closingEvent = (CloseOpenEndedDeviceMappingEvent) e;
closingEvents.put(closingEvent.getDeviceMappingEventId(), closingEvent);
}
}
for (ItemT item : events.keySet()) {
List<DeviceMapping<ItemT>> closedRanges = closeOpenRanges(events.get(item), item, closingEvents);
List<DeviceMapping<ItemT>> iterativelyResolved = new ArrayList<DeviceMapping<ItemT>>();
for (DeviceMapping<ItemT> mapping : closedRanges) {
iterativelyResolved = getOverlapFreeMappings(iterativelyResolved, mapping);
}
mappings.put(item, iterativelyResolved);
mappings.put(item, closeOpenRanges(events.get(item), item, closingEvents));
}
return mappings;
@@ -4360,7 +4360,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
List<Serializable> eventIds) throws NotRevokableException {
RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName);
for (Serializable idToRevoke : eventIds) {
raceLog.lockForRead();
RaceLogEvent event = raceLog.getEventById(idToRevoke);
raceLog.unlockAfterRead();
if (event instanceof Revokable) {
raceLog.revokeEvent(getService().getServerAuthor(), (Revokable) event);
}