diff --git a/home.md b/home.md index 0d852aee27c..2ff9a805a82 100644 --- a/home.md +++ b/home.md @@ -1,146 +1,148 @@ -# Welcome to the SAP Sailing Wiki - -This is the Wiki where useful information regarding this project can be found. - -### The Pitch - -Like businesses, sailors need the latest information to make strategic decisions - but they need it even faster. One wrong tack, a false estimation of the current, or the slightest wind shift can cost the skipper the entire race. As premium sponsor of the Kieler Woche 2011, and co-sponsor of Sailing Team Germany (STG), SAP is showing how innovative IT solutions providing real time data analysis can give teams the competitive edge. - -SAP is at the center of today’s technology revolution, developing innovations that not only help businesses run like never before, but also improve the lives of people everywhere. As market leader in enterprise application software, SAP SE (NYSE: SAP) helps companies of all sizes and industries run better. From back office to boardroom, warehouse to storefront, desktop to mobile device – SAP empowers people and organizations to work together more efficiently and use business insight more effectively to stay ahead of the competition. SAP applications and services enable more than 258,000 customers to operate profitably, adapt continuously, and grow sustainably. - -# Table of Contents - -## Information - -### General - -* [[Information about this Wiki and HowTo|wiki/info/general/wiki]] -* [[General Project Information|wiki/info/general/general-information]] -* [[Inventory|wiki/info/general/inventar-liste]] - -### Landscape and Development - -* [[Architecture and Infrastructure|wiki/info/landscape/architecture-and-infrastructure]] - * [[Runtime Environment|wiki/info/landscape/runtime-environment]] - * [[Basic architectual principles|wiki/info/landscape/basic-architectural-principles]] - * [[User Management|wiki/info/landscape/usermanagement]] - * [[Development Environment|wiki/info/landscape/development-environment]] - * [[Production Environment|wiki/info/landscape/production-environment]] - * [[Typical Development Scenarios|wiki/info/landscape/typical-development-scenarios]] -* [[RaceLog Tracking Server Architecture|wiki/info/landscape/server]] - * Environment Overview [[PDF|wiki/info/mobile/event-tracking/architecture.pdf]] | [[SVG|wiki/info/mobile/event-tracking/architecture.svg]] -* Amazon - * [[Amazon EC2|wiki/info/landscape/amazon-ec2]] - * [[EC2 Backup Strategy|wiki/info/landscape/amazon-ec2-backup-strategy]] - * [[Creating an EC2 image from scratch|wiki/info/landscape/creating-ec2-image-from-scratch]] - * [[EC2 mail relaying|wiki/info/landscape/mail-relaying]] -* [[Building and Deploying|wiki/info/landscape/building-and-deploying]] -* [[Data Mining Architecture|wiki/info/landscape/data-mining-architecture]] -* [[Typical Data Mining Scenarios|wiki/info/landscape/typical-data-mining-scenarios]] -* [[Create clickable UI prototypes with Axure|wiki/info/landscape/ui-clickable-prototypes]] -* [[Webdesign|wiki/info/landscape/webdesign]] - -### Mobile - -* [[Mobile Development|wiki/info/mobile/mobile-development]] -* Tracking App - * [[Tracking App Specification|wiki/info/mobile/app-spec/app-spec]] - * [[Event Tracking|wiki/info/mobile/event-tracking/event-tracking]] - * [[Steps for setting up Smartphone Tracking|wiki/info/mobile/smartphone-tracking-steps]] - * [[Tracking App Prototype Architecture|wiki/info/mobile/app]] -* Racecommittee App - * [[Racecommittee App|wiki/info/mobile/racecommittee-app]] - * [[Environment|wiki/info/mobile/racecommittee-app-environment]] - * [[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]] -* [[Mobile Sailing Analytics|wiki/info/mobile/mobilesailinganalytics]] -* [[Push Notifications|wiki/info/mobile/push-notifications]] - -### API - -* [[Web Services API|wiki/info/api/sailing-webservices]] -* [[API v1|wiki/info/api/api-v1]] -* [[Tracking App API|wiki/info/api/tracking-app-api]] -* [[Training API v1 draft|wiki/info/api/training-api-v1-draft]] - -### Security - * [[Fortify Tests|wiki/info/security/fortify]] - * [[SSL / HTTPS Support|wiki/info/security/ssl-support]] - -### Miscellaneous - -* [[Big Data - Numbers|wiki/info/misc/big-data-numbers]] -* [[Data Quality|wiki/info/misc/data-quality]] -* [[Sailing Domain Algorithms|wiki/info/misc/sailing-domain-algorithms]] -* [[Google Analytics (Web Page Tracking)|wiki/info/misc/ganalytics]] -* [[S3 Development Sample|wiki/info/misc/s3-sample]] - -* [[FAQ for League Operators|wiki/info/misc/league-operators-faq]] -* [[Prionet related information|wiki/info/misc/prionet-related-information]] -* [[TracTrac Workshop 2013|wiki/info/misc/tractracworkshop2013]] - -## HowTo - -* [[Onboarding|wiki/howto/onboarding]] - - -### Development - -* [[Create boat graphics for the 2D race viewer|wiki/howto/development/boatgraphicssvg]] -* [[Continuous Integration with Hudson/Jenkins|wiki/howto/development/ci]] -* [[Dispatch|wiki/howto/development/dispatch]] -* [[Working with GWT UI Binder|wiki/howto/development/gwt-ui-binder]] -* [[Java De(Serialization) and Circular Dependencies|wiki/howto/development/java-de-serialization-and-circular-dependencies]] -* [[JMX Support|wiki/howto/development/jmx]] -* [[Working with GWT Locally|wiki/howto/development/local-gwt]] -* [[Log File Analysis|wiki/howto/development/log-file-analysis]] -* [[UI Tests with Selenium|wiki/howto/development/selenium-ui-tests]] -* [[Profiling|wiki/howto/development/profiling]] -* [[Working with GWT Super Dev Mode|wiki/howto/development/super-dev-mode]] - -### For Event Managers - -* [[Set up local network with replication server|wiki/howto/eventmanagers/event-network-with-replica]] -* [[Operating Igtimi WindBots|wiki/howto/eventmanagers/windbot-operations]] -* [[Linking Race Videos|wiki/howto/eventmanagers/linking-race-videos]] -* [[Import official results|wiki/howto/eventmanagers/results-import]] - -### Setup - -* [[Configure Races on Server|wiki/howto/setup/configure-races-on-server]] -* [[Setting up internal Jenkins on SAP Monsoon|wiki/howto/setup/setting-up-jenkins-on-sap-monsoon]] - -### Miscellaneous - -* [[Cook Book|wiki/howto/misc/cook-book]] -* [[Polars|wiki/howto/misc/polars]] -* [[Server Replication|wiki/howto/misc/server-replication]] -* [[TracTrac|wiki/howto/misc/tractrac-lifecycle]] -* [[UI Tests|wiki/howto/misc/ui-tests-tutorial]] -* [[Uploading Media Content|wiki/howto/misc/uploading-media-content]] -* [[Monitoring Apache and RabbitMQ|wiki/misc/monitoring-apache-and-rabbitmq]] - -## Projects -* [[Analytics on a stick|wiki/projects/analytics-on-a-stick]] - -## Events and Planning -* [[Project Planning (bigger development)|wiki/events/planning]] -* [[General Event Planning|wiki/events/general-event-planning]] -* [[Information about Extreme Sailing Series|wiki/events/extreme-sailing-series]] -* [[Travemünder Woche 2014 event page|wiki/events/tw2014]] -* [[505 worlds Kiel 2014 event page|wiki/events/505-worlds-kiel-2014]] -* [[Kieler Woche event page|wiki/events/kieler-woche-2015]] -* [[Charleston Race Week 2016|wiki/events/Charleston-Race-Week-2016]] -* [[Sailing Leagues 2016|wiki/events/sailing-Leagues-2016]] - -## Planning -* [[Overview|https://wiki.sapsailing.com/pages/wiki/planning/]] - -## Internal services (not related to wiki but useful) - -* [Bugzilla Issue Tracking System](http://bugzilla.sapsailing.com/bugzilla/) -* [GIT Repository (SAP)](ssh://git.wdf.sap.corp:29418/SAPSail/sapsailingcapture.git) -* [Maven Repository Browser](http://maven.sapsailing.com/maven/) (see [[how to setup repository for Android builds|wiki/info/mobile/racecommittee-app-environment]]) -* [Main Sailing Website](http://www.sapsailing.com) -* [Visitor Statistics](http://analysis.sapsailing.com/) \ No newline at end of file +# Welcome to the SAP Sailing Wiki + +This is the Wiki where useful information regarding this project can be found. + +### The Pitch + +Like businesses, sailors need the latest information to make strategic decisions - but they need it even faster. One wrong tack, a false estimation of the current, or the slightest wind shift can cost the skipper the entire race. As premium sponsor of the Kieler Woche 2011, and co-sponsor of Sailing Team Germany (STG), SAP is showing how innovative IT solutions providing real time data analysis can give teams the competitive edge. + +SAP is at the center of today’s technology revolution, developing innovations that not only help businesses run like never before, but also improve the lives of people everywhere. As market leader in enterprise application software, SAP SE (NYSE: SAP) helps companies of all sizes and industries run better. From back office to boardroom, warehouse to storefront, desktop to mobile device – SAP empowers people and organizations to work together more efficiently and use business insight more effectively to stay ahead of the competition. SAP applications and services enable more than 258,000 customers to operate profitably, adapt continuously, and grow sustainably. + +# Table of Contents + +## Information + +### General + +* [[Information about this Wiki and HowTo|wiki/info/general/wiki]] +* [[General Project Information|wiki/info/general/general-information]] +* [[Inventory|wiki/info/general/inventar-liste]] + +### Landscape and Development + +* [[Architecture and Infrastructure|wiki/info/landscape/architecture-and-infrastructure]] + * [[Runtime Environment|wiki/info/landscape/runtime-environment]] + * [[Basic architectual principles|wiki/info/landscape/basic-architectural-principles]] + * [[User Management|wiki/info/landscape/usermanagement]] + * [[Development Environment|wiki/info/landscape/development-environment]] + * [[Production Environment|wiki/info/landscape/production-environment]] + * [[Typical Development Scenarios|wiki/info/landscape/typical-development-scenarios]] +* [[RaceLog Tracking Server Architecture|wiki/info/landscape/server]] + * Environment Overview [[PDF|wiki/info/mobile/event-tracking/architecture.pdf]] | [[SVG|wiki/info/mobile/event-tracking/architecture.svg]] +* Amazon + * [[Amazon EC2|wiki/info/landscape/amazon-ec2]] + * [[EC2 Backup Strategy|wiki/info/landscape/amazon-ec2-backup-strategy]] + * [[Creating an EC2 image from scratch|wiki/info/landscape/creating-ec2-image-from-scratch]] + * [[EC2 mail relaying|wiki/info/landscape/mail-relaying]] +* [[Building and Deploying|wiki/info/landscape/building-and-deploying]] +* [[Data Mining Architecture|wiki/info/landscape/data-mining-architecture]] +* [[Typical Data Mining Scenarios|wiki/info/landscape/typical-data-mining-scenarios]] +* [[Create clickable UI prototypes with Axure|wiki/info/landscape/ui-clickable-prototypes]] +* [[Webdesign|wiki/info/landscape/webdesign]] + +### Mobile + +* [[Mobile Development|wiki/info/mobile/mobile-development]] +* Tracking App + * [[Tracking App Specification|wiki/info/mobile/app-spec/app-spec]] + * [[Event Tracking|wiki/info/mobile/event-tracking/event-tracking]] + * [[Steps for setting up Smartphone Tracking|wiki/info/mobile/smartphone-tracking-steps]] + * [[Tracking App Prototype Architecture|wiki/info/mobile/app]] +* Racecommittee App + * [[Racecommittee App|wiki/info/mobile/racecommittee-app]] + * [[Environment|wiki/info/mobile/racecommittee-app-environment]] + * [[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]] + +### API + +* [[Web Services API|wiki/info/api/sailing-webservices]] +* [[API v1|wiki/info/api/api-v1]] +* [[Tracking App API|wiki/info/api/tracking-app-api]] +* [[Training API v1 draft|wiki/info/api/training-api-v1-draft]] + +### Security + * [[Fortify Tests|wiki/info/security/fortify]] + * [[SSL / HTTPS Support|wiki/info/security/ssl-support]] + +### Miscellaneous + +* [[Big Data - Numbers|wiki/info/misc/big-data-numbers]] +* [[Data Quality|wiki/info/misc/data-quality]] +* [[Sailing Domain Algorithms|wiki/info/misc/sailing-domain-algorithms]] +* [[Google Analytics (Web Page Tracking)|wiki/info/misc/ganalytics]] +* [[S3 Development Sample|wiki/info/misc/s3-sample]] + +* [[FAQ for League Operators|wiki/info/misc/league-operators-faq]] +* [[Prionet related information|wiki/info/misc/prionet-related-information]] +* [[TracTrac Workshop 2013|wiki/info/misc/tractracworkshop2013]] + +## HowTo + +* [[Onboarding|wiki/howto/onboarding]] + + +### Development + +* [[Create boat graphics for the 2D race viewer|wiki/howto/development/boatgraphicssvg]] +* [[Continuous Integration with Hudson/Jenkins|wiki/howto/development/ci]] +* [[Dispatch|wiki/howto/development/dispatch]] +* [[Working with GWT UI Binder|wiki/howto/development/gwt-ui-binder]] +* [[Java De(Serialization) and Circular Dependencies|wiki/howto/development/java-de-serialization-and-circular-dependencies]] +* [[JMX Support|wiki/howto/development/jmx]] +* [[Working with GWT Locally|wiki/howto/development/local-gwt]] +* [[Log File Analysis|wiki/howto/development/log-file-analysis]] +* [[UI Tests with Selenium|wiki/howto/development/selenium-ui-tests]] +* [[Profiling|wiki/howto/development/profiling]] +* [[Working with GWT Super Dev Mode|wiki/howto/development/super-dev-mode]] + +### For Event Managers + +* [[Set up local network with replication server|wiki/howto/eventmanagers/event-network-with-replica]] +* [[Operating Igtimi WindBots|wiki/howto/eventmanagers/windbot-operations]] +* [[Linking Race Videos|wiki/howto/eventmanagers/linking-race-videos]] +* [[Import official results|wiki/howto/eventmanagers/results-import]] + +### Setup + +* [[Configure Races on Server|wiki/howto/setup/configure-races-on-server]] +* [[Setting up internal Jenkins on SAP Monsoon|wiki/howto/setup/setting-up-jenkins-on-sap-monsoon]] + +### Miscellaneous + +* [[Cook Book|wiki/howto/misc/cook-book]] +* [[Polars|wiki/howto/misc/polars]] +* [[Server Replication|wiki/howto/misc/server-replication]] +* [[TracTrac|wiki/howto/misc/tractrac-lifecycle]] +* [[UI Tests|wiki/howto/misc/ui-tests-tutorial]] +* [[Uploading Media Content|wiki/howto/misc/uploading-media-content]] +* [[Monitoring Apache and RabbitMQ|wiki/misc/monitoring-apache-and-rabbitmq]] + +## Projects +* [[Analytics on a stick|wiki/projects/analytics-on-a-stick]] + +## Events and Planning +* [[Project Planning (bigger development)|wiki/events/planning]] +* [[General Event Planning|wiki/events/general-event-planning]] +* [[Information about Extreme Sailing Series|wiki/events/extreme-sailing-series]] +* [[Travemünder Woche 2014 event page|wiki/events/tw2014]] +* [[505 worlds Kiel 2014 event page|wiki/events/505-worlds-kiel-2014]] +* [[Kieler Woche event page|wiki/events/kieler-woche-2015]] +* [[Charleston Race Week 2016|wiki/events/Charleston-Race-Week-2016]] +* [[Sailing Leagues 2016|wiki/events/sailing-Leagues-2016]] + +## Planning +* [[Overview|https://wiki.sapsailing.com/pages/wiki/planning/]] + +## Internal services (not related to wiki but useful) + +* [Bugzilla Issue Tracking System](http://bugzilla.sapsailing.com/bugzilla/) +* [GIT Repository (SAP)](ssh://git.wdf.sap.corp:29418/SAPSail/sapsailingcapture.git) +* [Maven Repository Browser](http://maven.sapsailing.com/maven/) (see [[how to setup repository for Android builds|wiki/info/mobile/racecommittee-app-environment]]) +* [Main Sailing Website](http://www.sapsailing.com) +* [Visitor Statistics](http://analysis.sapsailing.com/) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings index 14e49d15af8..6e3a025ff81 100644 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings and b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings differ diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoSensorDataMetadata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoSensorDataMetadata.java index c7a7e10f408..969737c9f5e 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoSensorDataMetadata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/sensordata/BravoSensorDataMetadata.java @@ -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 getColumns() { return columns; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/DoubleVectorFixImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/DoubleVectorFixImpl.java index 67edd307e56..9c6b1ca99f3 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/DoubleVectorFixImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tracking/impl/DoubleVectorFixImpl.java @@ -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; + } } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoSensorFixStoreImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoSensorFixStoreImpl.java index 48b3411d21a..c7e72a6e978 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoSensorFixStoreImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoSensorFixStoreImpl.java @@ -125,56 +125,53 @@ public class MongoSensorFixStoreImpl implements MongoSensorFixStore { */ @Override public void storeFixes(DeviceIdentifier device, Iterable fixes) { - if (Util.isEmpty(fixes)) { - return; - } - try { - final Object dbDeviceId = storeDeviceId(deviceServiceFinder, device); - final int nrOfTotalFixes = Util.size(fixes); - final ArrayList dbFixes = new ArrayList<>(nrOfTotalFixes); - - TimePoint newFrom = null; - TimePoint newTo = null; - for (FixT fix : fixes) { - String type = fix.getClass().getName(); - FixMongoHandler 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 (!Util.isEmpty(fixes)) { + try { + final Object dbDeviceId = storeDeviceId(deviceServiceFinder, device); + final int nrOfTotalFixes = Util.size(fixes); + final ArrayList dbFixes = new ArrayList<>(nrOfTotalFixes); + + TimePoint newFrom = null; + TimePoint newTo = null; + for (FixT fix : fixes) { + String type = fix.getClass().getName(); + FixMongoHandler 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 diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java index ed21eaf7f14..cef4a6b47c7 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java @@ -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) { diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java new file mode 100644 index 00000000000..0fe9590a085 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreAndLoadTest.java @@ -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. 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 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 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 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 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 > SensorFixMapper createCompetitorMapper( + Class> 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 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 extends SensorFixTrackImpl { + 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, Competitor> { + + @Override + public DynamicSensorFixTrack getTrack(DynamicTrackedRace race, Competitor key) { + return race.getOrCreateSensorTrack(key, TestFixTrackImpl.TRACK_NAME, + () -> new TestFixTrackImpl(key, TestFixTrackImpl.TRACK_NAME)); + } + + @Override + public TestFixImpl map(DoubleVectorFix fix) { + return new TestFixImpl(fix); + } + + @Override + public boolean isResponsibleFor(Class> eventType) { + return RegattaLogDeviceCompetitorTestMappingEventImpl.class.isAssignableFrom(eventType); + } + } + + public class RegattaLogDeviceCompetitorTestMappingEventImpl extends RegattaLogDeviceMappingEventImpl + 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); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreTest.java new file mode 100644 index 00000000000..eb670d2c9cf --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/SensorFixStoreTest.java @@ -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 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 listener1 = mockFixReceivedListener(); + FixReceivedListener 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 listener = mockFixReceivedListener(); + store.addListener(listener, device); + addBravoFix(device2, FIX_TIMESTAMP, FIX_RIDE_HEIGHT); + + Mockito.verifyZeroInteractions(listener); + } + + @Test + public void testOnlyListenerForMatchingDeviceIsNotified() throws Exception { + FixReceivedListener listener1 = mockFixReceivedListener(); + FixReceivedListener 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 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 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 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 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 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 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 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 listener1 = mockFixReceivedListener(); + FixReceivedListener 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 listener1 = mockFixReceivedListener(); + FixReceivedListener 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 mockFixReceivedListener() { + FixReceivedListener listener = Mockito.mock(FixReceivedListener.class); + return listener; + } + + private void verifySingleFix(Timed expectedFix, long start, long end, DeviceIdentifier device, boolean inclusive) throws Exception { + List 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 loadedFixes = loadFixes(start, end, device, inclusive); + assertEquals(0, loadedFixes.size()); + } + + private List loadFixes(long start, long end, DeviceIdentifier device, boolean inclusive) + throws TransformationException { + List 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); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java index f7cc5ebfb8f..587b128c16d 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java @@ -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 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 mapping) { + if(timeRangeToLoad == null) { + return; + } if (preemptiveStopRequested.get()) { return; } @@ -194,8 +197,8 @@ public class FixLoaderAndTracker implements TrackingDataLoader { try { @SuppressWarnings({ "unchecked" }) DeviceMapping competitorMapping = (DeviceMapping) 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 markMapping = (DeviceMapping) 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 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 oldMapping, DeviceMappingWithRegattaLogEvent 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); + } } } } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java index 5efe9c08bf4..d0edf58d240 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RaceLogFixTrackerManager.java @@ -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; diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java index a7b5e30b265..e61d9abd4a9 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java @@ -48,7 +48,7 @@ public abstract class RegattaLogDeviceMappings { * sure to not produce a memory leak. */ private final Set knownRegattaLogs = new HashSet<>(); - + /** * Lock object to be used when accessing {@link #knownRegattaLogs}. */ @@ -127,16 +127,16 @@ public abstract class RegattaLogDeviceMappings { 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); } } - + /** * Calls the given callback for every known mapping that's currently known. * @@ -163,11 +163,11 @@ public abstract class RegattaLogDeviceMappings { public void forEachMapping(BiConsumer> callback) { LockUtil.executeWithReadLock(mappingsLock, () -> { for (Map.Entry>> entry : mappings.entrySet()) { - ItemT item = entry.getKey(); - for (DeviceMappingWithRegattaLogEvent mapping : entry.getValue()) { - callback.accept(item, mapping); - } + ItemT item = entry.getKey(); + for (DeviceMappingWithRegattaLogEvent mapping : entry.getValue()) { + callback.accept(item, mapping); } + } }); } @@ -187,13 +187,13 @@ public abstract class RegattaLogDeviceMappings { Consumer> callback) { LockUtil.executeWithReadLock(mappingsLock, () -> { List> mappingsForDevice = mappingsByDevice.get(device); - if (mappingsForDevice != null) { - for (DeviceMappingWithRegattaLogEvent mapping : mappingsForDevice) { - if (mapping.getTimeRange().includes(timePoint)) { - callback.accept(mapping); - } + if (mappingsForDevice != null) { + for (DeviceMappingWithRegattaLogEvent mapping : mappingsForDevice) { + if (mapping.getTimeRange().includes(timePoint)) { + callback.accept(mapping); } } + } }); } @@ -236,9 +236,7 @@ public abstract class RegattaLogDeviceMappings { * * @throws DoesNotHaveRegattaLogException */ - private final > 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 > void updateMappingsInternal() { final Map>> newMappings = calculateMappings(); final Map>> oldMappings = new HashMap<>(); LockUtil.lockForWrite(mappingsLock); @@ -261,18 +259,20 @@ public abstract class RegattaLogDeviceMappings { } finally { LockUtil.unlockAfterWrite(mappingsLock); } - if (loadIfNotCovered) { - calculateDiff(oldMappings, newMappings); - } + calculateDiff(oldMappings, newMappings); } + /** + * Calculates and applies 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>> previousMappings, Map>> newMappings) { Set itemsToProcess = new HashSet(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> oldMappings = previousMappings.containsKey(item) ? previousMappings.get(item) : Collections.emptyList(); @@ -281,22 +281,30 @@ public abstract class RegattaLogDeviceMappings { DeviceMappingWithRegattaLogEvent 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); } } } - + /** * Called when a {@link DeviceMapping} was removed. * * @param mapping the removed mapping */ protected abstract void mappingRemoved(DeviceMappingWithRegattaLogEvent mapping); + + private void mappingRemovedInternal(DeviceMappingWithRegattaLogEvent mapping) { + try { + mappingRemoved(mapping); + } catch(Exception e) { + logger.log(Level.SEVERE, "error while removing mapping " + mapping, e); + } + } /** * Called when a {@link DeviceMapping} was added. @@ -304,6 +312,14 @@ public abstract class RegattaLogDeviceMappings { * @param mapping the new mapping */ protected abstract void mappingAdded(DeviceMappingWithRegattaLogEvent mapping); + + private void mappingAddedInternal(DeviceMappingWithRegattaLogEvent 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. @@ -315,6 +331,15 @@ public abstract class RegattaLogDeviceMappings { protected abstract void mappingChanged(DeviceMappingWithRegattaLogEvent oldMapping, DeviceMappingWithRegattaLogEvent newMapping); + private void mappingChangedInternal(DeviceMappingWithRegattaLogEvent oldMapping, + DeviceMappingWithRegattaLogEvent newMapping) { + try { + mappingChanged(oldMapping, newMapping); + } catch(Exception e) { + logger.log(Level.SEVERE, "error while changing mapping old: " + oldMapping + "; new: " + newMapping, e); + } + } + private DeviceMappingWithRegattaLogEvent findAndRemoveMapping( DeviceMappingWithRegattaLogEvent mappingToFind, List> newItemsToProcess) { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/BaseRegattaLogDeviceMappingFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/BaseRegattaLogDeviceMappingFinder.java index 0399e83c387..76774039688 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/BaseRegattaLogDeviceMappingFinder.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/BaseRegattaLogDeviceMappingFinder.java @@ -65,7 +65,7 @@ public abstract class BaseRegattaLogDeviceMappingFinder 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())); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/WaypointImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/WaypointImpl.java index 7c262b59976..bb16d391cba 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/WaypointImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/WaypointImpl.java @@ -46,7 +46,7 @@ public class WaypointImpl implements Waypoint { @Override public String toString() { - return getName(); + return (getName()==null?"":getName()) + " ("+getPassingInstructions()+")"; } @Override diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java index b2ad7e825b0..113fa50e5ae 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java @@ -107,12 +107,12 @@ public class TrackImpl implements Track { } /** - * Callers that want to iterate over the collection returned need to synchronize on this 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 - * FixType 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 FixType may provide smoothening implementations. */ protected NavigableSet getInternalFixes() { NavigableSet result = getInternalRawFixes(); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrencyTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrencyTest.java index 0204088e717..e7d08ff101a 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrencyTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrencyTest.java @@ -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(teamAquaTrack, Executors.newFixedThreadPool(numberOfCPUs)); + dp = new DouglasPeucker(teamAquaTrack, ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor()); start = System.nanoTime(); List approximation2 = dp.approximate(new MeterDistance(3), firstMarkPassing.getTimePoint(), lastMarkPassing.getTimePoint()); duration = System.nanoTime()-start; diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogAttachOnTrackedRaceTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogAttachOnTrackedRaceTest.java new file mode 100644 index 00000000000..b9db265ecec --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogAttachOnTrackedRaceTest.java @@ -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 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 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)); + } + +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/tracking/impl/BravoFixTrackSerializationTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/tracking/impl/BravoFixTrackSerializationTest.java index 546eb8e0925..2a0f19e5877 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/tracking/impl/BravoFixTrackSerializationTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/tracking/impl/BravoFixTrackSerializationTest.java @@ -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); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceExecutionOrderProvider.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceExecutionOrderProvider.java index 07da1de7bed..34caffcaf8d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceExecutionOrderProvider.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/AbstractRaceExecutionOrderProvider.java @@ -43,6 +43,10 @@ public abstract class AbstractRaceExecutionOrderProvider implements RaceExecutio protected abstract Map> getRaceColumnsOfSeries(); + /** + * For all tracked races found on any of the {@link #getRaceColumnsOfSeries() race columns} produces a valid and + * potentially empty set. + */ private Map> reloadAndGetPreviousRacesByRace() { final Map> previousRacesByRace = new HashMap<>(); for (Entry> raceColumnsInSeries : getRaceColumnsOfSeries().entrySet()) { @@ -96,11 +100,13 @@ public abstract class AbstractRaceExecutionOrderProvider implements RaceExecutio @Override public Set getPreviousRacesInExecutionOrder(TrackedRace race) { - Set result = Collections.emptySet(); + final Set result; final Map> previousRacesByRace = getPreviousRacesByRace(); if (previousRacesByRace != null) { result = previousRacesByRace.get(race); - } + } else { + result = Collections.emptySet(); + } return result; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DouglasPeucker.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DouglasPeucker.java index 9971762470e..efd8c9a53a8 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DouglasPeucker.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DouglasPeucker.java @@ -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 { private final GPSFixTrack track; - private final Executor executor; + private final ScheduledExecutorService executor; public DouglasPeucker(GPSFixTrack track) { this.track = track; @@ -42,7 +39,7 @@ public class DouglasPeucker { * Allows clients to specify a non-default, optionally multi-threaded executor that will be used to perform the * high-effort computations. */ - public DouglasPeucker(GPSFixTrack track, Executor executor) { + public DouglasPeucker(GPSFixTrack track, ScheduledExecutorService executor) { this.track = track; this.executor = executor; } @@ -96,15 +93,8 @@ public class DouglasPeucker { if (fix.getTimePoint().compareTo(to) > 0) { break; } - RunnableFuture> crossTrackErrorFuture = new FutureTask>( - new Callable>() { - @Override - public Util.Pair call() throws Exception { - return new Util.Pair(fix, fix.getPosition().absoluteCrossTrackError( - fromPosition, bearing)); - } - }); - executor.execute(crossTrackErrorFuture); + Future> crossTrackErrorFuture = executor.submit( + ()->new Util.Pair(fix, fix.getPosition().absoluteCrossTrackError(fromPosition, bearing))); crossTrackErrorFutures.add(crossTrackErrorFuture); } for (Future> crossTrackErrorFuture : crossTrackErrorFutures) { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java index a4c9e684d01..eb6d8f44056 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java @@ -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(new Callable() { - @Override - public LeaderboardDTO call() throws Exception { - LeaderboardDTO result = leaderboard.computeDTO(adjustedTimePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, - waitForLatestAnalyses, trackedRegattaRegistry, baseDomainFactory, /* fillTotalPointsUncorrected */ false); - return result; - } - }); + future = new FutureTaskWithTracingGet(LeaderboardDTOCache.class.getName()+" for leaderboard "+leaderboard.getName(), + new Callable() { + @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); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index a60ec26b586..083e5915d18 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -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(); this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); this.raceColumnListeners = new RaceColumnListeners(); - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsAtEndOfTrackingCache = new HashMap<>(); initTransientFields(); } @@ -330,7 +330,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } private void initTransientFields() { - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsAtEndOfTrackingCache = new HashMap<>(); this.cacheInvalidationListeners = new HashSet(); // 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>> competitorsFromBestToWorstTasks = new HashMap<>(); + Map>> 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> task = new FutureTask>( + Future> 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>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { + for (Map.Entry>> 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> futuresForColumnName = new HashMap>(); final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); for (final RaceColumn raceColumn : this.getRaceColumns()) { - RunnableFuture future = new FutureTask(() -> { + Future 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> 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(new Callable() { + raceDetails = new FutureTaskWithTracingGet("RaceDetails for "+trackedRace, new Callable() { @Override public RaceDetails call() throws Exception { TimePoint end = trackedRace.getEndOfRace(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java index ed4b3280062..32f29af56d5 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java @@ -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> tasks = new ArrayList<>(); - for (Entry> c : combinedCompetitorFixes.entrySet()) { - tasks.add(Executors.callable(new ComputeMarkPassings(c.getKey(), c.getValue()))); + List> tasks = new ArrayList<>(); + for (final Entry> c : combinedCompetitorFixes.entrySet()) { + final Runnable runnable = new ComputeMarkPassings(c.getKey(), c.getValue()); + tasks.add(new Callable() { + @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 fixes; + final Competitor c; + final Iterable fixes; public ComputeMarkPassings(Competitor c, Iterable 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> 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> 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; + } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java index c15b26cfaa9..b82874e0663 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java @@ -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> tasks = new HashSet<>();; + final Set> tasks = new HashSet<>(); + final Thread executingThread = Thread.currentThread(); // most likely the MarkPassingCalculator.Listen thread for (Competitor c : race.getRace().getCompetitors()) { tasks.add(()->{ - List badCans = new ArrayList<>(); - List newCans = new ArrayList<>(); - for (Waypoint w : changedWaypoints) { - Map, Candidate> xteCans = getXteCandidates(c, w); - badCans.addAll(xteCans.values()); - xteCans.clear(); - Map 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 badCans = new ArrayList<>(); + List newCans = new ArrayList<>(); + for (Waypoint w : changedWaypoints) { + Map, Candidate> xteCans = getXteCandidates(c, w); + badCans.addAll(xteCans.values()); + xteCans.clear(); + Map distanceCans = getDistanceCandidates(c, w); + badCans.addAll(distanceCans.values()); + distanceCans.clear(); + } + Set allFixes = getAllFixes(c); + newCans.addAll(checkForDistanceCandidateChanges(c, allFixes, changedWaypoints).getA()); + newCans.addAll(checkForXTECandidatesChanges(c, allFixes, changedWaypoints).getA()); + result.put(c, new Util.Pair, List>(newCans, badCans)); + return null; + } finally { + LockUtil.unpropagateLockSetFrom(executingThread); // "return" the course read lock from the thread pool executor thread to the "calling" thread } - Set allFixes = getAllFixes(c); - newCans.addAll(checkForDistanceCandidateChanges(c, allFixes, changedWaypoints).getA()); - newCans.addAll(checkForXTECandidatesChanges(c, allFixes, changedWaypoints).getA()); - result.put(c, new Util.Pair, List>(newCans, badCans)); - return null; }); } executor.invokeAll(tasks); @@ -513,8 +520,8 @@ public class CandidateFinderImpl implements CandidateFinder { private Set getAllFixes(Competitor c) { Set fixes = new TreeSet(comp); DynamicGPSFixTrack 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 w. */ 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, Iterable>> 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(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/trackimport/DoubleVectorFixImporter.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/trackimport/DoubleVectorFixImporter.java index 6a44a51156b..0e1e75cdf6f 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/trackimport/DoubleVectorFixImporter.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/trackimport/DoubleVectorFixImporter.java @@ -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; /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRaceWithWindEssentials.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRaceWithWindEssentials.java index 5f84849d6a6..ce7bf54732c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRaceWithWindEssentials.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRaceWithWindEssentials.java @@ -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; } - - - } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java index b89c60c9421..2600557528a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java @@ -1075,8 +1075,10 @@ public class GPSFixTrackImpl extends MappedTra unlockAfterRead(); } } - for (GPSTrackListener listener : getListeners()) { - listener.gpsFixReceived(fix, getTrackedItem(), firstFixInTrack); + if (result) { + for (GPSTrackListener listener : getListeners()) { + listener.gpsFixReceived(fix, getTrackedItem(), firstFixInTrack); + } } return result; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java index d3f198f7562..4ce552698a5 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java @@ -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 track) { + // no action required + } } /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 8bb6f3e2098..9973e347fb1 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -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 * */ public void updateStartAndEndOfTracking(boolean waitForGPSFixesToLoad) { + final TimePoint oldStartOfTracking; + final TimePoint oldEndOfTracking; synchronized (updateStartAndEndOfTrackingMonitor) { final Pair 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(new Callable() { + newFuture = new FutureTaskWithTracingGet("getDirectionFromStartToNextMark for "+this, new Callable() { @Override public Wind call() { Wind result; diff --git a/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/ExpeditionWindTrackerFactory.java b/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/ExpeditionWindTrackerFactory.java index a30719ea1bf..961d030e3db 100755 --- a/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/ExpeditionWindTrackerFactory.java +++ b/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/ExpeditionWindTrackerFactory.java @@ -96,4 +96,9 @@ public class ExpeditionWindTrackerFactory implements WindTrackerFactory { windReceivers.remove(windTracker.getReceiver().getPort()); } } + + @Override + public String toString() { + return "ExpeditionWindTrackerFactory [defaultPort=" + defaultPort + "]"; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java index 6df38e1b7ed..bb42e9bafcb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java @@ -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 { } - @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(); - sapHeader = new SAPHeaderWithAuthentication(StringMessages.INSTANCE.sapSailingAnalytics(), - StringMessages.INSTANCE.autoplayConfiguration()); + sapHeader = new SAPSailingHeaderWithAuthentication(StringMessages.INSTANCE.autoplayConfiguration()); new FixedSailingAuthentication(userService, sapHeader.getAuthenticationMenuView()); eventSelectionBox = new ListBox(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java index d8df0b472aa..cb71942d676 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/shared/header/SAPHeaderComponent.java @@ -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 { 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); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/authentication/SAPSailingHeaderWithAuthentication.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/authentication/SAPSailingHeaderWithAuthentication.java new file mode 100644 index 00000000000..20101cccdec --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/authentication/SAPSailingHeaderWithAuthentication.java @@ -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); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java index f5653f8e579..00827fb9fa8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java @@ -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); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractFileImportWidget.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractFileImportWidget.java index 22f61ee64c9..d462a8ddd9a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractFileImportWidget.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractFileImportWidget.java @@ -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> callback); + protected void setShouldClearListOnNewUploadComplete(boolean shouldClearListOnNewUploadComplete) { + this.shouldClearListOnNewUploadComplete = shouldClearListOnNewUploadComplete; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index 00d781e3259..226aaa74264 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -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() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogImportSensorDataAndAddMappingsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogImportSensorDataAndAddMappingsDialog.java index 6691ddf6c7c..1db0f615e1e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogImportSensorDataAndAddMappingsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogImportSensorDataAndAddMappingsDialog.java @@ -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> competitorTable; private final StringMessages stringMessages; - private final Map 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> 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 void selectOrClear(SingleSelectionModel 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 getResult() { List 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; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SensorDataImportWidget.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SensorDataImportWidget.java index c6ff1885e74..20ec0834486 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SensorDataImportWidget.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SensorDataImportWidget.java @@ -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 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SimulationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SimulationPanel.java new file mode 100644 index 00000000000..ff4a621350f --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SimulationPanel.java @@ -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(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 416a780ba56..5e4e5272b6d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -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 racesFilterablePanel; private FlushableCellTable 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()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java index 12d730da684..b2217694978 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java @@ -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> { - - public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, - ErrorReporter errorReporter) { +public class TrackFileImportDeviceIdentifierTableWrapper extends + TableWrapper> { + private TextColumn trackNameColumn; + private final Map mappings = new HashMap<>(); + + public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService, + StringMessages stringMessages, ErrorReporter errorReporter) { super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true, new EntityIdentityComparator() { @Override @@ -21,54 +35,49 @@ RefreshableSingleSelectionModel> { TrackFileImportDeviceIdentifierDTO dto2) { return dto1.uuidAsString.equals(dto2.uuidAsString); } + @Override public int hashCode(TrackFileImportDeviceIdentifierDTO t) { return t.uuidAsString.hashCode(); } }); - TextColumn uuidColumn = new TextColumn() { @Override public String getValue(TrackFileImportDeviceIdentifierDTO object) { return object.uuidAsString; } }; - TextColumn fileNameColumn = new TextColumn() { @Override public String getValue(TrackFileImportDeviceIdentifierDTO object) { return object.fileName; } }; - - TextColumn trackNameColumn = new TextColumn() { + trackNameColumn = new TextColumn() { @Override public String getValue(TrackFileImportDeviceIdentifierDTO object) { return object.trackName; } }; - TextColumn fromColumn = new TextColumn() { @Override public String getValue(TrackFileImportDeviceIdentifierDTO object) { return DateAndTimeFormatterUtil.formatDateAndTime(object.from); } }; - TextColumn toColumn = new TextColumn() { @Override public String getValue(TrackFileImportDeviceIdentifierDTO object) { return DateAndTimeFormatterUtil.formatDateAndTime(object.to); } }; - TextColumn numberOfFixesColumn = new TextColumn() { @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> { table.addColumn(numberOfFixesColumn, "# fixes"); } + public void removeTrackNameColumn() { + table.removeColumn(trackNameColumn); + } + + class MappingTableBuilder extends DefaultCellTableBuilder { + 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 cellTable; + + public MappingTableBuilder(AbstractCellTable 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 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 getMappings() { + return mappings; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java index 47ceeda55ed..fdc69aa2711 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java @@ -1,71 +1,71 @@ -package com.sap.sailing.gwt.ui.leaderboardedit; - -import java.util.List; - -import com.google.gwt.dom.client.Style.Position; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RootPanel; -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.ui.client.AbstractSailingEntryPoint; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.security.ui.authentication.decorator.AuthorizedContentDecorator; -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 LeaderboardEditPage extends AbstractSailingEntryPoint { - @Override - protected void doOnModuleLoad() { - super.doOnModuleLoad(); - - sailingService.getLeaderboardNames(new MarkedAsyncCallback>( - new AsyncCallback>() { - @Override - public void onSuccess(List leaderboardNames) { - final String leaderboardName = Window.Location.getParameter("name"); - if (leaderboardNames.contains(leaderboardName)) { - - SAPHeaderWithAuthentication header = initHeader(); - GenericAuthentication genericSailingAuthentication = new FixedSailingAuthentication(getUserService(), header.getAuthenticationMenuView()); - AuthorizedContentDecorator authorizedContentDecorator = new GenericAuthorizedContentDecorator(genericSailingAuthentication); - authorizedContentDecorator.setPermissionToCheck(Permission.MANAGE_LEADERBOARD_RESULTS, SailingPermissionsForRoleProvider.INSTANCE); - authorizedContentDecorator.setContentWidgetFactory(new WidgetFactory() { - @Override - public Widget get() { - EditableLeaderboardPanel leaderboardPanel = new EditableLeaderboardPanel(sailingService, new AsyncActionsExecutor(), leaderboardName, null, - LeaderboardEditPage.this, getStringMessages(), userAgent); - leaderboardPanel.ensureDebugId("EditableLeaderboardPanel"); - return leaderboardPanel; - } - }); - - RootPanel.get().add(authorizedContentDecorator); - RootPanel.get().add(header); - } else { - RootPanel.get().add(new Label(getStringMessages().noSuchLeaderboard())); - } - } - @Override - public void onFailure(Throwable t) { - reportError("Error trying to obtain list of leaderboard names: "+t.getMessage()); - } - })); - } - - private SAPHeaderWithAuthentication initHeader() { - SAPHeaderWithAuthentication header = new SAPHeaderWithAuthentication(getStringMessages().sapSailingAnalytics(), - getStringMessages().editScores()); - header.getElement().getStyle().setPosition(Position.FIXED); - header.getElement().getStyle().setTop(0, Unit.PX); - header.getElement().getStyle().setWidth(100, Unit.PCT); - return header; - } -} +package com.sap.sailing.gwt.ui.leaderboardedit; + +import java.util.List; + +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootPanel; +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; +import com.sap.sse.security.ui.authentication.decorator.AuthorizedContentDecorator; +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 LeaderboardEditPage extends AbstractSailingEntryPoint { + @Override + protected void doOnModuleLoad() { + super.doOnModuleLoad(); + + sailingService.getLeaderboardNames(new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List leaderboardNames) { + final String leaderboardName = Window.Location.getParameter("name"); + if (leaderboardNames.contains(leaderboardName)) { + + SAPHeaderWithAuthentication header = initHeader(); + GenericAuthentication genericSailingAuthentication = new FixedSailingAuthentication(getUserService(), header.getAuthenticationMenuView()); + AuthorizedContentDecorator authorizedContentDecorator = new GenericAuthorizedContentDecorator(genericSailingAuthentication); + authorizedContentDecorator.setPermissionToCheck(Permission.MANAGE_LEADERBOARD_RESULTS, SailingPermissionsForRoleProvider.INSTANCE); + authorizedContentDecorator.setContentWidgetFactory(new WidgetFactory() { + @Override + public Widget get() { + EditableLeaderboardPanel leaderboardPanel = new EditableLeaderboardPanel(sailingService, new AsyncActionsExecutor(), leaderboardName, null, + LeaderboardEditPage.this, getStringMessages(), userAgent); + leaderboardPanel.ensureDebugId("EditableLeaderboardPanel"); + return leaderboardPanel; + } + }); + + RootPanel.get().add(authorizedContentDecorator); + RootPanel.get().add(header); + } else { + RootPanel.get().add(new Label(getStringMessages().noSuchLeaderboard())); + } + } + @Override + public void onFailure(Throwable t) { + reportError("Error trying to obtain list of leaderboard names: "+t.getMessage()); + } + })); + } + + private SAPHeaderWithAuthentication initHeader() { + 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); + return header; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 7fc9263cb34..420a98df543 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -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 competitorsFromBestToWorst = trackedRace.getCompetitorsFromBestToWorst(actualTimePoint); for (Competitor competitor : competitorsFromBestToWorst) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java index 2a2a72e011f..6f6fba2e3e7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java @@ -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(), - 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); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java index 4029f742f53..5f5561271f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java @@ -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); diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/resources/YES_29er_XRR.xml b/java/com.sap.sailing.manage2sail.resultimport.test/resources/YES_29er_XRR.xml index 6415684c895..7eb876b64f4 100644 --- a/java/com.sap.sailing.manage2sail.resultimport.test/resources/YES_29er_XRR.xml +++ b/java/com.sap.sailing.manage2sail.resultimport.test/resources/YES_29er_XRR.xml @@ -218,7 +218,7 @@ - + diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/ParserTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/ParserTest.java index c9587971f1f..5a957651261 100755 --- a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/ParserTest.java +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/ParserTest.java @@ -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 scoreCorrectionsForRaces = _29erResult.getScoreCorrectionsForRaces(); assertNotNull(scoreCorrectionsForRaces); - assertEquals(8, Util.size(scoreCorrectionsForRaces)); + assertEquals(numberOfRaceColumnsWithScores, Util.size(scoreCorrectionsForRaces)); + + Set raceNamesOrNumbers = new HashSet<>(); + for (ScoreCorrectionsForRace scoreCorrectionForRace : scoreCorrectionsForRaces) { + raceNamesOrNumbers.add(scoreCorrectionForRace.getRaceNameOrNumber()); + } + assertEquals(new HashSet(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()); + } + } +/** + + + + + + + + + + + + + + + + + + + + */ } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/FlatGPSFixJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/FlatGPSFixJsonSerializer.java index 86059c4429f..689a03fe977 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/FlatGPSFixJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/FlatGPSFixJsonSerializer.java @@ -17,8 +17,12 @@ public class FlatGPSFixJsonSerializer implements JsonSerializer { 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; } } diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java index f1092ae3abd..b9f844c78f5 100755 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/trackfiles/impl/GPSFixImportTest.java @@ -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 } } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/GPSFixesResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/GPSFixesResource.java index f8ef67471ec..6109f18251a 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/GPSFixesResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/GPSFixesResource.java @@ -52,9 +52,7 @@ public class GPSFixesResource extends AbstractSailingServerResource { List 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); @@ -62,5 +60,4 @@ public class GPSFixesResource extends AbstractSailingServerResource { return Response.ok().build(); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java index 422f0c50572..cf1a661dccd 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java @@ -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 fixes, DeviceIdentifier deviceIdentifier) { try { @@ -54,21 +52,16 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet { * * @throws IOException */ - private Iterable importFiles(Iterable> files) + private Iterable importFiles(Iterable> files) throws IOException { final Set deviceIds = new HashSet<>(); final Map from = new HashMap<>(); final Map to = new HashMap<>(); final Collection availableImporters = new LinkedHashSet<>(); availableImporters.addAll(getOSGiRegisteredImporters()); - for (Pair file : files) { + for (Pair 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 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 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 fileItems, HttpServletRequest req, HttpServletResponse resp) throws IOException { - List> 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> 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 mappingList = importFiles(files); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/TrackFilesImportServlet.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/TrackFilesImportServlet.java index 6ac0062645b..f0d9f915c00 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/TrackFilesImportServlet.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/TrackFilesImportServlet.java @@ -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}: *
    - *
  • If the type of a {@link #PREFERRED_IMPORTER preferred importer} is transmitted, this is the first that is used.
  • + *
  • If the type of a {@link #PREFERRED_IMPORTER preferred importer} is transmitted, this is the first that is used. + *
  • *
  • Then the importers registered for a matching {@link GPSFixImporter#FILE_EXTENSION_PROPERTY file extension} are * used.
  • *
  • If all this fails, all other available importers are used.
  • @@ -55,8 +55,6 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet { public static final String PREFERRED_IMPORTER = "preferredImporter"; 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 { @@ -84,69 +82,67 @@ public class TrackFilesImportServlet extends AbstractFileUploadServlet { } return result; } - - Iterable importFiles(Iterable> files, GPSFixImporter preferredImporter) - throws IOException { + + protected Iterable importFiles(Iterable> files, + GPSFixImporter preferredImporter) throws IOException { final Set deviceIds = new HashSet<>(); final Map from = new HashMap<>(); final Map to = new HashMap<>(); - - for (Pair file : files) { - final String fileName = file.getA(); + for (Pair 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 importersToTry = new LinkedHashSet<>(); + + Set 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 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 fileItems, HttpServletRequest req, HttpServletResponse resp) throws IOException { String prefImporterType = null; - List> files = new ArrayList<>(); + List> files = new ArrayList<>(); for (FileItem item : fileItems) { if (!item.isFormField()) - files.add(new Pair(item.getName(), item.getInputStream())); + files.add(new Pair(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 mappingList = importFiles(files, preferredImporter); - - //setJsonResponseHeader(resp); - //DO NOT set a JSON response header. This causes the browser to wrap the response in a - //
     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
    +        // 
     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);
    diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java
    index 852de012472..ce3116f033e 100644
    --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java
    +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java
    @@ -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,11 +112,12 @@ 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;
     import com.sap.sailing.domain.ranking.OneDesignRankingMetric;
    -import com.sap.sailing.domain.test.TrackBasedTest;
    +import com.sap.sailing.domain.test.TrackBasedTest;
     import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
     import com.sap.sailing.domain.tracking.TrackedRace;
     import com.sap.sailing.domain.tracking.WindTrack;
    @@ -123,9 +128,10 @@ import com.sap.sailing.server.gateway.jaxrs.spi.MasterDataResource;
     import com.sap.sailing.server.impl.RacingEventServiceImpl;
     import com.sap.sailing.server.masterdata.DummyTrackedRace;
     import com.sap.sailing.server.masterdata.MasterDataImporter;
    -import com.sap.sailing.server.testsupport.RacingEventServiceImplMock;
    +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;
    @@ -289,6 +295,18 @@ public class MasterDataImportTest {
             GPSFix gpsFix = new GPSFixMovingImpl(new DegreePosition(54.333, 10.133), logTimePoint2,
                     new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(90)));
             sourceService.getSensorFixStore().storeFix(deviceIdentifier, gpsFix);
    +        
    +        // Add sensor fix
    +        DeviceIdentifier deviceIdentifier2 = new SmartphoneImeiIdentifier("b");
    +        RegattaLogDeviceMappingEvent 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;
    @@ -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(){};
    @@ -446,6 +470,17 @@ public class MasterDataImportTest {
             Assert.assertNotNull(registerEventOnTarget2);
             Assert.assertEquals(registerEvent.getId(), registerEventOnTarget2.getId());
         }
    +    
    +    private void verifyFix(Timed expectedFix, SensorFixStore store, TimePoint start, TimePoint end, DeviceIdentifier deviceIdentifier) {
    +        List 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,
    diff --git a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/BravoDataImportTest.java b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/BravoDataImportTest.java
    index 7c07fa0f682..5fe5f150b5f 100644
    --- a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/BravoDataImportTest.java
    +++ b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/BravoDataImportTest.java
    @@ -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);
    diff --git a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/RouteConverterGpx11ExtensionsTest.java b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/RouteConverterGpx11ExtensionsTest.java
    index 2120e178879..660721e9118 100644
    --- a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/RouteConverterGpx11ExtensionsTest.java
    +++ b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/RouteConverterGpx11ExtensionsTest.java
    @@ -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() {
    -            @Override
    -            public GPSFix answer(InvocationOnMock invocation) throws Throwable {
    -                BaseNavigationPosition p = (BaseNavigationPosition) invocation.getArguments()[0];
    -                WptType waypoint = p.asGpxPosition().getOrigin(WptType.class);
    -                final List 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() {
    +                @Override
    +                public GPSFix answer(InvocationOnMock invocation) throws Throwable {
    +                    BaseNavigationPosition p = (BaseNavigationPosition) invocation.getArguments()[0];
    +                    WptType waypoint = p.asGpxPosition().getOrigin(WptType.class);
    +                    final List 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);
    +        }
         }
     }
    diff --git a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackFileImportTest.java b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackFileImportTest.java
    index d8927bc2111..4d12c4828b3 100644
    --- a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackFileImportTest.java
    +++ b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackFileImportTest.java
    @@ -17,37 +17,39 @@ import com.sap.sailing.server.trackfiles.impl.RouteConverterGPSFixImporterImpl;
     
     public class TrackFileImportTest {
         private boolean callbackCalled = false;
    -    
    +
         @Before
         public void setup() {
             callbackCalled = false;
         }
    -    
    +
         @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);
    +        }
         }
     }
    diff --git a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackedRacesExportTest.java b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackedRacesExportTest.java
    index 7b9f89f03a1..227fc3f97f6 100755
    --- a/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackedRacesExportTest.java
    +++ b/java/com.sap.sailing.server.trackfiles.test/src/com/sap/sailing/server/trackfiles/test/TrackedRacesExportTest.java
    @@ -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);
             }
         }
    diff --git a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BravoDataImporterImpl.java b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BravoDataImporterImpl.java
    index 48885321dd1..9d7f8045b14 100644
    --- a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BravoDataImporterImpl.java
    +++ b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/BravoDataImporterImpl.java
    @@ -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);
    diff --git a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java
    index fe52330b27b..0c11fe98d55 100644
    --- a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java
    +++ b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java
    @@ -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 callables = new ArrayList<>();
    -        ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true));
    +        ExecutorService executor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor();
             List errors = new ArrayList<>();
             for (TrackedRace race : races) {
                 for (TrackFilesDataSource d : data) {
    diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java
    index b22b1c6cabe..be93bfbe602 100755
    --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java
    +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java
    @@ -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;
     
    diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java
    index 68dd33950cf..2605ac6ddb2 100644
    --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java
    +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java
    @@ -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);
    +            }
             }
         }
     
    diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactory.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactory.java
    index 5cf6daff5c6..e214bfbf9ab 100644
    --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactory.java
    +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactory.java
    @@ -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);
         
     }
    diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactoryImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactoryImpl.java
    index cd73c3228b9..c7a6ce0bfbc 100644
    --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactoryImpl.java
    +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceFactoryImpl.java
    @@ -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);
         }
     
    diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java
    index 74d01a04541..515f7cac9ec 100644
    --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java
    +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java
    @@ -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 cache;
         final private RacingEventService racingEventService;
         final private ScheduledExecutorService scheduler;
    @@ -73,9 +70,9 @@ public class SimulationServiceImpl implements SimulationService {
         final private HashMap 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();
    @@ -437,31 +434,26 @@ public class SimulationServiceImpl implements SimulationService {
             Simulator simulator = new SimulatorImpl(simulationParameters);
             Map result = new HashMap();
     
    -        FutureTask taskOmniscient = null;
    +        Future taskOmniscient = null;
             if (simulationParameters.showOmniscient()) {
                 // schedule omniscient task
    -            taskOmniscient = new FutureTask(() -> simulator.getPath(PathType.OMNISCIENT));
    -            executor.execute(taskOmniscient);
    +            taskOmniscient = executor.submit(() -> simulator.getPath(PathType.OMNISCIENT));
             }
     
    -        FutureTask task1TurnerLeft = null;
    -        FutureTask task1TurnerRight = null;
    +        Future task1TurnerLeft = null;
    +        Future task1TurnerRight = null;
             if (simulationParameters.getLegType() != LegType.REACHING) {
                 // schedule 1-turner tasks
    -            task1TurnerLeft = new FutureTask(() -> simulator.getPath(PathType.ONE_TURNER_LEFT));
    -            task1TurnerRight = new FutureTask(() -> 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 taskOpportunistLeft = null;
    -        FutureTask taskOpportunistRight = null;
    +        Future taskOpportunistLeft = null;
    +        Future taskOpportunistRight = null;
             if (simulationParameters.showOpportunist()) {        
                 // schedule opportunist tasks (which depend on 1-turner results)
    -            taskOpportunistLeft = new FutureTask(() -> simulator.getPath(PathType.OPPORTUNIST_LEFT));
    -            taskOpportunistRight = new FutureTask(() -> 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;
    diff --git a/java/com.sap.sailing.xrr.schema/src/com/sap/sailing/xrr/schema/IFBoatStatus.java b/java/com.sap.sailing.xrr.schema/src/com/sap/sailing/xrr/schema/IFBoatStatus.java
    index 5c13f231516..ebdcfa6b361 100755
    --- a/java/com.sap.sailing.xrr.schema/src/com/sap/sailing/xrr/schema/IFBoatStatus.java
    +++ b/java/com.sap.sailing.xrr.schema/src/com/sap/sailing/xrr/schema/IFBoatStatus.java
    @@ -37,6 +37,9 @@ import javax.xml.bind.annotation.XmlType;
      *     <enumeration value="SCP"/>
      *     <enumeration value="ZFP"/>
      *     <enumeration value="RTD"/>
    + *     <enumeration value="UFD"/>
    + *     <enumeration value="TLE"/>
    + *     <enumeration value="STP"/>
      *   </restriction>
      * </simpleType>
      * 
    @@ -147,8 +150,21 @@ 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();
         }
    diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.gss b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.gss
    index a27a18eb8a0..5a6c09be762 100644
    --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.gss
    +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.gss
    @@ -1,73 +1,81 @@
    -.siteheader {
    -  font-size: 1rem;
    -  line-height: 1.333;
    -  background: #333;
    -  border-bottom: 4px solid #f0ab00;
    -  height: 75px;
    -  width: 100%;
    -  overflow: hidden;
    -  vertical-align: top;
    -  position: relative;
    -}
    -
    -.siteHeaderTitle {
    -  color: #fff;
    -  vertical-align: top;
    -  font-size: 20px;
    -  font-weight: 700;
    -}
    -
    -.siteHeaderSubTitle {
    -  color: #fff;
    -  font-size: 18px;
    -  vertical-align: top;
    -}
    -
    -.siteheaderLeft {
    -    position: relative; top:10px;left:10px;
    -	display: inline-block;
    -	text-align: left;
    -	height: 50px;
    -	vertical-align: top;
    -}
    -
    -.siteheader2ndCol {
    -    position: absolute; top:10px;left:235px;
    -	display: inline-block;
    -	text-align: left;
    -	height:50px;
    -	vertical-align: top;
    -}
    -
    -.siteheaderRight {
    -    position: absolute; top:10px;right:10px;
    -	text-align: right;
    -	display: inline-block;
    -	height:50px;
    -	vertical-align: top;
    -}
    -
    -.logo {
    -  margin: 0px;
    -  display: inline-block;
    -}
    -
    -.logotitle {
    -  font-size: 1rem;
    -  display: inline-block;
    -  color: #f6f9fc;
    -  width: 150px;
    -  height: 100%;
    -  padding: 17px 15px 0 0;
    -  vertical-align: top;
    -  text-align: right;
    -  border-right: 1px solid rgb(180,180,180);
    -  margin: 0px;
    -  margin-left: -25px;
    -  box-sizing: border-box;
    -}
    -.pagetitle {
    -  color: #ffffff;
    -  font-size: 1.1rem;
    -  font-weight: bold;
    -}
    \ No newline at end of file
    +.siteheader {
    +  font-size: 1rem;
    +  line-height: 1.333;
    +  background: #333;
    +  border-bottom: 4px solid #f0ab00;
    +  height: 75px;
    +  width: 100%;
    +  overflow: hidden;
    +  vertical-align: top;
    +  position: relative;
    +}
    +
    +.siteHeaderTitle {
    +  color: #fff;
    +  vertical-align: top;
    +  font-size: 20px;
    +  font-weight: 700;
    +}
    +
    +.siteHeaderSubTitle {
    +  color: #fff;
    +  font-size: 18px;
    +  vertical-align: top;
    +}
    +
    +.siteheaderLeft {
    +    position: relative; top:10px;left:10px;
    +	display: inline-block;
    +	text-align: left;
    +	height: 50px;
    +	vertical-align: top;
    +}
    +
    +.siteheader2ndCol {
    +    position: absolute; top:10px;left:235px;
    +	display: inline-block;
    +	text-align: left;
    +	height:50px;
    +	vertical-align: top;
    +}
    +
    +.siteheaderRight {
    +    position: absolute; top:10px;right:10px;
    +	text-align: right;
    +	display: inline-block;
    +	height:50px;
    +	vertical-align: top;
    +}
    +
    +.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:hover, .logotitle:visited {
    +  font-size: 1rem;
    +  display: inline-block;
    +  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;
    +  font-size: 1.1rem;
    +  font-weight: bold;
    +}
    diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.java
    index a5dafeef6f1..a7dcd226353 100644
    --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.java
    +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.java
    @@ -1,65 +1,75 @@
    -package com.sap.sse.gwt.client.sapheader;
    -
    -import com.google.gwt.core.client.GWT;
    -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.uibinder.client.UiBinder;
    -import com.google.gwt.uibinder.client.UiField;
    -import com.google.gwt.user.client.ui.Composite;
    -import com.google.gwt.user.client.ui.SimplePanel;
    -import com.google.gwt.user.client.ui.Widget;
    -
    -/**
    - * Generic header widget that is designed with a SAP logo and SAP color scheme.
    - * 
    - * The header has the following elements:
    - * 
      - *
    • SAP Logo with an application title (like "Sailing Analytics")
    • - *
    • Page title
    • - *
    • Optional Subtitle
    • - *
    • Optional widget to show on the right side of the header
    • - *
    - */ -public class SAPHeader extends Composite { - private static SAPHeaderUiBinder uiBinder = GWT.create(SAPHeaderUiBinder.class); - - interface SAPHeaderUiBinder extends UiBinder { - } - - @UiField - DivElement applicationNameUi; - @UiField - DivElement pageTitleUi; - @UiField - DivElement titleUi; - @UiField - DivElement subTitleUi; - @UiField - SimplePanel rightSideUi; - - public SAPHeader(String applicationName) { - SAPHeaderResources.INSTANCE.css().ensureInjected(); - initWidget(uiBinder.createAndBindUi(this)); - applicationNameUi.setInnerText(applicationName != null ? applicationName : " "); - } - - public void setHeaderTitle(String title) { - titleUi.setInnerText(title); - } - - public void setHeaderSubTitle(String subtitle) { - if (subtitle == null || subtitle.isEmpty()) { - subTitleUi.getStyle().setDisplay(Display.NONE); - titleUi.getStyle().setMarginTop(14, Unit.PX); - } else { - subTitleUi.setInnerText(subtitle); - subTitleUi.getStyle().clearDisplay(); - titleUi.getStyle().clearMarginTop(); - } - } - - public void addWidgetToRightSide(Widget widget) { - rightSideUi.add(widget); - } -} +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; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; + +/** + * Generic header widget that is designed with a SAP logo and SAP color scheme. + * + * The header has the following elements: + *
      + *
    • SAP Logo with an application title (like "Sailing Analytics")
    • + *
    • Page title
    • + *
    • Optional Subtitle
    • + *
    • Optional widget to show on the right side of the header
    • + *
    + */ +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 { + } + + @UiField + AnchorElement applicationNameAnchor; + @UiField + DivElement pageTitleUi; + @UiField + DivElement titleUi; + @UiField + DivElement subTitleUi; + @UiField + SimplePanel rightSideUi; + @UiField + AnchorElement logoAnchor; + + public SAPHeader(String applicationName, String applicationBaseUrl) { + SAPHeaderResources.INSTANCE.css().ensureInjected(); + initWidget(uiBinder.createAndBindUi(this)); + applicationNameAnchor.setInnerText(applicationName != null ? applicationName : " "); + logoAnchor.setHref(LOGO_URL); + String sapSailingUrl = applicationBaseUrl + "?locale=" + LocaleInfo.getCurrentLocale().getLocaleName(); + applicationNameAnchor.setHref(sapSailingUrl); + } + + public void setHeaderTitle(String title) { + titleUi.setInnerText(title); + } + + public void setHeaderSubTitle(String subtitle) { + if (subtitle == null || subtitle.isEmpty()) { + subTitleUi.getStyle().setDisplay(Display.NONE); + titleUi.getStyle().setMarginTop(14, Unit.PX); + } else { + subTitleUi.setInnerText(subtitle); + subTitleUi.getStyle().clearDisplay(); + titleUi.getStyle().clearMarginTop(); + } + } + + public void addWidgetToRightSide(Widget widget) { + rightSideUi.add(widget); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.ui.xml b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.ui.xml index f1ecf5f756f..02ca20a0f5a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.ui.xml +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeader.ui.xml @@ -1,24 +1,20 @@ - - - - - -
    -
    - -
    -
    -
    -
    - -
    - - -
    -
    -
    + + + + + +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeaderResources.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeaderResources.java index 74e6bb2ec47..004708ef3e0 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeaderResources.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/sapheader/SAPHeaderResources.java @@ -1,28 +1,28 @@ -package com.sap.sse.gwt.client.sapheader; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.resources.client.ClientBundle; -import com.google.gwt.resources.client.CssResource; -import com.google.gwt.resources.client.ImageResource; - -public interface SAPHeaderResources extends ClientBundle { - public static final SAPHeaderResources INSTANCE = GWT.create(SAPHeaderResources.class); - - @Source("SAPHeader.gss") - LocalCss css(); - - public interface LocalCss extends CssResource { - String siteheader(); - String siteheaderLeft(); - String siteHeaderTitle(); - String siteHeaderSubTitle(); - String siteheader2ndCol(); - String siteheaderRight(); - String logo(); - String logotitle(); - String pagetitle(); - } - - @Source("logo-small@2x.png") - ImageResource logo(); -} +package com.sap.sse.gwt.client.sapheader; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.ImageResource; + +public interface SAPHeaderResources extends ClientBundle { + public static final SAPHeaderResources INSTANCE = GWT.create(SAPHeaderResources.class); + + @Source("SAPHeader.gss") + LocalCss css(); + + public interface LocalCss extends CssResource { + String siteheader(); + String siteheaderLeft(); + String siteHeaderTitle(); + String siteHeaderSubTitle(); + String siteheader2ndCol(); + String siteheaderRight(); + String logo(); + String logotitle(); + String pagetitle(); + } + + @Source("logo-small@2x.png") + ImageResource logo(); +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/generic/sapheader/SAPHeaderWithAuthentication.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/generic/sapheader/SAPHeaderWithAuthentication.java index 24a614b4adf..e4b46cb4071 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/generic/sapheader/SAPHeaderWithAuthentication.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/generic/sapheader/SAPHeaderWithAuthentication.java @@ -1,58 +1,58 @@ -package com.sap.sse.security.ui.authentication.generic.sapheader; - -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sse.gwt.client.sapheader.SAPHeader; -import com.sap.sse.security.ui.authentication.generic.GenericAuthentication; -import com.sap.sse.security.ui.authentication.view.AuthenticationMenuView; -import com.sap.sse.security.ui.authentication.view.AuthenticationMenuViewImpl; - -/** - * {@link SAPHeader} which is decorated by a authentication control on the right side. This is typically used to - * integrate with {@link GenericAuthentication}. - */ -public class SAPHeaderWithAuthentication extends SAPHeader { - private static final SAPHeaderWithAuthenticationResources res = SAPHeaderWithAuthenticationResources.INSTANCE; - - private final SimplePanel rightWrapper; - - private AuthenticationMenuView authenticationMenuView; - - public SAPHeaderWithAuthentication(String applicationName, String headerTitle) { - this(applicationName); - setHeaderTitle(headerTitle); - } - - public SAPHeaderWithAuthentication(String applicationName) { - super(applicationName); - res.css().ensureInjected(); - - FlowPanel rightWithAuthentication = new FlowPanel(); - rightWithAuthentication.addStyleName(res.css().header_right_wrapper()); - - Anchor authenticationMenu = new Anchor(); - authenticationMenu.addStyleName(res.css().usermanagement_icon()); - rightWithAuthentication.add(authenticationMenu); - authenticationMenuView = new AuthenticationMenuViewImpl(authenticationMenu, res.css().usermanagement_loggedin(), res.css().usermanagement_open()); - - rightWrapper = new SimplePanel(); - rightWrapper.addStyleName(res.css().header_right_extension()); - rightWithAuthentication.add(rightWrapper); - - super.addWidgetToRightSide(rightWithAuthentication); - } - - @Override - public void addWidgetToRightSide(Widget widget) { - rightWrapper.add(widget); - } - - /** - * @return the {@link AuthenticationMenuView} associated with the authentication control on the right side of the header. - */ - public AuthenticationMenuView getAuthenticationMenuView() { - return authenticationMenuView; - } -} +package com.sap.sse.security.ui.authentication.generic.sapheader; + +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sse.gwt.client.sapheader.SAPHeader; +import com.sap.sse.security.ui.authentication.generic.GenericAuthentication; +import com.sap.sse.security.ui.authentication.view.AuthenticationMenuView; +import com.sap.sse.security.ui.authentication.view.AuthenticationMenuViewImpl; + +/** + * {@link SAPHeader} which is decorated by a authentication control on the right side. This is typically used to + * integrate with {@link GenericAuthentication}. + */ +public class SAPHeaderWithAuthentication extends SAPHeader { + private static final SAPHeaderWithAuthenticationResources res = SAPHeaderWithAuthenticationResources.INSTANCE; + + private final SimplePanel rightWrapper; + + private AuthenticationMenuView authenticationMenuView; + + public SAPHeaderWithAuthentication(String applicationName, String applicationBaseUrl, String headerTitle) { + this(applicationName, applicationBaseUrl); + setHeaderTitle(headerTitle); + } + + public SAPHeaderWithAuthentication(String applicationName, String applicationBaseUrl) { + super(applicationName, applicationBaseUrl); + res.css().ensureInjected(); + + FlowPanel rightWithAuthentication = new FlowPanel(); + rightWithAuthentication.addStyleName(res.css().header_right_wrapper()); + + Anchor authenticationMenu = new Anchor(); + authenticationMenu.addStyleName(res.css().usermanagement_icon()); + rightWithAuthentication.add(authenticationMenu); + authenticationMenuView = new AuthenticationMenuViewImpl(authenticationMenu, res.css().usermanagement_loggedin(), res.css().usermanagement_open()); + + rightWrapper = new SimplePanel(); + rightWrapper.addStyleName(res.css().header_right_extension()); + rightWithAuthentication.add(rightWrapper); + + super.addWidgetToRightSide(rightWithAuthentication); + } + + @Override + public void addWidgetToRightSide(Widget widget) { + rightWrapper.add(widget); + } + + /** + * @return the {@link AuthenticationMenuView} associated with the authentication control on the right side of the header. + */ + public AuthenticationMenuView getAuthenticationMenuView() { + return authenticationMenuView; + } +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java index bf489da87e7..7988f3f3fbb 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java +++ b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java @@ -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> { return callable.call(); } + @Override + public String toString() { + return callable==null?"null":callable.toString(); + } } /** @@ -201,7 +211,9 @@ public class SmartFutureCache> { * @author Axel Uhl (D043530) * */ - private class FutureTaskWithCancelBlocking extends FutureTask implements Callable { + private class FutureTaskWithCancelBlocking extends FutureTask implements KnowsExecutor, Callable { + private final KnowsExecutorAndTracingGet tracingGetHelper; + private boolean runningAndReadUpdateInterval; private final K key; @@ -228,6 +240,7 @@ public class SmartFutureCache> { public FutureTaskWithCancelBlocking(SettableCallable 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> { } } } - return super.get(); + return tracingGetHelper.callGetAndTraceAfterEachTimeout(this); } @Override @@ -363,6 +376,16 @@ public class SmartFutureCache> { 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 cacheUpdateComputer, String nameForLocks) { @@ -507,14 +530,33 @@ public class SmartFutureCache> { 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 implements Future { private final SettableFuture> future; + private final HasTracingGet tracingGetHelper; protected TransitiveFuture(SettableFuture> future) { super(); this.future = future; + this.tracingGetHelper = new HasTracingGetImpl() { + @Override + protected String getAdditionalTraceInfo() { + return "transitive future "+TransitiveFuture.this.future.toString(); + } + }; } @Override @@ -546,7 +588,7 @@ public class SmartFutureCache> { @Override public T get() throws InterruptedException, ExecutionException { - return future.get().get(); + return tracingGetHelper.callGetAndTraceAfterEachTimeout(this); } @Override diff --git a/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java b/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java index 26ae00ad34a..71a942b31d3 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java +++ b/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java @@ -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); } diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/FutureTaskWithTracingGet.java b/java/com.sap.sse/src/com/sap/sse/util/impl/FutureTaskWithTracingGet.java new file mode 100644 index 00000000000..95a807e7068 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/FutureTaskWithTracingGet.java @@ -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 extends FutureTask { + private final HasTracingGet hasTracingGet; + private final String traceInfo; + + public FutureTaskWithTracingGet(String traceInfo, Callable 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 createHasTracingGet() { + return new HasTracingGetImpl() { + @Override + protected String getAdditionalTraceInfo() { + return traceInfo; + } + }; + } + +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGet.java b/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGet.java new file mode 100644 index 00000000000..91228d39732 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGet.java @@ -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 { + /** + * 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 future) throws InterruptedException, ExecutionException; +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGetImpl.java b/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGetImpl.java new file mode 100644 index 00000000000..f0d0b1aedd7 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/HasTracingGetImpl.java @@ -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 + */ +public abstract class HasTracingGetImpl implements HasTracingGet { + 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 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(); +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java new file mode 100644 index 00000000000..ab7d0241a85 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java @@ -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); +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java new file mode 100644 index 00000000000..300a6ecc1b0 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java @@ -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 + */ +public interface KnowsExecutorAndTracingGet extends KnowsExecutor, HasTracingGet { +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java new file mode 100644 index 00000000000..288ebe998db --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java @@ -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 + */ +public class KnowsExecutorAndTracingGetImpl extends HasTracingGetImpl implements KnowsExecutorAndTracingGet { + 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); + } +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/NamedTracingScheduledThreadPoolExecutor.java b/java/com.sap.sse/src/com/sap/sse/util/impl/NamedTracingScheduledThreadPoolExecutor.java new file mode 100644 index 00000000000..0704932e53a --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/NamedTracingScheduledThreadPoolExecutor.java @@ -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 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, 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 RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { + return new ThreadPoolAwareRunnableScheduledFutureDelegate<>(this, task); + } + + @Override + protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { + return new ThreadPoolAwareRunnableScheduledFutureDelegate<>(this, task); + } + + @Override + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new ThreadPoolAwareFutureTask(this, runnable, value); + } + + @Override + protected RunnableFuture newTaskFor(Callable callable) { + return new ThreadPoolAwareFutureTask(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 + "]"; + } +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java new file mode 100644 index 00000000000..03664e8a4cd --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java @@ -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 + */ +public class ThreadPoolAwareFutureTask extends FutureTask implements KnowsExecutor { + private static final Logger logger = Logger.getLogger(ThreadPoolAwareFutureTask.class.getName()); + private final KnowsExecutorAndTracingGet getHelper = new KnowsExecutorAndTracingGetImpl(); + private final Object callableOrRunnableIfLoggingFine; + + public ThreadPoolAwareFutureTask(ThreadPoolExecutor executor, Callable 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())); + } +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java new file mode 100644 index 00000000000..64b5bfeaba4 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java @@ -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 extends KnowsExecutorAndTracingGetImpl implements KnowsExecutorAndTracingGet, RunnableScheduledFuture { + private final RunnableScheduledFuture future; + + public ThreadPoolAwareRunnableScheduledFutureDelegate(ThreadPoolExecutor executor, RunnableScheduledFuture 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); + } +} diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java index 33e030e489e..61576bd0520 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java @@ -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 diff --git a/mobile/com.sap.sailing.android.tracking.app/res/values-de/preferences.xml b/mobile/com.sap.sailing.android.tracking.app/res/values-de/preferences.xml index 9cdceb8cd52..190c72327e7 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/values-de/preferences.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/values-de/preferences.xml @@ -1,11 +1,21 @@ Batterie Sparen - Batteriesparmodus - Ein - Aus - GPS Updates werden alle 30 Sekunden verschickt. - GPS Updates werden jede Sekunde verschickt. + + Sendeintervall für GPS-Updates auswählen + Wählen Sie den größtmöglichen Wert, der dennoch eine dem Anwendungsfall entsprechend hohe Aktualisierungsrate ermöglicht. + 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. + + GPS Updates werden%1$s %2$s an den Server gesendet. + alle + + sofort + 5 Sekunden + 10 Sekunden + 20 Sekunden + 60 Sekunden + + Ein Aus Anzeige: Missweisend diff --git a/mobile/com.sap.sailing.android.tracking.app/res/values-de/strings.xml b/mobile/com.sap.sailing.android.tracking.app/res/values-de/strings.xml index f803398580d..314a1aa3a66 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/values-de/strings.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/values-de/strings.xml @@ -78,5 +78,6 @@ Wird hochgeladen… Fehler beim Hochladen: Fehler beim Hochladen.\nZum Wiederholen hier tippen. + GPS Signal gefunden! \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.tracking.app/res/values/preferences.xml b/mobile/com.sap.sailing.android.tracking.app/res/values/preferences.xml index 403f5b8e5d1..20f2003a35d 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/values/preferences.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/values/preferences.xml @@ -7,14 +7,34 @@ tracking preferenceEnergySavingEnabled + preferenceEnergySavingOverride preferenceHeadingWithDeclinationSubtracted Battery Saving - Save Energy - On - Off - GPS updates are being sent to server every 30 seconds. - GPS updates are being sent to server every second. + + msgSndInt + 0 + choose GPS update sending interval + Choose a value as high as possible to preserve battery life while still maintaining a reasonable live experience, depending on your use case. + 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. + + GPS updates will be sent to the server%1$s %2$s. + msgSndIntMessage + every + + immediately + 5 seconds + 10 seconds + 20 seconds + 60 seconds + + + 0 + 5 + 10 + 20 + 60 + gpsFixInterval 500 diff --git a/mobile/com.sap.sailing.android.tracking.app/res/values/strings_fragment.xml b/mobile/com.sap.sailing.android.tracking.app/res/values/strings_fragment.xml index 75dae9ad3f1..cf66464434a 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/values/strings_fragment.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/values/strings_fragment.xml @@ -19,4 +19,5 @@ No GPS Signal! Regatta User Image Add Photo Button + GPS signal found! \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.tracking.app/res/xml/preference_general.xml b/mobile/com.sap.sailing.android.tracking.app/res/xml/preference_general.xml index 37cec25d928..6f1087bd9d1 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/xml/preference_general.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/xml/preference_general.xml @@ -3,14 +3,17 @@ - + + + diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/services/TrackingService.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/services/TrackingService.java index e976e6fdc7d..bd475c737c3 100644 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/services/TrackingService.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/services/TrackingService.java @@ -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"); diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/TrackingFragment.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/TrackingFragment.java index 84e5b1057b1..cb01fbdb4d8 100644 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/TrackingFragment.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/TrackingFragment.java @@ -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; diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/preference/GeneralPreferenceFragment.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/preference/GeneralPreferenceFragment.java index 9131438e7ae..ede9973e121 100644 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/preference/GeneralPreferenceFragment.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/fragments/preference/GeneralPreferenceFragment.java @@ -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) + ); + } + } } diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/AppPreferences.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/AppPreferences.java index 8cf6ab2b105..51f76777b36 100644 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/AppPreferences.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/AppPreferences.java @@ -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(); } + } \ No newline at end of file diff --git a/mobile/com.sap.sailing.buoy.positioning/res/values-de/strings.xml b/mobile/com.sap.sailing.buoy.positioning/res/values-de/strings.xml index f77bf6fd3be..f466ee3f429 100755 --- a/mobile/com.sap.sailing.buoy.positioning/res/values-de/strings.xml +++ b/mobile/com.sap.sailing.buoy.positioning/res/values-de/strings.xml @@ -9,6 +9,7 @@ Position zurücksetzen Empfangen der Markierungen… Gesetzt + Unbekannt Nicht gesetzt (Genauigkeit ca. %1$sm) GPS Genauigkeit diff --git a/mobile/com.sap.sailing.buoy.positioning/res/values/strings.xml b/mobile/com.sap.sailing.buoy.positioning/res/values/strings.xml index 85ce4539351..d6adace3426 100644 --- a/mobile/com.sap.sailing.buoy.positioning/res/values/strings.xml +++ b/mobile/com.sap.sailing.buoy.positioning/res/values/strings.xml @@ -12,6 +12,7 @@ Reset Position Getting race marks… set + n/a not set (Accurary ca. %1$sm) ca. %1$.02fm diff --git a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/adapter/MarkAdapter.java b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/adapter/MarkAdapter.java index 721c2a3753c..5729fa1fadf 100644 --- a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/adapter/MarkAdapter.java +++ b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/adapter/MarkAdapter.java @@ -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); diff --git a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/ui/fragments/BuoyFragment.java b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/ui/fragments/BuoyFragment.java index c52c3aee9f1..766a4a3423e 100644 --- a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/ui/fragments/BuoyFragment.java +++ b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/ui/fragments/BuoyFragment.java @@ -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(); diff --git a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/util/CheckinManager.java b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/util/CheckinManager.java index 72cc7bf1814..6e41b915e4b 100644 --- a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/util/CheckinManager.java +++ b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/util/CheckinManager.java @@ -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); diff --git a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/valueobjects/MarkPingInfo.java b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/valueobjects/MarkPingInfo.java index dcdc8286672..1ace4490e7d 100644 --- a/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/valueobjects/MarkPingInfo.java +++ b/mobile/com.sap.sailing.buoy.positioning/src/com/sap/sailing/android/buoy/positioning/app/valueobjects/MarkPingInfo.java @@ -8,6 +8,8 @@ public class MarkPingInfo { private final Serializable markId; 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(); diff --git a/wiki/info/landscape/amazon-ec2.md b/wiki/info/landscape/amazon-ec2.md index 4eb307939d3..2c224bface0 100644 --- a/wiki/info/landscape/amazon-ec2.md +++ b/wiki/info/landscape/amazon-ec2.md @@ -397,6 +397,12 @@ cp -rf ~/servers/server/logs/* /var/log/old/// - Or you manually force a logrotate run with `logrotate --force /etc/logrotate.d/httpd`, which copies `/var/log/httpd/` towards `/var/log/old//` - 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 diff --git a/wiki/info/mobile/data-consumption.md b/wiki/info/mobile/data-consumption.md new file mode 100644 index 00000000000..21ec4b562b3 --- /dev/null +++ b/wiki/info/mobile/data-consumption.md @@ -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 \ No newline at end of file diff --git a/wiki/info/mobile/energy-consumption.md b/wiki/info/mobile/energy-consumption.md new file mode 100644 index 00000000000..cb6b4d532a9 --- /dev/null +++ b/wiki/info/mobile/energy-consumption.md @@ -0,0 +1,107 @@ +# Power Consumption Of Android Apps + +[[_TOC_]] + +## Introduction +This page documents energy consumption tests of the SailInsight app in various scenarios. It is a crucial topic to consider as tracking often goes on for several hours and it would be desirable that the phone lasts throughout the whole session. + +Though there are many opinions which can be found on the internet as to how big of an impact GPS or cellular network has on battery drain, most of them are very vague or do not comply with our specific usecase, so more abstract observations had to be done, see our findings below. + +For further discussion on the topic there is bug [#3883]( https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=3883); you can also find detailed measurements of the initial tests on this topic [here](http://media.sapsailing.com/SmartphoneLogs/energyTracking.zip). + +## Testing Methodology + +Monitoring energy consumption in Android is not easy. Detailed statistics can only be obtained from the system if you have root access from Android 4.4 onwards. Information like what app is responsible for what portion of the energy consumption is either not available in APIs before 4.4 or would require rooting all the testing devices. Since that was not an option we decided to monitor the general battery level over a time of 2 hours to get somewhat representative results. We made sure that the phone was only running the regular set of background activities and no unnecessary applications. + +Having decided that, we wrote an EnergyLog class that you can find in com.sap.sailing.android.shared.util on the “bug3883”-branch ready for future testing. It is able to monitor the following data by itself whenever `addEntryToLog` is called: a timestamp, the battery-percentage, battery temperature, CPU usage and GSM signal strength. Instantiate the class once for every log you want to write and when you are done logging/adding entries call `writeLog`. You can also monitor GPS accuracy and the amount of satellites used for finding the fix. This is useful if you want to measure energy consumption in the InSight app whenever `onLocationChanged` is called. + +### Location APIs + +Differences and considerations +There basically are two APIs for gathering location information: The "older" LocationManager API which is available in every API level, and the FusedLocationProvider API which works through the GooglePlayServices. The latter was used for all the Sailing Analytics Android apps and is also available on every phone in theory. Sometimes the app requires updates of the GooglePlayServices though, making it an additional hassle that could be avoided by not using it. Also we experienced issues on an Huawei Honor 6 device with Android 6 while using the InSight App when the phone was locked. The phone did not provide location updates anymore, which was the reason we looked into switching the APIs in the first place, since everything worked fine with the LocationManager. + +Furthermore, you can specify the LocationManager API to only use GPS to determine exact locations, while the FusedLocationProvider API only takes criterias (such as the need for high-precision location), but it will fall back to unreasonably unprecise location fixes aggregated through WiFi and cellular network if there is no GPS connection yet. + +The only advantage over the LocationManager here is that getting a coarse location as the initial fix can be much faster. + +Unprecise locations are insufficient for Sail InSight and Buoy Pinger though, so waiting for a fine location instead of accepting an unprecise one should be the way to go. Thus the API used in these two apps will be switched to LocationManager. +As for the Race Committee app, unprecise locations will do here, since those are only used for setting wind information. Therefore, FusedLocationProviderApi grants better useability in terms of the speed of finding the location. + +Battery drain aspects +We tested both APIs on three devices over a period of two hours. See the results below. + + +As you can see above, the difference between the two APIs is relatively small. Moreover it seems that on some phones, one API has less impact on battery drain than the other, while on other phones it is the other way around, though this delta is probably within margin of error. So switching APIs really has no impact on battery consumption. + +### 2G vs 3G connection in good network coverage + +There is the common opinion that a 2G network connection drains less battery than 3G. This is only partially true though as in certain situations it is the other way around, especially in situations without good coverage (which is the case while sailing on the water most of the time). + +See our tests in a good connection state below - again tested on three devices over two hours, the devices set to only use the respective network. + + +The results show that for the Sail InSight app, 2G does not consume less battery in any case. The difference again is very small and probably within margin of error on two of our testing phones. Interestingly though, on the third phone, 2G actually drains the phone's battery noticeably stronger than 3G. + +A possible reason for this might be that sending fixes takes longer on a 2G connection due to the lower bandwidth - leading the chip having to stay active longer each time a message is sent. Again, under hard conditions with problematic connectivity this would probably be the other way around. + +### Comparing different message sending intervals and also good and bad connectivity +In order to simulate a weak cellular connection often found on the water - constantly changing 2G/3G/4G networks and signal strengths; even signal loss from time to time, we put the phones in an elevator. On the top floor, there is a good connection with 3G, the further down, the worse it gets up to no connection in the basement. Here the phones are sometimes able to catch a 2G signal, which is also not that strong. +For comparison, we also put the phones in a well-covered area. + +As message sending intervals shown on the X-axis we chose the following: +immediately, 5 seconds, 10 seconds, 20 seconds, 30 seconds and 5 minutes. +On the Y-axis you can see the consumed energy over 2 hours. + +See the results below. + + + +What you can see in this diagram is that battery consumption will drop with increasing sending intervals, no matter how good or bad cellular connection is. The most significant differences are observable on intervals ranging from 5 to 25 seconds. The difference between 30 seconds and 5 minutes is negligible, so is the difference between immediate sending and a 5 second interval. + +When comparing good and bad connectivity, the graphs mostly confirm our concerns: The battery drains faster on most phones when exposed to changing cellular network or frequently lost connection. The phones consume more energy when they have to compensate for that with booking into other networks, reconnecting and so on. This means that tracking will use more energy under real conditions than in synthetic benchmarks on land, where you usually have a better connection. + +Some phones though behave unexpectedly on a bad connection. The S2 shows the exact opposite of the expected results with the battery consumption being lower under bad connectivity. From this we can conclude that there are phones which are able to handle these bad conditions better than others or even get an advantage from it. When a phone is booked into a cellular network for overall less time it also consumes less power. But seemingly the expected increased consumption for booking in and out of networks is not present on some phones. + +## Conclusion + +Generally we have to say that energy consumption heavily depends on the phone itself. On some phones the app would still be able to run 10 hours without optimization, on others only 4. It also depends on the specific device how effective energy conserving measures are. + +GPS fix interval + +* different GPS fix acquiring intervals do not make a difference in battery drain +* they would if the interval was much lower - say 5-10 minutes upwards +* this is not an option for a precise tracking app though so there is nothing which can be done at this point + +Connectivity state + +* better signal strength leads to a lower required transmission power by the phone's antenna + --> a solid 2G connection is better than a bad 3G connection +* the longer the chip needs to be active, the higher the drain + --> the faster data is sent, the better + --> 3G is more economical, if stable, especially with bigger data batches +* frequent signal loss forces the phone to constanly try to reconnect to the cellular network + +Message sending interval + +* the main thing where power saving measures can be applied on all phones + +Heat + +* heat will have no short term influence on battery life on the current cycle +* only long term influence on battery degredation + + +Resulting actions going forth + +* send messages _only_ as fast as necessary to still provide a reasonable experience +* intervals between 5 and 60 seconds are imaginable as a choice for the user +* provide a setting in the app for that - possibly displaying an educated guess on remaining battery life with each setting; taking into account the following: +* the user's device (battery level, capacity, average idle power drain) +* statistics provided by the backend (how power-intense are the different intervals on the average of devices) +* possibly taking android-provided expected time into account - see Intent.ACTION_POWER_USAGE_SUMMARY + +--- + +Authors: +Adrian Riedel (D064867) +Daniel Wagner (D065058) \ No newline at end of file diff --git a/wiki/info/mobile/energy-consumption/Comparison3G2G.png b/wiki/info/mobile/energy-consumption/Comparison3G2G.png new file mode 100644 index 00000000000..865d117e766 Binary files /dev/null and b/wiki/info/mobile/energy-consumption/Comparison3G2G.png differ diff --git a/wiki/info/mobile/energy-consumption/ComparisonAPIs.png b/wiki/info/mobile/energy-consumption/ComparisonAPIs.png new file mode 100644 index 00000000000..a150b00e266 Binary files /dev/null and b/wiki/info/mobile/energy-consumption/ComparisonAPIs.png differ diff --git a/wiki/info/mobile/energy-consumption/ComparisonConnectivity.png b/wiki/info/mobile/energy-consumption/ComparisonConnectivity.png new file mode 100644 index 00000000000..16dc6f49a55 Binary files /dev/null and b/wiki/info/mobile/energy-consumption/ComparisonConnectivity.png differ