Merge branch 'master' into service-monitoring
@@ -0,0 +1,2 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding//src/com/sap/sailing/domain/common/AbstractBearing.java=UTF-8
|
||||
@@ -47,7 +47,7 @@ public abstract class AbstractBearing implements Bearing {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ""+getDegrees()+"°";
|
||||
return ""+getDegrees()+"°";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.sap.sailing.domain.common;
|
||||
|
||||
public enum MarkType { BUOY, CAMERABOAT, STARTBOAT, UMPIREBOAT, LANDMARK }
|
||||
@@ -42,6 +42,10 @@ public interface Speed extends Comparable<Speed>, Serializable {
|
||||
return Distance.NULL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "0kn";
|
||||
}
|
||||
};
|
||||
|
||||
double getKnots();
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
|
||||
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
|
||||
public abstract class AbstractManeuverDetectionTestCase extends OnlineTracTracBasedTest {
|
||||
@@ -39,7 +40,7 @@ public abstract class AbstractManeuverDetectionTestCase extends OnlineTracTracBa
|
||||
* The tolerance of time, the maneuver should have happened in milliseconds.
|
||||
*/
|
||||
protected void assertManeuver(List<Maneuver> maneuverList, ManeuverType maneuverType,
|
||||
MillisecondsTimePoint maneuverTimePoint, int tolerance) {
|
||||
TimePoint maneuverTimePoint, int tolerance) {
|
||||
for (Maneuver maneuver : maneuverList) {
|
||||
assertNotNull(maneuver.getTimePoint());
|
||||
if (maneuver.getType() == maneuverType
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -64,10 +65,10 @@ public class ManeuverAnalysisIDMChampionsFinalTest extends AbstractManeuverDetec
|
||||
TimePoint epoch = new MillisecondsTimePoint(0l);
|
||||
TimePoint now = MillisecondsTimePoint.now();
|
||||
Map<String, Position> markPositions = new HashMap<String, Position>();
|
||||
markPositions.put("G2 Start-Finish (left)", new DegreePosition(53.96003300000019, 10.878697000000084));
|
||||
markPositions.put("G2 Start-Finish (right)", new DegreePosition(53.9674420000693, 10.894410000058738));
|
||||
markPositions.put("G2 Mark4 (right)", new DegreePosition(53.96002200000019, 10.878875000000063));
|
||||
markPositions.put("G2 Mark4 (left)", new DegreePosition(53.9599880000002, 10.878665000000069));
|
||||
markPositions.put("G2 Start-Finish (1)", new DegreePosition(53.96003300000019, 10.878697000000084));
|
||||
markPositions.put("G2 Start-Finish (2)", new DegreePosition(53.9674420000693, 10.894410000058738));
|
||||
markPositions.put("G2 Mark4 (2)", new DegreePosition(53.96002200000019, 10.878875000000063));
|
||||
markPositions.put("G2 Mark4 (1)", new DegreePosition(53.9599880000002, 10.878665000000069));
|
||||
markPositions.put("G2 Mark1", new DegreePosition(53.96355800000006, 10.885751999999806));
|
||||
for (Waypoint w : race.getRace().getCourse().getWaypoints()) {
|
||||
for (Mark mark : w.getMarks()) {
|
||||
@@ -91,12 +92,12 @@ public class ManeuverAnalysisIDMChampionsFinalTest extends AbstractManeuverDetec
|
||||
assertNotNull(competitor);
|
||||
Date toDate = new Date(1317650038784l); // that's shortly after their penalty circle
|
||||
Date fromDate = new Date(toDate.getTime()-450000l);
|
||||
Date maneuverTime = new Date(1317649967712l);
|
||||
final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
TimePoint maneuverTime = new MillisecondsTimePoint(dateFormatter.parse("2011-10-03T15:52:30.000+0200"));
|
||||
List<Maneuver> maneuvers = getTrackedRace().getManeuvers(competitor, new MillisecondsTimePoint(fromDate),
|
||||
new MillisecondsTimePoint(toDate), /* waitForLatest */ true);
|
||||
maneuversInvalid = new ArrayList<Maneuver>(maneuvers);
|
||||
assertManeuver(maneuvers, ManeuverType.PENALTY_CIRCLE,
|
||||
new MillisecondsTimePoint(maneuverTime), PENALTYCIRCLE_TOLERANCE);
|
||||
assertManeuver(maneuvers, ManeuverType.PENALTY_CIRCLE, maneuverTime, PENALTYCIRCLE_TOLERANCE);
|
||||
List<ManeuverType> maneuverTypesFound = new ArrayList<ManeuverType>();
|
||||
maneuverTypesFound.add(ManeuverType.PENALTY_CIRCLE);
|
||||
assertAllManeuversOfTypesDetected(maneuverTypesFound, maneuversInvalid);
|
||||
|
||||
@@ -83,9 +83,9 @@ public class ManeuverDetectionOnKielerWoche505Race2DataTest extends OnlineTracTr
|
||||
hassosMarkPassings.last().getTimePoint(), /* waitForLatest */ true);
|
||||
Calendar c = new GregorianCalendar(TimeZone.getTimeZone("Europe/Berlin"));
|
||||
c.clear();
|
||||
c.set(2011, 6-1, 23, 16, 5, 49);
|
||||
c.set(2011, 6-1, 23, 16, 5, 47);
|
||||
assertManeuver(maneuvers, ManeuverType.TACK, Tack.STARBOARD, new MillisecondsTimePoint(c.getTime()), /* tolerance in milliseconds */ 3000);
|
||||
c.set(2011, 6-1, 23, 16, 8, 37);
|
||||
c.set(2011, 6-1, 23, 16, 8, 31);
|
||||
assertManeuver(maneuvers, ManeuverType.TACK, Tack.PORT, new MillisecondsTimePoint(c.getTime()), /* tolerance in milliseconds */ 3000);
|
||||
c.set(2011, 6-1, 23, 16, 11, 03);
|
||||
assertManeuver(maneuvers, ManeuverType.TACK, Tack.STARBOARD, new MillisecondsTimePoint(c.getTime()), /* tolerance in milliseconds */ 3000);
|
||||
|
||||
@@ -54,8 +54,8 @@ public class ManeuverDetectionOnMdM2011SemifinalTest extends OnlineTracTracBased
|
||||
TimePoint epoch = new MillisecondsTimePoint(0l);
|
||||
TimePoint now = MillisecondsTimePoint.now();
|
||||
Map<String, Position> markPositions = new HashMap<String, Position>();
|
||||
markPositions.put("CR Start (left)", new DegreePosition(53.562944999999985, 10.010104000000046));
|
||||
markPositions.put("CR Start (right)", new DegreePosition(53.562944999999985, 10.010104000000046));
|
||||
markPositions.put("CR Start (1)", new DegreePosition(53.562944999999985, 10.010104000000046));
|
||||
markPositions.put("CR Start (2)", new DegreePosition(53.562944999999985, 10.010104000000046));
|
||||
markPositions.put("Leeward mark", new DegreePosition(53.562145000000015, 10.009252));
|
||||
markPositions.put("Luvtonne", new DegreePosition(53.560581899999995, 10.005657));
|
||||
for (Waypoint w : getTrackedRace().getRace().getCourse().getWaypoints()) {
|
||||
|
||||
@@ -157,13 +157,13 @@ public abstract class OnlineTracTracBasedTest extends AbstractTracTracLiveTest {
|
||||
TimePoint epoch = new MillisecondsTimePoint(0l);
|
||||
TimePoint now = MillisecondsTimePoint.now();
|
||||
Map<String, Position> markPositions = new HashMap<String, Position>();
|
||||
markPositions.put("K Start (left)", new DegreePosition(54.497439439999994, 10.205943000000001));
|
||||
markPositions.put("K Start (right)", new DegreePosition(54.500209999999996, 10.20206472));
|
||||
markPositions.put("K Mark4 (right)", new DegreePosition(54.499422999999986, 10.200381692));
|
||||
markPositions.put("K Mark4 (left)", new DegreePosition(54.498954999999995, 10.200982));
|
||||
markPositions.put("K Start (1)", new DegreePosition(54.497439439999994, 10.205943000000001));
|
||||
markPositions.put("K Start (2)", new DegreePosition(54.500209999999996, 10.20206472));
|
||||
markPositions.put("K Mark4 (2)", new DegreePosition(54.499422999999986, 10.200381692));
|
||||
markPositions.put("K Mark4 (1)", new DegreePosition(54.498954999999995, 10.200982));
|
||||
markPositions.put("K Mark1", new DegreePosition(54.489738990000006, 10.17079423000015));
|
||||
markPositions.put("K Finish (left)", new DegreePosition(54.48918199999999, 10.17003714));
|
||||
markPositions.put("K Finish (right)", new DegreePosition(54.48891756, 10.170632146666675));
|
||||
markPositions.put("K Finish (1)", new DegreePosition(54.48918199999999, 10.17003714));
|
||||
markPositions.put("K Finish (2)", new DegreePosition(54.48891756, 10.170632146666675));
|
||||
for (Waypoint w : race.getRace().getCourse().getWaypoints()) {
|
||||
for (Mark mark : w.getMarks()) {
|
||||
race.getOrCreateTrack(mark).addGPSFix(new GPSFixImpl(markPositions.get(mark.getName()), epoch));
|
||||
|
||||
@@ -8,6 +8,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.util.HashSet;
|
||||
@@ -750,4 +752,78 @@ public class TrackTest {
|
||||
assertFalse(newPositionAtEndOfTime.equals(positionAtEndOfTime));
|
||||
assertTrue(!intervalAffected.getB().before(endOfTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpeedEstimationForCourseChangeAtFix() throws ParseException {
|
||||
/*
|
||||
* See bug 1065, first test data set: [2011-06-23T16:08:27.000+0200: (54.49155,10.184947) with
|
||||
* 5.399568034557236kn to 291.0�, 2011-06-23T16:08:31.000+0200: (54.491597,10.184808) with 5.939524838012959kn
|
||||
* to 296.0�, 2011-06-23T16:08:37.000+0200: (54.491572999999995,10.184669999999999) with 3.7796976241900646kn
|
||||
* to 210.0�, 2011-06-23T16:08:39.000+0200: (54.491523,10.184602) with 5.939524838012959kn to 219.0�,
|
||||
* 2011-06-23T16:08:43.000+0200: (54.491457999999994,10.18451) with 4.859611231101512kn to 225.0�,
|
||||
* 2011-06-23T16:08:45.000+0200: (54.491386999999996,10.184455) with 5.939524838012959kn to 196.0�]
|
||||
*
|
||||
* let the track estimate the speed at 16:08:37 at
|
||||
*
|
||||
* 5.469864974239993kn to 215.50791079454874�
|
||||
*/
|
||||
DynamicGPSFixMovingTrackImpl<Object> myTrack = new DynamicGPSFixMovingTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */5000);
|
||||
GPSFixMoving fix1 = createFix("2011-06-23T16:08:27.000+0200", 54.49155, 10.184947, 5.399568034557236, 291.0);
|
||||
myTrack.addGPSFix(fix1);
|
||||
GPSFixMoving fix2 = createFix("2011-06-23T16:08:31.000+0200", 54.491597,10.184808, 5.939524838012959, 296.0);
|
||||
myTrack.addGPSFix(fix2);
|
||||
GPSFixMoving fix3 = createFix("2011-06-23T16:08:37.000+0200", 54.491572999999995,10.184669999999999, 3.7796976241900646, 210.0);
|
||||
myTrack.addGPSFix(fix3);
|
||||
GPSFixMoving fix4 = createFix("2011-06-23T16:08:39.000+0200", 54.491523, 10.184602, 5.939524838012959, 219.0);
|
||||
myTrack.addGPSFix(fix4);
|
||||
GPSFixMoving fix5 = createFix("2011-06-23T16:08:43.000+0200", 54.491457999999994, 10.18451, 4.859611231101512, 225.0);
|
||||
myTrack.addGPSFix(fix5);
|
||||
GPSFixMoving fix6 = createFix("2011-06-23T16:08:45.000+0200", 54.491386999999996, 10.184455, 5.939524838012959, 196.0);
|
||||
myTrack.addGPSFix(fix6);
|
||||
final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
SpeedWithBearing estimatedSpeedWithBearing = myTrack.getEstimatedSpeed(new MillisecondsTimePoint(dateFormatter.parse("2011-06-23T16:08:37.000+0200")));
|
||||
assertEquals(230., estimatedSpeedWithBearing.getBearing().getDegrees(), 5);
|
||||
assertEquals(5.0, estimatedSpeedWithBearing.getKnots(), 0.2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpeedEstimationForCourseChangeBetweenFixes() throws ParseException {
|
||||
/*
|
||||
* See bug 1065, first test data set: [2012-10-21T15:00:32.000+0200: (43.692862999999996,7.267875) with
|
||||
* 16.73866090712743kn to 294.0°, 2012-10-21T15:00:34.000+0200: (43.692937,7.267683) with 16.73866090712743kn
|
||||
* to 299.0°, 2012-10-21T15:00:36.000+0200: (43.693017,7.267487999999999) with 16.73866090712743kn to 300.0°,
|
||||
* 2012-10-21T15:00:38.000+0200: (43.693103,7.267297999999999) with 16.73866090712743kn to 300.0°,
|
||||
* 2012-10-21T15:00:40.000+0200: (43.693191999999996,7.267112) with 16.73866090712743kn to 303.0°,
|
||||
* 2012-10-21T15:00:43.000+0200: (43.693283,7.2669179999999995) with 18.898488120950326kn to 302.0°]
|
||||
* 2012-10-21T15:00:51.000+0200: (43.693245,7.266183) with 11.339092872570195kn to 237.0°
|
||||
*/
|
||||
DynamicGPSFixMovingTrackImpl<Object> myTrack = new DynamicGPSFixMovingTrackImpl<Object>(new Object(), /* millisecondsOverWhichToAverage */5000);
|
||||
GPSFixMoving fix1 = createFix("2012-10-21T15:00:32.000+0200", 43.692862999999996,7.267875, 16.73866090712743, 294.0);
|
||||
myTrack.addGPSFix(fix1);
|
||||
GPSFixMoving fix2 = createFix("2012-10-21T15:00:34.000+0200", 43.692937, 7.267683, 16.73866090712743, 299.0);
|
||||
myTrack.addGPSFix(fix2);
|
||||
GPSFixMoving fix3 = createFix("2012-10-21T15:00:36.000+0200", 43.693017, 7.267487999999999, 16.73866090712743, 300.0);
|
||||
myTrack.addGPSFix(fix3);
|
||||
GPSFixMoving fix4 = createFix("2012-10-21T15:00:38.000+0200", 43.693103, 7.267297999999999, 16.73866090712743, 300.0);
|
||||
myTrack.addGPSFix(fix4);
|
||||
GPSFixMoving fix5 = createFix("2012-10-21T15:00:40.000+0200", 43.693191999999996, 7.267112, 16.73866090712743, 303.0);
|
||||
myTrack.addGPSFix(fix5);
|
||||
GPSFixMoving fix6 = createFix("2012-10-21T15:00:43.000+0200", 43.693283, 7.2669179999999995, 18.898488120950326, 302.0);
|
||||
myTrack.addGPSFix(fix6);
|
||||
GPSFixMoving fix7 = createFix("2012-10-21T15:00:51.000+0200", 43.693245, 7.266183, 11.339092872570195, 237.0);
|
||||
myTrack.addGPSFix(fix7);
|
||||
final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
SpeedWithBearing estimatedSpeedWithBearing = myTrack.getEstimatedSpeed(new MillisecondsTimePoint(dateFormatter.parse("2012-10-21T15:00:45.000+0200")));
|
||||
assertEquals(275., estimatedSpeedWithBearing.getBearing().getDegrees(), 5);
|
||||
assertEquals(15.5, estimatedSpeedWithBearing.getKnots(), 0.2);
|
||||
}
|
||||
|
||||
private GPSFixMoving createFix(String isoDateTime, double lat, double lng, double knotSpeed, double bearingDeg) throws ParseException {
|
||||
final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
TimePoint timePoint = new MillisecondsTimePoint(dateFormatter.parse(isoDateTime));
|
||||
Position position = new DegreePosition(lat, lng);
|
||||
SpeedWithBearing speed = new KnotSpeedWithBearingImpl(knotSpeed, new DegreeBearingImpl(bearingDeg));
|
||||
GPSFixMoving fix = new GPSFixMovingImpl(position, timePoint, speed);
|
||||
return fix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,28 @@ import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.impl.KnotSpeedWithBearingImpl;
|
||||
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.common.NoWindException;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.common.WindSourceType;
|
||||
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
|
||||
import com.sap.sailing.domain.common.impl.DegreePosition;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
import com.sap.sailing.domain.common.impl.WindSourceImpl;
|
||||
import com.sap.sailing.domain.tracking.GPSFixMoving;
|
||||
import com.sap.sailing.domain.tracking.GPSFixTrack;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.domain.tracking.impl.GPSFixImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindImpl;
|
||||
|
||||
public class WindEstimationOnStoredTracksTest extends StoredTrackBasedTestWithTrackedRace {
|
||||
// ---------------- BearingWithConfidenceCluster -----------------
|
||||
@@ -28,12 +39,29 @@ public class WindEstimationOnStoredTracksTest extends StoredTrackBasedTestWithTr
|
||||
|
||||
@Test
|
||||
public void testSimpleWindEstimation() throws NoWindException, FileNotFoundException, IOException {
|
||||
Map<String, Position> markPositions = new HashMap<>();
|
||||
markPositions.put("Left lee gate buoy", new DegreePosition(54.4680424, 10.234451));
|
||||
markPositions.put("Right lee gate buoy", new DegreePosition(54.4681424, 10.234451));
|
||||
markPositions.put("Windward mark", new DegreePosition(54.4680424, 10.034451));
|
||||
|
||||
Competitor hasso = getCompetitorByName("Dr.Plattner");
|
||||
GPSFixTrack<Competitor, GPSFixMoving> hassosTrack = getTrackedRace().getTrack(hasso);
|
||||
TimePoint start = hassosTrack.getFirstRawFix().getTimePoint();
|
||||
TimePoint stop = hassosTrack.getLastRawFix().getTimePoint();
|
||||
TimePoint middle = new MillisecondsTimePoint(start.asMillis() + (stop.asMillis()-start.asMillis())*3/4);
|
||||
Wind estimatedWindDirection = getTrackedRace().getEstimatedWindDirection(/* position */ null, middle);
|
||||
TimePoint somewhenOnAnUpwind = new MillisecondsTimePoint(start.asMillis() + (stop.asMillis()-start.asMillis())*3/4);
|
||||
for (Mark mark : getTrackedRace().getMarks()) {
|
||||
getTrackedRace().getOrCreateTrack(mark).addGPSFix(
|
||||
new GPSFixImpl(markPositions.get(mark.getName()), new MillisecondsTimePoint(0)));
|
||||
getTrackedRace().getOrCreateTrack(mark).addGPSFix(
|
||||
new GPSFixImpl(markPositions.get(mark.getName()), somewhenOnAnUpwind));
|
||||
}
|
||||
getTrackedRace().recordWind(new WindImpl(markPositions.get("Windward mark"), new MillisecondsTimePoint(0),
|
||||
new KnotSpeedWithBearingImpl(
|
||||
/* speedInKnots */14.7, new DegreeBearingImpl(90))), new WindSourceImpl(WindSourceType.WEB));
|
||||
getTrackedRace().recordWind(new WindImpl(markPositions.get("Windward mark"), somewhenOnAnUpwind,
|
||||
new KnotSpeedWithBearingImpl(
|
||||
/* speedInKnots */14.7, new DegreeBearingImpl(90))), new WindSourceImpl(WindSourceType.WEB));
|
||||
Wind estimatedWindDirection = getTrackedRace().getEstimatedWindDirection(/* position */ null, somewhenOnAnUpwind);
|
||||
assertNotNull(estimatedWindDirection);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.sap.sailing.domain.base.impl.PersonImpl;
|
||||
import com.sap.sailing.domain.base.impl.RaceDefinitionImpl;
|
||||
import com.sap.sailing.domain.base.impl.RegattaImpl;
|
||||
import com.sap.sailing.domain.base.impl.TeamImpl;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.ScoringSchemeType;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
@@ -144,16 +145,27 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
if (domainControlPoint == null) {
|
||||
String controlPointName = controlPoint.getName();
|
||||
Map<String, String> controlPointMetadata = parseControlPointMetadata(controlPoint);
|
||||
String markColor = controlPointMetadata.get("Color");
|
||||
String markShape = controlPointMetadata.get("Shape");
|
||||
String markPattern = controlPointMetadata.get("Pattern");
|
||||
if (controlPoint.getHasTwoPoints()) {
|
||||
// it's a gate
|
||||
Mark leftMark = baseDomainFactory.getOrCreateMark(controlPointName + " (left)", markColor, markShape, markPattern);
|
||||
Mark rightMark = baseDomainFactory.getOrCreateMark(controlPointName + " (right)", markColor, markShape, markPattern);
|
||||
domainControlPoint = baseDomainFactory.createGate(leftMark, rightMark, controlPointName);
|
||||
MarkType type1 = resolveMarkTypeFromMetadata(controlPointMetadata, "P1.Type");
|
||||
MarkType type2 = resolveMarkTypeFromMetadata(controlPointMetadata, "P2.Type");
|
||||
String color1 = controlPointMetadata.get("P1.Color");
|
||||
String color2 = controlPointMetadata.get("P2.Color");
|
||||
String shape1 = controlPointMetadata.get("P1.Shape");
|
||||
String shape2 = controlPointMetadata.get("P2.Shape");
|
||||
String pattern1 = controlPointMetadata.get("P1.Pattern");
|
||||
String pattern2 = controlPointMetadata.get("P2.Pattern");
|
||||
|
||||
Mark mark1 = baseDomainFactory.getOrCreateMark(controlPointName + " (1)", type1, color1, shape1, pattern1);
|
||||
Mark mark2 = baseDomainFactory.getOrCreateMark(controlPointName + " (2)", type2, color2, shape2, pattern2);
|
||||
domainControlPoint = baseDomainFactory.createGate(mark1, mark2, controlPointName);
|
||||
} else {
|
||||
Mark mark = baseDomainFactory.getOrCreateMark(controlPointName, markColor, markShape, markPattern);
|
||||
MarkType type = resolveMarkTypeFromMetadata(controlPointMetadata, "Type");
|
||||
String color = controlPointMetadata.get("Color");
|
||||
String shape = controlPointMetadata.get("Shape");
|
||||
String pattern = controlPointMetadata.get("Pattern");
|
||||
|
||||
Mark mark = baseDomainFactory.getOrCreateMark(controlPointName, type, color, shape, pattern);
|
||||
domainControlPoint = mark;
|
||||
}
|
||||
controlPointCache.put(controlPoint, domainControlPoint);
|
||||
@@ -161,6 +173,20 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
return domainControlPoint;
|
||||
}
|
||||
}
|
||||
|
||||
private MarkType resolveMarkTypeFromMetadata(Map<String, String> controlPointMetadata, String typePropertyName) {
|
||||
MarkType result = MarkType.BUOY;
|
||||
String markType = controlPointMetadata.get(typePropertyName);
|
||||
if(markType != null && !markType.isEmpty()) {
|
||||
for(MarkType m: MarkType.values()) {
|
||||
if(m.name().equalsIgnoreCase(markType)) {
|
||||
result = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private Map<String, String> parseControlPointMetadata(ControlPoint controlPoint) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding//src/com/sap/sailing/domain/base/impl/AbstractSpeedWithAbstractBearingImpl.java=UTF-8
|
||||
encoding//src/com/sap/sailing/domain/tracking/impl/CompactGPSFixMovingImpl.java=UTF-8
|
||||
@@ -5,6 +5,7 @@ import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.sap.sailing.domain.base.impl.DomainFactoryImpl;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
import com.sap.sailing.domain.common.ScoringSchemeType;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.leaderboard.ScoringScheme;
|
||||
@@ -26,7 +27,7 @@ public interface DomainFactory {
|
||||
* in that case. Otherwise, a new {@link Mark} is created with <code>color</code> as its {@link Mark#getColor()}
|
||||
* and <code>shape</code> as its {@link Mark#getShape()}.
|
||||
*/
|
||||
Mark getOrCreateMark(String id, String color, String shape, String pattern);
|
||||
Mark getOrCreateMark(String id, MarkType type, String color, String shape, String pattern);
|
||||
|
||||
Gate createGate(Mark left, Mark right, String name);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sap.sailing.domain.base;
|
||||
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
|
||||
|
||||
/**
|
||||
* A marks name is used as its ID which is only identifying the mark uniquely within a single race or course
|
||||
@@ -12,4 +14,5 @@ public interface Mark extends WithID, ControlPoint, IsManagedByDomainFactory {
|
||||
public String getColor();
|
||||
public String getShape();
|
||||
public String getPattern();
|
||||
public MarkType getType();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public abstract class AbstractSpeedWithAbstractBearingImpl extends AbstractSpeed
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString()+" to "+getBearing().getDegrees()+"�";
|
||||
return super.toString()+" to "+getBearing().getDegrees()+"°";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,15 +16,16 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.sap.sailing.domain.base.Boat;
|
||||
import com.sap.sailing.domain.base.BoatClass;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.ControlPoint;
|
||||
import com.sap.sailing.domain.base.DomainFactory;
|
||||
import com.sap.sailing.domain.base.Gate;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.ObjectInputStreamResolvingAgainstDomainFactory;
|
||||
import com.sap.sailing.domain.base.Team;
|
||||
import com.sap.sailing.domain.base.Waypoint;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
import com.sap.sailing.domain.common.ScoringSchemeType;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.leaderboard.ScoringScheme;
|
||||
@@ -120,10 +121,10 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mark getOrCreateMark(String id, String color, String shape, String pattern) {
|
||||
public Mark getOrCreateMark(String id, MarkType type, String color, String shape, String pattern) {
|
||||
Mark result = markCache.get(id);
|
||||
if (result == null) {
|
||||
result = new MarkImpl(id, color, shape, pattern);
|
||||
result = new MarkImpl(id, type, color, shape, pattern);
|
||||
markCache.put(id, result);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -3,8 +3,9 @@ package com.sap.sailing.domain.base.impl;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.DomainFactory;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
import com.sap.sailing.domain.common.impl.NamedImpl;
|
||||
|
||||
public class MarkImpl extends NamedImpl implements Mark {
|
||||
@@ -13,30 +14,43 @@ public class MarkImpl extends NamedImpl implements Mark {
|
||||
private final String color;
|
||||
private final String shape;
|
||||
private final String pattern;
|
||||
private final MarkType type;
|
||||
|
||||
public MarkImpl(String name) {
|
||||
super(name);
|
||||
type = MarkType.BUOY;
|
||||
color = null;
|
||||
shape = null;
|
||||
pattern = null;
|
||||
}
|
||||
|
||||
public MarkImpl(String name, MarkType type) {
|
||||
super(name);
|
||||
this.type = type;
|
||||
color = null;
|
||||
shape = null;
|
||||
pattern = null;
|
||||
}
|
||||
|
||||
public MarkImpl(String name, String color) {
|
||||
public MarkImpl(String name, MarkType type, String color) {
|
||||
super(name);
|
||||
this.type = type;
|
||||
this.color = color;
|
||||
shape = null;
|
||||
pattern = null;
|
||||
}
|
||||
|
||||
public MarkImpl(String name, String color, String shape) {
|
||||
public MarkImpl(String name, MarkType type, String color, String shape) {
|
||||
super(name);
|
||||
this.type = type;
|
||||
this.color = color;
|
||||
this.shape = shape;
|
||||
pattern = null;
|
||||
}
|
||||
|
||||
public MarkImpl(String name, String color, String shape, String pattern) {
|
||||
public MarkImpl(String name, MarkType type, String color, String shape, String pattern) {
|
||||
super(name);
|
||||
this.type = type;
|
||||
this.color = color;
|
||||
this.shape = shape;
|
||||
this.pattern = pattern;
|
||||
@@ -56,7 +70,7 @@ public class MarkImpl extends NamedImpl implements Mark {
|
||||
|
||||
@Override
|
||||
public Mark resolve(DomainFactory domainFactory) {
|
||||
Mark result = domainFactory.getOrCreateMark(getName(), color, shape, pattern);
|
||||
Mark result = domainFactory.getOrCreateMark(getName(), type, color, shape, pattern);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -74,4 +88,9 @@ public class MarkImpl extends NamedImpl implements Mark {
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkType getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ public interface TrackedLegOfCompetitor extends Serializable {
|
||||
* as defined by <code>timePointBeforeManeuver</code> is extrapolated until <code>timePointAfterManeuver</code>,
|
||||
* and the resulting extrapolated position's "windward distance" is computed to the competitor's actual position
|
||||
* at that time. This distance is returned as the result of this method.
|
||||
* @param maneuverTimePoint TODO
|
||||
*/
|
||||
Distance getManeuverLoss(TimePoint timePointBeforeManeuver, TimePoint timePointAfterManeuver) throws NoWindException;
|
||||
Distance getManeuverLoss(TimePoint timePointBeforeManeuver, TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) throws NoWindException;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class CompactGPSFixMovingImpl extends CompactGPSFixImpl implements GPSFix
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString()+" to "+getBearing().getDegrees()+"�";
|
||||
return super.toString()+" to "+getBearing().getDegrees()+"°";
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.NavigableSet;
|
||||
|
||||
@@ -14,14 +13,12 @@ import com.sap.sailing.domain.base.impl.KnotSpeedWithBearingImpl;
|
||||
import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl;
|
||||
import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl;
|
||||
import com.sap.sailing.domain.common.Bearing;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.Speed;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.confidence.ConfidenceBasedAverager;
|
||||
import com.sap.sailing.domain.confidence.ConfidenceFactory;
|
||||
import com.sap.sailing.domain.confidence.HasConfidence;
|
||||
import com.sap.sailing.domain.confidence.Weigher;
|
||||
import com.sap.sailing.domain.tracking.GPSFix;
|
||||
import com.sap.sailing.domain.tracking.GPSFixMoving;
|
||||
|
||||
public class DynamicGPSFixMovingTrackImpl<ItemType> extends DynamicTrackImpl<ItemType, GPSFixMoving> {
|
||||
@@ -53,13 +50,12 @@ public class DynamicGPSFixMovingTrackImpl<ItemType> extends DynamicTrackImpl<Ite
|
||||
NavigableSet<GPSFixMoving> fixesToUseForSpeedEstimation, Weigher<TimePoint> weigher) {
|
||||
lockForRead();
|
||||
try {
|
||||
GPSFixMoving[] relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation);
|
||||
List<GPSFixMoving> relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation);
|
||||
List<SpeedWithConfidence<TimePoint>> speeds = new ArrayList<SpeedWithConfidence<TimePoint>>();
|
||||
BearingWithConfidenceCluster<TimePoint> bearingCluster = new BearingWithConfidenceCluster<TimePoint>(
|
||||
weigher);
|
||||
if (relevantFixes.length != 0) {
|
||||
BearingWithConfidenceCluster<TimePoint> bearingCluster = new BearingWithConfidenceCluster<TimePoint>(weigher);
|
||||
if (!relevantFixes.isEmpty()) {
|
||||
int i=0;
|
||||
GPSFixMoving last = relevantFixes[i];
|
||||
GPSFixMoving last = relevantFixes.get(i);
|
||||
// add fix's own speed/bearing; this also works if only one "relevant" fix is found
|
||||
SpeedWithConfidenceImpl<TimePoint> speedWithConfidence = new SpeedWithConfidenceImpl<TimePoint>(
|
||||
last.getSpeed(),
|
||||
@@ -67,9 +63,9 @@ public class DynamicGPSFixMovingTrackImpl<ItemType> extends DynamicTrackImpl<Ite
|
||||
speeds.add(speedWithConfidence);
|
||||
bearingCluster.add(new BearingWithConfidenceImpl<TimePoint>(last.getSpeed().getBearing(), /* confidence */
|
||||
0.9, last.getTimePoint()));
|
||||
while (i<relevantFixes.length-1) {
|
||||
while (i<relevantFixes.size()-1) {
|
||||
// add to average the position and time difference
|
||||
GPSFixMoving next = relevantFixes[++i];
|
||||
GPSFixMoving next = relevantFixes.get(++i);
|
||||
aggregateSpeedAndBearingFromLastToNext(speeds, bearingCluster, last, next);
|
||||
// add to average the speed and bearing provided by the GPSFixMoving
|
||||
SpeedWithConfidenceImpl<TimePoint> computedSpeedWithConfidence = new SpeedWithConfidenceImpl<TimePoint>(
|
||||
@@ -98,105 +94,6 @@ public class DynamicGPSFixMovingTrackImpl<ItemType> extends DynamicTrackImpl<Ite
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that no corresponding re-definition of {@link #getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(GPSFix)} is
|
||||
* necessary because the fixes affected are just the same.
|
||||
*/
|
||||
protected GPSFixMoving[] getFixesRelevantForSpeedEstimation(TimePoint at,
|
||||
NavigableSet<GPSFixMoving> fixesToUseForSpeedEstimation) {
|
||||
assertReadLock();
|
||||
DummyGPSFixMoving atTimed = new DummyGPSFixMoving(at);
|
||||
List<GPSFixMoving> relevantFixes = new LinkedList<GPSFixMoving>();
|
||||
boolean beforeSetEmpty;
|
||||
GPSFixMoving beforeSetLast = null;
|
||||
lockForRead();
|
||||
try {
|
||||
NavigableSet<GPSFixMoving> beforeSet = fixesToUseForSpeedEstimation.headSet(atTimed, /* inclusive */ false);
|
||||
beforeSetEmpty = beforeSet.isEmpty(); // ask this while holding the lock
|
||||
if (!beforeSetEmpty) {
|
||||
beforeSetLast = beforeSet.last();
|
||||
}
|
||||
for (GPSFixMoving beforeFix : beforeSet.descendingSet()) {
|
||||
if (at.asMillis() - beforeFix.getTimePoint().asMillis() > getMillisecondsOverWhichToAverage() / 2) {
|
||||
break;
|
||||
}
|
||||
relevantFixes.add(0, beforeFix);
|
||||
}
|
||||
} finally {
|
||||
unlockAfterRead();
|
||||
}
|
||||
boolean afterSetEmpty;
|
||||
GPSFixMoving afterSetFirst = null;
|
||||
lockForRead();
|
||||
try {
|
||||
NavigableSet<GPSFixMoving> afterSet = fixesToUseForSpeedEstimation.tailSet(atTimed, /* inclusive */ true);
|
||||
afterSetEmpty = afterSet.isEmpty(); // ask this while holding the lock
|
||||
if (!afterSetEmpty) {
|
||||
afterSetFirst = afterSet.first();
|
||||
}
|
||||
for (GPSFixMoving afterFix : afterSet) {
|
||||
if (afterFix.getTimePoint().asMillis() - at.asMillis() > getMillisecondsOverWhichToAverage() / 2) {
|
||||
break;
|
||||
}
|
||||
relevantFixes.add(afterFix);
|
||||
}
|
||||
} finally {
|
||||
unlockAfterRead();
|
||||
}
|
||||
if (relevantFixes.isEmpty()) {
|
||||
// find the fix closest to "at":
|
||||
if (beforeSetEmpty) {
|
||||
if (!afterSetEmpty) {
|
||||
relevantFixes.add(afterSetFirst);
|
||||
}
|
||||
} else {
|
||||
if (afterSetEmpty) {
|
||||
relevantFixes.add(beforeSetLast);
|
||||
} else {
|
||||
GPSFixMoving beforeFix = beforeSetLast;
|
||||
GPSFixMoving afterFix = afterSetFirst;
|
||||
relevantFixes.add(at.asMillis() - beforeFix.getTimePoint().asMillis() <= afterFix.getTimePoint()
|
||||
.asMillis() - at.asMillis() ? beforeFix : afterFix);
|
||||
}
|
||||
}
|
||||
}
|
||||
return relevantFixes.toArray(new GPSFixMoving[0]);
|
||||
}
|
||||
|
||||
private class DummyGPSFixMoving extends DummyTimed implements GPSFixMoving {
|
||||
private static final long serialVersionUID = 7135346050999824024L;
|
||||
public DummyGPSFixMoving(TimePoint timePoint) {
|
||||
super(timePoint);
|
||||
}
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public SpeedWithBearing getSpeed() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public SpeedWithBearing getSpeedAndBearingRequiredToReach(GPSFix to) {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public boolean isValidityCached() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public void invalidateCache() {
|
||||
|
||||
}
|
||||
@Override
|
||||
public void cacheValidity(boolean isValid) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In addition to the base class implementation, we additionally have the speed and bearing as
|
||||
* measured by the device. We use the device-measured speed and compare it with the speed computed
|
||||
|
||||
@@ -575,18 +575,16 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends TrackImpl
|
||||
NavigableSet<FixType> fixesToUseForSpeedEstimation, Weigher<TimePoint> weigher) {
|
||||
lockForRead();
|
||||
try {
|
||||
GPSFix[] relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation);
|
||||
List<FixType> relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation);
|
||||
List<SpeedWithConfidence<TimePoint>> speeds = new ArrayList<SpeedWithConfidence<TimePoint>>();
|
||||
BearingWithConfidenceCluster<TimePoint> bearingCluster = new BearingWithConfidenceCluster<TimePoint>(weigher);
|
||||
if (relevantFixes.length != 0) {
|
||||
int i=0;
|
||||
GPSFix last = relevantFixes[i];
|
||||
while (i<relevantFixes.length-1) {
|
||||
FixType last = null;
|
||||
for (FixType next : relevantFixes) {
|
||||
if (last != null) {
|
||||
// TODO bug #346: consider time difference between next.getTimepoint() and at to compute a confidence
|
||||
GPSFix next = relevantFixes[++i];
|
||||
aggregateSpeedAndBearingFromLastToNext(speeds, bearingCluster, last, next);
|
||||
last = next;
|
||||
}
|
||||
last = next;
|
||||
}
|
||||
ConfidenceBasedAverager<Double, Speed, TimePoint> speedAverager = ConfidenceFactory.INSTANCE.createAverager(weigher);
|
||||
HasConfidence<Double, Speed, TimePoint> speedWithConfidence = speedAverager.getAverage(speeds, at);
|
||||
@@ -690,32 +688,45 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends TrackImpl
|
||||
return new Pair<TimePoint, TimePoint>(intervalStart, intervalEnd);
|
||||
}
|
||||
|
||||
protected FixType createDummyGPSFix(TimePoint at) {
|
||||
@SuppressWarnings("unchecked")
|
||||
FixType result = (FixType) new DummyGPSFix(at);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A track that doesn't have {@link GPSFixMoving} fixes and therefore needs to compute the speed using the fix time
|
||||
* and position differences needs at least two fixes to compute a result. If only one fix is found within the
|
||||
* {@link #getMillisecondsOverWhichToAverage() averaging interval}, the closest existing fix later or earlier than
|
||||
* <code>at</code> is used.
|
||||
* and position differences. It therefore needs at least two fixes to compute a result. The algorithm always uses
|
||||
* at least the closest existing fix later and the closet existing fix earlier than <code>at</code>, if available.
|
||||
* <p>
|
||||
*
|
||||
* But even for a track with {@link GPSFixMoving} fixes this is a good algorithm because in case the speed changes
|
||||
* significantly between fixes, it is important to know the next fix to understand and consider the trend.
|
||||
*/
|
||||
protected GPSFix[] getFixesRelevantForSpeedEstimation(TimePoint at, NavigableSet<FixType> fixesToUseForSpeedEstimation) {
|
||||
protected List<FixType> getFixesRelevantForSpeedEstimation(TimePoint at, NavigableSet<FixType> fixesToUseForSpeedEstimation) {
|
||||
lockForRead();
|
||||
try {
|
||||
DummyGPSFix atTimed = new DummyGPSFix(at);
|
||||
List<GPSFix> relevantFixes = new LinkedList<GPSFix>();
|
||||
@SuppressWarnings("unchecked")
|
||||
NavigableSet<GPSFix> castFixesToUseForSpeedEstimation = (NavigableSet<GPSFix>) fixesToUseForSpeedEstimation;
|
||||
NavigableSet<GPSFix> beforeSet = castFixesToUseForSpeedEstimation.headSet(atTimed, /* inclusive */false);
|
||||
NavigableSet<GPSFix> afterSet = castFixesToUseForSpeedEstimation.tailSet(atTimed, /* inclusive */true);
|
||||
GPSFix beforeFix = null;
|
||||
Iterator<GPSFix> beforeFixIter = beforeSet.descendingIterator();
|
||||
FixType atTimed = createDummyGPSFix(at);
|
||||
List<FixType> relevantFixes = new LinkedList<FixType>();
|
||||
NavigableSet<FixType> beforeSet = fixesToUseForSpeedEstimation.headSet(atTimed, /* inclusive */false);
|
||||
NavigableSet<FixType> afterSet = fixesToUseForSpeedEstimation.tailSet(atTimed, /* inclusive */true);
|
||||
FixType beforeFix = null;
|
||||
Iterator<FixType> beforeFixIter = beforeSet.descendingIterator();
|
||||
boolean noBeforeFixUsedYet = true;
|
||||
while (beforeFixIter.hasNext() &&
|
||||
at.asMillis() - (beforeFix=beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2) {
|
||||
(at.asMillis() - (beforeFix=beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2 || noBeforeFixUsedYet)) {
|
||||
relevantFixes.add(0, beforeFix);
|
||||
noBeforeFixUsedYet = false;
|
||||
beforeFix = null; // mark the fix as used
|
||||
}
|
||||
GPSFix afterFix = null;
|
||||
Iterator<GPSFix> afterFixIter = afterSet.iterator();
|
||||
FixType afterFix = null;
|
||||
Iterator<FixType> afterFixIter = afterSet.iterator();
|
||||
boolean noAfterFixUsedYet = true;
|
||||
while (afterFixIter.hasNext() &&
|
||||
(afterFix=afterFixIter.next()).getTimePoint().asMillis() - at.asMillis() < getMillisecondsOverWhichToAverage() / 2) {
|
||||
((afterFix=afterFixIter.next()).getTimePoint().asMillis() - at.asMillis() < getMillisecondsOverWhichToAverage() / 2 || noAfterFixUsedYet)) {
|
||||
relevantFixes.add(afterFix);
|
||||
noAfterFixUsedYet = false;
|
||||
afterFix = null; // mark the fix as used
|
||||
}
|
||||
// now fill up relevantFixes until we have at least two fixes or we run out of fixes entirely (can't estimate speed
|
||||
// with this type of fix on a track with less than two fixes)
|
||||
@@ -755,7 +766,7 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends TrackImpl
|
||||
}
|
||||
}
|
||||
}
|
||||
return relevantFixes.toArray(new GPSFix[0]);
|
||||
return relevantFixes;
|
||||
} finally {
|
||||
unlockAfterRead();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.NavigableSet;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Leg;
|
||||
@@ -25,6 +28,7 @@ import com.sap.sailing.domain.tracking.MarkPassing;
|
||||
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.util.impl.ArrayListNavigableSet;
|
||||
|
||||
/**
|
||||
* Provides a convenient view on the tracked leg, projecting to a single competitor's performance.
|
||||
@@ -581,7 +585,7 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
|
||||
|
||||
@Override
|
||||
public Distance getManeuverLoss(TimePoint timePointBeforeManeuver,
|
||||
TimePoint timePointAfterManeuver) throws NoWindException {
|
||||
TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) throws NoWindException {
|
||||
// FIXME bug 156: the timePointAfterManeuver is not a good one to select the outbound speed from:
|
||||
// instead, the outbound speed should be chosen as the maximum speed reached after the acceleration phase
|
||||
// after the maneuver, at least within some reasonable threshold like five times the approximate
|
||||
@@ -590,38 +594,46 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
|
||||
assert timePointBeforeManeuver != null;
|
||||
assert timePointAfterManeuver != null;
|
||||
Distance result;
|
||||
long millisecondsOverWhichToAverageSpeed = getTrackedRace().getMillisecondsOverWhichToAverageSpeed();
|
||||
TimePoint extrapolationBase = timePointBeforeManeuver.minus(millisecondsOverWhichToAverageSpeed);
|
||||
final GPSFixTrack<Competitor, GPSFixMoving> track = getTrackedRace().getTrack(getCompetitor());
|
||||
SpeedWithBearing speedBeforeManeuver = track.getEstimatedSpeed(extrapolationBase);
|
||||
if (speedBeforeManeuver != null) {
|
||||
TimePoint extrapolationTarget = timePointAfterManeuver.plus(millisecondsOverWhichToAverageSpeed);
|
||||
SpeedWithBearing speedAfterManeuver = track.getEstimatedSpeed(extrapolationTarget);
|
||||
// find the mean course between inbound and outbound course and project actual and extrapolated
|
||||
// positions onto it:
|
||||
Bearing middle = speedBeforeManeuver.getBearing().middle(speedAfterManeuver.getBearing());
|
||||
Position positionAtBase = track.getEstimatedPosition(extrapolationBase, /* extrapolate */false);
|
||||
Speed projectedSpeedBeforeManeuver = speedBeforeManeuver.projectTo(positionAtBase, middle);
|
||||
double projectedSpeedBeforeManeuverInKnots = projectedSpeedBeforeManeuver.getKnots();
|
||||
Speed projectedSpeedAfterManeuver = speedAfterManeuver.projectTo(positionAtBase, middle);
|
||||
List<GPSFixMoving> fixes = getFixesToConsiderForManeuverLossAnalysis(timePointBeforeManeuver,
|
||||
maneuverTimePoint, timePointAfterManeuver);
|
||||
TimePoint timePointWhenSpeedStartedToDrop = fixes.get(0).getTimePoint();
|
||||
SpeedWithBearing speedWhenSpeedStartedToDrop = track.getEstimatedSpeed(timePointWhenSpeedStartedToDrop);
|
||||
if (speedWhenSpeedStartedToDrop != null) {
|
||||
TimePoint timePointWhenSpeedLevelledOffAfterManeuver = fixes.get(fixes.size()-1).getTimePoint();
|
||||
SpeedWithBearing speedAfterManeuver = track.getEstimatedSpeed(timePointWhenSpeedLevelledOffAfterManeuver);
|
||||
// For upwind/downwind legs, find the mean course between inbound and outbound course and project actual and
|
||||
// extrapolated positions onto it:
|
||||
Bearing middle = null;
|
||||
if (getTrackedLeg().isUpOrDownwindLeg(timePointWhenSpeedStartedToDrop)) {
|
||||
middle = getProjectionAngleForUpwindDownwindManeuverLossAnalysis(timePointWhenSpeedStartedToDrop, speedWhenSpeedStartedToDrop,
|
||||
speedAfterManeuver);
|
||||
}
|
||||
Position positionAtBase = track.getEstimatedPosition(timePointWhenSpeedStartedToDrop, /* extrapolate */false);
|
||||
Speed projectedSpeedWhenSpeedStartedToDrop = projectSpeedToMiddleIfUpwindDownwindElseToLegDirection(
|
||||
timePointWhenSpeedStartedToDrop, middle, speedWhenSpeedStartedToDrop, positionAtBase);
|
||||
double projectedSpeedBeforeManeuverInKnots = projectedSpeedWhenSpeedStartedToDrop.getKnots();
|
||||
Speed projectedSpeedAfterManeuver = projectSpeedToMiddleIfUpwindDownwindElseToLegDirection(
|
||||
timePointAfterManeuver, middle, speedAfterManeuver, positionAtBase);
|
||||
double projectedSpeedAfterManeuverInKnots = projectedSpeedAfterManeuver.getKnots();
|
||||
track.lockForRead();
|
||||
try {
|
||||
Iterator<GPSFixMoving> fixIter = track.getFixesIterator(timePointBeforeManeuver, /* inclusive */true);
|
||||
GPSFixMoving fix;
|
||||
TimePoint previousTimePoint = timePointBeforeManeuver;
|
||||
result = Distance.NULL;
|
||||
while (fixIter.hasNext() && !(fix = fixIter.next()).getTimePoint().after(timePointAfterManeuver)) {
|
||||
Speed projectedSpeedAtFix = fix.getSpeed().projectTo(fix.getPosition(), middle);
|
||||
final double estimatedSpeedWithoutManeuver = projectedSpeedBeforeManeuverInKnots +
|
||||
(fix.getTimePoint().asMillis()-timePointBeforeManeuver.asMillis()) /
|
||||
(timePointAfterManeuver.asMillis()-timePointBeforeManeuver.asMillis()) *
|
||||
(projectedSpeedAfterManeuverInKnots - projectedSpeedBeforeManeuverInKnots);
|
||||
Speed speedDifference = new KnotSpeedImpl(estimatedSpeedWithoutManeuver - projectedSpeedAtFix.getKnots());
|
||||
result = result.add(speedDifference.travel(previousTimePoint, fix.getTimePoint()));
|
||||
}
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
// now determine the fixes along which to accumulate the maneuver loss:
|
||||
|
||||
TimePoint previousTimePoint = timePointWhenSpeedStartedToDrop;
|
||||
result = Distance.NULL;
|
||||
for (GPSFixMoving fix : fixes) {
|
||||
// project onto average maneuver course for upwind/downwind and on leg direction otherwise
|
||||
final SpeedWithBearing fixSpeed = track.getEstimatedSpeed(fix.getTimePoint());
|
||||
final Position fixPosition = fix.getPosition();
|
||||
Speed projectedSpeedAtFix = projectSpeedToMiddleIfUpwindDownwindElseToLegDirection(
|
||||
timePointBeforeManeuver, middle, fixSpeed, fixPosition);
|
||||
final double estimatedSpeedWithoutManeuverInKnots = (double) projectedSpeedBeforeManeuverInKnots
|
||||
+ (double) (fix.getTimePoint().asMillis() - timePointWhenSpeedStartedToDrop.asMillis())
|
||||
/ (double) (timePointWhenSpeedLevelledOffAfterManeuver.asMillis() - timePointWhenSpeedStartedToDrop.asMillis())
|
||||
* (double) (projectedSpeedAfterManeuverInKnots - projectedSpeedBeforeManeuverInKnots);
|
||||
Speed speedDifference = new KnotSpeedImpl(estimatedSpeedWithoutManeuverInKnots
|
||||
- projectedSpeedAtFix.getKnots());
|
||||
result = result.add(speedDifference.travel(previousTimePoint, fix.getTimePoint()));
|
||||
previousTimePoint = fix.getTimePoint();
|
||||
}
|
||||
} else {
|
||||
result = null;
|
||||
@@ -629,4 +641,145 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the set of fixes that encompass the maneuver. Usually, during a maneuver a boat loses speed over ground
|
||||
* before it has reached the new tack and starts accelerating again, until the speed levels off. We assume that the
|
||||
* <code>timePointBeforeManeuver</code> minus some excess time based on the approximate maneuver duration is the
|
||||
* earliest time point to analyze. Starting there, we look for speed over ground minima between then and
|
||||
* <code>timePointAfterManeuver</code> plus three times the approximate maneuver time as excess time. There may be
|
||||
* multiple minima. We choose the one that has the best "fit" in terms of being close to the
|
||||
* <code>maneuverTimePoint</code> and being low in terms of speed over ground. From that time point, the nearest
|
||||
* maximum speeds over ground before and after are determined, and the fixes between them are returned.
|
||||
*/
|
||||
private List<GPSFixMoving> getFixesToConsiderForManeuverLossAnalysis(TimePoint timePointBeforeManeuver,
|
||||
TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) {
|
||||
final long EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds();
|
||||
final long EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS = 3*EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS;
|
||||
List<GPSFixMoving> fixes = new ArrayList<>();
|
||||
NavigableSet<GPSFixMoving> maxima = new ArrayListNavigableSet<GPSFixMoving>(new TimedComparator());
|
||||
NavigableSet<GPSFixMoving> minima = new ArrayListNavigableSet<GPSFixMoving>(new TimedComparator());
|
||||
GPSFixTrack<Competitor, GPSFixMoving> track = getTrackedRace().getTrack(getCompetitor());
|
||||
Speed lastSpeed = Speed.NULL;
|
||||
Speed lastLastSpeed = Speed.NULL;
|
||||
GPSFixMoving lastFix = null;
|
||||
Speed minimumSpeed = new KnotSpeedImpl(Double.MAX_VALUE);
|
||||
track.lockForRead();
|
||||
try {
|
||||
Iterator<GPSFixMoving> fixIter = track.getFixesIterator(
|
||||
timePointBeforeManeuver.minus(EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS), /* inclusive */true);
|
||||
GPSFixMoving fix;
|
||||
// The timePointAfterManeuver is determined based on the geometric shape of the boat's trajectory, not on
|
||||
// the speed development. To understand the full maneuver loss, we need to follow the boat speed until it levels off,
|
||||
// but no further than some reasonable threshold because the wind may continue to pick up, letting the boat accelerate
|
||||
// over a time much longer than accounted for by the maneuver.
|
||||
while (fixIter.hasNext()) {
|
||||
fix = fixIter.next();
|
||||
final SpeedWithBearing estimatedSpeedAtFix = track.getEstimatedSpeed(fix.getTimePoint());
|
||||
if (lastFix != null) {
|
||||
if (lastSpeed.compareTo(lastLastSpeed) > 0 && lastSpeed.compareTo(estimatedSpeedAtFix) > 0) {
|
||||
maxima.add(lastFix);
|
||||
} else if (lastSpeed.compareTo(lastLastSpeed) < 0 && lastSpeed.compareTo(estimatedSpeedAtFix) < 0) {
|
||||
minima.add(lastFix);
|
||||
if (estimatedSpeedAtFix.compareTo(minimumSpeed) < 0) {
|
||||
minimumSpeed = estimatedSpeedAtFix;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fix.getTimePoint().after(timePointAfterManeuver.plus(EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS))) {
|
||||
break;
|
||||
}
|
||||
fixes.add(fix);
|
||||
lastLastSpeed = lastSpeed;
|
||||
lastSpeed = estimatedSpeedAtFix;
|
||||
lastFix = fix;
|
||||
}
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
GPSFixMoving fixWithLowestSpeedOverGround = getBestFittingSpeedMinimumInManeuver(minima, minimumSpeed, maneuverTimePoint);
|
||||
if (fixWithLowestSpeedOverGround == null) {
|
||||
fixWithLowestSpeedOverGround = fixes.get(0);
|
||||
}
|
||||
// now remove all fixes before the last maximum before the fix with the lowest speed over ground during the maneuver if
|
||||
// there was such a maximum; otherwise, leave all fixes from the maneuver start on in place.
|
||||
final long MAX_SMOOTHENING_INTERVAL_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds();
|
||||
GPSFixMoving lastMaxSpeedFixBeforeLowSpeed = maxima.lower(fixWithLowestSpeedOverGround);
|
||||
// now check if there's a greater one that's only a little bit earlier
|
||||
if (lastMaxSpeedFixBeforeLowSpeed != null) {
|
||||
GPSFixMoving stillGreater;
|
||||
while ((stillGreater=maxima.lower(lastMaxSpeedFixBeforeLowSpeed)) != null &&
|
||||
track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo(
|
||||
track.getEstimatedSpeed(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) > 0 &&
|
||||
lastMaxSpeedFixBeforeLowSpeed.getTimePoint().asMillis()-
|
||||
stillGreater.getTimePoint().asMillis() < MAX_SMOOTHENING_INTERVAL_MILLIS) {
|
||||
lastMaxSpeedFixBeforeLowSpeed = stillGreater;
|
||||
}
|
||||
Iterator<GPSFixMoving> i = fixes.iterator();
|
||||
while (i.hasNext() && i.next().getTimePoint().before(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
// now remove all fixes after the first maximum after the global minimum:
|
||||
GPSFixMoving firstMaxSpeedFixAfterLowSpeed = maxima.higher(fixWithLowestSpeedOverGround);
|
||||
if (firstMaxSpeedFixAfterLowSpeed != null) {
|
||||
GPSFixMoving stillGreater;
|
||||
while ((stillGreater=maxima.higher(firstMaxSpeedFixAfterLowSpeed)) != null &&
|
||||
track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo(
|
||||
track.getEstimatedSpeed(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) > 0 &&
|
||||
stillGreater.getTimePoint().asMillis() - firstMaxSpeedFixAfterLowSpeed.getTimePoint().asMillis()
|
||||
< MAX_SMOOTHENING_INTERVAL_MILLIS) {
|
||||
firstMaxSpeedFixAfterLowSpeed = stillGreater;
|
||||
}
|
||||
ListIterator<GPSFixMoving> i = fixes.listIterator(fixes.size());
|
||||
while (i.hasPrevious() && i.previous().getTimePoint().after(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
return fixes;
|
||||
}
|
||||
|
||||
private GPSFixMoving getBestFittingSpeedMinimumInManeuver(NavigableSet<GPSFixMoving> minima, Speed minimumSpeed, TimePoint maneuverTimePoint) {
|
||||
// Idea: being the approximate maneuver duration away from maneuverTimePoint is as bad as having twice the speed.
|
||||
GPSFixMoving bestSpeedMinimum = null;
|
||||
double lowestBadness = Double.MAX_VALUE;
|
||||
final long approximateManeuverTimeInMillis = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds();
|
||||
final GPSFixTrack<Competitor, GPSFixMoving> track = getTrackedRace().getTrack(getCompetitor());
|
||||
for (GPSFixMoving minimum : minima) {
|
||||
// best speedBadness can be 1 which represents the absolute speed minimum in the interval considered
|
||||
double speedBadness = track.getEstimatedSpeed(minimum.getTimePoint()).getKnots() / minimumSpeed.getKnots();
|
||||
// best timePointBadness can be 1 which is a minimum exactly at the maneuver time point
|
||||
double timePointBadness = 1. + (double) Math.abs(minimum.getTimePoint().asMillis()-maneuverTimePoint.asMillis()) /
|
||||
(double) approximateManeuverTimeInMillis;
|
||||
final double totalBadness = speedBadness * timePointBadness;
|
||||
if (totalBadness < lowestBadness) {
|
||||
bestSpeedMinimum = minimum;
|
||||
lowestBadness = totalBadness;
|
||||
}
|
||||
}
|
||||
return bestSpeedMinimum;
|
||||
}
|
||||
|
||||
protected Speed projectSpeedToMiddleIfUpwindDownwindElseToLegDirection(TimePoint at,
|
||||
Bearing projectToForUpwindDownwind, final SpeedWithBearing speed, final Position position) throws NoWindException {
|
||||
Speed projectedSpeedAtFix = getTrackedLeg().isUpOrDownwindLeg(at) ?
|
||||
speed.projectTo(position, projectToForUpwindDownwind) :
|
||||
getWindwardSpeed(speed, at);
|
||||
return projectedSpeedAtFix;
|
||||
}
|
||||
|
||||
protected Bearing getProjectionAngleForUpwindDownwindManeuverLossAnalysis(TimePoint timePointBeforeManeuver,
|
||||
SpeedWithBearing speedBeforeManeuver, SpeedWithBearing speedAfterManeuver) {
|
||||
Bearing middle;
|
||||
middle = speedBeforeManeuver.getBearing().middle(speedAfterManeuver.getBearing());
|
||||
Wind wind = getTrackedRace().getWind(getTrackedRace().getTrack(getCompetitor()).
|
||||
getEstimatedPosition(timePointBeforeManeuver, /* extrapolate */ false), timePointBeforeManeuver);
|
||||
// If the average course in the maneuver deviates from the wind's
|
||||
final int MAX_DIFF_AVERAGE_COURSE_VERSUS_WIND_IN_DEGREES = 20;
|
||||
if (Math.abs(wind.getBearing().getDifferenceTo(middle).getDegrees()) > MAX_DIFF_AVERAGE_COURSE_VERSUS_WIND_IN_DEGREES &&
|
||||
Math.abs(wind.getFrom().getDifferenceTo(middle).getDegrees()) > MAX_DIFF_AVERAGE_COURSE_VERSUS_WIND_IN_DEGREES) {
|
||||
middle = wind.getBearing();
|
||||
}
|
||||
return middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1895,15 +1895,15 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
SpeedWithBearing speedWithBearingOnApproximationAtBeginning, List<Pair<GPSFixMoving, CourseChange>> group,
|
||||
SpeedWithBearing speedWithBearingOnApproximationAtEnd, double totalCourseChangeInDegrees,
|
||||
long totalMilliseconds) throws NoWindException {
|
||||
TimePoint maneuverTimePoint = new MillisecondsTimePoint(totalMilliseconds / group.size());
|
||||
Position maneuverPosition = getTrack(competitor)
|
||||
.getEstimatedPosition(maneuverTimePoint, /* extrapolate */false);
|
||||
MillisecondsTimePoint timePointBeforeManeuver = new MillisecondsTimePoint(group.get(0).getA().getTimePoint()
|
||||
.asMillis()
|
||||
- getApproximateManeuverDurationInMilliseconds() / 2);
|
||||
MillisecondsTimePoint timePointAfterManeuver = new MillisecondsTimePoint(group.get(group.size() - 1).getA()
|
||||
.getTimePoint().asMillis()
|
||||
+ getApproximateManeuverDurationInMilliseconds() / 2);
|
||||
TimePoint maneuverTimePoint = computeManeuverTimepoint(competitor, timePointBeforeManeuver, timePointAfterManeuver);
|
||||
Position maneuverPosition = getTrack(competitor)
|
||||
.getEstimatedPosition(maneuverTimePoint, /* extrapolate */false);
|
||||
Tack tackBeforeManeuver = getTack(maneuverPosition, timePointBeforeManeuver,
|
||||
speedWithBearingOnApproximationAtBeginning.getBearing());
|
||||
Tack tackAfterManeuver = getTack(maneuverPosition, timePointAfterManeuver,
|
||||
@@ -1922,7 +1922,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
} else if (Math.abs(totalCourseChangeInDegrees) > PENALTY_CIRCLE_DEGREES_THRESHOLD) {
|
||||
maneuverType = ManeuverType.PENALTY_CIRCLE;
|
||||
if (legBeforeManeuver != null) {
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, timePointAfterManeuver);
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, timePointAfterManeuver);
|
||||
}
|
||||
} else {
|
||||
if (tackBeforeManeuver != tackAfterManeuver) {
|
||||
@@ -1935,13 +1935,13 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
case UPWIND:
|
||||
maneuverType = ManeuverType.TACK;
|
||||
if (legBeforeManeuver != null) {
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, timePointAfterManeuver);
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, timePointAfterManeuver);
|
||||
}
|
||||
break;
|
||||
case DOWNWIND:
|
||||
maneuverType = ManeuverType.JIBE;
|
||||
if (legBeforeManeuver != null) {
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, timePointAfterManeuver);
|
||||
maneuverLoss = legBeforeManeuver.getManeuverLoss(timePointBeforeManeuver, maneuverTimePoint, timePointAfterManeuver);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -1977,6 +1977,41 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
return maneuver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the maneuver time point as the time point along between maneuver start and end where the competitor's track has greatest change
|
||||
* in course.
|
||||
*/
|
||||
private TimePoint computeManeuverTimepoint(Competitor competitor, MillisecondsTimePoint timePointBeforeManeuver,
|
||||
MillisecondsTimePoint timePointAfterManeuver) {
|
||||
TimePoint result = timePointBeforeManeuver;
|
||||
GPSFixTrack<Competitor, GPSFixMoving> track = getTrack(competitor);
|
||||
GPSFixMoving lastFix = null;
|
||||
double maxAngleSpeedInDegreesPerSecond = 0;
|
||||
track.lockForRead();
|
||||
try {
|
||||
for (Iterator<GPSFixMoving> i=track.getFixesIterator(timePointBeforeManeuver, /* inclusive */ true); i.hasNext(); ) {
|
||||
GPSFixMoving fix = i.next();
|
||||
if (fix.getTimePoint().after(timePointAfterManeuver)) {
|
||||
break;
|
||||
}
|
||||
if (lastFix != null) {
|
||||
Bearing courseAtLastFix = track.getEstimatedSpeed(lastFix.getTimePoint()).getBearing();
|
||||
Bearing courseAtFix = track.getEstimatedSpeed(fix.getTimePoint()).getBearing();
|
||||
double angleSpeedInDegreesPerSecond = Math.abs((courseAtFix.getDifferenceTo(courseAtLastFix).getDegrees()) /
|
||||
(double) (fix.getTimePoint().asMillis()-lastFix.getTimePoint().asMillis()));
|
||||
if (angleSpeedInDegreesPerSecond > maxAngleSpeedInDegreesPerSecond) {
|
||||
maxAngleSpeedInDegreesPerSecond = angleSpeedInDegreesPerSecond;
|
||||
result = lastFix.getTimePoint();
|
||||
}
|
||||
}
|
||||
lastFix = fix;
|
||||
}
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the boat class-specific parameter
|
||||
*/
|
||||
|
||||
@@ -24,8 +24,8 @@ public class ColorMap<T> {
|
||||
baseColors[0] = new HSVColor(0.0f, 1.0f, 1.0f); // Red
|
||||
baseColors[1] = new HSVColor(30.0f, 1.0f, 1.0f); // Orange
|
||||
baseColors[2] = new HSVColor(45.f, 1.0f, 1.0f);
|
||||
baseColors[3] = new HSVColor(120.0f, 1.0f, 1.0f); // Green
|
||||
baseColors[4] = new HSVColor(180.0f, 1.0f, 1.0f); // Cyan
|
||||
baseColors[3] = new HSVColor(120.0f, 1.0f, 0.8f); // Green
|
||||
baseColors[4] = new HSVColor(200.0f, 1.0f, 1.0f); // Cyan
|
||||
baseColors[5] = new HSVColor(240.0f, 1.0f, 1.0f); // Blue
|
||||
baseColors[6] = new HSVColor(270.0f, 1.0f, 1.0f); // Pink
|
||||
baseColors[7] = new HSVColor(285.0f, 1.0f, 1.0f);
|
||||
@@ -63,7 +63,7 @@ public class ColorMap<T> {
|
||||
int baseColorCount = baseColors.length;
|
||||
int baseColorsIndex = index % baseColorCount;
|
||||
int factor = index / (baseColorCount*3);
|
||||
float decreaseStepSize = 0.1f;
|
||||
float decreaseStepSize = 0.075f;
|
||||
|
||||
float brightnessDecrease = 0;
|
||||
float saturationDecrease = 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sap.sailing.gwt.ui.client;
|
||||
|
||||
import com.google.gwt.i18n.client.NumberFormat;
|
||||
import com.sap.sailing.domain.common.DetailType;
|
||||
|
||||
public class DetailTypeFormatter {
|
||||
@@ -76,6 +77,16 @@ public class DetailTypeFormatter {
|
||||
return stringMessages.averageJibeLossInMeters();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static NumberFormat getNumberFormat(DetailType detailType) {
|
||||
String decimalPlaces = "";
|
||||
for (int i = 0; i < detailType.getPrecision(); i++) {
|
||||
if (i == 0) {
|
||||
decimalPlaces += ".";
|
||||
}
|
||||
decimalPlaces += "0";
|
||||
}
|
||||
return NumberFormat.getFormat("0" + decimalPlaces);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,6 +884,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
|
||||
markDTO.color = mark.getColor();
|
||||
markDTO.shape = mark.getShape();
|
||||
markDTO.pattern = mark.getPattern();
|
||||
markDTO.type = mark.getType();
|
||||
return markDTO;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ package com.sap.sailing.gwt.ui.shared;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
|
||||
public class MarkDTO extends ControlPointDTO {
|
||||
public PositionDTO position;
|
||||
public String color;
|
||||
public String shape;
|
||||
public String pattern;
|
||||
public MarkType type;
|
||||
|
||||
MarkDTO() {}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.DetailType;
|
||||
import com.sap.sailing.domain.common.RaceIdentifier;
|
||||
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
import com.sap.sailing.domain.common.impl.Util.Pair;
|
||||
import com.sap.sailing.domain.common.impl.Util.Triple;
|
||||
import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor;
|
||||
@@ -57,8 +58,8 @@ import com.sap.sailing.gwt.ui.shared.MultiCompetitorRaceDataDTO;
|
||||
import com.sap.sailing.gwt.ui.shared.components.Component;
|
||||
|
||||
/**
|
||||
* ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward distance to
|
||||
* leader) for different races in a chart.
|
||||
* ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward
|
||||
* distance to leader) for different races in a chart.
|
||||
*
|
||||
* When calling the constructor a chart is created that creates a final amount of series (so the maximum number of
|
||||
* competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the
|
||||
@@ -68,8 +69,8 @@ import com.sap.sailing.gwt.ui.shared.components.Component;
|
||||
* @author Benjamin Ebling (D056866), Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractChartPanel<SettingsType extends ChartSettings> extends RaceChart
|
||||
implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
public abstract class AbstractChartPanel<SettingsType extends ChartSettings> extends RaceChart implements
|
||||
CompetitorSelectionChangeListener, RequiresResize {
|
||||
private static final int LINE_WIDTH = 1;
|
||||
private MultiCompetitorRaceDataDTO chartData;
|
||||
private boolean compactChart;
|
||||
@@ -78,22 +79,18 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
private final Map<CompetitorDTO, Series> markPassingSeriesByCompetitor;
|
||||
private final boolean allowTimeAdjust;
|
||||
private long stepSize = 5000;
|
||||
private DetailType dataToShow;
|
||||
private DetailType selectedDetailType;
|
||||
private final CompetitorSelectionProvider competitorSelectionProvider;
|
||||
|
||||
/**
|
||||
* When the chart is not visible and the competitor selection changes, no loading is performed. However, in this case
|
||||
* we need to remember that loading will be necessary as soon as the chart becomes visible.
|
||||
*/
|
||||
private boolean needToLoadWhenMadeVisible;
|
||||
|
||||
private Long timeOfEarliestRequestInMillis;
|
||||
private Long timeOfLatestRequestInMillis;
|
||||
|
||||
public AbstractChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor,
|
||||
CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider,
|
||||
Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, ErrorReporter errorReporter, DetailType dataToShow,
|
||||
boolean compactChart, boolean allowTimeAdjust) {
|
||||
Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages,
|
||||
ErrorReporter errorReporter, DetailType detailType, boolean compactChart, boolean allowTimeAdjust) {
|
||||
super(sailingService, timer, timeRangeWithZoomProvider, stringMessages, asyncActionsExecutor, errorReporter);
|
||||
this.competitorSelectionProvider = competitorSelectionProvider;
|
||||
this.dataToShow = dataToShow;
|
||||
this.competitorSelectionProvider = competitorSelectionProvider;
|
||||
this.compactChart = compactChart;
|
||||
this.allowTimeAdjust = allowTimeAdjust;
|
||||
dataSeriesByCompetitor = new HashMap<CompetitorDTO, Series>();
|
||||
@@ -103,44 +100,45 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
|
||||
noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + ".");
|
||||
noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart");
|
||||
|
||||
chart = createChart(dataToShow);
|
||||
|
||||
|
||||
chart = createChart();
|
||||
setSelectedDetailType(detailType);
|
||||
|
||||
competitorSelectionProvider.addCompetitorSelectionChangeListener(this);
|
||||
raceSelectionProvider.addRaceSelectionChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean visible) {
|
||||
super.setVisible(visible);
|
||||
if (needToLoadWhenMadeVisible) {
|
||||
loadData(/* showBusyIndicator */ true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new chart for the given {@link DetailType} <code>dataToShow</code> and also
|
||||
* clears the {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}.
|
||||
* @param dataToShow The detail type for the new chart.
|
||||
* Creates a new chart for the given {@link DetailType} <code>dataToShow</code> and also clears the
|
||||
* {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}.
|
||||
*
|
||||
* @param detailType
|
||||
* The detail type for the new chart.
|
||||
* @return A chart for the given detail Type
|
||||
*/
|
||||
private Chart createChart(DetailType dataToShow) {
|
||||
private Chart createChart() {
|
||||
Chart chart = new Chart().setZoomType(Chart.ZoomType.X)
|
||||
.setPersistent(true)
|
||||
.setWidth100()
|
||||
.setHeight100()
|
||||
.setMarginLeft(65)
|
||||
.setMarginRight(65)
|
||||
.setBorderColor(new Color("#A6A6A6"))
|
||||
.setBorderColor(new Color("#CACACA"))
|
||||
.setBackgroundColor(new Color("#EBEBEB"))
|
||||
.setBorderWidth(1)
|
||||
.setBorderRadius(0)
|
||||
.setPlotBorderWidth(0)
|
||||
.setCredits(new Credits().setEnabled(false))
|
||||
.setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn()))
|
||||
.setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker(new Marker().setEnabled(false).setHoverState(
|
||||
.setLinePlotOptions(
|
||||
new LinePlotOptions()
|
||||
.setLineWidth(LINE_WIDTH)
|
||||
.setMarker(
|
||||
new Marker().setEnabled(false).setHoverState(
|
||||
new Marker().setEnabled(true).setRadius(4))).setShadow(false)
|
||||
.setHoverStateLineWidth(LINE_WIDTH));
|
||||
ChartUtil.useCheckboxesToShowAndHide(chart);
|
||||
chart.setChartTitle(new ChartTitle().setText(DetailTypeFormatter.format(dataToShow, stringMessages)));
|
||||
|
||||
|
||||
if (allowTimeAdjust) {
|
||||
chart.setClickEventHandler(new ChartClickEventHandler() {
|
||||
@Override
|
||||
@@ -155,240 +153,199 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
}
|
||||
});
|
||||
}
|
||||
final String unit = getUnit();
|
||||
if (!compactChart) {
|
||||
chart.getYAxis().setAxisTitleText(DetailTypeFormatter.format(dataToShow, stringMessages) + " ["+unit+"]");
|
||||
} else {
|
||||
chart.getYAxis().setAxisTitleText("["+unit+"]");
|
||||
}
|
||||
|
||||
chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000); // 1 minute
|
||||
chart.getYAxis().setStartOnTick(false).setShowFirstLabel(false);
|
||||
chart.getYAxis().setReversed((dataToShow == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER ||
|
||||
dataToShow == DetailType.GAP_TO_LEADER_IN_SECONDS) ? true : false);
|
||||
chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000) // 1 minute
|
||||
.setAxisTitleText(stringMessages.time());
|
||||
chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() {
|
||||
@Override
|
||||
public String format(AxisLabelsData axisLabelsData) {
|
||||
return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong()));
|
||||
}
|
||||
}));
|
||||
|
||||
String decimalPlaces = "";
|
||||
for (int i = 0; i < dataToShow.getPrecision(); i++) {
|
||||
if (i == 0) {
|
||||
decimalPlaces += ".";
|
||||
}
|
||||
decimalPlaces += "0";
|
||||
}
|
||||
final NumberFormat numberFormat = NumberFormat.getFormat("0" + decimalPlaces);
|
||||
chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() {
|
||||
@Override
|
||||
public String format(ToolTipData toolTipData) {
|
||||
String seriesName = toolTipData.getSeriesName();
|
||||
|
||||
if (seriesName.equals(stringMessages.time())) {
|
||||
return "<b>" + seriesName + ":</b> " + dateFormat.format(new Date(toolTipData.getXAsLong()))
|
||||
+ "<br/>(" + stringMessages.clickChartToSetTime() + ")";
|
||||
} else {
|
||||
return "<b>" + seriesName
|
||||
+ (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "")
|
||||
+ "</b><br/>" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": "
|
||||
+ numberFormat.format(toolTipData.getYAsDouble()) + unit;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
if (compactChart) {
|
||||
chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2)
|
||||
.setOption("legend/margin", 2)
|
||||
.setOption("title/margin", 5)
|
||||
.setChartSubtitle(null)
|
||||
.getXAxis().setAxisTitleText(null);
|
||||
.setOption("legend/margin", 2).setOption("title/margin", 5).setChartSubtitle(null).getXAxis()
|
||||
.setAxisTitleText(null);
|
||||
}
|
||||
|
||||
setChartData(null);
|
||||
dataSeriesByCompetitor.clear();
|
||||
markPassingSeriesByCompetitor.clear();
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
protected abstract Component<SettingsType> getComponent();
|
||||
|
||||
|
||||
/**
|
||||
* Loads the needed data (data which isn't in the {@link #chartData cache}) for the {@link #getVisibleCompetitors()
|
||||
* visible competitors} via
|
||||
* Loads the needed data (data which isn't in the {@link #chartData cache}) for the
|
||||
* {@link #getSelectedCompetitors() visible competitors} via
|
||||
* {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After
|
||||
* loading is the method {@link #drawChartData()} called.<br />
|
||||
* If no data needs to be {@link #needsDataLoading() loaded}, the no competitors selected label is displayed.
|
||||
*
|
||||
* @param showBusyIndicator If <code>true</code> is the busy indicator shown while loading the data from the server.
|
||||
* @param showBusyIndicator
|
||||
* If <code>true</code> is the busy indicator shown while loading the data from the server.
|
||||
*/
|
||||
protected void loadData(boolean showBusyIndicator) {
|
||||
if (isVisible()) {
|
||||
if (hasVisibleCompetitors()) {
|
||||
needToLoadWhenMadeVisible = false; // we're loading it now
|
||||
if (showBusyIndicator) {
|
||||
setWidget(chart);
|
||||
showLoading(stringMessages.loadingCompetitorData());
|
||||
}
|
||||
if (chartData == null || chartData.getDetailType() != getDataToShow()) {
|
||||
chartData = new MultiCompetitorRaceDataDTO(getDataToShow());
|
||||
}
|
||||
|
||||
// Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis());
|
||||
final ArrayList<Pair<Date, CompetitorDTO>> dataQuery = new ArrayList<Pair<Date, CompetitorDTO>>();
|
||||
for (CompetitorDTO competitor : getVisibleCompetitors()) {
|
||||
Date chartDataDateOfNewestData = chartData.getDateOfNewestData();
|
||||
Date competitorDateOfNewestData = chartData.contains(competitor) ? chartData.getCompetitorData(
|
||||
competitor).getDateOfNewestData() : null;
|
||||
if (!chartData.contains(competitor)) {
|
||||
dataQuery.add(new Pair<Date, CompetitorDTO>(new Date(0), competitor));
|
||||
} else if (competitorDateOfNewestData.before(chartDataDateOfNewestData)
|
||||
|| competitorDateOfNewestData.before(timeRangeWithZoomProvider.getToTime())) {
|
||||
dataQuery.add(new Pair<Date, CompetitorDTO>(new Date(competitorDateOfNewestData.getTime()
|
||||
+ getStepSize()), competitor));
|
||||
}
|
||||
}
|
||||
|
||||
GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(
|
||||
sailingService, selectedRaceIdentifier, dataQuery, timeRangeWithZoomProvider.getToTime(), getStepSize(),
|
||||
getDataToShow(), new AsyncCallback<MultiCompetitorRaceDataDTO>() {
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
errorReporter.reportError(
|
||||
getStringMessages().failedToLoadRaceData() + ": " + caught.toString(),
|
||||
timer.getPlayMode() == PlayModes.Live);
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(MultiCompetitorRaceDataDTO result) {
|
||||
if (result != null) {
|
||||
for (CompetitorRaceDataDTO competitorData : result.getAllRaceData()) {
|
||||
if (chartData.contains(competitorData.getCompetitor())) {
|
||||
chartData.addCompetitorRaceData(competitorData);
|
||||
chartData.setCompetitorMarkPassingsData(competitorData);
|
||||
} else {
|
||||
chartData.setCompetitorData(competitorData.getCompetitor(), competitorData);
|
||||
}
|
||||
}
|
||||
}
|
||||
chart.getXAxis().setMin(timeRangeWithZoomProvider.getFromTime().getTime());
|
||||
chart.getXAxis().setMax(timeRangeWithZoomProvider.getToTime().getTime());
|
||||
|
||||
drawChartData();
|
||||
chart.redraw();
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
asyncActionsExecutor.execute(getCompetitorsRaceDataAction);
|
||||
} else {
|
||||
setWidget(noCompetitorsSelectedLabel);
|
||||
private void updateChart(Date from, Date to, boolean append, boolean showBusyIndicator) {
|
||||
if (hasSelectedCompetitors()) {
|
||||
setWidget(chart);
|
||||
if (showBusyIndicator) {
|
||||
showLoading(stringMessages.loadingCompetitorData());
|
||||
}
|
||||
loadData(from, to, append);
|
||||
} else {
|
||||
// not visible; don't load data but remember that data needs to be loaded as soon as the chart becomes visible
|
||||
needToLoadWhenMadeVisible = true;
|
||||
setWidget(noCompetitorsSelectedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData(Date from, Date to, boolean append) {
|
||||
boolean hasDetailTypeChanged = chartData == null || chartData.getDetailType() != selectedDetailType;
|
||||
|
||||
if (hasDetailTypeChanged) {
|
||||
chartData = new MultiCompetitorRaceDataDTO(selectedDetailType);
|
||||
}
|
||||
|
||||
// Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis());
|
||||
final ArrayList<Pair<Date, CompetitorDTO>> dataQuery = new ArrayList<Pair<Date, CompetitorDTO>>();
|
||||
for (CompetitorDTO competitor : getSelectedCompetitors()) {
|
||||
Date chartDataDateOfNewestData = chartData.getDateOfNewestData();
|
||||
Date competitorDateOfNewestData = chartData.contains(competitor) ? chartData.getCompetitorData(competitor)
|
||||
.getDateOfNewestData() : null;
|
||||
if (!chartData.contains(competitor)) {
|
||||
dataQuery.add(new Pair<Date, CompetitorDTO>(new Date(0), competitor));
|
||||
} else if (competitorDateOfNewestData.before(chartDataDateOfNewestData)
|
||||
|| competitorDateOfNewestData.before(timeRangeWithZoomProvider.getToTime())) {
|
||||
dataQuery.add(new Pair<Date, CompetitorDTO>(new Date(competitorDateOfNewestData.getTime()
|
||||
+ getStepSize()), competitor));
|
||||
}
|
||||
}
|
||||
|
||||
GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService,
|
||||
selectedRaceIdentifier, dataQuery, timeRangeWithZoomProvider.getToTime(), getStepSize(),
|
||||
getSelectedDetailType(), new AsyncCallback<MultiCompetitorRaceDataDTO>() {
|
||||
@Override
|
||||
public void onSuccess(MultiCompetitorRaceDataDTO result) {
|
||||
if (result != null) {
|
||||
for (CompetitorRaceDataDTO competitorData : result.getAllRaceData()) {
|
||||
if (chartData.contains(competitorData.getCompetitor())) {
|
||||
chartData.addCompetitorRaceData(competitorData);
|
||||
chartData.setCompetitorMarkPassingsData(competitorData);
|
||||
} else {
|
||||
chartData.setCompetitorData(competitorData.getCompetitor(), competitorData);
|
||||
}
|
||||
}
|
||||
}
|
||||
hideLoading();
|
||||
|
||||
drawChartData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
errorReporter.reportError(
|
||||
getStringMessages().failedToLoadRaceData() + ": " + caught.toString(),
|
||||
timer.getPlayMode() == PlayModes.Live);
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
asyncActionsExecutor.execute(getCompetitorsRaceDataAction);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addedToSelection(CompetitorDTO competitor) {
|
||||
loadData(true);
|
||||
if (isVisible()) {
|
||||
timeChanged(timer.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void removedFromSelection(CompetitorDTO competitor) {
|
||||
Series competitorSeries = getCompetitorSeries(competitor);
|
||||
Series competitorSeries = dataSeriesByCompetitor.get(competitor);
|
||||
if (competitorSeries != null) {
|
||||
chart.removeSeries(competitorSeries);
|
||||
chart.removeSeries(competitorSeries, isVisible());
|
||||
}
|
||||
Series competitorMarkPassingSeries = getCompetitorMarkPassingSeries(competitor);
|
||||
Series competitorMarkPassingSeries = markPassingSeriesByCompetitor.get(competitor);
|
||||
if (competitorMarkPassingSeries != null) {
|
||||
chart.removeSeries(competitorMarkPassingSeries);
|
||||
chart.removeSeries(competitorMarkPassingSeries, isVisible());
|
||||
}
|
||||
if (!hasVisibleCompetitors()) {
|
||||
|
||||
if (isVisible() && !hasSelectedCompetitors()) {
|
||||
setWidget(noCompetitorsSelectedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the series for all selected competitors if these aren't created yet.<br />
|
||||
* Fills the series for the selected competitors with the data in {@link AbstractChartPanel#chartData}.<br />
|
||||
* Removes series of competitors, which aren't selected and adds series for competitors, which are newly selected.<br />
|
||||
* Also {@link #forceTimeLineUpdate() updates} the {@link #timeLineSeries} if {@link #timeLineNeedsUpdate} is <code>true</code>.<br /><br />
|
||||
* Also {@link #forceTimeLineUpdate() updates} the {@link #timeLineSeries} if {@link #timeLineNeedsUpdate} is
|
||||
* <code>true</code>.<br />
|
||||
* <br />
|
||||
*
|
||||
* The data for all {@link #getVisibleCompetitors() visible competitors} needs to be filled before calling this method.
|
||||
* The data for all {@link #getSelectedCompetitors() visible competitors} needs to be filled before calling this
|
||||
* method.
|
||||
*/
|
||||
private synchronized void drawChartData() {
|
||||
//Make sure the busy indicator is removed at this point, or plotting the data results in an exception
|
||||
// Make sure the busy indicator is removed at this point, or plotting the data results in an exception
|
||||
setWidget(chart);
|
||||
Iterable<CompetitorDTO> competitors = competitorSelectionProvider.getAllCompetitors();
|
||||
if (chartData != null && competitors != null && competitors.iterator().hasNext()) {
|
||||
|
||||
if (chartData != null) {
|
||||
List<Series> chartSeries = Arrays.asList(chart.getSeries());
|
||||
for (CompetitorDTO competitor : competitors) {
|
||||
Series compSeries = getCompetitorSeries(competitor);
|
||||
Series markSeries = getCompetitorMarkPassingSeries(competitor);
|
||||
if (isCompetitorVisible(competitor)) {
|
||||
CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor);
|
||||
if (competitorData != null) {
|
||||
Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis());
|
||||
|
||||
List<Triple<String, Date, Double>> markPassingsData = competitorData.getMarkPassingsData();
|
||||
List<Point> markPassingPoints = new ArrayList<Point>();
|
||||
for (Triple<String, Date, Double> markPassingData : markPassingsData) {
|
||||
if (markPassingData.getB() != null && markPassingData.getC() != null) {
|
||||
if (markPassingData.getB().before(toDate)) {
|
||||
Point markPassingPoint = new Point(markPassingData.getB().getTime(),
|
||||
markPassingData.getC());
|
||||
markPassingPoint.setName(markPassingData.getA());
|
||||
markPassingPoints.add(markPassingPoint);
|
||||
}
|
||||
for (CompetitorDTO competitor : competitorSelectionProvider.getSelectedCompetitors()) {
|
||||
Series competitorDataSeries = getOrCreateCompetitorDataSeries(competitor);
|
||||
Series markPassingSeries = getOrCreateCompetitorMarkPassingSeries(competitor);
|
||||
CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor);
|
||||
if (competitorData != null) {
|
||||
Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis());
|
||||
|
||||
List<Triple<String, Date, Double>> markPassingsData = competitorData.getMarkPassingsData();
|
||||
List<Point> markPassingPoints = new ArrayList<Point>();
|
||||
for (Triple<String, Date, Double> markPassingData : markPassingsData) {
|
||||
if (markPassingData.getB() != null && markPassingData.getC() != null) {
|
||||
if (markPassingData.getB().before(toDate)) {
|
||||
Point markPassingPoint = new Point(markPassingData.getB().getTime(),
|
||||
markPassingData.getC());
|
||||
markPassingPoint.setName(markPassingData.getA());
|
||||
markPassingPoints.add(markPassingPoint);
|
||||
}
|
||||
}
|
||||
markSeries.setPoints(markPassingPoints.toArray(new Point[0]));
|
||||
|
||||
Point[] compSeriesPoints = compSeries.getPoints();
|
||||
Date dateOfNewestSeriesPoint = compSeriesPoints.length == 0 ? null :
|
||||
new Date(compSeriesPoints[compSeriesPoints.length - 1].getX().longValue());
|
||||
List<Pair<Date, Double>> raceData = dateOfNewestSeriesPoint == null ? competitorData
|
||||
.getRaceData() : competitorData.getRaceDataAfterDate(dateOfNewestSeriesPoint);
|
||||
for (Pair<Date, Double> data : raceData) {
|
||||
if (data.getA() != null && data.getB() != null) {
|
||||
if (data.getA().before(toDate)) {
|
||||
Point competitorPoint = new Point(data.getA().getTime(), data.getB());
|
||||
compSeries.addPoint(competitorPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Adding the series if chart doesn't contain it
|
||||
if (!chartSeries.contains(compSeries)) {
|
||||
chart.addSeries(compSeries);
|
||||
chart.addSeries(markSeries);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Removing the series if chart contains it
|
||||
if (chartSeries.contains(compSeries)) {
|
||||
chart.removeSeries(compSeries);
|
||||
chart.removeSeries(markSeries);
|
||||
markPassingSeries.setPoints(markPassingPoints.toArray(new Point[0]));
|
||||
|
||||
Point[] compSeriesPoints = competitorDataSeries.getPoints();
|
||||
Date dateOfNewestSeriesPoint = compSeriesPoints.length == 0 ? null : new Date(
|
||||
compSeriesPoints[compSeriesPoints.length - 1].getX().longValue());
|
||||
List<Pair<Date, Double>> raceData = dateOfNewestSeriesPoint == null ? competitorData.getRaceData()
|
||||
: competitorData.getRaceDataAfterDate(dateOfNewestSeriesPoint);
|
||||
for (Pair<Date, Double> data : raceData) {
|
||||
if (data.getA() != null && data.getB() != null) {
|
||||
if (data.getA().before(toDate)) {
|
||||
Point competitorPoint = new Point(data.getA().getTime(), data.getB());
|
||||
competitorDataSeries.addPoint(competitorPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding the series if chart doesn't contain it
|
||||
if (!chartSeries.contains(competitorDataSeries)) {
|
||||
chart.addSeries(competitorDataSeries);
|
||||
chart.addSeries(markPassingSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isZoomed) {
|
||||
chart.getXAxis().setMin(timeRangeWithZoomProvider.getFromTime().getTime());
|
||||
chart.getXAxis().setMax(timeRangeWithZoomProvider.getToTime().getTime());
|
||||
}
|
||||
chart.redraw();
|
||||
}
|
||||
|
||||
private Iterable<CompetitorDTO> getVisibleCompetitors() {
|
||||
private Iterable<CompetitorDTO> getSelectedCompetitors() {
|
||||
return competitorSelectionProvider.getSelectedCompetitors();
|
||||
}
|
||||
|
||||
private boolean hasVisibleCompetitors() {
|
||||
return getVisibleCompetitors().iterator().hasNext();
|
||||
}
|
||||
|
||||
private boolean isCompetitorVisible(CompetitorDTO competitor) {
|
||||
return competitorSelectionProvider.isSelected(competitor);
|
||||
private boolean hasSelectedCompetitors() {
|
||||
return Util.size(getSelectedCompetitors()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,56 +353,69 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
* @param competitor
|
||||
* @return A series in the chart, that can be used to show the data of a specific competitor.
|
||||
*/
|
||||
private Series getCompetitorSeries(final CompetitorDTO competitor){
|
||||
private Series getOrCreateCompetitorDataSeries(final CompetitorDTO competitor) {
|
||||
Series result = dataSeriesByCompetitor.get(competitor);
|
||||
if (result == null) {
|
||||
result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name);
|
||||
if (result == null) {
|
||||
result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name);
|
||||
result.setPlotOptions(new LinePlotOptions()
|
||||
.setLineWidth(LINE_WIDTH)
|
||||
.setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4)))
|
||||
.setShadow(false).setHoverStateLineWidth(LINE_WIDTH)
|
||||
.setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true));
|
||||
dataSeriesByCompetitor.put(competitor, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getUnit() {
|
||||
String unit = "";
|
||||
switch (getDataToShow()) {
|
||||
private String getDetailTypeUnit() {
|
||||
String detailTypeUnit = "";
|
||||
switch (getSelectedDetailType()) {
|
||||
case CURRENT_SPEED_OVER_GROUND_IN_KNOTS:
|
||||
unit = getStringMessages().currentSpeedOverGroundInKnotsUnit();
|
||||
detailTypeUnit = getStringMessages().currentSpeedOverGroundInKnotsUnit();
|
||||
break;
|
||||
case DISTANCE_TRAVELED:
|
||||
unit = getStringMessages().distanceInMetersUnit();
|
||||
detailTypeUnit = getStringMessages().distanceInMetersUnit();
|
||||
break;
|
||||
case GAP_TO_LEADER_IN_SECONDS:
|
||||
unit = getStringMessages().gapToLeaderInSecondsUnit();
|
||||
detailTypeUnit = getStringMessages().gapToLeaderInSecondsUnit();
|
||||
break;
|
||||
case VELOCITY_MADE_GOOD_IN_KNOTS:
|
||||
unit = getStringMessages().velocityMadeGoodInKnotsUnit();
|
||||
detailTypeUnit = getStringMessages().velocityMadeGoodInKnotsUnit();
|
||||
break;
|
||||
case WINDWARD_DISTANCE_TO_OVERALL_LEADER:
|
||||
unit = getStringMessages().windwardDistanceToGoInMetersUnit();
|
||||
detailTypeUnit = getStringMessages().windwardDistanceToGoInMetersUnit();
|
||||
}
|
||||
return unit;
|
||||
return detailTypeUnit;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param competitor
|
||||
* @return A series in the chart, that can be used to show the mark passings.
|
||||
*/
|
||||
private Series getCompetitorMarkPassingSeries(CompetitorDTO competitor){
|
||||
private Series getOrCreateCompetitorMarkPassingSeries(CompetitorDTO competitor) {
|
||||
Series result = markPassingSeriesByCompetitor.get(competitor);
|
||||
if (result == null) {
|
||||
result = chart.createSeries().setType(Series.Type.SCATTER).setName(stringMessages.markPassing()+" "+competitor.name);
|
||||
result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true));
|
||||
result = chart.createSeries().setType(Series.Type.SCATTER)
|
||||
.setName(stringMessages.markPassing() + " " + competitor.name);
|
||||
result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor))
|
||||
.setSelected(true));
|
||||
markPassingSeriesByCompetitor.put(competitor, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean visible) {
|
||||
super.setVisible(visible);
|
||||
if(visible) {
|
||||
// Workaround for a highcharts bug:
|
||||
// Set a chart title, overwrite the title, switch chart to invisible and visible again -> the old title appears
|
||||
chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)),
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the whole chart and empties cached data.
|
||||
*/
|
||||
@@ -457,7 +427,7 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
}
|
||||
|
||||
public String getLocalizedShortName() {
|
||||
return DetailTypeFormatter.format(getDataToShow(), getStringMessages());
|
||||
return DetailTypeFormatter.format(getSelectedDetailType(), getStringMessages());
|
||||
}
|
||||
|
||||
public Widget getEntryWidget() {
|
||||
@@ -467,14 +437,15 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
public boolean hasSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public ChartSettings getAbstractSettings() {
|
||||
return new ChartSettings(getStepSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided
|
||||
* by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #loadData(boolean)}, if this method returns <code>true</code>;
|
||||
* by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #updateChart(boolean)}, if
|
||||
* this method returns <code>true</code>;
|
||||
*
|
||||
* @return <code>true</code> if the settings had been changed and a clearing and loading is needed.
|
||||
*/
|
||||
@@ -498,38 +469,68 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
protected void setStepSize(long stepSize) {
|
||||
this.stepSize = stepSize;
|
||||
}
|
||||
|
||||
protected DetailType getDataToShow() {
|
||||
return this.dataToShow;
|
||||
|
||||
protected DetailType getSelectedDetailType() {
|
||||
return this.selectedDetailType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the {@link #dataToShow} field, creates a new chart for the new <code>dataToShow</code> and clears the {@link #chartData}.<br />
|
||||
* Doesn't {@link #loadData(boolean) load the data}.
|
||||
* Updates the {@link #selectedDetailType} field, clears the chart for the new <code>selectedDetailType</code> and
|
||||
* clears the {@link #chartData}.<br />
|
||||
* Doesn't {@link #updateChart(boolean) load the data}.
|
||||
*
|
||||
* @return <code>true</code> if the data to show changed
|
||||
* @return <code>true</code> if the detail type changed
|
||||
*/
|
||||
protected boolean setDataToShow(DetailType dataToShow) {
|
||||
if (dataToShow != this.dataToShow) {
|
||||
this.dataToShow = dataToShow;
|
||||
setChartData(null);
|
||||
chart = createChart(dataToShow);
|
||||
setWidget(chart);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
protected boolean setSelectedDetailType(DetailType newSelectedDetailType) {
|
||||
boolean hasDetailTypeChanged = newSelectedDetailType != this.selectedDetailType;
|
||||
if (hasDetailTypeChanged) {
|
||||
this.selectedDetailType = newSelectedDetailType;
|
||||
clearChart();
|
||||
|
||||
chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)),
|
||||
null);
|
||||
|
||||
final String unit = getDetailTypeUnit();
|
||||
if (!compactChart) {
|
||||
chart.getYAxis().setAxisTitleText(
|
||||
DetailTypeFormatter.format(selectedDetailType, stringMessages) + " [" + unit + "]");
|
||||
} else {
|
||||
chart.getYAxis().setAxisTitleText("[" + unit + "]");
|
||||
}
|
||||
chart.getYAxis()
|
||||
.setReversed(
|
||||
(selectedDetailType == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || selectedDetailType == DetailType.GAP_TO_LEADER_IN_SECONDS) ? true
|
||||
: false);
|
||||
|
||||
final NumberFormat numberFormat = DetailTypeFormatter.getNumberFormat(selectedDetailType);
|
||||
chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() {
|
||||
@Override
|
||||
public String format(ToolTipData toolTipData) {
|
||||
String seriesName = toolTipData.getSeriesName();
|
||||
|
||||
if (seriesName.equals(stringMessages.time())) {
|
||||
return "<b>" + seriesName + ":</b> " + dateFormat.format(new Date(toolTipData.getXAsLong()))
|
||||
+ "<br/>(" + stringMessages.clickChartToSetTime() + ")";
|
||||
} else {
|
||||
return "<b>" + seriesName
|
||||
+ (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "")
|
||||
+ "</b><br/>" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": "
|
||||
+ numberFormat.format(toolTipData.getYAsDouble()) + " " + unit;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
return hasDetailTypeChanged;
|
||||
}
|
||||
|
||||
|
||||
protected MultiCompetitorRaceDataDTO getChartData() {
|
||||
return chartData;
|
||||
}
|
||||
|
||||
|
||||
protected void setChartData(MultiCompetitorRaceDataDTO chartData) {
|
||||
this.chartData = chartData;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRaceSelectionChange(List<RegattaAndRaceIdentifier> selectedRaces) {
|
||||
if (selectedRaces != null && !selectedRaces.isEmpty()) {
|
||||
@@ -539,8 +540,8 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
}
|
||||
|
||||
clearChart();
|
||||
if(selectedRaceIdentifier != null) {
|
||||
loadData(true);
|
||||
if (selectedRaceIdentifier != null) {
|
||||
timeChanged(timer.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,28 +581,56 @@ implements CompetitorSelectionChangeListener, RequiresResize {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new Pair<Boolean, Boolean>(everyPassingInRange, twoPassingsInRangeBeforeError);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void timeChanged(Date date) {
|
||||
if (getChartData() != null) {
|
||||
Date newestEvent = getChartData().getOldestDateOfNewestData();
|
||||
if ((newestEvent == null || (newestEvent.before(date) && (date.getTime() - newestEvent.getTime()) >= getStepSize()))) {
|
||||
loadData(false);
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (timer.getPlayMode()) {
|
||||
case Live: {
|
||||
boolean showBusyIndicator = false;
|
||||
// is date before first cache entry or is cache empty?
|
||||
if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) {
|
||||
updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true, showBusyIndicator);
|
||||
} else if (date.getTime() > timeOfLatestRequestInMillis) {
|
||||
updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */
|
||||
true, showBusyIndicator);
|
||||
}
|
||||
// otherwise the cache spans across date and so we don't need to load anything
|
||||
break;
|
||||
}
|
||||
case Replay: {
|
||||
boolean showBusyIndicator = true;
|
||||
if (timeOfLatestRequestInMillis == null) {
|
||||
// pure replay mode
|
||||
updateChart(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */
|
||||
false, showBusyIndicator);
|
||||
} else {
|
||||
// replay mode during live play
|
||||
if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) {
|
||||
updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true, showBusyIndicator);
|
||||
} else if (date.getTime() > timeOfLatestRequestInMillis) {
|
||||
updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */
|
||||
true, showBusyIndicator);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResize() {
|
||||
if (getChartData() != null) {
|
||||
chart.setSizeToMatchContainer();
|
||||
// it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions (nativeAdjustCheckboxPosition)
|
||||
// in the BaseChart class would not be called
|
||||
chart.redraw();
|
||||
}
|
||||
chart.setSizeToMatchContainer();
|
||||
// it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions
|
||||
// (nativeAdjustCheckboxPosition)
|
||||
// in the BaseChart class would not be called
|
||||
chart.redraw();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,16 +4,16 @@ import com.google.gwt.user.client.ui.DoubleBox;
|
||||
import com.google.gwt.user.client.ui.FocusWidget;
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.gwt.ui.client.DataEntryDialog;
|
||||
import com.sap.sailing.gwt.ui.client.DataEntryDialog.Validator;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent;
|
||||
|
||||
public abstract class AbstractChartSettingsComponent<SettingsType extends ChartSettings> implements SettingsDialogComponent<SettingsType> {
|
||||
|
||||
private DoubleBox stepSizeBox;
|
||||
private final ChartSettings settings;
|
||||
private final StringMessages stringMessages;
|
||||
protected DoubleBox stepSizeBox;
|
||||
protected final StringMessages stringMessages;
|
||||
|
||||
public AbstractChartSettingsComponent(ChartSettings settings, StringMessages stringMessages) {
|
||||
this.settings = settings;
|
||||
@@ -21,18 +21,14 @@ public abstract class AbstractChartSettingsComponent<SettingsType extends ChartS
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerticalPanel getAdditionalWidget(DataEntryDialog<?> dialog) {
|
||||
VerticalPanel panel = new VerticalPanel();
|
||||
panel.add(new Label(getStringMessages().stepSizeInSeconds()));
|
||||
public Widget getAdditionalWidget(DataEntryDialog<?> dialog) {
|
||||
VerticalPanel mainPanel = new VerticalPanel();
|
||||
mainPanel.add(new Label(stringMessages.stepSizeInSeconds()));
|
||||
stepSizeBox = dialog.createDoubleBox(((double) settings.getStepSize()) / 1000, 5);
|
||||
panel.add(stepSizeBox);
|
||||
return panel;
|
||||
mainPanel.add(stepSizeBox);
|
||||
return mainPanel;
|
||||
}
|
||||
|
||||
protected DoubleBox getStepSizeBox() {
|
||||
return stepSizeBox;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Validator<SettingsType> getValidator() {
|
||||
return new Validator<SettingsType>() {
|
||||
@@ -40,7 +36,7 @@ public abstract class AbstractChartSettingsComponent<SettingsType extends ChartS
|
||||
public String getErrorMessage(SettingsType valueToValidate) {
|
||||
String errorMessage = null;
|
||||
if (valueToValidate.getStepSize() < 1) {
|
||||
errorMessage = getStringMessages().stepSizeMustBeGreaterThanNull();
|
||||
errorMessage = stringMessages.stepSizeMustBeGreaterThanNull();
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
@@ -52,14 +48,14 @@ public abstract class AbstractChartSettingsComponent<SettingsType extends ChartS
|
||||
return stepSizeBox;
|
||||
}
|
||||
|
||||
protected StringMessages getStringMessages() {
|
||||
return stringMessages;
|
||||
}
|
||||
|
||||
public ChartSettings getAbstractResult() {
|
||||
Double valueInSeconds = getStepSizeBox().getValue();
|
||||
Long value = valueInSeconds == null ? 0 : (long) (getStepSizeBox().getValue() * 1000);
|
||||
Double valueInSeconds = stepSizeBox.getValue();
|
||||
Long value = valueInSeconds == null ? 0 : (long) (stepSizeBox.getValue() * 1000);
|
||||
return new ChartSettings(value);
|
||||
}
|
||||
|
||||
public ChartSettings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ChartPanel extends AbstractChartPanel<ChartSettings> implements Com
|
||||
public void updateSettings(ChartSettings newSettings) {
|
||||
updateSettingsOnly(newSettings);
|
||||
clearChart();
|
||||
loadData(true);
|
||||
timeChanged(timer.getTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -71,8 +71,11 @@ public class LeaderboardRankChart extends SimplePanel implements RequiresResize,
|
||||
.setMarginRight(65)
|
||||
.setWidth100()
|
||||
.setHeight100()
|
||||
.setBorderColor(new Color("#A6A6A6"))
|
||||
.setBorderColor(new Color("#CACACA"))
|
||||
.setBorderWidth(1)
|
||||
.setBorderRadius(0)
|
||||
.setBackgroundColor(new Color("#EBEBEB"))
|
||||
.setPlotBorderWidth(0)
|
||||
.setCredits(new Credits().setEnabled(false))
|
||||
.setChartTitle(new ChartTitle().setText(stringMessages.rank()))
|
||||
.setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker(
|
||||
|
||||
@@ -17,8 +17,8 @@ import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent;
|
||||
* leader) for different races in a chart. The chart type can be selected from the settings.
|
||||
*
|
||||
* When calling the constructor a chart is created that creates a final amount of series (so the maximum number of
|
||||
* competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the
|
||||
* data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the
|
||||
* competitors cannot be changed in one chart) which are connected to competitors, when the SailingService returns the
|
||||
* data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if you know for example the
|
||||
* seriesID-index, you can get the competitor by calling competitorID.get(index).
|
||||
*
|
||||
* @author Benjamin Ebling (D056866), Axel Uhl (d043530)
|
||||
@@ -35,16 +35,15 @@ public class MultiChartPanel extends AbstractChartPanel<MultiChartSettings> impl
|
||||
|
||||
@Override
|
||||
public SettingsDialogComponent<MultiChartSettings> getSettingsDialogComponent() {
|
||||
return new MultiChartSettingsComponent(new MultiChartSettings(getAbstractSettings(), getDataToShow()), getStringMessages());
|
||||
return new MultiChartSettingsComponent(new MultiChartSettings(getAbstractSettings(), getSelectedDetailType()), getStringMessages());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSettings(MultiChartSettings newSettings) {
|
||||
boolean settingsChanged = updateSettingsOnly(newSettings);
|
||||
boolean dataToShowChanged = setDataToShow(newSettings.getDataToShow());
|
||||
if (dataToShowChanged || settingsChanged) {
|
||||
clearChart();
|
||||
loadData(true);
|
||||
boolean selectedDetailTypeChanged = setSelectedDetailType(newSettings.getDetailType());
|
||||
if (selectedDetailTypeChanged || settingsChanged) {
|
||||
timeChanged(timer.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package com.sap.sailing.gwt.ui.shared.charts;
|
||||
import com.sap.sailing.domain.common.DetailType;
|
||||
|
||||
public class MultiChartSettings extends ChartSettings {
|
||||
private final DetailType dataToShow;
|
||||
|
||||
public MultiChartSettings(ChartSettings superResult, DetailType dataToShow) {
|
||||
private final DetailType detailType;
|
||||
|
||||
public MultiChartSettings(ChartSettings superResult, DetailType detailType) {
|
||||
super(superResult);
|
||||
this.dataToShow = dataToShow;
|
||||
this.detailType = detailType;
|
||||
}
|
||||
|
||||
public DetailType getDataToShow() {
|
||||
return dataToShow;
|
||||
public DetailType getDetailType() {
|
||||
return detailType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.sap.sailing.gwt.ui.shared.charts;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.ListBox;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.DetailType;
|
||||
import com.sap.sailing.gwt.ui.client.DataEntryDialog;
|
||||
import com.sap.sailing.gwt.ui.client.DetailTypeFormatter;
|
||||
@@ -11,44 +15,60 @@ import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent;
|
||||
|
||||
public class MultiChartSettingsComponent extends AbstractChartSettingsComponent<MultiChartSettings> implements
|
||||
SettingsDialogComponent<MultiChartSettings> {
|
||||
private ListBox dataSelection;
|
||||
private final DetailType initialDataToShow;
|
||||
private ListBox chartTypeSelectionListBox;
|
||||
private final DetailType initialDetailType;
|
||||
private final List<DetailType> availableDetailsTypes;
|
||||
|
||||
public MultiChartSettingsComponent(MultiChartSettings settings, StringMessages stringMessages) {
|
||||
super(settings, stringMessages);
|
||||
this.initialDataToShow = settings.getDataToShow();
|
||||
this.initialDetailType = settings.getDetailType();
|
||||
|
||||
availableDetailsTypes = new ArrayList<DetailType>();
|
||||
availableDetailsTypes.add(DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER);
|
||||
availableDetailsTypes.add(DetailType.DISTANCE_TRAVELED);
|
||||
availableDetailsTypes.add(DetailType.VELOCITY_MADE_GOOD_IN_KNOTS);
|
||||
availableDetailsTypes.add(DetailType.GAP_TO_LEADER_IN_SECONDS);
|
||||
availableDetailsTypes.add(DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS);
|
||||
availableDetailsTypes.add(DetailType.RACE_RANK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerticalPanel getAdditionalWidget(DataEntryDialog<?> dialog) {
|
||||
VerticalPanel configPanel = super.getAdditionalWidget(dialog);
|
||||
configPanel.setSpacing(5);
|
||||
Label lblChart = new Label(getStringMessages().chooseChart());
|
||||
configPanel.add(lblChart);
|
||||
dataSelection = dialog.createListBox(/* isMultiSelect */ false);
|
||||
public Widget getAdditionalWidget(DataEntryDialog<?> dialog) {
|
||||
VerticalPanel mainPanel = new VerticalPanel();
|
||||
mainPanel.setSpacing(5);
|
||||
|
||||
Label chartSelectionLabel = new Label(stringMessages.chooseChart());
|
||||
mainPanel.add(chartSelectionLabel);
|
||||
chartTypeSelectionListBox = dialog.createListBox(/* isMultiSelect */ false);
|
||||
int i=0;
|
||||
for (DetailType detailType : new DetailType[] { DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER, DetailType.DISTANCE_TRAVELED,
|
||||
DetailType.VELOCITY_MADE_GOOD_IN_KNOTS, DetailType.GAP_TO_LEADER_IN_SECONDS, DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS,
|
||||
DetailType.RACE_RANK }) {
|
||||
dataSelection.addItem(DetailTypeFormatter.format(detailType, getStringMessages()), detailType.toString());
|
||||
if (detailType == initialDataToShow) {
|
||||
dataSelection.setSelectedIndex(i);
|
||||
for (DetailType detailType : availableDetailsTypes) {
|
||||
chartTypeSelectionListBox.addItem(DetailTypeFormatter.format(detailType, stringMessages), detailType.name());
|
||||
if (detailType == initialDetailType) {
|
||||
chartTypeSelectionListBox.setSelectedIndex(i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
configPanel.add(dataSelection);
|
||||
return configPanel;
|
||||
mainPanel.add(chartTypeSelectionListBox);
|
||||
|
||||
mainPanel.add(new Label(stringMessages.stepSizeInSeconds()));
|
||||
stepSizeBox = dialog.createDoubleBox(((double) getSettings().getStepSize()) / 1000, 5);
|
||||
mainPanel.add(stepSizeBox);
|
||||
|
||||
return mainPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiChartSettings getResult() {
|
||||
DetailType dataToShow = null;
|
||||
for (DetailType dt : DetailType.values()){
|
||||
if (dt.toString().equals(dataSelection.getValue(dataSelection.getSelectedIndex()))){
|
||||
dataToShow = dt;
|
||||
DetailType newDetailType = null;
|
||||
int selectedIndex = chartTypeSelectionListBox.getSelectedIndex();
|
||||
String selectedDetailType = chartTypeSelectionListBox.getValue(selectedIndex);
|
||||
for (DetailType detailType : availableDetailsTypes){
|
||||
if (detailType.name().equals(selectedDetailType)){
|
||||
newDetailType = detailType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new MultiChartSettings(getAbstractResult(), dataToShow);
|
||||
return new MultiChartSettings(getAbstractResult(), newDetailType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -101,8 +101,11 @@ public class WindChart extends RaceChart implements Component<WindChartSettings>
|
||||
.setMarginRight(65)
|
||||
.setWidth100()
|
||||
.setHeight100()
|
||||
.setBorderColor(new Color("#A6A6A6"))
|
||||
.setBorderColor(new Color("#CACACA"))
|
||||
.setBorderWidth(1)
|
||||
.setBorderRadius(0)
|
||||
.setBackgroundColor(new Color("#EBEBEB"))
|
||||
.setPlotBorderWidth(0)
|
||||
.setCredits(new Credits().setEnabled(false))
|
||||
.setChartTitle(new ChartTitle().setText(stringMessages.wind()))
|
||||
.setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn()))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sap.sailing.gwt.ui.shared.racemap;
|
||||
|
||||
import com.google.gwt.maps.client.overlay.Icon;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
|
||||
/**
|
||||
* A descriptor class for display properties of a mark icon.
|
||||
@@ -10,9 +11,11 @@ public class MarkIconDescriptor {
|
||||
private final String color;
|
||||
private final String shape;
|
||||
private final String pattern;
|
||||
private final MarkType type;
|
||||
|
||||
public MarkIconDescriptor(Icon icon, String color, String shape, String pattern) {
|
||||
public MarkIconDescriptor(Icon icon, MarkType type, String color, String shape, String pattern) {
|
||||
this.icon = icon;
|
||||
this.type = type;
|
||||
this.color = color;
|
||||
this.shape = shape;
|
||||
this.pattern = pattern;
|
||||
@@ -33,14 +36,24 @@ public class MarkIconDescriptor {
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public MarkType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* An utility method to check if the mark icon has compatible display properties.
|
||||
* In case of null values at least one valid display property must exist and fit.
|
||||
*/
|
||||
public boolean isCompatible(String colorToCheck, String shapeToCheck, String patternToCheck) {
|
||||
public boolean isCompatible(MarkType typeToCheck, String colorToCheck, String shapeToCheck, String patternToCheck) {
|
||||
boolean result = false;
|
||||
|
||||
if((typeToCheck != null && type == null) || (typeToCheck == null && type != null)) {
|
||||
result = false;
|
||||
} else if(typeToCheck != null && type != null) {
|
||||
result = type == typeToCheck;
|
||||
}
|
||||
|
||||
if((colorToCheck != null && color == null) || (colorToCheck == null && color != null)) {
|
||||
result = false;
|
||||
} else if(colorToCheck != null && color != null) {
|
||||
|
||||
@@ -953,7 +953,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
|
||||
protected Marker createCourseMarkMarker(final MarkDTO markDTO) {
|
||||
MarkerOptions options = MarkerOptions.newInstance();
|
||||
final Icon markIcon = raceMapImageManager.resolveMarkIcon(markDTO.color, markDTO.shape, markDTO.pattern);
|
||||
final Icon markIcon = raceMapImageManager.resolveMarkIcon(markDTO.type, markDTO.color, markDTO.shape, markDTO.pattern);
|
||||
if (markIcon != null) {
|
||||
options.setIcon(markIcon);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.google.gwt.maps.client.geom.Size;
|
||||
import com.google.gwt.maps.client.overlay.Icon;
|
||||
import com.google.gwt.resources.client.ImageResource;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.MarkType;
|
||||
import com.sap.sailing.domain.common.Tack;
|
||||
import com.sap.sailing.domain.common.impl.Util.Pair;
|
||||
|
||||
@@ -47,11 +48,11 @@ public class RaceMapImageManager {
|
||||
expeditionWindIconTransformer = new ImageTransformer(resources.expeditionWindIcon());
|
||||
}
|
||||
|
||||
public Icon resolveMarkIcon(String color, String shape, String pattern) {
|
||||
public Icon resolveMarkIcon(MarkType type, String color, String shape, String pattern) {
|
||||
Icon result = defaultCourseMarkIcon;
|
||||
|
||||
for (MarkIconDescriptor iconDescriptor: markIconDescriptors) {
|
||||
if(iconDescriptor.isCompatible(color, shape, pattern)) {
|
||||
if(iconDescriptor.isCompatible(type, color, shape, pattern)) {
|
||||
result = iconDescriptor.getIcon();
|
||||
break;
|
||||
}
|
||||
@@ -70,16 +71,22 @@ public class RaceMapImageManager {
|
||||
defaultCourseMarkIcon.setIconSize(Size.newInstance(19, 28));
|
||||
defaultCourseMarkIcon.setIconAnchor(Point.newInstance(6, 15));
|
||||
|
||||
createMarkIconDescriptor(resources.buoyRedIcon(), "red", null, null);
|
||||
createMarkIconDescriptor(resources.buoyGreenIcon(), "green", null, null);
|
||||
createMarkIconDescriptor(resources.buoyYellowIcon(), "yellow", null, null);
|
||||
createMarkIconDescriptor(resources.buoyGreyIcon(), "grey", null, null);
|
||||
createMarkIconDescriptor(resources.buoyWhiteIcon(), "white", null, null);
|
||||
createMarkIconDescriptor(resources.buoyWhiteConeIcon(), "white", "conical", null);
|
||||
createMarkIconDescriptor(resources.buoyBlackIcon(), "black", null, null);
|
||||
createMarkIconDescriptor(resources.buoyBlackConeIcon(), "black", "conical", null);
|
||||
createMarkIconDescriptor(resources.buoyDarkOrangeIcon(), "orange", null, null);
|
||||
createMarkIconDescriptor(resources.buoyBlackFinishIcon(), "black", "cylinder", "checkered");
|
||||
createMarkIconDescriptor(resources.buoyRedIcon(), MarkType.BUOY, "red", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyGreenIcon(), MarkType.BUOY, "green", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyYellowIcon(), MarkType.BUOY, "yellow", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyGreyIcon(), MarkType.BUOY, "grey", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyWhiteIcon(), MarkType.BUOY, "white", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyWhiteConeIcon(), MarkType.BUOY, "white", "conical", null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyBlackIcon(), MarkType.BUOY, "black", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyBlackConeIcon(), MarkType.BUOY, "black", "conical", null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyDarkOrangeIcon(), MarkType.BUOY, "orange", null, null, 6, 20);
|
||||
createMarkIconDescriptor(resources.buoyBlackFinishIcon(), MarkType.BUOY, "black", "cylinder", "checkered", 6, 20);
|
||||
|
||||
createMarkIconDescriptor(resources.cameraBoatIcon(), MarkType.CAMERABOAT, null, null, null, 35, 20);
|
||||
createMarkIconDescriptor(resources.umpireBoatIcon(), MarkType.UMPIREBOAT, null, null, null, 35, 20);
|
||||
createMarkIconDescriptor(resources.startBoatIcon(), MarkType.STARTBOAT, null, null, null, 35, 20);
|
||||
|
||||
createMarkIconDescriptor(resources.landmarkIcon(), MarkType.LANDMARK, null, null, null, 6, 15);
|
||||
|
||||
Icon tackToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|00FF00|000000");
|
||||
@@ -137,11 +144,12 @@ public class RaceMapImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void createMarkIconDescriptor(ImageResource imgResource, String color, String shape, String pattern) {
|
||||
private void createMarkIconDescriptor(ImageResource imgResource, MarkType type, String color, String shape, String pattern,
|
||||
int anchorPointX, int anchorPointY) {
|
||||
Icon icon = Icon.newInstance(imgResource.getSafeUri().asString());
|
||||
icon.setIconSize(Size.newInstance(19, 28));
|
||||
icon.setIconAnchor(Point.newInstance(6, 15));
|
||||
MarkIconDescriptor markIconDescriptor = new MarkIconDescriptor(icon, color, shape, pattern);
|
||||
//icon.setIconSize(Size.newInstance(19, 28));
|
||||
icon.setIconAnchor(Point.newInstance(anchorPointX, anchorPointY));
|
||||
MarkIconDescriptor markIconDescriptor = new MarkIconDescriptor(icon, type, color, shape, pattern);
|
||||
markIconDescriptors.add(markIconDescriptor);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,17 @@ interface RaceMapResources extends ClientBundle {
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/buoys/buoy_green.png")
|
||||
ImageResource buoyGreenIcon();
|
||||
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/buoys/landmark.png")
|
||||
ImageResource landmarkIcon();
|
||||
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/cameraBoat.png")
|
||||
ImageResource cameraBoatIcon();
|
||||
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/juryBoat.png")
|
||||
ImageResource juryBoatIcon();
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/umpireBoat.png")
|
||||
ImageResource umpireBoatIcon();
|
||||
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/startBoat.png")
|
||||
ImageResource startBoatIcon();
|
||||
|
||||
@Source("com/sap/sailing/gwt/ui/client/images/wind_combined.png")
|
||||
ImageResource combinedWindIcon();
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,20 +1,83 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SAP Sailing Analytics - Extreme Sailing Series 2012</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<meta http-equiv="content-style-type" content="text/css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/sap.ico" />
|
||||
<link href="../../fontface.css" media="screen" rel="stylesheet" title="Default stylesheet" type="text/css" />
|
||||
<link href="../main.css" media="screen" rel="stylesheet" title="Default stylesheet" type="text/css" />
|
||||
<link href='../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
|
||||
<link href='../site.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
|
||||
<title>Welcome to the Sailing Analytics Homepage</title>
|
||||
</head>
|
||||
<!-- start body -->
|
||||
<body>
|
||||
<h2>SAP Sailing Analytics - Extreme Sailing Series 2012</h2>
|
||||
<p>
|
||||
<a href="http://ess40-2012.sapsailing.com/gwt/Leaderboard.html?name=Extreme%2040%20Rio&showRaceDetails=true">Live Rio Leaderboard</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="http://ess40-2012.sapsailing.com/gwt/Leaderboard.html?name=Extreme%20Sailing%20Series%202012%20Overall&showRaceDetails=true&displayName=Overall%20Standings&leaderboardGroupName=Extreme%20Sailing%20Series%202012&root=leaderboardGroupPanel">Live Overall Leaderboard</a>
|
||||
</p>
|
||||
<!-- = partial "info_message" -->
|
||||
<!-- start yield_content -->
|
||||
<div id='yield_content'>
|
||||
<!-- start header -->
|
||||
<!-- start logo and title panel _header -->
|
||||
<div id='logoAndTitlePanel'>
|
||||
<a class='sapLogo'>
|
||||
<img alt='SAP Website' src='../images/sap-logo.png' />
|
||||
</a>
|
||||
</div>
|
||||
<!-- end header -->
|
||||
<!-- start contentWrapper -->
|
||||
<div id='contentWrapper'>
|
||||
<!-- start keyVisual -->
|
||||
<div id='keyVisual'>
|
||||
<img alt='Sailer' src='../images/key-visual.png' />
|
||||
<div id='infoBox'>
|
||||
<h1 id='pageHeadline'>Extreme Sailing Series 2012</h1>
|
||||
<p>
|
||||
Understanding what happens out on the race course isn't always easy.
|
||||
To help solve this challenge and bring the excitement of sailing to the fans,
|
||||
we have developed a leader board based on SAP analytics.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end keyVisual -->
|
||||
<!-- start breadcrumb -->
|
||||
<!-- = partial "breadcrumb" -->
|
||||
<!-- end breadcrumb -->
|
||||
<!-- start yield -->
|
||||
<div id='mainContent'>
|
||||
<!-- = partial "former_events" -->
|
||||
<h2 class='articleHeadline'>Act 8 - Rio de Janeiro, Brazil</h2>
|
||||
<div class='innerContent'>
|
||||
<h3 class='articleSubheadline'>6th - 9th December</h3>
|
||||
<div class='leaderBoardGroup'>
|
||||
<a class='btn-cta' href='http://ess40-2012.sapsailing.com/gwt/Leaderboard.html?name=Extreme%2040%20Rio&showRaceDetails=true'>
|
||||
<span>Live Rio Act Leaderboard</span>
|
||||
</a>
|
||||
<div class='clearer'></div>
|
||||
<a class='btn-cta' href='http://ess40-2012.sapsailing.com/gwt/Leaderboard.html?name=Extreme%20Sailing%20Series%202012%20Overall&showRaceDetails=true&displayName=Overall%20Standings&leaderboardGroupName=Extreme%20Sailing%20Series%202012&root=leaderboardGroupPanel'>
|
||||
<span>Live Series Leaderboard</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='spaceBar'></div>
|
||||
<!-- end yield -->
|
||||
</div>
|
||||
<!-- end contentWrapper -->
|
||||
</div>
|
||||
<!-- end yield_content -->
|
||||
<!-- start footer -->
|
||||
<div id='footer'>
|
||||
<ul class='regularNav'>
|
||||
<li>
|
||||
<a href='http://www12.sap.com/corporate-en/legal/impressum.epx' target='_blank'>
|
||||
Legal Disclosure
|
||||
</a>
|
||||
<span>
|
||||
|
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href='http://www12.sap.com/corporate-en/our-company/legal/privacy.epx' target='_blank'>
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- end footer -->
|
||||
</body>
|
||||
<!-- end body -->
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 74 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 1021 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 989 B |
@@ -1,84 +0,0 @@
|
||||
/* line 2, _reset.sass */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline; }
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
/* line 23, _reset.sass */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block; }
|
||||
|
||||
/* line 27, _reset.sass */
|
||||
body {
|
||||
line-height: 1; }
|
||||
|
||||
/* line 30, _reset.sass */
|
||||
ol, ul {
|
||||
list-style: none; }
|
||||
|
||||
/* line 33, _reset.sass */
|
||||
blockquote, q {
|
||||
quotes: none; }
|
||||
|
||||
/* line 36, _reset.sass */
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: "";
|
||||
content: none; }
|
||||
|
||||
/* line 41, _reset.sass */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0; }
|
||||
|
||||
/* line 7, site.css.sass */
|
||||
html {
|
||||
height: 100%; }
|
||||
|
||||
/* line 10, site.css.sass */
|
||||
body {
|
||||
height: 100%;
|
||||
color: #222222;
|
||||
font-size: 14px;
|
||||
font-family: OpenSansRegular, Arial, Verdana, sans-serif;
|
||||
font-weight: normal;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
line-height: 18px; }
|
||||
|
||||
/* line 27, site.css.sass */
|
||||
ul {
|
||||
font-size: 12px;
|
||||
padding-left: 0px;
|
||||
margin-bottom: 25px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
/* line 34, site.css.sass */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0066b3;
|
||||
}
|
||||
|
||||
/* line 38, site.css.sass */
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
/* line 2, _reset.sass */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
/* line 23, _reset.sass */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* line 27, _reset.sass */
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* line 30, _reset.sass */
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* line 33, _reset.sass */
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
/* line 36, _reset.sass */
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* line 41, _reset.sass */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* line 2, _default.sass */
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* line 5, _default.sass */
|
||||
body {
|
||||
height: 100%;
|
||||
color: #222222;
|
||||
font-size: 12px;
|
||||
font-family: OpenSansRegular, Arial, Verdana, sans-serif;
|
||||
font-weight: normal;
|
||||
background-image: url(images/bg-image.jpg);
|
||||
background-attachment: fixed;
|
||||
background-color: #222222;
|
||||
background-position: 50% 0%;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
/* line 22, _default.sass */
|
||||
ul {
|
||||
font-size: 12px;
|
||||
padding-left: 0px;
|
||||
margin-bottom: 25px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
/* line 29, _default.sass */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0066b3;
|
||||
}
|
||||
/* line 33, _default.sass */
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* line 38, _helpers.sass */
|
||||
.clearer {
|
||||
clear: both;
|
||||
font-size: 1px;
|
||||
height: 0px;
|
||||
line-height: 0px;
|
||||
margin: -1px 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* line 14, site.css.sass */
|
||||
#infoMessage {
|
||||
display: none;
|
||||
color: white;
|
||||
}
|
||||
/* line 17, site.css.sass */
|
||||
#infoMessage .center {
|
||||
padding: 10px 0;
|
||||
width: 950px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* line 22, site.css.sass */
|
||||
#yield_content {
|
||||
width: 950px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
/* line 2, _key_visual.sass */
|
||||
#yield_content #keyVisual {
|
||||
height: 240px;
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
/* line 7, _key_visual.sass */
|
||||
#yield_content #keyVisual #infoBox {
|
||||
width: 502px;
|
||||
height: 80px;
|
||||
background: #999999;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
/* line 16, _key_visual.sass */
|
||||
#yield_content #keyVisual #infoBox p {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* line 21, _key_visual.sass */
|
||||
#yield_content #keyVisual #infoBox #pageHeadline {
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
/* line 2, _breadcrumb.sass */
|
||||
#yield_content #breadcrumb {
|
||||
height: 23px;
|
||||
background: #e5e5e5;
|
||||
margin: 2px 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* line 8, _breadcrumb.sass */
|
||||
#yield_content #breadcrumb .breadcrumbList {
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
float: left;
|
||||
}
|
||||
/* line 12, _breadcrumb.sass */
|
||||
#yield_content #breadcrumb .breadcrumbList li {
|
||||
height: 23px;
|
||||
float: left;
|
||||
background: url(images/breadcrumb-seperator.png) right center no-repeat;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
/* line 17, _breadcrumb.sass */
|
||||
#yield_content #breadcrumb .breadcrumbList li:last-child {
|
||||
background: none;
|
||||
}
|
||||
/* line 19, _breadcrumb.sass */
|
||||
#yield_content #breadcrumb .breadcrumbList li a {
|
||||
display: block;
|
||||
margin: 2px 10px 0 0;
|
||||
}
|
||||
/* line 3, _main_content.sass */
|
||||
#yield_content #mainContent {
|
||||
padding: 0 10px 10px 10px;
|
||||
background: white;
|
||||
*display: inline-block;
|
||||
}
|
||||
/* line 3, _helpers.sass */
|
||||
#yield_content #mainContent:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
}
|
||||
/* line 2, _former_events.sass */
|
||||
#yield_content #mainContent #formerEvents {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 12px;
|
||||
padding: 10px 0 0 0;
|
||||
float: left;
|
||||
}
|
||||
/* line 8, _former_events.sass */
|
||||
#yield_content #mainContent #formerEvents dt {
|
||||
margin: 0 5px 0 0;
|
||||
font-family: OpenSansBold, Arial, Verdana, sans-serif;
|
||||
}
|
||||
/* line 12, _former_events.sass */
|
||||
#yield_content #mainContent #formerEvents dt, #yield_content #mainContent #formerEvents dd {
|
||||
float: left;
|
||||
}
|
||||
/* line 14, _former_events.sass */
|
||||
#yield_content #mainContent #formerEvents dd {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
/* line 9, _main_content.sass */
|
||||
#yield_content #mainContent .articleHeadline {
|
||||
font-size: 18px;
|
||||
font-family: OpenSansBold, Arial, Verdana, sans-serif;
|
||||
color: #222222;
|
||||
border-bottom: 2px solid #222222;
|
||||
clear: both;
|
||||
padding: 10px 0 5px;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
/* line 19, _main_content.sass */
|
||||
#yield_content #mainContent .articleSubheadline {
|
||||
font-size: 14px;
|
||||
font-family: OpenSansBold, Arial, Verdana, sans-serif;
|
||||
color: #222222;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
/* line 26, _main_content.sass */
|
||||
#yield_content #mainContent h4 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-family: OpenSansBold, Arial, Verdana, sans-serif;
|
||||
}
|
||||
/* line 33, _main_content.sass */
|
||||
#yield_content #mainContent a.btn-cta {
|
||||
background: url("images/btn-cta-orangeright.png") 100% 0% no-repeat;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 30px;
|
||||
padding: 0px 25px 0px 0px;
|
||||
text-decoration: none;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
/* line 41, _main_content.sass */
|
||||
#yield_content #mainContent a.btn-cta:hover {
|
||||
background-position: 100% -30px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
/* line 44, _main_content.sass */
|
||||
#yield_content #mainContent a.btn-cta:hover span {
|
||||
background-position: 0% -30px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
/* line 48, _main_content.sass */
|
||||
#yield_content #mainContent a.btn-cta span {
|
||||
background: url("images/btn-cta-orangeleft.png") no-repeat transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
float: left;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0px 0px 0px 17px;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* line 31, site.css.sass */
|
||||
#yield_content #spaceBar {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* line 2, _footer.sass */
|
||||
#footer {
|
||||
color: white;
|
||||
width: 940px;
|
||||
height: 20px;
|
||||
margin: -40px auto 0 auto;
|
||||
background: url(images/bkg-footer1.png);
|
||||
padding: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
/* line 11, _footer.sass */
|
||||
#footer a {
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
}
|
||||
/* line 16, _footer.sass */
|
||||
#footer .regularNav {
|
||||
margin: 0;
|
||||
}
|
||||
/* line 18, _footer.sass */
|
||||
#footer .regularNav li {
|
||||
display: inline;
|
||||
}
|
||||