store the GPS fix received last for each device in the metadata collection

This commit is contained in:
Axel Uhl
2020-01-09 12:53:39 +01:00
parent 88117c64d9
commit 1cd701132f
14 changed files with 185 additions and 79 deletions
@@ -145,7 +145,7 @@ public enum FieldNames {
DEVICE_ID, GPSFIX_TYPE, GPSFIX,
// GPSFixes metadata
TIMERANGE, NUM_FIXES,
TIMERANGE, NUM_FIXES, LAST_FIX_RECEIVED,
// Timespan
FROM_MILLIS, TO_MILLIS,
@@ -195,37 +195,28 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore {
final Object dbDeviceId = storeDeviceId(deviceServiceFinder, device);
final int nrOfTotalFixes = Util.size(fixes);
final ArrayList<Document> dbFixes = new ArrayList<>(nrOfTotalFixes);
TimePoint newFrom = null;
TimePoint newTo = null;
final TimeRange oldTimeRange = getTimeRangeCoveredByFixes(device);
TimeRange newTimeRange = oldTimeRange;
FixT latestFix = null;
for (FixT fix : fixes) {
String type = fix.getClass().getName();
FixMongoHandler<FixT> mongoHandler = findService(type);
Object fixObject = mongoHandler.transformForth(fix);
Document entry = new Document().append(FieldNames.DEVICE_ID.name(), dbDeviceId)
.append(FieldNames.GPSFIX_TYPE.name(), type).append(FieldNames.GPSFIX.name(), fixObject);
if (latestFix == null || latestFix.getTimePoint().before(fix.getTimePoint())) {
latestFix = fix;
}
Document entry = new Document().append(FieldNames.DEVICE_ID.name(), dbDeviceId);
storeFixToDocument(entry, fix);
mongoOF.storeTimed(fix, entry);
dbFixes.add(entry);
TimePoint fixTP = fix.getTimePoint();
if (newFrom == null || newFrom.after(fixTP)) {
newFrom = fixTP;
}
if (newTo == null || newTo.before(fixTP)) {
newTo = fixTP;
}
final TimeRangeImpl fixTimeRange = new TimeRangeImpl(fixTP, fixTP, /* toIsInclusive */ true);
newTimeRange = newTimeRange == null ? fixTimeRange : newTimeRange.extend(fixTimeRange);
}
fixesCollection.withWriteConcern(WriteConcern.UNACKNOWLEDGED).insertMany(dbFixes);
final Document updateOperation = new Document();
final Document newMetadata = new Document();
newMetadata.put(FieldNames.DEVICE_ID.name(), dbDeviceId);
TimeRange oldTimeRange = getTimeRangeCoveredByFixes(device);
if (oldTimeRange != null) {
newFrom = oldTimeRange.from().before(newFrom) ? oldTimeRange.from() : newFrom;
newTo = oldTimeRange.to().after(newTo) ? oldTimeRange.to() : newTo;
if (latestFix != null) {
newMetadata.put(FieldNames.LAST_FIX_RECEIVED.name(), storeFixToDocument(new Document(), latestFix));
}
final TimeRange newTimeRange = new TimeRangeImpl(newFrom, newTo);
storeTimeRange(newTimeRange, newMetadata, FieldNames.TIMERANGE);
updateOperation.append("$set", newMetadata);
updateOperation.append("$inc", new Document(FieldNames.NUM_FIXES.name(), nrOfTotalFixes));
@@ -239,6 +230,20 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore {
return maneuverChanged;
}
/**
* Writes the {@code fix} to the {@code entry} document such that {@link #loadFix(Document)} can re-establish
* the fix from that document.
*
* @return for convenience and call chaining, the updated {@code entry} is returned
*/
private <FixT extends Timed> Document storeFixToDocument(Document entry, FixT fix) throws TransformationException {
String type = fix.getClass().getName();
FixMongoHandler<FixT> mongoHandler = findService(type);
Object fixObject = mongoHandler.transformForth(fix);
entry.append(FieldNames.GPSFIX_TYPE.name(), type).append(FieldNames.GPSFIX.name(), fixObject);
return entry;
}
@Override
public <FixT extends Timed> void storeFix(DeviceIdentifier device, FixT fix) {
storeFixes(device, Collections.singletonList(fix));
@@ -294,21 +299,27 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore {
@Override
public TimeRange getTimeRangeCoveredByFixes(DeviceIdentifier device)
throws TransformationException, NoCorrespondingServiceRegisteredException {
Document result = findMetadataObject(device);
if (result == null) {
return null;
final Document resultDocument = findMetadataObject(device);
final TimeRange result;
if (resultDocument == null) {
result = null;
} else {
result = DomainObjectFactoryImpl.loadTimeRange(resultDocument, FieldNames.TIMERANGE);
}
return DomainObjectFactoryImpl.loadTimeRange(result, FieldNames.TIMERANGE);
return result;
}
@Override
public long getNumberOfFixes(DeviceIdentifier device)
throws TransformationException, NoCorrespondingServiceRegisteredException {
Document result = findMetadataObject(device);
if (result == null) {
return 0;
final Document resultDocument = findMetadataObject(device);
final long result;
if (resultDocument == null) {
result = 0;
} else {
result = ((Number) resultDocument.get(FieldNames.NUM_FIXES.name())).longValue();
}
return ((Number) result.get(FieldNames.NUM_FIXES.name())).longValue();
return result;
}
/**
@@ -331,20 +342,19 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore {
}
@Override
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getLastFix(Iterable<DeviceIdentifier> forDevices)
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getFixLastReceived(Iterable<DeviceIdentifier> forDevices)
throws TransformationException, NoCorrespondingServiceRegisteredException {
Map<DeviceIdentifier, FixT> result = new HashMap<>();
for (final DeviceIdentifier deviceIdentifier : forDevices) {
final Bson deviceQuery = getDeviceQuery(deviceIdentifier);
final Document orderBy = new Document(
FieldNames.GPSFIX.name() + "." + FieldNames.TIME_AS_MILLIS.name(), -1);
FindIterable<Document> lastFixForDeviceCursor = fixesCollection.find(deviceQuery).sort(orderBy).limit(1);
if (lastFixForDeviceCursor.iterator().hasNext()) {
final Document lastFixForDeviceDbObject = lastFixForDeviceCursor.iterator().next();
final Timed lastFixForDevice = loadFix(lastFixForDeviceDbObject);
@SuppressWarnings("unchecked")
final FixT lastFixForDeviceTyped = (FixT) lastFixForDevice;
result.put(deviceIdentifier, lastFixForDeviceTyped);
final Document metadataDoc = findMetadataObject(deviceIdentifier);
if (metadataDoc != null) {
final Document lastFixForDeviceDbObject = (Document) metadataDoc.get(FieldNames.LAST_FIX_RECEIVED.name());
if (lastFixForDeviceDbObject != null) {
final Timed lastFixForDevice = loadFix(lastFixForDeviceDbObject);
@SuppressWarnings("unchecked")
final FixT lastFixForDeviceTyped = (FixT) lastFixForDevice;
result.put(deviceIdentifier, lastFixForDeviceTyped);
}
}
}
return result;
@@ -30,6 +30,7 @@ import com.sap.sailing.domain.base.impl.CourseImpl;
import com.sap.sailing.domain.base.impl.RaceDefinitionImpl;
import com.sap.sailing.domain.base.impl.WaypointImpl;
import com.sap.sailing.domain.common.DeviceIdentifier;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.racelog.tracking.TransformationException;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
@@ -543,7 +544,8 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
store.storeFix(device, createFix(100, 10, 20, 30, 40));
store.storeFix(device, createFix(200, 10, 20, 30, 40));
assertEquals(2, store.getNumberOfFixes(device));
assertEquals(TimeRangeImpl.create(100, 200), store.getTimeRangeCoveredByFixes(device));
assertEquals(TimeRangeImpl.create(100, 200, /* toIsInclusive */ true), store.getTimeRangeCoveredByFixes(device));
assertEquals(new DegreePosition(10, 20), ((GPSFix) store.getFixLastReceived(Collections.singleton(device)).get(device)).getPosition());
}
@Test
@@ -551,28 +553,28 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
store.storeFix(device, createFix(100, 10, 20, 30, 40));
store.storeFix(device, createFix(1100, 10, 20, 30, 40));
store.storeFix(device, createFix(2100, 10, 20, 30, 40));
final Map<DeviceIdentifier, Timed> lastFixes = store.getLastFix(Collections.singleton(device));
final Map<DeviceIdentifier, Timed> lastFixes = store.getFixLastReceived(Collections.singleton(device));
assertEquals(1, lastFixes.size());
Timed lastFix = lastFixes.get(device);
assertEquals(2100, lastFix.getTimePoint().asMillis());
store.storeFix(device, createFix(2000, 10, 20, 30, 40));
final Map<DeviceIdentifier, Timed> lastFixes2 = store.getLastFix(Collections.singleton(device));
final Map<DeviceIdentifier, Timed> lastFixes2 = store.getFixLastReceived(Collections.singleton(device));
assertEquals(1, lastFixes2.size());
Timed lastFix2 = lastFixes2.get(device);
assertEquals(2100, lastFix2.getTimePoint().asMillis());
assertEquals(2000, lastFix2.getTimePoint().asMillis()); // not the fix with the latest time point but the fix that was last received by the store
store.storeFix(device, createFix(2200, 10, 20, 30, 40));
final Map<DeviceIdentifier, Timed> lastFixes3 = store.getLastFix(Collections.singleton(device));
final Map<DeviceIdentifier, Timed> lastFixes3 = store.getFixLastReceived(Collections.singleton(device));
assertEquals(1, lastFixes3.size());
Timed lastFix3 = lastFixes3.get(device);
assertEquals(2200, lastFix3.getTimePoint().asMillis());
final DeviceIdentifier device2 = new SmartphoneImeiIdentifier("b");
store.storeFix(device2, createFix(1200, 10, 20, 30, 40));
store.storeFix(device2, createFix(1100, 10, 20, 30, 40));
final Map<DeviceIdentifier, Timed> lastFixes4 = store.getLastFix(Arrays.asList(device, device2));
final Map<DeviceIdentifier, Timed> lastFixes4 = store.getFixLastReceived(Arrays.asList(device, device2));
assertEquals(2, lastFixes4.size());
Timed lastFix4 = lastFixes4.get(device);
assertEquals(2200, lastFix4.getTimePoint().asMillis());
Timed lastFixDevice2 = lastFixes4.get(device2);
assertEquals(1200, lastFixDevice2.getTimePoint().asMillis());
assertEquals(1100, lastFixDevice2.getTimePoint().asMillis()); // not the fix with the latest time point but the fix that was last received by the store
}
}
@@ -75,6 +75,32 @@ public class TimeRangeTest {
assertFalse(one.startsBefore(three));
}
@Test
public void extendWithTimePoint() {
TimeRange one = create(5, 10);
assertFalse(one.includes(create(10))); // end is exclusive
TimePoint two = create(20);
TimeRange extension = one.extend(two);
assertTrue(extension.includes(one));
assertTrue(extension.includes(two));
assertTrue(extension.includes(create(5)));
assertTrue(extension.includes(create(10)));
}
@Test
public void extendWithTimeRange() {
TimeRange one = create(5, 10);
assertFalse(one.includes(create(10))); // end is exclusive
TimeRange two = create(20, 30);
assertFalse(two.includes(create(30))); // end is exclusive
TimeRange extension = one.extend(two);
assertTrue(extension.includes(one));
assertTrue(extension.includes(two));
assertTrue(extension.includes(create(5)));
assertTrue(extension.includes(create(10)));
assertFalse(extension.includes(create(30)));
}
@Test
public void union() {
TimeRange one = create(5, 10);
@@ -62,7 +62,7 @@ public enum EmptySensorFixStore implements SensorFixStore {
}
@Override
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getLastFix(Iterable<DeviceIdentifier> forDevices) {
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getFixLastReceived(Iterable<DeviceIdentifier> forDevices) {
return null;
}
@@ -101,7 +101,13 @@ public interface SensorFixStore {
long getNumberOfFixes(DeviceIdentifier device) throws TransformationException, NoCorrespondingServiceRegisteredException;
<FixT extends Timed> Map<DeviceIdentifier, FixT> getLastFix(Iterable<DeviceIdentifier> forDevices) throws TransformationException, NoCorrespondingServiceRegisteredException;
/**
* Obtains the fixes that were received last for each of the devices specified. For devices that have not delivered
* fixes yet, no mapping is created in the resulting map. Note that due to the possibility of out-of-order delivery
* the fixes returned may not be the fixes with the latest time stamp for that device.
*/
<FixT extends Timed> Map<DeviceIdentifier, FixT> getFixLastReceived(Iterable<DeviceIdentifier> forDevices)
throws TransformationException, NoCorrespondingServiceRegisteredException;
/**
* Loads the oldest fix for the given device in the specified {@link TimeRange}.
@@ -186,7 +186,7 @@ public class UDPExpeditionReceiverTest {
}
@Override
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getLastFix(Iterable<DeviceIdentifier> forDevices)
public <FixT extends Timed> Map<DeviceIdentifier, FixT> getFixLastReceived(Iterable<DeviceIdentifier> forDevices)
throws TransformationException, NoCorrespondingServiceRegisteredException {
final Map<DeviceIdentifier, FixT> result = new HashMap<>();
for (final Entry<DeviceIdentifier, List<Timed>> fixes : fixesReceived.entrySet()) {
@@ -348,10 +348,10 @@ public class UDPExpeditionReceiverTest {
final ExpeditionGpsDeviceIdentifier gpsDevice = deviceRegistry.getGpsDeviceIdentifier(0);
final ExpeditionSensorDeviceIdentifier sensorDevice = deviceRegistry.getSensorDeviceIdentifier(0);
assertTrue(sensorFixStore.getNumberOfFixes(gpsDevice) > 0);
assertEquals(-33.907350, ((GPSFixMoving) sensorFixStore.getLastFix(Collections.singleton(gpsDevice)).get(gpsDevice)).getPosition().getLatDeg(), 0.0001);
assertEquals(18.419951, ((GPSFixMoving) sensorFixStore.getLastFix(Collections.singleton(gpsDevice)).get(gpsDevice)).getPosition().getLngDeg(), 0.0001);
assertEquals(-33.907350, ((GPSFixMoving) sensorFixStore.getFixLastReceived(Collections.singleton(gpsDevice)).get(gpsDevice)).getPosition().getLatDeg(), 0.0001);
assertEquals(18.419951, ((GPSFixMoving) sensorFixStore.getFixLastReceived(Collections.singleton(gpsDevice)).get(gpsDevice)).getPosition().getLngDeg(), 0.0001);
assertTrue(sensorFixStore.getNumberOfFixes(sensorDevice) > 0);
final BravoExtendedFix sensorFix = new BravoExtendedFixImpl((DoubleVectorFix) sensorFixStore.getLastFix(Collections.singleton(sensorDevice)).get(sensorDevice));
final BravoExtendedFix sensorFix = new BravoExtendedFixImpl((DoubleVectorFix) sensorFixStore.getFixLastReceived(Collections.singleton(sensorDevice)).get(sensorDevice));
assertEquals(1.872, sensorFix.getRake().getDegrees(), 0.000001);
assertEquals(18.4, sensorFix.getRudder().getDegrees(), 0.05);
}
@@ -7111,7 +7111,7 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet
}
private DeviceMappingDTO convertToDeviceMappingDTO(DeviceMapping<?> mapping) throws TransformationException {
final Map<DeviceIdentifier, Timed> lastFixes = getService().getSensorFixStore().getLastFix(Collections.singleton(mapping.getDevice()));
final Map<DeviceIdentifier, Timed> lastFixes = getService().getSensorFixStore().getFixLastReceived(Collections.singleton(mapping.getDevice()));
final Timed lastFix;
if (lastFixes != null && lastFixes.containsKey(mapping.getDevice())) {
lastFix = lastFixes.get(mapping.getDevice());
@@ -99,7 +99,7 @@ public class CourseConfigurationResource extends AbstractSailingServerResource {
Position lastPosition = null;
try {
// FIXME terribly slow! Furthermore, looking up by deviceIdentifier only misses any other position sources; use regatta?
final Map<DeviceIdentifier, Timed> lastFix = getService().getSensorFixStore().getLastFix(Collections.singleton(deviceIdentifier));
final Map<DeviceIdentifier, Timed> lastFix = getService().getSensorFixStore().getFixLastReceived(Collections.singleton(deviceIdentifier));
final Timed t = lastFix.get(deviceIdentifier);
if (t instanceof GPSFix) {
lastPosition = ((GPSFix) t).getPosition();
@@ -34,7 +34,7 @@ public final class TrackingDeviceStatus {
public static TrackingDeviceStatus calculateDeviceStatus(DeviceIdentifier deviceIdentifier, RacingEventService service) {
try {
Map<DeviceIdentifier, Timed> lastFixMap = service.getSensorFixStore().getLastFix(Collections.singletonList(deviceIdentifier));
Map<DeviceIdentifier, Timed> lastFixMap = service.getSensorFixStore().getFixLastReceived(Collections.singletonList(deviceIdentifier));
Timed lastFix = lastFixMap.get(deviceIdentifier);
GPSFix lastGPSFix = lastFix instanceof GPSFix ? (GPSFix) lastFix : null;
return new TrackingDeviceStatus(deviceIdentifier, lastGPSFix);
@@ -132,7 +132,7 @@ public class CourseAndMarkConfigurationFactoryImpl implements CourseAndMarkConfi
// TODO could as well return the GPSFix object which conveniently would combine the position with the time point which may also be interesting to clients...
Position lastPosition = null;
try {
final Map<DeviceIdentifier, Timed> lastFix = sensorFixStore.getLastFix(Collections.singleton(identifier));
final Map<DeviceIdentifier, Timed> lastFix = sensorFixStore.getFixLastReceived(Collections.singleton(identifier));
final Timed t = lastFix.get(identifier);
if (t instanceof GPSFix) {
lastPosition = ((GPSFix) t).getPosition();
@@ -364,39 +364,40 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
// load passing instruction
final PassingInstruction passingInstruction = PassingInstruction
.valueOf(bdo.get(FieldNames.WAYPOINT_TEMPLATE_PASSINGINSTRUCTION.name()).toString());
// load master data
final String name = bdo.getString(FieldNames.WAYPOINT_TEMPLATE_CONTROL_POINT_NAME.name());
final String shortName = bdo.getString(FieldNames.WAYPOINT_TEMPLATE_CONTROL_POINT_SHORT_NAME.name());
// load mark templates for control point
final ArrayList<?> markTemplateUUIDsDbList = bdo.get(FieldNames.WAYPOINT_TEMPLATE_MARK_TEMPLATES.name(),
ArrayList.class);
boolean hasParsingError = false;
final List<MarkTemplate> markTemplates = new ArrayList<>();
for (Object obj : markTemplateUUIDsDbList) {
final MarkTemplate markTemplate = markTemplateResolver.apply(UUID.fromString(obj.toString()));
if (markTemplate == null) {
logger.warning(String.format("Could not resolve MarkTemplate with id %s for WaypointTemplate.",
obj.toString()));
hasParsingError = true;
break;
} else {
markTemplates.add(markTemplate);
if (markTemplateUUIDsDbList != null) {
for (Object obj : markTemplateUUIDsDbList) {
final MarkTemplate markTemplate = markTemplateResolver.apply(UUID.fromString(obj.toString()));
if (markTemplate == null) {
logger.warning(String.format("Could not resolve MarkTemplate with id %s for WaypointTemplate.",
obj.toString()));
hasParsingError = true;
break;
} else {
markTemplates.add(markTemplate);
}
}
}
final WaypointTemplate result;
if (hasParsingError) {
return null;
}
// create MarkTemplate or MarkTemplatePairImpl
final ControlPointTemplate controlPointTemplate;
if (markTemplates.size() == 2) {
controlPointTemplate = markPairResolver.apply(new Pair<>(name, shortName), markTemplates);
result = null;
} else {
controlPointTemplate = markTemplates.get(0);
// create MarkTemplate or MarkTemplatePairImpl
final ControlPointTemplate controlPointTemplate;
if (markTemplates.size() == 2) {
controlPointTemplate = markPairResolver.apply(new Pair<>(name, shortName), markTemplates);
} else {
controlPointTemplate = markTemplates.get(0);
}
result = new WaypointTemplateImpl(controlPointTemplate, passingInstruction);
}
return new WaypointTemplateImpl(controlPointTemplate, passingInstruction);
return result;
}
}
@@ -63,9 +63,13 @@ public interface TimeRange extends Comparable<TimeRange>, Serializable {
boolean startsBefore(TimeRange other);
boolean startsBefore(TimePoint other);
boolean startsAtOrAfter(TimePoint timePoint);
boolean startsAfter(TimeRange other);
boolean startsAfter(TimePoint timePoint);
boolean endsAfter(TimeRange other);
@@ -125,4 +129,19 @@ public interface TimeRange extends Comparable<TimeRange>, Serializable {
* Short for {@link #from()}.{@link TimePoint#until(TimePoint) until}({@link #to()}).
*/
Duration getDuration();
/**
* Produces a {@link TimeRange} that {@link TimeRange#includes(TimeRange) includes} {@code this} and
* {@link TimeRange#includes(TimePoint) includes} {@code timePoint}. If {@code this} time range already
* {@link #includes(TimePoint)} {@code timePoint}, {@code this} is returned.
*/
TimeRange extend(TimePoint timePoint);
/**
* Produces a {@link TimeRange} that {@link TimeRange#includes(TimeRange) includes} both, {@code this} and
* {@code other}. Other than {@link #union(TimeRange)}, this will also work in case {@code other} does not
* {@link #touches(TimeRange) touch} {@code this} time range. If {@code this} already {@link #includes(TimeRange)}
* {@code other}, {@code this} time range is returned.
*/
TimeRange extend(TimeRange other);
}
@@ -22,6 +22,10 @@ public class TimeRangeImpl extends Util.Pair<TimePoint, TimePoint> implements Ti
return new TimeRangeImpl(new MillisecondsTimePoint(fromMillis), new MillisecondsTimePoint(toMillisExclusive));
}
public static TimeRange create(long fromMillis, long toMillis, boolean toIsInclusive) {
return new TimeRangeImpl(new MillisecondsTimePoint(fromMillis), new MillisecondsTimePoint(toMillis), toIsInclusive);
}
public TimeRangeImpl(TimePoint from, TimePoint to, boolean toIsInclusive) {
this(from, computeInclusiveOrExclusiveTo(to, toIsInclusive));
}
@@ -121,11 +125,21 @@ public class TimeRangeImpl extends Util.Pair<TimePoint, TimePoint> implements Ti
return from().before(other.from());
}
@Override
public boolean startsBefore(TimePoint other) {
return from().before(other);
}
@Override
public boolean startsAtOrAfter(TimePoint timePoint) {
return !from().before(timePoint);
}
@Override
public boolean startsAfter(TimePoint timePoint) {
return from().after(timePoint);
}
@Override
public boolean startsAfter(TimeRange other) {
return startsAtOrAfter(other.to());
@@ -225,6 +239,34 @@ public class TimeRangeImpl extends Util.Pair<TimePoint, TimePoint> implements Ti
return new MultiTimeRangeImpl(result);
}
@Override
public TimeRange extend(TimePoint timePoint) {
final TimeRange result;
if (this.includes(timePoint)) {
result = this;
} else {
if (this.startsAfter(timePoint)) {
result = new TimeRangeImpl(timePoint, to());
} else {
assert this.endsBefore(timePoint);
result = new TimeRangeImpl(from(), computeInclusiveOrExclusiveTo(timePoint, /* isInclusive */ true));
}
}
return result;
}
@Override
public TimeRange extend(TimeRange other) {
final TimeRange preResult = this.extend(other.from());
final TimeRange result;
if (preResult.to().before(other.to())) {
result = new TimeRangeImpl(preResult.from(), other.to());
} else {
result = preResult;
}
return result;
}
@Override
public Duration getDuration() {
return from().until(to());