mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-07 21:01:03 +00:00
deal with open-ended mappings in DeviceMappingFinder
This commit is contained in:
1 parent
6d52cc1354
commit
7222777c72
4 files changed
+250
-54
No files matched your search
+63
-14
@@ -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());
|
||||
}
|
||||
}
|
||||
+5
@@ -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();
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
+146
-40
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user