Merge branch 'master' into translation

This commit is contained in:
Axel Uhl committed 2016-09-29 17:18:19 +02:00
commit 7bdc6e61d1
104 files changed
+2849 -669

No files matched your search

+3 -1
View File
@@ -1,4 +1,4 @@
# Welcome to the SAP Sailing Wiki
# Welcome to the SAP Sailing Wiki
This is the <img src="https://www.sapsailing.com/images/sap-logo_grey.png"/> Wiki where useful information regarding this project can be found.
@@ -54,6 +54,8 @@ SAP is at the center of today’s technology revolution, developing innovations
* [[Administrator|wiki/info/mobile/racecommittee-app-administrator]]
* [[User|wiki/info/mobile/racecommittee-app-user]]
* [[Android and Release Build|wiki/info/mobile/android-and-release-build]]
* [[Energy consumption of mobile apps|wiki/info/mobile/energy-consumption]]
* [[Data consumption of mobile apps|wiki/info/mobile/data-consumption]]
* [[Mobile Sailing Analytics|wiki/info/mobile/mobilesailinganalytics]]
* [[Push Notifications|wiki/info/mobile/push-notifications]]
@@ -24,6 +24,7 @@ public enum BravoSensorDataMetadata {
"BravoNet_Node0x0A0_Voltage", "BravoNet_Node0x0A0_Temperature", "BravoNet_Node0x0A0_Current"));
public final int rideHeightColumn = getColumnIndex(RIDE_HEIGHT);
public final int columnCount = columns.size();
public List<String> getColumns() {
return columns;
@@ -1,5 +1,7 @@
package com.sap.sailing.domain.common.tracking.impl;
import java.util.Arrays;
import com.sap.sailing.domain.common.tracking.DoubleVectorFix;
import com.sap.sse.common.TimePoint;
@@ -34,4 +36,31 @@ public class DoubleVectorFixImpl implements DoubleVectorFix {
return fixData;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(fixData);
result = prime * result + ((timePoint == null) ? 0 : timePoint.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DoubleVectorFixImpl other = (DoubleVectorFixImpl) obj;
if (!Arrays.equals(fixData, other.fixData))
return false;
if (timePoint == null) {
if (other.timePoint != null)
return false;
} else if (!timePoint.equals(other.timePoint))
return false;
return true;
}
}
@@ -125,56 +125,53 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore {
*/
@Override
public <FixT extends Timed> void storeFixes(DeviceIdentifier device, Iterable<FixT> fixes) {
if (Util.isEmpty(fixes)) {
return;
}
try {
final Object dbDeviceId = storeDeviceId(deviceServiceFinder, device);
final int nrOfTotalFixes = Util.size(fixes);
final ArrayList<DBObject> dbFixes = new ArrayList<>(nrOfTotalFixes);
if (!Util.isEmpty(fixes)) {
try {
final Object dbDeviceId = storeDeviceId(deviceServiceFinder, device);
final int nrOfTotalFixes = Util.size(fixes);
final ArrayList<DBObject> dbFixes = new ArrayList<>(nrOfTotalFixes);
TimePoint newFrom = null;
TimePoint newTo = null;
for (FixT fix : fixes) {
String type = fix.getClass().getName();
FixMongoHandler<FixT> mongoHandler = findService(type);
Object fixObject = mongoHandler.transformForth(fix);
DBObject entry = new BasicDBObjectBuilder().add(FieldNames.DEVICE_ID.name(), dbDeviceId)
.add(FieldNames.GPSFIX_TYPE.name(), type).add(FieldNames.GPSFIX.name(), fixObject).get();
mongoOF.storeTimed(fix, entry);
dbFixes.add(entry);
TimePoint fixTP = fix.getTimePoint();
if (newFrom == null || newFrom.after(fixTP)) {
newFrom = fixTP;
TimePoint newFrom = null;
TimePoint newTo = null;
for (FixT fix : fixes) {
String type = fix.getClass().getName();
FixMongoHandler<FixT> mongoHandler = findService(type);
Object fixObject = mongoHandler.transformForth(fix);
DBObject entry = new BasicDBObjectBuilder().add(FieldNames.DEVICE_ID.name(), dbDeviceId)
.add(FieldNames.GPSFIX_TYPE.name(), type).add(FieldNames.GPSFIX.name(), fixObject).get();
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;
}
}
if (newTo == null || newTo.before(fixTP)) {
newTo = fixTP;
fixesCollection.insert(dbFixes);
final BasicDBObject updateOperation = new BasicDBObject();
final BasicDBObject newMetadata = new BasicDBObject();
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;
}
final TimeRange newTimeRange = new TimeRangeImpl(newFrom, newTo);
storeTimeRange(newTimeRange, newMetadata, FieldNames.TIMERANGE);
updateOperation.append("$set", newMetadata);
updateOperation.append("$inc", new BasicDBObject(FieldNames.NUM_FIXES.name(), nrOfTotalFixes));
metadataCollection.update(getDeviceQuery(device), updateOperation, /* create if not existent */ true,
/* update multiple */ false);
} catch (TransformationException e) {
logger.log(Level.WARNING, "Could not store fix in MongoDB");
e.printStackTrace();
}
fixesCollection.insert(dbFixes);
final BasicDBObject updateOperation = new BasicDBObject();
final BasicDBObject newMetadata = new BasicDBObject();
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;
}
final TimeRange newTimeRange = new TimeRangeImpl(newFrom, newTo);
storeTimeRange(newTimeRange, newMetadata, FieldNames.TIMERANGE);
updateOperation.append("$set", newMetadata);
updateOperation.append("$inc", new BasicDBObject(FieldNames.NUM_FIXES.name(), nrOfTotalFixes));
metadataCollection.update(getDeviceQuery(device), updateOperation, /* create if not existent */ true,
/* update multiple */ false);
} catch (TransformationException e) {
logger.log(Level.WARNING, "Could not store fix in MongoDB");
e.printStackTrace();
notifyListeners(device, fixes);
}
notifyListeners(device, fixes);
}
@Override
@@ -71,6 +71,8 @@ public class AbstractGPSFixStoreTest {
MongoObjectFactoryImpl mongoOF = (MongoObjectFactoryImpl) service.getMongoObjectFactory();
mongoOF.getGPSFixCollection().drop();
mongoOF.getGPSFixMetadataCollection().drop();
mongoOF.getRaceLogCollection().drop();
mongoOF.getRegattaLogCollection().drop();
}
protected void map(RegattaLog regattaLog, Competitor comp, DeviceIdentifier device, long from, long to) {
@@ -0,0 +1,657 @@
package com.sap.sailing.domain.racelogtracking.test.impl;
import static com.sap.sse.common.Util.size;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.mockito.Mockito.mock;
import java.io.Serializable;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.mongodb.DB;
import com.mongodb.MongoException;
import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor;
import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl;
import com.sap.sailing.domain.abstractlog.race.RaceLog;
import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver;
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEndOfTrackingEventImpl;
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogImpl;
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.MappingEventVisitor;
import com.sap.sailing.domain.abstractlog.regatta.RegattaLog;
import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEventVisitor;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorSensorDataMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogCloseOpenEndedDeviceMappingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDefineMarkEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDeviceCompetitorBravoMappingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogRevokeEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.impl.RegattaLogImpl;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Sideline;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.CourseImpl;
import com.sap.sailing.domain.base.impl.RaceDefinitionImpl;
import com.sap.sailing.domain.base.impl.RegattaImpl;
import com.sap.sailing.domain.base.impl.WaypointImpl;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.sensordata.BravoSensorDataMetadata;
import com.sap.sailing.domain.common.tracking.DoubleVectorFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.SensorFix;
import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.persistence.PersistenceFactory;
import com.sap.sailing.domain.persistence.impl.CollectionNames;
import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoSensorFixStoreImpl;
import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore;
import com.sap.sailing.domain.racelog.tracking.SensorFixMapper;
import com.sap.sailing.domain.racelog.tracking.SensorFixStore;
import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory;
import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier;
import com.sap.sailing.domain.racelogsensortracking.SensorFixMapperFactory;
import com.sap.sailing.domain.racelogtracking.DeviceIdentifier;
import com.sap.sailing.domain.racelogtracking.impl.BravoDataFixMapper;
import com.sap.sailing.domain.racelogtracking.impl.fixtracker.FixLoaderAndTracker;
import com.sap.sailing.domain.ranking.OneDesignRankingMetric;
import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore;
import com.sap.sailing.domain.tracking.BravoFixTrack;
import com.sap.sailing.domain.tracking.DynamicSensorFixTrack;
import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.SensorFixTrackImpl;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
import com.sap.sse.common.WithID;
import com.sap.sse.common.impl.MillisecondsTimePoint;
public class SensorFixStoreAndLoadTest {
private static final long START_OF_TRACKING = 100;
private static final long MID_OF_TRACKING = 200;
private static final long END_OF_TRACKING = 300;
private static final long FIX_TIMESTAMP = 110;
private static final long FIX_TIMESTAMP2 = 120;
private static final long FIX_TIMESTAMP3 = 210;
private static final long AFTER_LAST_FIX = FIX_TIMESTAMP3 + 1;
private static final double FIX_RIDE_HEIGHT = 1337.0;
private static final double FIX_RIDE_HEIGHT2 = 1338.0;
private static final double FIX_RIDE_HEIGHT3 = 1336.0;
private static final double FIX_TEST_VALUE = 12.0;
protected final MockSmartphoneImeiServiceFinderFactory serviceFinderFactory = new MockSmartphoneImeiServiceFinderFactory();
protected final DeviceIdentifier device = new SmartphoneImeiIdentifier("a");
protected final DeviceIdentifier deviceTest = new SmartphoneImeiIdentifier("b");
protected RaceLog raceLog;
protected RegattaLog regattaLog;
protected SensorFixStore store;
protected final Competitor comp = DomainFactory.INSTANCE.getOrCreateCompetitor("comp", "comp", null, null, null,
null, null, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowanceInSecondsPerNauticalMile */ null, null);
protected final Competitor comp2 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp2", "comp2", null, null, null,
null, null, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowanceInSecondsPerNauticalMile */ null, null);
protected final Mark mark = DomainFactory.INSTANCE.getOrCreateMark("mark");
protected final Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2");
private final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er");
protected final AbstractLogEventAuthor author = new LogEventAuthorImpl("author", 0);
private DynamicTrackedRace trackedRace;
protected GPSFixMoving createFix(long millis, double lat, double lng, double knots, double degrees) {
return new GPSFixMovingImpl(new DegreePosition(lat, lng), new MillisecondsTimePoint(millis),
new KnotSpeedWithBearingImpl(knots, new DegreeBearingImpl(degrees)));
}
@Before
public void setUp() throws UnknownHostException, MongoException {
dropPersistedData();
raceLog = new RaceLogImpl("racelog");
raceLog.add(new RaceLogStartOfTrackingEventImpl(new MillisecondsTimePoint(START_OF_TRACKING), author, 0));
raceLog.add(new RaceLogEndOfTrackingEventImpl(new MillisecondsTimePoint(END_OF_TRACKING), author, 0));
regattaLog = new RegattaLogImpl("regattalog");
store = new MongoSensorFixStoreImpl(PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(),
PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(), serviceFinderFactory);
regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(1), author,
new MillisecondsTimePoint(1), 0, mark));
regattaLog.add(new RegattaLogDefineMarkEventImpl(new MillisecondsTimePoint(2), author,
new MillisecondsTimePoint(1), 0, mark2));
Course course = new CourseImpl("course",
Arrays.asList(new Waypoint[] { new WaypointImpl(mark), new WaypointImpl(mark2) }));
RaceDefinition race = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(comp, comp2));
DynamicTrackedRegatta regatta = new DynamicTrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE,
EmptyRegattaLogStore.INSTANCE, RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass,
/* startDate */ null, /* endDate */null, null, null, "a", null));
trackedRace = new DynamicTrackedRaceImpl(regatta, race, Collections.<Sideline> emptyList(),
EmptyWindStore.INSTANCE, 0, 0, 0, /* useMarkPassingCalculator */ false, OneDesignRankingMetric::new,
mock(RaceLogResolver.class));
}
private void dropPersistedData() {
DB db = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().getDatabase();
db.getCollection(CollectionNames.GPS_FIXES.name()).drop();
db.getCollection(CollectionNames.GPS_FIXES_METADATA.name()).drop();
db.getCollection(CollectionNames.REGATTA_LOGS.name()).drop();
db.getCollection(CollectionNames.RACE_LOGS.name()).drop();
}
@After
public void after() {
dropPersistedData();
}
@Test
public void testLoadAlreadyAddedFixes() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true);
}
@Test
public void testAddFixesWhileTracking() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true);
}
@Test
public void testNoFixesAreLoadedIfNoStoredFixIsInTimeRange() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(AFTER_LAST_FIX), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 0);
fixLoaderAndTracker.stop(true);
}
@Test
public void testFixesNotInMappedTimeRangeAreIgnoredWhileTracking() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(AFTER_LAST_FIX), new MillisecondsTimePoint(END_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 0);
fixLoaderAndTracker.stop(true);
}
@Test
public void testCompetitorWithoutMappingHasNoTrack() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
assertNull(trackedRace.getSensorTrack(comp2, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true);
}
@Test
public void testDeviceIsMappedToDifferentCompetitorsInDifferentTimeRanges() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp2,
device, new MillisecondsTimePoint(MID_OF_TRACKING + 1), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp2, BravoFixTrack.TRACK_NAME), 1);
fixLoaderAndTracker.stop(true);
}
@Test
public void testMultipleMappingsForOneDeviceAndCompetitor() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(MID_OF_TRACKING + 1), new MillisecondsTimePoint(END_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 3);
fixLoaderAndTracker.stop(true);
}
@Test
public void testNothingLoadedForRevokedMapping() throws InterruptedException {
RegattaLogDeviceCompetitorBravoMappingEventImpl mappingEvent = new RegattaLogDeviceCompetitorBravoMappingEventImpl(
new MillisecondsTimePoint(3), author, comp, device, new MillisecondsTimePoint(START_OF_TRACKING),
new MillisecondsTimePoint(MID_OF_TRACKING));
regattaLog.add(mappingEvent);
regattaLog.add(new RegattaLogRevokeEventImpl(author, mappingEvent, "Test purposes"));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
assertNull(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true);
}
@Test
public void testFixesAreLoadedIfThereIsOneRevokedAndOneNonRevokedMapping() throws InterruptedException {
// revoked mapping timestamp 100-300
RegattaLogDeviceCompetitorBravoMappingEventImpl mappingEvent = new RegattaLogDeviceCompetitorBravoMappingEventImpl(
new MillisecondsTimePoint(3), author, comp, device, new MillisecondsTimePoint(START_OF_TRACKING),
new MillisecondsTimePoint(END_OF_TRACKING));
regattaLog.add(mappingEvent);
regattaLog.add(new RegattaLogRevokeEventImpl(author, mappingEvent, "Test purposes"));
// non-revoked mapping timestamp 100-200
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addBravoFixes();
trackedRace.waitForLoadingToFinish();
// Only Fixes from 100 to 200 may be included
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true);
}
@Test
public void testLoadFixesWhenClosingEventIsRevoked() throws InterruptedException {
UUID mappingEventId = UUID.randomUUID();
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(MillisecondsTimePoint.now(),
new MillisecondsTimePoint(3), author, mappingEventId, comp, device, new MillisecondsTimePoint(START_OF_TRACKING),
null));
RegattaLogCloseOpenEndedDeviceMappingEventImpl closeEvent = new RegattaLogCloseOpenEndedDeviceMappingEventImpl(
new MillisecondsTimePoint(4), author, mappingEventId, new MillisecondsTimePoint(MID_OF_TRACKING));
regattaLog.add(closeEvent);
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
testNumberOfRawFixes(bravoFixTrack, 2);
regattaLog.add(new RegattaLogRevokeEventImpl(author, closeEvent, "Test purposes"));
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(bravoFixTrack, 3);
fixLoaderAndTracker.stop(true);
}
@Test
public void testBravoFixIsCorrectlyWrapped() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
assertEquals(FIX_RIDE_HEIGHT,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP)).getRideHeight());
fixLoaderAndTracker.stop(true);
}
@Test
public void testMultipleFixTypesAreLoadedInSeparateTracks() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorTestMappingEventImpl(new MillisecondsTimePoint(1), author, comp,
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
addTestFixes();
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME), 1);
fixLoaderAndTracker.stop(true);
}
@Test
public void testMultipleFixTypesAreLoadedInSeparateTracksWhileTracking() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorTestMappingEventImpl(new MillisecondsTimePoint(1), author, comp,
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addTestFixes();
addBravoFixes();
trackedRace.waitForLoadingToFinish();
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME), 1);
testNumberOfRawFixes(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME), 2);
fixLoaderAndTracker.stop(true);
}
@Test
public void testMultipleFixTypesAreMappedCorrectly() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorTestMappingEventImpl(new MillisecondsTimePoint(1), author, comp,
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(END_OF_TRACKING)));
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
addTestFixes();
addBravoFixes();
trackedRace.waitForLoadingToFinish();
BravoFixTrack<Competitor> bravoFixTrack = trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME);
assertEquals(FIX_RIDE_HEIGHT,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP)).getRideHeight());
assertEquals(FIX_RIDE_HEIGHT2,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP2)).getRideHeight());
assertEquals(FIX_RIDE_HEIGHT3,
bravoFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP3)).getRideHeight());
TestFixTrackImpl<Competitor> testFixTrack = trackedRace.getSensorTrack(comp, TestFixTrackImpl.TRACK_NAME);
assertEquals(FIX_TEST_VALUE,
testFixTrack.getFirstFixAtOrAfter(new MillisecondsTimePoint(FIX_TIMESTAMP)).getTestValue());
fixLoaderAndTracker.stop(true);
}
private void addBravoFixes() {
store.storeFix(device, createBravoDoubleVectorFixWithRideHeight(FIX_TIMESTAMP, FIX_RIDE_HEIGHT));
store.storeFix(device, createBravoDoubleVectorFixWithRideHeight(FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2));
store.storeFix(device, createBravoDoubleVectorFixWithRideHeight(FIX_TIMESTAMP3, FIX_RIDE_HEIGHT3));
}
private FixLoaderAndTracker createFixLoaderAndTracker() {
return new FixLoaderAndTracker(trackedRace, store, new SensorFixMapperFactory() {
private TestDataFixMapper testDataFixMapper = new TestDataFixMapper();
private BravoDataFixMapper bravoDataFixMapper = new BravoDataFixMapper();
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public <FixT extends Timed, TrackT extends DynamicTrack<FixT>> SensorFixMapper<FixT, TrackT, Competitor> createCompetitorMapper(
Class<? extends RegattaLogDeviceMappingEvent<?>> eventType) {
if(bravoDataFixMapper.isResponsibleFor(eventType)) {
return (SensorFixMapper) bravoDataFixMapper;
}
if(testDataFixMapper.isResponsibleFor(eventType)) {
return (SensorFixMapper) testDataFixMapper;
}
throw new IllegalArgumentException("Unknown event type");
}
});
}
protected void testNumberOfRawFixes(Track<?> track, long expected) {
track.lockForRead();
assertEquals(expected, size(track.getRawFixes()));
track.unlockAfterRead();
}
private DoubleVectorFix createBravoDoubleVectorFixWithRideHeight(long timestamp, double rideHeight) {
double[] fixData = new double[BravoSensorDataMetadata.INSTANCE.columnCount];
fixData[BravoSensorDataMetadata.INSTANCE.rideHeightColumn] = rideHeight;
return new DoubleVectorFixImpl(new MillisecondsTimePoint(timestamp), fixData);
}
private void addTestFixes() {
store.storeFix(deviceTest, createTestDoubleVectorFixWithTestValue(FIX_TIMESTAMP, FIX_TEST_VALUE));
}
private DoubleVectorFix createTestDoubleVectorFixWithTestValue(long timestamp, double testValue) {
double[] fixData = new double[TestFixImpl.COLUMNS.size()];
fixData[TestFixImpl.TEST_COLUMN_INDEX] = testValue;
return new DoubleVectorFixImpl(new MillisecondsTimePoint(timestamp), fixData);
}
private static class TestFixImpl implements SensorFix {
private static final long serialVersionUID = 2033254212013220L;
public static final String TEST_COLUMN = "testColumn";
public static final int TEST_COLUMN_INDEX = 1;
public static final List<String> COLUMNS = Collections.unmodifiableList(Arrays.asList("blub", TEST_COLUMN));
private final DoubleVectorFix fix;
public TestFixImpl(DoubleVectorFix fix) {
this.fix = fix;
}
@Override
public double get(String valueName) {
return fix.get(COLUMNS.indexOf(valueName));
}
@Override
public TimePoint getTimePoint() {
return fix.getTimePoint();
}
public double getTestValue() {
return fix.get(TEST_COLUMN_INDEX);
}
}
public class TestFixTrackImpl<ItemType extends WithID & Serializable> extends SensorFixTrackImpl<ItemType, TestFixImpl> {
private static final long serialVersionUID = 5517848726454386L;
public static final String TRACK_NAME = "TestFixTrack";
public TestFixTrackImpl(ItemType trackedItem, String trackName) {
super(trackedItem, trackName, TestFixImpl.COLUMNS,
TRACK_NAME + " for " + trackedItem);
}
public Double getTextValue(TimePoint timePoint) {
TestFixImpl fixAfter = getFirstFixAtOrAfter(timePoint);
if (fixAfter != null && fixAfter.getTimePoint().compareTo(timePoint) == 0) {
// exact match of timepoint -> no interpolation necessary
return fixAfter.getTestValue();
}
TestFixImpl fixBefore = getLastFixAtOrBefore(timePoint);
if (fixBefore != null && fixBefore.getTimePoint().compareTo(timePoint) == 0) {
// exact match of timepoint -> no interpolation necessary
return fixBefore.getTestValue();
}
if (fixAfter == null || fixBefore == null) {
// the fix is out of the TimeRange where we have fixes
return null;
}
return fixBefore.getTestValue();
}
}
public class TestDataFixMapper implements SensorFixMapper<TestFixImpl, DynamicSensorFixTrack<Competitor, TestFixImpl>, Competitor> {
@Override
public DynamicSensorFixTrack<Competitor, TestFixImpl> getTrack(DynamicTrackedRace race, Competitor key) {
return race.getOrCreateSensorTrack(key, TestFixTrackImpl.TRACK_NAME,
() -> new TestFixTrackImpl<Competitor>(key, TestFixTrackImpl.TRACK_NAME));
}
@Override
public TestFixImpl map(DoubleVectorFix fix) {
return new TestFixImpl(fix);
}
@Override
public boolean isResponsibleFor(Class<? extends RegattaLogDeviceMappingEvent<?>> eventType) {
return RegattaLogDeviceCompetitorTestMappingEventImpl.class.isAssignableFrom(eventType);
}
}
public class RegattaLogDeviceCompetitorTestMappingEventImpl extends RegattaLogDeviceMappingEventImpl<Competitor>
implements RegattaLogDeviceCompetitorSensorDataMappingEvent {
private static final long serialVersionUID = -14940305448048753L;
public RegattaLogDeviceCompetitorTestMappingEventImpl(TimePoint createdAt, TimePoint logicalTimePoint,
AbstractLogEventAuthor author, Serializable pId, Competitor mappedTo, DeviceIdentifier device,
TimePoint from, TimePoint to) {
super(createdAt, logicalTimePoint, author, pId, mappedTo, device, from, to);
}
public RegattaLogDeviceCompetitorTestMappingEventImpl(TimePoint logicalTimePoint, AbstractLogEventAuthor author,
Competitor mappedTo, DeviceIdentifier device, TimePoint from, TimePoint to) {
super(logicalTimePoint, author, mappedTo, device, from, to);
}
@Override
public void accept(RegattaLogEventVisitor visitor) {
visitor.visit(this);
}
@Override
public void accept(MappingEventVisitor visitor) {
visitor.visit(this);
}
}
@Test
public void testThatMappingsOutsideOfTheTrackedIntervalDontCauseLoadingToFail() throws InterruptedException {
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp,
device, new MillisecondsTimePoint(10), new MillisecondsTimePoint(20)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp2,
deviceTest, new MillisecondsTimePoint(START_OF_TRACKING), new MillisecondsTimePoint(MID_OF_TRACKING)));
regattaLog.add(new RegattaLogDeviceCompetitorBravoMappingEventImpl(new MillisecondsTimePoint(3), author, comp2,
deviceTest, new MillisecondsTimePoint(10), new MillisecondsTimePoint(20)));
addBravoFixes();
FixLoaderAndTracker fixLoaderAndTracker = createFixLoaderAndTracker();
trackedRace.attachRaceLog(raceLog);
trackedRace.attachRegattaLog(regattaLog);
trackedRace.waitForLoadingToFinish();
assertNotNull(trackedRace.getSensorTrack(comp, BravoFixTrack.TRACK_NAME));
assertNotNull(trackedRace.getSensorTrack(comp2, BravoFixTrack.TRACK_NAME));
fixLoaderAndTracker.stop(true);
}
}
@@ -0,0 +1,301 @@
package com.sap.sailing.domain.racelogtracking.test.impl;
import static org.junit.Assert.assertEquals;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import com.mongodb.DB;
import com.mongodb.MongoException;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.racelog.tracking.TransformationException;
import com.sap.sailing.domain.common.sensordata.BravoSensorDataMetadata;
import com.sap.sailing.domain.common.tracking.DoubleVectorFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.persistence.PersistenceFactory;
import com.sap.sailing.domain.persistence.impl.CollectionNames;
import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoSensorFixStoreImpl;
import com.sap.sailing.domain.racelog.tracking.FixReceivedListener;
import com.sap.sailing.domain.racelog.tracking.SensorFixStore;
import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory;
import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier;
import com.sap.sailing.domain.racelogtracking.DeviceIdentifier;
import com.sap.sse.common.Timed;
import com.sap.sse.common.impl.MillisecondsTimePoint;
public class SensorFixStoreTest {
private static final long FIX_TIMESTAMP = 110;
private static final long FIX_TIMESTAMP2 = 120;
private static final double FIX_RIDE_HEIGHT = 1337.0;
private static final double FIX_RIDE_HEIGHT2 = 1338.0;
protected final MockSmartphoneImeiServiceFinderFactory serviceFinderFactory = new MockSmartphoneImeiServiceFinderFactory();
protected final DeviceIdentifier device = new SmartphoneImeiIdentifier("a");
protected final DeviceIdentifier device2 = new SmartphoneImeiIdentifier("b");
protected SensorFixStore store;
protected GPSFixMoving createFix(long millis, double lat, double lng, double knots, double degrees) {
return new GPSFixMovingImpl(new DegreePosition(lat, lng), new MillisecondsTimePoint(millis),
new KnotSpeedWithBearingImpl(knots, new DegreeBearingImpl(degrees)));
}
@Before
public void setUp() throws UnknownHostException, MongoException {
dropPersistedData();
newStore();
}
private void newStore() {
store = new MongoSensorFixStoreImpl(PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(),
PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(), serviceFinderFactory);
}
@After
public void after() {
dropPersistedData();
}
private void dropPersistedData() {
DB db = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().getDatabase();
db.getCollection(CollectionNames.GPS_FIXES.name()).drop();
db.getCollection(CollectionNames.GPS_FIXES_METADATA.name()).drop();
}
@Test
public void testFixIsPersisted() throws Exception {
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
assertEquals(1, store.getNumberOfFixes(device));
}
/**
* Ensures that no local caching in the store makes in seem to work but data will be lost on restart of the system.
*/
@Test
public void testFixIsFoundByOtherStoreInstance() throws Exception {
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
newStore();
assertEquals(1, store.getNumberOfFixes(device));
}
@Test
public void testFixDataIsPreservedOnStore() throws Exception {
DoubleVectorFix fix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
verifySingleFix(fix, 100, 200, device, true);
}
@Test
public void testFixWithinExclusiveBoundsIsLoaded() throws Exception {
DoubleVectorFix fix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
verifySingleFix(fix, FIX_TIMESTAMP - 1, FIX_TIMESTAMP + 1, device, false);
}
@Test
public void testFixOnInclusiveLowerBoundIsLoaded() throws Exception {
DoubleVectorFix fix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
verifySingleFix(fix, FIX_TIMESTAMP, FIX_TIMESTAMP + 1, device, true);
}
@Test
public void testFixOnInclusiveUpperBoundIsLoaded() throws Exception {
DoubleVectorFix fix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
verifySingleFix(fix, FIX_TIMESTAMP-1, FIX_TIMESTAMP, device, true);
}
@Test
public void testFixesOnExclusiveBoundsArentLoaded() throws Exception {
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
addBravoFix(device, FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2);
verifyNoFix(FIX_TIMESTAMP, FIX_TIMESTAMP2, device, false);
}
@Test
public void testSingleListenerIsNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device, doubleVectorFix);
}
@Test
public void testMultipleListenersForSameDeviceAreNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener1 = mockFixReceivedListener();
FixReceivedListener<DoubleVectorFix> listener2 = mockFixReceivedListener();
store.addListener(listener1, device);
store.addListener(listener2, device);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener1, Mockito.times(1)).fixReceived(device, doubleVectorFix);
Mockito.verify(listener2, Mockito.times(1)).fixReceived(device, doubleVectorFix);
}
@Test
public void testListenerForNonMatchingDeviceIsNotNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
addBravoFix(device2, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verifyZeroInteractions(listener);
}
@Test
public void testOnlyListenerForMatchingDeviceIsNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener1 = mockFixReceivedListener();
FixReceivedListener<DoubleVectorFix> listener2 = mockFixReceivedListener();
store.addListener(listener1, device);
store.addListener(listener2, device2);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener1, Mockito.times(1)).fixReceived(device, doubleVectorFix);
Mockito.verifyZeroInteractions(listener2);
}
@Test
public void testListenerForMatchingDeviceIsNotifiedMultipleTimes() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
DoubleVectorFix doubleVectorFix1 = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
DoubleVectorFix doubleVectorFix2 = addBravoFix(device, FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device, doubleVectorFix1);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device, doubleVectorFix2);
Mockito.verifyNoMoreInteractions(listener);
}
@Test
public void testAfterRemoveListenerIsNotNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.removeListener(listener);
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verifyZeroInteractions(listener);
}
@Test
public void testAfterRemoveForDeviceListenerIsNotNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.removeListener(listener, device);
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verifyZeroInteractions(listener);
}
@Test
public void testAfterRemoveForNonMatchingDeviceListenerIsNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.removeListener(listener, device2);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device, doubleVectorFix);
}
@Test
public void testListenerForMultipleDevicesIsNotifiedMultipleTimes() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.addListener(listener, device2);
DoubleVectorFix doubleVectorFix1 = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
DoubleVectorFix doubleVectorFix2 = addBravoFix(device2, FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device, doubleVectorFix1);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device2, doubleVectorFix2);
Mockito.verifyNoMoreInteractions(listener);
}
@Test
public void testAfterRemoveListenerForMultipleDevicesIsNotNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.addListener(listener, device2);
store.removeListener(listener);
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
addBravoFix(device2, FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2);
Mockito.verifyZeroInteractions(listener);
}
@Test
public void testAfterRemoveListenerForOneDevicesIsNotifiedForOtherDevice() throws Exception {
FixReceivedListener<DoubleVectorFix> listener = mockFixReceivedListener();
store.addListener(listener, device);
store.addListener(listener, device2);
store.removeListener(listener, device);
addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
DoubleVectorFix doubleVectorFix2 = addBravoFix(device2, FIX_TIMESTAMP2, FIX_RIDE_HEIGHT2);
Mockito.verify(listener, Mockito.times(1)).fixReceived(device2, doubleVectorFix2);
Mockito.verifyNoMoreInteractions(listener);
}
@Test
public void testAfterRemoveListenerOtherListenerIsNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener1 = mockFixReceivedListener();
FixReceivedListener<DoubleVectorFix> listener2 = mockFixReceivedListener();
store.addListener(listener1, device);
store.addListener(listener2, device);
store.removeListener(listener1);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener2, Mockito.times(1)).fixReceived(device, doubleVectorFix);
Mockito.verifyNoMoreInteractions(listener2);
Mockito.verifyZeroInteractions(listener1);
}
@Test
public void testAfterRemoveForDeviceListenerOtherListenerIsNotified() throws Exception {
FixReceivedListener<DoubleVectorFix> listener1 = mockFixReceivedListener();
FixReceivedListener<DoubleVectorFix> listener2 = mockFixReceivedListener();
store.addListener(listener1, device);
store.addListener(listener2, device);
store.removeListener(listener1, device);
DoubleVectorFix doubleVectorFix = addBravoFix(device, FIX_TIMESTAMP, FIX_RIDE_HEIGHT);
Mockito.verify(listener2, Mockito.times(1)).fixReceived(device, doubleVectorFix);
Mockito.verifyNoMoreInteractions(listener2);
Mockito.verifyZeroInteractions(listener1);
}
@SuppressWarnings("unchecked")
private FixReceivedListener<DoubleVectorFix> mockFixReceivedListener() {
FixReceivedListener<DoubleVectorFix> listener = Mockito.mock(FixReceivedListener.class);
return listener;
}
private void verifySingleFix(Timed expectedFix, long start, long end, DeviceIdentifier device, boolean inclusive) throws Exception {
List<Timed> loadedFixes = loadFixes(start, end, device, inclusive);
assertEquals(1, loadedFixes.size());
assertEquals(expectedFix, loadedFixes.get(0));
}
private void verifyNoFix(long start, long end, DeviceIdentifier device, boolean inclusive) throws Exception {
List<Timed> loadedFixes = loadFixes(start, end, device, inclusive);
assertEquals(0, loadedFixes.size());
}
private List<Timed> loadFixes(long start, long end, DeviceIdentifier device, boolean inclusive)
throws TransformationException {
List<Timed> loadedFixes = new ArrayList<>();
store.loadFixes(loadedFixes::add, device, new MillisecondsTimePoint(start), new MillisecondsTimePoint(end), inclusive);
return loadedFixes;
}
private DoubleVectorFix addBravoFix(DeviceIdentifier device, long timestamp, double rideHeight) {
DoubleVectorFix fix = createBravoDoubleVectorFixWithRideHeight(timestamp, rideHeight);
store.storeFix(device, fix);
return fix;
}
private DoubleVectorFix createBravoDoubleVectorFixWithRideHeight(long timestamp, double rideHeight) {
double[] fixData = new double[BravoSensorDataMetadata.INSTANCE.columnCount];
fixData[BravoSensorDataMetadata.INSTANCE.rideHeightColumn] = rideHeight;
return new DoubleVectorFixImpl(new MillisecondsTimePoint(timestamp), fixData);
}
}
@@ -6,11 +6,13 @@ import java.util.Set;
import com.sap.sailing.domain.common.tracking.impl.CompactGPSFixImpl;
import com.sap.sailing.domain.common.tracking.impl.CompactGPSFixMovingImpl;
import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl;
import com.sap.sailing.domain.common.tracking.impl.GPSFixImpl;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl;
import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl;
import com.sap.sailing.domain.persistence.racelog.tracking.FixMongoHandler;
import com.sap.sailing.domain.persistence.racelog.tracking.impl.DoubleVectorFixMongoHandlerImpl;
import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMongoHandlerImpl;
import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMovingMongoHandlerImpl;
import com.sap.sse.common.TypeBasedServiceFinder;
@@ -20,6 +22,7 @@ public class MockGPSFixMongoServiceFinder implements TypeBasedServiceFinder<FixM
private final DomainObjectFactoryImpl dof = new DomainObjectFactoryImpl(null, null);
private final FixMongoHandler<?> movingHandler = new GPSFixMovingMongoHandlerImpl(mof, dof);
private final FixMongoHandler<?> handler = new GPSFixMongoHandlerImpl(mof, dof);
private final FixMongoHandler<?> doubleVectorFixHandler = new DoubleVectorFixMongoHandlerImpl(mof, dof);
@Override
public FixMongoHandler<?> findService(String fixType) {
@@ -27,6 +30,7 @@ public class MockGPSFixMongoServiceFinder implements TypeBasedServiceFinder<FixM
fixType.equals(CompactGPSFixMovingImpl.class.getName())) return movingHandler;
if (fixType.equals(GPSFixImpl.class.getName()) ||
fixType.equals(CompactGPSFixImpl.class.getName())) return handler;
if (fixType.equals(DoubleVectorFixImpl.class.getName())) return doubleVectorFixHandler;
return null;
}
@@ -48,7 +48,6 @@ public class EndOfTrackingController extends AbstractRaceChangeListener {
if (!Util.equalsWithNull(trackedRace.getEndOfTracking(), newEndOfTracking)) {
raceLog.add(new RaceLogEndOfTrackingEventImpl(newEndOfTracking,
raceLogEventAuthor, raceLog.getCurrentPassId()));
trackedRace.updateStartAndEndOfTracking(/* waitForGPSFixesToLoad */ false);
}
}
}
@@ -46,7 +46,6 @@ public class StartOfTrackingController implements StartTimeChangedListener {
if (!Util.equalsWithNull(trackedRace.getStartOfTracking(), newStartOfTracking)) {
raceLog.add(new RaceLogStartOfTrackingEventImpl(newStartOfTracking,
raceLogEventAuthor, raceLog.getCurrentPassId()));
trackedRace.updateStartAndEndOfTracking(/* waitForGPSFixesToLoad */ false);
}
}
}
@@ -161,6 +161,9 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
}
private void loadFixes(TimeRange timeRangeToLoad, DeviceMappingWithRegattaLogEvent<? extends WithID> mapping) {
if(timeRangeToLoad == null) {
return;
}
if (preemptiveStopRequested.get()) {
return;
}
@@ -194,8 +197,8 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
try {
@SuppressWarnings({ "unchecked" })
DeviceMapping<Competitor> competitorMapping = (DeviceMapping<Competitor>) mapping;
gpsFixStore.loadCompetitorTrack(track, competitorMapping, getStartOfTracking(),
getEndOfTracking());
gpsFixStore.loadCompetitorTrack(track, competitorMapping, timeRangeToLoad.from(),
timeRangeToLoad.to());
} catch (TransformationException | NoCorrespondingServiceRegisteredException e) {
logger.log(Level.WARNING, "Could not load competitor track " + mapping.getMappedTo());
}
@@ -208,12 +211,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
try {
@SuppressWarnings("unchecked")
DeviceMapping<Mark> markMapping = (DeviceMapping<Mark>) mapping;
TimePoint from = getStartOfTracking();
TimePoint to = getEndOfTracking();
gpsFixStore.loadMarkTrack(track, markMapping, from, to);
gpsFixStore.loadMarkTrack(track, markMapping, timeRangeToLoad.from(), timeRangeToLoad.to());
if (track.getFirstRawFix() == null) {
logger.fine("Loading mark positions from outside of start/end of tracking interval (" + from
+ ".." + to + ") because no fixes were found in that interval");
logger.fine("Loading mark positions from outside of start/end of tracking interval (" + timeRangeToLoad.from()
+ ".." + timeRangeToLoad.to() + ") because no fixes were found in that interval");
// got an empty track for the mark; try again without constraining the mapping interval
// by start/end of tracking to at least attempt to get fixes at all in case there were any
// within the device mapping interval specified
@@ -264,12 +265,10 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
}
}
// TEMP: kommt von RaceLogSensorFixTracker
protected TimePoint getStartOfTracking() {
return trackedRace.getStartOfTracking();
}
// TEMP: kommt von RaceLogSensorFixTracker
protected TimePoint getEndOfTracking() {
return trackedRace.getEndOfTracking();
}
@@ -277,10 +276,7 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
protected void loadFixesForExtendedTimeRange(TimePoint loadFixesFrom, TimePoint loadFixesTo) {
final TimeRangeImpl extendedTimeRange = new TimeRangeImpl(loadFixesFrom, loadFixesTo);
deviceMappings.forEachMapping((mapping) -> {
TimeRange timeRangeToLoad = extendedTimeRange.intersection(mapping.getTimeRange());
if (timeRangeToLoad != null && !preemptiveStopRequested.get()) {
loadFixes(timeRangeToLoad, mapping);
}
loadFixes(extendedTimeRange.intersection(mapping.getTimeRange()), mapping);
});
}
@@ -292,7 +288,6 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
* @param updateCallback
* the {@link Runnable} callback used to run the update
*/
// TODO Consider using a thread pool here, after merging bug3864 into master
private void updateAsyncInternal(final Runnable updateCallback) {
synchronized (FixLoaderAndTracker.this) {
activeLoaders.incrementAndGet();
@@ -301,9 +296,9 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
trackedRace.lockForSerializationRead();
try {
if (!preemptiveStopRequested.get()) {
trackedRace.lockForSerializationRead();
setStatusAndProgress(TrackedRaceStatusEnum.LOADING, 0.5);
synchronized (FixLoaderAndTracker.this) {
FixLoaderAndTracker.this.notifyAll();
@@ -311,15 +306,18 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
updateCallback.run();
}
} finally {
synchronized (FixLoaderAndTracker.this) {
int currentActiveLoaders = activeLoaders.decrementAndGet();
FixLoaderAndTracker.this.notifyAll();
if (currentActiveLoaders == 0) {
setStatusAndProgress(stopRequested.get() ? TrackedRaceStatusEnum.FINISHED
: TrackedRaceStatusEnum.TRACKING, 1.0);
try {
synchronized (FixLoaderAndTracker.this) {
int currentActiveLoaders = activeLoaders.decrementAndGet();
FixLoaderAndTracker.this.notifyAll();
if (currentActiveLoaders == 0) {
setStatusAndProgress(stopRequested.get() ? TrackedRaceStatusEnum.FINISHED
: TrackedRaceStatusEnum.TRACKING, 1.0);
}
}
} finally {
trackedRace.unlockAfterSerializationRead();
}
trackedRace.unlockAfterSerializationRead();
}
}
});
@@ -350,7 +348,7 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
@Override
protected void mappingRemoved(DeviceMappingWithRegattaLogEvent<WithID> mapping) {
final DeviceIdentifier device = mapping.getDevice();
if(!hasMappingForDevice(device)) {
if (!hasMappingForDevice(device)) {
sensorFixStore.removeListener(listener, device);
}
// TODO if tracks are always associated to only one device mapping, we could remove tracks here
@@ -366,8 +364,22 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
@Override
protected void mappingChanged(DeviceMappingWithRegattaLogEvent<WithID> oldMapping,
DeviceMappingWithRegattaLogEvent<WithID> newMapping) {
// TODO can the new time range be bigger than the old one? In this case we would need to load the
// additional time range.
final TimeRange newTimeRange = newMapping.getTimeRange();
final TimeRange oldTimeRange = oldMapping.getTimeRange();
if(newTimeRange.endsAfter(oldTimeRange)) {
final TimePoint oldTo= oldTimeRange.to();
final TimePoint newFrom = newTimeRange.from();
final TimePoint from = oldTo.after(newFrom) ? oldTo : newFrom;
final TimeRange rangeToLoad = new TimeRangeImpl(from, newTimeRange.to());
loadFixes(rangeToLoad, newMapping);
}
if(newTimeRange.startsBefore(oldTimeRange)) {
final TimePoint oldFrom= oldTimeRange.from();
final TimePoint newTo = newTimeRange.to();
final TimePoint to = oldFrom.after(newTo) ? oldFrom : newTo;
final TimeRange rangeToLoad = new TimeRangeImpl(newTimeRange.from(), to);
loadFixes(rangeToLoad, newMapping);
}
}
}
}
@@ -120,7 +120,7 @@ public class RaceLogFixTrackerManager implements TrackingDataLoader {
stopTrackerIfStillRunning(preemptive);
trackedRace.removeListener(raceChangeListener);
synchronized (knownRaceLogs) {
for (RaceLog raceLog : trackedRace.getAttachedRaceLogs()) {
for (RaceLog raceLog : knownRaceLogs) {
removeRaceLogUnlocked(raceLog);
}
}
@@ -142,10 +142,12 @@ public class RaceLogFixTrackerManager implements TrackingDataLoader {
}
boolean isForTracking() {
for (RaceLog raceLog : trackedRace.getAttachedRaceLogs()) {
RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze();
if (raceLogTrackingState.isForTracking()) {
return true;
synchronized (knownRaceLogs) {
for (RaceLog raceLog : knownRaceLogs) {
RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze();
if (raceLogTrackingState.isForTracking()) {
return true;
}
}
}
return false;
@@ -127,11 +127,11 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
knownRegattaLogs.forEach((log) -> log.removeListener(regattaLogEventVisitor));
knownRegattaLogs.clear();
});
}
}
protected void updateMappings() {
try {
updateMappings(true);
updateMappingsInternal();
} catch (Exception e) {
logger.log(Level.WARNING, "Could not update device mappings", e);
}
@@ -163,11 +163,11 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
public void forEachMapping(BiConsumer<ItemT, DeviceMappingWithRegattaLogEvent<ItemT>> callback) {
LockUtil.executeWithReadLock(mappingsLock, () -> {
for (Map.Entry<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> entry : mappings.entrySet()) {
ItemT item = entry.getKey();
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : entry.getValue()) {
callback.accept(item, mapping);
}
ItemT item = entry.getKey();
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : entry.getValue()) {
callback.accept(item, mapping);
}
}
});
}
@@ -187,13 +187,13 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
Consumer<DeviceMappingWithRegattaLogEvent<ItemT>> callback) {
LockUtil.executeWithReadLock(mappingsLock, () -> {
List<DeviceMappingWithRegattaLogEvent<ItemT>> mappingsForDevice = mappingsByDevice.get(device);
if (mappingsForDevice != null) {
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
if (mapping.getTimeRange().includes(timePoint)) {
callback.accept(mapping);
}
if (mappingsForDevice != null) {
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
if (mapping.getTimeRange().includes(timePoint)) {
callback.accept(mapping);
}
}
}
});
}
@@ -236,9 +236,7 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
*
* @throws DoesNotHaveRegattaLogException
*/
private final <FixT extends Timed, TrackT extends DynamicTrack<FixT>> void updateMappings(boolean loadIfNotCovered) {
// TODO remove fixes, if mappings have been removed
// check if there are new time ranges not covered so far
private final <FixT extends Timed, TrackT extends DynamicTrack<FixT>> void updateMappingsInternal() {
final Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> newMappings = calculateMappings();
final Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> oldMappings = new HashMap<>();
LockUtil.lockForWrite(mappingsLock);
@@ -261,18 +259,20 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
} finally {
LockUtil.unlockAfterWrite(mappingsLock);
}
if (loadIfNotCovered) {
calculateDiff(oldMappings, newMappings);
}
calculateDiff(oldMappings, newMappings);
}
/**
* Calculates <em>and applies</em> the mapping changes by removing listeners no longer needed for the mappings removed,
* and by loading and adding the fixes for extended or added mappings.
*/
private void calculateDiff(Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> previousMappings,
Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> newMappings) {
Set<ItemT> itemsToProcess = new HashSet<ItemT>(previousMappings.keySet());
itemsToProcess.addAll(newMappings.keySet());
for(ItemT item : itemsToProcess) {
if(!newMappings.containsKey(item)) {
previousMappings.get(item).forEach(this::mappingRemoved);
for (ItemT item : itemsToProcess) {
if (!newMappings.containsKey(item)) {
previousMappings.get(item).forEach(this::mappingRemovedInternal);
} else {
final List<DeviceMappingWithRegattaLogEvent<ItemT>> oldMappings = previousMappings.containsKey(item)
? previousMappings.get(item) : Collections.emptyList();
@@ -281,12 +281,12 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
DeviceMappingWithRegattaLogEvent<ItemT> oldMapping = findAndRemoveMapping(newMapping,
oldMappings);
if (oldMapping == null) {
mappingAdded(newMapping);
} else if (newMapping.getTimeRange().equals(oldMapping.getTimeRange())) {
mappingChanged(oldMapping, newMapping);
mappingAddedInternal(newMapping);
} else if (!newMapping.getTimeRange().equals(oldMapping.getTimeRange())) {
mappingChangedInternal(oldMapping, newMapping);
}
}
oldMappings.forEach(this::mappingRemoved);
oldMappings.forEach(this::mappingRemovedInternal);
}
}
}
@@ -298,6 +298,14 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
*/
protected abstract void mappingRemoved(DeviceMappingWithRegattaLogEvent<ItemT> mapping);
private void mappingRemovedInternal(DeviceMappingWithRegattaLogEvent<ItemT> mapping) {
try {
mappingRemoved(mapping);
} catch(Exception e) {
logger.log(Level.SEVERE, "error while removing mapping " + mapping, e);
}
}
/**
* Called when a {@link DeviceMapping} was added.
*
@@ -305,6 +313,14 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
*/
protected abstract void mappingAdded(DeviceMappingWithRegattaLogEvent<ItemT> mapping);
private void mappingAddedInternal(DeviceMappingWithRegattaLogEvent<ItemT> mapping) {
try {
mappingAdded(mapping);
} catch(Exception e) {
logger.log(Level.SEVERE, "error while adding mapping " + mapping, e);
}
}
/**
* Called when a {@link DeviceMapping} was changed regarding its mapped time range.
* This can occur if an open ended mapping is being closed or a close event gets revoked.
@@ -315,6 +331,15 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
protected abstract void mappingChanged(DeviceMappingWithRegattaLogEvent<ItemT> oldMapping,
DeviceMappingWithRegattaLogEvent<ItemT> newMapping);
private void mappingChangedInternal(DeviceMappingWithRegattaLogEvent<ItemT> oldMapping,
DeviceMappingWithRegattaLogEvent<ItemT> newMapping) {
try {
mappingChanged(oldMapping, newMapping);
} catch(Exception e) {
logger.log(Level.SEVERE, "error while changing mapping old: " + oldMapping + "; new: " + newMapping, e);
}
}
private DeviceMappingWithRegattaLogEvent<ItemT> findAndRemoveMapping(
DeviceMappingWithRegattaLogEvent<ItemT> mappingToFind,
List<DeviceMappingWithRegattaLogEvent<ItemT>> newItemsToProcess) {
@@ -65,7 +65,7 @@ public abstract class BaseRegattaLogDeviceMappingFinder<ItemT extends WithID>
if (!endOfFirstHalf.before(from)) {
log.add(createDeviceMappingEvent(item, event.getAuthor(), from, endOfFirstHalf, event.getDevice()));
}
if (!to.before(startOfSecondHalf)) {
if (to == null || !to.before(startOfSecondHalf)) {
log.add(createDeviceMappingEvent(item, event.getAuthor(), startOfSecondHalf, to,
event.getDevice()));
}
@@ -46,7 +46,7 @@ public class WaypointImpl implements Waypoint {
@Override
public String toString() {
return getName();
return (getName()==null?"":getName()) + " ("+getPassingInstructions()+")";
}
@Override
@@ -107,12 +107,12 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
}
/**
* Callers that want to iterate over the collection returned need to synchronize on <code>this</code> object to
* avoid {@link ConcurrentModificationException}s.
* Callers that want to iterate over the collection returned need to use {@link #lockForRead()} and
* {@link #unlockAfterRead()} to avoid {@link ConcurrentModificationException}s.
*
* @return the smoothened fixes ordered by their time points; this implementation simply delegates to {@link #getInternalRawFixes()} because for
* only {@link Timed} fixes we can't know how to remove outliers. Subclasses that constrain the
* <code>FixType</code> may provide smoothening implementations.
* @return the smoothened fixes ordered by their time points; this implementation simply delegates to
* {@link #getInternalRawFixes()} because for only {@link Timed} fixes we can't know how to remove outliers.
* Subclasses that constrain the <code>FixType</code> may provide smoothening implementations.
*/
protected NavigableSet<FixType> getInternalFixes() {
NavigableSet<FixType> result = getInternalRawFixes();
@@ -10,7 +10,6 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import org.junit.Before;
@@ -30,6 +29,7 @@ import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.util.ThreadPoolUtil;
import com.tractrac.model.lib.api.event.CreateModelException;
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
@@ -77,7 +77,7 @@ public class ConcurrencyTest extends OnlineTracTracBasedTest {
logger.info("1 thread: "+duration+"ns");
logger.info("number of approximation points: "+approximation1.size());
dp = new DouglasPeucker<Competitor, GPSFixMoving>(teamAquaTrack, Executors.newFixedThreadPool(numberOfCPUs));
dp = new DouglasPeucker<Competitor, GPSFixMoving>(teamAquaTrack, ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor());
start = System.nanoTime();
List<GPSFixMoving> approximation2 = dp.approximate(new MeterDistance(3), firstMarkPassing.getTimePoint(), lastMarkPassing.getTimePoint());
duration = System.nanoTime()-start;
@@ -0,0 +1,80 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.abstractlog.regatta.RegattaLog;
import com.sap.sailing.domain.abstractlog.regatta.impl.RegattaLogImpl;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard;
import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl;
import com.sap.sailing.domain.leaderboard.impl.LowPoint;
import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore;
import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
import com.sap.sailing.domain.test.mock.MockedTrackedRace;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sse.common.Util;
public class RegattaLogAttachOnTrackedRaceTest {
private class MyMockedRegattaLogStore implements RegattaLogStore {
private Map<RegattaLikeIdentifier, RegattaLog> regattaLogs = new HashMap<>();
@Override
public RegattaLog getRegattaLog(RegattaLikeIdentifier regattaLikeId, boolean ignoreCache) {
if(!regattaLogs.containsKey(regattaLikeId)) {
regattaLogs.put(regattaLikeId, new RegattaLogImpl(regattaLikeId));
}
return regattaLogs.get(regattaLikeId);
}
@Override
public void removeRegattaLog(RegattaLikeIdentifier regattaLikeId) {
}
@Override
public void addImportedRegattaLog(RegattaLog regattaLog, RegattaLikeIdentifier identifier) {
}
}
private class MyMockedTrackedRace extends MockedTrackedRace {
private static final long serialVersionUID = -5450144964435682919L;
private RegattaLog regattaLog;
@Override
public void attachRegattaLog(RegattaLog regattaLog) {
this.regattaLog = regattaLog;
}
@Override
public Iterable<RegattaLog> getAttachedRegattaLogs() {
return Collections.singletonList(regattaLog);
}
}
private FlexibleLeaderboard leaderboard;
@Before
public void setUp() {
leaderboard = new FlexibleLeaderboardImpl(EmptyRaceLogStore.INSTANCE, new MyMockedRegattaLogStore(), "", null, new LowPoint(), null);
}
@Test
public void testAttachRegattaLogIsCalledWhenTrackedRaceIsSetToARaceColumn() {
RaceColumn column = leaderboard.addRaceColumn("R1", false);
TrackedRace trackedRace = new MyMockedTrackedRace();
Fleet defaultFleet = Util.get(column.getFleets(), 0);
column.setTrackedRace(defaultFleet, trackedRace);
assertEquals(1, Util.size(trackedRace.getAttachedRegattaLogs()));
assertSame(leaderboard.getRegattaLog(), Util.get(trackedRace.getAttachedRegattaLogs(), 0));
}
}
@@ -116,7 +116,7 @@ public class BravoFixTrackSerializationTest {
}
private void addOrReplaceBravoFixToTrack(boolean replace) {
double[] fixData = new double[BravoSensorDataMetadata.INSTANCE.getColumns().size()];
double[] fixData = new double[BravoSensorDataMetadata.INSTANCE.columnCount];
fixData[BravoSensorDataMetadata.INSTANCE.rideHeightColumn] = rideHeight;
track.add(new BravoFixImpl(new DoubleVectorFixImpl(timePoint, fixData)), replace);
}
@@ -43,6 +43,10 @@ public abstract class AbstractRaceExecutionOrderProvider implements RaceExecutio
protected abstract Map<Fleet, Iterable<? extends RaceColumn>> getRaceColumnsOfSeries();
/**
* For all tracked races found on any of the {@link #getRaceColumnsOfSeries() race columns} produces a valid and
* potentially empty set.
*/
private Map<TrackedRace, Set<TrackedRace>> reloadAndGetPreviousRacesByRace() {
final Map<TrackedRace, Set<TrackedRace>> previousRacesByRace = new HashMap<>();
for (Entry<Fleet, Iterable<? extends RaceColumn>> raceColumnsInSeries : getRaceColumnsOfSeries().entrySet()) {
@@ -96,10 +100,12 @@ public abstract class AbstractRaceExecutionOrderProvider implements RaceExecutio
@Override
public Set<TrackedRace> getPreviousRacesInExecutionOrder(TrackedRace race) {
Set<TrackedRace> result = Collections.emptySet();
final Set<TrackedRace> result;
final Map<TrackedRace, Set<TrackedRace>> previousRacesByRace = getPreviousRacesByRace();
if (previousRacesByRace != null) {
result = previousRacesByRace.get(race);
} else {
result = Collections.emptySet();
}
return result;
}
@@ -6,12 +6,9 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.ScheduledExecutorService;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
@@ -31,7 +28,7 @@ import com.sap.sse.common.Util;
public class DouglasPeucker<ItemType, FixType extends GPSFix> {
private final GPSFixTrack<ItemType, FixType> track;
private final Executor executor;
private final ScheduledExecutorService executor;
public DouglasPeucker(GPSFixTrack<ItemType, FixType> track) {
this.track = track;
@@ -42,7 +39,7 @@ public class DouglasPeucker<ItemType, FixType extends GPSFix> {
* Allows clients to specify a non-default, optionally multi-threaded executor that will be used to perform the
* high-effort computations.
*/
public DouglasPeucker(GPSFixTrack<ItemType, FixType> track, Executor executor) {
public DouglasPeucker(GPSFixTrack<ItemType, FixType> track, ScheduledExecutorService executor) {
this.track = track;
this.executor = executor;
}
@@ -96,15 +93,8 @@ public class DouglasPeucker<ItemType, FixType extends GPSFix> {
if (fix.getTimePoint().compareTo(to) > 0) {
break;
}
RunnableFuture<Util.Pair<FixType, Distance>> crossTrackErrorFuture = new FutureTask<Util.Pair<FixType, Distance>>(
new Callable<Util.Pair<FixType, Distance>>() {
@Override
public Util.Pair<FixType, Distance> call() throws Exception {
return new Util.Pair<FixType, Distance>(fix, fix.getPosition().absoluteCrossTrackError(
fromPosition, bearing));
}
});
executor.execute(crossTrackErrorFuture);
Future<Util.Pair<FixType, Distance>> crossTrackErrorFuture = executor.submit(
()->new Util.Pair<FixType, Distance>(fix, fix.getPosition().absoluteCrossTrackError(fromPosition, bearing)));
crossTrackErrorFutures.add(crossTrackErrorFuture);
}
for (Future<Util.Pair<FixType, Distance>> crossTrackErrorFuture : crossTrackErrorFutures) {
@@ -23,6 +23,7 @@ import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.util.impl.FutureTaskWithTracingGet;
/**
* Caches the expensive to compute {@link LeaderboardDTO} results of a
@@ -140,15 +141,16 @@ public class LeaderboardDTOCache implements LeaderboardCache {
LockUtil.unlockAfterRead(leaderboardCacheLock);
}
if (future == null) {
future = new FutureTask<LeaderboardDTO>(new Callable<LeaderboardDTO>() {
@Override
public LeaderboardDTO call() throws Exception {
LeaderboardDTO result = leaderboard.computeDTO(adjustedTimePoint,
namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails,
waitForLatestAnalyses, trackedRegattaRegistry, baseDomainFactory, /* fillTotalPointsUncorrected */ false);
return result;
}
});
future = new FutureTaskWithTracingGet<LeaderboardDTO>(LeaderboardDTOCache.class.getName()+" for leaderboard "+leaderboard.getName(),
new Callable<LeaderboardDTO>() {
@Override
public LeaderboardDTO call() throws Exception {
LeaderboardDTO result = leaderboard.computeDTO(adjustedTimePoint,
namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails,
waitForLatestAnalyses, trackedRegattaRegistry, baseDomainFactory, /* fillTotalPointsUncorrected */ false);
return result;
}
});
// The add(Leaderboard) method that the cache manager calls back on this class does nothing, so no synchronization required
this.leaderboardCacheManager.add(leaderboard); // ensure the leaderboard is tracked for changes to invalidate
LockUtil.lockForWrite(leaderboardCacheLock);
@@ -21,7 +21,6 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -97,6 +96,7 @@ import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.util.ThreadPoolUtil;
import com.sap.sse.util.impl.FutureTaskWithTracingGet;
/**
* Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events
@@ -311,7 +311,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
this.suppressedCompetitors = new HashSet<Competitor>();
this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false);
this.raceColumnListeners = new RaceColumnListeners();
this.raceDetailsAtEndOfTrackingCache = new HashMap<com.sap.sse.common.Util.Pair<TrackedRace, Competitor>, RunnableFuture<RaceDetails>>();
this.raceDetailsAtEndOfTrackingCache = new HashMap<>();
initTransientFields();
}
@@ -330,7 +330,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
}
private void initTransientFields() {
this.raceDetailsAtEndOfTrackingCache = new HashMap<com.sap.sse.common.Util.Pair<TrackedRace,Competitor>, RunnableFuture<RaceDetails>>();
this.raceDetailsAtEndOfTrackingCache = new HashMap<>();
this.cacheInvalidationListeners = new HashSet<CacheInvalidationListener>();
// When many updates are triggered in a short period of time by a single thread, ensure that the single thread
// providing the updates is not outperformed by all the re-calculations happening here. Leave at least one
@@ -1225,7 +1225,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
}
// Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these
// futures to finish:
Map<RaceColumn, FutureTask<List<CompetitorDTO>>> competitorsFromBestToWorstTasks = new HashMap<>();
Map<RaceColumn, Future<List<CompetitorDTO>>> competitorsFromBestToWorstTasks = new HashMap<>();
for (final RaceColumn raceColumn : this.getRaceColumns()) {
boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn;
RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(),
@@ -1253,13 +1253,12 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null,
fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn);
}
FutureTask<List<CompetitorDTO>> task = new FutureTask<List<CompetitorDTO>>(
Future<List<CompetitorDTO>> task = executor.submit(
() -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint)));
executor.execute(task);
competitorsFromBestToWorstTasks.put(raceColumn, task);
}
// wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data
for (Map.Entry<RaceColumn, FutureTask<List<CompetitorDTO>>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) {
for (Map.Entry<RaceColumn, Future<List<CompetitorDTO>>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) {
try {
result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get());
} catch (InterruptedException e) {
@@ -1323,7 +1322,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
Map<String, Future<LeaderboardEntryDTO>> futuresForColumnName = new HashMap<String, Future<LeaderboardEntryDTO>>();
final Set<RaceColumn> discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint);
for (final RaceColumn raceColumn : this.getRaceColumns()) {
RunnableFuture<LeaderboardEntryDTO> future = new FutureTask<LeaderboardEntryDTO>(() -> {
Future<LeaderboardEntryDTO> future = executor.submit(() -> {
Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns);
return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint,
namesOfRaceColumnsForWhichToLoadLegDetails != null
@@ -1331,7 +1330,6 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
.getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory,
fillTotalPointsUncorrected, cache);
});
executor.execute(future);
futuresForColumnName.put(raceColumn.getName(), future);
}
for (Map.Entry<String, Future<LeaderboardEntryDTO>> raceColumnNameAndFuture : futuresForColumnName.entrySet()) {
@@ -1611,7 +1609,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
raceDetails = raceDetailsAtEndOfTrackingCache.get(key);
if (raceDetails == null) {
needToRunRaceDetails = true;
raceDetails = new FutureTask<RaceDetails>(new Callable<RaceDetails>() {
raceDetails = new FutureTaskWithTracingGet<RaceDetails>("RaceDetails for "+trackedRace, new Callable<RaceDetails>() {
@Override
public RaceDetails call() throws Exception {
TimePoint end = trackedRace.getEndOfRace();
@@ -9,7 +9,6 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -173,7 +172,7 @@ public class MarkPassingCalculator {
+ " while waiting for new GPSFixes");
}
listener.getQueue().drainTo(allNewFixInsertions);
logger.finer("MPC for "+raceName+" recieved "+ allNewFixInsertions.size()+" new updates.");
logger.fine("MPC for "+raceName+" received "+ allNewFixInsertions.size()+" new updates.");
for (StorePositionUpdateStrategy fixInsertion : allNewFixInsertions) {
if (listener.isEndMarker(fixInsertion)) {
logger.info("Stopping "+MarkPassingCalculator.this+"'s listener for race "+raceName);
@@ -212,8 +211,7 @@ public class MarkPassingCalculator {
}
executor.invokeAll(tasks);
}
updateManuallySetMarkPassings(fixedMarkPassings, removedFixedMarkPassings, suppressedMarkPassings,
unsuppressedMarkPassings);
updateManuallySetMarkPassings(fixedMarkPassings, removedFixedMarkPassings, suppressedMarkPassings, unsuppressedMarkPassings);
computeMarkPasses(competitorFixes, markFixes);
competitorFixes.clear();
markFixes.clear();
@@ -284,20 +282,35 @@ public class MarkPassingCalculator {
fixes.addAll(fixesAffectedByNewMarkFixes.getValue());
}
}
List<Callable<Object>> tasks = new ArrayList<>();
for (Entry<Competitor, Set<GPSFix>> c : combinedCompetitorFixes.entrySet()) {
tasks.add(Executors.callable(new ComputeMarkPassings(c.getKey(), c.getValue())));
List<Callable<Void>> tasks = new ArrayList<>();
for (final Entry<Competitor, Set<GPSFix>> c : combinedCompetitorFixes.entrySet()) {
final Runnable runnable = new ComputeMarkPassings(c.getKey(), c.getValue());
tasks.add(new Callable<Void>() {
@Override
public Void call() throws Exception {
runnable.run();
return null;
}
/**
* A reasonable toString implementation helps identifying these tasks in log messages for long-running tasks
*/
@Override
public String toString() {
return "Mark passing calculation for competitor "+c.getKey()+" with "+c.getValue().size()+" fixes";
}
});
}
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
e.printStackTrace();
logger.log(Level.INFO, "mark passing calculation interrupted", e);
}
}
private class ComputeMarkPassings implements Runnable {
Competitor c;
Iterable<GPSFix> fixes;
final Competitor c;
final Iterable<GPSFix> fixes;
public ComputeMarkPassings(Competitor c, Iterable<GPSFix> fixes) {
this.c = c;
@@ -306,12 +319,22 @@ public class MarkPassingCalculator {
@Override
public void run() {
logger.finer("Calculating MarkPassings for race "+raceName+", competitor " + c + " (" + Util.size(fixes) + " new fixes)");
Util.Pair<Iterable<Candidate>, Iterable<Candidate>> candidateDeltas = finder.getCandidateDeltas(c, fixes);
logger.finer("Received " + Util.size(candidateDeltas.getA()) + " new Candidates for race "+raceName+
" and competitor "+c+" and will remove "
+ Util.size(candidateDeltas.getB()) + " old Candidates for " + c);
chooser.calculateMarkPassDeltas(c, candidateDeltas.getA(), candidateDeltas.getB());
try {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Calculating MarkPassings for race "+raceName+", competitor " + c + " (" + Util.size(fixes) + " new fixes)");
}
Util.Pair<Iterable<Candidate>, Iterable<Candidate>> candidateDeltas = finder.getCandidateDeltas(c, fixes);
if (logger.isLoggable(Level.FINER)) {
logger.finer("Received " + Util.size(candidateDeltas.getA()) + " new Candidates for race "+raceName+
" and competitor "+c+" and will remove "
+ Util.size(candidateDeltas.getB()) + " old Candidates for " + c);
}
chooser.calculateMarkPassDeltas(c, candidateDeltas.getA(), candidateDeltas.getB());
} catch (Exception e) {
// make sure the exception is logged and not only "swallowed" and suppressed by an invokeAll(...) statement
logger.log(Level.SEVERE, "Exception trying to compute mark passings for competitor "+c, e);
throw e;
}
}
}
@@ -47,6 +47,7 @@ import com.sap.sse.common.Timed;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.impl.TimeRangeImpl;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.util.ThreadPoolUtil;
/**
@@ -383,24 +384,30 @@ public class CandidateFinderImpl implements CandidateFinder {
changedWaypoints.add(w);
}
}
final Set<Callable<Void>> tasks = new HashSet<>();;
final Set<Callable<Void>> tasks = new HashSet<>();
final Thread executingThread = Thread.currentThread(); // most likely the MarkPassingCalculator.Listen thread
for (Competitor c : race.getRace().getCompetitors()) {
tasks.add(()->{
List<Candidate> badCans = new ArrayList<>();
List<Candidate> newCans = new ArrayList<>();
for (Waypoint w : changedWaypoints) {
Map<List<GPSFix>, Candidate> xteCans = getXteCandidates(c, w);
badCans.addAll(xteCans.values());
xteCans.clear();
Map<GPSFix, Candidate> distanceCans = getDistanceCandidates(c, w);
badCans.addAll(distanceCans.values());
distanceCans.clear();
LockUtil.propagateLockSetFrom(executingThread); // "inherit" the course read lock from the "calling" thread into the thread pool executor thread
try {
List<Candidate> badCans = new ArrayList<>();
List<Candidate> newCans = new ArrayList<>();
for (Waypoint w : changedWaypoints) {
Map<List<GPSFix>, Candidate> xteCans = getXteCandidates(c, w);
badCans.addAll(xteCans.values());
xteCans.clear();
Map<GPSFix, Candidate> distanceCans = getDistanceCandidates(c, w);
badCans.addAll(distanceCans.values());
distanceCans.clear();
}
Set<GPSFix> allFixes = getAllFixes(c);
newCans.addAll(checkForDistanceCandidateChanges(c, allFixes, changedWaypoints).getA());
newCans.addAll(checkForXTECandidatesChanges(c, allFixes, changedWaypoints).getA());
result.put(c, new Util.Pair<List<Candidate>, List<Candidate>>(newCans, badCans));
return null;
} finally {
LockUtil.unpropagateLockSetFrom(executingThread); // "return" the course read lock from the thread pool executor thread to the "calling" thread
}
Set<GPSFix> allFixes = getAllFixes(c);
newCans.addAll(checkForDistanceCandidateChanges(c, allFixes, changedWaypoints).getA());
newCans.addAll(checkForXTECandidatesChanges(c, allFixes, changedWaypoints).getA());
result.put(c, new Util.Pair<List<Candidate>, List<Candidate>>(newCans, badCans));
return null;
});
}
executor.invokeAll(tasks);
@@ -513,8 +520,8 @@ public class CandidateFinderImpl implements CandidateFinder {
private Set<GPSFix> getAllFixes(Competitor c) {
Set<GPSFix> fixes = new TreeSet<GPSFix>(comp);
DynamicGPSFixTrack<Competitor, GPSFixMoving> track = race.getTrack(c);
track.lockForRead();
try {
track.lockForRead();
for (GPSFix fix : track.getFixes(
timeRangeForValidCandidates.from(), /* fromInclusive */ true,
timeRangeForValidCandidates.to(), /* toInclusive */ true)) {
@@ -542,8 +549,8 @@ public class CandidateFinderImpl implements CandidateFinder {
affectedFixes.add(fix);
GPSFix fixBefore;
GPSFix fixAfter;
track.lockForRead();
try {
track.lockForRead();
TimePoint timePoint = fix.getTimePoint();
fixBefore = track.getLastFixBefore(timePoint);
fixAfter = track.getFirstFixAfter(timePoint);
@@ -1206,19 +1213,24 @@ public class CandidateFinderImpl implements CandidateFinder {
* @return an average of the estimated legs before and after <code>w</code>.
*/
private Distance getAverageLengthOfAdjacentLegs(TimePoint t, Waypoint w) {
Course course = race.getRace().getCourse();
if (w == course.getFirstWaypoint()) {
return race.getTrackedLegStartingAt(w).getGreatCircleDistance(t);
final Distance result;
final Course course = race.getRace().getCourse();
if (course.getNumberOfWaypoints() < 2) {
result = null;
} else if (w == course.getFirstWaypoint()) {
result = race.getTrackedLegStartingAt(w).getGreatCircleDistance(t);
} else if (w == course.getLastWaypoint()) {
return race.getTrackedLegFinishingAt(w).getGreatCircleDistance(t);
result = race.getTrackedLegFinishingAt(w).getGreatCircleDistance(t);
} else {
Distance before = race.getTrackedLegStartingAt(w).getGreatCircleDistance(t);
Distance after = race.getTrackedLegFinishingAt(w).getGreatCircleDistance(t);
if (after != null && before != null) {
return new MeterDistance(before.add(after).getMeters() / 2);
result = new MeterDistance(before.add(after).getMeters() / 2);
} else {
result = null;
}
return null;
}
return result;
}
/**
@@ -1414,8 +1426,9 @@ public class CandidateFinderImpl implements CandidateFinder {
final TimePoint newNonInferredStartTime = race.getStartOfRace(/* inferred */ false);
final TimePoint newTimePointWhenToStartConsideringCandidates = getTimePointWhenToStartConsideringCandidates(newNonInferredStartTime);
if (!Util.equalsWithNull(newTimePointWhenToStartConsideringCandidates, timeRangeForValidCandidates.from())) {
final TimePoint oldTimePointWhenToStartConsideringCandidates = timeRangeForValidCandidates.from();
timeRangeForValidCandidates = new TimeRangeImpl(newTimePointWhenToStartConsideringCandidates, timeRangeForValidCandidates.to());
result = updateCandiatesAfterRaceTimeRangeChanged(newTimePointWhenToStartConsideringCandidates, timeRangeForValidCandidates.from());
result = updateCandiatesAfterRaceTimeRangeChanged(newTimePointWhenToStartConsideringCandidates, oldTimePointWhenToStartConsideringCandidates);
} else {
result = Collections.emptyMap();
}
@@ -1427,12 +1440,10 @@ public class CandidateFinderImpl implements CandidateFinder {
TimePoint oldFinishedTime, TimePoint newFinishedTime) {
final Map<Competitor, Pair<Iterable<Candidate>, Iterable<Candidate>>> result;
final TimePoint newTimePointWhenToFinishConsideringCandidates = getTimePointWhenToFinishConsideringCandidates(newFinishedTime);
final TimePoint oldTimePointWhenToFinishConsideringCandidates = timeRangeForValidCandidates.to();
if (!Util.equalsWithNull(timeRangeForValidCandidates.to(), newTimePointWhenToFinishConsideringCandidates)) {
final TimePoint oldTimePointWhenToFinishConsideringCandidates = timeRangeForValidCandidates.to();
timeRangeForValidCandidates = new TimeRangeImpl(timeRangeForValidCandidates.from(), newTimePointWhenToFinishConsideringCandidates);
result = updateCandiatesAfterRaceTimeRangeChanged(
oldTimePointWhenToFinishConsideringCandidates,
newTimePointWhenToFinishConsideringCandidates);
result = updateCandiatesAfterRaceTimeRangeChanged(oldTimePointWhenToFinishConsideringCandidates, newTimePointWhenToFinishConsideringCandidates);
} else {
result = Collections.emptyMap();
}
@@ -37,7 +37,7 @@ public interface DoubleVectorFixImporter {
* @throws IOException
* if there is a problem while reading the file
*/
void importFixes(InputStream inputStream, Callback callback, String sourceName)
void importFixes(InputStream inputStream, Callback callback, String filename, String sourceName)
throws FormatNotSupportedException, IOException;
/**
@@ -23,7 +23,7 @@ public abstract class TrackedRaceWithWindEssentials implements TrackedRace {
private static final Logger logger = Logger.getLogger(TrackedRaceWithWindEssentials.class.getName());
/**
* Serializing an instance of this class has to serialized the various data structures holding the tracked race's
* Serializing an instance of this class has to serialize the various data structures holding the tracked race's
* state. When a race is currently on, these structures change very frequently, and
* {@link ConcurrentModificationException}s during serialization will be the norm rather than the exception. To
* avoid this, all modifications to any data structure that is not in itself synchronized obtains this lock's
@@ -146,7 +146,4 @@ public abstract class TrackedRaceWithWindEssentials implements TrackedRace {
protected NamedReentrantReadWriteLock getSerializationLock() {
return serializationLock;
}
}
@@ -1075,8 +1075,10 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends MappedTra
unlockAfterRead();
}
}
for (GPSTrackListener<ItemType, FixType> listener : getListeners()) {
listener.gpsFixReceived(fix, getTrackedItem(), firstFixInTrack);
if (result) {
for (GPSTrackListener<ItemType, FixType> listener : getListeners()) {
listener.gpsFixReceived(fix, getTrackedItem(), firstFixInTrack);
}
}
return result;
}
@@ -28,6 +28,8 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.SensorFix;
import com.sap.sailing.domain.tracking.DynamicSensorFixTrack;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
@@ -740,6 +742,16 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl {
scheduleCacheRefresh(startOfInvalidation, endOfInvalidation);
}
}
@Override
public void competitorSensorFixAdded(Competitor competitor, String trackName, SensorFix fix) {
// no action required
}
@Override
public void competitorSensorTrackAdded(DynamicSensorFixTrack<Competitor, ?> track) {
// no action required
}
}
/**
@@ -158,6 +158,7 @@ import com.sap.sse.util.SmartFutureCache;
import com.sap.sse.util.SmartFutureCache.AbstractCacheUpdater;
import com.sap.sse.util.SmartFutureCache.EmptyUpdateInterval;
import com.sap.sse.util.impl.ArrayListNavigableSet;
import com.sap.sse.util.impl.FutureTaskWithTracingGet;
import difflib.DiffUtils;
import difflib.Patch;
@@ -750,7 +751,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
/**
* Monitor object to synchronize access to the {@link #updateStartAndEndOfTracking(boolean)} method. See bug 3922.
*/
private final Serializable updateStartAndEndOfTrackingMonitor = "";
private final Serializable updateStartAndEndOfTrackingMonitor = "updateStartAndEndOfTrackingMonitor";
/**
* Updates the start and end of tracking in the following precedence order:
@@ -762,10 +763,12 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
* </ol>
*/
public void updateStartAndEndOfTracking(boolean waitForGPSFixesToLoad) {
final TimePoint oldStartOfTracking;
final TimePoint oldEndOfTracking;
synchronized (updateStartAndEndOfTrackingMonitor) {
final Pair<TimePointSpecificationFoundInLog, TimePointSpecificationFoundInLog> trackingTimesFromRaceLog = this.getTrackingTimesFromRaceLogs();
TimePoint oldStartOfTracking = getStartOfTracking();
TimePoint oldEndOfTracking = getEndOfTracking();
oldStartOfTracking = getStartOfTracking();
oldEndOfTracking = getEndOfTracking();
boolean startOfTrackingFound = false;
boolean endOfTrackingFound = false;
// check race log
@@ -801,9 +804,9 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
endOfTrackingFound = true;
}
}
startOfTrackingChanged(oldStartOfTracking, waitForGPSFixesToLoad);
endOfTrackingChanged(oldEndOfTracking, waitForGPSFixesToLoad);
}
startOfTrackingChanged(oldStartOfTracking, waitForGPSFixesToLoad);
endOfTrackingChanged(oldEndOfTracking, waitForGPSFixesToLoad);
}
@Override
@@ -1873,7 +1876,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
synchronized (directionFromStartToNextMarkCache) {
future = directionFromStartToNextMarkCache.get(at);
if (future == null) {
newFuture = new FutureTask<Wind>(new Callable<Wind>() {
newFuture = new FutureTaskWithTracingGet<Wind>("getDirectionFromStartToNextMark for "+this, new Callable<Wind>() {
@Override
public Wind call() {
Wind result;
@@ -96,4 +96,9 @@ public class ExpeditionWindTrackerFactory implements WindTrackerFactory {
windReceivers.remove(windTracker.getReceiver().getPort());
}
}
@Override
public String toString() {
return "ExpeditionWindTrackerFactory [defaultPort=" + defaultPort + "]";
}
}
@@ -28,6 +28,7 @@ import com.sap.sailing.gwt.autoplay.client.place.player.AutoPlayerConfiguration;
import com.sap.sailing.gwt.autoplay.client.shared.leaderboard.LeaderboardWithHeaderPerspectiveLifecycle;
import com.sap.sailing.gwt.autoplay.client.shared.leaderboard.LeaderboardWithHeaderPerspectiveSettings;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.common.client.SharedResources;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.raceboard.RaceBoardPerspectiveLifecycle;
@@ -44,7 +45,6 @@ import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeLifecycleTa
import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveLifecycle;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveLifecycleWithAllSettings;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
import com.sap.sse.security.ui.client.UserService;
public class DesktopStartView extends Composite implements StartView {
@@ -53,7 +53,7 @@ public class DesktopStartView extends Composite implements StartView {
interface StartPageViewUiBinder extends UiBinder<Widget, DesktopStartView> {
}
@UiField(provided=true) SAPHeaderWithAuthentication sapHeader;
@UiField(provided=true) SAPSailingHeaderWithAuthentication sapHeader;
@UiField(provided=true) ListBox localeSelectionBox;
@UiField(provided=true) ListBox eventSelectionBox;
@UiField(provided=true) ListBox leaderboardSelectionBox;
@@ -82,8 +82,7 @@ public class DesktopStartView extends Composite implements StartView {
this.eventBus = eventBus;
this.events = new ArrayList<EventDTO>();
sapHeader = new SAPHeaderWithAuthentication(StringMessages.INSTANCE.sapSailingAnalytics(),
StringMessages.INSTANCE.autoplayConfiguration());
sapHeader = new SAPSailingHeaderWithAuthentication(StringMessages.INSTANCE.autoplayConfiguration());
new FixedSailingAuthentication(userService, sapHeader.getAuthenticationMenuView());
eventSelectionBox = new ListBox();
@@ -4,23 +4,23 @@ import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.common.client.FullscreenUtil;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sse.gwt.client.shared.components.AbstractCompositeComponent;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
import com.sap.sse.security.ui.client.UserService;
public class SAPHeaderComponent extends AbstractCompositeComponent<SAPHeaderComponentSettings> {
private SAPHeaderComponentSettings settings;
private final SAPHeaderComponentLifecycle componentLifecycle;
private final SAPHeaderWithAuthentication sapHeader;
private final SAPSailingHeaderWithAuthentication sapHeader;
public SAPHeaderComponent(SAPHeaderComponentLifecycle componentLifecycle, UserService userService, SAPHeaderComponentSettings settings,
StringMessages stringMessages, boolean startInAutoScreenMode) {
this.componentLifecycle = componentLifecycle;
this.settings = settings;
this.sapHeader = new SAPHeaderWithAuthentication(stringMessages.sapSailingAnalytics(), settings.getTitle());
this.sapHeader = new SAPSailingHeaderWithAuthentication(settings.getTitle());
new FixedSailingAuthentication(userService, sapHeader.getAuthenticationMenuView());
initWidget(sapHeader);
@@ -0,0 +1,20 @@
package com.sap.sailing.gwt.common.authentication;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
public class SAPSailingHeaderWithAuthentication extends SAPHeaderWithAuthentication {
private static final String SAP_SAILING_URL = "http://www.sapsailing.com";
private static final String SAP_SAILING_APP_NAME = StringMessages.INSTANCE.sapSailingAnalytics();
public SAPSailingHeaderWithAuthentication() {
super(SAP_SAILING_APP_NAME, SAP_SAILING_URL);
}
public SAPSailingHeaderWithAuthentication(String headerText) {
super(SAP_SAILING_APP_NAME, SAP_SAILING_URL, headerText);
}
}
@@ -14,6 +14,7 @@ import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.common.client.SharedResources;
import com.sap.sailing.gwt.regattaoverview.client.RegattaRaceStatesComponent.EntryHandler;
import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaOverviewBaseSettings;
@@ -21,7 +22,6 @@ import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSett
import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint;
import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO;
import com.sap.sse.gwt.settings.SettingsToUrlSerializer;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint {
@@ -33,7 +33,7 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint {
private final Label clockLabel = new Label();
private final RegattaOverviewResources.LocalCss style = RegattaOverviewResources.INSTANCE.css();
private SAPHeaderWithAuthentication siteHeader;
private SAPSailingHeaderWithAuthentication siteHeader;
@Override
public void doOnModuleLoad() {
@@ -50,8 +50,7 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint {
clockLabel.addStyleName(style.clockLabel());
siteHeader = new SAPHeaderWithAuthentication(getStringMessages()
.sapSailingAnalytics());
siteHeader = new SAPSailingHeaderWithAuthentication();
new FixedSailingAuthentication(getUserService(), siteHeader.getAuthenticationMenuView());
siteHeader.addWidgetToRightSide(clockLabel);
@@ -41,6 +41,7 @@ public abstract class AbstractFileImportWidget extends Composite {
protected final SailingServiceAsync sailingService;
private final StringMessages stringMessages;
private final ErrorReporter errorReporter;
private boolean shouldClearListOnNewUploadComplete = true;
protected AbstractFileImportWidget(String formActionUrl, StringMessages stringMessages,
TrackFileImportDeviceIdentifierTableWrapper table, SailingServiceAsync sailingService,
@@ -73,7 +74,9 @@ public abstract class AbstractFileImportWidget extends Composite {
@UiHandler("formPanelUi")
void onFileImportComplete(SubmitCompleteEvent event) {
table.getDataProvider().getList().clear();
if (shouldClearListOnNewUploadComplete) {
table.getDataProvider().getList().clear();
}
String[] uuids = event.getResults().split("\\n");
if (uuids.length != 1 || (uuids.length > 0 && !uuids[0].isEmpty())) {
sailingService.getTrackFileImportDeviceIds(Arrays.asList(uuids),
@@ -106,4 +109,7 @@ public abstract class AbstractFileImportWidget extends Composite {
protected abstract void getImporterTypes(AsyncCallback<Collection<String>> callback);
protected void setShouldClearListOnNewUploadComplete(boolean shouldClearListOnNewUploadComplete) {
this.shouldClearListOnNewUploadComplete = shouldClearListOnNewUploadComplete;
}
}
@@ -16,6 +16,7 @@ import com.sap.sailing.domain.common.security.Permission;
import com.sap.sailing.domain.common.security.Roles;
import com.sap.sailing.domain.common.security.SailingPermissionsForRoleProvider;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint;
import com.sap.sailing.gwt.ui.client.LeaderboardGroupsDisplayer;
import com.sap.sailing.gwt.ui.client.LeaderboardGroupsRefresher;
@@ -45,7 +46,6 @@ import com.sap.sse.security.ui.authentication.decorator.AuthorizedContentDecorat
import com.sap.sse.security.ui.authentication.decorator.WidgetFactory;
import com.sap.sse.security.ui.authentication.generic.GenericAuthentication;
import com.sap.sse.security.ui.authentication.generic.GenericAuthorizedContentDecorator;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
import com.sap.sse.security.ui.client.component.UserManagementPanel;
import com.sap.sse.security.ui.client.i18n.StringMessages;
@@ -66,8 +66,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint implements
private void createUI() {
HeaderPanel headerPanel = new HeaderPanel();
SAPHeaderWithAuthentication header = new SAPHeaderWithAuthentication(getStringMessages().sapSailingAnalytics(),
getStringMessages().administration());
SAPSailingHeaderWithAuthentication header = new SAPSailingHeaderWithAuthentication(getStringMessages().administration());
GenericAuthentication genericSailingAuthentication = new FixedSailingAuthentication(getUserService(), header.getAuthenticationMenuView());
AuthorizedContentDecorator authorizedContentDecorator = new GenericAuthorizedContentDecorator(genericSailingAuthentication);
authorizedContentDecorator.setContentWidgetFactory(new WidgetFactory() {
@@ -2,9 +2,7 @@ package com.sap.sailing.gwt.ui.adminconsole;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.CaptionPanel;
@@ -12,7 +10,6 @@ import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;
import com.sap.sailing.domain.common.dto.CompetitorDTO;
import com.sap.sailing.domain.common.racelog.tracking.MappableToDevice;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
@@ -31,12 +28,6 @@ public class RegattaLogImportSensorDataAndAddMappingsDialog extends DataEntryDia
protected final CompetitorTableWrapper<RefreshableSingleSelectionModel<CompetitorDTO>> competitorTable;
private final StringMessages stringMessages;
private final Map<TrackFileImportDeviceIdentifierDTO, CompetitorDTO> mappings = new HashMap<>();
private TrackFileImportDeviceIdentifierDTO deviceToSelect;
private CompetitorDTO compToSelect;
private boolean inInstableTransitionState = false;
public RegattaLogImportSensorDataAndAddMappingsDialog(SailingServiceAsync sailingService,
final ErrorReporter errorReporter, final StringMessages stringMessages, String leaderboardName,
DialogCallback<Collection<TypedDeviceMappingDTO>> callback) {
@@ -50,11 +41,16 @@ public class RegattaLogImportSensorDataAndAddMappingsDialog extends DataEntryDia
}, true, callback);
this.stringMessages = stringMessages;
deviceIdTable = new TrackFileImportDeviceIdentifierTableWrapper(sailingService, stringMessages, errorReporter);
deviceIdTable.removeTrackNameColumn();
importWidget = new SensorDataImportWidget(deviceIdTable, stringMessages, sailingService, errorReporter);
deviceIdTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
@Override
public void onSelectionChange(SelectionChangeEvent event) {
deviceSelectionChanged(deviceIdTable.getSelectionModel().getSelectedObject());
CompetitorDTO mappedComp = deviceIdTable.getMappedCompetitorForCurrentSelection();
if (mappedComp != null) {
competitorTable.getSelectionModel().setSelected(mappedComp, true);
}
}
});
competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */
@@ -63,7 +59,8 @@ public class RegattaLogImportSensorDataAndAddMappingsDialog extends DataEntryDia
competitorTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
@Override
public void onSelectionChange(SelectionChangeEvent event) {
mappedToSelectionChanged(competitorTable.getSelectionModel().getSelectedObject());
deviceIdTable.didSelectCompetitorForMapping(competitorTable.getSelectionModel().getSelectedObject());
validate();
}
});
@@ -87,50 +84,6 @@ public class RegattaLogImportSensorDataAndAddMappingsDialog extends DataEntryDia
});
}
private static <T> void selectOrClear(SingleSelectionModel<T> selectionModel, T object) {
if (object == null) {
selectionModel.clear();
} else {
selectionModel.setSelected(object, true);
}
}
/**
* Avoid programmatic deselections that re-trigger the selection listeners and lead to a loop.
*/
private void select() {
if (inInstableTransitionState) {
if (deviceIdTable.getSelectionModel().getSelectedObject() == deviceToSelect
&& competitorTable.getSelectionModel().getSelectedObject() == compToSelect) {
inInstableTransitionState = false;
}
} else {
inInstableTransitionState = true;
selectOrClear(deviceIdTable.getSelectionModel(), deviceToSelect);
selectOrClear(competitorTable.getSelectionModel(), compToSelect);
}
}
private void mappedToSelectionChanged(CompetitorDTO mappedTo) {
if (!inInstableTransitionState) {
TrackFileImportDeviceIdentifierDTO device = deviceIdTable.getSelectionModel().getSelectedObject();
if (device != null) {
mappings.put(device, mappedTo);
}
compToSelect = mappedTo;
}
select();
validate();
}
private void deviceSelectionChanged(TrackFileImportDeviceIdentifierDTO deviceId) {
if (!inInstableTransitionState) {
deviceToSelect = deviceId;
compToSelect = deviceId != null ? mappings.get(deviceId) : null;
}
select();
}
@Override
protected Widget getAdditionalWidget() {
HorizontalPanel panel = new HorizontalPanel();
@@ -150,9 +103,9 @@ public class RegattaLogImportSensorDataAndAddMappingsDialog extends DataEntryDia
protected Collection<TypedDeviceMappingDTO> getResult() {
List<TypedDeviceMappingDTO> result = new ArrayList<>();
String dataType = importWidget.getSelectedImporterType();
for (TrackFileImportDeviceIdentifierDTO device : mappings.keySet()) {
for (TrackFileImportDeviceIdentifierDTO device : deviceIdTable.getMappings().keySet()) {
DeviceIdentifierDTO deviceIdDto = new DeviceIdentifierDTO("FILE", device.uuidAsString);
MappableToDevice mappedTo = mappings.get(device);
MappableToDevice mappedTo = deviceIdTable.getMappings().get(device);
result.add(new TypedDeviceMappingDTO(deviceIdDto, device.from, device.to, mappedTo, null, dataType));
}
return result;
@@ -14,6 +14,7 @@ public class SensorDataImportWidget extends AbstractFileImportWidget implements
final SailingServiceAsync sailingService, final ErrorReporter errorReporter) {
super("/sailingserver/sensordata/import", stringMessages, table, sailingService, errorReporter);
setMultipleFileUploadEnabled(true);
setShouldClearListOnNewUploadComplete(false);
}
@Override
@@ -0,0 +1,60 @@
package com.sap.sailing.gwt.ui.adminconsole;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.sap.sailing.gwt.ui.client.StringMessages;
/**
* Can be used in {@link AbstractEventManagementPanel}s to enable some sort of time-delayed race simulation.
* Provides a checkbox (see {@link #isSimulate()}) that enables simulation and an offset in minutes (see
* {@link #getOffsetToStartTimeInMinutes()}).
*
* @author Axel Uhl (d043530)
*
*/
public class SimulationPanel extends VerticalPanel {
private static final String ZERO_AS_STRING = "0";
final CheckBox simulateWithStartTimeNowCheckBox;
final TextBox offsetToStartTimeOfSimulatedRaceTextBox;
public SimulationPanel(StringMessages stringMessages) {
offsetToStartTimeOfSimulatedRaceTextBox = new TextBox();
offsetToStartTimeOfSimulatedRaceTextBox.setWidth("40px");
offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(false);
offsetToStartTimeOfSimulatedRaceTextBox.setValue(ZERO_AS_STRING);
simulateWithStartTimeNowCheckBox = new CheckBox(stringMessages.simulateAsLiveRace());
simulateWithStartTimeNowCheckBox.ensureDebugId("SimulateWithStartTimeNowCheckBox");
simulateWithStartTimeNowCheckBox.setWordWrap(false);
simulateWithStartTimeNowCheckBox.setValue(Boolean.FALSE);
simulateWithStartTimeNowCheckBox.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(simulateWithStartTimeNowCheckBox.getValue());
offsetToStartTimeOfSimulatedRaceTextBox.setFocus(simulateWithStartTimeNowCheckBox.getValue());
}
});
final FlowPanel simulateAsLiveRacePanel = new FlowPanel();
simulateAsLiveRacePanel.add(simulateWithStartTimeNowCheckBox);
final Label offsetToStartLabel = new Label(stringMessages.simulateWithOffset());
final HorizontalPanel simulateWithOffsetPanel = new HorizontalPanel();
simulateWithOffsetPanel.add(offsetToStartLabel);
simulateWithOffsetPanel.add(offsetToStartTimeOfSimulatedRaceTextBox);
add(simulateAsLiveRacePanel);
add(simulateWithOffsetPanel);
}
public boolean isSimulate() {
return simulateWithStartTimeNowCheckBox.getValue();
}
public String getOffsetToStartTimeInMinutes() {
return offsetToStartTimeOfSimulatedRaceTextBox.getValue();
}
}
@@ -23,7 +23,6 @@ import com.google.gwt.user.client.ui.CaptionPanel;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.ColumnFormatter;
import com.google.gwt.user.client.ui.HorizontalPanel;
@@ -85,7 +84,6 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
private LabeledAbstractFilterablePanel<TracTracRaceRecordDTO> racesFilterablePanel;
private FlushableCellTable<TracTracRaceRecordDTO> racesTable;
private static final String ZERO_AS_STRING = "0";
public TracTracEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter,
RegattaRefresher regattaRefresher, StringMessages stringMessages) {
@@ -275,32 +273,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
correctWindCheckBox.setWordWrap(false);
correctWindCheckBox.setValue(Boolean.TRUE);
final TextBox offsetToStartTimeOfSimulatedRaceTextBox = new TextBox();
offsetToStartTimeOfSimulatedRaceTextBox.setWidth("40px");
offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(false);
offsetToStartTimeOfSimulatedRaceTextBox.setValue(ZERO_AS_STRING);
final CheckBox simulateWithStartTimeNowCheckBox = new CheckBox(stringMessages.simulateAsLiveRace());
simulateWithStartTimeNowCheckBox.ensureDebugId("SimulateWithStartTimeNowCheckBox");
simulateWithStartTimeNowCheckBox.setWordWrap(false);
simulateWithStartTimeNowCheckBox.setValue(Boolean.FALSE);
simulateWithStartTimeNowCheckBox.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(simulateWithStartTimeNowCheckBox.getValue());
offsetToStartTimeOfSimulatedRaceTextBox.setFocus(simulateWithStartTimeNowCheckBox.getValue());
}
});
final FlowPanel simulateAsLiveRacePanel = new FlowPanel();
simulateAsLiveRacePanel.add(simulateWithStartTimeNowCheckBox);
final Label offsetToStartLabel = new Label(stringMessages.simulateWithOffset());
final HorizontalPanel simulateWithOffsetPanel = new HorizontalPanel();
simulateWithOffsetPanel.add(offsetToStartLabel);
simulateWithOffsetPanel.add(offsetToStartTimeOfSimulatedRaceTextBox);
final SimulationPanel simulationPanel = new SimulationPanel(stringMessages);
final CheckBox ignoreTracTracMarkPassingsCheckbox = new CheckBox(stringMessages.useInternalAlgorithm());
ignoreTracTracMarkPassingsCheckbox.setWordWrap(false);
ignoreTracTracMarkPassingsCheckbox.setValue(Boolean.FALSE);
@@ -308,8 +281,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
layoutTable.setWidget(++row, 0, trackSettingsLabel);
layoutTable.setWidget(row, 1, trackWindCheckBox);
layoutTable.setWidget(++row, 1, correctWindCheckBox);
layoutTable.setWidget(++row, 1, simulateAsLiveRacePanel);
layoutTable.setWidget(++row, 1, simulateWithOffsetPanel);
layoutTable.setWidget(++row, 1, simulationPanel);
layoutTable.setWidget(++row, 1, ignoreTracTracMarkPassingsCheckbox);
// Filter
@@ -413,8 +385,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
@Override
public void onClick(ClickEvent event) {
Duration offsetToStartTimeOfSimulatedRace = null;
if(simulateWithStartTimeNowCheckBox.getValue().booleanValue()) {
offsetToStartTimeOfSimulatedRace = getMillisecondsDurationFromMinutesAsString(offsetToStartTimeOfSimulatedRaceTextBox.getValue());
if (simulationPanel.isSimulate()) {
offsetToStartTimeOfSimulatedRace = getMillisecondsDurationFromMinutesAsString(simulationPanel.getOffsetToStartTimeInMinutes());
}
trackSelectedRaces(trackWindCheckBox.getValue(), correctWindCheckBox.getValue(),
offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassingsCheckbox.getValue());
@@ -1,6 +1,18 @@
package com.sap.sailing.gwt.ui.adminconsole;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.dom.builder.shared.TableCellBuilder;
import com.google.gwt.dom.builder.shared.TableRowBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.AbstractCellTable;
import com.google.gwt.user.cellview.client.AbstractCellTable.Style;
import com.google.gwt.user.cellview.client.DefaultCellTableBuilder;
import com.google.gwt.user.cellview.client.RowStyles;
import com.google.gwt.user.cellview.client.TextColumn;
import com.sap.sailing.domain.common.dto.CompetitorDTO;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil;
@@ -9,11 +21,13 @@ import com.sap.sse.gwt.client.ErrorReporter;
import com.sap.sse.gwt.client.celltable.EntityIdentityComparator;
import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel;
public class TrackFileImportDeviceIdentifierTableWrapper extends TableWrapper<TrackFileImportDeviceIdentifierDTO,
RefreshableSingleSelectionModel<TrackFileImportDeviceIdentifierDTO>> {
public class TrackFileImportDeviceIdentifierTableWrapper extends
TableWrapper<TrackFileImportDeviceIdentifierDTO, RefreshableSingleSelectionModel<TrackFileImportDeviceIdentifierDTO>> {
private TextColumn<TrackFileImportDeviceIdentifierDTO> trackNameColumn;
private final Map<TrackFileImportDeviceIdentifierDTO, CompetitorDTO> mappings = new HashMap<>();
public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages,
ErrorReporter errorReporter) {
public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService,
StringMessages stringMessages, ErrorReporter errorReporter) {
super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true,
new EntityIdentityComparator<TrackFileImportDeviceIdentifierDTO>() {
@Override
@@ -21,54 +35,49 @@ RefreshableSingleSelectionModel<TrackFileImportDeviceIdentifierDTO>> {
TrackFileImportDeviceIdentifierDTO dto2) {
return dto1.uuidAsString.equals(dto2.uuidAsString);
}
@Override
public int hashCode(TrackFileImportDeviceIdentifierDTO t) {
return t.uuidAsString.hashCode();
}
});
TextColumn<TrackFileImportDeviceIdentifierDTO> uuidColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return object.uuidAsString;
}
};
TextColumn<TrackFileImportDeviceIdentifierDTO> fileNameColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return object.fileName;
}
};
TextColumn<TrackFileImportDeviceIdentifierDTO> trackNameColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
trackNameColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return object.trackName;
}
};
TextColumn<TrackFileImportDeviceIdentifierDTO> fromColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return DateAndTimeFormatterUtil.formatDateAndTime(object.from);
}
};
TextColumn<TrackFileImportDeviceIdentifierDTO> toColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return DateAndTimeFormatterUtil.formatDateAndTime(object.to);
}
};
TextColumn<TrackFileImportDeviceIdentifierDTO> numberOfFixesColumn = new TextColumn<TrackFileImportDeviceIdentifierDTO>() {
@Override
public String getValue(TrackFileImportDeviceIdentifierDTO object) {
return "" + object.numFixes;
}
};
table.setTableBuilder(new MappingTableBuilder(table));
table.addColumn(uuidColumn, "UUID");
table.addColumn(fileNameColumn, "Filename");
table.addColumn(trackNameColumn, "Trackname");
@@ -77,4 +86,87 @@ RefreshableSingleSelectionModel<TrackFileImportDeviceIdentifierDTO>> {
table.addColumn(numberOfFixesColumn, "# fixes");
}
public void removeTrackNameColumn() {
table.removeColumn(trackNameColumn);
}
class MappingTableBuilder extends DefaultCellTableBuilder<TrackFileImportDeviceIdentifierDTO> {
private final String evenRowStyle;
private final String oddRowStyle;
private final String cellStyle;
private final String evenCellStyle;
private final String oddCellStyle;
private final String firstColumnStyle;
private final AbstractCellTable<TrackFileImportDeviceIdentifierDTO> cellTable;
public MappingTableBuilder(AbstractCellTable<TrackFileImportDeviceIdentifierDTO> cellTable) {
super(cellTable);
this.cellTable = cellTable;
Style style = cellTable.getResources().style();
evenRowStyle = style.evenRow();
oddRowStyle = style.oddRow();
cellStyle = style.cell();
evenCellStyle = " " + style.evenRowCell();
oddCellStyle = " " + style.oddRowCell();
firstColumnStyle = " " + style.firstColumn();
}
@Override
public void buildRowImpl(TrackFileImportDeviceIdentifierDTO rowValue, int absRowIndex) {
super.buildRowImpl(rowValue, absRowIndex);
boolean isEven = absRowIndex % 2 == 0;
StringBuilder trClasses = new StringBuilder(isEven ? evenRowStyle : oddRowStyle);
RowStyles<TrackFileImportDeviceIdentifierDTO> rowStyles = cellTable.getRowStyles();
if (rowStyles != null) {
String extraRowStyles = rowStyles.getStyleNames(rowValue, absRowIndex);
if (extraRowStyles != null) {
trClasses.append(" ").append(extraRowStyles);
}
}
StringBuilder tdClasses = new StringBuilder(cellStyle);
tdClasses.append(isEven ? evenCellStyle : oddCellStyle);
TableRowBuilder tr = startRow();
tr.className(trClasses.toString());
tr.startTD().className(new StringBuilder(tdClasses).append(firstColumnStyle).toString())
.html(SafeHtmlUtils.fromSafeConstant(" ")).end();
TableCellBuilder td = tr.startTD();
td.className(tdClasses.toString());
td.colSpan(getColumns().size() - 1);
CompetitorDTO competitorDTO = mappings.get(rowValue);
if (competitorDTO == null) {
td.html(new SafeHtmlBuilder().appendEscaped("--").toSafeHtml());
} else {
td.html(new SafeHtmlBuilder().appendEscaped(competitorDTO.getName()).toSafeHtml());
}
td.end();
tr.end();
}
}
public CompetitorDTO getMappedCompetitorForCurrentSelection() {
TrackFileImportDeviceIdentifierDTO selectedObject = getSelectionModel().getSelectedObject();
if (selectedObject != null) {
return mappings.get(selectedObject);
}
return null;
}
public void didSelectCompetitorForMapping(CompetitorDTO competitor) {
TrackFileImportDeviceIdentifierDTO selectedObject = getSelectionModel().getSelectedObject();
if (selectedObject != null) {
if (competitor != null) {
mappings.put(selectedObject, competitor);
} else {
mappings.remove(selectedObject);
}
}
table.redraw();
}
public Map<TrackFileImportDeviceIdentifierDTO, CompetitorDTO> getMappings() {
return mappings;
}
}
@@ -12,6 +12,7 @@ import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.domain.common.security.Permission;
import com.sap.sailing.domain.common.security.SailingPermissionsForRoleProvider;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint;
import com.sap.sse.gwt.client.async.AsyncActionsExecutor;
import com.sap.sse.gwt.client.async.MarkedAsyncCallback;
@@ -61,8 +62,7 @@ public class LeaderboardEditPage extends AbstractSailingEntryPoint {
}
private SAPHeaderWithAuthentication initHeader() {
SAPHeaderWithAuthentication header = new SAPHeaderWithAuthentication(getStringMessages().sapSailingAnalytics(),
getStringMessages().editScores());
SAPSailingHeaderWithAuthentication header = new SAPSailingHeaderWithAuthentication(getStringMessages().editScores());
header.getElement().getStyle().setPosition(Position.FIXED);
header.getElement().getStyle().setTop(0, Unit.PX);
header.getElement().getStyle().setWidth(100, Unit.PCT);
@@ -1139,7 +1139,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
* can be obtained within this timeout, wind for the race will be tracked; otherwise, the method returns without
* taking any effect.
*/
private void startTrackingWind(RaceHandle raceHandle, boolean correctByDeclination, long timeoutInMilliseconds) throws Exception {
private void startTrackingWind(RaceHandle raceHandle, boolean correctByDeclination, long timeoutInMilliseconds) {
Regatta regatta = raceHandle.getRegatta();
if (regatta != null) {
RaceDefinition race = raceHandle.getRace(timeoutInMilliseconds);
@@ -2173,7 +2173,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
} else {
actualTimePoint = timePoint;
}
RaceDefinition race = trackedRace.getRace();
final RaceDefinition race = trackedRace.getRace();
int rank = 1;
final List<Competitor> competitorsFromBestToWorst = trackedRace.getCompetitorsFromBestToWorst(actualTimePoint);
for (Competitor competitor : competitorsFromBestToWorst) {
@@ -10,10 +10,7 @@ import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
@@ -86,7 +83,7 @@ import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.util.impl.ThreadFactoryWithPriority;
import com.sap.sse.util.ThreadPoolUtil;
public class SimulatorServiceImpl extends RemoteServiceServlet implements SimulatorService {
@@ -107,12 +104,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
private SpeedWithBearing averageWind = null;
public SimulatorServiceImpl() {
final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()/2, 3);
Executor simulatorExecutor = new ThreadPoolExecutor(/* corePoolSize */THREAD_POOL_SIZE,
/* maximumPoolSize */THREAD_POOL_SIZE,
/* keepAliveTime */60, TimeUnit.SECONDS,
/* workQueue */new LinkedBlockingQueue<Runnable>(),
new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true));
final ScheduledExecutorService simulatorExecutor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor();
// TODO: initialize smart-future-cache for simulation-results and add to simulation-service
simulationService = SimulationServiceFactory.INSTANCE.getService(simulatorExecutor, null);
}
@@ -10,6 +10,7 @@ import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication;
import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication;
import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint;
import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants;
import com.sap.sailing.gwt.ui.client.SimulatorService;
@@ -21,7 +22,6 @@ import com.sap.sse.security.ui.authentication.decorator.AuthorizedContentDecorat
import com.sap.sse.security.ui.authentication.decorator.WidgetFactory;
import com.sap.sse.security.ui.authentication.generic.GenericAuthentication;
import com.sap.sse.security.ui.authentication.generic.GenericAuthorizedContentDecorator;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
public class SimulatorEntryPoint extends AbstractSailingEntryPoint {
@@ -187,8 +187,7 @@ public class SimulatorEntryPoint extends AbstractSailingEntryPoint {
}
private void createSimulatorPanel() {
SAPHeaderWithAuthentication header = new SAPHeaderWithAuthentication(getStringMessages()
.sapSailingAnalytics(), getStringMessages().strategySimulatorTitle());
SAPSailingHeaderWithAuthentication header = new SAPSailingHeaderWithAuthentication(getStringMessages().strategySimulatorTitle());
GenericAuthentication genericSailingAuthentication = new FixedSailingAuthentication(getUserService(), header.getAuthenticationMenuView());
AuthorizedContentDecorator authorizedContentDecorator = new GenericAuthorizedContentDecorator(genericSailingAuthentication);
@@ -218,7 +218,7 @@
<RaceResult RaceID="e18498ce-3885-4a4a-8ad5-be92893cdb55" TeamID="faddda24-6fab-4db4-8657-cdb471d12cc1" RacePoints="29" Discard="true" />
<SeriesResult TeamID="faddda24-6fab-4db4-8657-cdb471d12cc1" TotalPoints="151" NetPoints="93" Rank="50" />
<RaceResult RaceID="b68b5758-e172-417a-ba4f-bfd7b441940e" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="20" />
<RaceResult RaceID="3c4563fe-b12b-4dec-8b0f-222ee079c7d5" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="29" Discard="true" />
<RaceResult RaceID="3c4563fe-b12b-4dec-8b0f-222ee079c7d5" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="29" Discard="true" ScoreCode="UFD" />
<RaceResult RaceID="857d4587-9e52-46e1-ab3a-8892a6aac167" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="21" />
<RaceResult RaceID="87dfaa87-1d6c-4e92-820e-dd8e96ae3690" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="12" />
<RaceResult RaceID="62ed6be3-281a-44a3-885e-f0edbbf85409" TeamID="01723ea5-2910-4e7c-a8dc-c69ca8535cbb" RacePoints="35" Discard="true" ScoreCode="BFD" />
@@ -2,6 +2,7 @@ package com.sap.sailing.manage2sail.resultimport.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import java.io.File;
@@ -10,7 +11,9 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -22,7 +25,9 @@ import javax.xml.parsers.ParserConfigurationException;
import org.junit.Test;
import org.xml.sax.SAXException;
import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.RegattaScoreCorrections;
import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionForCompetitorInRace;
import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionsForRace;
import com.sap.sailing.domain.persistence.DomainObjectFactory;
import com.sap.sailing.domain.persistence.MongoObjectFactory;
@@ -128,11 +133,52 @@ public class ParserTest {
}
assertNotNull(resultFor29er);
int numberOfRaceColumnsWithScores = 8; // R9 is in 'planned' state and has no results
RegattaScoreCorrections _29erResult = scoreCorrectionProvider.getScoreCorrections(YES_EVENT_NAME, ISAF_ID_29ER , resultFor29er.getB());
assertNotNull(_29erResult);
Iterable<ScoreCorrectionsForRace> scoreCorrectionsForRaces = _29erResult.getScoreCorrectionsForRaces();
assertNotNull(scoreCorrectionsForRaces);
assertEquals(8, Util.size(scoreCorrectionsForRaces));
}
assertEquals(numberOfRaceColumnsWithScores, Util.size(scoreCorrectionsForRaces));
Set<String> raceNamesOrNumbers = new HashSet<>();
for (ScoreCorrectionsForRace scoreCorrectionForRace : scoreCorrectionsForRaces) {
raceNamesOrNumbers.add(scoreCorrectionForRace.getRaceNameOrNumber());
}
assertEquals(new HashSet<String>(Arrays.asList(new String[] { "1", "2", "3", "4", "5", "6", "7", "8" })), raceNamesOrNumbers);
{
final ScoreCorrectionsForRace resultsForR2 = Util.get(scoreCorrectionsForRaces, 1);
ScoreCorrectionForCompetitorInRace scoreCorrectionR2ForDEN63 = resultsForR2.getScoreCorrectionForCompetitor("DEN 63");
assertEquals(0, scoreCorrectionR2ForDEN63.getPoints(), 29.00000001);
assertSame(MaxPointsReason.UFD, scoreCorrectionR2ForDEN63.getMaxPointsReason());
}
{
final ScoreCorrectionsForRace resultsForR5 = Util.get(scoreCorrectionsForRaces, 4);
ScoreCorrectionForCompetitorInRace scoreCorrectionR5ForDEN63 = resultsForR5.getScoreCorrectionForCompetitor("DEN 63");
assertEquals(0, scoreCorrectionR5ForDEN63.getPoints(), 35.00000001);
assertSame(MaxPointsReason.BFD, scoreCorrectionR5ForDEN63.getMaxPointsReason());
}
}
/**
<Race RaceID="3c4563fe-b12b-4dec-8b0f-222ee079c7d5" RaceName="R2 Yellow" RaceNumber="2" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="b68b5758-e172-417a-ba4f-bfd7b441940e" RaceName="R1 Yellow" RaceNumber="1" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="5f78543c-e17c-4130-83ac-bfc15c2a5f50" RaceName="R1 Blue" RaceNumber="1" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="753d9dd7-7fdf-40cb-8243-5d70e3771fc1" RaceName="R3 Yellow" RaceNumber="3" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="857d4587-9e52-46e1-ab3a-8892a6aac167" RaceName="R3 Blue" RaceNumber="3" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="1f4d9046-1067-4d2e-bc78-083a072d63e1" RaceName="R4 Yellow" RaceNumber="4" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="87dfaa87-1d6c-4e92-820e-dd8e96ae3690" RaceName="R4 Blue" RaceNumber="4" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="345e28a8-efa4-4ec5-a7a3-410cb251c88c" RaceName="R5 Yellow" RaceNumber="5" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="62ed6be3-281a-44a3-885e-f0edbbf85409" RaceName="R5 Blue" RaceNumber="5" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="8422bca3-bbfd-4761-ba6c-21e25083c38b" RaceName="R6 Yellow" RaceNumber="6" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="52d92322-d4d6-4f52-a66e-92fdde72767e" RaceName="R6 Blue" RaceNumber="6" RaceStartDate="2013-05-19" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="ec270635-04b2-4a12-afed-2018be075f7e" RaceName="R9 Gold" RaceNumber="9" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="planned" />
<Race RaceID="7e6ddb42-8db8-4a3e-9358-d7cb40f28a77" RaceName="R9 Silver" RaceNumber="9" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="planned" />
<Race RaceID="e18498ce-3885-4a4a-8ad5-be92893cdb55" RaceName="R7 Silver" RaceNumber="7" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="086b0bfc-5712-44b1-91de-c1c7f74f2218" RaceName="R7 Gold" RaceNumber="7" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="7cdec8a2-5175-4736-9296-823f7e1325f7" RaceName="R8 Gold" RaceNumber="8" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="finished" />
<Race RaceID="3025aa09-0427-42ab-8f03-6b019b8d4ddd" RaceName="R8 Silver" RaceNumber="8" RaceStartDate="2013-05-20" RaceStartTime="12:00:00" RaceStatus="inprogress" />
<Race RaceID="37c2ae0f-4889-46ff-aad9-54d964dd193a" RaceName="R2 Blue" RaceNumber="2" RaceStartDate="2013-05-18" RaceStartTime="12:00:00" RaceStatus="finished" />
*/
}
@@ -17,8 +17,12 @@ public class FlatGPSFixJsonSerializer implements JsonSerializer<GPSFix> {
result.put(FIELD_LON_DEG, object.getPosition().getLngDeg());
result.put(FIELD_LAT_DEG, object.getPosition().getLatDeg());
result.put(FIELD_TIME_MILLIS, object.getTimePoint().asMillis());
// TODO the default accuracy is 1.0; when fixes ever have an accuracy, report it here
result.put(FIELD_ACCURACY, 1.0);
// TODO Insert Accuracy if someday available from the server
// Buoy Pinger app will interpret no accuracy in JSON message as MarkPingInfo.NOT_SET_BY_USER (-1) and store that in DB
// For the user these values are displayed as R.string.unknown ("n/a");
// (CheckinManager in Buoy Pinger can accept messages with and without accuracy)
// result.put(FIELD_ACCURACY, XXXXXXXXX);
return result;
}
}
@@ -4,11 +4,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import org.apache.commons.fileupload.FileItem;
import org.junit.Test;
import com.sap.sailing.domain.common.tracking.GPSFix;
@@ -30,32 +34,33 @@ public class GPSFixImportTest {
@Test
public void testReReadingEOFFromBufferedInputStream() throws IOException {
InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx");
BufferedInputStreamWithPublicBuffer bis = new BufferedInputStreamWithPublicBuffer(in);
bis.mark(10*1024*1024); // the file has ~2MB, so 10MB is much more than twice the size, so should never be required
int size = 0;
while (bis.read() != -1) {
size++;
try (InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx");
BufferedInputStreamWithPublicBuffer bis = new BufferedInputStreamWithPublicBuffer(in)) {
bis.mark(10 * 1024 * 1024); // the file has ~2MB, so 10MB is much more than twice the size, so should never
// be required
int size = 0;
while (bis.read() != -1) {
size++;
}
bis.reset();
int secondSize = 0;
while (bis.read() != -1) {
secondSize++;
}
assertEquals(size, secondSize);
assertTrue(bis.getBuffer().length < 8 * 1024 * 1024);
bis.reset();
int thirdSize = 0;
while (bis.read() != -1) {
thirdSize++;
}
assertEquals(size, thirdSize);
assertTrue(bis.getBuffer().length < 8 * 1024 * 1024);
// Now try to read EOF multiple times
for (int i = 1; i < 1000; i++) {
assertEquals(-1, bis.read());
}
}
bis.reset();
int secondSize = 0;
while (bis.read() != -1) {
secondSize++;
}
assertEquals(size, secondSize);
assertTrue(bis.getBuffer().length < 8*1024*1024);
bis.reset();
int thirdSize = 0;
while (bis.read() != -1) {
thirdSize++;
}
assertEquals(size, thirdSize);
assertTrue(bis.getBuffer().length < 8*1024*1024);
// Now try to read EOF multiple times
for (int i=1; i<1000; i++) {
assertEquals(-1, bis.read());
}
bis.close();
}
@Test
@@ -73,8 +78,81 @@ public class GPSFixImportTest {
public void storeFix(GPSFix fix, DeviceIdentifier deviceIdentifier) {
}
};
InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx");
servlet.importFiles(Arrays.asList(new Pair<>("test.gpx", in)), new AlwaysFailingGPSFixImporter(-1));
FileItem fi = new FileItem() {
private static final long serialVersionUID = 1L;
@Override
public void write(File file) throws Exception {
}
@Override
public void setFormField(boolean state) {
}
@Override
public void setFieldName(String name) {
}
@Override
public boolean isInMemory() {
return false;
}
@Override
public boolean isFormField() {
return false;
}
@Override
public String getString(String encoding) throws UnsupportedEncodingException {
return null;
}
@Override
public String getString() {
return null;
}
@Override
public long getSize() {
return 0;
}
@Override
public OutputStream getOutputStream() throws IOException {
return null;
}
@Override
public String getName() {
return "Cardiff Race17 - COMPETITORS.gpx";
}
@Override
public InputStream getInputStream() throws IOException {
return getClass().getResourceAsStream("/" + getName());
}
@Override
public String getFieldName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public byte[] get() {
return null;
}
@Override
public void delete() {
}
};
servlet.importFiles(Arrays.asList(new Pair<>("test.gpx", fi)), new AlwaysFailingGPSFixImporter(-1));
// getting to here without errors is good enough
}
}
@@ -52,9 +52,7 @@ public class GPSFixesResource extends AbstractSailingServerResource {
List<GPSFixMoving> fixes = data.getB();
try {
for (GPSFixMoving fix : fixes) {
getService().getSensorFixStore().storeFix(device, fix);
}
getService().getSensorFixStore().storeFixes(device, fixes);
logger.log(Level.INFO, "Added " + fixes.size() + " fixes for device " + device.toString() + " to store");
} catch (NoCorrespondingServiceRegisteredException e) {
logger.log(Level.WARNING, "Could not store fix for device " + device);
@@ -63,4 +61,3 @@ public class GPSFixesResource extends AbstractSailingServerResource {
return Response.ok().build();
}
}
@@ -2,7 +2,6 @@ package com.sap.sailing.server.gateway.trackfiles.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -32,13 +31,12 @@ import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
/**
* Import servlet for sensor data files. Importers are located through the OSGi service registry and mathed against the
* name provided by the upload formular.
* Import servlet for sensor data files. Importers are located through the OSGi service registry and matched against the
* name provided by the upload form.
*/
public class SensorDataImportServlet extends AbstractFileUploadServlet {
private static final long serialVersionUID = 1120226743039934620L;
private static final Logger logger = Logger.getLogger(SensorDataImportServlet.class.getName());
private static final int READ_BUFFER_SIZE = 1024 * 1024 * 1024;
public void storeFixes(Iterable<DoubleVectorFix> fixes, DeviceIdentifier deviceIdentifier) {
try {
@@ -54,21 +52,16 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet {
*
* @throws IOException
*/
private Iterable<TrackFileImportDeviceIdentifier> importFiles(Iterable<Pair<String, InputStream>> files)
private Iterable<TrackFileImportDeviceIdentifier> importFiles(Iterable<Pair<String, FileItem>> files)
throws IOException {
final Set<TrackFileImportDeviceIdentifier> deviceIds = new HashSet<>();
final Map<DeviceIdentifier, TimePoint> from = new HashMap<>();
final Map<DeviceIdentifier, TimePoint> to = new HashMap<>();
final Collection<DoubleVectorFixImporter> availableImporters = new LinkedHashSet<>();
availableImporters.addAll(getOSGiRegisteredImporters());
for (Pair<String, InputStream> file : files) {
for (Pair<String, FileItem> file : files) {
final String requestedImporterName = file.getA();
BufferedInputStream in = new BufferedInputStream(file.getB()) {
@Override
public void close() throws IOException {
// prevent importers from closing this stream
}
};
final FileItem fi = file.getB();
DoubleVectorFixImporter importerToUse = null;
for (DoubleVectorFixImporter candidate : availableImporters) {
if (candidate.getType().equals(requestedImporterName)) {
@@ -77,43 +70,37 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet {
}
}
if (importerToUse == null) {
in.close();
throw new RuntimeException("Sensor importer not found");
}
in.mark(READ_BUFFER_SIZE);
try {
in.reset();
} catch (IOException e1) {
logger.log(Level.SEVERE, "Could not reset stream", e1);
}
logger.log(Level.INFO,
"Going to import sensor data file with importer " + importerToUse.getClass().getSimpleName());
try {
importerToUse.importFixes(in, new DoubleVectorFixImporter.Callback() {
@Override
public void addFixes(Iterable<DoubleVectorFix> fixes, TrackFileImportDeviceIdentifier device) {
deviceIds.add(device);
storeFixes(fixes, device);
TimePoint earliestFixSoFarFromCurrentDevice = from.get(device);
TimePoint latestFixSoFarFromCurrentDevice = to.get(device);
for (DoubleVectorFix fix : fixes) {
if (earliestFixSoFarFromCurrentDevice == null
|| earliestFixSoFarFromCurrentDevice.after(fix.getTimePoint())) {
earliestFixSoFarFromCurrentDevice = fix.getTimePoint();
from.put(device, earliestFixSoFarFromCurrentDevice);
}
if (latestFixSoFarFromCurrentDevice == null
|| latestFixSoFarFromCurrentDevice.before(fix.getTimePoint())) {
latestFixSoFarFromCurrentDevice = fix.getTimePoint();
to.put(device, latestFixSoFarFromCurrentDevice);
try (BufferedInputStream in = new BufferedInputStream(fi.getInputStream())) {
try {
importerToUse.importFixes(in, new DoubleVectorFixImporter.Callback() {
@Override
public void addFixes(Iterable<DoubleVectorFix> fixes, TrackFileImportDeviceIdentifier device) {
deviceIds.add(device);
storeFixes(fixes, device);
TimePoint earliestFixSoFarFromCurrentDevice = from.get(device);
TimePoint latestFixSoFarFromCurrentDevice = to.get(device);
for (DoubleVectorFix fix : fixes) {
if (earliestFixSoFarFromCurrentDevice == null
|| earliestFixSoFarFromCurrentDevice.after(fix.getTimePoint())) {
earliestFixSoFarFromCurrentDevice = fix.getTimePoint();
from.put(device, earliestFixSoFarFromCurrentDevice);
}
if (latestFixSoFarFromCurrentDevice == null
|| latestFixSoFarFromCurrentDevice.before(fix.getTimePoint())) {
latestFixSoFarFromCurrentDevice = fix.getTimePoint();
to.put(device, latestFixSoFarFromCurrentDevice);
}
}
}
}
}, requestedImporterName);
logger.log(Level.INFO, "Successfully imported file " + requestedImporterName);
} catch (FormatNotSupportedException e) {
logger.log(Level.INFO, "Failed to import file " + requestedImporterName);
}, fi.getName(), requestedImporterName);
logger.log(Level.INFO, "Successfully imported file " + requestedImporterName);
} catch (FormatNotSupportedException e) {
logger.log(Level.INFO, "Failed to import file " + requestedImporterName);
}
}
}
return deviceIds;
@@ -125,7 +112,6 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet {
@Override
protected void process(List<FileItem> fileItems, HttpServletRequest req, HttpServletResponse resp)
throws IOException {
List<Pair<String, InputStream>> files = new ArrayList<>();
String importerName = null;
searchForPrefferedImporter: for (FileItem fi : fileItems) {
if ("preferredImporter".equalsIgnoreCase(fi.getFieldName())) {
@@ -134,11 +120,12 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet {
}
}
if (importerName == null) {
importerName = "BRAVO";
throw new RuntimeException("Missing preferred importer");
}
List<Pair<String, FileItem>> files = new ArrayList<>();
for (FileItem fi : fileItems) {
if ("file".equalsIgnoreCase(fi.getFieldName())) {
files.add(new Pair<>(importerName, fi.getInputStream()));
files.add(new Pair<>(importerName, fi));
}
}
final Iterable<TrackFileImportDeviceIdentifier> mappingList = importFiles(files);
@@ -2,18 +2,17 @@ package com.sap.sailing.server.gateway.trackfiles.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -42,7 +41,8 @@ import com.sap.sse.common.Util.Pair;
* The available importers are tried one by one in the following order, until the first one is found that does not fail
* with an {@link FormatNotSupportedException}:
* <ul>
* <li>If the type of a {@link #PREFERRED_IMPORTER preferred importer} is transmitted, this is the first that is used.</li>
* <li>If the type of a {@link #PREFERRED_IMPORTER preferred importer} is transmitted, this is the first that is used.
* </li>
* <li>Then the importers registered for a matching {@link GPSFixImporter#FILE_EXTENSION_PROPERTY file extension} are
* used.</li>
* <li>If all this fails, all other available importers are used.</li>
@@ -56,8 +56,6 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet {
private static final long serialVersionUID = 1120226743039934620L;
private static final Logger logger = Logger.getLogger(TrackFilesImportServlet.class.getName());
private static final int READ_BUFFER_SIZE = 1024 * 1024 * 1024;
public void storeFix(GPSFix fix, DeviceIdentifier deviceIdentifier) {
try {
getService().getSensorFixStore().storeFix(deviceIdentifier, fix);
@@ -85,68 +83,66 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet {
return result;
}
Iterable<TrackFileImportDeviceIdentifier> importFiles(Iterable<Pair<String, InputStream>> files, GPSFixImporter preferredImporter)
throws IOException {
protected Iterable<TrackFileImportDeviceIdentifier> importFiles(Iterable<Pair<String, FileItem>> files,
GPSFixImporter preferredImporter) throws IOException {
final Set<TrackFileImportDeviceIdentifier> deviceIds = new HashSet<>();
final Map<DeviceIdentifier, TimePoint> from = new HashMap<>();
final Map<DeviceIdentifier, TimePoint> to = new HashMap<>();
for (Pair<String, InputStream> file : files) {
final String fileName = file.getA();
for (Pair<String, FileItem> pair : files) {
final String fileName = pair.getA();
final FileItem fileItem = pair.getB();
String fileExt = null;
if (fileName.contains(".")) {
fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
}
Collection<GPSFixImporter> importersToTry = new LinkedHashSet<>();
Set<GPSFixImporter> importersToTry = new LinkedHashSet<>();
if (preferredImporter != null) {
importersToTry.add(preferredImporter);
}
importersToTry.addAll(getGPSFixImporters(fileExt));
importersToTry.addAll(getGPSFixImporters(null));
BufferedInputStream in = new BufferedInputStream(file.getB()) {
@Override
public void close() throws IOException {
//prevent importers from closing this stream
}
};
in.mark(READ_BUFFER_SIZE);
boolean done = false;
Iterator<GPSFixImporter> iter = importersToTry.iterator();
while (iter.hasNext() && ! done) {
try {
in.reset();
} catch (IOException e1) {
e1.printStackTrace();
}
boolean failed = false;
GPSFixImporter importer = iter.next();
logger.log(Level.INFO, "Trying to import file " + fileName + " with importer " + importer.getType());
try {
importer.importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
deviceIds.add(device);
storeFix(fix, device);
TimePoint earliestFixSoFarFromCurrentDevice = from.get(device);
if (earliestFixSoFarFromCurrentDevice == null || earliestFixSoFarFromCurrentDevice.after(fix.getTimePoint())) {
earliestFixSoFarFromCurrentDevice = fix.getTimePoint();
from.put(device, earliestFixSoFarFromCurrentDevice);
}
TimePoint latestFixSoFarFromCurrentDevice = to.get(device);
if (latestFixSoFarFromCurrentDevice == null || latestFixSoFarFromCurrentDevice.before(fix.getTimePoint())) {
latestFixSoFarFromCurrentDevice = fix.getTimePoint();
to.put(device, latestFixSoFarFromCurrentDevice);
}
}
}, true, fileName);
} catch (FormatNotSupportedException e) {
failed = true;
logger.log(Level.INFO,
"System knows " +
importersToTry.size() +
" importers: "+
importersToTry.stream().map(i -> i.getType()).collect(Collectors.joining(", "))
);
parsersLoop: for (GPSFixImporter importer : importersToTry) {
boolean succeeded = false;
logger.log(Level.INFO, "Trying to import file " + fileName + " with importer " + importer.getType());
try (BufferedInputStream in = new BufferedInputStream(fileItem.getInputStream())) {
try {
importer.importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
deviceIds.add(device);
storeFix(fix, device);
TimePoint earliestFixSoFarFromCurrentDevice = from.get(device);
if (earliestFixSoFarFromCurrentDevice == null
|| earliestFixSoFarFromCurrentDevice.after(fix.getTimePoint())) {
earliestFixSoFarFromCurrentDevice = fix.getTimePoint();
from.put(device, earliestFixSoFarFromCurrentDevice);
}
TimePoint latestFixSoFarFromCurrentDevice = to.get(device);
if (latestFixSoFarFromCurrentDevice == null
|| latestFixSoFarFromCurrentDevice.before(fix.getTimePoint())) {
latestFixSoFarFromCurrentDevice = fix.getTimePoint();
to.put(device, latestFixSoFarFromCurrentDevice);
}
}
}, true, fileName);
succeeded = true;
} catch (Exception e) {
logger.log(Level.INFO, "Failed with " + e.getClass().getSimpleName()
+ " while importing file using " + importer.getType());
}
}
if (! failed) {
done = true;
logger.log(Level.INFO, "Successfully imported file " + fileName);
if (succeeded) {
logger.log(Level.INFO, "Successfully imported file " + fileName + " using " + importer.getType());
break parsersLoop;
}
}
}
@@ -156,10 +152,10 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet {
@Override
protected void process(List<FileItem> fileItems, HttpServletRequest req, HttpServletResponse resp) throws IOException {
String prefImporterType = null;
List<Pair<String, InputStream>> files = new ArrayList<>();
List<Pair<String, FileItem>> files = new ArrayList<>();
for (FileItem item : fileItems) {
if (!item.isFormField())
files.add(new Pair<String, InputStream>(item.getName(), item.getInputStream()));
files.add(new Pair<String, FileItem>(item.getName(), item));
else {
if (item.getFieldName() != null && item.getFieldName().equals(PREFERRED_IMPORTER)) {
prefImporterType = item.getString();
@@ -167,18 +163,15 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet {
}
}
GPSFixImporter preferredImporter = null;
if (prefImporterType != null && ! prefImporterType.isEmpty()) {
preferredImporter = getServiceFinderFactory().createServiceFinder(GPSFixImporter.class).
findService(prefImporterType);
if (prefImporterType != null && !prefImporterType.isEmpty()) {
preferredImporter = getServiceFinderFactory().createServiceFinder(GPSFixImporter.class)
.findService(prefImporterType);
}
final Iterable<TrackFileImportDeviceIdentifier> mappingList = importFiles(files, preferredImporter);
//setJsonResponseHeader(resp);
//DO NOT set a JSON response header. This causes the browser to wrap the response in a
//<pre> tag when uploading from GWT, as this is an AJAX-request inside an iFrame.
// setJsonResponseHeader(resp);
// DO NOT set a JSON response header. This causes the browser to wrap the response in a
// <pre> tag when uploading from GWT, as this is an AJAX-request inside an iFrame.
resp.setContentType("text/html");
for (TrackFileImportDeviceIdentifier mapping : mappingList) {
String stringRep = mapping.getId().toString();
resp.getWriter().println(stringRep);
@@ -47,7 +47,9 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogFinishPositioningConf
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl;
import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMappingEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRegisterCompetitorEvent;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDeviceCompetitorBravoMappingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogDeviceCompetitorMappingEventImpl;
import com.sap.sailing.domain.abstractlog.regatta.events.impl.RegattaLogRegisterCompetitorEventImpl;
import com.sap.sailing.domain.base.BoatClass;
@@ -94,7 +96,9 @@ import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.common.media.MediaTrack;
import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus;
import com.sap.sailing.domain.common.sensordata.BravoSensorDataMetadata;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.impl.DoubleVectorFixImpl;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard;
import com.sap.sailing.domain.leaderboard.Leaderboard;
@@ -108,6 +112,7 @@ import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard;
import com.sap.sailing.domain.persistence.PersistenceFactory;
import com.sap.sailing.domain.persistence.media.MediaDBFactory;
import com.sap.sailing.domain.racelog.tracking.EmptySensorFixStore;
import com.sap.sailing.domain.racelog.tracking.SensorFixStore;
import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory;
import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier;
import com.sap.sailing.domain.racelogtracking.DeviceIdentifier;
@@ -126,6 +131,7 @@ import com.sap.sailing.server.masterdata.MasterDataImporter;
import com.sap.sailing.server.testsupport.RacingEventServiceImplMock;
import com.sap.sse.common.Color;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Timed;
import com.sap.sse.common.Util;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
@@ -290,6 +296,18 @@ public class MasterDataImportTest {
new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(90)));
sourceService.getSensorFixStore().storeFix(deviceIdentifier, gpsFix);
// Add sensor fix
DeviceIdentifier deviceIdentifier2 = new SmartphoneImeiIdentifier("b");
RegattaLogDeviceMappingEvent<Competitor> bravoMappingEvent = new RegattaLogDeviceCompetitorBravoMappingEventImpl(
logTimePoint4, author, competitor, deviceIdentifier2, logTimePoint,
logTimePoint3);
regatta.getRegattaLog().add(bravoMappingEvent);
double[] fixData = new double[BravoSensorDataMetadata.INSTANCE.columnCount];
double rideHeightValue = 1337.0;
fixData[BravoSensorDataMetadata.INSTANCE.rideHeightColumn] = rideHeightValue;
DoubleVectorFixImpl doubleVectorFix = new DoubleVectorFixImpl(logTimePoint2, fixData);
sourceService.getSensorFixStore().storeFix(deviceIdentifier2, doubleVectorFix);
// Set score correction
double scoreCorrection = 12.0;
leaderboard.getScoreCorrection().correctScore(competitor, raceColumn, scoreCorrection);
@@ -427,7 +445,13 @@ public class MasterDataImportTest {
Assert.assertEquals(competitor.getId(), registerEventOnTarget.getCompetitor().getId());
// Check for import of racelogtracking fix
Assert.assertTrue(destService.getSensorFixStore().getNumberOfFixes(deviceIdentifier) == 1);
SensorFixStore sensorFixStore = destService.getSensorFixStore();
Assert.assertEquals(1, sensorFixStore.getNumberOfFixes(deviceIdentifier));
verifyFix(gpsFix, sensorFixStore, logTimePoint, logTimePoint3, deviceIdentifier);
// Check for import of sensor fix
Assert.assertEquals(1, sensorFixStore.getNumberOfFixes(deviceIdentifier2));
verifyFix(doubleVectorFix, sensorFixStore, logTimePoint, logTimePoint3, deviceIdentifier2);
// Check for persisting of race log events:
RacingEventService dest2 = new RacingEventServiceImplMock(){};
@@ -447,6 +471,17 @@ public class MasterDataImportTest {
Assert.assertEquals(registerEvent.getId(), registerEventOnTarget2.getId());
}
private void verifyFix(Timed expectedFix, SensorFixStore store, TimePoint start, TimePoint end, DeviceIdentifier deviceIdentifier) {
List<Timed> fixes = new ArrayList<>(1);
try {
store.loadFixes(fixes::add, deviceIdentifier, start, end, true);
assertEquals(1, fixes.size());
assertEquals(expectedFix, fixes.get(0));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testMasterDataImportForScoreCorrections() throws MalformedURLException, IOException,
InterruptedException, ClassNotFoundException {
@@ -53,7 +53,7 @@ public class BravoDataImportTest {
callbackCallCount++;
sumRideHeight += new BravoFixImpl(fix).getRideHeight();
}
}, "source");
}, "filename", "source");
Assert.assertEquals(importData.expectedFixesCount, callbackCallCount);
Assert.assertEquals(importData.expectedAverageRideHeight,
callbackCallCount == 0 ? sumRideHeight : sumRideHeight / callbackCallCount, 0.00001);
@@ -13,46 +13,43 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.w3c.dom.Element;
import slash.navigation.base.BaseNavigationPosition;
import slash.navigation.gpx.binding11.WptType;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.trackfiles.TrackFileImportDeviceIdentifier;
import com.sap.sailing.server.trackfiles.impl.BaseRouteConverterGPSFixImporterImpl;
import com.sap.sailing.server.trackfiles.impl.RouteConverterGPSFixImporterImpl;
import slash.navigation.base.BaseNavigationPosition;
import slash.navigation.gpx.binding11.WptType;
public class RouteConverterGpx11ExtensionsTest {
private boolean extensionsFound = false;
@Test
public void areExtensionsRead() throws Exception {
String testFile = "/equine-extensions.gpx";
InputStream in = getClass().getResourceAsStream(testFile);
assert in != null : new Exception("Input file not found");
BaseRouteConverterGPSFixImporterImpl importer = spy(new RouteConverterGPSFixImporterImpl());
doAnswer(new Answer<GPSFix>() {
@Override
public GPSFix answer(InvocationOnMock invocation) throws Throwable {
BaseNavigationPosition p = (BaseNavigationPosition) invocation.getArguments()[0];
WptType waypoint = p.asGpxPosition().getOrigin(WptType.class);
final List<Object> extensions = waypoint.getExtensions().getAny();
for (Object o : extensions) {
Element el = (Element) o;
System.out.println(el.getLocalName() + " " + el.getTextContent());
extensionsFound = true;
try (InputStream in = getClass().getResourceAsStream(testFile)) {
assert in != null : new Exception("Input file not found");
BaseRouteConverterGPSFixImporterImpl importer = spy(new RouteConverterGPSFixImporterImpl());
doAnswer(new Answer<GPSFix>() {
@Override
public GPSFix answer(InvocationOnMock invocation) throws Throwable {
BaseNavigationPosition p = (BaseNavigationPosition) invocation.getArguments()[0];
WptType waypoint = p.asGpxPosition().getOrigin(WptType.class);
final List<Object> extensions = waypoint.getExtensions().getAny();
for (Object o : extensions) {
Element el = (Element) o;
System.out.println(el.getLocalName() + " " + el.getTextContent());
extensionsFound = true;
}
return (GPSFix) invocation.callRealMethod();
}
return (GPSFix) invocation.callRealMethod();
}
}).when(importer).convertToGPSFix(any(BaseNavigationPosition.class));
importer.importFixes(in, new com.sap.sailing.domain.trackimport.GPSFixImporter.Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
}
}, false, "source");
assertTrue(extensionsFound);
}).when(importer).convertToGPSFix(any(BaseNavigationPosition.class));
importer.importFixes(in, new com.sap.sailing.domain.trackimport.GPSFixImporter.Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
}
}, false, "source");
assertTrue(extensionsFound);
}
}
}
@@ -25,29 +25,31 @@ public class TrackFileImportTest {
@Test
public void testGpx() throws IOException, FormatNotSupportedException {
InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx");
new RouteConverterGPSFixImporterImpl().importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
if (fix instanceof GPSFixMoving) {
callbackCalled = true;
try (InputStream in = getClass().getResourceAsStream("/Cardiff Race17 - COMPETITORS.gpx")) {
new RouteConverterGPSFixImporterImpl().importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
if (fix instanceof GPSFixMoving) {
callbackCalled = true;
}
}
}
}, false, "source");
assertTrue(callbackCalled);
}, false, "source");
assertTrue(callbackCalled);
}
}
@Test
public void testKmlOnlyLatLong() throws IOException, FormatNotSupportedException {
InputStream in = getClass().getResourceAsStream("/Cardiff Race22 - COMPETITORS.kml");
new RouteConverterGPSFixImporterImpl().importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
if (fix instanceof GPSFix) {
callbackCalled = true;
try (InputStream in = getClass().getResourceAsStream("/Cardiff Race22 - COMPETITORS.kml")) {
new RouteConverterGPSFixImporterImpl().importFixes(in, new Callback() {
@Override
public void addFix(GPSFix fix, TrackFileImportDeviceIdentifier device) {
if (fix instanceof GPSFix) {
callbackCalled = true;
}
}
}
}, false, "source");
assertTrue(callbackCalled);
}, false, "source");
assertTrue(callbackCalled);
}
}
}
@@ -26,7 +26,6 @@ import com.tractrac.model.lib.api.event.CreateModelException;
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
public class TrackedRacesExportTest extends OnlineTracTracBasedTest {
public TrackedRacesExportTest() throws MalformedURLException, URISyntaxException {
super();
}
@@ -37,35 +36,34 @@ public class TrackedRacesExportTest extends OnlineTracTracBasedTest {
}
@Before
public void setUp() throws URISyntaxException, IOException, InterruptedException, SubscriberInitializationException, CreateModelException {
URI storedUri = new URI("file:///"+new File("resources/event_20120905_erEuropean-Gold_fleet_-_race_1.mtb").getCanonicalPath().replace('\\', '/'));
super.setUp(new URL("file:///"+new File("resources/event_20120905_erEuropean-Gold_fleet_-_race_1.txt").getCanonicalPath()),
public void setUp() throws URISyntaxException, IOException, InterruptedException, SubscriberInitializationException,
CreateModelException {
URI storedUri = new URI("file:///" + new File("resources/event_20120905_erEuropean-Gold_fleet_-_race_1.mtb")
.getCanonicalPath().replace('\\', '/'));
super.setUp(
new URL("file:///"
+ new File("resources/event_20120905_erEuropean-Gold_fleet_-_race_1.txt").getCanonicalPath()),
/* liveUri */ null, /* storedUri */ storedUri,
new ReceiverType[] { ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS, ReceiverType.MARKPASSINGS });
}
private byte[] getBytes(TrackFilesDataSource data, TrackFilesFormat format, TrackedRace race,
boolean dataBeforeAfter, boolean rawFixes) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(out);
TrackFileExporterImpl.INSTANCE.writeAllData(Collections.singletonList(data), format, Collections.singletonList(race),
true, true, zip);
zip.flush();
byte[] result = out.toByteArray();
out.close();
return result;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ZipOutputStream zip = new ZipOutputStream(out);
TrackFileExporterImpl.INSTANCE.writeAllData(Collections.singletonList(data), format,
Collections.singletonList(race), true, true, zip);
zip.flush();
byte[] result = out.toByteArray();
return result;
}
}
@Test
public void doAllFormatsWork() throws IOException, FormatNotSupportedException {
TrackedRace race = getTrackedRace();
for (TrackFilesFormat format : TrackFilesFormat.values()) {
byte[] data = getBytes(TrackFilesDataSource.COMPETITORS, format, race, true, true);
assertTrue(data.length > 4000);
}
}
@@ -73,10 +71,8 @@ public class TrackedRacesExportTest extends OnlineTracTracBasedTest {
@Test
public void doAllDataSourcesWork() throws IOException, FormatNotSupportedException {
TrackedRace race = getTrackedRace();
for (TrackFilesDataSource source : TrackFilesDataSource.values()) {
byte[] data = getBytes(source, TrackFilesFormat.Gpx11, race, true, true);
assertTrue(data.length > 40);
}
}
@@ -46,15 +46,15 @@ public class BravoDataImporterImpl implements DoubleVectorFixImporter {
private final String BOF = "jjlDATE\tjjlTIME\tEpoch";
private static final int BATCH_SIZE = 5000;
public void importFixes(InputStream inputStream, Callback callback, String sourceName)
public void importFixes(InputStream inputStream, Callback callback, String filename, String sourceName)
throws FormatNotSupportedException, IOException {
final TrackFileImportDeviceIdentifier trackIdentifier = new TrackFileImportDeviceIdentifierImpl(
UUID.randomUUID(), sourceName, sourceName + "_Imu", MillisecondsTimePoint.now());
UUID.randomUUID(), filename, sourceName, MillisecondsTimePoint.now());
try {
LOG.fine("Import CSV from " + sourceName);
LOG.fine("Import CSV from " + filename);
final InputStreamReader isr;
if (sourceName.endsWith("gz")) {
LOG.fine("Using gzip stream reader " + sourceName);
LOG.fine("Using gzip stream reader " + filename);
isr = new InputStreamReader(new GZIPInputStream(inputStream));
} else {
isr = new InputStreamReader(inputStream);
@@ -63,10 +63,10 @@ public class BravoDataImporterImpl implements DoubleVectorFixImporter {
try (BufferedReader buffer = new BufferedReader(isr)) {
String headerLine = null;
headerSearch: while (headerLine == null) {
LOG.fine("Searching for header in imu file");
LOG.fine("Searching for header in bravo file");
String headerCandidate = buffer.readLine();
if (headerCandidate == null) {
throw new RuntimeException("Missing required header in file " + sourceName);
throw new RuntimeException("Missing required header in file " + filename);
}
if (headerCandidate.startsWith(BOF)) {
LOG.fine("Found header");
@@ -91,7 +91,7 @@ public class BravoDataImporterImpl implements DoubleVectorFixImporter {
buffer.close();
}
} catch (Exception e) {
LOG.log(Level.SEVERE, "Exception parsing CSV file " + sourceName, e);
LOG.log(Level.SEVERE, "Exception parsing bravo CSV file " + filename, e);
}
}
@@ -125,7 +125,7 @@ public class BravoDataImporterImpl implements DoubleVectorFixImporter {
// long r16TP = ZonedDateTime.of(LocalDateTime.of(2016, 3, 19, 11, 55), ZoneId.of("Europe/Berlin"))
// .toEpochSecond() * 1000;
// fixTp = fixTp.plus(r16TP - 1461828459589l);
double[] fixData = new double[metadata.getColumns().size()];
double[] fixData = new double[metadata.columnCount];
for (int columnIndexInFix = 0; columnIndexInFix < fixData.length; columnIndexInFix++) {
String columnName = metadata.getColumns().get(columnIndexInFix);
Integer columnIndexInFile = colIndices.get(columnName);
@@ -6,7 +6,6 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -18,7 +17,7 @@ import com.sap.sailing.domain.common.trackfiles.TrackFilesFormat;
import com.sap.sailing.domain.trackimport.FormatNotSupportedException;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.server.trackfiles.TrackFileExporter;
import com.sap.sse.util.impl.ThreadFactoryWithPriority;
import com.sap.sse.util.ThreadPoolUtil;
/**
* Export data to well-known formats such as GPX, KML etc. Internally, the RouteConverter Library is used.
@@ -56,7 +55,7 @@ public class TrackFileExporterImpl implements TrackFileExporter {
};
List<WriteRaceDataCallable> callables = new ArrayList<>();
ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true));
ExecutorService executor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor();
List<String> errors = new ArrayList<>();
for (TrackedRace race : races) {
for (TrackFilesDataSource d : data) {
@@ -189,7 +189,7 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch
* @throws SocketException
* thrown, e.g., in case there is already another listener on the port requested
*/
void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) throws Exception;
void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination);
void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException;
@@ -28,7 +28,6 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -605,7 +604,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
operationExecutionListeners = new ConcurrentHashMap<>();
courseListeners = new ConcurrentHashMap<>();
persistentRegattasForRaceIDs = new ConcurrentHashMap<>();
Executor simulatorExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
final ScheduledExecutorService simulatorExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor();
// TODO: initialize smart-future-cache for simulation-results and add to simulation-service
simulationService = SimulationServiceFactory.INSTANCE.getService(simulatorExecutor, this);
this.raceLogReplicator = new RaceLogReplicatorAndNotifier(this);
@@ -2291,9 +2290,13 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
}
@Override
public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) throws Exception {
public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) {
for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) {
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
try {
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error trying to track wind using wind tracker factory "+windTrackerFactory, e);
}
}
}
@@ -1,6 +1,6 @@
package com.sap.sailing.server.simulation;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import com.sap.sailing.server.RacingEventService;
@@ -8,6 +8,6 @@ public interface SimulationServiceFactory {
static SimulationServiceFactory INSTANCE = new SimulationServiceFactoryImpl();
SimulationService getService(Executor executor, RacingEventService racingEventService);
SimulationService getService(ScheduledExecutorService executor, RacingEventService racingEventService);
}
@@ -1,12 +1,12 @@
package com.sap.sailing.server.simulation;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import com.sap.sailing.server.RacingEventService;
public class SimulationServiceFactoryImpl implements SimulationServiceFactory {
public SimulationService getService(Executor executor, RacingEventService racingEventService) {
public SimulationService getService(ScheduledExecutorService executor, RacingEventService racingEventService) {
return new SimulationServiceImpl(executor, racingEventService);
}
@@ -8,9 +8,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
@@ -59,13 +57,12 @@ import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.util.SmartFutureCache;
import com.sap.sse.util.impl.ThreadFactoryWithPriority;
public class SimulationServiceImpl implements SimulationService {
private static final Logger logger = Logger.getLogger(SimulationService.class.getName());
final private Executor executor;
final private ScheduledExecutorService executor;
final private SmartFutureCache<LegIdentifier, SimulationResults, SmartFutureCache.EmptyUpdateInterval> cache;
final private RacingEventService racingEventService;
final private ScheduledExecutorService scheduler;
@@ -73,9 +70,9 @@ public class SimulationServiceImpl implements SimulationService {
final private HashMap<RaceIdentifier, LegChangeListener> legListeners;
final private long WAIT_MILLIS = 20000; // milliseconds to wait until earliest cache-update for simulation
public SimulationServiceImpl(Executor executor, RacingEventService racingEventService) {
public SimulationServiceImpl(ScheduledExecutorService executor, RacingEventService racingEventService) {
this.executor = executor;
this.scheduler = Executors.newScheduledThreadPool(1, new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true));
this.scheduler = executor;
this.racingEventService = racingEventService;
if (racingEventService != null) {
this.raceListeners = new HashMap<String, SimulationRaceListener>();
@@ -437,31 +434,26 @@ public class SimulationServiceImpl implements SimulationService {
Simulator simulator = new SimulatorImpl(simulationParameters);
Map<PathType, Path> result = new HashMap<PathType, Path>();
FutureTask<Path> taskOmniscient = null;
Future<Path> taskOmniscient = null;
if (simulationParameters.showOmniscient()) {
// schedule omniscient task
taskOmniscient = new FutureTask<Path>(() -> simulator.getPath(PathType.OMNISCIENT));
executor.execute(taskOmniscient);
taskOmniscient = executor.submit(() -> simulator.getPath(PathType.OMNISCIENT));
}
FutureTask<Path> task1TurnerLeft = null;
FutureTask<Path> task1TurnerRight = null;
Future<Path> task1TurnerLeft = null;
Future<Path> task1TurnerRight = null;
if (simulationParameters.getLegType() != LegType.REACHING) {
// schedule 1-turner tasks
task1TurnerLeft = new FutureTask<Path>(() -> simulator.getPath(PathType.ONE_TURNER_LEFT));
task1TurnerRight = new FutureTask<Path>(() -> simulator.getPath(PathType.ONE_TURNER_RIGHT));
executor.execute(task1TurnerLeft);
executor.execute(task1TurnerRight);
task1TurnerLeft = executor.submit(() -> simulator.getPath(PathType.ONE_TURNER_LEFT));
task1TurnerRight = executor.submit(() -> simulator.getPath(PathType.ONE_TURNER_RIGHT));
}
FutureTask<Path> taskOpportunistLeft = null;
FutureTask<Path> taskOpportunistRight = null;
Future<Path> taskOpportunistLeft = null;
Future<Path> taskOpportunistRight = null;
if (simulationParameters.showOpportunist()) {
// schedule opportunist tasks (which depend on 1-turner results)
taskOpportunistLeft = new FutureTask<Path>(() -> simulator.getPath(PathType.OPPORTUNIST_LEFT));
taskOpportunistRight = new FutureTask<Path>(() -> simulator.getPath(PathType.OPPORTUNIST_RIGHT));
executor.execute(taskOpportunistLeft);
executor.execute(taskOpportunistRight);
taskOpportunistLeft = executor.submit(() -> simulator.getPath(PathType.OPPORTUNIST_LEFT));
taskOpportunistRight = executor.submit(() -> simulator.getPath(PathType.OPPORTUNIST_RIGHT));
}
Path path1TurnerLeft = null;
@@ -37,6 +37,9 @@ import javax.xml.bind.annotation.XmlType;
* &lt;enumeration value="SCP"/>
* &lt;enumeration value="ZFP"/>
* &lt;enumeration value="RTD"/>
* &lt;enumeration value="UFD"/>
* &lt;enumeration value="TLE"/>
* &lt;enumeration value="STP"/>
* &lt;/restriction>
* &lt;/simpleType>
* </pre>
@@ -147,7 +150,20 @@ public enum IFBoatStatus {
* Retired
*
*/
RTD;
RTD,
/**
* Uniform Flag Disqualification
*/
UFD,
/**
* Time limit Expired
*/
TLE,
/**
* Standard Penalty by Race Committee; gives a certain number of penalty points on top of rank-inferred score
*/
STP;
public String value() {
return name();
@@ -47,24 +47,32 @@
vertical-align: top;
}
.logo {
.logo, .logo:hover, .logo:visited {
margin: 0px;
display: inline-block;
gwt-sprite: "logo";
background-size: cover;
width: 80px;
height: 40px;
border: none;
text-decoration: none;
}
.logotitle {
.logotitle, .logotitle:hover, .logotitle:visited {
font-size: 1rem;
display: inline-block;
color: #f6f9fc;
color: #fff;
width: 150px;
height: 100%;
padding: 17px 15px 0 0;
vertical-align: top;
text-align: right;
border: none;
border-right: 1px solid rgb(180,180,180);
margin: 0px;
margin-left: -25px;
box-sizing: border-box;
text-decoration: none;
}
.pagetitle {
color: #ffffff;
@@ -1,9 +1,11 @@
package com.sap.sse.gwt.client.sapheader;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
@@ -22,13 +24,16 @@ import com.google.gwt.user.client.ui.Widget;
* </ul>
*/
public class SAPHeader extends Composite {
private static final String LOGO_URL = "https://sapsponsorships.com/";
private static SAPHeaderUiBinder uiBinder = GWT.create(SAPHeaderUiBinder.class);
interface SAPHeaderUiBinder extends UiBinder<Widget, SAPHeader> {
}
@UiField
DivElement applicationNameUi;
AnchorElement applicationNameAnchor;
@UiField
DivElement pageTitleUi;
@UiField
@@ -37,11 +42,16 @@ public class SAPHeader extends Composite {
DivElement subTitleUi;
@UiField
SimplePanel rightSideUi;
@UiField
AnchorElement logoAnchor;
public SAPHeader(String applicationName) {
public SAPHeader(String applicationName, String applicationBaseUrl) {
SAPHeaderResources.INSTANCE.css().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
applicationNameUi.setInnerText(applicationName != null ? applicationName : "&nbsp;");
applicationNameAnchor.setInnerText(applicationName != null ? applicationName : "&nbsp;");
logoAnchor.setHref(LOGO_URL);
String sapSailingUrl = applicationBaseUrl + "?locale=" + LocaleInfo.getCurrentLocale().getLocaleName();
applicationNameAnchor.setHref(sapSailingUrl);
}
public void setHeaderTitle(String title) {
@@ -7,18 +7,14 @@
<g:HTMLPanel>
<header class="{local_res.css.siteheader}">
<div class="{local_res.css.siteheaderLeft}">
<g:Image resource="{local_res.logo}" pixelSize="80, 40" altText="SAP Sailing Analytics" styleName="{local_res.css.logo}"/>
<div ui:field="applicationNameUi" class="{local_res.css.logotitle}"></div>
<a class="{local_res.css.logo}" ui:field="logoAnchor" target="_blank"></a>
<a class="{local_res.css.logotitle}" target="_blank" ui:field="applicationNameAnchor"></a>
</div>
<div
ui:field="pageTitleUi"
class="{local_res.css.siteheader2ndCol}"
>
<div ui:field="pageTitleUi" class="{local_res.css.siteheader2ndCol}">
<div ui:field="titleUi" style="margin-top:14px;" class="{local_res.css.siteHeaderTitle}"></div>
<div ui:field="subTitleUi" style="display:none;" class="{local_res.css.siteHeaderSubTitle}"></div>
</div>
<g:SimplePanel ui:field="rightSideUi" styleName="{local_res.css.siteheaderRight}">
</g:SimplePanel>
<g:SimplePanel ui:field="rightSideUi" styleName="{local_res.css.siteheaderRight}" />
</header>
</g:HTMLPanel>
</ui:UiBinder>
@@ -20,13 +20,13 @@ public class SAPHeaderWithAuthentication extends SAPHeader {
private AuthenticationMenuView authenticationMenuView;
public SAPHeaderWithAuthentication(String applicationName, String headerTitle) {
this(applicationName);
public SAPHeaderWithAuthentication(String applicationName, String applicationBaseUrl, String headerTitle) {
this(applicationName, applicationBaseUrl);
setHeaderTitle(headerTitle);
}
public SAPHeaderWithAuthentication(String applicationName) {
super(applicationName);
public SAPHeaderWithAuthentication(String applicationName, String applicationBaseUrl) {
super(applicationName, applicationBaseUrl);
res.css().ensureInjected();
FlowPanel rightWithAuthentication = new FlowPanel();
@@ -13,6 +13,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -23,6 +24,11 @@ import com.sap.sse.common.Util.Pair;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.util.SmartFutureCache.UpdateInterval;
import com.sap.sse.util.impl.HasTracingGet;
import com.sap.sse.util.impl.HasTracingGetImpl;
import com.sap.sse.util.impl.KnowsExecutor;
import com.sap.sse.util.impl.KnowsExecutorAndTracingGet;
import com.sap.sse.util.impl.KnowsExecutorAndTracingGetImpl;
/**
* A cache for which a background update can be triggered. Readers can decide whether they want to wait for any ongoing
@@ -188,6 +194,10 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
return callable.call();
}
@Override
public String toString() {
return callable==null?"null":callable.toString();
}
}
/**
@@ -201,7 +211,9 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
* @author Axel Uhl (D043530)
*
*/
private class FutureTaskWithCancelBlocking extends FutureTask<V> implements Callable<V> {
private class FutureTaskWithCancelBlocking extends FutureTask<V> implements KnowsExecutor, Callable<V> {
private final KnowsExecutorAndTracingGet<V> tracingGetHelper;
private boolean runningAndReadUpdateInterval;
private final K key;
@@ -228,6 +240,7 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
public FutureTaskWithCancelBlocking(SettableCallable<V> callable, final K key, final U updateInterval,
final boolean callerWaitsSynchronouslyForResult, final Thread callerThread) {
super(callable);
tracingGetHelper = new KnowsExecutorAndTracingGetImpl<>();
callable.setCallable(this);
this.gettingThreads = new HashSet<>();
this.key = key;
@@ -274,7 +287,7 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
}
}
}
return super.get();
return tracingGetHelper.callGetAndTraceAfterEachTimeout(this);
}
@Override
@@ -363,6 +376,16 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
public U getUpdateInterval() {
return updateInterval;
}
@Override
public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) {
tracingGetHelper.setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor);
}
@Override
public String toString() {
return getClass().getName()+" [key="+key+", updateInterval="+updateInterval+", cacheUpdateComputer="+cacheUpdateComputer+"]";
}
}
public SmartFutureCache(CacheUpdater<K, V, U> cacheUpdateComputer, String nameForLocks) {
@@ -507,14 +530,33 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
notifyAll();
}
}
public String toString() {
final String result;
synchronized (this) {
if (isSet) {
result = value==null?"null":value.toString();
} else {
result = "unset";
}
}
return result;
}
}
private static class TransitiveFuture<T> implements Future<T> {
private final SettableFuture<Future<T>> future;
private final HasTracingGet<T> tracingGetHelper;
protected TransitiveFuture(SettableFuture<Future<T>> future) {
super();
this.future = future;
this.tracingGetHelper = new HasTracingGetImpl<T>() {
@Override
protected String getAdditionalTraceInfo() {
return "transitive future "+TransitiveFuture.this.future.toString();
}
};
}
@Override
@@ -546,7 +588,7 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
@Override
public T get() throws InterruptedException, ExecutionException {
return future.get().get();
return tracingGetHelper.callGetAndTraceAfterEachTimeout(this);
}
@Override
@@ -52,7 +52,7 @@ public interface ThreadPoolUtil {
*
* The same configuration as for {@link #getDefaultBackgroundTaskThreadPoolExecutor()} is used.
*/
ScheduledExecutorService createBackgroundTaskThreadPoolExecutor();
ScheduledExecutorService createBackgroundTaskThreadPoolExecutor(String name);
/**
* In case an application module really requires its own thread pool instead of
@@ -74,5 +74,7 @@ public interface ThreadPoolUtil {
*
* The same configuration as for {@link #getDefaultForegroundTaskThreadPoolExecutor()} is used.
*/
ScheduledExecutorService createForegroundTaskThreadPoolExecutor();
ScheduledExecutorService createForegroundTaskThreadPoolExecutor(String name);
ScheduledExecutorService createForegroundTaskThreadPoolExecutor(int size, String name);
}
@@ -0,0 +1,37 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskWithTracingGet<V> extends FutureTask<V> {
private final HasTracingGet<V> hasTracingGet;
private final String traceInfo;
public FutureTaskWithTracingGet(String traceInfo, Callable<V> callable) {
super(callable);
this.traceInfo = traceInfo;
hasTracingGet = createHasTracingGet();
}
public FutureTaskWithTracingGet(String traceInfo, Runnable runnable, V result) {
super(runnable, result);
this.traceInfo = traceInfo;
hasTracingGet = createHasTracingGet();
}
@Override
public V get() throws InterruptedException, ExecutionException {
return hasTracingGet.callGetAndTraceAfterEachTimeout(this);
}
private HasTracingGet<V> createHasTracingGet() {
return new HasTracingGetImpl<V>() {
@Override
protected String getAdditionalTraceInfo() {
return traceInfo;
}
};
}
}
@@ -0,0 +1,16 @@
package com.sap.sse.util.impl;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
public interface HasTracingGet<V> {
/**
* Keeps calling {@link Future#get(long, java.util.concurrent.TimeUnit)} with a timeout until a result is available
* or an exception other than a {@link TimeoutException} is thrown. When a timeout occurs, some trace output is
* written to the log. The method is guaranteed not to call {@link Future#get()} and therefore can be used in an
* implementation of a {@link Future#get()} method without causing an endless recursion as long as
* {@link Future#get(long, java.util.concurrent.TimeUnit)} is not calling this method.
*/
V callGetAndTraceAfterEachTimeout(Future<V> future) throws InterruptedException, ExecutionException;
}
@@ -0,0 +1,37 @@
package com.sap.sse.util.impl;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.common.Duration;
/**
* Can be used with a {@link Future} task and outputs trace messages to the log after {@link #MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET} milliseconds
* of a {@link #get()} call not returning. The {@link #getAdditionalTraceInfo()} method must be implemented by subclasses to provide some meaningful
* hint as to which object is currently timing out.
*
* @author Axel Uhl (d043530)
*
* @param <V>
*/
public abstract class HasTracingGetImpl<V> implements HasTracingGet<V> {
private static final Logger logger = Logger.getLogger(HasTracingGetImpl.class.getName());
private final long MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET = Duration.ONE_SECOND.times(5).asMillis();
public V callGetAndTraceAfterEachTimeout(Future<V> future) throws InterruptedException, ExecutionException {
while (true) {
try {
return future.get(MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.log(Level.INFO, "Timeout waiting for future task "+future+
" (retrying); "+ getAdditionalTraceInfo(), e);
}
}
}
protected abstract String getAdditionalTraceInfo();
}
@@ -0,0 +1,14 @@
package com.sap.sse.util.impl;
import java.util.concurrent.ThreadPoolExecutor;
/**
* A {@link ThreadPoolExecutor} can be set on this object. This is used, in particular, by the
* {@link NamedTracingScheduledThreadPoolExecutor} whenever a task of this type is enqueued.
*
* @author Axel Uhl (d043530)
*
*/
public interface KnowsExecutor {
void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor);
}
@@ -0,0 +1,17 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Future;
import com.sap.sse.util.ThreadPoolUtil;
/**
* Can be used with a {@link Future} task and outputs trace messages to the log after {@link #MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET} milliseconds
* of a {@link #get()} call not returning. When used in conjunction with a {@link NamedTracingScheduledThreadPoolExecutor} such as
* the ones returned by {@link ThreadPoolUtil}, the task will be
*
* @author Axel Uhl (d043530)
*
* @param <V>
*/
public interface KnowsExecutorAndTracingGet<V> extends KnowsExecutor, HasTracingGet<V> {
}
@@ -0,0 +1,31 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import com.sap.sse.util.ThreadPoolUtil;
/**
* Can be used with a {@link Future} task and outputs trace messages to the log after
* {@link #MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET} milliseconds of a {@link #get()} call not returning. When used
* in conjunction with a {@link NamedTracingScheduledThreadPoolExecutor} such as the ones returned by
* {@link ThreadPoolUtil}, the task will be
*
* @author Axel Uhl (d043530)
*
* @param <V>
*/
public class KnowsExecutorAndTracingGetImpl<V> extends HasTracingGetImpl<V> implements KnowsExecutorAndTracingGet<V> {
private ThreadPoolExecutor executorThisTaskIsScheduledFor;
@Override
public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) {
this.executorThisTaskIsScheduledFor = executorThisTaskIsScheduledFor;
}
@Override
protected String getAdditionalTraceInfo() {
return executorThisTaskIsScheduledFor == null ? "not scheduled"
: ("scheduled with executor " + executorThisTaskIsScheduledFor);
}
}
@@ -0,0 +1,164 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.concurrent.ConcurrentWeakHashMap;
/**
* A scheduled thread pool executor with a name that produces {@link Future} tasks that write a trace message
* to the log after their {@link Future#get()} method hasn't produced a result for some time, then keep trying.
* The tasks are linked to this executor, also when a {@link KnowsExecutor} task is submitted to the {@link #execute(Runnable)}
* method, so that when they need to trace they
*
* @author Axel Uhl (d043530)
*
*/
public class NamedTracingScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
private static final Logger logger = Logger.getLogger(NamedTracingScheduledThreadPoolExecutor.class.getName());
private final String name;
/**
* If logging level is at least {@link Level#FINE}, each runnable's start time point is recorded in the
* {@link #beforeExecute(Thread, Runnable)} method. If at this logging level, the
* {@link #afterExecute(Runnable, Throwable)} method calculates the runnable's duration, and if above the
* {@link #DURATION_LOGGING_THRESHOLD}, writes a log entry.
*/
private final ConcurrentHashMap<Runnable, TimePoint> startTimePoints = new ConcurrentHashMap<>();
/**
* When logging {@link Level#FINE}, the {@link #schedule(Runnable, long, TimeUnit)} variants produce wrappers
* around the runnables whose content is nulled when {@link #afterExecute(Runnable, Throwable)} is called. In order
* to be able to log long-running runnables correctly, this map stores the connection between the wrapping
* future and the runnable wrapped by it.
*/
private final ConcurrentWeakHashMap<ScheduledFuture<?>, Runnable> wrappedRunnables = new ConcurrentWeakHashMap<>();
private final Duration DURATION_LOGGING_THRESHOLD = Duration.ONE_SECOND.times(5);
public NamedTracingScheduledThreadPoolExecutor(String name, int corePoolSize, RejectedExecutionHandler handler) {
super(corePoolSize, handler);
this.name = name;
}
public NamedTracingScheduledThreadPoolExecutor(String name, int corePoolSize, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, threadFactory, handler);
this.name = name;
}
public NamedTracingScheduledThreadPoolExecutor(String name, int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize, threadFactory);
this.name = name;
}
public NamedTracingScheduledThreadPoolExecutor(String name, int corePoolSize) {
super(corePoolSize);
this.name = name;
}
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) {
return new ThreadPoolAwareRunnableScheduledFutureDelegate<>(this, task);
}
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(Callable<V> callable, RunnableScheduledFuture<V> task) {
return new ThreadPoolAwareRunnableScheduledFutureDelegate<>(this, task);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new ThreadPoolAwareFutureTask<T>(this, runnable, value);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new ThreadPoolAwareFutureTask<T>(this, callable);
}
@Override
public void execute(Runnable command) {
if (command instanceof KnowsExecutor) {
((KnowsExecutor) command).setExecutorThisTaskIsScheduledFor(this);
}
super.execute(command);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
if (logger.isLoggable(Level.FINE)) {
startTimePoints.put(r, MillisecondsTimePoint.now());
}
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (logger.isLoggable(Level.FINE)) {
final TimePoint end = MillisecondsTimePoint.now();
final TimePoint startTime = startTimePoints.remove(r);
Runnable theRunnableToLog = wrappedRunnables.get(r);
if (theRunnableToLog == null) {
theRunnableToLog = r;
}
if (startTime != null) {
final Duration duration = startTime.until(end);
if (duration.compareTo(DURATION_LOGGING_THRESHOLD) > 0) {
logger.fine("Runnable "+theRunnableToLog+" took "+duration+" to complete in executor "+this+(t==null?"":("It failed with exception "+t)));
}
} else {
logger.fine("Strange internal problem: had expected to find start time for "+r);
}
}
super.afterExecute(r, t);
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
final ScheduledFuture<?> result = super.schedule(command, delay, unit);
if (logger.isLoggable(Level.FINE)) {
// remember which runnable was wrapped by the future because it will have been nulled out when afterExecute is called
wrappedRunnables.put(result, command);
}
return result;
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
final ScheduledFuture<?> result = super.scheduleAtFixedRate(command, initialDelay, period, unit);
if (logger.isLoggable(Level.FINE)) {
// remember which runnable was wrapped by the future because it will have been nulled out when afterExecute is called
wrappedRunnables.put(result, command);
}
return result;
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
final ScheduledFuture<?> result = super.scheduleWithFixedDelay(command, initialDelay, delay, unit);
if (logger.isLoggable(Level.FINE)) {
// remember which runnable was wrapped by the future because it will have been nulled out when afterExecute is called
wrappedRunnables.put(result, command);
}
return result;
}
@Override
public String toString() {
return super.toString() + "[name=" + name + "]";
}
}
@@ -0,0 +1,62 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.util.ThreadPoolUtil;
/**
* A future task that outputs trace messages to the log after {@link #MILLIS_AFTER_WHICH_TO_TRACE_NON_RETURNING_GET} milliseconds
* of a {@link #get()} call not returning. When used in conjunction with a {@link NamedTracingScheduledThreadPoolExecutor} such as
* the ones returned by {@link ThreadPoolUtil}, the task will be updated by the executor when added to the queue. When the {@link #get()}
* method doesn't respond after the timeout period, the executor that scheduled this tasks, its name as well as its queue size
* are traced into the log.
*
* @author Axel Uhl (d043530)
*
* @param <V>
*/
public class ThreadPoolAwareFutureTask<V> extends FutureTask<V> implements KnowsExecutor {
private static final Logger logger = Logger.getLogger(ThreadPoolAwareFutureTask.class.getName());
private final KnowsExecutorAndTracingGet<V> getHelper = new KnowsExecutorAndTracingGetImpl<V>();
private final Object callableOrRunnableIfLoggingFine;
public ThreadPoolAwareFutureTask(ThreadPoolExecutor executor, Callable<V> callable) {
super(callable);
if (logger.isLoggable(Level.FINE)) {
callableOrRunnableIfLoggingFine = callable;
} else {
callableOrRunnableIfLoggingFine = null;
}
getHelper.setExecutorThisTaskIsScheduledFor(executor);
}
public ThreadPoolAwareFutureTask(ThreadPoolExecutor executor, Runnable runnable, V result) {
super(runnable, result);
if (logger.isLoggable(Level.FINE)) {
callableOrRunnableIfLoggingFine = runnable;
} else {
callableOrRunnableIfLoggingFine = null;
}
getHelper.setExecutorThisTaskIsScheduledFor(executor);
}
@Override
public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) {
this.getHelper.setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor);
}
@Override
public V get() throws InterruptedException, ExecutionException {
return getHelper.callGetAndTraceAfterEachTimeout(this);
}
@Override
public String toString() {
return super.toString()+(callableOrRunnableIfLoggingFine==null?"":(" "+callableOrRunnableIfLoggingFine.toString()));
}
}
@@ -0,0 +1,53 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class ThreadPoolAwareRunnableScheduledFutureDelegate<V> extends KnowsExecutorAndTracingGetImpl<V> implements KnowsExecutorAndTracingGet<V>, RunnableScheduledFuture<V> {
private final RunnableScheduledFuture<V> future;
public ThreadPoolAwareRunnableScheduledFutureDelegate(ThreadPoolExecutor executor, RunnableScheduledFuture<V> future) {
this.future = future;
setExecutorThisTaskIsScheduledFor(executor);
}
public long getDelay(TimeUnit unit) {
return future.getDelay(unit);
}
public void run() {
future.run();
}
public boolean isPeriodic() {
return future.isPeriodic();
}
public boolean cancel(boolean mayInterruptIfRunning) {
return future.cancel(mayInterruptIfRunning);
}
public int compareTo(Delayed o) {
return future.compareTo(o);
}
public boolean isCancelled() {
return future.isCancelled();
}
public boolean isDone() {
return future.isDone();
}
public V get() throws InterruptedException, ExecutionException {
return callGetAndTraceAfterEachTimeout(future);
}
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return future.get(timeout, unit);
}
}
@@ -1,21 +1,18 @@
package com.sap.sse.util.impl;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import com.sap.sse.util.ThreadPoolUtil;
public class ThreadPoolUtilImpl implements ThreadPoolUtil {
private static final int REASONABLE_THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()/2, 3);
private static final int REASONABLE_THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()-1, 3);
private final ScheduledExecutorService defaultBackgroundTaskThreadPoolExecutor;
private final ScheduledExecutorService defaultForegroundTaskThreadPoolExecutor;
public ThreadPoolUtilImpl() {
defaultBackgroundTaskThreadPoolExecutor = Executors.newScheduledThreadPool(/* corePoolSize */ REASONABLE_THREAD_POOL_SIZE,
new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true));
defaultForegroundTaskThreadPoolExecutor = Executors.newScheduledThreadPool(/* corePoolSize */ Math.max(Runtime.getRuntime().availableProcessors()-1, 6),
new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true));
defaultBackgroundTaskThreadPoolExecutor = createBackgroundTaskThreadPoolExecutor("Default background executor");
defaultForegroundTaskThreadPoolExecutor = createForegroundTaskThreadPoolExecutor(2*REASONABLE_THREAD_POOL_SIZE, "Default foreground executor");
}
@Override
@@ -29,18 +26,26 @@ public class ThreadPoolUtilImpl implements ThreadPoolUtil {
}
@Override
public ScheduledExecutorService createBackgroundTaskThreadPoolExecutor() {
return createThreadPoolExecutor(Thread.NORM_PRIORITY-1);
public ScheduledExecutorService createBackgroundTaskThreadPoolExecutor(String name) {
return createThreadPoolExecutor(name, Thread.NORM_PRIORITY-1);
}
@Override
public ScheduledExecutorService createForegroundTaskThreadPoolExecutor() {
return createThreadPoolExecutor(Thread.NORM_PRIORITY);
public ScheduledExecutorService createForegroundTaskThreadPoolExecutor(String name) {
return createThreadPoolExecutor(name, Thread.NORM_PRIORITY);
}
private ScheduledExecutorService createThreadPoolExecutor(final int priority) {
return Executors.newScheduledThreadPool(/* corePoolSize */ REASONABLE_THREAD_POOL_SIZE,
new ThreadFactoryWithPriority(priority, /* daemon */ true));
@Override
public ScheduledExecutorService createForegroundTaskThreadPoolExecutor(int size, String name) {
return createThreadPoolExecutor(name, Thread.NORM_PRIORITY, size);
}
private ScheduledExecutorService createThreadPoolExecutor(String name, final int priority) {
return createThreadPoolExecutor(name, priority, /* corePoolSize */ REASONABLE_THREAD_POOL_SIZE);
}
private ScheduledExecutorService createThreadPoolExecutor(String name, final int priority, final int size) {
return new NamedTracingScheduledThreadPoolExecutor(name, /* corePoolSize */ size, new ThreadFactoryWithPriority(priority, /* daemon */ true));
}
@Override
@@ -1,11 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="settings_category_energy_saving">Batterie Sparen</string>
<string name="energy_saving_enabled">Batteriesparmodus</string>
<string name="preference_energy_saving_is_enabled">Ein</string>
<string name="preference_energy_saving_is_disabled">Aus</string>
<string name="preference_energy_saving_summary_is_enabled">GPS Updates werden alle 30 Sekunden verschickt.</string>
<string name="preference_energy_saving_summary_is_disabled">GPS Updates werden jede Sekunde verschickt.</string>
<string name="preference_energy_saving_sending_interval">Sendeintervall für GPS-Updates auswählen</string>
<string name="preference_energy_saving_sending_interval_summary">Wählen Sie den größtmöglichen Wert, der dennoch eine dem Anwendungsfall entsprechend hohe Aktualisierungsrate ermöglicht.</string>
<string name="preference_energy_saving_enabled_automatically">Der Energiesparmodus wurde automatisch aktiviert, GPS Updates werden alle 20 Sekunden an den Server gesendet. Diese Einstellung wird überschrieben, wenn Sie oben ein anderes Intervall auswählen.</string>
<string name="preference_energy_saving_sending_interval_message">GPS Updates werden%1$s %2$s an den Server gesendet.</string>
<string name="preference_energy_saving_sending_interval_message_every">alle</string>
<string-array name="preference_energy_saving_sending_interval_entries">
<item>sofort</item>
<item>5 Sekunden</item>
<item>10 Sekunden</item>
<item>20 Sekunden</item>
<item>60 Sekunden</item>
</string-array>
<string name="preference_heading_with_declination_subtracted_is_enabled">Ein</string>
<string name="preference_heading_with_declination_subtracted_is_disabled">Aus</string>
<string name="preference_heading_with_declination_subtracted_summary_is_enabled">Anzeige: Missweisend</string>
@@ -78,5 +78,6 @@
<string name="upload_in_progress">Wird hochgeladen&#8230;</string>
<string name="upload_io_error">Fehler beim Hochladen: </string>
<string name="upload_retry">Fehler beim Hochladen.\nZum Wiederholen hier tippen.</string>
<string name="tracking_status_gps_found">GPS Signal gefunden!</string>
</resources>
@@ -7,14 +7,34 @@
<string name="app_folder" translatable="false">tracking</string>
<string name="preference_energy_saving_enabled_key" translatable="false">preferenceEnergySavingEnabled</string>
<string name="preference_energy_saving_override_key" translatable="false">preferenceEnergySavingOverride</string>
<string name="preference_heading_with_declination_subtracted_key" translatable="false">preferenceHeadingWithDeclinationSubtracted</string>
<string name="settings_category_energy_saving">Battery Saving</string>
<string name="energy_saving_enabled">Save Energy</string>
<string name="preference_energy_saving_is_enabled">On</string>
<string name="preference_energy_saving_is_disabled">Off</string>
<string name="preference_energy_saving_summary_is_enabled">GPS updates are being sent to server every 30 seconds.</string>
<string name="preference_energy_saving_summary_is_disabled">GPS updates are being sent to server every second.</string>
<string name="preference_energy_saving_sending_interval_key" translatable="false">msgSndInt</string>
<string name="preference_energy_saving_sending_interval_default" translatable="false">0</string>
<string name="preference_energy_saving_sending_interval">choose GPS update sending interval</string>
<string name="preference_energy_saving_sending_interval_summary">Choose a value as high as possible to preserve battery life while still maintaining a reasonable live experience, depending on your use case.</string>
<string name="preference_energy_saving_enabled_automatically">Energy saving mode has been enabled automatically, GPS updates are being sent to the server every 20 seconds. You can change that by selecting an interval above.</string>
<string name="preference_energy_saving_sending_interval_message">GPS updates will be sent to the server%1$s %2$s.</string>
<string name="preference_energy_saving_sending_interval_message_key">msgSndIntMessage</string>
<string name="preference_energy_saving_sending_interval_message_every">every</string>
<string-array name="preference_energy_saving_sending_interval_entries">
<item>immediately</item>
<item>5 seconds</item>
<item>10 seconds</item>
<item>20 seconds</item>
<item>60 seconds</item>
</string-array>
<string-array name="preference_energy_saving_sending_interval_values" translatable="false">
<item>0</item>
<item>5</item>
<item>10</item>
<item>20</item>
<item>60</item>
</string-array>
<string name="preference_gps_fix_interval_ms_key" translatable="false">gpsFixInterval</string>
<string name="preference_gps_fix_interval_ms_default" translatable="false">500</string>
@@ -19,4 +19,5 @@
<string name="tracking_status_no_gps_signal">No GPS Signal!</string>
<string name="desc_regatta_user_image">Regatta User Image</string>
<string name="desc_add_photo_button">Add Photo Button</string>
<string name="tracking_status_gps_found">GPS signal found!</string>
</resources>
@@ -3,14 +3,17 @@
<!-- Energy Saving -->
<PreferenceCategory android:title="@string/settings_category_energy_saving">
<SwitchPreference
android:defaultValue="false"
android:key="@string/preference_energy_saving_enabled_key"
android:switchTextOn="@string/preference_energy_saving_is_enabled"
android:switchTextOff="@string/preference_energy_saving_is_disabled"
android:summaryOn="@string/preference_energy_saving_summary_is_enabled"
android:summaryOff="@string/preference_energy_saving_summary_is_disabled"
android:title="@string/energy_saving_enabled"/>
<ListPreference
android:defaultValue="@string/preference_energy_saving_sending_interval_default"
android:key="@string/preference_energy_saving_sending_interval_key"
android:entryValues="@array/preference_energy_saving_sending_interval_values"
android:entries="@array/preference_energy_saving_sending_interval_entries"
android:title="@string/preference_energy_saving_sending_interval"
android:summary="@string/preference_energy_saving_sending_interval_summary"/>
<EditTextPreference
android:key="@string/preference_energy_saving_sending_interval_message_key"
android:selectable="false"/>
</PreferenceCategory>
<!-- Connection -->
@@ -68,7 +68,7 @@ public class TrackingService extends Service implements GoogleApiClient.Connecti
private int NOTIFICATION_ID = R.string.tracker_started;
public final static int UPDATE_INTERVAL_IN_MILLIS_DEFAULT = 1000;
private final static int UPDATE_INTERVAL_IN_MILLIS_POWERSAVE_MODE = 30000;
private final static int UPDATE_INTERVAL_IN_MILLIS_POWERSAVE_MODE = 20000;
private final static float BATTERY_POWER_SAVE_THRESHOLD = 0.2f;
private boolean initialLocation;
@@ -163,6 +163,7 @@ public class TrackingService extends Service implements GoogleApiClient.Connecti
prefs.setTrackerIsTracking(true);
prefs.setTrackerIsTrackingCheckinDigest(checkinDigest);
prefs.setEnergySavingOverride(false);
}
private void stopTracking() {
@@ -348,17 +349,18 @@ public class TrackingService extends Service implements GoogleApiClient.Connecti
}
/**
* Update whether message sending service should retry every 3 seconds or every 30.
* Update the interval in which message sending service should retry - possibly enable power saving
*/
private void updateResendIntervalSetting() {
float batteryPct = getBatteryPercentage();
boolean batteryIsCharging = prefs.getBatteryIsCharging();
int updateInterval = UPDATE_INTERVAL_IN_MILLIS_DEFAULT;
if (prefs.getEnergySavingEnabledByUser() || (batteryPct < BATTERY_POWER_SAVE_THRESHOLD && !batteryIsCharging)) {
int updateInterval = prefs.getMessageSendingIntervalInMillis();
if (batteryPct < BATTERY_POWER_SAVE_THRESHOLD && !batteryIsCharging && !prefs.getEnergySavingOverride()) {
if (BuildConfig.DEBUG) {
ExLog.i(this, "POWER-LEVELS", "in power saving mode");
}
updateInterval = UPDATE_INTERVAL_IN_MILLIS_POWERSAVE_MODE;
prefs.setEnergySavingEnabledAutomatically(true);
} else {
if (BuildConfig.DEBUG) {
ExLog.i(this, "POWER-LEVELS", "in default power mode");
@@ -7,11 +7,11 @@ import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.sap.sailing.android.shared.logging.ExLog;
import com.sap.sailing.android.shared.services.sending.MessageSendingService.APIConnectivity;
@@ -32,6 +32,8 @@ public class TrackingFragment extends BaseFragment {
private AppPreferences prefs;
private long lastGPSQualityUpdate;
private Toast gpsToast;
private boolean gpsFound = true;
private String TAG = TrackingFragment.class.getName();
@@ -69,6 +71,9 @@ public class TrackingFragment extends BaseFragment {
qualityIndicator.setSignalQuality(savedInstanceState.getInt(SIS_GPS_QUALITY));
accuracyText.setText(savedInstanceState.getString(SIS_GPS_ACCURACY));
unsentFixesText.setText(savedInstanceState.getString(SIS_GPS_UNSENT_FIXES));
} else {
//initially set quality to "No GPS" on start tracking
updateTrackingStatus(GPSQuality.noSignal);
}
}
@@ -115,9 +120,22 @@ public class TrackingFragment extends BaseFragment {
if (quality == GPSQuality.noSignal) {
textView.setText(getString(R.string.tracking_status_no_gps_signal));
textView.setTextColor(getResources().getColor(R.color.sap_red));
//either GPS has been found before or tracking just started (but is not available now) --> show noGPS Toast
if (gpsFound || gpsToast == null) {
gpsToast = Toast.makeText(getActivity(),getString(R.string.tracking_status_no_gps_signal), Toast.LENGTH_LONG);
gpsToast.show();
gpsFound = false;
}
} else {
textView.setText(getString(R.string.tracking_status_tracking));
textView.setTextColor(getResources().getColor(R.color.fiori_text_color));
gpsToast.cancel();
//GPS was lost before but is available again now --> show gpsFound toast
if (!gpsFound) {
gpsToast = Toast.makeText(getActivity(),getString(R.string.tracking_status_gps_found), Toast.LENGTH_SHORT);
gpsToast.show();
gpsFound = true;
}
}
}
}
@@ -134,9 +152,9 @@ public class TrackingFragment extends BaseFragment {
public void run() {
TextView textView = (TextView) getActivity().findViewById(R.id.mode);
if (apiConnectivity == APIConnectivity.transmissionSuccess) {
if (prefs.getEnergySavingEnabledByUser()) {
if (prefs.getEnergySavingEnabledAutomatically() && !prefs.getEnergySavingOverride()) {
textView.setText(getString(R.string.tracking_mode_battery_saving));
textView.setTextColor(getResources().getColor(R.color.fiori_text_color));
textView.setTextColor(getResources().getColor(R.color.sap_red));
} else {
textView.setText(getString(R.string.tracking_mode_live));
textView.setTextColor(getResources().getColor(R.color.fiori_text_color));
@@ -183,7 +201,7 @@ public class TrackingFragment extends BaseFragment {
} else {
locationProviders = Settings.Secure.getString(context.getContentResolver(),
Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
return !TextUtils.isEmpty(locationProviders);
return locationProviders.contains("gps");
}
} else {
return false;
@@ -1,7 +1,9 @@
package com.sap.sailing.android.tracking.app.ui.fragments.preference;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.SwitchPreference;
import com.sap.sailing.android.shared.ui.fragments.preference.BasePreferenceFragment;
@@ -9,8 +11,7 @@ import com.sap.sailing.android.shared.util.UniqueDeviceUuid;
import com.sap.sailing.android.tracking.app.R;
import com.sap.sailing.android.tracking.app.utils.AppPreferences;
public class GeneralPreferenceFragment extends BasePreferenceFragment {
public class GeneralPreferenceFragment extends BasePreferenceFragment implements OnPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -24,23 +25,83 @@ public class GeneralPreferenceFragment extends BasePreferenceFragment {
if (currentDeviceId != null && currentDeviceId.length() > 0) {
deviceIdPreference.setSummary(currentDeviceId);
} else {
// this would, in most caes, be set elsewhere, but since we don't want to show an empty summary,
// this would, in most cases, be set elsewhere, but since we don't want to show an empty summary,
// we can just set it here. (UniqueDeviceUuid.getUniqueId sets it in prefs.)
String newDeviceId = UniqueDeviceUuid.getUniqueId(getActivity());
deviceIdPreference.setSummary(newDeviceId);
}
ListPreference prefSendingInterval = findPreference(R.string.preference_energy_saving_sending_interval_key);
prefSendingInterval.setDefaultValue(R.string.preference_energy_saving_sending_interval_default);
prefSendingInterval.setOnPreferenceChangeListener(this);
updateEnergySavingMessage(null);
}
@Override
public void onPause() {
super.onPause();
SwitchPreference prefEnergy = findPreference(R.string.preference_energy_saving_enabled_key);
//SwitchPreference prefEnergy = findPreference(R.string.preference_energy_saving_enabled_key);
SwitchPreference prefDeclination = findPreference(R.string.preference_heading_with_declination_subtracted_key);
AppPreferences prefs = new AppPreferences(getActivity());
prefs.setEnergySavingEnabledByUser(prefEnergy.isChecked());
//prefs.setEnergySavingEnabledByUser(prefEnergy.isChecked());
prefs.setDisplayHeadingWithSubtractedDeclination(prefDeclination.isChecked());
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
AppPreferences prefs = new AppPreferences(getActivity());
//GPS message sending interval settings
if(preference.getKey().equals(getString(R.string.preference_energy_saving_sending_interval_key))) {
int msgSndInt = Integer.parseInt((String)newValue);
switch (msgSndInt){
case 0:
//send every second
prefs.setMessageResendIntervalInMillis(1000);
break;
default:
prefs.setMessageResendIntervalInMillis(msgSndInt * 1000);
break;
}
if(prefs.getEnergySavingEnabledAutomatically())
prefs.setEnergySavingOverride(true);
updateEnergySavingMessage(newValue.toString());
}
return true;
}
private void updateEnergySavingMessage(String value){
Preference msgSndIntMessage = findPreference(getString(R.string.preference_energy_saving_sending_interval_message_key));
ListPreference msgSndIntPreference = (ListPreference) findPreference(getString(R.string.preference_energy_saving_sending_interval_key));
//when value == null, (meaning the settings screen just launched) - current entry will be selected
//otherwise, we need to get the index of the newly selected entry as the one resulting from getEntry() will be the previous one
String entry;
if (value == null){
entry = msgSndIntPreference.getEntry().toString();
} else {
int indexOfEntry = msgSndIntPreference.findIndexOfValue(value);
entry = msgSndIntPreference.getEntries()[indexOfEntry].toString();
}
//possibly fill in the word "every"
String fillEvery = (entry.equals(msgSndIntPreference.getEntries()[0])) ? "" : " " + getString(R.string.preference_energy_saving_sending_interval_message_every);
//set Text
AppPreferences prefs = new AppPreferences(getActivity());
if(prefs.getEnergySavingEnabledAutomatically() && !prefs.getEnergySavingOverride()){
msgSndIntMessage.setSummary(getString(R.string.preference_energy_saving_enabled_automatically));
} else {
msgSndIntMessage.setSummary(String.format(
getString(R.string.preference_energy_saving_sending_interval_message),
fillEvery,
entry)
);
}
}
}
@@ -112,15 +112,24 @@ public class AppPreferences extends BaseAppPreferences {
preferences.edit().putString(context.getString(R.string.preference_competitor_key), id).commit();
}
public void setEnergySavingEnabledByUser(boolean newValue) {
public void setEnergySavingEnabledAutomatically(boolean newValue) {
preferences.edit().putBoolean(context.getString(R.string.preference_energy_saving_enabled_key), newValue)
.commit();
}
public boolean getEnergySavingEnabledByUser() {
public boolean getEnergySavingEnabledAutomatically() {
return preferences.getBoolean(context.getString(R.string.preference_energy_saving_enabled_key), false);
}
public void setEnergySavingOverride(boolean newValue) {
preferences.edit().putBoolean(context.getString(R.string.preference_energy_saving_override_key), newValue)
.commit();
}
public boolean getEnergySavingOverride() {
return preferences.getBoolean(context.getString(R.string.preference_energy_saving_override_key), false);
}
public void setDisplayHeadingWithSubtractedDeclination(boolean newValue) {
preferences.edit()
.putBoolean(context.getString(R.string.preference_heading_with_declination_subtracted_key), newValue)
@@ -186,4 +195,5 @@ public class AppPreferences extends BaseAppPreferences {
public void removeFailedUpload(String key) {
pref.edit().remove(key).commit();
}
}
@@ -9,6 +9,7 @@
<string name="reset_position">Position zurücksetzen</string>
<string name="getting_marks">Empfangen der Markierungen&#8230;</string>
<string name="set">Gesetzt</string>
<string name="unknown">Unbekannt</string>
<string name="not_set">Nicht gesetzt</string>
<string name="mark_list_accuracy">(Genauigkeit ca. %1$sm)</string>
<string name="accuracy">GPS Genauigkeit</string>
@@ -12,6 +12,7 @@
<string name="reset_position">Reset Position</string>
<string name="getting_marks">Getting race marks&#8230;</string>
<string name="set">set</string>
<string name="unknown">n/a</string>
<string name="not_set">not set</string>
<string name="mark_list_accuracy">(Accurary ca. %1$sm)</string>
<string name="buoy_detail_accuracy_ca" translatable="false">ca. %1$.02fm</string>
@@ -33,8 +33,16 @@ public class MarkAdapter extends ResourceCursorAdapter {
} else {
final DecimalFormat df = new DecimalFormat("#.##");
final double accuracy = markPings.get(0).getAccuracy();
final String accuracyString = String.format(context.getString(R.string.mark_list_accuracy), df.format(accuracy));
setText = context.getString(R.string.set) + " " + accuracyString;
String accuracyString= "";
//accuracy-values that are stored as -1 will be displayed as "n/a"
if(accuracy == MarkPingInfo.NOT_SET_BY_USER){
accuracyString = context.getString(R.string.unknown);
}
else {
accuracyString = context.getString(R.string.set) + " " +
String.format(context.getString(R.string.mark_list_accuracy), df.format(accuracy));
}
setText = accuracyString;
}
markName.setText(name);
markSet.setText(setText);
@@ -55,7 +55,6 @@ public class BuoyFragment extends BaseFragment
private static final String TAG = BuoyFragment.class.getName();
private static final int GPS_MIN_TIME = 1000;
private static final String N_A = "n/a";
private static final int REQUEST_CHECK_SETTINGS = 1 << 16;
private TextView accuracyTextView;
@@ -230,8 +229,8 @@ public class BuoyFragment extends BaseFragment
}
private void setUpTextUI(Location location) {
String accuracyText = N_A;
String distanceText = N_A;
String accuracyText = this.getString(R.string.unknown);
String distanceText = this.getString(R.string.unknown);
if (location != null) {
accuracyText = getString(R.string.buoy_detail_accuracy_ca, location.getAccuracy());
MarkPingInfo markPing = positioningActivity.getMarkPing();
@@ -189,8 +189,17 @@ public class CheckinManager {
org.json.simple.JSONObject simplePosition;
simplePosition = JsonHelper.convertToSimple(positionJson);
GPSFix gpsFix = deserializer.deserialize(simplePosition);
MarkPingInfo ping = new MarkPingInfo(mark.getId(), gpsFix, positionJson.getDouble(FlatGPSFixJsonSerializer.FIELD_ACCURACY));
//accepts JSON messages without accuracy and with, without will be displayed as "n/a"
MarkPingInfo ping;
if (positionJson.has(FlatGPSFixJsonSerializer.FIELD_ACCURACY)){
ping = new MarkPingInfo(mark.getId(), gpsFix, positionJson.getDouble(FlatGPSFixJsonSerializer.FIELD_ACCURACY));
}
else {
ping = new MarkPingInfo(mark.getId(), gpsFix, MarkPingInfo.NOT_SET_BY_USER);
}
if (ping != null){
pings.add(ping);
}
}
}
marks.add(mark);
@@ -9,6 +9,8 @@ public class MarkPingInfo {
private final GPSFix gpsFix;
private final double accuracy;
public static final int NOT_SET_BY_USER = -1; // this is used for accuracies that are displayed as "n/a"
public MarkPingInfo(Serializable markId, GPSFix gpsFix, double accuracy) {
super();
this.markId = markId;
+6
View File
@@ -397,6 +397,12 @@ cp -rf ~/servers/server/logs/* /var/log/old/<event-name>/<instance-public-ipv4>/
- Or you manually force a logrotate run with `logrotate --force /etc/logrotate.d/httpd`, which copies `/var/log/httpd/` towards `/var/log/old/<event-name>/<instance-public-ipv4>`
- Once all this is done, you can go ahead and terminate the instance via AWS
### Comparing Server Content after Master Data Import
The script ``java/target/compareServers`` helps comparing server content after master data import. Run with two server URLs you want to compare, ideally in an empty directory where file downloads can be stored. Run initially with the ``-elv`` option to get verbose output. Make sure you have your ``http_proxy`` and ``https_proxy`` environment variables set or unset, depending on your network environment. Should the initial comparison fail, analyze the differences and continue by using ``-cel`` as command line arguments, telling the script to continue where it left off, exiting when hitting a difference and listing the leaderboard groups currently being compared. Repeat until done.
Should you want to compare servers of which you know they have different sets of leaderboard groups, start with ``compareServers -elv`` and then manually adjust the ``leaderboardgroups.new.sed`` and ``leaderboardgroups.old.sed`` files according to your needs, then continue with the ``-cel`` switches to restrict comparisons to what you left in the ``leaderboardgroups.*.sed`` files.
## Migration Checklist
### Before switching sapsailing.com to the EC2 webserver
+31
View File
@@ -0,0 +1,31 @@
This article can be used to store information on the data consumption that can be experienced with the SAP Sailing Analytics Android apps.
## Android apps
### Sail InSight
Energy consumption of the tracking itself is - without any optimization and compression of the messages at the time of writing this - quite low already. Ten hours of tracking mean less than five MB of data volume for the messages themselve, if you choose to send them immediately. If sent in intervalls you can minimize the size of the messages further, since you do not have to sent the devices UUID with every fix. Sending every second message together with the previous one will theoretically equal to around three to four MB of data volume needed for ten hours of tracking.
It is technically possible to compress the sent JSON messages with GZIP. Even if every message is sent individually you can achieve a compression ratio of ~103% which on it's own is not a lot, but at least it does not make it worse. Where it would really come in handy is when several messages are sent together. At a 5 second sending intervall, you can already achieve compression ratios of 300% and more. The challenge with sending GZIP-compressed JSON messages to the server is that this has to be supported by the webserver. Normally only messages sent from the server to the client can contain compressed content. There are ways to enable Apaches to receive compressed content but that probably is a lot of work.
A better idea to cut down on data consumption of the fixes messages in the InSight app is to assemble byte messages directly instead of taking JSON messages and encoding them with UTF-8. You can simply assemble a DataOutputStream / ByteArrayOutputStream with the UUID as 2 64-bit longs at the beginning and then append the values of all the parameters in a certain order with certain data types of certain lengths. On the server-side you can then read that stream of data, extract the first 16 bytes as UUID and then split the rest of the data into the fixes again.
On [Bug 3933](https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=3933) you can already find an implementation for assembling such a byte message. We already evaluated that you can save more than two thirds of the message length by doing this.
This only has to be implemented further down to the point where the data is sent and on the server side, possibly with a parameter in the URL to enable the servers to accept both JSON and direct byte messages, an implementation to split them apart again.
Since the app not only produces traffic when tracking, but also when checking in to a event or reopening that, when loading and sending team photos etc. energy consumption is significantly higher than the previously mentioned values. Since we cannot determine how often users will open and close the app, open the regattas, refresh photos etc. we cannot give good estimations for real world use cases even if we knew how much data volume every single one of these actions costs.
What we can say is, that testing the app extensively over a time of four weeks with booking into events probably more than 100 times and tracking way more than 30 hours caused around 50 to 100 MB of traffic on the testing phones.
Under normal conditions the traffic for one event should stay well below the former of these numbers.
### Buoy Pinger app
no information yet
### Racecommittee app
no information yet
## iOS apps
### Sail InSight
no information yet
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more