Merge branch 'origin/master'

This commit is contained in:
fmittag committed 2014-06-26 13:05:40 +02:00
commit ab5f3ef1d2
155 files changed
+2805 -1237

No files matched your search

+1
View File
@@ -46,6 +46,7 @@ Like businesses, sailors need the latest information to make strategic decisions
* [[Project Planning (bigger development)|wiki/planning]]
* [[General Event Planning|wiki/general-event-planning]]
* [[Information about Extreme Sailing Series|wiki/extreme-sailing-series]]
* [[Kieler Woche 2014 event page|wiki/kielerwoche2014]]
### Internal services (not related to wiki but useful)
@@ -0,0 +1,18 @@
package com.sap.sailing.domain.common;
public interface Bounds {
Position getNorthEast();
Position getSouthWest();
Position getNorthWest();
Position getSouthEast();
Bounds intersect(Bounds other);
Bounds union(Bounds other);
boolean intersects(Bounds other);
boolean contains(Position other);
boolean contains(Bounds other);
boolean isCrossesDateLine();
boolean containsLatDeg(double latDeg);
boolean containsLngDeg(double lngDeg);
boolean isEmpty();
}
@@ -36,7 +36,7 @@ public enum DetailType implements Serializable {
MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS(1, DESCENDING),
TOTAL_DISTANCE_TRAVELED(0, ASCENDING), TOTAL_AVERAGE_SPEED_OVER_GROUND(2, DESCENDING),
TOTAL_TIME_SAILED_IN_SECONDS(1, ASCENDING), RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS(2, DESCENDING),
RACE_TOTAL_POINTS(2, ASCENDING), REGATTA_TOTAL_POINTS(2, ASCENDING),
RACE_TOTAL_POINTS(2, ASCENDING), REGATTA_TOTAL_POINTS(2, ASCENDING), REGATTA_TOTAL_POINTS_SUM(2, ASCENDING),
RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL(1, ASCENDING), RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START(1, ASCENDING),
RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START(2, DESCENDING),
RACE_TIME_TRAVELED(0, ASCENDING), RACE_TIME_TRAVELED_UPWIND(0, ASCENDING), RACE_TIME_TRAVELED_DOWNWIND(0, ASCENDING), RACE_TIME_TRAVELED_REACHING(0, ASCENDING),
@@ -0,0 +1,166 @@
package com.sap.sailing.domain.common.impl;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
public class BoundsImpl implements Bounds {
private final Position sw;
private final Position ne;
private final boolean crossesDateLine;
public BoundsImpl(Position sw, Position ne) {
super();
this.sw = sw;
this.ne = ne;
crossesDateLine = ne.getLngDeg() < sw.getLngDeg();
}
@Override
public Position getNorthWest() {
return new DegreePosition(getNorthEast().getLatDeg(), getSouthWest().getLngDeg());
}
@Override
public Position getSouthEast() {
return new DegreePosition(getSouthWest().getLatDeg(), getNorthEast().getLngDeg());
}
@Override
public Position getNorthEast() {
return ne;
}
@Override
public Position getSouthWest() {
return sw;
}
/**
* Considering whether or not this bounds object crosses the date line, calculates the width in degrees longitude
* going from <code>west</code> to <code>east</code>. The result is always a non-negative number.
*/
private double getDistanceDeg(final double west, final double east) {
final double diff = east-west;
final double result;
if (isCrossingDateLine(west, east)) {
result = 360+diff; // diff is negative in this case
} else {
result = diff;
}
return result;
}
@Override
public Bounds intersect(Bounds other) {
final double maxSouthLatDeg = Math.max(getSouthWest().getLatDeg(), other.getSouthWest().getLatDeg());
final double minNorthLatDeg = Math.min(getNorthEast().getLatDeg(), other.getNorthEast().getLatDeg());
final double westDeg;
if (containsLngDeg(other.getSouthWest().getLngDeg())) {
westDeg = other.getSouthWest().getLngDeg();
} else if (other.containsLngDeg(getSouthWest().getLngDeg())) {
westDeg = getSouthWest().getLngDeg();
} else {
// no intersection
westDeg = getSouthWest().getLngDeg();
}
final double eastDeg;
if (containsLngDeg(other.getNorthEast().getLngDeg())) {
eastDeg = other.getNorthEast().getLngDeg();
} else if (other.containsLngDeg(getNorthEast().getLngDeg())) {
eastDeg = getNorthEast().getLngDeg();
} else {
// no intersection
eastDeg = getSouthWest().getLngDeg();
}
return new BoundsImpl(new DegreePosition(maxSouthLatDeg, westDeg), new DegreePosition(minNorthLatDeg, eastDeg));
}
@Override
public Bounds union(Bounds other) {
final double minLatDeg = Math.min(getSouthWest().getLatDeg(), other.getSouthWest().getLatDeg());
final double maxLatDeg = Math.max(getNorthEast().getLatDeg(), other.getNorthEast().getLatDeg());
// For lng, we can go left or right around the earth; which way results in the smaller bounds? We have four options:
final double[] west = new double[] { getSouthWest().getLngDeg(), other.getSouthWest().getLngDeg() };
final double[] east = new double[] { getNorthEast().getLngDeg(), other.getNorthEast().getLngDeg() };
double minLngDegDistance = Double.MAX_VALUE;
double bestWest = 0;
double bestEast = 0;
for (int w=0; w<2; w++) {
for (int e=0; e<2; e++) {
double currentLngDegDistance;
if (spansLngDeg(west[w], east[e], west[1 - w]) && spansLngDeg(west[w], east[e], east[1 - e])
&& (currentLngDegDistance = Math.abs(getDistanceDeg(west[w], east[e]))) < minLngDegDistance) {
minLngDegDistance = currentLngDegDistance;
bestWest = west[w];
bestEast = east[e];
}
}
}
return new BoundsImpl(new DegreePosition(minLatDeg, bestWest), new DegreePosition(maxLatDeg, bestEast));
}
private boolean spansLngDeg(double westLngDeg, double eastLngDeg, double lngDeg) {
return isCrossingDateLine(westLngDeg, eastLngDeg)
? (lngDeg >= westLngDeg && lngDeg <= 180) || (lngDeg >= -180 && lngDeg <= eastLngDeg)
: westLngDeg <= lngDeg && lngDeg <= eastLngDeg;
}
private boolean isCrossingDateLine(double westLngDeg, double eastLngDeg) {
return westLngDeg > eastLngDeg;
}
@Override
public boolean intersects(Bounds other) {
return (containsLatDeg(other.getSouthWest().getLatDeg()) || containsLatDeg(other.getNorthEast().getLatDeg()) ||
other.containsLatDeg(getSouthWest().getLatDeg()) || other.containsLatDeg(getNorthEast().getLatDeg()))
&& (containsLngDeg(other.getSouthWest().getLngDeg()) || containsLngDeg(other.getNorthEast().getLngDeg()) ||
other.containsLngDeg(getSouthWest().getLngDeg()) || other.containsLngDeg(getNorthEast().getLngDeg()));
}
@Override
public boolean containsLatDeg(double latDeg) {
return latDeg >= getSouthWest().getLatDeg() && latDeg <= getNorthEast().getLatDeg();
}
@Override
public boolean containsLngDeg(double lngDeg) {
return spansLngDeg(getSouthWest().getLngDeg(), getNorthEast().getLngDeg(), lngDeg);
}
@Override
public boolean contains(Position other) {
return
// lat contained:
other.getLatDeg() >= getSouthWest().getLatDeg() && other.getLatDeg() <= getNorthEast().getLatDeg() &&
// lng contained:
(isCrossesDateLine()
// between SW and date line...
? (other.getLngDeg() <= 180 && other.getLngDeg() >= getSouthWest().getLngDeg()) ||
// ...or between date line and NE
(other.getLngDeg() >= -180 && other.getLngDeg() <= getNorthEast().getLngDeg())
// these bounds are not crossing the date line; simple numeric comparison
: other.getLngDeg() >= getSouthWest().getLngDeg() && other.getLngDeg() <= getNorthEast().getLngDeg())
;
}
@Override
public boolean contains(Bounds other) {
return isCrossesDateLine() == other.isCrossesDateLine() && contains(other.getNorthEast()) && contains(other.getSouthWest());
}
@Override
public boolean isCrossesDateLine() {
return crossesDateLine;
}
@Override
public boolean isEmpty() {
return getSouthWest().getLatDeg() >= getNorthEast().getLatDeg() ||
getSouthWest().getLngDeg() == getNorthEast().getLngDeg();
}
@Override
public String toString() {
return "{SW: "+getSouthWest().toString()+", NE: "+getNorthEast().toString()+"}";
}
}
@@ -34,4 +34,9 @@ public class FlagPole {
this.isDisplayed == otherPole.isDisplayed;
}
public boolean describesSameButForDisplayed(FlagPole otherPole) {
return this.upperFlag == otherPole.upperFlag &&
this.lowerFlag == otherPole.lowerFlag;
}
}
@@ -9,7 +9,9 @@ import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.igtimiadapter.datatypes.AntHrm;
@@ -19,6 +21,8 @@ import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualitySatCount;
import com.sap.sse.common.Util;
public class FixFactoryTest {
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(2 * 60 * 1000);
private static final String demoJson = "{\n"+
"\"EA-AK-AAAG\": {\n"+
"\"1\": {\n"+
@@ -8,7 +8,9 @@ import java.net.URISyntaxException;
import org.apache.http.client.ClientProtocolException;
import org.json.simple.parser.ParseException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import com.sap.sailing.domain.igtimiadapter.persistence.PersistenceFactory;
import com.sap.sailing.mongodb.MongoDBConfiguration;
@@ -16,6 +18,8 @@ import com.sap.sailing.mongodb.MongoDBService;
import com.sap.sse.common.Util;
public class WebSocketServerListTest {
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(2 * 60 * 1000);
@Test
public void testWebSocketServerList() throws IllegalStateException, ClientProtocolException, IOException, ParseException, URISyntaxException {
final ClientImpl testAppClient = new ClientImpl("7fcdd217e0aa16090edb4ad55b09ec43b2021090e209541fc9b7003c2a2b70c6",
@@ -10,7 +10,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import com.sap.sailing.declination.DeclinationService;
import com.sap.sailing.domain.common.SpeedWithBearing;
@@ -29,6 +31,8 @@ import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
import com.sap.sailing.domain.tracking.Wind;
public class WindReceiverTest {
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(2 * 60 * 1000);
@Test
public void simpleWindReceiverTest() {
final List<Wind> windReceived = new ArrayList<>();
@@ -11,7 +11,9 @@ import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.client.ClientProtocolException;
import org.json.simple.parser.ParseException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.xml.sax.SAXException;
import com.sap.sailing.domain.igtimiadapter.Account;
@@ -27,6 +29,8 @@ import com.sap.sailing.mongodb.MongoDBService;
public class SignInTest {
private static final Logger logger = Logger.getLogger(SignInTest.class.getName());
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(2 * 60 * 1000);
@Test
public void testSimpleSignIn() throws ClientProtocolException, IOException, IllegalStateException,
ParserConfigurationException, SAXException, ClassNotFoundException, InstantiationException,
@@ -4,6 +4,8 @@ import java.io.IOException;
import org.apache.http.client.ClientProtocolException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.Timeout;
import com.sap.sailing.domain.igtimiadapter.Account;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
@@ -13,6 +15,8 @@ import com.sap.sailing.domain.igtimiadapter.impl.Activator;
public class AbstractTestWithIgtimiConnection {
protected IgtimiConnection connection;
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(2 * 60 * 1000);
@Before
public void setUp() throws ClientProtocolException, IOException, org.json.simple.parser.ParseException {
final IgtimiConnectionFactory connectionFactory = Activator.getInstance().getConnectionFactory();
@@ -378,8 +378,14 @@ public class IgtimiWindReceiver implements BulkFixReceiver {
if (declinationService == null) {
trueHeading = hdgm;
} else {
Declination declination = declinationService.getDeclination(timePoint, position, /* timeoutForOnlineFetchInMilliseconds 5s */ 5000);
trueHeading = hdgm.add(declination.getBearingCorrectedTo(timePoint));
try {
Declination declination = declinationService.getDeclination(timePoint, position, /* timeoutForOnlineFetchInMilliseconds 5s */ 5000);
trueHeading = hdgm.add(declination.getBearingCorrectedTo(timePoint));
} catch (Exception e) {
logger.log(Level.SEVERE, "Correction of declination was requested but unsuccessful. Can't correct heading "+
hdgm+"@"+timePoint+" by declination. Forwarding exception.");
throw e;
}
}
} else {
trueHeading = null;
@@ -154,7 +154,11 @@ public class WebSocketConnectionManager extends WebSocketAdapter implements Live
private void notifyListeners(List<Fix> fixes) {
for (BulkFixReceiver listener : listeners.keySet()) {
listener.received(fixes);
try {
listener.received(fixes);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error notifying listener "+listener+" of Igtimi fixes "+fixes, e);
}
}
}
@@ -1,8 +1,10 @@
package com.sap.sailing.domain.persistence;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.mongodb.DBObject;
import com.sap.sailing.domain.base.Competitor;
@@ -123,4 +125,6 @@ public interface DomainObjectFactory {
DomainFactory getBaseDomainFactory();
Iterable<Entry<DeviceConfigurationMatcher, DeviceConfiguration>> loadAllDeviceConfigurations();
Map<String, Set<URL>> loadResultUrls();
}
@@ -1,6 +1,7 @@
package com.sap.sailing.domain.persistence;
import java.io.Serializable;
import java.net.URL;
import com.mongodb.DB;
import com.mongodb.DBObject;
@@ -131,4 +132,8 @@ public interface MongoObjectFactory {
void removeDeviceConfiguration(DeviceConfigurationMatcher matcher);
void removeRaceLog(RaceLogIdentifier identifier);
void storeResultUrl(String resultProviderName, URL url);
void removeResultUrl(String resultProviderName, URL url);
}
@@ -87,4 +87,9 @@ public enum CollectionNames {
* Metadata for the GPSFixes, grouped by Device Identifier.
*/
GPS_FIXES_METADATA,
/**
* URLs providing race results
*/
RESULT_URLS,
}
@@ -1151,10 +1151,10 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
Boolean hasSplitFleetContiguousScoring = (Boolean) dbSeries.get(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name());
Boolean firstColumnIsNonDiscardableCarryForward = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name());
final BasicDBList dbFleets = (BasicDBList) dbSeries.get(FieldNames.SERIES_FLEETS.name());
Map<String, Fleet> fleetsByName = loadFleets(dbFleets);
List<Fleet> fleets = loadFleets(dbFleets);
BasicDBList dbRaceColumns = (BasicDBList) dbSeries.get(FieldNames.SERIES_RACE_COLUMNS.name());
Iterable<String> raceColumnNames = loadRaceColumnNames(dbRaceColumns, fleetsByName);
Series series = new SeriesImpl(name, isMedal, fleetsByName.values(), raceColumnNames, trackedRegattaRegistry);
Iterable<String> raceColumnNames = loadRaceColumnNames(dbRaceColumns);
Series series = new SeriesImpl(name, isMedal, fleets, raceColumnNames, trackedRegattaRegistry);
if (dbSeries.get(FieldNames.SERIES_DISCARDING_THRESHOLDS.name()) != null) {
ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbSeries, FieldNames.SERIES_DISCARDING_THRESHOLDS);
series.setResultDiscardingRule(resultDiscardingRule);
@@ -1172,11 +1172,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
return series;
}
/**
* @param fleetsByName used to ensure the {@link RaceColumn#getFleets()} points to the same {@link Fleet} objects also
* used in the {@link Series#getFleets()} collection.
*/
private Iterable<String> loadRaceColumnNames(BasicDBList dbRaceColumns, Map<String, Fleet> fleetsByName) {
private Iterable<String> loadRaceColumnNames(BasicDBList dbRaceColumns) {
List<String> result = new ArrayList<String>();
for (Object o : dbRaceColumns) {
DBObject dbRaceColumn = (DBObject) o;
@@ -1201,12 +1197,12 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
}
private Map<String, Fleet> loadFleets(BasicDBList dbFleets) {
Map<String, Fleet> result = new HashMap<String, Fleet>();
private List<Fleet> loadFleets(BasicDBList dbFleets) {
List<Fleet> result = new ArrayList<Fleet>();
for (Object o : dbFleets) {
DBObject dbFleet = (DBObject) o;
Fleet fleet = loadFleet(dbFleet);
result.put(fleet.getName(), fleet);
result.add(fleet);
}
return result;
}
@@ -1760,4 +1756,27 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
stringRepresentation, deviceType, stringRepresentation);
}
}
@Override
public Map<String, Set<URL>> loadResultUrls() {
Map<String, Set<URL>> resultUrls = new HashMap<>();
DBCollection resultUrlCollection = database.getCollection(CollectionNames.RESULT_URLS.name());
for (DBObject dbObject : resultUrlCollection.find()) {
String providerName = (String) dbObject.get(FieldNames.RESULT_PROVIDERNAME.name());
String urlString = (String) dbObject.get(FieldNames.RESULT_URL.name());
URL url;
try {
url = new URL(urlString);
} catch (MalformedURLException e) {
logger.log(Level.SEVERE, "Failed to parse result Url String: " + urlString + ". Did not load url!");
continue;
}
if (!resultUrls.containsKey(providerName)) {
resultUrls.put(providerName, new HashSet<URL>());
}
Set<URL> set = resultUrls.get(providerName);
set.add(url);
}
return resultUrls;
}
}
@@ -108,4 +108,7 @@ public enum FieldNames {
// Timespan
FROM_MILLIS, TO_MILLIS,
// Result Import url fields
RESULT_PROVIDERNAME, RESULT_URL
}
@@ -1225,4 +1225,21 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
storeRaceLogIdentifier(identifier, query);
getRaceLogCollection().remove(query);
}
@Override
public void storeResultUrl(String resultProviderName, URL url) {
DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name());
DBObject query = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName);
DBObject entry = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName);
entry.put(FieldNames.RESULT_URL.name(), url.toString());
resultUrlsCollection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE);
}
@Override
public void removeResultUrl(String resultProviderName, URL url) {
DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name());
DBObject query = new BasicDBObjectBuilder().add(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName)
.add(FieldNames.RESULT_URL.name(), url.toString()).get();
resultUrlsCollection.remove(query);
}
}
@@ -1,7 +1,6 @@
package com.sap.sailing.domain.racelogtracking.test.impl;
import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.Collections;
@@ -31,7 +30,7 @@ import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifie
import com.sap.sailing.domain.racelogtracking.test.AbstractGPSFixStoreTest;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.TrackedRegatta;
import com.sap.sailing.domain.tracking.impl.BaseRaceChangeListener;
import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.TrackedRegattaImpl;
@@ -39,7 +38,8 @@ import com.sap.sailing.domain.tracking.impl.TrackedRegattaImpl;
public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
private final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er");
private boolean finishedLoadingFirstBeforeSecond = true;
private int numberOfSwitchesBetweenLoadingRaceLogs = 0;
private Integer previouslyLoading = null;
private int numFixesReceived = 0;
@Test
@@ -114,19 +114,16 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
DynamicTrackedRaceImpl trackedRace = new DynamicTrackedRaceImpl(regatta, race, Collections.<Sideline>emptyList(),
EmptyWindStore.INSTANCE, store, 0, 0, 0);
trackedRace.addListener(new BaseRaceChangeListener() {
boolean currentlyLoadingFirst = true;
trackedRace.addListener(new AbstractRaceChangeListener() {
@Override
public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) {
numFixesReceived++;
if (currentlyLoadingFirst) {
if (fix.getTimePoint().asMillis() > numFixes/2) {
currentlyLoadingFirst = false;
}
} else {
if (fix.getTimePoint().asMillis() <= numFixes/2) {
finishedLoadingFirstBeforeSecond = false;
}
int currentlyLoading = fix.getTimePoint().asMillis() <= numFixes/2 ? 1 : 2;
if (previouslyLoading == null) {
previouslyLoading = currentlyLoading;
} else if (previouslyLoading != currentlyLoading) {
previouslyLoading = currentlyLoading;
numberOfSwitchesBetweenLoadingRaceLogs++;
}
}
});
@@ -137,7 +134,7 @@ public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest {
trackedRace.waitForLoadingFromGPSFixStoreToFinishRunning(raceLog);
trackedRace.waitForLoadingFromGPSFixStoreToFinishRunning(raceLog2);
assertTrue(finishedLoadingFirstBeforeSecond);
assertEquals(1, numberOfSwitchesBetweenLoadingRaceLogs);
assertEquals(numFixes, numFixesReceived);
}
}
@@ -124,6 +124,9 @@ public class SharedDomainFactoryImpl implements SharedDomainFactory {
@Override
public Nationality getOrCreateNationality(String threeLetterIOCCode) {
if (threeLetterIOCCode == null) {
threeLetterIOCCode = " ";
}
synchronized (nationalityCache) {
Nationality result = nationalityCache.get(threeLetterIOCCode);
if (result == null) {
@@ -1,17 +1,25 @@
package com.sap.sailing.domain.racelog.analyzing.impl;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.racelog.Flags;
import com.sap.sailing.domain.racelog.RaceLog;
import com.sap.sailing.domain.racelog.RaceLogFlagEvent;
public class IndividualRecallDisplayedFinder extends IndividualRecallFinder {
private final TimePoint at;
public IndividualRecallDisplayedFinder(RaceLog raceLog) {
this(raceLog, null);
}
public IndividualRecallDisplayedFinder(RaceLog raceLog, TimePoint at) {
super(raceLog);
this.at = at;
}
protected boolean isRelevant(RaceLogFlagEvent flagEvent) {
return flagEvent.getUpperFlag().equals(Flags.XRAY) && flagEvent.isDisplayed();
boolean disregardBecauseAfterAt = at != null && flagEvent.getTimePoint().after(at);
return ! disregardBecauseAfterAt && flagEvent.getUpperFlag().equals(Flags.XRAY) && flagEvent.isDisplayed();
}
}
@@ -1,17 +1,26 @@
package com.sap.sailing.domain.racelog.analyzing.impl;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.racelog.Flags;
import com.sap.sailing.domain.racelog.RaceLog;
import com.sap.sailing.domain.racelog.RaceLogFlagEvent;
public class IndividualRecallRemovedFinder extends IndividualRecallFinder {
private final TimePoint at;
public IndividualRecallRemovedFinder(RaceLog raceLog) {
this(raceLog, null);
}
public IndividualRecallRemovedFinder(RaceLog raceLog, TimePoint at) {
super(raceLog);
this.at = at;
}
protected boolean isRelevant(RaceLogFlagEvent flagEvent) {
return flagEvent.getUpperFlag().equals(Flags.XRAY) && !flagEvent.isDisplayed();
boolean disregardBecauseAfterAt = at != null && flagEvent.getTimePoint().after(at);
return ! disregardBecauseAfterAt && flagEvent.getUpperFlag().equals(Flags.XRAY) && !flagEvent.isDisplayed();
}
}
@@ -9,11 +9,15 @@ public class IsIndividualRecallDisplayedAnalyzer extends RaceLogAnalyzer<Boolean
private final IndividualRecallFinder removedFinder;
public IsIndividualRecallDisplayedAnalyzer(RaceLog raceLog) {
this(raceLog, new IndividualRecallDisplayedFinder(raceLog), new IndividualRecallRemovedFinder(raceLog));
this(raceLog, null);
}
public IsIndividualRecallDisplayedAnalyzer(RaceLog raceLog, TimePoint at) {
this(raceLog, new IndividualRecallDisplayedFinder(raceLog, at), new IndividualRecallRemovedFinder(raceLog, at), at);
}
public IsIndividualRecallDisplayedAnalyzer(RaceLog raceLog, IndividualRecallDisplayedFinder displayedFinder,
IndividualRecallRemovedFinder removedFinder) {
IndividualRecallRemovedFinder removedFinder, TimePoint at) {
super(raceLog);
this.displayedFinder = displayedFinder;
this.removedFinder = removedFinder;
@@ -8,25 +8,29 @@ import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.racelog.FlagPole;
/**
* Allows you to query the current and state of displayed and remove flags.
* Allows you to query the current state of displayed and removed flags.
*/
public class FlagPoleState {
private final List<FlagPole> currentState;
private final List<FlagPole> nextState;
private final TimePoint currentStateValidFrom;
private final TimePoint nextStateValidFrom;
private final boolean hasNextState;
public FlagPoleState(List<FlagPole> currentState) {
this(currentState, null, null, false);
public FlagPoleState(List<FlagPole> currentState, TimePoint currentStateValidFrom) {
this(currentState, currentStateValidFrom, null, null, false);
}
public FlagPoleState(List<FlagPole> currentState, List<FlagPole> nextState, TimePoint nextStateValidFrom) {
this(currentState, nextState, nextStateValidFrom, true);
public FlagPoleState(List<FlagPole> currentState, TimePoint currentStateValidFrom,
List<FlagPole> nextState, TimePoint nextStateValidFrom) {
this(currentState, currentStateValidFrom, nextState, nextStateValidFrom, true);
}
private FlagPoleState(List<FlagPole> currentState, List<FlagPole> nextState, TimePoint nextStateValidFrom, boolean hasNextState) {
private FlagPoleState(List<FlagPole> currentState, TimePoint currentStateValidFrom,
List<FlagPole> nextState, TimePoint nextStateValidFrom, boolean hasNextState) {
this.currentState = currentState;
this.currentStateValidFrom = currentStateValidFrom;
this.nextState = nextState;
this.nextStateValidFrom = nextStateValidFrom;
this.hasNextState = hasNextState;
@@ -42,30 +46,51 @@ public class FlagPoleState {
public TimePoint getNextStateValidFrom() {
return nextStateValidFrom;
}
public TimePoint getCurrentStateValidFrom() {
return currentStateValidFrom;
}
public boolean hasNextState() {
return hasNextState;
}
public static List<FlagPole> computeChanges(List<FlagPole> before, List<FlagPole> after) {
before = before == null ? Collections.<FlagPole>emptyList() : before;
after = after == null ? Collections.<FlagPole>emptyList() : after;
List<FlagPole> beforeWithoutMatchInAfter = new ArrayList<FlagPole>(before);
List<FlagPole> changes = new ArrayList<FlagPole>();
for (FlagPole poleAfter : after) {
boolean hasSamePoleBefore = false;
for (FlagPole poleBefore : before) {
if (poleAfter.describesSameButForDisplayed(poleBefore)) {
beforeWithoutMatchInAfter.remove(poleBefore);
}
if (poleAfter.describesSame(poleBefore)) {
hasSamePoleBefore = true;
break;
}
}
if (!hasSamePoleBefore) {
changes.add(poleAfter);
}
}
for (FlagPole poleBefore : beforeWithoutMatchInAfter) {
if (poleBefore.isDisplayed()) {
changes.add(new FlagPole(poleBefore.getUpperFlag(), poleBefore.getLowerFlag(), false));
}
}
return changes;
}
public List<FlagPole> computeUpcomingChanges() {
if (!hasNextState) {
return Collections.emptyList();
}
List<FlagPole> changes = new ArrayList<FlagPole>();
for (FlagPole nextPole : nextState) {
boolean hasSamePoleInCurrentState = false;
for (FlagPole currentPole : currentState) {
if (nextPole.describesSame(currentPole)) {
hasSamePoleInCurrentState = true;
break;
}
}
if (!hasSamePoleInCurrentState) {
changes.add(nextPole);
}
}
return changes;
return computeChanges(currentState, nextState);
}
public static boolean describesSameState(FlagPoleState left, FlagPoleState right) {
@@ -99,5 +124,52 @@ public class FlagPoleState {
return true;
}
public FlagPoleState getPreviousState(ReadonlyRacingProcedure racingProcedure, TimePoint startTime) {
if (currentStateValidFrom == null) {
return this;
}
return racingProcedure.getActiveFlags(startTime, currentStateValidFrom.minus(1));
}
public static FlagPole getMostInterestingFlagPole(List<FlagPole> poles) {
return getMostInterestingFlagPole(poles, poles);
}
public static FlagPole getMostInterestingFlagPole(List<FlagPole> previousPoles, List<FlagPole> currentPoles) {
List<FlagPole> changes = computeChanges(previousPoles, currentPoles);
FlagPole poleRemoved = null;
for (FlagPole changedPole : changes) {
if (changedPole.isDisplayed()) {
return changedPole;
} else {
poleRemoved = changedPole;
}
}
if (poleRemoved != null) {
return poleRemoved;
}
for (FlagPole currentPole : currentPoles) {
if (currentPole.isDisplayed()) {
return currentPole;
}
}
return currentPoles.size() == 0 ? null : currentPoles.get(0);
}
public boolean hasPoleChanged(FlagPole newPole) {
boolean found = false;
for (FlagPole current : currentState) {
if (current.describesSameButForDisplayed(newPole)) {
found = true;
if (current.isDisplayed() != newPole.isDisplayed()) {
return true;
}
}
}
if (! found && newPole.isDisplayed()) {
return true;
}
return false;
}
}
@@ -68,6 +68,16 @@ public interface ReadonlyRacingProcedure extends RaceStateEventProcessor, RaceSt
*/
boolean isIndividualRecallDisplayed();
/**
* Returns <code>true</code> if there is an individual recall signaled in the {@link RaceLog} at the TimePoint {@code at}.
*/
boolean isIndividualRecallDisplayed(TimePoint at);
/**
* Gets the time the individual recall flag was displayed (or <code>null</code>).
*/
TimePoint getIndividualRecallDisplayedTime();
/**
* Gets the time the individual recall flag was removed (or <code>null</code>).
*/
@@ -47,7 +47,7 @@ public class BasicRacingProcedureImpl extends BaseRacingProcedure {
@Override
public FlagPoleState getActiveFlags(TimePoint startTime, TimePoint now) {
return new FlagPoleState(Collections.<FlagPole>emptyList());
return new FlagPoleState(Collections.<FlagPole>emptyList(), null);
}
@Override
@@ -91,37 +91,43 @@ public class ESSRacingProcedureImpl extends BaseRacingProcedure implements ESSRa
public FlagPoleState getActiveFlags(TimePoint startTime, TimePoint now) {
if (now.before(startTime.minus(startPhaseAPDownInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.AP, true), new FlagPole(Flags.ESSTHREE, false)),
Arrays.asList(new FlagPole(Flags.AP, true), new FlagPole(Flags.ESSTHREE, false)), null,
Arrays.asList(new FlagPole(Flags.AP, false), new FlagPole(Flags.ESSTHREE, false)),
startTime.minus(startPhaseAPDownInterval));
} else if (now.before(startTime.minus(startPhaseESSThreeUpInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.AP, false), new FlagPole(Flags.ESSTHREE, false)),
Arrays.asList(new FlagPole(Flags.AP, false), new FlagPole(Flags.ESSTHREE, false)),
startTime.minus(startPhaseAPDownInterval),
Arrays.asList(new FlagPole(Flags.ESSTHREE, true), new FlagPole(Flags.ESSTWO, false)),
startTime.minus(startPhaseESSThreeUpInterval));
} else if (now.before(startTime.minus(startPhaseESSTwoUpInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.ESSTHREE, true), new FlagPole(Flags.ESSTWO, false)),
Arrays.asList(new FlagPole(Flags.ESSTHREE, true), new FlagPole(Flags.ESSTWO, false)),
startTime.minus(startPhaseESSThreeUpInterval),
Arrays.asList(new FlagPole(Flags.ESSTWO, true), new FlagPole(Flags.ESSONE, false)),
startTime.minus(startPhaseESSTwoUpInterval));
} else if (now.before(startTime.minus(startPhaseESSOneUpInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.ESSTWO, true), new FlagPole(Flags.ESSONE, false)),
Arrays.asList(new FlagPole(Flags.ESSTWO, true), new FlagPole(Flags.ESSONE, false)),
startTime.minus(startPhaseESSTwoUpInterval),
Arrays.asList(new FlagPole(Flags.ESSONE, true)),
startTime.minus(startPhaseESSOneUpInterval));
} if (now.before(startTime)) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.ESSONE, true)),
Arrays.asList(new FlagPole(Flags.ESSONE, true)),
startTime.minus(startPhaseESSOneUpInterval),
Arrays.asList(new FlagPole(Flags.ESSONE, false)),
startTime);
} else {
if (isIndividualRecallDisplayed()) {
if (isIndividualRecallDisplayed(now)) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.XRAY, true)),
getIndividualRecallDisplayedTime(),
Arrays.asList(new FlagPole(Flags.XRAY, false)),
getIndividualRecallRemovalTime());
} else {
return new FlagPoleState(Collections.<FlagPole>emptyList());
TimePoint recallRemoved = getIndividualRecallRemovalTime();
return new FlagPoleState(Collections.<FlagPole>emptyList(), recallRemoved == null ? startTime : recallRemoved);
}
}
}
@@ -146,24 +146,27 @@ public class GateStartRacingProcedureImpl extends BaseRacingProcedure implements
TimePoint gateShutdownTime = getGateShutdownTimePoint(startTime);
if (now.before(startTime.minus(startPhaseClassOverGolfUpIntervall))) {
return new FlagPoleState(Arrays.asList(new FlagPole(classFlag, Flags.GOLF, false), new FlagPole(Flags.PAPA,
false)), Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, false)),
false)), null, Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, false)),
startTime.minus(startPhaseClassOverGolfUpIntervall));
} else if (now.before(startTime.minus(startPhasePapaUpInterval))) {
return new FlagPoleState(Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA,
false)), Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, true)),
false)), startTime.minus(startPhaseClassOverGolfUpIntervall),
Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, true)),
startTime.minus(startPhasePapaUpInterval));
} else if (now.before(startTime.minus(startPhasePapaDownInterval))) {
return new FlagPoleState(Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA,
true)), Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, false)),
true)), startTime.minus(startPhasePapaUpInterval),
Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA, false)),
startTime.minus(startPhasePapaDownInterval));
} else if (now.before(startTime)) {
return new FlagPoleState(Arrays.asList(new FlagPole(classFlag, Flags.GOLF, true), new FlagPole(Flags.PAPA,
false)), Arrays.asList(new FlagPole(classFlag, false), new FlagPole(Flags.GOLF, true)), startTime);
false)), startTime.minus(startPhasePapaDownInterval),
Arrays.asList(new FlagPole(classFlag, false), new FlagPole(Flags.GOLF, true)), startTime);
} else if (now.before(gateShutdownTime)) {
return new FlagPoleState(Arrays.asList(new FlagPole(Flags.GOLF, true)), Arrays.asList(new FlagPole(
Flags.GOLF, false)), gateShutdownTime);
return new FlagPoleState(Arrays.asList(new FlagPole(Flags.GOLF, true)), startTime,
Arrays.asList(new FlagPole(Flags.GOLF, false)), gateShutdownTime);
} else {
return new FlagPoleState(Arrays.asList(new FlagPole(Flags.GOLF, false)));
return new FlagPoleState(Arrays.asList(new FlagPole(Flags.GOLF, false)), gateShutdownTime);
}
}
@@ -13,6 +13,7 @@ import com.sap.sailing.domain.racelog.RaceLogEventAuthor;
import com.sap.sailing.domain.racelog.RaceLogEventFactory;
import com.sap.sailing.domain.racelog.RaceLogEventVisitor;
import com.sap.sailing.domain.racelog.analyzing.impl.IndividualRecallDisplayedFinder;
import com.sap.sailing.domain.racelog.analyzing.impl.IndividualRecallRemovedFinder;
import com.sap.sailing.domain.racelog.analyzing.impl.IsIndividualRecallDisplayedAnalyzer;
import com.sap.sailing.domain.racelog.impl.RaceLogChangedVisitor;
import com.sap.sailing.domain.racelog.state.RaceStateEvent;
@@ -46,6 +47,7 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i
private final RacingProcedureChangedListeners<? extends RacingProcedureChangedListener> changedListeners;
private final IsIndividualRecallDisplayedAnalyzer isRecallDisplayedAnalyzer;
private final IndividualRecallDisplayedFinder recallDisplayedFinder;
private final IndividualRecallRemovedFinder recallRemovedFinder;
private final RaceLogEventVisitor raceLogListener;
private boolean cachedIsIndividualRecallDisplayed;
@@ -67,6 +69,7 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i
this.changedListeners = createChangedListenerContainer();
this.isRecallDisplayedAnalyzer = new IsIndividualRecallDisplayedAnalyzer(raceLog);
this.recallDisplayedFinder = new IndividualRecallDisplayedFinder(raceLog);
this.recallRemovedFinder = new IndividualRecallRemovedFinder(raceLog);
this.raceLogListener = new RaceLogChangedVisitor(this);
this.raceLog.addListener(raceLogListener);
@@ -126,17 +129,34 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i
public boolean isIndividualRecallDisplayed() {
return cachedIsIndividualRecallDisplayed;
}
@Override
public boolean isIndividualRecallDisplayed(TimePoint at) {
if (hasIndividualRecall()) {
return new IsIndividualRecallDisplayedAnalyzer(getRaceLog(), at).analyze();
}
return false;
}
@Override
public TimePoint getIndividualRecallDisplayedTime() {
if (hasIndividualRecall()) {
return recallDisplayedFinder.analyze();
}
return null;
}
@Override
public TimePoint getIndividualRecallRemovalTime() {
TimePoint displayed = getIndividualRecallDisplayedTime();
if (hasIndividualRecall()) {
if (isIndividualRecallDisplayed()) {
TimePoint displayTime = recallDisplayedFinder.analyze();
if (displayTime != null) {
return displayTime.plus(individualRecallRemovalTimeout);
}
TimePoint removedEvent = recallRemovedFinder.analyze();
if (removedEvent != null && (displayed == null || removedEvent.after(displayed))) {
return removedEvent;
}
}
if (displayed != null) {
return displayed.plus(individualRecallRemovalTimeout);
}
return null;
}
@@ -177,18 +197,22 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i
protected void update() {
boolean isRecallDisplayed = isRecallDisplayedAnalyzer.analyze();
if (cachedIsIndividualRecallDisplayed != isRecallDisplayed) {
cachedIsIndividualRecallDisplayed = isRecallDisplayed;
if (cachedIsIndividualRecallDisplayed) {
changedListeners.onIndividualRecallDisplayed(this);
changedListeners.onActiveFlagsChanged(this);
rescheduleIndividualRecallTimeout(recallDisplayedFinder.analyze());
} else {
changedListeners.onIndividualRecallRemoved(this);
changedListeners.onActiveFlagsChanged(this);
unscheduleStateEvent(RaceStateEvents.INDIVIDUAL_RECALL_TIMEOUT);
}
}
// always call listeners for changed flag, as this does not only affect recal, but also
// changes start procedure, for which some text elements need to be updated
// ({@link BaseRaceInfoFragmen#renderFlagChangesCountdown}
changedListeners.onActiveFlagsChanged(this);
}
private void rescheduleIndividualRecallTimeout(TimePoint displayTime) {
@@ -126,32 +126,38 @@ public class RRS26RacingProcedureImpl extends BaseRacingProcedure implements RRS
getConfiguration().getClassFlag() : Flags.CLASS;
if (now.before(startTime.minus(startPhaseClassUpInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(classFlag, false), new FlagPole(cachedStartmodeFlag, false)),
Arrays.asList(new FlagPole(classFlag, false), new FlagPole(cachedStartmodeFlag, false)),
null,
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
startTime.minus(startPhaseClassUpInterval));
} else if (now.before(startTime.minus(startPhaseStartModeUpInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
startTime.minus(startPhaseClassUpInterval),
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, true)),
startTime.minus(startPhaseStartModeUpInterval));
} else if (now.before(startTime.minus(startPhaseStartModeDownInterval))) {
return new FlagPoleState(
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, true)),
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, true)),
startTime.minus(startPhaseStartModeUpInterval),
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
startTime.minus(startPhaseStartModeDownInterval));
} else if (now.before(startTime)) {
return new FlagPoleState(
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
Arrays.asList(new FlagPole(classFlag, true), new FlagPole(cachedStartmodeFlag, false)),
startTime.minus(startPhaseStartModeDownInterval),
Arrays.asList(new FlagPole(classFlag, false), new FlagPole(cachedStartmodeFlag, false)),
startTime);
} else {
if (isIndividualRecallDisplayed()) {
if (isIndividualRecallDisplayed(now)) {
return new FlagPoleState(
Arrays.asList(new FlagPole(Flags.XRAY, true)),
getIndividualRecallDisplayedTime(),
Arrays.asList(new FlagPole(Flags.XRAY, false)),
getIndividualRecallRemovalTime());
} else {
return new FlagPoleState(Collections.<FlagPole>emptyList());
TimePoint recallRemoved = getIndividualRecallRemovalTime();
return new FlagPoleState(Collections.<FlagPole>emptyList(), recallRemoved == null ? startTime : recallRemoved);
}
}
}
@@ -0,0 +1,228 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.impl.BoundsImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
public class BoundsTest {
@Test
public void simpleContains() {
Bounds b = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
assertTrue(b.contains(new DegreePosition(2, 2)));
assertFalse(b.contains(new DegreePosition(4, 4)));
assertFalse(b.contains(new DegreePosition(0, 0)));
assertFalse(b.contains(new DegreePosition(0, 2)));
assertFalse(b.contains(new DegreePosition(2, 0)));
assertFalse(b.contains(new DegreePosition(2, 4)));
}
@Test
public void crossDateLineContains() {
Bounds b = new BoundsImpl(new DegreePosition(1, 179), new DegreePosition(3, -179));
assertTrue(b.contains(new DegreePosition(2, 180)));
assertFalse(b.contains(new DegreePosition(4, -178)));
assertFalse(b.contains(new DegreePosition(0, 178)));
assertFalse(b.contains(new DegreePosition(0, 180)));
assertFalse(b.contains(new DegreePosition(2, 178)));
assertFalse(b.contains(new DegreePosition(2, -178)));
}
@Test
public void simpleUnionTestOverlap() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 2), new DegreePosition(4, 4));
Bounds u = b1.union(b2);
assertEquals(1.0, u.getSouthWest().getLatDeg(), 0.00000001);
assertEquals(1.0, u.getSouthWest().getLngDeg(), 0.00000001);
assertEquals(4.0, u.getNorthEast().getLatDeg(), 0.00000001);
assertEquals(4.0, u.getNorthEast().getLngDeg(), 0.00000001);
}
@Test
public void testDatelineUnionOverlap() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 179), new DegreePosition(3, -179));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 180), new DegreePosition(4, -178));
Bounds u = b1.union(b2);
assertEquals(1.0, u.getSouthWest().getLatDeg(), 0.00000001);
assertEquals(179, u.getSouthWest().getLngDeg(), 0.00000001);
assertEquals(4.0, u.getNorthEast().getLatDeg(), 0.00000001);
assertEquals(-178, u.getNorthEast().getLngDeg(), 0.00000001);
}
@Test
public void testDatelineUnionOutside() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 179), new DegreePosition(3, -179));
Bounds b2 = new BoundsImpl(new DegreePosition(4, -178), new DegreePosition(5, -177));
Bounds u = b1.union(b2);
assertEquals(1.0, u.getSouthWest().getLatDeg(), 0.00000001);
assertEquals(179, u.getSouthWest().getLngDeg(), 0.00000001);
assertEquals(5.0, u.getNorthEast().getLatDeg(), 0.00000001);
assertEquals(-177, u.getNorthEast().getLngDeg(), 0.00000001);
}
@Test
public void testDatelineUnionOneSideAndOtherSide() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, 179));
Bounds b2 = new BoundsImpl(new DegreePosition(4, -178), new DegreePosition(5, -177));
Bounds u = b1.union(b2);
assertEquals(1.0, u.getSouthWest().getLatDeg(), 0.00000001);
assertEquals(178, u.getSouthWest().getLngDeg(), 0.00000001);
assertEquals(5.0, u.getNorthEast().getLatDeg(), 0.00000001);
assertEquals(-177, u.getNorthEast().getLngDeg(), 0.00000001);
}
@Test
public void simpleUnionTestOutside() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(4, 4), new DegreePosition(5, 5));
Bounds u = b1.union(b2);
assertEquals(1.0, u.getSouthWest().getLatDeg(), 0.00000001);
assertEquals(1.0, u.getSouthWest().getLngDeg(), 0.00000001);
assertEquals(5.0, u.getNorthEast().getLatDeg(), 0.00000001);
assertEquals(5.0, u.getNorthEast().getLngDeg(), 0.00000001);
}
@Test
public void simpleIntersectsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 2), new DegreePosition(5, 5));
assertTrue(b1.intersects(b2));
assertTrue(b2.intersects(b1));
}
@Test
public void simpleIntersectTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 2), new DegreePosition(5, 5));
Bounds i = b1.intersect(b2);
assertEquals(2, i.getSouthWest().getLatDeg(), 0.0000001);
assertEquals(3, i.getNorthEast().getLatDeg(), 0.0000001);
assertEquals(2, i.getSouthWest().getLngDeg(), 0.0000001);
assertEquals(3, i.getNorthEast().getLngDeg(), 0.0000001);
}
@Test
public void simpleNegativeIntersectsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(4, 4), new DegreePosition(5, 5));
assertFalse(b1.intersects(b2));
assertFalse(b2.intersects(b1));
}
@Test
public void simpleNegativeIntersectTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(4, 4), new DegreePosition(5, 5));
Bounds i = b1.intersect(b2);
assertTrue(i.isEmpty());
}
@Test
public void simpleNegativeIntersectsTestNonIntersectingLat() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(4, 2), new DegreePosition(5, 5));
assertFalse(b1.intersects(b2));
assertFalse(b2.intersects(b1));
}
@Test
public void simpleNegativeIntersectTestNonIntersectingLat() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(3, 3));
Bounds b2 = new BoundsImpl(new DegreePosition(4, 2), new DegreePosition(5, 5));
Bounds i = b1.intersect(b2);
assertTrue(i.isEmpty());
}
@Test
public void intersectsWithContainsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(4, 4));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 2), new DegreePosition(3, 3));
assertTrue(b1.intersects(b2));
assertTrue(b2.intersects(b1));
}
@Test
public void intersectWithContainsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 1), new DegreePosition(4, 4));
Bounds b2 = new BoundsImpl(new DegreePosition(2, 2), new DegreePosition(3, 3));
Bounds i = b1.intersect(b2);
assertEquals(2, i.getSouthWest().getLatDeg(), 0.0000001);
assertEquals(3, i.getNorthEast().getLatDeg(), 0.0000001);
assertEquals(2, i.getSouthWest().getLngDeg(), 0.0000001);
assertEquals(3, i.getNorthEast().getLngDeg(), 0.0000001);
}
@Test
public void dateLineIntersectsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -177));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -176));
assertTrue(b1.intersects(b2));
assertTrue(b2.intersects(b1));
}
@Test
public void dateLineIntersectTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -177));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -176));
Bounds i = b1.intersect(b2);
assertEquals(2, i.getSouthWest().getLatDeg(), 0.0000001);
assertEquals(3, i.getNorthEast().getLatDeg(), 0.0000001);
assertEquals(-178, i.getSouthWest().getLngDeg(), 0.0000001);
assertEquals(-177, i.getNorthEast().getLngDeg(), 0.0000001);
}
@Test
public void dateLineNegativeIntersectsTestNonMatchingLat() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -177));
Bounds b2 = new BoundsImpl(new DegreePosition(4, -178), new DegreePosition(5, -176));
assertFalse(b1.intersects(b2));
assertFalse(b2.intersects(b1));
}
@Test
public void dateLineNegativeIntersectTestNonMatchingLat() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -177));
Bounds b2 = new BoundsImpl(new DegreePosition(4, -178), new DegreePosition(5, -176));
Bounds i = b1.intersect(b2);
assertTrue(i.isEmpty());
}
@Test
public void dateLineNegativeIntersectsTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, 179));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -177));
assertFalse(b1.intersects(b2));
assertFalse(b2.intersects(b1));
}
@Test
public void dateLineNegativeIntersectTest() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, 179));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -177));
Bounds i = b1.intersect(b2);
assertTrue(i.isEmpty());
}
@Test
public void dateLineNegativeIntersectsTestBoundsCrossingDateLine() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -179));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -177));
assertFalse(b1.intersects(b2));
assertFalse(b2.intersects(b1));
}
@Test
public void dateLineNegativeIntersectTestBoundsCrossingDateLine() {
Bounds b1 = new BoundsImpl(new DegreePosition(1, 178), new DegreePosition(3, -179));
Bounds b2 = new BoundsImpl(new DegreePosition(2, -178), new DegreePosition(5, -177));
Bounds i = b1.intersect(b2);
assertTrue(i.isEmpty());
}
}
@@ -0,0 +1,178 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.ControlPoint;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.CourseArea;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Series;
import com.sap.sailing.domain.base.Sideline;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.BoatClassImpl;
import com.sap.sailing.domain.base.impl.CompetitorImpl;
import com.sap.sailing.domain.base.impl.CourseImpl;
import com.sap.sailing.domain.base.impl.DomainFactoryImpl;
import com.sap.sailing.domain.base.impl.DynamicBoat;
import com.sap.sailing.domain.base.impl.DynamicTeam;
import com.sap.sailing.domain.base.impl.FleetImpl;
import com.sap.sailing.domain.base.impl.MarkImpl;
import com.sap.sailing.domain.base.impl.RegattaImpl;
import com.sap.sailing.domain.base.impl.SeriesImpl;
import com.sap.sailing.domain.base.impl.WaypointImpl;
import com.sap.sailing.domain.common.Color;
import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.ScoringScheme;
import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule;
import com.sap.sailing.domain.leaderboard.impl.LowPoint;
import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl;
import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl;
import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRegatta;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
public class LeaderboardCourseChangeTest {
/**
* See bug 2011
*
* @throws NoWindException
* @throws InterruptedException
* @throws ExecutionException
*/
@Test
public void testLeaderboardDTOCreationForCourseChange() throws NoWindException, InterruptedException, ExecutionException {
Date date = Calendar.getInstance().getTime();
TimePoint timePoint = new MillisecondsTimePoint(date);
TrackedRegattaRegistry trackedRegattaRegistry = mock(TrackedRegattaRegistry.class);
Fleet fleet = new FleetImpl("TestFleet");
Series series = createSeries(trackedRegattaRegistry, fleet);
Set<Series> seriesSet = new HashSet<>();
seriesSet.add(series);
BoatClass boatClass = new BoatClassImpl("TestClass", true);
String raceColumnName = "TestRace";
RaceColumn raceColumn = series.addRaceColumn(raceColumnName, trackedRegattaRegistry);
ScoringScheme scoringScheme = new LowPoint();
Regatta mockedRegatta = new RegattaImpl("TestRegatta", boatClass, seriesSet, false, scoringScheme,
UUID.randomUUID(), mock(CourseArea.class));
ControlPoint start = new MarkImpl("Start");
ControlPoint m1 = new MarkImpl("M1");
ControlPoint m2 = new MarkImpl("M2");
ControlPoint finish = new MarkImpl("Finish");
Set<Waypoint> waypoints = new HashSet<>();
Waypoint startWaypoint = new WaypointImpl(start);
waypoints.add(startWaypoint);
Waypoint m1Waypoint1 = new WaypointImpl(m1);
Waypoint m2Waypoint1 = new WaypointImpl(m2);
Waypoint m1Waypoint2 = new WaypointImpl(m1);
Waypoint m2Waypoint2 = new WaypointImpl(m2);
Waypoint m1Waypoint3 = new WaypointImpl(m1);
waypoints.add(m1Waypoint1);
waypoints.add(m2Waypoint1);
waypoints.add(m1Waypoint2);
waypoints.add(m2Waypoint2);
waypoints.add(m1Waypoint3);
Waypoint finishWaypoint = new WaypointImpl(finish);
waypoints.add(finishWaypoint);
Course course = new CourseImpl(raceColumnName, waypoints);
TrackedRace mockedTrackedRace = createSpyedTrackedRace(mockedRegatta, course, timePoint, boatClass);
raceColumn.setTrackedRace(fleet, mockedTrackedRace);
Set<String> raceColumnNames = new HashSet<>();
raceColumnNames.add(raceColumnName);
int[] ruleRaw = { 5, 3, 2 };
ThresholdBasedResultDiscardingRule rule = new ThresholdBasedResultDiscardingRuleImpl(ruleRaw);
Leaderboard leaderboard = new RegattaLeaderboardImpl(mockedRegatta, rule);
DomainFactory baseDomainFactory = new DomainFactoryImpl();
LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(timePoint, raceColumnNames, false,
trackedRegattaRegistry, baseDomainFactory);
assertEquals(6, leaderboardDTO.rows.values().iterator().next().fieldsByRaceColumnName.values().iterator()
.next().legDetails.size());
course.removeWaypoint(2);
course.removeWaypoint(3);
leaderboardDTO = leaderboard.getLeaderboardDTO(timePoint, raceColumnNames, false, trackedRegattaRegistry,
baseDomainFactory);
assertEquals(4, leaderboardDTO.rows.values().iterator().next().fieldsByRaceColumnName.values().iterator()
.next().legDetails.size());
}
private TrackedRace createSpyedTrackedRace(Regatta regatta, Course course, TimePoint timePoint, BoatClass boatClass) {
TrackedRegatta mockedTrackedRegatta = createMockedTrackedRegatta(regatta);
RaceDefinition mockedRace = createMockedRace(course, boatClass);
TrackedRace spyedTrackedRace = spy(new DynamicTrackedRaceImpl(mockedTrackedRegatta, mockedRace,
new HashSet<Sideline>(), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, 5000, 20000, 20000));
return spyedTrackedRace;
}
private RaceDefinition createMockedRace(Course course, BoatClass boatClass) {
RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class);
when(mockedRaceDefinition.getCompetitors()).thenReturn(new HashSet<Competitor>());
when(mockedRaceDefinition.getCourse()).thenReturn(course);
when(mockedRaceDefinition.getBoatClass()).thenReturn(new BoatClassImpl("TestClass", true));
when(mockedRaceDefinition.getName()).thenReturn("TestRace");
Iterable<Competitor> competitorSet = createCompetitorSet(boatClass);
when(mockedRaceDefinition.getCompetitors()).thenReturn(competitorSet);
return mockedRaceDefinition;
}
private Iterable<Competitor> createCompetitorSet(BoatClass boatClass) {
Set<Competitor> competitors = new HashSet<>();
DynamicBoat mockedBoat = mock(DynamicBoat.class);
when(mockedBoat.getBoatClass()).thenReturn(boatClass);
competitors.add(new CompetitorImpl(UUID.randomUUID(), "TestCompetitor", Color.BLACK, mock(DynamicTeam.class),
mockedBoat));
return competitors;
}
private TrackedRegatta createMockedTrackedRegatta(Regatta regatta) {
TrackedRegatta mockedTrackedRegatta = mock(DynamicTrackedRegatta.class);
when(mockedTrackedRegatta.getRegatta()).thenReturn(regatta);
return mockedTrackedRegatta;
}
private Series createSeries(TrackedRegattaRegistry trackedRegattaRegistry, Fleet fleet) {
Set<Fleet> fleets = new HashSet<>();
fleets.add(fleet);
Series series = new SeriesImpl("TestSeries", false, fleets, new HashSet<String>(), trackedRegattaRegistry);
return series;
}
}
@@ -80,7 +80,6 @@ public class MultipleClassesInRegattaTest {
assertEquals("505", kiwotest2.getRegatta().getBoatClass().getName());
assertEquals("49er", kiwotest3.getRegatta().getBoatClass().getName());
assertEquals("STG", weym470may112014_2.getRegatta().getBoatClass().getName());
assertNotSame(weym470may112014_2.getRegatta(), kiwotest1.getRegatta()); // expecting that each race gets a distinct default regatta
assertNotSame(kiwotest1.getRegatta(), kiwotest2.getRegatta());
assertNotSame(kiwotest1.getRegatta(), kiwotest3.getRegatta());
assertNotSame(kiwotest2.getRegatta(), kiwotest3.getRegatta());
@@ -28,6 +28,7 @@ import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
@@ -538,6 +539,12 @@ public class MockedTrackedRace implements DynamicTrackedRace {
// TODO Auto-generated method stub
return null;
}
@Override
public Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint) {
// TODO Auto-generated method stub
return null;
}
@Override
public Wind getDirectionFromStartToNextMark(TimePoint at) {
@@ -20,6 +20,7 @@ import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
@@ -315,6 +316,12 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace {
// TODO Auto-generated method stub
return null;
}
@Override
public Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint) {
// TODO Auto-generated method stub
return null;
}
@Override
public TrackedRegatta getTrackedRegatta() {
@@ -372,7 +372,7 @@ public class DomainFactoryImpl implements DomainFactory {
// This is particularly bad if a persistent regatta was loaded but a default regatta was accidentally created.
// Then, there is no way but restart the server to get rid of this stale cache entry here.
if (result == null) {
result = new RegattaImpl(raceLogStore, race.getName(), boatClass, trackedRegattaRegistry,
result = new RegattaImpl(raceLogStore, defaultRegattaNameAndBoatClass.getA(), boatClass, trackedRegattaRegistry,
// use the low-point system as the default scoring scheme
getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), race.getId(), null);
regattaCache.put(key, result);
@@ -389,7 +389,8 @@ public class DomainFactoryImpl implements DomainFactory {
for (IRaceCompetitor competitor : race.getRaceCompetitors()) {
competitorClassList.add(competitor.getCompetitor().getCompetitorClass());
}
Pair<String, BoatClass> defaultRegattaNameAndBoatClass = new Pair<String, BoatClass>(race.getName(), getDominantBoatClass(competitorClassList));
Pair<String, BoatClass> defaultRegattaNameAndBoatClass = new Pair<String, BoatClass>(race.getEvent().getName(),
getDominantBoatClass(competitorClassList));
return defaultRegattaNameAndBoatClass;
}
@@ -182,7 +182,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
} else {
startTime = null;
}
if (startTime != null) {
if (trackedRace != null && startTime != null) {
trackedRace.setStartTimeReceived(startTime);
}
final TimePoint startTrackingTime;
@@ -196,7 +196,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
} else {
startTrackingTime = null;
}
if (startTrackingTime != null) {
if (trackedRace != null && startTrackingTime != null) {
trackedRace.setStartOfTrackingReceived(startTrackingTime);
}
final TimePoint endTrackingTime;
@@ -210,7 +210,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
} else {
endTrackingTime = null;
}
if (endTrackingTime != null) {
if (trackedRace != null && endTrackingTime != null) {
trackedRace.setEndOfTrackingReceived(endTrackingTime);
}
}
@@ -210,6 +210,8 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
*/
private boolean stopped;
private final TrackedRegattaRegistry trackedRegattaRegistry;
/**
* Creates a race tracked for the specified URL/URIs and starts receiving all available existing and future push
* data from there. Receiving continues until {@link #stop()} is called.
@@ -294,6 +296,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry)
throws URISyntaxException, MalformedURLException, FileNotFoundException, SubscriberInitializationException {
super();
this.trackedRegattaRegistry = trackedRegattaRegistry;
this.tractracRace = tractracRace;
this.tractracEvent = tractracRace.getEvent();
urls = createID(paramURL, liveURI, storedURI);
@@ -593,11 +596,13 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
updateStatusOfTrackedRaces();
if (!stopped) {
try {
// See also bug 1517; with TracAPI we assume that when stopped(IEvent) is called by the TracAPI then
// all subscriptions have received all their data and it's therefore safe to stop all subscriptions
// at this point without missing any data.
stop();
} catch (InterruptedException e) {
for (RaceDefinition race : getRaces()) {
// See also bug 1517; with TracAPI we assume that when stopped(IEvent) is called by the TracAPI then
// all subscriptions have received all their data and it's therefore safe to stop all subscriptions
// at this point without missing any data.
trackedRegattaRegistry.stopTracking(regatta, race);
}
} catch (InterruptedException | IOException e) {
logger.log(Level.INFO, "Interrupted while trying to stop tracker "+this, e);
}
}
@@ -62,7 +62,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
private static final Logger logger = Logger.getLogger(RegattaImpl.class.getName());
private static final long serialVersionUID = 6509564189552478869L;
private final Set<RaceDefinition> races;
private Set<RaceDefinition> races;
private final BoatClass boatClass;
private transient Set<RegattaListener> regattaListeners;
private List<? extends Series> series;
@@ -185,6 +185,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
MasterDataImportInformation masterDataImportInformation = ongoingMasterDataImportInformation.get();
if (masterDataImportInformation != null) {
raceLogStore = masterDataImportInformation.getRaceLogStore();
races = new HashSet<RaceDefinition>();
} else {
raceLogStore = EmptyRaceLogStore.INSTANCE;
}
@@ -76,7 +76,8 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe
for (Fleet fleet : fleets) {
this.fleetsByName.put(fleet.getName(), fleet);
}
fleetsInAscendingOrder = new ArrayList<Fleet>(fleetsByName.values());
fleetsInAscendingOrder = new ArrayList<Fleet>();
Util.addAll(fleets, fleetsInAscendingOrder);
Collections.sort(fleetsInAscendingOrder);
List<RaceColumnInSeries> myRaceColumns = new ArrayList<RaceColumnInSeries>();
this.raceColumns = myRaceColumns;
@@ -122,7 +122,7 @@ public interface Leaderboard extends LeaderboardBase {
* For split fleets things can vary slightly. There, one fleet may complete a few races before the another fleet
* starts with those races. In this case there isn't even any point in time at which all fleets have finished
* exactly <i>n</i> races. Still, this method pretends such a time point would have existed, actually ignoring
* the <i>times</i> at which a race took place but only looking at the resulting scores and discard.<p>
* the <i>times</i> at which a race took place but only looking at the resulting scores and discards.<p>
*
* When computing the ranks after all columns up to and including the race column that is the key of the resulting
* map, the method applies the discarding and tie breaking rules as they would have had to be applied had the races
@@ -133,6 +133,25 @@ public interface Leaderboard extends LeaderboardBase {
*/
Map<RaceColumn, List<Competitor>> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException;
/**
* Computes the competitor's total points sum as they were or would have been after each race column (from left to right)
* was completed.<p>
*
* A leaderboard fills up over time, usually "from left to right" with one race after another finishing.
* For split fleets things can vary slightly. There, one fleet may complete a few races before the another fleet
* starts with those races. In this case there isn't even any point in time at which all fleets have finished
* exactly <i>n</i> races. Still, this method pretends such a time point would have existed, actually ignoring
* the <i>times</i> at which a race took place but only looking at the resulting scores and discards.<p>
*
* When computing the total points sum after all columns up to and including the race column that is the key of the resulting
* map, the method applies the discarding and tie breaking rules as they would have had to be applied had the races
* in the respective column just completed.
*
* @return The resulting map is guaranteed to have the same iteration order regarding the race columns
* as {@link #getRaceColumns()}.
*/
Map<RaceColumn, Map<Competitor, Double>> getTotalPointsSumAfterRaceColumn(TimePoint timePoint) throws NoWindException;
/**
* Tells the number of points carried over from previous races not tracked by this leaderboard for
* the <code>competitor</code>. Returns <code>0</code> if there is no carried points definition for
@@ -216,6 +235,14 @@ public interface Leaderboard extends LeaderboardBase {
*/
Double getTotalPoints(Competitor competitor, TimePoint timePoint) throws NoWindException;
/**
* Sums up the {@link #getTotalPoints(Competitor, RaceColumn, TimePoint) total points} of <code>competitor</code>
* across all race columns listed in <code>raceColumnsToconsider</code>, respecting the
* {@link RaceColumn#isStartsWithZeroScore()} property.
*/
Double getTotalPoints(Competitor competitor, Iterable<RaceColumn> raceColumnsToConsider, TimePoint timePoint)
throws NoWindException;
/**
* Sorts the competitors according to their ranking in the race column specified. Only competitors who have a score
* are added to the result list. This excludes competitors whose fleet hasn't raced for the <code>raceColumn</code>
@@ -312,10 +339,11 @@ public interface Leaderboard extends LeaderboardBase {
* Tells if the column represented by <code>raceColumn</code> shall be considered when counting the number of "races
* so far" for discarding. Although medal races are never discarded themselves, they still count in determining the
* number of "races so far" which is then the basis for deciding how many races may be discarded. If a leaderboard
* has corrections for a column for the <code>competitor</code> then that column shall be considered for discarding
* and counts for determining the number of races so far. Also, if a tracked race is connected to the column and has
* started already, the column is to be considered for discarding unless the column has several unordered fleets and
* not all fleets have started their race yet (see
* has corrections for a column for the <code>competitor</code> that are already to be applied at
* <code>timePoint</code> (e.g., because the race can be proven to have finished at <code>timePoint</code>), then
* that column shall be considered for discarding and counts for determining the number of races so far. Also, if a
* tracked race is connected to the column and has started already, the column is to be considered for discarding
* unless the column has several unordered fleets and not all fleets have started their race yet (see
* {@link ScoringScheme#isValidInTotalScore(Leaderboard, RaceColumn, TimePoint)}).
*/
boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint);
@@ -459,4 +487,5 @@ public interface Leaderboard extends LeaderboardBase {
InterruptedException, ExecutionException;
NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher();
}
@@ -1,10 +1,11 @@
package com.sap.sailing.domain.leaderboard;
import java.util.HashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorChangeListener;
@@ -29,6 +30,7 @@ import com.sap.sailing.domain.tracking.RaceChangeListener;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.util.impl.ConcurrentWeakHashMap;
import com.sap.sailing.util.impl.LockUtil;
import com.sap.sailing.util.impl.NamedReentrantReadWriteLock;
@@ -45,7 +47,7 @@ public class LeaderboardCacheManager {
private final WeakHashMap<Leaderboard, CacheInvalidationUponScoreCorrectionListener> scoreCorrectionListeners;
private final NamedReentrantReadWriteLock scoreCorrectionAndCompetitorChangeListenersLock;
private final WeakHashMap<Leaderboard, CacheInvalidationUponCompetitorChangeListener> competitorChangeListeners;
private final WeakHashMap<Leaderboard, Map<TrackedRace, Set<CacheInvalidationListener>>> invalidationListenersPerLeaderboard;
private final ConcurrentWeakHashMap<Leaderboard, ConcurrentHashMap<TrackedRace, Set<CacheInvalidationListener>>> invalidationListenersPerLeaderboard;
private final WeakHashMap<Leaderboard, RaceColumnListener> raceColumnListeners;
private class CacheInvalidationListener implements RaceChangeListener {
@@ -103,6 +105,16 @@ public class LeaderboardCacheManager {
removeFromCache(leaderboard);
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
removeFromCache(leaderboard);
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
removeFromCache(leaderboard);
}
@Override
public void delayToLiveChanged(long delayToLiveInMillis) {
removeFromCache(leaderboard);
@@ -209,7 +221,7 @@ public class LeaderboardCacheManager {
public LeaderboardCacheManager(LeaderboardCache leaderboardCache) {
this.leaderboardCache = leaderboardCache;
this.invalidationListenersPerLeaderboard = new WeakHashMap<>();
this.invalidationListenersPerLeaderboard = new ConcurrentWeakHashMap<>();
this.raceColumnListeners = new WeakHashMap<Leaderboard, RaceColumnListener>();
this.scoreCorrectionListeners = new WeakHashMap<>();
this.scoreCorrectionAndCompetitorChangeListenersLock = new NamedReentrantReadWriteLock(
@@ -218,11 +230,10 @@ public class LeaderboardCacheManager {
}
private void removeFromCache(Leaderboard leaderboard) {
synchronized (invalidationListenersPerLeaderboard) {
Map<TrackedRace, Set<CacheInvalidationListener>> listenersMap = invalidationListenersPerLeaderboard
.remove(leaderboard);
if (listenersMap != null) {
for (Map.Entry<TrackedRace, Set<CacheInvalidationListener>> e : listenersMap.entrySet()) {
ConcurrentHashMap<TrackedRace, Set<CacheInvalidationListener>> listenersMap = invalidationListenersPerLeaderboard.remove(leaderboard);
if (listenersMap != null) {
for (Map.Entry<TrackedRace, Set<CacheInvalidationListener>> e : listenersMap.entrySet()) {
synchronized (e.getValue()) { // the Set is a Collections.synchronizedSet, and synchronization is required for iteration
for (CacheInvalidationListener listener : e.getValue()) {
listener.removeFromTrackedRace();
}
@@ -305,17 +316,7 @@ public class LeaderboardCacheManager {
@Override
public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) {
removeFromCache(leaderboard);
Map<TrackedRace, Set<CacheInvalidationListener>> listenersMap = invalidationListenersPerLeaderboard
.get(leaderboard);
if (listenersMap != null) {
Set<CacheInvalidationListener> listeners = listenersMap.get(trackedRace);
if (listeners != null) {
for (CacheInvalidationListener listener : listeners) {
listener.removeFromTrackedRace();
}
}
}
removeFromCache(leaderboard); // removes all listeners from invalidationListenersPerLeaderboard and from their TrackedRaces
}
@Override
@@ -393,24 +394,20 @@ public class LeaderboardCacheManager {
}
private void registerListener(final Leaderboard leaderboard, TrackedRace trackedRace) {
Map<TrackedRace, Set<CacheInvalidationListener>> invalidationListeners;
ConcurrentHashMap<TrackedRace, Set<CacheInvalidationListener>> invalidationListeners;
final CacheInvalidationListener listener;
synchronized (invalidationListenersPerLeaderboard) {
listener = new CacheInvalidationListener(leaderboard, trackedRace);
trackedRace.addListener(listener);
invalidationListeners = invalidationListenersPerLeaderboard.get(leaderboard);
if (invalidationListeners == null) {
invalidationListeners = new HashMap<TrackedRace, Set<CacheInvalidationListener>>();
invalidationListenersPerLeaderboard.put(leaderboard, invalidationListeners);
}
listener = new CacheInvalidationListener(leaderboard, trackedRace);
trackedRace.addListener(listener);
invalidationListeners = invalidationListenersPerLeaderboard.get(leaderboard);
if (invalidationListeners == null) {
invalidationListeners = new ConcurrentHashMap<TrackedRace, Set<CacheInvalidationListener>>();
invalidationListenersPerLeaderboard.put(leaderboard, invalidationListeners);
}
Set<CacheInvalidationListener> listeners = invalidationListeners.get(trackedRace);
if (listeners == null) {
listeners = new HashSet<CacheInvalidationListener>();
listeners = Collections.synchronizedSet(new HashSet<CacheInvalidationListener>());
invalidationListeners.put(trackedRace, listeners);
}
listeners.add(listener);
}
}
@@ -88,7 +88,7 @@ public class LeaderboardDTOCache implements LeaderboardCache {
private static final long serialVersionUID = 7287916997229815039L;
@Override
protected boolean removeEldestEntry(Map.Entry<Util.Triple<TimePoint, Collection<String>, Boolean>, FutureTask<LeaderboardDTO>> e) {
return size() > 10; // remember 10 LeaderboardDTOs per leaderborad
return size() > 10; // remember 10 LeaderboardDTOs per leaderboard
}
};
leaderboardCacheLock = new NamedReentrantReadWriteLock("leaderboardCacheLock for "+leaderboard.getName(), /* fair */ false);
@@ -287,6 +287,16 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
invalidateCacheAndRemoveThisListenerFromTrackedRace();
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
invalidateCacheAndRemoveThisListenerFromTrackedRace();
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
invalidateCacheAndRemoveThisListenerFromTrackedRace();
}
@Override
public void statusChanged(TrackedRaceStatus newStatus) {
// when the status changes away from LOADING, calculations may start or resume, making it necessary to clear the cache
@@ -520,15 +530,21 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
@Override
public Double getTotalPoints(Competitor competitor, TimePoint timePoint) throws NoWindException {
return getTotalPoints(competitor, getRaceColumns(), timePoint);
}
@Override
public Double getTotalPoints(Competitor competitor, final Iterable<RaceColumn> raceColumnsToConsider,
TimePoint timePoint) throws NoWindException {
// when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on
boolean needToResetScoreUponNextNonEmptyEntry = false;
double result = getCarriedPoints(competitor);
for (RaceColumn r : getRaceColumns()) {
if (r.isStartsWithZeroScore()) {
for (RaceColumn raceColumn : raceColumnsToConsider) {
if (raceColumn.isStartsWithZeroScore()) {
needToResetScoreUponNextNonEmptyEntry = true;
}
if (getScoringScheme().isValidInTotalScore(this, r, timePoint)) {
final Double totalPoints = getTotalPoints(competitor, r, timePoint);
if (getScoringScheme().isValidInTotalScore(this, raceColumn, timePoint)) {
final Double totalPoints = getTotalPoints(competitor, raceColumn, raceColumnsToConsider, timePoint);
if (totalPoints != null) {
if (needToResetScoreUponNextNonEmptyEntry) {
result = 0;
@@ -690,6 +706,46 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
return result;
}
@Override
public Map<RaceColumn, Map<Competitor, Double>> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint)
throws NoWindException {
final Map<RaceColumn, Map<Competitor, Double>> result = new LinkedHashMap<>();
List<RaceColumn> raceColumnsToConsider = new ArrayList<>();
Map<RaceColumn, Future<Map<Competitor, Double>>> futures = new HashMap<>();
for (final RaceColumn raceColumn : getRaceColumns()) {
raceColumnsToConsider.add(raceColumn);
final Iterable<RaceColumn> finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider);
futures.put(raceColumn, executor.submit(new Callable<Map<Competitor, Double>>() {
@Override
public Map<Competitor, Double> call() {
Map<Competitor, Double> totalPointsSumPerCompetitorInColumn = new HashMap<>();
for (Competitor competitor : getCompetitors()) {
try {
totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint));
} catch (NoWindException e) {
throw new NoWindError(e);
}
}
synchronized (result) {
return totalPointsSumPerCompetitorInColumn;
}
}
}));
}
for (RaceColumn raceColumn : getRaceColumns()) {
try {
result.put(raceColumn, futures.get(raceColumn).get());
} catch (InterruptedException | ExecutionException e) {
if (e.getCause() instanceof NoWindError) {
throw ((NoWindError) e.getCause()).getCause();
} else {
throw new RuntimeException(e); // no caught exceptions occur in the futures executed
}
}
}
return result;
}
@Override
public Map<com.sap.sse.common.Util.Pair<Competitor, RaceColumn>, Entry> getContent(final TimePoint timePoint) throws NoWindException {
Map<com.sap.sse.common.Util.Pair<Competitor, RaceColumn>, Entry> result = new HashMap<com.sap.sse.common.Util.Pair<Competitor, RaceColumn>, Entry>();
@@ -3,13 +3,14 @@ package com.sap.sailing.domain.tracking;
import java.util.Map;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CourseListener;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
public interface RaceChangeListener {
public interface RaceChangeListener extends CourseListener {
void competitorPositionChanged(GPSFixMoving fix, Competitor competitor);
void markPositionChanged(GPSFix fix, Mark mark);
@@ -20,6 +20,7 @@ import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.TimingConstants;
@@ -381,6 +382,17 @@ public interface TrackedRace extends Serializable {
*/
Tack getTack(Competitor competitor, TimePoint timePoint) throws NoWindException;
/**
* Determines whether the <code>competitor</code> is sailing on port or starboard tack at the <code>timePoint</code>
* requested.
* <p>
* This method outperforms {@link #getTack(Competitor, TimePoint)}, based on being passed an already calculated wind
* for the given time and competitor position as well as the competitors speed and course over ground.
* <p>
* This method will acquire the read lock for the competitor's track.
*/
Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint);
TrackedRegatta getTrackedRegatta();
/**
@@ -578,8 +590,11 @@ public interface TrackedRace extends Serializable {
RaceLog getRaceLog(Serializable identifier);
/**
* a setter for the listener on course design changes. The listener is mostly part of the tracking provider adapter.
* @param listener the listener to operate with.
* A setter for the listener on course design changes suggested by one of the {@link RaceLog}s attached to this
* race. The listener is mostly part of the tracking provider adapter.
*
* @param listener
* the listener to operate with.
*/
void addCourseDesignChangedListener(CourseDesignChangedListener listener);
@@ -59,5 +59,13 @@ public abstract class AbstractRaceChangeListener implements RaceChangeListener {
@Override
public void delayToLiveChanged(long delayToLiveInMillis) {
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
}
}
@@ -1,64 +0,0 @@
package com.sap.sailing.domain.tracking.impl;
import java.util.Map;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.tracking.GPSFix;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceChangeListener;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.Wind;
public class BaseRaceChangeListener implements RaceChangeListener {
@Override
public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) {
}
@Override
public void markPositionChanged(GPSFix fix, Mark mark) {
}
@Override
public void markPassingReceived(Competitor competitor, Map<Waypoint, MarkPassing> oldMarkPassings,
Iterable<MarkPassing> markPassings) {
}
@Override
public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
}
@Override
public void windDataReceived(Wind wind, WindSource windSource) {
}
@Override
public void windDataRemoved(Wind wind, WindSource windSource) {
}
@Override
public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
}
@Override
public void raceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, TimePoint startTimeReceived) {
}
@Override
public void delayToLiveChanged(long delayToLiveInMillis) {
}
@Override
public void windSourcesToExcludeChanged(Iterable<? extends WindSource> windSourcesToExclude) {
}
@Override
public void statusChanged(TrackedRaceStatus newStatus) {
}
}
@@ -85,6 +85,15 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener {
private static final long serialVersionUID = 4884868659665863604L;
public void deleteAll() {
lockForWrite();
try {
getInternalRawFixes().clear();
} finally {
unlockAfterWrite();
}
}
public void deleteAllLaterThan(TimePoint from) {
// TODO use a specialized ArrayList in a specialized ArrayListNavigableSet and then make removeRange public
lockForWrite();
@@ -171,7 +180,11 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener {
FromTimePointToEndUpdateInterval updateInterval) {
CrossTrackErrorSumAndNumberOfFixesTrack result;
if (oldValue != null) {
oldValue.deleteAllLaterThan(updateInterval.getFrom());
if (updateInterval == null) {
oldValue.deleteAll();
} else {
oldValue.deleteAllLaterThan(updateInterval.getFrom());
}
computedCacheUpdate.lockForRead();
try {
for (CrossTrackErrorSumAndNumberOfFixes entry : computedCacheUpdate.getRawFixes()) {
@@ -434,6 +447,27 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener {
}
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
invalidate();
}
/**
* Invalidates all cache contents for all competitors from the beginning of time
*/
private void invalidate() {
final List<Competitor> shuffledCompetitors = new ArrayList<>(cachePerCompetitor.keySet());
Collections.shuffle(shuffledCompetitors);
for (Competitor competitor : shuffledCompetitors) {
cachePerCompetitor.triggerUpdate(competitor, null /* meaning: from the beginning of time */);
}
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
invalidate();
}
@Override
public String toString() {
return "CrossTrackErrorCache for competitors "+cachePerCompetitor.keySet();
@@ -362,6 +362,48 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
}
}
private void notifyListenersWaypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
RaceChangeListener[] listeners;
synchronized (getListeners()) {
listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]);
}
for (RaceChangeListener listener : listeners) {
try {
listener.waypointAdded(zeroBasedIndex, waypointThatGotAdded);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.log(Level.SEVERE, "notifyListenersWaypointAdded(int, Waypoint)", e);
}
}
}
private void notifyListenersWaypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
RaceChangeListener[] listeners;
synchronized (getListeners()) {
listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]);
}
for (RaceChangeListener listener : listeners) {
try {
listener.waypointRemoved(zeroBasedIndex, waypointThatGotRemoved);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.log(Level.SEVERE, "notifyListenersWaypointRemoved(int, Waypoint)", e);
}
}
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
super.waypointAdded(zeroBasedIndex, waypointThatGotAdded);
notifyListenersWaypointAdded(zeroBasedIndex, waypointThatGotAdded);
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
super.waypointRemoved(zeroBasedIndex, waypointThatGotRemoved);
notifyListenersWaypointRemoved(zeroBasedIndex, waypointThatGotRemoved);
}
private void notifyListeners(GPSFix fix, Mark mark) {
RaceChangeListener[] listeners;
synchronized (getListeners()) {
@@ -677,6 +677,16 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
clearCache();
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
clearCache();
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
clearCache();
}
@Override
public void markPositionChanged(GPSFix fix, Mark mark) {
assert fix != null && fix.getTimePoint() != null;
@@ -198,6 +198,16 @@ public class TrackedLegImpl implements TrackedLeg, RaceChangeListener {
clearCaches();
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
clearCaches(); // necessary because the competitor tracks ordered by rank may change for legs adjacent to the waypoint added
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
clearCaches(); // necessary because the competitor tracks ordered by rank may change for legs adjacent to the waypoint removed
}
@Override
public void statusChanged(TrackedRaceStatus newStatus) {
// no-op; the leg doesn't mind the tracked race's status being updated
@@ -17,6 +17,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Set;
import java.util.Timer;
@@ -178,7 +179,12 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
private long updateCount;
private transient Map<TimePoint, List<Competitor>> competitorRankings;
/**
* Limit for the cache size in {@link #competitorRankings} and respectively in {@link #competitorRankingsLocks}.
*/
private static final int MAX_COMPETITOR_RANKINGS_CACHE_SIZE = 10;
private transient LinkedHashMap<TimePoint, List<Competitor>> competitorRankings;
/**
* The locks managed here correspond with the {@link #competitorRankings} structure. When
@@ -186,7 +192,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
* time point. Readers use the read lock. Checking / entering a lock into this map uses <code>synchronized</code> on
* the map itself.
*/
private transient Map<TimePoint, NamedReentrantReadWriteLock> competitorRankingsLocks;
private transient LinkedHashMap<TimePoint, NamedReentrantReadWriteLock> competitorRankingsLocks;
/**
* legs appear in the order in which they appear in the race's course
@@ -374,8 +380,8 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
WindSource trackBasedWindSource = new WindSourceImpl(WindSourceType.TRACK_BASED_ESTIMATION);
windTracks.put(trackBasedWindSource,
getOrCreateWindTrack(trackBasedWindSource, delayForWindEstimationCacheInvalidation));
competitorRankings = new HashMap<TimePoint, List<Competitor>>();
competitorRankingsLocks = new HashMap<TimePoint, NamedReentrantReadWriteLock>();
competitorRankings = createCompetitorRankingsCache();
competitorRankingsLocks = createCompetitorRankingsLockMap();
// now wait until wind loading has at least started; then we know that the serialization lock is safely held by the loader
try {
waitUntilLoadingFromWindStoreComplete();
@@ -384,6 +390,26 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
}
}
private LinkedHashMap<TimePoint, NamedReentrantReadWriteLock> createCompetitorRankingsLockMap() {
return new LinkedHashMap<TimePoint, NamedReentrantReadWriteLock>() {
private static final long serialVersionUID = 6298801656693955386L;
@Override
protected boolean removeEldestEntry(Entry<TimePoint, NamedReentrantReadWriteLock> eldest) {
return size() > MAX_COMPETITOR_RANKINGS_CACHE_SIZE;
}
};
}
private LinkedHashMap<TimePoint, List<Competitor>> createCompetitorRankingsCache() {
return new LinkedHashMap<TimePoint, List<Competitor>>() {
private static final long serialVersionUID = -6044369612727021861L;
@Override
protected boolean removeEldestEntry(Entry<TimePoint, List<Competitor>> eldest) {
return size() > MAX_COMPETITOR_RANKINGS_CACHE_SIZE;
}
};
}
/**
* Assuming that the tracks were loaded from the persistent store, this method updates
* the time stamps that frame the data held by this tracked race. See {@link #timePointOfLastEvent}, {@link #timePointOfNewestEvent}
@@ -426,8 +452,8 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
markPassingsTimes = new ArrayList<com.sap.sse.common.Util.Pair<Waypoint, com.sap.sse.common.Util.Pair<TimePoint, TimePoint>>>();
cacheInvalidationTimerLock = new Object();
windStore = EmptyWindStore.INSTANCE;
competitorRankings = new HashMap<TimePoint, List<Competitor>>();
competitorRankingsLocks = new HashMap<TimePoint, NamedReentrantReadWriteLock>();
competitorRankings = createCompetitorRankingsCache();
competitorRankingsLocks = createCompetitorRankingsLockMap();
directionFromStartToNextMarkCache = new HashMap<TimePoint, Future<Wind>>();
crossTrackErrorCache = new CrossTrackErrorCache(this);
maneuverCache = createManeuverCache();
@@ -995,44 +1021,33 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
}
}
List<Competitor> rankedCompetitors;
final boolean lockForWrite;
synchronized (competitorRankings) {
rankedCompetitors = competitorRankings.get(timePoint);
if (rankedCompetitors == null) {
lockForWrite = true;
} else {
lockForWrite = false;
}
}
if (lockForWrite) {
if (rankedCompetitors == null) {
LockUtil.lockForWrite(readWriteLock);
} else {
LockUtil.lockForRead(readWriteLock);
}
try {
if (rankedCompetitors == null) {
rankedCompetitors = competitorRankings.get(timePoint); // try again; maybe a writer released the write
// lock after updating the cache
try {
if (rankedCompetitors == null) {
RaceRankComparator comparator = new RaceRankComparator(this, timePoint);
rankedCompetitors = new ArrayList<Competitor>();
for (Competitor c : getRace().getCompetitors()) {
rankedCompetitors.add(c);
}
Collections.sort(rankedCompetitors, comparator);
synchronized (competitorRankings) {
competitorRankings.put(timePoint, rankedCompetitors);
rankedCompetitors = competitorRankings.get(timePoint); // try again; maybe a writer released the
// write
// lock after updating the cache
if (rankedCompetitors == null) {
RaceRankComparator comparator = new RaceRankComparator(this, timePoint);
rankedCompetitors = new ArrayList<Competitor>();
for (Competitor c : getRace().getCompetitors()) {
rankedCompetitors.add(c);
}
Collections.sort(rankedCompetitors, comparator);
synchronized (competitorRankings) {
competitorRankings.put(timePoint, rankedCompetitors);
}
}
}
}
return rankedCompetitors;
} finally {
if (lockForWrite) {
} finally {
LockUtil.unlockAfterWrite(readWriteLock);
} else {
LockUtil.unlockAfterRead(readWriteLock);
}
}
return rankedCompetitors;
}
@Override
@@ -1851,6 +1866,42 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
return result;
}
/**
* This is probably best explained by example. If the wind bearing is from port to starboard, the situation looks
* like this:
*
* <pre>
* ^
* Wind | Boat
* -----------> |
* |
*
* </pre>
*
* In this case, the boat gets the wind from port, so the result has to be {@link Tack#PORT}. The angle between the
* boat's heading (which we can only approximate by the boat's course over ground) and the wind bearing in this case
* is 90 degrees. <code>wind.{@link Bearing#getDifferenceTo(Bearing) getDifferenceTo}(boat)</code> in this case will
* return a bearing representing -90 degrees.
* <p>
*
* If the wind is blowing the other way, the angle returned by {@link Bearing#getDifferenceTo(Bearing)} will
* correspond to +90 degrees. In other words, a negative angle means starboard tack, a positive angle represents
* port tack.
* <p>
*
* For the unlikely case of 0 degrees difference, {@link Tack#STARBOARD} will result.
*
* @return <code>null</code> in case the boat's bearing cannot be determined for <code>timePoint</code>
*/
@Override
public Tack getTack(SpeedWithBearing estimatedSpeed, Wind wind, TimePoint timePoint) {
Tack result = null;
if (estimatedSpeed != null) {
result = getTack(wind, estimatedSpeed.getBearing());
}
return result;
}
/**
* Based on the wind direction at <code>timePoint</code> and at position <code>where</code>, compares the
* <code>boatBearing</code> to the wind's bearing at that time and place and determined the tack.
@@ -1861,15 +1912,22 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
*/
private Tack getTack(Position where, TimePoint timePoint, Bearing boatBearing) throws NoWindException {
final Wind wind = getWind(where, timePoint);
Tack result;
if (wind == null) {
throw new NoWindException("Can't determine wind direction in position " + where + " at " + timePoint
+ ", therefore cannot determine tack");
}
return getTack(wind, boatBearing);
}
/**
* Based on the wind, compares the <code>boatBearing</code> to the wind's bearing at
* that time and place and determined the tack.
*/
private Tack getTack(Wind wind, Bearing boatBearing) {
Bearing windBearing = wind.getBearing();
Bearing difference = windBearing.getDifferenceTo(boatBearing);
result = difference.getDegrees() <= 0 ? Tack.PORT : Tack.STARBOARD;
return result;
return difference.getDegrees() <= 0 ? Tack.PORT : Tack.STARBOARD;
}
@Override
@@ -2026,6 +2084,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
GPSFixMoving next = approximationPointsIter.next();
speedWithBearingOnApproximationFromCurrentToNext = current.getSpeedAndBearingRequiredToReach(next);
// compute course change on "approximation track"
// FIXME bug 2009: when a maneuver (particularly a penalty circle) is executed at high turn rates, approximations may lead to turns >180deg, hence inferred to turn the wrong way; need to loop across the non-approximated fixes here!
CourseChange courseChange = speedWithBearingOnApproximationFromPreviousToCurrent
.getCourseChangeRequiredToReach(speedWithBearingOnApproximationFromCurrentToNext);
com.sap.sse.common.Util.Pair<GPSFixMoving, CourseChange> courseChangeAtFix = new com.sap.sse.common.Util.Pair<GPSFixMoving, CourseChange>(current,
@@ -8,7 +8,8 @@ Bundle-Vendor: SAP
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Import-Package: com.sap.sailing.resultimport,
com.sap.sailing.resultimport.impl,
org.osgi.framework;version="1.3.0"
org.osgi.framework;version="1.3.0",
org.osgi.util.tracker;version="1.5.1"
Bundle-ActivationPolicy: lazy
Require-Bundle: com.sap.sailing.domain.common,
com.sap.sailing.domain,
@@ -4,9 +4,11 @@ import java.util.logging.Logger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.common.ScoreCorrectionProvider;
import com.sap.sailing.resultimport.ResultUrlRegistry;
import com.sap.sailing.resultimport.impl.ResultUrlRegistryServiceTrackerCustomizer;
public class Activator implements BundleActivator {
private static final Logger logger = Logger.getLogger(Activator.class.getName());
@@ -14,6 +16,8 @@ public class Activator implements BundleActivator {
private static final String SCAN_DIR_PATH_PROPERTY_NAME = "freg.results";
private static final String DEFAULT_SCAN_DIR = "fregresults";
private ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> resultUrlRegistryServiceTracker;
/*
* For testing, consider using the following URLs:
* allUrls.add(new URL("http://www.axel-uhl.de/freg/freg_html_export_sample.html"));
@@ -24,11 +28,26 @@ public class Activator implements BundleActivator {
if (scanDirPath == null) {
scanDirPath = System.getProperty(SCAN_DIR_PATH_PROPERTY_NAME, DEFAULT_SCAN_DIR);
}
final ScoreCorrectionProviderImpl service = new ScoreCorrectionProviderImpl(ResultUrlRegistry.INSTANCE);
bundleContext.registerService(ScoreCorrectionProvider.class, service, /* properties */null);
logger.info("Scanning " + scanDirPath + " for official result lists of " + service.getName());
logger.info("Scanning " + scanDirPath + " for official result lists of "
+ ScoreCorrectionProviderImpl.PROVIDER_NAME);
resultUrlRegistryServiceTracker = new ServiceTracker<>(bundleContext, ResultUrlRegistry.class,
new ResultUrlRegistryServiceTrackerCustomizer(bundleContext) {
@Override
protected ScoreCorrectionProvider configureScoreCorrectionProvider(
ResultUrlRegistry resultUrlRegistry) {
final ScoreCorrectionProviderImpl service = new ScoreCorrectionProviderImpl(resultUrlRegistry);
return service;
}
});
resultUrlRegistryServiceTracker.open();
}
public void stop(BundleContext bundleContext) throws Exception {
resultUrlRegistryServiceTracker.close();
}
}
@@ -27,6 +27,7 @@ import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
@@ -539,6 +540,12 @@ public class MockedTrackedRace implements DynamicTrackedRace {
// TODO Auto-generated method stub
return null;
}
@Override
public Tack getTack(SpeedWithBearing speedWithBearing, Wind wind, TimePoint timePoint) {
// TODO Auto-generated method stub
return null;
}
@Override
public Wind getDirectionFromStartToNextMark(TimePoint at) {
@@ -0,0 +1,25 @@
package com.sap.sailing.gwt.ui.actions;
import java.util.Collection;
import java.util.List;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO;
import com.sap.sse.gwt.client.async.AsyncAction;
public class GetRaceTimesInfoAction implements AsyncAction<List<RaceTimesInfoDTO>> {
private final SailingServiceAsync sailingService;
private final Collection<RegattaAndRaceIdentifier> raceIdentifiers;
public GetRaceTimesInfoAction(SailingServiceAsync sailingService, Collection<RegattaAndRaceIdentifier> raceIdentifiers) {
this.sailingService = sailingService;
this.raceIdentifiers = raceIdentifiers;
}
@Override
public void execute(AsyncCallback<List<RaceTimesInfoDTO>> callback) {
sailingService.getRaceTimesInfos(raceIdentifiers, callback);
}
}
@@ -296,6 +296,11 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
List<String> strings = new ArrayList<String>();
strings.add(t.name);
strings.add(t.regattaName);
if (t.boatClassNames != null) {
for (String boatClassName : t.boatClassNames) {
strings.add(boatClassName);
}
}
return strings;
}
};
@@ -223,15 +223,16 @@ public class CompetitorSelectionModel implements CompetitorSelectionProvider {
@Override
public Color getColor(CompetitorDTO competitor) {
Color result = null;
final Color result;
if (allCompetitors.contains(competitor)) {
if (competitor.getColor() != null) {
result = competitor.getColor();
} else {
result = competitorsColorMap.getColorByID(competitor);
}
} else {
result = null;
}
return result;
}
@@ -28,6 +28,8 @@ public class DetailTypeFormatter {
return stringMessages.totalPoints();
case REGATTA_TOTAL_POINTS:
return stringMessages.totalPoints();
case REGATTA_TOTAL_POINTS_SUM:
return stringMessages.totalPoints();
case NUMBER_OF_MANEUVERS:
return stringMessages.numberOfManeuvers();
case CURRENT_SPEED_OVER_GROUND_IN_KNOTS:
@@ -194,6 +196,7 @@ public class DetailTypeFormatter {
case OVERALL_RANK:
case RACE_TOTAL_POINTS:
case REGATTA_TOTAL_POINTS:
case REGATTA_TOTAL_POINTS_SUM:
case CURRENT_LEG:
case TACK:
case START_TACK:
@@ -293,6 +296,8 @@ public class DetailTypeFormatter {
return stringMessages.raceTotalPointsTooltip();
case REGATTA_TOTAL_POINTS:
return stringMessages.regattaTotalPointsTooltip();
case REGATTA_TOTAL_POINTS_SUM:
return stringMessages.regattaTotalPointsTooltip();
case RANK_GAIN:
return stringMessages.rankGainTooltip();
case TACK:
@@ -17,26 +17,29 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.dto.FleetDTO;
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
import com.sap.sailing.domain.common.dto.RaceColumnDTO;
import com.sap.sailing.gwt.ui.actions.GetRaceTimesInfoAction;
import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO;
import com.sap.sse.gwt.client.async.AsyncActionsExecutor;
public class RaceTimesInfoProvider {
private final SailingServiceAsync sailingService;
private final AsyncActionsExecutor asyncActionsExecutor;
private final ErrorReporter errorReporter;
private final Set<RegattaAndRaceIdentifier> raceIdentifiers;
private long requestIntervalInMillis;
private final HashMap<RegattaAndRaceIdentifier, RaceTimesInfoDTO> raceTimesInfos;
private final Set<RaceTimesInfoProviderListener> listeners;
/**
* The <code>raceIdentifiers</code> has to be non-<code>null</code>, but can be empty.
*/
public RaceTimesInfoProvider(SailingServiceAsync sailingService, ErrorReporter errorReporter,
public RaceTimesInfoProvider(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter,
Collection<RegattaAndRaceIdentifier> raceIdentifiers, long requestIntervalInMillis) {
this.sailingService = sailingService;
this.asyncActionsExecutor = asyncActionsExecutor;
this.errorReporter = errorReporter;
this.raceIdentifiers = new HashSet<RegattaAndRaceIdentifier>(raceIdentifiers);
this.requestIntervalInMillis = requestIntervalInMillis;
@@ -97,7 +100,8 @@ public class RaceTimesInfoProvider {
private void readTimesInfos() {
if (!raceIdentifiers.isEmpty()) {
final long clientTimeWhenRequestWasSent = System.currentTimeMillis();
sailingService.getRaceTimesInfos(raceIdentifiers, new AsyncCallback<List<RaceTimesInfoDTO>>() {
GetRaceTimesInfoAction getRaceTimesInfoAction = new GetRaceTimesInfoAction(sailingService, raceIdentifiers);
asyncActionsExecutor.execute(getRaceTimesInfoAction, new AsyncCallback<List<RaceTimesInfoDTO>>() {
@Override
public void onFailure(Throwable caught) {
errorReporter.reportError("Error trying to obtain the race time infos: " + caught.getMessage(),
@@ -457,7 +457,7 @@ public interface SailingServiceAsync {
/**
* @param detailType
* supports {@link DetailType#REGATTA_RANK}, {@link DetailType#REGATTA_TOTAL_POINTS} and
* supports {@link DetailType#REGATTA_RANK}, {@link DetailType#REGATTA_TOTAL_POINTS_SUM} and
* {@link DetailType#OVERALL_RANK}.
*
* @return the first triple element is the race column name; then follows the list of competitors, and finally the
@@ -1044,7 +1044,7 @@ public interface StringMessages extends Messages {
String eventLogoImageURL();
String showPingMarkMappings();
String regattaNameMustNotContainSlashes();
String showNumberOfRacesCompleted();
String racesCompleted();
String racesCompletedTooltip();
String showNumberOfRacesScored();
String racesScored();
String racesScoredTooltip();
}
@@ -1030,6 +1030,6 @@ eventOfficialWebsiteURL=Official Website URL
eventLogoImageURL=Logo Image URL
showPingMarkMappings=Show Mappings for pinged Marks
regattaNameMustNotContainSlashes=Regatta name must not contain slashes
showNumberOfRacesCompleted=Show number of races completed
racesCompleted=Races completed
racesCompletedTooltip=Number of races the competitor has completed in this leaderboard.
showNumberOfRacesScored=Number of races scored
racesScored=Races scored
racesScoredTooltip=Number of races the competitor has completed or gotten a score for in this leaderboard.
@@ -1024,6 +1024,6 @@ eventOfficialWebsiteURL=URL der offiziellen Veranstaltungs-Website
eventLogoImageURL=URL des Veranstaltungs-Logos
showPingMarkMappings=Mappings für gepingte Marks zeigen
regattaNameMustNotContainSlashes=Regattaname darf keine Schrägstriche enthalten
showNumberOfRacesCompleted=Anzahl vollendeter Rennen
racesCompleted=Vollendete Rennen
racesCompletedTooltip=Anzahl von Rennen, die der Segler vollendet hat
showNumberOfRacesScored=Anzahl bepunkteter Rennen
racesScored=Bepunktete Rennen
racesScoredTooltip=Anzahl von Rennen, die der Segler vollendet oder für die er Punkte bekommen hat
@@ -982,7 +982,7 @@ pleaseSelectAScoringResult=Please select a scoring result...
eventOfficialWebsiteURL=Official Website URL
eventLogoImageURL=Logo Image URL
showPingMarkMappings=Show Mappings for pinged Marks
regattaNameMustNotContainSlashes=Regatta name must not contain slashes
showNumberOfRacesCompleted=Show number of races completed
racesCompleted=Races completed
racesCompletedTooltip=Number of races the competitor has completed in this leaderboard.
showNumberOfRacesScored=Показать количество завершенных гонок
racesScored=Завершенные гонки
racesScoredTooltip=Количество гонок завершенное участником в данной таблице лидеров
regattaNameMustNotContainSlashes=Regatta name must not contain slashes
@@ -150,7 +150,7 @@ public abstract class AbstractCompetitorLeaderboardChart<SettingsType> extends A
chart.getYAxis().setReversed(true);
chart.getYAxis().setTickInterval(1.0);
break;
case REGATTA_TOTAL_POINTS:
case REGATTA_TOTAL_POINTS_SUM:
chart.getYAxis().setTickInterval(5.0);
chart.getYAxis().setReversed(false);
break;
@@ -231,12 +231,12 @@ public abstract class AbstractCompetitorLeaderboardChart<SettingsType> extends A
setWidget(chart);
raceColumnNames.clear();
switch(selectedDetailType) {
switch (selectedDetailType) {
case OVERALL_RANK:
case REGATTA_RANK:
fillTotalRanksSeries(result, chartSeries);
break;
case REGATTA_TOTAL_POINTS:
case REGATTA_TOTAL_POINTS_SUM:
fillTotalPointsSeries(result, chartSeries);
break;
default:
@@ -298,7 +298,9 @@ public abstract class AbstractCompetitorLeaderboardChart<SettingsType> extends A
chart.setHeight(maxCompetitorCount * 30 + 100 + "px");
}
private void fillTotalPointsSeries(List<com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>>> result, List<Series> chartSeries) {
private void fillTotalPointsSeries(
List<com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>>> result,
List<Series> chartSeries) {
Double maxTotalPoints = 0.0;
Set<Series> unusedSeries = new HashSet<Series>(competitorSeries.values());
for (Series series : competitorSeries.values()) {
@@ -308,11 +310,10 @@ public abstract class AbstractCompetitorLeaderboardChart<SettingsType> extends A
}
int raceColumnNumber = 0;
int maxCompetitorCount = 0;
Map<CompetitorDTO, Double> sumTotalPointsMap = new HashMap<CompetitorDTO, Double>();
for (com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>> entry : result) {
List<Double> dataValues = entry.getC();
raceColumnNames.add(entry.getA());
if(hasValidValues(dataValues)) {
if (hasValidValues(dataValues)) {
raceColumnNamesWithData.add(entry.getA());
int index = 0;
maxCompetitorCount = Math.max(maxCompetitorCount, entry.getB().size());
@@ -320,18 +321,13 @@ public abstract class AbstractCompetitorLeaderboardChart<SettingsType> extends A
if (isCompetitorVisible(competitor)) {
Series series = getOrCreateSeries(competitor);
Double dataValue = dataValues.get(index);
if(dataValue != null) {
if (dataValue != null) {
Double sumTotalPoints = dataValue;
if(sumTotalPointsMap.containsKey(competitor)) {
sumTotalPoints += sumTotalPointsMap.get(competitor);
}
sumTotalPointsMap.put(competitor, sumTotalPoints);
series.addPoint(raceColumnNumber, sumTotalPoints, /* redraw */ false, /* shift */ false, /* animation */ false);
if(sumTotalPoints > maxTotalPoints) {
if (sumTotalPoints > maxTotalPoints) {
maxTotalPoints = sumTotalPoints;
}
}
unusedSeries.remove(series);
if (!chartSeries.contains(series)) {
chart.addSeries(series);
@@ -27,7 +27,7 @@ public class MultiCompetitorLeaderboardChartSettingsDialogComponent implements S
availableDetailsTypes = new ArrayList<DetailType>();
availableDetailsTypes.add(DetailType.REGATTA_RANK);
availableDetailsTypes.add(DetailType.REGATTA_TOTAL_POINTS);
availableDetailsTypes.add(DetailType.REGATTA_TOTAL_POINTS_SUM);
}
@Override
@@ -171,7 +171,7 @@ public class LeaderboardEntryPoint extends AbstractEntryPoint {
String chartDetailParam = GwtHttpRequestUtils.getStringParameter(PARAM_CHART_DETAIL, null);
DetailType chartDetailType;
if (chartDetailParam != null && (DetailType.REGATTA_RANK.name().equals(chartDetailParam) || DetailType.OVERALL_RANK.name().equals(chartDetailParam) ||
DetailType.REGATTA_TOTAL_POINTS.name().equals(chartDetailParam))) {
DetailType.REGATTA_TOTAL_POINTS_SUM.name().equals(chartDetailParam))) {
chartDetailType = DetailType.valueOf(chartDetailParam);
} else {
chartDetailType = leaderboardType.isMetaLeaderboard() ? DetailType.OVERALL_RANK : DetailType.REGATTA_RANK;
@@ -518,7 +518,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS
} else {
trackedRacesIdentifiers = Collections.emptyList();
}
raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), errorReporter,
raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, errorReporter,
trackedRacesIdentifiers, timer.getRefreshInterval());
}
return raceTimesInfoProvider;
@@ -1336,7 +1336,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS
@Override
public SafeHtmlHeader getHeader() {
return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesCompleted()), stringMessages.racesCompletedTooltip());
return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesScored()), stringMessages.racesScoredTooltip());
}
}
@@ -180,8 +180,8 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone
overallDetailCheckboxes);
overallDetailDialogContent.add(checkbox);
}
showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox = dialog.createCheckbox(stringMessages.showNumberOfRacesCompleted());
dialog.addTooltip(showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox, stringMessages.showNumberOfRacesCompleted());
showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox = dialog.createCheckbox(stringMessages.showNumberOfRacesScored());
dialog.addTooltip(showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox, stringMessages.showNumberOfRacesScored());
showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox.setValue(showOverallColumnWithNumberOfRacesSailedPerCompetitor);
overallDetailDialogContent.add(showOverallColumnWithNumberOfRacesSailedPerCompetitorCheckBox);
overallDetailDialog.add(overallDetailDialogContent);
@@ -172,12 +172,12 @@ public class LeaderboardUrlConfigurationDialog extends SettingsDialog<Leaderboar
chartDetailListBox = dialog.createListBox(false);
urlSettingsContent.add(chartDetailListBox);
if(leaderboardType.isMetaLeaderboard()) {
if (leaderboardType.isMetaLeaderboard()) {
chartDetailListBox.addItem(DetailType.OVERALL_RANK.name());
chartDetailListBox.addItem(DetailType.REGATTA_TOTAL_POINTS.name());
chartDetailListBox.addItem(DetailType.REGATTA_TOTAL_POINTS_SUM.name());
} else {
chartDetailListBox.addItem(DetailType.REGATTA_RANK.name());
chartDetailListBox.addItem(DetailType.REGATTA_TOTAL_POINTS.name());
chartDetailListBox.addItem(DetailType.REGATTA_TOTAL_POINTS_SUM.name());
}
chartDetailListBox.setSelectedIndex(0);
@@ -5,7 +5,13 @@ import java.util.Collections;
import java.util.List;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.DockLayoutPanel;
@@ -34,6 +40,7 @@ import com.sap.sailing.gwt.ui.shared.RegattaDTO;
import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO;
import com.sap.sailing.gwt.ui.shared.UserDTO;
import com.sap.sse.gwt.client.EntryPointHelper;
import com.sap.sse.gwt.client.async.AsyncActionsExecutor;
import com.sap.sse.gwt.client.player.Timer;
import com.sap.sse.gwt.client.player.Timer.PlayModes;
import com.sap.sse.gwt.client.useragent.UserAgentChecker;
@@ -59,6 +66,8 @@ public class RaceBoardEntryPoint extends AbstractEntryPoint {
private final MediaServiceAsync mediaService = GWT.create(MediaService.class);
private final UserManagementServiceAsync userManagementService = GWT.create(UserManagementService.class);
private boolean toolbarAndLogoAndTitleBarHidden;
@Override
protected void doOnModuleLoad() {
super.doOnModuleLoad();
@@ -92,9 +101,10 @@ public class RaceBoardEntryPoint extends AbstractEntryPoint {
final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, false);
final boolean autoSelectMedia = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_AUTOSELECT_MEDIA, false);
final boolean showMapControls = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_MAPCONTROLS, true /* default*/);
raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard, showWindChart,
showCompetitorsChart, canReplayWhileLiveIsPossible, autoSelectMedia);
final boolean showNavigationPanel = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_NAVIGATION_PANEL, true /* default */);
raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard,
showWindChart, showCompetitorsChart, canReplayWhileLiveIsPossible, autoSelectMedia, showNavigationPanel);
final ParallelExecutionCallback<List<String>> getLeaderboardNamesCallback = new ParallelExecutionCallback<List<String>>();
final ParallelExecutionCallback<List<RegattaDTO>> getRegattasCallback = new ParallelExecutionCallback<List<RegattaDTO>>();
final ParallelExecutionCallback<LeaderboardGroupDTO> getLeaderboardGroupByNameCallback = new ParallelExecutionCallback<LeaderboardGroupDTO>();
@@ -161,11 +171,12 @@ public class RaceBoardEntryPoint extends AbstractEntryPoint {
List<RegattaAndRaceIdentifier> singletonList = Collections.singletonList(selectedRace.getRaceIdentifier());
raceSelectionModel.setSelection(singletonList);
Timer timer = new Timer(PlayModes.Replay, 1000l);
RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, this, singletonList, 5000l /* requestInterval*/);
RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, user, timer, raceSelectionModel, leaderboardName,
AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor();
RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/);
RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, asyncActionsExecutor, user, timer, raceSelectionModel, leaderboardName,
leaderboardGroupName, raceboardViewConfig, RaceBoardEntryPoint.this, stringMessages, userAgent, raceTimesInfoProvider, showMapControls);
raceBoardPanel.fillRegattas(regattas);
createRaceBoardInOneScreenMode(raceBoardPanel);
createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig);
}
private RaceDTO findRace(String regattaName, String raceName, List<RegattaDTO> regattas) {
@@ -217,21 +228,47 @@ public class RaceBoardEntryPoint extends AbstractEntryPoint {
return logoAndTitlePanel;
}
private void createRaceBoardInOneScreenMode(RaceBoardPanel raceBoardPanel) {
DockLayoutPanel p = new DockLayoutPanel(Unit.PX);
private void createRaceBoardInOneScreenMode(RaceBoardPanel raceBoardPanel,
RaceBoardViewConfiguration raceboardViewConfiguration) {
final DockLayoutPanel p = new DockLayoutPanel(Unit.PX);
RootLayoutPanel.get().add(p);
Panel toolbarPanel = raceBoardPanel.getToolbarPanel();
final Panel toolbarPanel = raceBoardPanel.getToolbarPanel();
if (!UserAgentChecker.INSTANCE.isUserAgentSupported(userAgent)) {
HTML lbl = new HTML(stringMessages.warningBrowserUnsupported());
lbl.setStyleName("browserOptimizedMessage");
toolbarPanel.add(lbl);
}
FlowPanel logoAndTitlePanel = createLogoAndTitlePanel(raceBoardPanel);
final FlowPanel logoAndTitlePanel = createLogoAndTitlePanel(raceBoardPanel);
FlowPanel timePanel = createTimePanel(raceBoardPanel);
p.addNorth(logoAndTitlePanel, 68);
p.addNorth(toolbarPanel, 40);
p.addSouth(timePanel, 90);
toolbarAndLogoAndTitleBarHidden = false;
if (!raceboardViewConfiguration.isShowNavigationPanel()) {
p.setWidgetHidden(toolbarPanel, true);
globalNavigationPanel.setVisible(false);
toolbarAndLogoAndTitleBarHidden = true;
}
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
NativeEvent ne = event.getNativeEvent();
if ("keydown".equals(ne.getType()) && ne.getCtrlKey()
&& (ne.getKeyCode() == 'm' || ne.getKeyCode() == 'M')) {
ne.preventDefault();
Scheduler.get().scheduleDeferred(new Command() {
@Override
public void execute() {
p.setWidgetHidden(toolbarPanel, !toolbarAndLogoAndTitleBarHidden);
globalNavigationPanel.setVisible(toolbarAndLogoAndTitleBarHidden);
toolbarAndLogoAndTitleBarHidden = !toolbarAndLogoAndTitleBarHidden;
}
});
}
}
});
p.addSouth(timePanel, 90);
p.add(raceBoardPanel);
p.addStyleName("dockLayoutPanel");
}
}
}
@@ -131,7 +131,7 @@ public class RaceBoardPanel extends SimplePanel implements RegattasDisplayer, Ra
private final static String LOCAL_STORAGE_COMPETITORS_FILTER_SETS_KEY = "sailingAnalytics.raceBoard.competitorsFilterSets";
public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, UserDTO theUser, Timer timer,
public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, AsyncActionsExecutor asyncActionsExecutor, UserDTO theUser, Timer timer,
RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, String leaderboardGroupName,
RaceBoardViewConfiguration raceboardViewConfiguration, ErrorReporter errorReporter, final StringMessages stringMessages,
UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls) {
@@ -150,7 +150,7 @@ public class RaceBoardPanel extends SimplePanel implements RegattasDisplayer, Ra
racesByIdentifier = new HashMap<RaceIdentifier, RaceDTO>();
selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next();
this.setRaceBoardName(selectedRaceIdentifier.getRaceName());
asyncActionsExecutor = new AsyncActionsExecutor();
this.asyncActionsExecutor = asyncActionsExecutor;
FlowPanel mainPanel = new FlowPanel();
mainPanel.setSize("100%", "100%");
setWidget(mainPanel);
@@ -163,6 +163,7 @@ public class RaceBoardPanel extends SimplePanel implements RegattasDisplayer, Ra
componentControlsPanel = new FlowPanel();
componentControlsPanel.addStyleName("raceBoardNavigation");
toolbarPanel.add(componentControlsPanel);
viewControlsPanel = new FlowPanel();
viewControlsPanel.addStyleName("raceBoardControls");
toolbarPanel.add(viewControlsPanel);
@@ -12,6 +12,7 @@ public class RaceBoardViewConfiguration {
private String activeCompetitorsFilterSetName;
private boolean canReplayDuringLiveRaces;
private final boolean autoSelectMedia;
private final boolean showNavigationPanel;
public static final String PARAM_VIEW_MODE = "viewMode";
public static final String PARAM_VIEW_SHOW_LEADERBOARD = "viewShowLeaderboard";
@@ -28,16 +29,19 @@ public class RaceBoardViewConfiguration {
showWindChart = false;
showCompetitorsChart = false;
autoSelectMedia = false;
showNavigationPanel = true;
}
public RaceBoardViewConfiguration(String activeCompetitorsFilterSetName, boolean showLeaderboard,
boolean showWindChart, boolean showCompetitorsChart, boolean canReplayDuringLiveRaces, boolean autoSelectMedia) {
boolean showWindChart, boolean showCompetitorsChart, boolean canReplayDuringLiveRaces,
boolean autoSelectMedia, boolean showNavigationPanel) {
this.activeCompetitorsFilterSetName = activeCompetitorsFilterSetName;
this.showLeaderboard = showLeaderboard;
this.showWindChart = showWindChart;
this.showCompetitorsChart = showCompetitorsChart;
this.canReplayDuringLiveRaces = canReplayDuringLiveRaces;
this.autoSelectMedia = autoSelectMedia;
this.autoSelectMedia = autoSelectMedia;
this.showNavigationPanel = showNavigationPanel;
}
public boolean isShowLeaderboard() {
@@ -64,4 +68,8 @@ public class RaceBoardViewConfiguration {
return autoSelectMedia;
}
public boolean isShowNavigationPanel() {
return showNavigationPanel;
}
}
@@ -38,17 +38,19 @@ public class FlagImageResolver {
flagImageMap.put(Flags.ZULU, resources.flagZulu());
}
public ImageResource resolveFlagToImage(Flags flag) {
return flagImageMap.get(flag);
public ImageResource resolveFlagToImage(Flags flag, boolean isDisplayed, boolean displayStateChanged) {
return isDisplayed || displayStateChanged ? flagImageMap.get(flag) : null;
}
public ImageResource resolveFlagDirectionToImage(boolean isDisplayed) {
ImageResource result;
public ImageResource resolveFlagDirectionToImage(boolean isDisplayed, boolean displayStateChanged) {
ImageResource result = null;
if (isDisplayed) {
result = resources.arrowUp();
} else {
result = resources.arrowDown();
if (displayStateChanged) {
if (isDisplayed) {
result = resources.arrowUp();
} else {
result = resources.arrowDown();
}
}
return result;
@@ -31,7 +31,7 @@ public class RaceDetailPanel extends SimplePanel {
d = -d;
}
NumberFormat decimalFormat = NumberFormat.getFormat("##.###");
return "" + d + '\u00B0' + m + '\u2032' + decimalFormat.format(s) + '\u2033'; // add º, ', " symbols
return "" + d + '\u00B0' + m + '\u2032' + decimalFormat.format(s) + '\u2033'; // add �, ', " symbols
}
private static final int MAX_PROCEDURE_FIELDS = 2;
@@ -50,7 +50,9 @@ public class RaceDetailPanel extends SimplePanel {
private final Label finishTimeLabel = new Label();
private final Label finishDurationLabel = new Label();
private final Label protestTimeLabel = new Label();
private final Label protestDateLabel = new Label();
private final Label updateTimeLabel = new Label();
private final Label updateDateLabel = new Label();
private final Label vesselPositionLabel = new Label();
private final Label windLabel = new Label();
private final Label startProcedureLabel = new Label();
@@ -94,9 +96,9 @@ public class RaceDetailPanel extends SimplePanel {
basicsGrid.setWidget(2, 1, protestTimeLabel);
basicsGrid.setWidget(0, 2, startDateLabel);
basicsGrid.setWidget(1, 2, finishDurationLabel);
basicsGrid.setWidget(2, 2, new Label(""));
basicsGrid.setWidget(2, 2, protestDateLabel);
Grid managementGrid = new Grid(3, 2);
Grid managementGrid = new Grid(3, 3);
managementGrid.setCellSpacing(cellSpacing);
managementGrid.setWidget(0, 0, new Label("Last update at"));
managementGrid.setWidget(1, 0, new Label("Vessel position"));
@@ -104,6 +106,7 @@ public class RaceDetailPanel extends SimplePanel {
managementGrid.setWidget(0, 1, updateTimeLabel);
managementGrid.setWidget(1, 1, vesselPositionLabel);
managementGrid.setWidget(2, 1, windLabel);
managementGrid.setWidget(0, 2, updateDateLabel);
procedureGrid = new FlexTable();
procedureGrid.setCellSpacing(cellSpacing);
@@ -138,7 +141,7 @@ public class RaceDetailPanel extends SimplePanel {
return;
}
String statusText = flagInterpreter.getMeaningOfRaceStateAndFlags(data.raceInfo.lastStatus,
data.raceInfo.lastUpperFlag, data.raceInfo.lastLowerFlag, data.raceInfo.isLastFlagDisplayed);
data.raceInfo.lastUpperFlag, data.raceInfo.lastLowerFlag, data.raceInfo.lastFlagsAreDisplayed);
String headerLabelText = data.regattaDisplayName + " " + data.raceInfo.fleetName + " " + data.raceInfo.raceName
+ " (" + statusText + ")";
headerLabel.setText(stringMessages.showingDetailsOfRace(headerLabelText));
@@ -166,10 +169,18 @@ public class RaceDetailPanel extends SimplePanel {
.format(data.raceInfo.protestFinishTime);
protestTimeLabel.setText(protestTimeText);
String protestDateText = data.raceInfo.protestFinishTime == null ? "" : dateFormatter
.format(data.raceInfo.protestFinishTime);
protestDateLabel.setText(protestDateText);
String updateTimeText = data.raceInfo.lastUpdateTime == null ? "-" : timeFormatter
.format(data.raceInfo.lastUpdateTime);
updateTimeLabel.setText(updateTimeText);
String updateDateText = data.raceInfo.lastUpdateTime == null ? "" : dateFormatter
.format(data.raceInfo.lastUpdateTime);
updateDateLabel.setText(updateDateText);
String vesselPositionText = stringMessages.unknown();
String windText = stringMessages.unknown();
if (data.raceInfo.lastWind != null) {
@@ -186,27 +186,30 @@ public class RegattaOverviewPanel extends SimplePanel {
flexTable.setWidget(0, 0, eventVenueGrid);
flexTable.getCellFormatter().setVerticalAlignment(0, 0, HasVerticalAlignment.ALIGN_MIDDLE);
boolean showLeaderboardButton = Window.Location.getParameter("enableLeaderboard") != null
final boolean showLeaderboardButton = Window.Location.getParameter("enableLeaderboard") != null
&& Window.Location.getParameter("enableLeaderboard").equalsIgnoreCase("true");
if (showLeaderboardButton) {
leaderboardCheckBox = addLeaderboardEnablerButton();
leaderboardCheckBox.getElement().getStyle().setMarginLeft(20.0, Unit.PX);
leaderboardsTabPanel = new TabPanel();
leaderboardsTabPanel.setStyleName("RegattaOverview-Leaderboards");
mainPanel.add(leaderboardsTabPanel);
} else {
leaderboardCheckBox = null;
leaderboardsTabPanel = null;
}
HorizontalPanel refreshStartStopClockPanel = getRefreshStartStopClockPanel();
flexTable.setWidget(0, 1, refreshStartStopClockPanel);
mainPanel.add(flexTable);
mainPanel.add(regattaRaceStatesComponent);
if (showLeaderboardButton) {
leaderboardsTabPanel = new TabPanel();
leaderboardsTabPanel.setStyleName("RegattaOverview-Leaderboards");
leaderboardsTabPanel.setVisible(false);
mainPanel.add(leaderboardsTabPanel);
} else {
leaderboardsTabPanel = null;
}
onUpdateUI(uiUpdateTimer.getLiveTimePointAsDate());
}
@@ -254,8 +257,13 @@ public class RegattaOverviewPanel extends SimplePanel {
errorReporter, stringMessages, userAgent, /*showRaceDetails*/false);
leaderboardsTabPanel.add(leaderboardPanel, leaderboard.getDisplayName() + " " + stringMessages.leaderboard());
}
leaderboardsTabPanel.setVisible(true);
leaderboardsTabPanel.selectTab(0);
if (!result.isEmpty()) {
leaderboardsTabPanel.setVisible(true);
leaderboardsTabPanel.selectTab(0);
} else {
leaderboardCheckBox.setValue(false);
errorReporter.reportError("Error trying to load leaderboard. Event could not be associated to Regatta.");
}
}
@Override
public void onFailure(Throwable caught) {
@@ -16,6 +16,7 @@ import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
@@ -38,6 +39,7 @@ import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.datepicker.client.CalendarUtil;
import com.google.gwt.view.client.ListDataProvider;
import com.sap.sailing.domain.common.LeaderboardNameConstants;
import com.sap.sailing.domain.common.PassingInstruction;
@@ -83,9 +85,11 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
private ListDataProvider<RegattaOverviewEntryDTO> regattaOverviewDataProvider;
private final VerticalPanel mainPanel;
private final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm:ss");
private final DateTimeFormat dateFormatter = DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT);
private final SailingServiceAsync sailingService;
private final StringMessages stringMessages;
private final ErrorReporter errorReporter;
private final UUID eventId;
private EventDTO eventDTO;
@@ -126,6 +130,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
final StringMessages stringMessages, final UUID eventId, RegattaRaceStatesSettings settings, Timer timerToSynchronize) {
this.sailingService = sailingService;
this.stringMessages = stringMessages;
this.errorReporter = errorReporter;
this.eventId = eventId;
this.allEntries = new ArrayList<RegattaOverviewEntryDTO>();
this.timerToSynchronize = timerToSynchronize;
@@ -190,7 +195,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
new AsyncCallback<List<RegattaOverviewEntryDTO>>() {
@Override
public void onFailure(Throwable cause) {
errorReporter.reportError(cause.getMessage());
}
@Override
@@ -207,6 +212,18 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
}
}));
}
private String formatTime(Date time) {
String result = "-";
if (time != null) {
result = "";
if (! CalendarUtil.isSameDate(time, new Date())) {
result += dateFormatter.format(time) + " ";
}
result += timeFormatter.format(time);
}
return result;
}
private CellTable<RegattaOverviewEntryDTO> createRegattaTable() {
CellTable<RegattaOverviewEntryDTO> table = new CellTable<RegattaOverviewEntryDTO>(/* pageSize */10000, tableRes);
@@ -341,11 +358,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
TextColumn<RegattaOverviewEntryDTO> raceStartTimeColumn = new TextColumn<RegattaOverviewEntryDTO>() {
@Override
public String getValue(RegattaOverviewEntryDTO entryDTO) {
String result = "-";
if (entryDTO.raceInfo.startTime != null) {
result = timeFormatter.format(entryDTO.raceInfo.startTime);
}
return result;
return formatTime(entryDTO.raceInfo.startTime);
}
};
raceStartTimeColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
@@ -375,7 +388,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
@Override
public SafeHtml getValue(final RegattaOverviewEntryDTO entryDTO) {
String status = flagInterpreter.getMeaningOfRaceStateAndFlags(entryDTO.raceInfo.lastStatus, entryDTO.raceInfo.lastUpperFlag,
entryDTO.raceInfo.lastLowerFlag, entryDTO.raceInfo.isLastFlagDisplayed);
entryDTO.raceInfo.lastLowerFlag, entryDTO.raceInfo.lastFlagsAreDisplayed);
SafeHtmlBuilder sb = new SafeHtmlBuilder();
sb.appendHtmlConstant("<a class=\"pointeredLink\">");
sb.appendEscaped(status);
@@ -444,12 +457,13 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
tooltip += " " + FlagsMeaningExplanator.getFlagsMeaning(stringMessages,
entryDTO.raceInfo.lastUpperFlag,
entryDTO.raceInfo.lastLowerFlag,
entryDTO.raceInfo.isLastFlagDisplayed);
entryDTO.raceInfo.lastFlagsAreDisplayed);
}
return SailingFlagsBuilder.render(
entryDTO.raceInfo.lastUpperFlag,
entryDTO.raceInfo.lastLowerFlag,
entryDTO.raceInfo.isLastFlagDisplayed,
entryDTO.raceInfo.lastFlagsAreDisplayed,
entryDTO.raceInfo.lastFlagsDisplayedStateChanged,
tooltip);
}
@@ -459,11 +473,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
TextColumn<RegattaOverviewEntryDTO> lastUpdateColumn = new TextColumn<RegattaOverviewEntryDTO>() {
@Override
public String getValue(RegattaOverviewEntryDTO entryDTO) {
String lastUpdateTime = "-";
if (entryDTO.raceInfo.lastUpdateTime != null) {
lastUpdateTime = timeFormatter.format(entryDTO.raceInfo.lastUpdateTime);
}
return lastUpdateTime;
return formatTime(entryDTO.raceInfo.lastUpdateTime);
}
};
lastUpdateColumn.setSortable(true);
@@ -486,7 +496,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
} else if (entryDTO.raceInfo.lastLowerFlag != null && entryDTO.raceInfo.lastLowerFlag.equals(Flags.HOTEL)) {
additionalInformation.append(stringMessages.furtherSignalsAshore());
isInfoBefore = true;
} else if (entryDTO.raceInfo.lastUpperFlag != null && entryDTO.raceInfo.lastUpperFlag.equals(Flags.XRAY) && entryDTO.raceInfo.isLastFlagDisplayed) {
} else if (entryDTO.raceInfo.lastUpperFlag != null && entryDTO.raceInfo.lastUpperFlag.equals(Flags.XRAY) && entryDTO.raceInfo.lastFlagsAreDisplayed) {
additionalInformation.append(stringMessages.earlyStarters());
isInfoBefore = true;
}
@@ -40,15 +40,15 @@ public class SailingFlagsBuilder {
return SafeStylesUtils.fromTrustedString("float:left;padding-right:10px;");
}
public static SafeHtml render(Flags upperFlag, Flags lowerFlag, boolean isDisplayed, String tooltip) {
public static SafeHtml render(Flags upperFlag, Flags lowerFlag, boolean isDisplayed, boolean displayStateChanged, String tooltip) {
FlagImageResolver flagImageResolver = new FlagImageResolver();
ImageResource upperFlagImage = flagImageResolver.resolveFlagToImage(upperFlag);
ImageResource lowerFlagImage = flagImageResolver.resolveFlagToImage(lowerFlag);
ImageResource directionImage = flagImageResolver.resolveFlagDirectionToImage(isDisplayed);
ImageResource upperFlagImage = flagImageResolver.resolveFlagToImage(upperFlag, isDisplayed, displayStateChanged);
ImageResource lowerFlagImage = flagImageResolver.resolveFlagToImage(lowerFlag, isDisplayed, displayStateChanged);
ImageResource directionImage = flagImageResolver.resolveFlagDirectionToImage(isDisplayed, displayStateChanged);
SafeHtml upperFlagHtml = (upperFlagImage != null) ? makeImagePrototype(upperFlagImage).getSafeHtml() : SafeHtmlUtils.EMPTY_SAFE_HTML;
SafeHtml lowerFlagHtml = (lowerFlagImage != null) ? makeImagePrototype(lowerFlagImage).getSafeHtml() : SafeHtmlUtils.EMPTY_SAFE_HTML;
SafeHtml directionHtml = (upperFlagImage != null) ? makeImagePrototype(directionImage).getSafeHtml() : SafeHtmlUtils.EMPTY_SAFE_HTML;
SafeHtml directionHtml = (directionImage != null) ? makeImagePrototype(directionImage).getSafeHtml() : SafeHtmlUtils.EMPTY_SAFE_HTML;
SafeHtmlBuilder builder = new SafeHtmlBuilder();
SafeStyles imgStyle = getImageStyle();
@@ -67,6 +67,7 @@ import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.LeaderboardGroupBase;
import com.sap.sailing.domain.base.LeaderboardSearchResult;
import com.sap.sailing.domain.base.LeaderboardSearchResultBase;
import com.sap.sailing.domain.base.Leg;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.RaceColumn;
@@ -142,8 +143,6 @@ import com.sap.sailing.domain.common.dto.FullLeaderboardDTO;
import com.sap.sailing.domain.common.dto.IncrementalLeaderboardDTO;
import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO;
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO;
import com.sap.sailing.domain.common.dto.LeaderboardRowDTO;
import com.sap.sailing.domain.common.dto.PositionDTO;
import com.sap.sailing.domain.common.dto.RaceColumnDTO;
import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO;
@@ -382,6 +381,7 @@ import com.sap.sailing.util.BuildVersion;
import com.sap.sailing.xrr.resultimport.schema.RegattaResults;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.Util.Triple;
import com.sap.sse.common.search.KeywordQuery;
import com.sap.sse.common.search.Result;
@@ -397,6 +397,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
private final ServiceTracker<ReplicationService, ReplicationService> replicationServiceTracker;
private final ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> resultUrlRegistryServiceTracker;
private final ServiceTracker<ScoreCorrectionProvider, ScoreCorrectionProvider> scoreCorrectionProviderServiceTracker;
private final MongoObjectFactory mongoObjectFactory;
@@ -451,6 +453,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
context = Activator.getDefault();
racingEventServiceTracker = createAndOpenRacingEventServiceTracker(context);
replicationServiceTracker = createAndOpenReplicationServiceTracker(context);
resultUrlRegistryServiceTracker = createAndOpenResultUrlRegistryServiceTracker(context);
swissTimingAdapterTracker = createAndOpenSwissTimingAdapterTracker(context);
tractracAdapterTracker = createAndOpenTracTracAdapterTracker(context);
raceLogTrackingAdapterTracker = createAndOpenRaceLogTrackingAdapterTracker(context);
@@ -556,6 +559,14 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return result;
}
protected ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> createAndOpenResultUrlRegistryServiceTracker(
BundleContext context) {
ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> result = new ServiceTracker<ResultUrlRegistry, ResultUrlRegistry>(
context, ResultUrlRegistry.class.getName(), null);
result.open();
return result;
}
/**
* Asks the OSGi system for registered score correction provider services
*/
@@ -820,11 +831,16 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
if (startTime != null) {
FlagPoleState activeFlagState = state.getRacingProcedure().getActiveFlags(startTime, MillisecondsTimePoint.now());
List<FlagPole> activeFlags = activeFlagState.getCurrentState();
FlagPoleState previousFlagState = activeFlagState.getPreviousState(state.getRacingProcedure(), startTime);
List<FlagPole> previousFlags = previousFlagState.getCurrentState();
FlagPole mostInterestingFlagPole = FlagPoleState.getMostInterestingFlagPole(previousFlags, activeFlags);
// TODO: adapt the LastFlagFinder#getMostRecent method!
if (!activeFlags.isEmpty()) {
raceInfoDTO.lastUpperFlag = activeFlags.get(0).getUpperFlag();
raceInfoDTO.lastLowerFlag = activeFlags.get(0).getLowerFlag();
raceInfoDTO.isLastFlagDisplayed = activeFlags.get(0).isDisplayed();
if (mostInterestingFlagPole != null) {
raceInfoDTO.lastUpperFlag = mostInterestingFlagPole.getUpperFlag();
raceInfoDTO.lastLowerFlag = mostInterestingFlagPole.getLowerFlag();
raceInfoDTO.lastFlagsAreDisplayed = mostInterestingFlagPole.isDisplayed();
raceInfoDTO.lastFlagsDisplayedStateChanged = previousFlagState.hasPoleChanged(mostInterestingFlagPole);
}
}
@@ -837,7 +853,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
if (raceInfoDTO.lastStatus.equals(RaceLogRaceStatus.UNSCHEDULED)) {
raceInfoDTO.lastUpperFlag = abortingFlagEvent.getUpperFlag();
raceInfoDTO.lastLowerFlag = abortingFlagEvent.getLowerFlag();
raceInfoDTO.isLastFlagDisplayed = abortingFlagEvent.isDisplayed();
raceInfoDTO.lastFlagsAreDisplayed = abortingFlagEvent.isDisplayed();
raceInfoDTO.lastFlagsDisplayedStateChanged = true;
}
}
@@ -854,7 +871,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
raceInfoDTO.protestFinishTime = protestStartTime.plus(protestDuration).asDate();
raceInfoDTO.lastUpperFlag = Flags.BRAVO;
raceInfoDTO.lastLowerFlag = Flags.NONE;
raceInfoDTO.isLastFlagDisplayed = true;
raceInfoDTO.lastFlagsAreDisplayed = true;
raceInfoDTO.lastFlagsDisplayedStateChanged = true;
}
}
@@ -873,7 +891,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
raceInfoDTO.isTracked = raceColumn.getTrackedRace(fleet) != null ? true : false;
return raceInfoDTO;
}
}
private void fillStartProcedureSpecifics(RaceInfoDTO raceInfoDTO, ReadonlyRaceState state) {
RaceInfoExtensionDTO info = null;
@@ -1388,12 +1406,18 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public CompactRaceMapDataDTO getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date,
Map<String, Date> fromPerCompetitorIdAsString, Map<String, Date> toPerCompetitorIdAsString,
boolean extrapolate) throws NoWindException {
return new CompactRaceMapDataDTO(getBoatPositions(raceIdentifier, fromPerCompetitorIdAsString,
toPerCompetitorIdAsString, extrapolate), getCoursePositions(raceIdentifier, date), getCourseSidelines(raceIdentifier, date), getQuickRanks(
raceIdentifier, date));
final Map<CompetitorDTO, List<GPSFixDTO>> boatPositions = getBoatPositions(raceIdentifier,
fromPerCompetitorIdAsString, toPerCompetitorIdAsString, extrapolate);
final CoursePositionsDTO coursePositions = getCoursePositions(raceIdentifier, date);
final List<SidelineDTO> courseSidelines = getCourseSidelines(raceIdentifier, date);
final List<QuickRankDTO> quickRanks = getQuickRanks(raceIdentifier, date);
return new CompactRaceMapDataDTO(boatPositions, coursePositions, courseSidelines, quickRanks);
}
/**
* {@link LegType}s are cached within the method with a resolution of one minute. The cache key is a pair of
* {@link TrackedLegOfCompetitor} and {@link TimePoint}.
*
* @param from
* for the list of competitors provided as keys of this map, requests the GPS fixes starting with the
* date provided as value
@@ -1410,7 +1434,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
*/
private Map<CompetitorDTO, List<GPSFixDTO>> getBoatPositions(RegattaAndRaceIdentifier raceIdentifier,
Map<String, Date> fromPerCompetitorIdAsString, Map<String, Date> toPerCompetitorIdAsString,
boolean extrapolate) throws NoWindException {
boolean extrapolate)
throws NoWindException {
Map<Pair<Leg, TimePoint>, LegType> legTypeCache = new HashMap<>();
Map<CompetitorDTO, List<GPSFixDTO>> result = new HashMap<CompetitorDTO, List<GPSFixDTO>>();
TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier);
if (trackedRace != null) {
@@ -1460,19 +1486,28 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
GPSFixMoving fix = fixIter.next();
while (fix != null && (fix.getTimePoint().compareTo(toTimePointExcluding) < 0 ||
(fix.getTimePoint().equals(toTimePointExcluding) && toTimePointExcluding.equals(fromTimePoint)))) {
Tack tack;
try {
tack = trackedRace.getTack(competitor, fix.getTimePoint());
} catch (NoWindException nwe) {
tack = null;
}
Wind wind = trackedRace.getWind(fix.getPosition(), toTimePointExcluding);
final SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(fix.getTimePoint());
Tack tack = wind == null? null : trackedRace.getTack(estimatedSpeed, wind, fix.getTimePoint());
TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor,
fix.getTimePoint());
LegType legType = trackedLegOfCompetitor == null ? null : trackedRace.getTrackedLeg(
trackedLegOfCompetitor.getLeg()).getLegType(fix.getTimePoint());
Wind wind = trackedRace.getWind(fix.getPosition(),toTimePointExcluding);
LegType legType;
if (trackedLegOfCompetitor != null && trackedLegOfCompetitor.getLeg() != null) {
TimePoint quantifiedTimePoint = quantifyTimePointWithResolution(fix.getTimePoint(), /* resolutionInMilliseconds */
60000);
Pair<Leg, TimePoint> cacheKey = new Pair<Leg, TimePoint>(
trackedLegOfCompetitor.getLeg(), quantifiedTimePoint);
legType = legTypeCache.get(cacheKey);
if (legType == null) {
legType = trackedRace.getTrackedLeg(trackedLegOfCompetitor.getLeg()).getLegType(
fix.getTimePoint());
legTypeCache.put(cacheKey, legType);
}
} else {
legType = null;
}
WindDTO windDTO = wind == null ? null : createWindDTOFromAlreadyAveraged(wind, toTimePointExcluding);
GPSFixDTO fixDTO = createGPSFixDTO(fix, track.getEstimatedSpeed(fix.getTimePoint()), windDTO, tack, legType, /* extrapolate */
GPSFixDTO fixDTO = createGPSFixDTO(fix, estimatedSpeed, windDTO, tack, legType, /* extrapolate */
false);
fixesForCompetitor.add(fixDTO);
if (fixIter.hasNext()) {
@@ -1481,22 +1516,31 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
// check if fix was at date and if extrapolation is requested
if (!fix.getTimePoint().equals(toTimePointExcluding) && extrapolate) {
Position position = track.getEstimatedPosition(toTimePointExcluding, extrapolate);
Tack tack2;
try {
tack2 = trackedRace.getTack(competitor, toTimePointExcluding);
} catch (NoWindException nwe) {
tack2 = null;
}
LegType legType2 = trackedLegOfCompetitor == null ? null : trackedRace
.getTrackedLeg(trackedLegOfCompetitor.getLeg()).getLegType(
fix.getTimePoint());
SpeedWithBearing speedWithBearing = track.getEstimatedSpeed(toTimePointExcluding);
Wind wind2 = trackedRace.getWind(position, toTimePointExcluding);
SpeedWithBearing estimatedSpeed2 = track.getEstimatedSpeed(toTimePointExcluding);
Tack tack2 = wind2 == null ? null : trackedRace.getTack(estimatedSpeed2, wind2, toTimePointExcluding);
LegType legType2;
if (trackedLegOfCompetitor != null && trackedLegOfCompetitor.getLeg() != null) {
TimePoint quantifiedTimePoint = quantifyTimePointWithResolution(
fix.getTimePoint(), /* resolutionInMilliseconds */
60000);
Pair<Leg, TimePoint> cacheKey = new Pair<Leg, TimePoint>(
trackedLegOfCompetitor.getLeg(), quantifiedTimePoint);
legType2 = legTypeCache.get(cacheKey);
if (legType2 == null) {
legType2 = trackedRace.getTrackedLeg(trackedLegOfCompetitor.getLeg())
.getLegType(
fix.getTimePoint());
legTypeCache.put(cacheKey, legType2);
}
} else {
legType2 = null;
}
WindDTO windDTO2 = wind2 == null ? null : createWindDTOFromAlreadyAveraged(wind2, toTimePointExcluding);
GPSFixDTO extrapolated = new GPSFixDTO(
toPerCompetitorIdAsString.get(competitorDTO.getIdAsString()),
position==null?null:new PositionDTO(position.getLatDeg(), position.getLngDeg()),
speedWithBearing==null?null:createSpeedWithBearingDTO(speedWithBearing), windDTO2,
estimatedSpeed2==null?null:createSpeedWithBearingDTO(estimatedSpeed2), windDTO2,
tack2, legType2, /* extrapolated */ true);
fixesForCompetitor.add(extrapolated);
}
@@ -1510,6 +1554,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return result;
}
private TimePoint quantifyTimePointWithResolution(TimePoint timePoint, long resolutionInMilliseconds) {
return new MillisecondsTimePoint(timePoint.asMillis() / resolutionInMilliseconds * resolutionInMilliseconds);
}
private SpeedWithBearingDTO createSpeedWithBearingDTO(SpeedWithBearing speedWithBearing) {
return new SpeedWithBearingDTO(speedWithBearing.getKnots(), speedWithBearing
.getBearing().getDegrees());
@@ -2563,18 +2611,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return result;
}
private com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>> getTotalPointsForRaceColumn(LeaderboardDTO leaderboard,
RaceColumnDTO raceColumn) throws NoWindException {
List<Double> values = new ArrayList<Double>();
List<CompetitorDTO> competitorDTOs = new ArrayList<CompetitorDTO>();
for (Entry<CompetitorDTO, LeaderboardRowDTO> entry : leaderboard.rows.entrySet()) {
LeaderboardEntryDTO leaderboardEntryDTO = entry.getValue().fieldsByRaceColumnName.get(raceColumn.getName());
values.add(leaderboardEntryDTO != null ? leaderboardEntryDTO.totalPoints : null);
competitorDTOs.add(entry.getKey());
}
return new com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>>(raceColumn.getName(), competitorDTOs, values);
}
@Override
public List<com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>>> getLeaderboardDataEntriesForAllRaceColumns(String leaderboardName,
Date date, DetailType detailType) throws Exception {
@@ -2589,19 +2625,22 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
} else {
timePoint = new MillisecondsTimePoint(date);
}
TimePoint effectiveTimePoint = timePoint == null ? leaderboard.getNowMinusDelay() : timePoint;
if (detailType != null) {
switch (detailType) {
case REGATTA_TOTAL_POINTS:
LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(timePoint, Collections.<String> emptyList(),
/* addOverallDetails */ false, getService(), baseDomainFactory);
for (RaceColumnDTO raceColumnDTO : leaderboardDTO.getRaceList()) {
result.add(getTotalPointsForRaceColumn(leaderboardDTO, raceColumnDTO));
case REGATTA_TOTAL_POINTS_SUM:
for (Entry<RaceColumn, Map<Competitor, Double>> e : leaderboard.getTotalPointsSumAfterRaceColumn(effectiveTimePoint).entrySet()) {
List<CompetitorDTO> competitorDTOs = new ArrayList<>();
List<Double> pointSums = new ArrayList<>();
for (Entry<Competitor, Double> e2 : e.getValue().entrySet()) {
competitorDTOs.add(baseDomainFactory.convertToCompetitorDTO(e2.getKey()));
pointSums.add(e2.getValue());
}
result.add(new Triple<String, List<CompetitorDTO>, List<Double>>(e.getKey().getName(), competitorDTOs, pointSums));
}
break;
case REGATTA_RANK:
case OVERALL_RANK:
TimePoint effectiveTimePoint = timePoint == null ?
leaderboard.getNowMinusDelay() : timePoint;
Map<RaceColumn, List<Competitor>> competitorsFromBestToWorst = leaderboard
.getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(effectiveTimePoint);
for (Entry<RaceColumn, List<Competitor>> e : competitorsFromBestToWorst.entrySet()) {
@@ -2613,7 +2652,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
competitorDTOs.add(baseDomainFactory.convertToCompetitorDTO(competitor));
rank++;
}
result.add(new com.sap.sse.common.Util.Triple<String, List<CompetitorDTO>, List<Double>>(e.getKey().getName(), competitorDTOs, values));
result.add(new Triple<String, List<CompetitorDTO>, List<Double>>(e.getKey().getName(), competitorDTOs, values));
}
break;
default:
@@ -3397,8 +3436,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public List<String> getResultImportUrls(String resultProviderName) {
List<String> result = new ArrayList<String>();
ResultUrlProvider urlBasedScoreCorrectionProvider = getUrlBasedScoreCorrectionProvider(resultProviderName);
ResultUrlRegistry resultUrlRegistry = getResultUrlRegistry();
if (urlBasedScoreCorrectionProvider != null) {
Iterable<URL> allUrls = ResultUrlRegistry.INSTANCE.getResultUrls(resultProviderName);
Iterable<URL> allUrls = resultUrlRegistry.getResultUrls(resultProviderName);
for (URL url : allUrls) {
result.add(url.toString());
}
@@ -3409,9 +3449,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
@Override
public void removeResultImportURLs(String resultProviderName, Set<String> toRemove) throws Exception {
ResultUrlProvider urlBasedScoreCorrectionProvider = getUrlBasedScoreCorrectionProvider(resultProviderName);
ResultUrlRegistry resultUrlRegistry = getResultUrlRegistry();
if (urlBasedScoreCorrectionProvider != null) {
for (String urlToRemove : toRemove) {
ResultUrlRegistry.INSTANCE.unregisterResultUrl(resultProviderName, new URL(urlToRemove));
resultUrlRegistry.unregisterResultUrl(resultProviderName, new URL(urlToRemove));
}
}
}
@@ -3420,8 +3461,13 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public void addResultImportUrl(String resultProviderName, String url) throws Exception {
ResultUrlProvider urlBasedScoreCorrectionProvider = getUrlBasedScoreCorrectionProvider(resultProviderName);
if (urlBasedScoreCorrectionProvider != null) {
ResultUrlRegistry.INSTANCE.registerResultUrl(resultProviderName, new URL(url));
ResultUrlRegistry resultUrlRegistry = getResultUrlRegistry();
resultUrlRegistry.registerResultUrl(resultProviderName, new URL(url));
}
}
private ResultUrlRegistry getResultUrlRegistry() {
return resultUrlRegistryServiceTracker.getService();
}
@Override
@@ -14,6 +14,7 @@ import java.util.logging.Logger;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.Duration;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
@@ -42,6 +43,7 @@ import com.sap.sailing.gwt.ui.shared.SimulatorResultsDTO;
import com.sap.sailing.gwt.ui.shared.SimulatorUISelectionDTO;
import com.sap.sailing.gwt.ui.shared.SimulatorWindDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldDTO.WindData;
import com.sap.sailing.gwt.ui.shared.WindFieldGenParamsDTO;
import com.sap.sailing.gwt.ui.shared.WindLatticeDTO;
import com.sap.sailing.gwt.ui.shared.WindLatticeGenParamsDTO;
@@ -202,13 +204,9 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
Position[][] grid = bd.extractGrid(params.getxRes(), params.getyRes(), params.getBorderY(), params.getBorderX());
wf.setPositionGrid(grid);
TimePoint startTime = new MillisecondsTimePoint(params.getStartTime().getTime());// new
// MillisecondsTimePoint(0);
TimePoint timeStep = new MillisecondsTimePoint(params.getTimeStep().getTime());// new
// MillisecondsTimePoint(30*1000);
TimePoint endTime = new MillisecondsTimePoint(params.getEndTime().getTime());// new MillisecondsTimePoint(10 *
// 60 * 1000);
TimePoint startTime = new MillisecondsTimePoint(params.getStartTime().getTime());
Duration timeStep = params.getTimeStep();
TimePoint endTime = new MillisecondsTimePoint(params.getEndTime().getTime());
wf.generate(startTime, null, timeStep);
if (params.getMode() != SailingSimulatorConstants.ModeMeasured) {
@@ -217,8 +215,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
wf.setGridAreaGps(gridAreaGps);
}
WindFieldDTO wfDTO = createWindFieldDTO(wf, startTime, endTime, timeStep, params); //params.isShowStreamlets2(), params.isShowLines(), params.getSeedLines());
WindFieldDTO wfDTO = createWindFieldDTO(wf, startTime, endTime, timeStep, params);
LOGGER.info("Exiting getWindField");
return wfDTO;
@@ -249,7 +246,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
WindFieldGenerator wf = null;
List<Position> course = null;
TimePoint startTime = new MillisecondsTimePoint(params.getStartTime().getTime());
TimePoint timeStep = new MillisecondsTimePoint(params.getTimeStep().getTime());
Duration timeStep = params.getTimeStep();
this.controlParameters.resetBlastRandomStream = params.isKeepState();
this.retreiveWindControlParameters(pattern);
@@ -724,120 +721,49 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
return result;
}
private WindFieldDTO createWindFieldDTO(WindFieldGenerator wf, TimePoint startTime, TimePoint endTime, TimePoint timeStep, WindFieldGenParamsDTO params) {
//boolean isStreamlets, boolean isShowLines, char seedLines) {
private WindFieldDTO createWindFieldDTO(WindFieldGenerator wf, TimePoint startTime, TimePoint endTime,
Duration timeStep, WindFieldGenParamsDTO params) {
WindFieldDTO windFieldDTO = new WindFieldDTO();
Position[][] positionGrid = wf.getPositionGrid();
//if (!params.isShowStreamlets2()) {
List<SimulatorWindDTO> wList = new ArrayList<SimulatorWindDTO>();
if (positionGrid != null && positionGrid.length > 0) {
TimePoint t = startTime;
while (t.compareTo(endTime) <= 0) {
for (int i = 0; i < positionGrid.length; ++i) {
for (int j = 0; j < positionGrid[i].length; ++j) {
Wind localWind = wf.getWind(new TimedPositionWithSpeedImpl(t, positionGrid[i][j], null));
LOGGER.finer(localWind.toString());
wList.add(createSimulatorWindDTO(localWind));
}
}
t = new MillisecondsTimePoint(t.asMillis() + timeStep.asMillis());
}
TimePoint t = startTime;
while (t.compareTo(endTime) <= 0) {
for (int i = 0; i < positionGrid.length; ++i) {
for (int j = 0; j < positionGrid[i].length; ++j) {
Wind localWind = wf.getWind(new TimedPositionWithSpeedImpl(t, positionGrid[i][j], null));
LOGGER.finer(localWind.toString());
wList.add(createSimulatorWindDTO(localWind));
}
}
t = new MillisecondsTimePoint(t.asMillis() + timeStep.asMillis());
}
}
windFieldDTO.setMatrix(wList);
if (params.isShowLines() && params.getSeedLines() == 'f') {
this.getWindLinesFromStartLine(wf, windFieldDTO, startTime, endTime, timeStep);
this.getWindLinesFromStartLine(wf, windFieldDTO, startTime, endTime, timeStep);
}
if (params.isShowLines() && params.getSeedLines() == 'b') {
this.getWindLinesFromEndLine(wf, windFieldDTO, startTime, endTime, timeStep);
this.getWindLinesFromEndLine(wf, windFieldDTO, startTime, endTime, timeStep);
}
windFieldDTO.curBearing = wf.getWindParameters().curBearing;
windFieldDTO.curSpeed = wf.getWindParameters().curSpeed;
//} else {
if (params.isShowStreamlets2()) {
// generate JSON-vectorfield
String jsonField = "$wnd.windFieldSim={";
//this.rcStart = field.rcStart;
jsonField += "rcStart:{lat:" + params.getNorthWest().latDeg + ",lng:" + params.getNorthWest().lngDeg + "},";
//this.rcEnd = field.rcEnd;
jsonField += "rcEnd:{lat:" + params.getSouthEast().latDeg + ",lng:" + params.getSouthEast().lngDeg + "},";
// SouthWest
jsonField += "boundsSW:{lat:" + positionGrid[0][0].getLatDeg() + ",lng:" + positionGrid[0][0].getLngDeg() + "},";
// NorthEast
jsonField += "boundsNE:{lat:" + positionGrid[params.getyRes()+2*params.getBorderY()-1][params.getxRes()+2*params.getBorderX()-1].getLatDeg() + ",lng:" + positionGrid[params.getyRes()+2*params.getBorderY()-1][params.getxRes()+2*params.getBorderX()-1].getLngDeg() + "},";
//this.resY = field.resY;
jsonField += "resY:" + params.getyRes() + ",";
//this.resX = field.resX;
jsonField += "resX:" + params.getxRes() + ",";
// border
jsonField += "borderY:" + params.getBorderY() + ",";
jsonField += "borderX:" + params.getBorderX() + ",";
//this.xScale = field.xScale;
jsonField += "xScale:1.5,";
/*String jsonField2 = "data:[";
DecimalFormat df = new DecimalFormat("#.00");
double maxWindSpeed = 0;
//List<SimulatorWindDTO> windMat = wfDTO.getMatrix();
if (positionGrid != null && positionGrid.length > 0) {
TimePoint t = startTime;
while (t.compareTo(endTime) <= 0) {
jsonField2 += "[";
for (int i = 0; i < positionGrid.length; ++i) {
jsonField2 += "[";
for (int j = 0; j < positionGrid[i].length; ++j) {
Wind localWind = wf.getWind(new TimedPositionWithSpeedImpl(t, positionGrid[i][j], null));
if (localWind.getKnots() > maxWindSpeed) {
maxWindSpeed = localWind.getKnots();
}
double windY = localWind.getKnots() * Math.cos(localWind.getBearing().getRadians());
double windX = localWind.getKnots() * Math.sin(localWind.getBearing().getRadians());
jsonField2 += df.format(windX) + "," + df.format(windY);
if (j<(params.getxRes()-1)) {
jsonField2 += ",";
}
}
if (i<(params.getyRes()-1)) {
jsonField2 += "],";
} else {
jsonField2 += "]";
}
}
t = new MillisecondsTimePoint(t.asMillis() + timeStep.asMillis());
if (t.compareTo(endTime) <= 0) {
jsonField2 += "],";
} else {
jsonField2 += "]";
}
}
}
jsonField += "maxLength:" + maxWindSpeed + "," + jsonField2;
jsonField += "]";
// finalize JSON Field
jsonField += "};";*/
//LOGGER.info("JSON Field:\n" + jsonField);
windFieldDTO.windDataJSON = jsonField;
// windFieldDTO.setMatrix(null);
WindData windData = new WindData();
windData.rcStart = params.getNorthWest();
windData.rcEnd = params.getSouthEast();
windData.resX = params.getxRes();
windData.resY = params.getyRes();
windData.borderX = params.getBorderX();
windData.borderY = params.getBorderY();
windData.xScale = 1.5;
windFieldDTO.windData = windData;
}
return windFieldDTO;
}
private void getWindLinesFromStartLine(WindFieldGenerator wf, WindFieldDTO windFieldDTO, TimePoint startTime, TimePoint endTime, TimePoint timeStep) {
private void getWindLinesFromStartLine(WindFieldGenerator wf, WindFieldDTO windFieldDTO, TimePoint startTime,
TimePoint endTime, Duration timeStep) {
Position[][] positionGrid = wf.getPositionGrid();
WindLinesDTO windLinesDTO = windFieldDTO.getWindLinesDTO();
if (windLinesDTO == null) {
@@ -875,8 +801,8 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula
//logger.info("Added : " + windFieldDTO.getWindLinesDTO().getWindLinesMap().size() + " wind lines");
}
private void getWindLinesFromEndLine(WindFieldGenerator wf, WindFieldDTO windFieldDTO, TimePoint startTime, TimePoint endTime, TimePoint timeStep) {
private void getWindLinesFromEndLine(WindFieldGenerator wf, WindFieldDTO windFieldDTO, TimePoint startTime,
TimePoint endTime, Duration timeStep) {
Position[][] positionGrid = wf.getPositionGrid();
WindLinesDTO windLinesDTO = windFieldDTO.getWindLinesDTO();
if (windLinesDTO == null) {
@@ -49,7 +49,8 @@ public class RaceInfoDTO implements IsSerializable {
public Date lastUpdateTime;
public Flags lastUpperFlag;
public Flags lastLowerFlag;
public boolean isLastFlagDisplayed;
public boolean lastFlagsAreDisplayed;
public boolean lastFlagsDisplayedStateChanged;
public boolean isRaceAbortedInPassBefore;
public boolean isTracked;
public RaceCourseDTO lastCourseDesign;
@@ -3,13 +3,24 @@ package com.sap.sailing.gwt.ui.shared;
import java.util.List;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.sap.sailing.domain.common.dto.PositionDTO;
public class WindFieldDTO implements IsSerializable {
public double curSpeed;
public double curBearing;
public String windDataJSON;
public static class WindData implements IsSerializable {
public PositionDTO rcStart;
public PositionDTO rcEnd;
public int resX;
public int resY;
public int borderX;
public int borderY;
public double xScale;
}
public WindData windData;
private List<SimulatorWindDTO> matrix;
@@ -3,6 +3,7 @@ package com.sap.sailing.gwt.ui.shared;
import java.util.Date;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.sap.sailing.domain.common.Duration;
import com.sap.sailing.domain.common.dto.PositionDTO;
public class WindFieldGenParamsDTO implements IsSerializable {
@@ -17,7 +18,7 @@ public class WindFieldGenParamsDTO implements IsSerializable {
private Date startTime;
private Date endTime;
private Date timeStep;
private Duration timeStep;
private boolean keepState;
@@ -111,15 +112,15 @@ public class WindFieldGenParamsDTO implements IsSerializable {
this.endTime = endTime;
}
public Date getTimeStep() {
public Duration getTimeStep() {
return timeStep;
}
public void setTimeStep(Date timeStep) {
public void setTimeStep(Duration timeStep) {
this.timeStep = timeStep;
}
public void setDefaultTimeSettings(Date startTime, Date timeStep, Date endTime) {
public void setDefaultTimeSettings(Date startTime, Duration timeStep, Date endTime) {
this.startTime = startTime;
this.timeStep = timeStep;
this.endTime = endTime;
@@ -173,7 +173,7 @@ public class PathCanvasOverlay extends WindFieldCanvasOverlay implements Named {
}
index++;
long timeStep = windParams.getTimeStep().getTime();
long timeStep = windParams.getTimeStep().asMillis();
if ((windDTO.timepoint - startTime) % (timeStep) == 0) {
drawPoint(windDTO);
}
@@ -27,7 +27,9 @@ public class SimulatorEntryPoint extends AbstractEntryPoint {
private int xRes = 40;
private int yRes = 20;
private int border = 0;
private int particles = 0;
private StreamletParameters streamletPars = new StreamletParameters();
private boolean autoUpdate = false;
private char mode = SailingSimulatorConstants.ModeEvent; // default mode: 'e'vent
private char event = SailingSimulatorConstants.EventKielerWoche; // default event: 'k'ieler woche
@@ -41,6 +43,7 @@ public class SimulatorEntryPoint extends AbstractEntryPoint {
private boolean showStreamlets2 = false; // show animated wind streamlets in the wind display and replay modes.
private boolean injectWindDataJS = false;
private static Logger logger = Logger.getLogger(SimulatorEntryPoint.class.getName());
@Override
@@ -79,12 +82,49 @@ public class SimulatorEntryPoint extends AbstractEntryPoint {
} else {
this.border = Integer.parseInt(border);
}
streamletPars.particles = 0;
streamletPars.motionScale = 1.0;
streamletPars.swarmScale = 1.0;
streamletPars.detailZoom = 15;
String particlesStr = Window.Location.getParameter("particles");
if (particlesStr == null || particlesStr.isEmpty()) {
logger.config("Using default particles " + this.particles);
logger.config("Using default particles " + this.streamletPars.particles);
} else {
this.particles = Integer.parseInt(particlesStr);
this.streamletPars.particles = Integer.parseInt(particlesStr);
}
String tmpStr = Window.Location.getParameter("motionScale");
if (tmpStr == null || tmpStr.isEmpty()) {
logger.config("Using default motionScale.");
} else {
this.streamletPars.motionScale = Double.parseDouble(tmpStr);
}
tmpStr = Window.Location.getParameter("swarmScale");
if (tmpStr == null || tmpStr.isEmpty()) {
logger.config("Using default swarmScale.");
} else {
this.streamletPars.swarmScale = Double.parseDouble(tmpStr);
}
tmpStr = Window.Location.getParameter("lineBase");
if (tmpStr == null || tmpStr.isEmpty()) {
logger.config("Using default lineBase.");
} else {
this.streamletPars.lineBase = Double.parseDouble(tmpStr);
}
tmpStr = Window.Location.getParameter("lineScale");
if (tmpStr == null || tmpStr.isEmpty()) {
logger.config("Using default lineScale.");
} else {
this.streamletPars.lineScale = Double.parseDouble(tmpStr);
}
tmpStr = Window.Location.getParameter("detailZoom");
if (tmpStr == null || tmpStr.isEmpty()) {
logger.config("Using default detailZoom.");
} else {
this.streamletPars.detailZoom = Integer.parseInt(tmpStr);
}
String autoUpdateStr = Window.Location.getParameter("autoUpdate");
if (autoUpdateStr == null || autoUpdateStr.isEmpty()) {
logger.config("Using default auto update " + autoUpdate);
@@ -179,7 +219,7 @@ public class SimulatorEntryPoint extends AbstractEntryPoint {
}
private void createSimulatorPanel() {
SimulatorMainPanel simulatorPanel = new SimulatorMainPanel(simulatorService, stringMessages, this, xRes, yRes, border, particles,
SimulatorMainPanel simulatorPanel = new SimulatorMainPanel(simulatorService, stringMessages, this, xRes, yRes, border, streamletPars,
autoUpdate, mode, event, showGrid, showLines, seedLines, showArrows, showStreamlets, showStreamlets2, injectWindDataJS, showMapControls);
DockLayoutPanel p = new DockLayoutPanel(Unit.PX);
@@ -42,7 +42,9 @@ import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.sap.sailing.domain.common.Duration;
import com.sap.sailing.domain.common.dto.BoatClassDTO;
import com.sap.sailing.domain.common.impl.MillisecondsDurationImpl;
import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.SimulatorServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages;
@@ -227,11 +229,11 @@ public class SimulatorMainPanel extends SimplePanel {
return NumberFormat.getFormat("0.0").format(value);
}
public SimulatorMainPanel(SimulatorServiceAsync svc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, int particles, boolean autoUpdate,
public SimulatorMainPanel(SimulatorServiceAsync svc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, StreamletParameters streamletPars, boolean autoUpdate,
char mode, char event, boolean showGrid, boolean showLines, char seedLines, boolean showArrows, boolean showStreamlets, boolean showStreamlets2, boolean injectWindDataJS, boolean showMapControls) {
super();
this.particles = particles;
this.particles = streamletPars.particles;
this.simulatorSvc = svc;
this.stringMessages = stringMessages;
@@ -306,13 +308,13 @@ public class SimulatorMainPanel extends SimplePanel {
TimeRangeWithZoomProvider timeRangeProvider = new TimeRangeWithZoomModel();
initTimer();
timer.setTime(windParams.getStartTime().getTime());
int secondsTimeStep = (int) windParams.getTimeStep().getTime() / 1000;
int secondsTimeStep = (int) windParams.getTimeStep().asSeconds();
timer.setPlaySpeedFactor(secondsTimeStep);
timePanel = new TimePanel<TimePanelSettings>(timer, timeRangeProvider, stringMessages, false);
busyIndicator = new SimpleBusyIndicator(false, 0.8f);
simulatorMap = new SimulatorMap(simulatorSvc, stringMessages, errorReporter, xRes, yRes, border, timer, timePanel,
simulatorMap = new SimulatorMap(simulatorSvc, stringMessages, errorReporter, xRes, yRes, border, streamletPars, timer, timePanel,
windParams, busyIndicator, mode, this, showMapControls);
simulatorMap.setSize("100%", "100%");
@@ -363,7 +365,7 @@ public class SimulatorMainPanel extends SimplePanel {
long epochTime = (defaultNow.getTime()/86400000)*86400000 - utcOffSet*3600000; // today, midnight in default time zone
Date startTime = new Date(epochTime);
Date timeStep = new Date(15 * 1000);
Duration timeStep = new MillisecondsDurationImpl(15 * 1000);
Date endTime = new Date(startTime.getTime() + 10 * 60 * 1000);
windParams.setDefaultTimeSettings(startTime, timeStep, endTime);
}
@@ -101,6 +101,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
private int xRes;
private int yRes;
private int border;
private StreamletParameters streamletPars;
private boolean warningAlreadyShown = false;
private SimulatorMainPanel parent = null;
private PathPolyline pathPolyline = null;
@@ -296,7 +297,36 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
}
public SimulatorMap(SimulatorServiceAsync simulatorSvc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, Timer timer,
/*public SimulatorMap(SimulatorServiceAsync simulatorSvc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, StreamletParameters streamletPars, Timer timer,
WindFieldGenParamsDTO windParams, SimpleBusyIndicator busyIndicator, char mode,
SimulatorMainPanel parent) {
this.simulatorService = simulatorSvc;
this.stringMessages = stringMessages;
this.errorReporter = errorReporter;
this.xRes = xRes;
this.yRes = yRes;
this.border = border;
this.timer = timer;
this.timePanel = null;
timer.addTimeListener(this);
this.windParams = windParams;
this.busyIndicator = busyIndicator;
this.mode = mode;
this.colorPalette = new ColorPaletteGenerator();
this.dataInitialized = false;
this.overlaysInitialized = false;
this.windFieldCanvasOverlay = null;
this.windLineGuidesCanvasOverlay = null;
this.windGridCanvasOverlay = null;
this.windLineCanvasOverlay = null;
this.replayPathCanvasOverlays = null;
this.raceCourseCanvasOverlay = null;
this.timeListeners = new LinkedList<TimeListenerWithStoppingCriteria>();
this.initializeData();
this.parent = parent;
}*/
public SimulatorMap(SimulatorServiceAsync simulatorSvc, StringMessages stringMessages, ErrorReporter errorReporter, int xRes, int yRes, int border, StreamletParameters streamletPars, Timer timer,
TimePanel<TimePanelSettings> timePanel, WindFieldGenParamsDTO windParams, SimpleBusyIndicator busyIndicator, char mode, SimulatorMainPanel parent, boolean showMapControls) {
this.simulatorService = simulatorSvc;
this.stringMessages = stringMessages;
@@ -304,6 +334,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
this.xRes = xRes;
this.yRes = yRes;
this.border = border;
this.streamletPars = streamletPars;
this.timer = timer;
this.timePanel = timePanel;
timer.addTimeListener(this);
@@ -549,7 +580,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
windLineGuidesCanvasOverlay = new WindLineGuidesCanvasOverlay(map, SimulatorMapOverlaysZIndexes.WINDSTREAMLETS_ZINDEX, timer, xRes);
}
if (windParams.isShowStreamlets2()) {
windStreamletsCanvasOverlay = new WindStreamletsCanvasOverlay(this, SimulatorMapOverlaysZIndexes.WINDFIELD_ZINDEX, timer, windParams);
windStreamletsCanvasOverlay = new WindStreamletsCanvasOverlay(this, SimulatorMapOverlaysZIndexes.WINDFIELD_ZINDEX, timer, streamletPars, windParams);
}
if (windParams.isShowGrid()) {
windGridCanvasOverlay = new WindGridCanvasOverlay(map, SimulatorMapOverlaysZIndexes.WINDGRID_ZINDEX, timer, xRes, yRes);
@@ -612,6 +643,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
windParams.setyRes(yRes);
windParams.setBorder(border);
busyIndicator.setBusy(true);
timer.pause();
timer.setTime(windParams.getStartTime().getTime());
simulatorService.getWindField(windParams, windPatternDisplay, new AsyncCallback<WindFieldDTO>() {
@@ -734,6 +766,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
windParams.setBorder(border);
busyIndicator.setBusy(true);
timer.pause();
timer.setTime(windParams.getStartTime().getTime());
simulatorService.getSimulatorResults(mode, raceCourseDirection, windParams, windPatternDisplay, true, selection, new ResultManager(summaryView));
@@ -1090,8 +1123,8 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ
map.panTo(regattaAreaCanvasOverlay.getCurrentCourseArea().getCenterPos());
mapPan = true;
if (map.getZoom() < 14) {
map.setZoom(14);
if (map.getZoom() < streamletPars.detailZoom) {
map.setZoom(streamletPars.detailZoom);
}
}
@@ -0,0 +1,15 @@
package com.sap.sailing.gwt.ui.simulator;
public class StreamletParameters {
public int particles = 0;
public double motionScale = 1.0;
public double swarmScale = 1.0;
public int detailZoom = 15;
public double lineScale = 1.0;
public double lineBase = 1.5;
public StreamletParameters() {
}
}
@@ -1,16 +1,7 @@
package com.sap.sailing.gwt.ui.simulator;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.maps.client.base.LatLng;
import com.sap.sailing.gwt.ui.shared.SimulatorWindDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldGenParamsDTO;
import com.sap.sailing.gwt.ui.simulator.racemap.FullCanvasOverlay;
@@ -18,42 +9,36 @@ import com.sap.sailing.gwt.ui.simulator.streamlets.Swarm;
import com.sap.sse.gwt.client.player.Timer;
/**
* A google map overlay based on a HTML5 canvas for drawing a wind field. The overlay covers the whole map and displays
* A Google Maps overlay based on an HTML5 canvas for drawing a wind field. The overlay covers the whole map and displays
* the wind objects inside it.
*
* @author Nidhi Sawhney (D054070)
*
*/
public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements TimeListenerWithStoppingCriteria {
/** The wind field that is to be displayed in the overlay */
protected WindFieldDTO windFieldDTO;
protected WindFieldGenParamsDTO windParams = null;
protected SimulatorMap simulatorMap;
private WindFieldDTO windFieldDTO;
private final WindFieldGenParamsDTO windParams;
private final SimulatorMap simulatorMap;
private StreamletParameters streamletPars;
private boolean visible = false;
private Timer timer;
private final Timer timer;
private Date endDate;
private int nParticles;
private Swarm swarm;
private static Logger logger = Logger.getLogger(WindStreamletsCanvasOverlay.class.getName());
public WindStreamletsCanvasOverlay(SimulatorMap simulatorMap, int zIndex, final Timer timer,
public WindStreamletsCanvasOverlay(SimulatorMap simulatorMap, int zIndex, final Timer timer, final StreamletParameters streamletPars,
final WindFieldGenParamsDTO windParams) {
super(simulatorMap.getMap(), zIndex);
this.simulatorMap = simulatorMap;
this.timer = timer;
this.windParams = windParams;
this.streamletPars = streamletPars;
this.nParticles = this.simulatorMap.getMainPanel().particles;
windFieldDTO = null;
getCanvas().getElement().setId("swarm-display");
}
public WindFieldDTO getWindFieldDTO() {
@@ -64,96 +49,10 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
return this.windParams;
}
public JavaScriptObject getJSONRandomWindData() {
JSONObject jsonWindData = new JSONObject();
jsonWindData.put("timestamp", new JSONString("11:55 am on March 18, 2014"));
LatLng boundsSW = LatLng.newInstance(53.854617, 8.159124);
LatLng boundsNE = LatLng.newInstance(55.263922, 11.413824);
jsonWindData.put("x0", new JSONNumber(boundsSW.getLongitude()));
jsonWindData.put("y0", new JSONNumber(boundsSW.getLatitude()));
jsonWindData.put("x1", new JSONNumber(boundsNE.getLongitude()));
jsonWindData.put("y1", new JSONNumber(boundsNE.getLatitude()));
int maxSteps = 1;
int gridWidth = 100;
int gridHeight = 100;
jsonWindData.put("gridWidth", new JSONNumber(gridWidth));
jsonWindData.put("gridHeight", new JSONNumber(gridHeight));
JSONArray windField = new JSONArray();
for (int stp = 0; stp < maxSteps; stp++) {
JSONArray vectorField = new JSONArray();
for (int idx = 0; idx < (gridWidth * gridHeight * 2); idx++) {
vectorField.set(idx, new JSONNumber((Math.random() - 0.5) * 10.0 + 3.0));
// windField.set(idx, new JSONNumber(10.0));
}
windField.set(stp, vectorField);
}
jsonWindData.put("field", windField);
return jsonWindData.getJavaScriptObject();
}
public native boolean isSwarmDataExt() /*-{
if ($wnd.swarmDataExt) {
return true;
} else {
return false;
}
}-*/;
public native String getSwarmData() /*-{
return JSON.stringify($wnd.swarmData);
}-*/;
public native void setSwarmData(JavaScriptObject swarmData) /*-{
$wnd.swarmData = swarmData;
}-*/;
private native void getJSNIWind(WindStreamletsCanvasOverlay wsc) /*-{
$wnd.getWindfromSimulator = function(idx) {
return wsc.@com.sap.sailing.gwt.ui.simulator.WindStreamletsCanvasOverlay::getWind(I)(idx);
};
}-*/;
private native JavaScriptObject getWindInst(double x, double y) /*-{
return {
x : x,
y : y
};
}-*/;
public JavaScriptObject getWind(int idx) {
SimulatorWindDTO wind = this.windFieldDTO.getMatrix().get(idx);
double y = wind.trueWindSpeedInKnots * Math.cos(wind.trueWindBearingDeg * Math.PI / 180.0);
double x = wind.trueWindSpeedInKnots * Math.sin(wind.trueWindBearingDeg * Math.PI / 180.0);
return getWindInst(x, y);
}
public native void setWindDataJSON(String jsonField) /*-{
eval(jsonField);
}-*/;
public WindStreamletsCanvasOverlay(SimulatorMap simulatorMap, int zIndex) {
this(simulatorMap, zIndex, null, null);
}
public native void setMapInstance(Object mapInstance) /*-{
$wnd.swarmMap = mapInstance;
}-*/;
public native void setCanvasProjectionInstance(Object instance) /*-{
$wnd.swarmCanvasProjection = instance;
}-*/;
public void startStreamlets() {
if (swarm == null) {
this.swarm = new Swarm(this, map);
this.swarm = new Swarm(this, map, this.streamletPars);
}
this.swarm.start(40, windFieldDTO);
}
@@ -168,46 +67,6 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
}
}
public void extendWindDataJSON() {
List<SimulatorWindDTO> data = this.windFieldDTO.getMatrix();
String jsonData = "data:[";
int p = 0;
int imax = this.windParams.getyRes() + 2 * this.windParams.getBorderY();
int jmax = this.windParams.getxRes() + 2 * this.windParams.getBorderX();
int steps = data.size() / (imax * jmax);
double maxWindSpeed = 0;
for (int s = 0; s < steps; s++) {
jsonData += "[";
for (int i = 0; i < imax; i++) {
jsonData += "[";
for (int j = 0; j < jmax; j++) {
SimulatorWindDTO wind = data.get(p);
p++;
if (wind.trueWindSpeedInKnots > maxWindSpeed) {
maxWindSpeed = wind.trueWindSpeedInKnots;
}
double y = wind.trueWindSpeedInKnots * Math.cos(wind.trueWindBearingDeg * Math.PI / 180.0);
double x = wind.trueWindSpeedInKnots * Math.sin(wind.trueWindBearingDeg * Math.PI / 180.0);
jsonData += x + "," + y;
if (j < (jmax - 1)) {
jsonData += ",";
}
}
if (i < (imax - 1)) {
jsonData += "],";
} else {
jsonData += "]";
}
}
if (s < (steps - 1)) {
jsonData += "],";
} else {
jsonData += "]";
}
}
this.windFieldDTO.windDataJSON += jsonData + "],maxLength:" + maxWindSpeed + "};";
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
@@ -261,8 +120,8 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
super.draw();
if (mapProjection != null) {
if ((nParticles > 0) && (swarm == null)) {
this.swarm = new Swarm(this, map);
this.swarm.start(40, null);
this.swarm = new Swarm(this, map, this.streamletPars);
this.swarm.start(/* animationIntervalMillis */ 40, /* windField */ null);
}
if (windFieldDTO != null) {
// drawing is done by external JavaScript for Streamlets
@@ -274,14 +133,9 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
return this.swarm;
}
private void clear() {
this.stopStreamlets();
}
@Override
public void timeChanged(final Date newDate, Date oldDate) {
int step = (int) ((newDate.getTime() - this.windParams.getStartTime().getTime()) / this.windParams
.getTimeStep().getTime());
int step = (int) ((newDate.getTime() - this.windParams.getStartTime().getTime()) / this.windParams.getTimeStep().asMillis());
this.setStreamletsStep(step);
}
@@ -293,12 +147,4 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
return false;
}
}
public void setTimer(final Timer timer) {
this.timer = timer;
}
public Timer getTimer() {
return timer;
}
}
@@ -37,23 +37,12 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi
public FullCanvasOverlay(MapWidget map, int zIndex) {
super(map, zIndex);
/*getMap().addDragEndHandler( new DragEndMapHandler() {
@Override
public void onEvent(DragEndMapEvent event) {
// TODO Auto-generated method stub
draw();
};
});*/
getMap().addCenterChangeHandler(new CenterChangeMapHandler() {
@Override
public void onEvent(CenterChangeMapEvent event) {
// TODO Auto-generated method stub
drawCenterChanged();
};
@Override
public void onEvent(CenterChangeMapEvent event) {
drawCenterChanged();
};
});
}
/**
@@ -157,11 +146,11 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi
drawCircle(x, y,radius,color);
context2d.setGlobalAlpha(1.0f);
if (getMap().getZoom() >= 11) {
context2d.setFillStyle(textColor);
double fontsize = 9.0 + (12.0-9.0)*(getMap().getZoom() - 10.0)/(12.0-10.0);
context2d.setFont("normal "+fontsize+"px Calibri");
context2d.fillText(text, x + 0.7*fontsize, y + 0.3*fontsize);
//System.out.println("ZoomLevel: "+getMap().getZoomLevel()+", Fontsize: "+fontsize);
context2d.setFillStyle(textColor);
double fontsize = 9.0 + (12.0 - 9.0) * (getMap().getZoom() - 10.0) / (12.0 - 10.0);
context2d.setFont("normal " + fontsize + "px Calibri");
context2d.fillText(text, x + 0.7 * fontsize, y + 0.3 * fontsize);
// System.out.println("ZoomLevel: "+getMap().getZoomLevel()+", Fontsize: "+fontsize);
}
}
@@ -220,40 +209,27 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi
String msg = "Wind @ P" + index + ": time : " + windDTO.timepoint + " speed: " + windDTO.trueWindSpeedInKnots
+ "knots " + windDTO.trueWindBearingDeg;
logger.fine(msg);
PositionDTO position = windDTO.position;
LatLng positionLatLng = LatLng.newInstance(position.latDeg, position.lngDeg);
Point canvasPositionInPx = mapProjection.fromLatLngToDivPixel(positionLatLng);
double x = canvasPositionInPx.getX() - getWidgetPosLeft();
double y = canvasPositionInPx.getY() - getWidgetPosTop();
//windFieldPoints.put(new ToolTip(x, y), windDTO);
drawArrowPx(x, y, angle, length, weight, drawHead, color);
}
protected void drawArrowPx(double x, double y, double angle, double length, double weight, boolean drawHead, String color) {
final double dx = length * Math.sin(angle);
final double dy = -length * Math.cos(angle);
final double x1 = x + dx / 2;
final double y1 = y + dy / 2;
drawLine(x - dx / 2, y - dy / 2, x1, y1, weight, color);
final double theta = Math.atan2(-dy, dx);
final double hLength = Math.max(6.,6.+(10./(60.-10.))*Math.max(length-6.,0));
logger.finer("headlength: "+hLength+", arrowlength: "+length);
if (drawHead) {
drawHead(x1, y1, theta, hLength, weight, color);
}
//String text = "P" + index;// + NumberFormat.getFormat("0.00").format(windDTO.trueWindBearingDeg) + "�";
//drawPointWithText(x, y, text);
//drawPoint(x, y);
}
protected void drawHead(final double x, final double y, final double theta, final double headLength, final double weight, String color) {
@@ -265,7 +241,6 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi
if (t2 <= (-Math.PI)) {
t2 += 2 * Math.PI;
}
final double x1 = (x - Math.cos(t) * headLength);
final double y1 = (y + Math.sin(t) * headLength);
final double x1o = (x + Math.cos(t) * weight/2);
@@ -274,7 +249,6 @@ public abstract class FullCanvasOverlay extends CanvasOverlayV3 implements Requi
final double y2 = (y + Math.sin(t2) * headLength);
final double x2o = (x + Math.cos(t2) * weight/2);
final double y2o = (y - Math.sin(t2) * weight/2);
drawLine(x1o, y1o, x1, y1, weight, color);
drawLine(x2o, y2o, x2, y2, weight, color);
}
@@ -1,12 +1,11 @@
package com.sap.sailing.gwt.ui.simulator.streamlets;
public class Index {
public final long x;
public final long y;
public long x;
public long y;
public Index(long x, long y) {
this.x = x;
this.y = y;
}
public Index(long x, long y) {
this.x = x;
this.y = y;
}
}
@@ -1,20 +1,19 @@
package com.sap.sailing.gwt.ui.simulator.streamlets;
public class Neighbors {
public final int xBot;
public final int xTop;
public final double xMod;
public final int yBot;
public final int yTop;
public final double yMod;
public int xBot;
public int xTop;
public double xMod;
public int yBot;
public int yTop;
public double yMod;
public Neighbors(int xTop, int yTop, int xBot, int yBot, double xMod, double yMod) {
this.xTop = xTop;
this.yTop = yTop;
this.xBot = xBot;
this.yBot = yBot;
this.xMod = xMod;
this.yMod = yMod;
}
public Neighbors(int xTop, int yTop, int xBot, int yBot, double xMod, double yMod) {
this.xTop = xTop;
this.yTop = yTop;
this.xBot = xBot;
this.yBot = yBot;
this.xMod = xMod;
this.yMod = yMod;
}
}
@@ -3,9 +3,9 @@ package com.sap.sailing.gwt.ui.simulator.streamlets;
import com.sap.sailing.domain.common.Position;
public class Particle {
public Position pos;
public Vector pxOld;
public double speed;
public int age;
public Position currentPosition;
public Vector previousPixelCoordinate;
public Vector currentPixelCoordinate;
public int stepsToLive;
public Vector v;
}
@@ -3,8 +3,11 @@ package com.sap.sailing.gwt.ui.simulator.streamlets;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.maps.client.base.LatLng;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.impl.BoundsImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.gwt.ui.simulator.StreamletParameters;
public class RectField implements VectorField {
@@ -15,8 +18,7 @@ public class RectField implements VectorField {
private final double y0;
private final double y1;
private Position visSW;
private Position visNE;
private double motionFactor;
private final int w;
private final int h;
@@ -25,16 +27,13 @@ public class RectField implements VectorField {
private final double particleFactor;
private final String[] colorsForSpeeds;
public RectField(Vector[][] field, double x0, double y0, double x1, double y1) {
public RectField(Vector[][] field, double x0, double y0, double x1, double y1, StreamletParameters parameters) {
colorsForSpeeds = createColorsForSpeeds();
this.x0 = x0;
this.x1 = x1;
this.y0 = y0;
this.y1 = y1;
this.visSW = new DegreePosition(0.0, 0.0);
this.visNE = new DegreePosition(0.0, 0.0);
this.motionFactor = 0.9 * parameters.motionScale;
this.field = field;
this.w = field.length;
this.h = field[0].length;
@@ -57,7 +56,7 @@ public class RectField implements VectorField {
my = (my / this.h) * (y1 - y0) + y0;
}
public static RectField read(String jsonData, boolean correctForSphere) {
public static RectField read(String jsonData, boolean correctForSphere, StreamletParameters parameters) {
JSONObject data = JSONParser.parseLenient(jsonData).isObject();
int w = (int) data.get("gridWidth").isNumber().doubleValue();
int h = (int) data.get("gridHeight").isNumber().doubleValue();
@@ -82,7 +81,7 @@ public class RectField implements VectorField {
}
}
RectField result = new RectField(field, data.get("x0").isNumber().doubleValue(), data.get("y0").isNumber()
.doubleValue(), data.get("x1").isNumber().doubleValue(), data.get("y1").isNumber().doubleValue());
.doubleValue(), data.get("x1").isNumber().doubleValue(), data.get("y1").isNumber().doubleValue(), parameters);
return result;
}
@@ -98,15 +97,6 @@ public class RectField implements VectorField {
public void prevStep() {
}
@Override
public Position getRandomPosition() {
double rndY = Math.random();
double rndX = Math.random();
double latDeg = rndY * this.visSW.getLatDeg() + (1 - rndY) * this.visNE.getLatDeg();
double lngDeg = rndX * this.visSW.getLngDeg() + (1 - rndX) * this.visNE.getLngDeg();
return new DegreePosition(latDeg, lngDeg);
}
@Override
public boolean inBounds(Position p) {
return p.getLngDeg() >= this.x0 && p.getLngDeg() < this.x1 && p.getLatDeg() >= this.y0
@@ -151,12 +141,12 @@ public class RectField implements VectorField {
@Override
public double motionScale(int zoomLevel) {
return 0.9 * Math.pow(1.7, Math.min(1.0, 6.0 - zoomLevel));
return this.motionFactor * Math.pow(1.7, Math.min(1.0, 6.0 - zoomLevel));
}
@Override
public double particleWeight(Position p, Vector v) {
return 1.0 - v.length() / this.maxLength;
return v == null ? 0 : (1.0 - v.length() / this.maxLength);
}
@Override
@@ -185,25 +175,11 @@ public class RectField implements VectorField {
}
@Override
public Position[] getFieldCorners() {
DegreePosition[] result = new DegreePosition[2];
result[0] = new DegreePosition(Math.min(this.y0, this.y1), Math.min(this.x0, this.x1));
result[1] = new DegreePosition(Math.max(this.y0, this.y1), Math.max(this.x0, this.x1));
return result;
}
@Override
public void setVisNE(Position visNE) {
this.visNE = visNE;
}
@Override
public void setVisSW(Position visSW) {
this.visSW = visSW;
}
@Override
public void setVisFullCanvas(boolean full) {
public Bounds getFieldCorners() {
// FIXME this is not date line-safe. If the field crosses +180°E (the international date line), this will fail
Position sw = new DegreePosition(Math.min(this.y0, this.y1), Math.min(this.x0, this.x1));
Position ne = new DegreePosition(Math.max(this.y0, this.y1), Math.max(this.x0, this.x1));
return new BoundsImpl(sw, ne);
}
@Override
@@ -3,13 +3,14 @@ package com.sap.sailing.gwt.ui.simulator.streamlets;
import java.util.List;
import com.google.gwt.core.client.GWT;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.impl.BoundsImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.gwt.ui.shared.SimulatorWindDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldDTO;
import com.sap.sailing.gwt.ui.shared.WindFieldGenParamsDTO;
import com.sap.sailing.gwt.ui.simulator.StreamletParameters;
public class SimulatorField implements VectorField {
private boolean swarmDebug = false;
@@ -17,60 +18,56 @@ public class SimulatorField implements VectorField {
private Position rcStart;
private Position rcEnd;
private int resY;
private int resX;
private final int resY;
private final int resX;
private int borderY;
private int borderX;
private final int borderY;
private final int borderX;
private double bdXi;
private double bdPhi;
private final Position bdA;
private final Position bdB;
private final Position bdC;
private Position bdA;
private Position bdB;
private Position bdC;
private final double xScale;
private double motionFactor;
private double lineBase;
private double lineScale;
private final double maxLength;
private final double particleFactor;
private double xScale;
/**
* The cosine of the field's average latitude (arithmetic average of NW's latitude and SE's latitude) which represents
* the average ratio of pixels used per longitude and latitude angle in the Mercator projection. At the equator, this
* ration is 1; towards the poles it gets less.
*/
private final double lngScale;
private Position visSW;
private Position visNE;
private boolean visFull = false;
private final Position nvY;
private final Position nvX;
private double maxLength;
private double particleFactor;
private double lngScale;
private Position nvY;
private Position nvX;
private Position gvX;
private double[][][] data;
private final double[][][] data;
private final String[] colorsForSpeeds;
private int step;
public SimulatorField(WindFieldDTO windData, WindFieldGenParamsDTO windParams) {
public SimulatorField(WindFieldDTO windData, WindFieldGenParamsDTO windParams, StreamletParameters parameters) {
this.colorsForSpeeds = createColorsForSpeeds();
this.step = 0;
String parseString = windData.windDataJSON.substring(18, windData.windDataJSON.length() - 1) + "}";
JSONObject baseData = JSONParser.parseLenient(parseString).isObject();
this.rcStart = new DegreePosition(baseData.get("rcStart").isObject().get("lat").isNumber().doubleValue(),
baseData.get("rcStart").isObject().get("lng").isNumber().doubleValue());
this.rcEnd = new DegreePosition(baseData.get("rcEnd").isObject().get("lat").isNumber().doubleValue(), baseData
.get("rcEnd").isObject().get("lng").isNumber().doubleValue());
this.resY = (int) baseData.get("resY").isNumber().doubleValue();
this.resX = (int) baseData.get("resX").isNumber().doubleValue();
this.borderY = (int) baseData.get("borderY").isNumber().doubleValue();
this.borderX = (int) baseData.get("borderX").isNumber().doubleValue();
this.bdXi = (this.borderY + 0.5) / (this.resY - 1);
this.bdPhi = 1.0 + 2 * this.bdXi;
this.rcStart = new DegreePosition(windData.windData.rcStart.latDeg, windData.windData.rcStart.lngDeg);
this.rcEnd = new DegreePosition(windData.windData.rcEnd.latDeg, windData.windData.rcEnd.lngDeg);
this.resX = windData.windData.resX;
this.resY = windData.windData.resY;
this.borderX = windData.windData.borderX;
this.borderY = windData.windData.borderY;
final double bdXi = (this.borderY + 0.5) / (this.resY - 1);
final double bdPhi = 1.0 + 2 * bdXi;
this.bdA = new DegreePosition(this.rcEnd.getLatDeg() + (this.rcEnd.getLatDeg() - this.rcStart.getLatDeg())
* this.bdXi, this.rcEnd.getLngDeg() + (this.rcEnd.getLngDeg() - this.rcStart.getLngDeg()) * this.bdXi);
this.bdB = new DegreePosition((this.rcStart.getLatDeg() - this.rcEnd.getLatDeg()) * this.bdPhi,
(this.rcStart.getLngDeg() - this.rcEnd.getLngDeg()) * this.bdPhi);
this.xScale = baseData.get("xScale").isNumber().doubleValue();
this.visSW = new DegreePosition(0.0, 0.0);
this.visNE = new DegreePosition(0.0, 0.0);
* bdXi, this.rcEnd.getLngDeg() + (this.rcEnd.getLngDeg() - this.rcStart.getLngDeg()) * bdXi);
this.bdB = new DegreePosition((this.rcStart.getLatDeg() - this.rcEnd.getLatDeg()) * bdPhi,
(this.rcStart.getLngDeg() - this.rcEnd.getLngDeg()) * bdPhi);
this.xScale = windData.windData.xScale;
this.motionFactor = 0.07 * parameters.motionScale;
this.lineBase = parameters.lineBase;
this.lineScale = parameters.lineScale;
List<SimulatorWindDTO> gridData = windData.getMatrix();
int p = 0;
int imax = windParams.getyRes() + 2 * windParams.getBorderY();
@@ -78,7 +75,8 @@ public class SimulatorField implements VectorField {
int steps = gridData.size() / (imax * jmax);
this.data = new double[steps][imax][2 * jmax];
double maxWindSpeed = 0;
double minWindSpeed = 100;
double minWindSpeed = Double.MAX_VALUE;
// copy the wind data from WindFieldDTO.getMatrix() to the data array, determining min/max wind speed
for (int s = 0; s < steps; s++) {
for (int i = 0; i < imax; i++) {
for (int j = 0; j < jmax; j++) {
@@ -99,37 +97,20 @@ public class SimulatorField implements VectorField {
}
this.maxLength = maxWindSpeed;
this.particleFactor = 2.0;
double latAvg = (this.rcEnd.getLatDeg() + this.rcStart.getLatDeg()) / 2.;
final double latAvg = (this.rcEnd.getLatDeg() + this.rcStart.getLatDeg()) / 2.;
this.lngScale = Math.cos(latAvg * Math.PI / 180.0);
double difLat = this.rcEnd.getLatDeg() - this.rcStart.getLatDeg();
double difLng = (this.rcEnd.getLngDeg() - this.rcStart.getLngDeg()) * this.lngScale;
double difLen = Math.sqrt(difLat * difLat + difLng * difLng);
final double difLat = this.rcEnd.getLatDeg() - this.rcStart.getLatDeg();
final double difLng = (this.rcEnd.getLngDeg() - this.rcStart.getLngDeg()) * this.lngScale;
final double difLen = Math.sqrt(difLat * difLat + difLng * difLng);
this.nvY = new DegreePosition(difLat / difLen / difLen * (this.resY - 1), difLng / difLen / difLen
* (this.resY - 1));
double nrmLat = -difLng / difLen;
double nrmLng = difLat / difLen;
this.nvX = new DegreePosition(nrmLat / this.xScale / difLen * (this.resX - 1), nrmLng / this.xScale / difLen
* (this.resX - 1));
this.gvX = new DegreePosition(nrmLat * this.xScale * difLen, nrmLng / this.lngScale * this.xScale * difLen);
this.bdC = new DegreePosition(this.gvX.getLatDeg() * (this.resX + 2 * this.borderX - 1) / (this.resX - 1),
this.gvX.getLngDeg() * (this.resX + 2 * this.borderX - 1) / (this.resX - 1));
}
@Override
public Position getRandomPosition() {
final Position result;
if (this.visFull) {
double rndY = Math.random();
double rndX = Math.random();
double latDeg = rndY * this.visSW.getLatDeg() + (1 - rndY) * this.visNE.getLatDeg();
double lngDeg = rndX * this.visSW.getLngDeg() + (1 - rndX) * this.visNE.getLngDeg();
result = new DegreePosition(latDeg, lngDeg);
} else {
double rndY = Math.random();
double rndX = Math.random() - 0.5;
result = this.getInnerPosition(rndX, rndY);
}
return result;
final Position gvX = new DegreePosition(nrmLat * this.xScale * difLen, nrmLng / this.lngScale * this.xScale * difLen);
this.bdC = new DegreePosition(gvX.getLatDeg() * (this.resX + 2 * this.borderX - 1) / (this.resX - 1),
gvX.getLngDeg() * (this.resX + 2 * this.borderX - 1) / (this.resX - 1));
}
private Position getInnerPosition(double factX, double factY) {
@@ -151,19 +132,20 @@ public class SimulatorField implements VectorField {
}
private Vector interpolate(Position p) {
Neighbors idx = this.getNeighbors(p);
Neighbors idx = getNeighbors(p);
if (swarmDebug
&& ((idx.xTop >= (this.resX + 2 * this.borderX)) || (idx.yTop >= (this.resY + 2 * this.borderY)))) {
GWT.log("interpolate: out of range: " + idx.xTop + " " + idx.yTop);
}
double avgX = this.data[this.step][idx.yBot][2 * idx.xBot] * (1 - idx.yMod) * (1 - idx.xMod)
+ this.data[this.step][idx.yTop][2 * idx.xBot] * idx.yMod * (1 - idx.xMod)
+ this.data[this.step][idx.yBot][2 * idx.xTop] * (1 - idx.yMod) * idx.xMod
+ this.data[this.step][idx.yTop][2 * idx.xTop] * idx.yMod * idx.xMod;
double avgY = this.data[this.step][idx.yBot][2 * idx.xBot + 1] * (1 - idx.yMod) * (1 - idx.xMod)
+ this.data[this.step][idx.yTop][2 * idx.xBot + 1] * idx.yMod * (1 - idx.xMod)
+ this.data[this.step][idx.yBot][2 * idx.xTop + 1] * (1 - idx.yMod) * idx.xMod
+ this.data[this.step][idx.yTop][2 * idx.xTop + 1] * idx.yMod * idx.xMod;
final double[][] dataAtStep = this.data[this.step];
double avgX = dataAtStep[idx.yBot][2 * idx.xBot] * (1 - idx.yMod) * (1 - idx.xMod)
+ dataAtStep[idx.yTop][2 * idx.xBot] * idx.yMod * (1 - idx.xMod)
+ dataAtStep[idx.yBot][2 * idx.xTop] * (1 - idx.yMod) * idx.xMod
+ dataAtStep[idx.yTop][2 * idx.xTop] * idx.yMod * idx.xMod;
double avgY = dataAtStep[idx.yBot][2 * idx.xBot + 1] * (1 - idx.yMod) * (1 - idx.xMod)
+ dataAtStep[idx.yTop][2 * idx.xBot + 1] * idx.yMod * (1 - idx.xMod)
+ dataAtStep[idx.yBot][2 * idx.xTop + 1] * (1 - idx.yMod) * idx.xMod
+ dataAtStep[idx.yTop][2 * idx.xTop + 1] * idx.yMod * idx.xMod;
return new Vector(avgX / this.lngScale, avgY);
}
@@ -199,30 +181,11 @@ public class SimulatorField implements VectorField {
private Index getIndex(Position p) {
// calculate grid indexes
Position posR = new DegreePosition(p.getLatDeg() - this.rcStart.getLatDeg(),
(p.getLngDeg() - this.rcStart.getLngDeg()) * this.lngScale);
final double latOffset = p.getLatDeg() - this.rcStart.getLatDeg();
final double lngOffset = (p.getLngDeg() - this.rcStart.getLngDeg()) * this.lngScale;
// closest grid point
long yIdx = Math.round(posR.getLatDeg() * this.nvY.getLatDeg() + posR.getLngDeg() * this.nvY.getLngDeg())
+ this.borderY;
long xIdx = Math.round(posR.getLatDeg() * this.nvX.getLatDeg() + posR.getLngDeg() * this.nvX.getLngDeg()
+ (this.resX - 1) / 2.)
+ this.borderX;
if (this.visFull) {
if (yIdx >= (this.resY + 2 * this.borderY)) {
yIdx = this.resY + 2 * this.borderY - 1;
}
if (yIdx < 0) {
yIdx = 0;
}
if (xIdx >= (this.resX + 2 * this.borderX)) {
xIdx = this.resX + 2 * this.borderX - 1;
}
if (xIdx < 0) {
xIdx = 0;
}
}
long yIdx = Math.round(latOffset * this.nvY.getLatDeg() + lngOffset * this.nvY.getLngDeg()) + this.borderY;
long xIdx = Math.round(latOffset * this.nvX.getLatDeg() + lngOffset * this.nvX.getLngDeg() + (this.resX - 1) / 2.) + this.borderX;
return new Index(xIdx, yIdx);
}
@@ -271,12 +234,12 @@ public class SimulatorField implements VectorField {
@Override
public double motionScale(int zoomLevel) {
return 0.07 * Math.pow(1.6, Math.min(1.0, 6.0 - zoomLevel));
return this.motionFactor * Math.pow(1.6, Math.min(1.0, 6.0 - zoomLevel));
}
@Override
public double particleWeight(Position p, Vector v) {
return v.length() / this.maxLength + 0.1;
return v == null ? 0 : (v.length() / this.maxLength + 0.1);
}
private String[] createColorsForSpeeds() {
@@ -317,49 +280,27 @@ public class SimulatorField implements VectorField {
* absolute linewidth speed == 12kn => linewidth 1.5 speed == 24kn => linewidth 3.0 speed == 6kn => linewidth
* 0.75
*/
return Math.round(speed / 8.0 * 100.0) / 100.0;
return Math.max(0.01, Math.round((lineBase + lineScale * (speed - 12.0) / 8.0) * 100.0) / 100.0);
}
@Override
public Position[] getFieldCorners() {
public Bounds getFieldCorners() {
// FIXME this is not date line-safe. If the field crosses +180°E (the international date line), this will fail
Position fieldNE = this.getInnerPosition(+0.5, 1.0);
Position fieldSW = this.getInnerPosition(-0.5, 0.0);
Position fieldSE = this.getInnerPosition(+0.5, 0.0);
Position fieldNW = this.getInnerPosition(-0.5, 1.0);
DegreePosition[] result = new DegreePosition[2];
double minLat = Math.min(Math.min(fieldNE.getLatDeg(), fieldSW.getLatDeg()),
Math.min(fieldNW.getLatDeg(), fieldSE.getLatDeg()));
double minLng = Math.min(Math.min(fieldNE.getLngDeg(), fieldSW.getLngDeg()),
Math.min(fieldNW.getLngDeg(), fieldSE.getLngDeg()));
result[0] = new DegreePosition(minLat, minLng);
Position sw = new DegreePosition(minLat, minLng);
double maxLat = Math.max(Math.max(fieldNE.getLatDeg(), fieldSW.getLatDeg()),
Math.max(fieldNW.getLatDeg(), fieldSE.getLatDeg()));
double maxLng = Math.max(Math.max(fieldNE.getLngDeg(), fieldSW.getLngDeg()),
Math.max(fieldNW.getLngDeg(), fieldSE.getLngDeg()));
result[1] = new DegreePosition(maxLat, maxLng);
return result;
}
@Override
public void setVisNE(Position visNE) {
this.visNE = visNE;
}
@Override
public void setVisSW(Position visSW) {
this.visSW = visSW;
}
@Override
public void setVisFullCanvas(boolean full) {
if (this.visFull != full) {
System.out.println("visFull: " + full);
this.visFull = full;
}
Position ne = new DegreePosition(maxLat, maxLng);
return new BoundsImpl(sw, ne);
}
@Override
@@ -7,9 +7,12 @@ import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.core.client.GWT;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.user.client.Timer;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.impl.BoundsImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.gwt.ui.shared.WindFieldDTO;
import com.sap.sailing.gwt.ui.simulator.StreamletParameters;
import com.sap.sailing.gwt.ui.simulator.WindStreamletsCanvasOverlay;
import com.sap.sailing.gwt.ui.simulator.racemap.FullCanvasOverlay;
@@ -17,37 +20,55 @@ public class Swarm {
private final FullCanvasOverlay fullcanvas;
private final Canvas canvas;
private final MapWidget map;
private StreamletParameters parameters;
private Timer loopTimer;
private Mercator projection;
private VectorField field;
/**
* The number of particles to show. After {@link #updateBounds()} has been run, this also reflects the size of the
* {@link #particles} array.
*/
private int nParticles;
/**
* The particles shown in this swarm.
*/
private Particle[] particles;
/**
* Tells if nothing of this swarm is currently visible on the {@link #map}. This is the case when there is no
* intersection between the {@link #field vector field's} {@link VectorField#getFieldCorners() bounds} and the
* visible map area.
*/
private boolean swarmOffScreen = false;
private int swarmPause = 0;
private boolean swarmContinue = true;
private Position boundsNE;
private Position boundsSW;
private Bounds visibleBoundsOfField;
public Swarm(FullCanvasOverlay fullcanvas, MapWidget map) {
public Swarm(FullCanvasOverlay fullcanvas, MapWidget map, StreamletParameters streamletPars) {
this.fullcanvas = fullcanvas;
this.canvas = fullcanvas.getCanvas();
this.map = map;
this.parameters = streamletPars;
}
public void start(int animationDuration, WindFieldDTO windField) {
public void start(int animationIntervalMillis, WindFieldDTO windField) {
projection = new Mercator(fullcanvas, map);
// TODO make the VectorField a parameter of Swarm
if (windField == null) {
SimulatorJSBundle bundle = GWT.create(SimulatorJSBundle.class);
String jsonStr = bundle.windStreamletsDataJS().getText();
RectField f = RectField.read(jsonStr.substring(19, jsonStr.length() - 1), false);
RectField f = RectField.read(jsonStr.substring(19, jsonStr.length() - 1), false, this.parameters);
field = f;
map.setZoom(5);
map.panTo(f.getCenter());
projection.calibrate();
} else {
field = new SimulatorField(((WindStreamletsCanvasOverlay) fullcanvas).getWindFieldDTO(),
((WindStreamletsCanvasOverlay) fullcanvas).getWindParams());
((WindStreamletsCanvasOverlay) fullcanvas).getWindParams(), this.parameters);
fullcanvas.setCanvasSettings();
projection.calibrate();
}
@@ -56,36 +77,47 @@ public class Swarm {
ctxt.setFillStyle("red");
particles = this.createParticles();
this.swarmContinue = true;
startLoop(animationDuration);
startLoop(animationIntervalMillis);
}
public void stop() {
this.swarmContinue = false;
}
public Particle createParticle() {
private Particle createParticle() {
Particle particle = new Particle();
boolean done = false;
// try to create a particle at a random position until the weight is high enough for it to be displayed
while (!done) {
particle.pos = field.getRandomPosition();
Vector v = field.getVector(particle.pos);
double weight = field.particleWeight(particle.pos, v);
particle.currentPosition = getRandomPosition();
Vector v = field.getVector(particle.currentPosition);
double weight = field.particleWeight(particle.currentPosition, v);
if (weight >= Math.random()) {
if (v.length() == 0) {
particle.age = 0;
if (v == null || v.length() == 0) {
particle.stepsToLive = 0;
} else {
particle.age = 1 + (int) Math.round(Math.random() * 40);
particle.stepsToLive = 1 + (int) Math.round(Math.random() * 40);
}
particle.pxOld = projection.latlng2pixel(particle.pos);
particle.speed = 0;
particle.currentPixelCoordinate = projection.latlng2pixel(particle.currentPosition);
particle.previousPixelCoordinate = particle.currentPixelCoordinate;
particle.v = v;
done = true;
}
}
return particle;
}
private Position getRandomPosition() {
final Position result;
double rndY = Math.random();
double rndX = Math.random();
double latDeg = rndY * this.visibleBoundsOfField.getSouthWest().getLatDeg() + (1 - rndY) * this.visibleBoundsOfField.getNorthEast().getLatDeg();
double lngDeg = rndX * this.visibleBoundsOfField.getSouthWest().getLngDeg() + (1 - rndX) * this.visibleBoundsOfField.getNorthEast().getLngDeg();
result = new DegreePosition(latDeg, lngDeg);
return result;
}
public Particle[] createParticles() {
private Particle[] createParticles() {
Particle[] newParticles = new Particle[nParticles];
for (int idx = 0; idx < newParticles.length; idx++) {
newParticles[idx] = this.createParticle();
@@ -98,64 +130,21 @@ public class Swarm {
swarmPause = 5;
}
public void updateBounds() {
Position mapNE = new DegreePosition(map.getBounds().getNorthEast().getLatitude(), map.getBounds()
.getNorthEast().getLongitude());
Position mapSW = new DegreePosition(map.getBounds().getSouthWest().getLatitude(), map.getBounds()
.getSouthWest().getLongitude());
Position[] fieldCorners = this.field.getFieldCorners();
Position fieldNE = fieldCorners[1];
Position fieldSW = fieldCorners[0];
Vector visibleNE = this.isVisible(fieldNE);
Vector visibleSW = this.isVisible(fieldSW);
boolean useBoundsNorth = (visibleNE.y == 0);
boolean useBoundsEast = (visibleNE.x == 0);
boolean useBoundsSouth = (visibleSW.y == 0);
boolean useBoundsWest = (visibleSW.x == 0);
swarmOffScreen = (visibleNE.y > 0) || (visibleSW.y < 0) || (visibleNE.x < 0) || (visibleSW.x > 0);
if (swarmOffScreen) {
this.boundsNE = fieldNE;
this.boundsSW = fieldSW;
} else {
if ((!useBoundsNorth) && (!useBoundsEast)) {
this.boundsNE = mapNE;
} else if (!useBoundsNorth) {
this.boundsNE = new DegreePosition(mapNE.getLatDeg(), fieldNE.getLngDeg());
} else if (!useBoundsEast) {
this.boundsNE = new DegreePosition(fieldNE.getLatDeg(), mapNE.getLngDeg());
} else {
this.boundsNE = fieldNE;
}
if ((!useBoundsSouth) && (!useBoundsWest)) {
this.boundsSW = mapSW;
} else if (!useBoundsSouth) {
this.boundsSW = new DegreePosition(mapSW.getLatDeg(), fieldSW.getLngDeg());
} else if (!useBoundsWest) {
this.boundsSW = new DegreePosition(fieldSW.getLatDeg(), mapSW.getLngDeg());
} else {
this.boundsSW = fieldSW;
}
}
this.field.setVisSW(this.boundsSW);
this.field.setVisNE(this.boundsNE);
this.field.setVisFullCanvas((!useBoundsNorth) && (!useBoundsEast) && (!useBoundsSouth) && (!useBoundsWest));
Vector boundsSWpx = this.projection.latlng2pixel(this.boundsSW);
Vector boundsNEpx = this.projection.latlng2pixel(this.boundsNE);
private void updateBounds() {
Bounds fieldBounds = this.field.getFieldCorners();
final BoundsImpl mapBounds = new BoundsImpl(
new DegreePosition(map.getBounds().getSouthWest().getLatitude(), map.getBounds().getSouthWest().getLongitude()),
new DegreePosition(map.getBounds().getNorthEast().getLatitude(), map.getBounds().getNorthEast().getLongitude()));
swarmOffScreen = !fieldBounds.intersects(mapBounds);
visibleBoundsOfField = fieldBounds.intersect(mapBounds);
Vector boundsSWpx = this.projection.latlng2pixel(visibleBoundsOfField.getSouthWest());
Vector boundsNEpx = this.projection.latlng2pixel(visibleBoundsOfField.getNorthEast());
double boundsWidthpx = Math.abs(boundsNEpx.x - boundsSWpx.x);
double boundsHeightpx = Math.abs(boundsSWpx.y - boundsNEpx.y);
this.nParticles = (int) Math.round(Math.sqrt(boundsWidthpx * boundsHeightpx) * this.field.getParticleFactor());
this.nParticles = (int) Math.round(Math.sqrt(boundsWidthpx * boundsHeightpx) * this.field.getParticleFactor() * this.parameters.swarmScale);
};
public Vector isVisible(Position pos) {
// test for visibility of swarm
Vector proj = this.projection.latlng2pixel(pos);
Vector result = new Vector();
result.x = (proj.x < 0 ? -1 : 0) + (proj.x > canvas.getOffsetWidth() ? 1 : 0);
result.y = (proj.y < 0 ? -1 : 0) + (proj.y > canvas.getOffsetHeight() ? 1 : 0);
return result;
}
public void startLoop(final int millis) {
private void startLoop(final int animationIntervalMillis) {
// Create animation-loop based on timer timeout
loopTimer = new com.google.gwt.user.client.Timer() {
public void run() {
@@ -169,23 +158,21 @@ public class Swarm {
particles = createParticles();
swarmPause = 0;
}
// console("swarmOffScreen:"+swarmOffScreen);
if ((!swarmOffScreen) && (swarmPause == 0)) {
execute();
}
Date time1 = new Date();
// console("delta:"+(time1.getTime()-time0.getTime())+"/"+millis);
if (swarmContinue) {
loopTimer.schedule((int) Math.max(10, millis - (time1.getTime() - time0.getTime())));
loopTimer.schedule((int) Math.max(10, animationIntervalMillis - (time1.getTime() - time0.getTime())));
} else {
projection.clearCanvas();
}
}
};
loopTimer.schedule(millis);
loopTimer.schedule(animationIntervalMillis);
}
public void drawSwarm() {
private void drawSwarm() {
Context2d ctxt = canvas.getContext2d();
ctxt.setGlobalAlpha(0.08);
ctxt.setGlobalCompositeOperation("destination-out");
@@ -196,35 +183,44 @@ public class Swarm {
ctxt.setFillStyle("white");
for (int idx = 0; idx < particles.length; idx++) {
Particle particle = particles[idx];
if (particle.age == 0) {
if (particle.stepsToLive == 0) {
continue;
}
ctxt.setLineWidth(field.lineWidth(particle.speed));
ctxt.setStrokeStyle(field.getColor(particle.speed));
double particleSpeed = particle.v == null ? 0 : particle.v.length();
ctxt.setLineWidth(field.lineWidth(particleSpeed));
ctxt.setStrokeStyle(field.getColor(particleSpeed));
ctxt.beginPath();
ctxt.moveTo(particle.pxOld.x, particle.pxOld.y);
particle.pxOld = projection.latlng2pixel(particle.pos);
ctxt.lineTo(particle.pxOld.x, particle.pxOld.y);
ctxt.moveTo(particle.previousPixelCoordinate.x, particle.previousPixelCoordinate.y);
ctxt.lineTo(particle.currentPixelCoordinate.x, particle.currentPixelCoordinate.y);
ctxt.stroke();
}
}
public boolean execute() {
/**
* Moves each particle by its vector {@link Particle#v} multiplied by the speed which is 0.01 times the
* {@link VectorField#motionScale(int)} at the map's current zoom level.
*/
private boolean execute() {
double speed = 0.01 * field.motionScale(map.getZoom());
for (int idx = 0; idx < particles.length; idx++) {
Particle particle = particles[idx];
if ((particle.age > 0) && (particle.v != null)) {
double latDeg = particle.pos.getLatDeg() + speed * particle.v.y;
double lngDeg = particle.pos.getLngDeg() + speed * particle.v.x;
particle.pos = new DegreePosition(latDeg, lngDeg);
particle.speed = particle.v.length();
particle.age--;
if ((particle.age > 0) && (this.field.inBounds(particle.pos))) {
particle.v = field.getVector(particle.pos);
if ((particle.stepsToLive > 0) && (particle.v != null)) {
// move the particle one step in the direction and with the speed indicated by particle.v and
// update its currentPosition, currentPixelCoordinate and previousPixelCoordinate fields;
// also, its particle.v field is updated based on its new position from the vector field
particle.previousPixelCoordinate = particle.currentPixelCoordinate;
double latDeg = particle.currentPosition.getLatDeg() + speed * particle.v.y;
double lngDeg = particle.currentPosition.getLngDeg() + speed * particle.v.x;
particle.currentPosition = new DegreePosition(latDeg, lngDeg);
particle.currentPixelCoordinate = projection.latlng2pixel(particle.currentPosition);
particle.stepsToLive--;
if ((particle.stepsToLive > 0) && (this.field.inBounds(particle.currentPosition))) {
particle.v = field.getVector(particle.currentPosition);
} else {
particle.v = null;
}
} else {
// particle timed out (age became 0); create a new one
particles[idx] = this.createParticle();
}
}
@@ -25,4 +25,9 @@ public class Vector {
}
return this;
}
@Override
public String toString() {
return "{x: "+x+", y: "+y+"}";
}
}
@@ -1,29 +1,72 @@
package com.sap.sailing.gwt.ui.simulator.streamlets;
import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
/**
* A field of vectors to display in a streamlet {@link Swarm}. The {@link Vector}s returned by {@link #getVector(Position)}
* are used to initialize {@link Particle}s. Their "speed" is the {@link Vector#length() length} of the vector.<p>
*
* This field provides color strings for each possible speed which are then used to style the particles as they fly through
* the swarm.
*
* @author Christopher Ronnewinkel (D036654)
* @author Axel Uhl (D043530)
*
*/
public interface VectorField {
Position getRandomPosition();
/**
* Tells whether <code>p</code> is within the bounds of this field.
*/
boolean inBounds(Position p);
/**
* The vector field's value at position <code>p</code>.
*
* @return the speed/direction vector that tells how a particle will fly at this position in the vector field, or
* <code>null</code> if there should not be a flying particle, e.g., because the field does not know how a
* particle would fly at this position. An implementation that does not cleverly extrapolate outside the
* field's {@link #inBounds(Position) bounds} should return <code>null</code> for out-of-bounds
* positions.
*/
Vector getVector(Position p);
/**
* @return the maximum {@link Vector#length() length} of any vector returned by {@link #getVector(Position)} in this field for
* positions that are {@link #inBounds(Position) within bounds}.
*/
double getMaxLength();
double motionScale(int zoomLevel);
/**
* A weight between 0.0 and 1.0 (inclusive) that tells the probability at which a particle at position
* <code>p</code> and with speed vector <code>v</code> will be shown. 1.0 means it will certainly be shown; 0.0
* means it will certainly not be shown.
*
* @param p
* the particle's position
* @param v
* the particle's speed/direction vector which may be <code>null</code>; in this case, an implementation
* should return 0.0 as the particle's weight.
*/
double particleWeight(Position p, Vector v);
/**
* Computes a line width for a particle flying at a certain speed.
*
* @param speed
* a speed as obtained by computing a {@link Vector}'s {@link Vector#length() length}.
*/
double lineWidth(double speed);
Position[] getFieldCorners();
void setVisNE(Position visNE);
void setVisSW(Position visSW);
void setVisFullCanvas(boolean full);
/**
* @return the north-east and the south-west corner of the rectangular area on a Mercator projection that fully
* contains this field's {@link #inBounds(Position) bounds}. Note that the field's
* {@link #inBounds(Position) bounds} are not necessarily defined as a rectangle on a Mercator map. There
* may be a rotation or some other shape in place.
*/
Bounds getFieldCorners();
double getParticleFactor();
@@ -33,5 +76,13 @@ public interface VectorField {
void prevStep();
/**
* Computes a color for a particle flying at a certain speed.
*
* @param speed
* a speed as obtained by computing a {@link Vector}'s {@link Vector#length() length}.
* @return an RGB color with optional transparency that is a legal color in CSS. Example:
* <code>"rgba(1, 2, 3, 0.4)"</code>
*/
String getColor(double speed);
}
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more