deal with open-ended mappings in DeviceMappingFinder

This commit is contained in:
Fredrik Teschke committed 2014-02-15 21:50:41 +01:00
1 parent 6d52cc1354
commit 7222777c72
4 files changed
+250 -54

No files matched your search

@@ -1,5 +1,7 @@
package com.sap.sailing.domain.racelog.tracking.analyzing.test;
import static org.junit.Assert.*;
import java.util.List;
import junit.framework.Assert;
@@ -18,7 +20,7 @@ import com.sap.sailing.domain.racelog.tracking.DeviceMapping;
import com.sap.sailing.domain.racelog.tracking.SmartphoneImeiIdentifier;
import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceCompetitorMappingFinder;
public class MappingAnalyzerTest extends AbstractRaceLogTrackingTest {
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");
@@ -28,40 +30,87 @@ public class MappingAnalyzerTest extends AbstractRaceLogTrackingTest {
return new DeviceCompetitorMappingFinder(log).analyze().get(competitor);
}
private void addMapping(RaceLogEventAuthor author, long from, long to, Competitor item) {
private void addMapping(RaceLogEventAuthor author, DeviceIdentifier device, long from, long to, Competitor item) {
RaceLogEvent mapping = factory.createDeviceCompetitorMappingEvent(now, author, device, item, 0,
new MillisecondsTimePoint(from), new MillisecondsTimePoint(to));
log.add(mapping);
}
private void addMapping(RaceLogEventAuthor author, long from, long to, Mark item) {
private void addMapping(RaceLogEventAuthor author, DeviceIdentifier device, long from, long to, Mark item) {
RaceLogEvent mapping = factory.createDeviceMarkMappingEvent(now, author, device, item, 0,
new MillisecondsTimePoint(from), new MillisecondsTimePoint(to));
log.add(mapping);
}
@Test
public void isCorrectMappingGenerate() {
addMapping(author, 10, 40, competitor);
addMapping(author, 10, 40, competitor2);
addMapping(author, 20, 30, competitor2);
addMapping(author, 50, 90, competitor);
public void notDisturbedByMappingsForOtherItemsAndDevices() {
//two mappings for first competitor found? (competitor2 mappings should not "distract")
addMapping(author, device, 10, 40, competitor);
addMapping(author, device, 10, 40, competitor2);
addMapping(author, device, 20, 30, competitor2);
addMapping(author, device, 50, 90, competitor);
Assert.assertEquals(2, getMappings().size());
addMapping(author, 5, 100, competitor);
addMapping(author, 15, 90, mark);
//are mappings merged? (mark mapping should not "distract")
addMapping(author, device, 5, 100, competitor);
addMapping(author, device, 15, 90, mark);
Assert.assertEquals(1, getMappings().size());
addMapping(author, 110, 150, competitor);
//overlap with same device -> should be merged
addMapping(author, device, 30, 101, competitor);
Assert.assertEquals(1, getMappings().size());
//non-overlap with new device
addMapping(author, new SmartphoneImeiIdentifier("imei2"), 110, 150, competitor);
Assert.assertEquals(2, getMappings().size());
addMapping(author, 0, 50, competitor);
//different device at start with overlap
addMapping(author, new SmartphoneImeiIdentifier("imei3"), 0, 50, competitor);
Assert.assertEquals(3, getMappings().size());
addMapping(author, 60, 70, competitor);
//different device in middle -> split
addMapping(author, new SmartphoneImeiIdentifier("imei3"), 60, 70, competitor);
Assert.assertEquals(5, getMappings().size());
addMapping(author, 140, 160, competitor);
//different device at end with overlap
addMapping(author, new SmartphoneImeiIdentifier("imei4"), 140, 160, competitor);
Assert.assertEquals(6, getMappings().size());
}
@Test
public void openRangesCloseEachOther() {
//close one range
addMapping(author, device, 10, Long.MAX_VALUE, competitor);
addMapping(author, device, Long.MIN_VALUE, 20, competitor);
List<DeviceMapping<Competitor>> mappings = getMappings();
assertEquals(1, mappings.size());
DeviceMapping<Competitor> mapping = mappings.get(0);
assertEquals(10, mapping.getTimeRange().from().asMillis());
assertEquals(20, mapping.getTimeRange().to().asMillis());
//another independent range
addMapping(author, device, 0, Long.MAX_VALUE, competitor);
addMapping(author, device, Long.MIN_VALUE, 5, competitor);
mappings = getMappings();
assertEquals(2, mappings.size());
mapping = mappings.get(0);
assertEquals(0, mapping.getTimeRange().from().asMillis());
assertEquals(5, mapping.getTimeRange().to().asMillis());
//another closing mapping, that is closer, but is merged
addMapping(author, device, Long.MIN_VALUE, 3, competitor);
assertEquals(2, getMappings().size());
//another opening mapping, so that two independent closed pairs are created
addMapping(author, device, 4, Long.MAX_VALUE, competitor);
assertEquals(3, getMappings().size());
//open-ended at end, not closed
addMapping(author, device, 30, Long.MAX_VALUE, competitor);
assertEquals(4, getMappings().size());
//open-ended at beginning, eats others up
addMapping(author, device, -10, Long.MAX_VALUE, competitor);
assertEquals(1, getMappings().size());
}
}
@@ -4,8 +4,13 @@ import com.sap.sailing.domain.base.Timed;
import com.sap.sailing.domain.common.TimeRange;
import com.sap.sailing.domain.common.WithID;
/**
* Refer to the documentation of {@link DeviceMappingEvent} for details on how have-open ranges are resolved.
* @author Fredrik Teschke
*/
public interface DeviceMapping<ItemType extends WithID> extends Timed {
ItemType getMappedTo();
DeviceIdentifier getDevice();
TimeRange getTimeRange();
}
@@ -4,10 +4,46 @@ import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WithID;
import com.sap.sailing.domain.racelog.RaceLogEvent;
/**
* Not both {@link #getFrom()} and {@link #getTo()} may be {@code null} at the same time (which would be a mapping for "all times",
* as the semantics of somehow later limiting this range would become unclear, whereas for a time range only open to one end the
* semantics are explained below.
*
* If one end of the time range enclosed by {@link #getFrom()} and {@link #getTo()} is open, this can be closed
* by another {@link DeviceMappingEvent} in the same {@link RaceLog} with equal {@code item} and {@code device},
* that is open to the other side and lies in the open direction of this event.
* E.g. the following two {@link DeviceMappingEvent}s, that are both open to one side, together form one closed range (|---- shall denote
* a time range, that is open in the direction of increasing time values):
* <ul>
* <li>1: |------ </li>
* <li>2: ------| </li>
* <li>1+2: |----------| </li>
* </ul>
*
* Or for the case of multiple such events:
* <ul>
* <li>1: |------ </li>
* <li>2: ------| </li>
* <li>3: |------- </li>
* <li>4: |------ </li>
* <li>5: -----| </li>
* <li>1+2+3+4+5: |-----------| |---------| </li>
* </ul>
* @author Fredrik Teschke
*/
public interface DeviceMappingEvent<ItemType extends WithID> extends RaceLogEvent {
ItemType getMappedTo();
DeviceIdentifier getDevice();
/**
* The {@code return} type may be {@code null}, symbolizing an open range.
* In that case the device should be mapped to the item for any timepoint before that specified by {@link #getTo()}.
*/
TimePoint getFrom();
/**
* The {@code return} type may be {@code null}, symbolizing an open range.
* In that case the device should be mapped to the item for any timepoint after that specified by {@link #getFrom()}.
*/
TimePoint getTo();
}
@@ -13,12 +13,32 @@ 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.DeviceIdentifier;
import com.sap.sailing.domain.racelog.tracking.DeviceMapping;
import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent;
import com.sap.sailing.domain.racelog.tracking.impl.DeviceMappingImpl;
import com.sap.sailing.domain.tracking.impl.TimedComparator;
/**
* Extracts the {@link DeviceMapping}s with the appropriate {@link TimeRange}s from the {@link RaceLog}.
* If there are overlapping mappings for an {@code item} (overlap in time), then the resulting mappings are constructed as follows:
*
* Depending on the overlap between two mappings for the same item, one of the following cases is true
* (--- is timeRange of higher priority mapping, xxx is timeRange of lower priority mapping):
* xxxxxxxxxxxx starting situation
* -------------- case 1: higher prio range completely includes other: overwrite other completely
* xxx----xxxxx case 2: higher prio range lies within other: keep two parts of other (combination of 3 & 4)
* --------xxxxx case 3: higher prio range ends within other: keep only right part of other
* xxx---------- case 4: higher prio range starts within other: keep only left part of other
*
* If both mappings are for the same {@code item} and {@code device}, they can however simply be merged.
*
* As stated in {@link DeviceMappingEvent}, an open-ended mapping can be closed by another open-ended mapping for the same
* {@code item} and {@link competitor}.
* If there are several possible {@link DeviceMappingEvent}s that could close an open range, the closest one is chosen.
* An open-ended {@link DeviceMappingEvent} can close several others.
*/
public abstract class DeviceMappingFinder<ItemT extends WithID> extends RaceLogAnalyzer<Map<ItemT, List<DeviceMapping<ItemT>>>> {
public DeviceMappingFinder(RaceLog raceLog) {
super(raceLog);
@@ -36,8 +56,12 @@ public abstract class DeviceMappingFinder<ItemT extends WithID> extends RaceLogA
return new DeviceMappingImpl<ItemT>(item, device, new TimeRangeImpl(from, to));
}
private List<DeviceMapping<ItemT>> getItemSet(
Map<ItemT, List<DeviceMapping<ItemT>>> map, ItemT item) {
protected DeviceMapping<ItemT> getMapping(DeviceIdentifier device, ItemT item,
TimeRange range) {
return new DeviceMappingImpl<ItemT>(item, device, range);
}
private <T> List<DeviceMapping<ItemT>> getItemSet(Map<T, List<DeviceMapping<ItemT>>> map, T item) {
List<DeviceMapping<ItemT>> list = map.get(item);
if (list == null) {
list = new ArrayList<DeviceMapping<ItemT>>();
@@ -47,11 +71,8 @@ public abstract class DeviceMappingFinder<ItemT extends WithID> extends RaceLogA
}
/**
* If an overlap in time is found between the this mapping, and one already found, the new mapping overrides
* the one in the initialSet for the time of the overlap.
* @param initial
* @param toBeAdded
* @return
* If an overlap in time is found between this mapping, and one already found for the same {@code item}, the new
* mapping overrides the one in the initialSet for the time of the overlap.
*/
private List<DeviceMapping<ItemT>> getOverlapFreeMappings(List<DeviceMapping<ItemT>> initial,
DeviceMapping<ItemT> toBeAdded) {
@@ -59,47 +80,63 @@ public abstract class DeviceMappingFinder<ItemT extends WithID> extends RaceLogA
ItemT item = toBeAdded.getMappedTo();
DeviceIdentifier device = toBeAdded.getDevice();
List<DeviceMapping<ItemT>> result = new ArrayList<DeviceMapping<ItemT>>();
result.add(toBeAdded);
for (DeviceMapping<ItemT> otherMapping : initial) {
TimeRange otherTimeRange = otherMapping.getTimeRange();
if (otherTimeRange.intersects(timeRange)) {
/*
* one of the following can be true (--- is timeRange of toBeAdded, xxx is timeRange of otherMapping)
* xxxxxxxxxxxx timeRange of the otherMapping
*
* -------------- case 1: overwrite otherMapping completely
* xxx----xxxxx case 2: keep two parts of otherMapping (combination of 3 & 4)
* --------xxxxx case 3: keep only right part of otherMapping
* xxx---------- case 4: keep only left part of otherMapping
*/
if (otherTimeRange.liesWithin(timeRange)) {
//case 1: just ignore the other mapping, as it is completely overwritten by the new one
} else {
if (otherTimeRange.startsBefore(timeRange)) {
//case 4
result.add(getMapping(device, item, otherTimeRange.from(), timeRange.from().minus(1)));
}
if (otherTimeRange.endsAfter(timeRange)) {
//case 3
result.add(getMapping(device, item, timeRange.to().plus(1), otherTimeRange.to()));
}
}
} else {
//no overlap between these mappings, can just add the other one
result.add(otherMapping);
}
TimeRange otherTimeRange = otherMapping.getTimeRange();
//time ranges intersect? further investigate
if (otherTimeRange.intersects(timeRange)) {
//same device
if (otherMapping.getDevice() == 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);
}
//different device -> transform according to four cases
} else {
if (otherTimeRange.liesWithin(timeRange)) {
//case 1: just ignore the other mapping, as it is completely overwritten by the new one
} else {
if (otherTimeRange.startsBefore(timeRange)) {
//case 4
result.add(getMapping(device, item, otherTimeRange.from(), timeRange.from().minus(1)));
}
if (otherTimeRange.endsAfter(timeRange)) {
//case 3
result.add(getMapping(device, item, timeRange.to().plus(1), otherTimeRange.to()));
}
}
}
//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));
return result;
}
@Override
protected Map<ItemT, List<DeviceMapping<ItemT>>> performAnalysis() {
Map<ItemT, List<DeviceMapping<ItemT>>> preliminary = new HashMap<ItemT, List<DeviceMapping<ItemT>>>();
Map<ItemT, List<DeviceMapping<ItemT>>> result = new HashMap<ItemT, List<DeviceMapping<ItemT>>>();
/* iterate over events in order
* events with higher importance (later, higher author prio) are located towards the end, and should
* -> events with higher importance (later, higher author prio) are located towards the end, and should
* therefore override those before
*/
for (RaceLogEvent e : raceLog.getUnrevokedEvents()) {
@@ -107,12 +144,81 @@ public abstract class DeviceMappingFinder<ItemT extends WithID> extends RaceLogA
@SuppressWarnings("unchecked")
DeviceMappingEvent<ItemT> mappingEvent = (DeviceMappingEvent<ItemT>) e;
ItemT item = mappingEvent.getMappedTo();
List<DeviceMapping<ItemT>> mappings = getOverlapFreeMappings(getItemSet(result, item), getMapping(mappingEvent));
Collections.sort(mappings, new TimedComparator());
result.put(item, getOverlapFreeMappings(getItemSet(result, item), getMapping(mappingEvent)));
getItemSet(preliminary, item).add(getMapping(mappingEvent));
}
}
//combine mappings that close each other
for (ItemT item : preliminary.keySet()) {
preliminary.put(item, combineOpenRanges(preliminary.get(item), item));
}
//resolve overlaps
for (ItemT item : preliminary.keySet()) {
for (DeviceMapping<ItemT> mapping : preliminary.get(item)) {
List<DeviceMapping<ItemT>> mappings = getOverlapFreeMappings(getItemSet(result, item), mapping);
Collections.sort(mappings, new TimedComparator());
result.put(item, mappings);
}
}
return result;
}
/**
* Combines mappings with open ranges for same device, that also close each other.
* If no matching open-ended mapping is found at all, the mapping is left untouched.
* One open-ended mapping can close multiple other open-ended mappings, if it is the closest for those.
* Mappings without open end are left untouched.
*/
private List<DeviceMapping<ItemT>> combineOpenRanges(List<DeviceMapping<ItemT>> list, ItemT item) {
//sort by devices
Map<DeviceIdentifier, List<DeviceMapping<ItemT>>> byDevice = new HashMap<DeviceIdentifier, List<DeviceMapping<ItemT>>>();
for (DeviceMapping<ItemT> mapping : list) {
getItemSet(byDevice, mapping.getDevice()).add(mapping);
}
List<DeviceMapping<ItemT>> overallResult = new ArrayList<DeviceMapping<ItemT>>();
for (DeviceIdentifier device : byDevice.keySet()) {
List<DeviceMapping<ItemT>> result = new ArrayList<DeviceMapping<ItemT>>();
List<DeviceMapping<ItemT>> openEnds = new ArrayList<DeviceMapping<ItemT>>();
List<DeviceMapping<ItemT>> openBeginnings = new ArrayList<DeviceMapping<ItemT>>();
//find open-ended
for (DeviceMapping<ItemT> mapping : list) {
if (mapping.getTimeRange().openEnd()) openEnds.add(mapping);
else if (mapping.getTimeRange().openBeginning()) openBeginnings.add(mapping);
else result.add(mapping);
}
Collections.sort(openEnds, Collections.reverseOrder(new TimedComparator()));
List<DeviceMapping<ItemT>> usedOpenEnds = new ArrayList<DeviceMapping<ItemT>>();
List<DeviceMapping<ItemT>> usedOpenBeginnings = new ArrayList<DeviceMapping<ItemT>>();
//now find closest matching
for (DeviceMapping<ItemT> openBeginning : openBeginnings) {
TimeRange openBeginningRange = openBeginning.getTimeRange();
for (DeviceMapping<ItemT> openEnd : openEnds) {
TimeRange openEndRange = openEnd.getTimeRange();
if (openEndRange.from().before(openBeginningRange.to())) {
usedOpenEnds.add(openEnd);
usedOpenBeginnings.add(openBeginning);
result.add(getMapping(device, item, openEndRange.intersection(openBeginningRange)));
break;
}
}
}
openEnds.removeAll(usedOpenEnds);
openBeginnings.removeAll(usedOpenBeginnings);
result.addAll(openEnds);
result.addAll(openBeginnings);
overallResult.addAll(result);
}
Collections.sort(overallResult, RaceLogEventComparator.INSTANCE);
return overallResult;
}
}