mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-26 23:46:36 +00:00
Merge remote-tracking branch 'server/master'
This commit is contained in:
+5
@@ -529,4 +529,9 @@ public class MockedTrackedRace implements DynamicTrackedRace {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<TimePoint> getStartTimesOfTrackedLegs() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ public interface TrackedRace extends Serializable {
|
||||
/**
|
||||
* Computes the estimated start time for this race (not to be confused with the {@link #getStartOfTracking()} time point
|
||||
* which is expected to be before the race start time). When there are no {@link MarkPassing}s for the first mark, <code>null</code>
|
||||
* is returned. If there are mark passings for
|
||||
* the first mark and the start time is less than
|
||||
* is returned. If there are mark passings for the first mark and the start time is less than
|
||||
* {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} before the first mark passing for the
|
||||
* first mark. Otherwise, the first mark passing for the first mark minus
|
||||
* {@link #MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS} is returned as the race start time.
|
||||
@@ -64,7 +63,14 @@ public interface TrackedRace extends Serializable {
|
||||
* no boat passed the finish line yet.
|
||||
*/
|
||||
TimePoint getAssumedEnd();
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of the start times of all legs as far as we know them
|
||||
* The leg time of the first leg is equal to #{@link #getStart()}
|
||||
* All other leg times are equal to the first mark passing of the leg
|
||||
*/
|
||||
Iterable<TimePoint> getStartTimesOfTrackedLegs();
|
||||
|
||||
/**
|
||||
* Shorthand for <code>{@link #getStart()}.{@link TimePoint#compareTo(TimePoint) compareTo(at)} <= 0</code>
|
||||
*/
|
||||
|
||||
+19
@@ -260,16 +260,28 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
@Override
|
||||
public void updateMarkPassings(Competitor competitor, Iterable<MarkPassing> markPassings) {
|
||||
Map<Waypoint, MarkPassing> oldMarkPassings = new HashMap<Waypoint, MarkPassing>();
|
||||
MarkPassing oldStartMarkPassing = null;
|
||||
boolean requiresStartTimeUpdate = true;
|
||||
synchronized (this) {
|
||||
NavigableSet<MarkPassing> markPassingsForCompetitor = getMarkPassings(competitor);
|
||||
synchronized (markPassingsForCompetitor) {
|
||||
for (MarkPassing oldMarkPassing : markPassingsForCompetitor) {
|
||||
if(oldStartMarkPassing == null) {
|
||||
oldStartMarkPassing = oldMarkPassing;
|
||||
}
|
||||
oldMarkPassings.put(oldMarkPassing.getWaypoint(), oldMarkPassing);
|
||||
}
|
||||
}
|
||||
clearMarkPassings(competitor);
|
||||
TimePoint timePointOfLatestEvent = new MillisecondsTimePoint(0);
|
||||
for (MarkPassing markPassing : markPassings) {
|
||||
// try to find corresponding old start mark passing
|
||||
if(oldStartMarkPassing != null && markPassing.getWaypoint().getName().equals(oldStartMarkPassing.getWaypoint().getName())) {
|
||||
if(markPassing.getTimePoint() != null && oldStartMarkPassing.getTimePoint() != null &&
|
||||
markPassing.getTimePoint().equals(oldStartMarkPassing.getTimePoint())) {
|
||||
requiresStartTimeUpdate = false;
|
||||
}
|
||||
}
|
||||
synchronized (markPassingsForCompetitor) {
|
||||
markPassingsForCompetitor.add(markPassing);
|
||||
}
|
||||
@@ -283,6 +295,13 @@ public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements
|
||||
}
|
||||
updated(timePointOfLatestEvent);
|
||||
}
|
||||
// update the race times like start, end and the leg times
|
||||
if(requiresStartTimeUpdate) {
|
||||
invalidateStartTime();
|
||||
}
|
||||
invalidateLegTimes();
|
||||
invalidateEndTime();
|
||||
|
||||
// notify *after* all mark passings have been re-established; should avoid flicker
|
||||
for (MarkPassing markPassing : markPassings) {
|
||||
notifyListeners(oldMarkPassings.get(markPassing.getWaypoint()), markPassing);
|
||||
|
||||
+176
-76
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
@@ -86,10 +87,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
private final RaceDefinition race;
|
||||
|
||||
private final TrackedEvent trackedEvent;
|
||||
|
||||
|
||||
/**
|
||||
* By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind sources
|
||||
* such as the track-based estimation wind source, may be excluded by adding them to this set.
|
||||
* By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind
|
||||
* sources such as the track-based estimation wind source, may be excluded by adding them to this set.
|
||||
*/
|
||||
private final Set<WindSource> windSourcesToExclude;
|
||||
|
||||
@@ -114,8 +115,31 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
*/
|
||||
private TimePoint startTimeReceived;
|
||||
|
||||
/**
|
||||
* The calculated race start time
|
||||
*/
|
||||
private TimePoint startTime;
|
||||
|
||||
/**
|
||||
* The calculated race end time
|
||||
*/
|
||||
private TimePoint endTime;
|
||||
|
||||
/**
|
||||
* The calculated start times of the legs
|
||||
*/
|
||||
private final List<TimePoint> startTimesOfLegs;
|
||||
|
||||
/**
|
||||
* The latest time point contained by any of the events received and processed
|
||||
*/
|
||||
private TimePoint timePointOfNewestEvent;
|
||||
|
||||
/**
|
||||
* Time stamp that the event received last from the underlying push service carried on it
|
||||
*/
|
||||
private TimePoint timePointOfLastEvent;
|
||||
|
||||
private long updateCount;
|
||||
|
||||
private final Map<TimePoint, List<Competitor>> competitorRankings;
|
||||
@@ -164,9 +188,9 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
public TrackedRaceImpl(TrackedEvent trackedEvent, RaceDefinition race, WindStore windStore,
|
||||
long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) {
|
||||
this(trackedEvent, race, windStore, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed,
|
||||
/* delay for wind estimation cache invalidation */ millisecondsOverWhichToAverageWind/2);
|
||||
/* delay for wind estimation cache invalidation */millisecondsOverWhichToAverageWind / 2);
|
||||
}
|
||||
|
||||
|
||||
public TrackedRaceImpl(TrackedEvent trackedEvent, RaceDefinition race, WindStore windStore,
|
||||
long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed,
|
||||
long delayForWindEstimationCacheInvalidation) {
|
||||
@@ -213,17 +237,18 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
markPassingsForWaypoint.put(waypoint, new ConcurrentSkipListSet<MarkPassing>(
|
||||
MarkPassingByTimeComparator.INSTANCE));
|
||||
}
|
||||
startTimesOfLegs = new ArrayList<TimePoint>();
|
||||
windTracks = new HashMap<WindSource, WindTrack>();
|
||||
windTracks.putAll(windStore.loadWindTracks(trackedEvent, this, millisecondsOverWhichToAverageWind));
|
||||
// by default, a tracked race offers one course-based wind estimation, one track-based wind estimation track and
|
||||
// one "WEB" track for manual or REST-based wind reception; other wind tracks may be added as fixes are received
|
||||
// for them.
|
||||
WindSource courseBasedWindSource = new WindSourceImpl(WindSourceType.COURSE_BASED);
|
||||
windTracks.put(courseBasedWindSource,
|
||||
windStore.getWindTrack(trackedEvent, this, courseBasedWindSource, millisecondsOverWhichToAverageWind, delayForWindEstimationCacheInvalidation));
|
||||
windTracks.put(courseBasedWindSource, windStore.getWindTrack(trackedEvent, this, courseBasedWindSource,
|
||||
millisecondsOverWhichToAverageWind, delayForWindEstimationCacheInvalidation));
|
||||
WindSource trackBasedWindSource = new WindSourceImpl(WindSourceType.TRACK_BASED_ESTIMATION);
|
||||
windTracks.put(trackBasedWindSource,
|
||||
windStore.getWindTrack(trackedEvent, this, trackBasedWindSource, millisecondsOverWhichToAverageWind, delayForWindEstimationCacheInvalidation));
|
||||
windTracks.put(trackBasedWindSource, windStore.getWindTrack(trackedEvent, this, trackBasedWindSource,
|
||||
millisecondsOverWhichToAverageWind, delayForWindEstimationCacheInvalidation));
|
||||
this.trackedEvent = trackedEvent;
|
||||
competitorRankings = new HashMap<TimePoint, List<Competitor>>();
|
||||
}
|
||||
@@ -241,7 +266,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
public NavigableSet<MarkPassing> getMarkPassings(Competitor competitor) {
|
||||
return markPassingsForCompetitor.get(competitor);
|
||||
}
|
||||
|
||||
|
||||
protected NavigableSet<MarkPassing> getMarkPassingsInOrderAsNavigableSet(Waypoint waypoint) {
|
||||
return markPassingsForWaypoint.get(waypoint);
|
||||
}
|
||||
@@ -261,48 +286,82 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
return endOfTrackingReceived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimePoint getStart() {
|
||||
TimePoint result = startTimeReceived;
|
||||
// If not null, check if the first mark passing for the start line is too much after the startTimeReceived;
|
||||
// if so, return an adjusted, later start time.
|
||||
// If no official start time was received, try to estimate the start time using the mark passings for the start line.
|
||||
if (startTimeReceived != null) {
|
||||
TimePoint timeOfFirstMarkPassingFirstMark = getFirstStartPassingTime();
|
||||
if (timeOfFirstMarkPassingFirstMark != null) {
|
||||
long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassingFirstMark.asMillis()
|
||||
- startTimeReceived.asMillis();
|
||||
if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) {
|
||||
result = new MillisecondsTimePoint(timeOfFirstMarkPassingFirstMark.asMillis()
|
||||
- MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS);
|
||||
} else {
|
||||
result = startTimeReceived;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = calculateStartOfRaceFromMarkPassings(getMarkPassingsInOrderAsNavigableSet(getRace().getCourse()
|
||||
.getFirstWaypoint()), getRace().getCompetitors());
|
||||
}
|
||||
return result;
|
||||
protected void invalidateStartTime() {
|
||||
startTime = null;
|
||||
}
|
||||
|
||||
private TimePoint getFirstStartPassingTime() {
|
||||
Iterable<MarkPassing> markPassingsInOrder = getMarkPassingsInOrder(getRace().getCourse().getFirstWaypoint());
|
||||
MarkPassing firstMarkPassingFirstMark = null;
|
||||
synchronized (markPassingsInOrder) {
|
||||
Iterator<MarkPassing> markPassingsFirstMarkIter = markPassingsInOrder.iterator();
|
||||
if (markPassingsFirstMarkIter.hasNext()) {
|
||||
firstMarkPassingFirstMark = markPassingsFirstMarkIter.next();
|
||||
protected void invalidateEndTime() {
|
||||
endTime = null;
|
||||
}
|
||||
|
||||
protected void invalidateLegTimes() {
|
||||
startTimesOfLegs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the start time of the race from various sources
|
||||
*/
|
||||
@Override
|
||||
public TimePoint getStart() {
|
||||
if (startTime == null) {
|
||||
startTime = startTimeReceived;
|
||||
// If not null, check if the first mark passing for the start line is too much after the startTimeReceived;
|
||||
// if so, return an adjusted, later start time.
|
||||
// If no official start time was received, try to estimate the start time using the mark passings for the
|
||||
// start line.
|
||||
if (startTimeReceived != null) {
|
||||
TimePoint timeOfFirstMarkPassing = getFirstPassingTime(getRace().getCourse().getFirstWaypoint());
|
||||
if (timeOfFirstMarkPassing != null) {
|
||||
long startTimeReceived2timeOfFirstMarkPassingFirstMark = timeOfFirstMarkPassing.asMillis()
|
||||
- startTimeReceived.asMillis();
|
||||
if (startTimeReceived2timeOfFirstMarkPassingFirstMark > MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS) {
|
||||
startTime = new MillisecondsTimePoint(timeOfFirstMarkPassing.asMillis()
|
||||
- MAX_TIME_BETWEEN_START_AND_FIRST_MARK_PASSING_IN_MILLISECONDS);
|
||||
} else {
|
||||
startTime = startTimeReceived;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTime = calculateStartOfRaceFromMarkPassings(getMarkPassingsInOrderAsNavigableSet(getRace()
|
||||
.getCourse().getFirstWaypoint()), getRace().getCompetitors());
|
||||
}
|
||||
}
|
||||
TimePoint timeOfFirstMarkPassingFirstMark = null;
|
||||
if (firstMarkPassingFirstMark != null) {
|
||||
timeOfFirstMarkPassingFirstMark = firstMarkPassingFirstMark.getTimePoint();
|
||||
}
|
||||
return timeOfFirstMarkPassingFirstMark;
|
||||
return startTime;
|
||||
}
|
||||
|
||||
private TimePoint calculateStartOfRaceFromMarkPassings(NavigableSet<MarkPassing> markPassings, Iterable<Competitor> competitors) {
|
||||
|
||||
/**
|
||||
* Calculates the end time of the race from the mark passings of the last course waypoint
|
||||
*/
|
||||
@Override
|
||||
public TimePoint getAssumedEnd() {
|
||||
if (endTime == null) {
|
||||
Iterable<MarkPassing> markPassingsInOrder = getMarkPassingsInOrder(getRace().getCourse().getLastWaypoint());
|
||||
synchronized (markPassingsInOrder) {
|
||||
for (MarkPassing passingFinishLine : markPassingsInOrder) {
|
||||
endTime = passingFinishLine.getTimePoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
return endTime;
|
||||
}
|
||||
|
||||
private TimePoint getFirstPassingTime(Waypoint waypoint) {
|
||||
NavigableSet<MarkPassing> markPassingsInOrder = getMarkPassingsInOrderAsNavigableSet(waypoint);
|
||||
MarkPassing firstMarkPassing = null;
|
||||
synchronized (markPassingsInOrder) {
|
||||
if (!markPassingsInOrder.isEmpty()) {
|
||||
firstMarkPassing = markPassingsInOrder.first();
|
||||
}
|
||||
}
|
||||
TimePoint timeOfFirstMarkPassing = null;
|
||||
if (firstMarkPassing != null) {
|
||||
timeOfFirstMarkPassing = firstMarkPassing.getTimePoint();
|
||||
}
|
||||
return timeOfFirstMarkPassing;
|
||||
}
|
||||
|
||||
private TimePoint calculateStartOfRaceFromMarkPassings(NavigableSet<MarkPassing> markPassings,
|
||||
Iterable<Competitor> competitors) {
|
||||
TimePoint startOfRace = null;
|
||||
// Find the first mark passing within the largest cluster crossing the line within one minute.
|
||||
final long ONE_MINUTE_IN_MILLIS = 60 * 1000;
|
||||
@@ -313,7 +372,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
int candiateGroupSize = 0;
|
||||
MarkPassing candidateForStartOfLargestGroupSoFar = null;
|
||||
Iterator<MarkPassing> iterator = markPassings.iterator();
|
||||
// sweep over all start mark passings and for each element find the number of competitors that passed the start up to one minute later;
|
||||
// sweep over all start mark passings and for each element find the number of competitors that passed
|
||||
// the start up to one minute later;
|
||||
// pick the start mark passing of the competitor leading the largest such group
|
||||
while (iterator.hasNext()) {
|
||||
MarkPassing currentMarkPassing = iterator.next();
|
||||
@@ -324,8 +384,10 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
startOfLargestGroupSoFar = currentMarkPassing;
|
||||
largestStartGroupWithinOneMinuteSize = 1;
|
||||
} else {
|
||||
if (currentMarkPassing.getTimePoint().asMillis() - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() <= ONE_MINUTE_IN_MILLIS) {
|
||||
// currentMarkPassing is within one minute of candidateForStartOfLargestGroupSoFar; extend candidate group...
|
||||
if (currentMarkPassing.getTimePoint().asMillis()
|
||||
- candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() <= ONE_MINUTE_IN_MILLIS) {
|
||||
// currentMarkPassing is within one minute of candidateForStartOfLargestGroupSoFar; extend
|
||||
// candidate group...
|
||||
candiateGroupSize++;
|
||||
if (candiateGroupSize > largestStartGroupWithinOneMinuteSize) {
|
||||
// ...and remember as best fit if greater than largest group so far
|
||||
@@ -333,12 +395,17 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
largestStartGroupWithinOneMinuteSize = candiateGroupSize;
|
||||
}
|
||||
} else {
|
||||
// currentMarkPassing is more than a minute after candidateForStartOfLargestGroupSoFar; advance
|
||||
// candidateForStartOfLargestGroupSoFar and reduce group size counter, until candidateForStartOfLargestGroupSoFar
|
||||
// is again within the one-minute interval; may catch up all the way to currentMarkPassing if that was
|
||||
// currentMarkPassing is more than a minute after candidateForStartOfLargestGroupSoFar;
|
||||
// advance
|
||||
// candidateForStartOfLargestGroupSoFar and reduce group size counter, until
|
||||
// candidateForStartOfLargestGroupSoFar
|
||||
// is again within the one-minute interval; may catch up all the way to currentMarkPassing
|
||||
// if that was
|
||||
// more than a minute after its predecessor
|
||||
while (currentMarkPassing.getTimePoint().asMillis() - candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() > ONE_MINUTE_IN_MILLIS) {
|
||||
candidateForStartOfLargestGroupSoFar = markPassings.higher(candidateForStartOfLargestGroupSoFar);
|
||||
while (currentMarkPassing.getTimePoint().asMillis()
|
||||
- candidateForStartOfLargestGroupSoFar.getTimePoint().asMillis() > ONE_MINUTE_IN_MILLIS) {
|
||||
candidateForStartOfLargestGroupSoFar = markPassings
|
||||
.higher(candidateForStartOfLargestGroupSoFar);
|
||||
candiateGroupSize--;
|
||||
}
|
||||
}
|
||||
@@ -349,18 +416,6 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
}
|
||||
return startOfRace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimePoint getAssumedEnd() {
|
||||
TimePoint result = null;
|
||||
Iterable<MarkPassing> markPassingsInOrder = getMarkPassingsInOrder(getRace().getCourse().getLastWaypoint());
|
||||
synchronized (markPassingsInOrder) {
|
||||
for (MarkPassing passingFinishLine : markPassingsInOrder) {
|
||||
result = passingFinishLine.getTimePoint();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStarted(TimePoint at) {
|
||||
@@ -369,6 +424,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
|
||||
protected void setStartTimeReceived(TimePoint start) {
|
||||
this.startTimeReceived = start;
|
||||
invalidateStartTime();
|
||||
invalidateLegTimes();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -381,6 +438,48 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
return trackedLegs.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<TimePoint> getStartTimesOfTrackedLegs() {
|
||||
if (startTimesOfLegs.isEmpty()) {
|
||||
int legNumber = 1;
|
||||
// Remark: sometimes it can happen that a mark passing with a wrong time stamp breaks the right time order
|
||||
// of the leg times
|
||||
Date previousLegPassingTime = null;
|
||||
for (TrackedLeg trackedLeg : trackedLegs.values()) {
|
||||
if (legNumber == 1) {
|
||||
// For the first leg the use of "firstPassingDate" is not correct,
|
||||
// because boats can pass the start line before the actual start;
|
||||
// therefore we are using the calculated start time here
|
||||
TimePoint startOfRace = getStart();
|
||||
if (startOfRace != null) {
|
||||
startTimesOfLegs.add(startOfRace);
|
||||
}
|
||||
}
|
||||
Waypoint to = trackedLeg.getLeg().getTo();
|
||||
NavigableSet<MarkPassing> markPassings = getMarkPassingsInOrderAsNavigableSet(to);
|
||||
if (markPassings != null && !markPassings.isEmpty()) {
|
||||
// ensure the leg times are in the right time order; there may perhaps be left-overs for marks to be
|
||||
// reached later that
|
||||
// claim it has been passed in the past which may have been an accidental tracker read-out;
|
||||
// the results of getMarkPassingsInOrder(to) has by definition an ascending time-point ordering
|
||||
synchronized (markPassings) {
|
||||
for (MarkPassing currentMarkPassing : markPassings) {
|
||||
Date currentPassingDate = currentMarkPassing.getTimePoint().asDate();
|
||||
if (previousLegPassingTime == null || currentPassingDate.after(previousLegPassingTime)) {
|
||||
startTimesOfLegs.add(currentMarkPassing.getTimePoint());
|
||||
previousLegPassingTime = currentPassingDate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
legNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
return startTimesOfLegs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint) {
|
||||
NavigableSet<MarkPassing> markPassings = getMarkPassings(competitor);
|
||||
@@ -633,7 +732,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
* no other wind source exists yet.
|
||||
*/
|
||||
protected WindTrack createWindTrack(WindSource windSource) {
|
||||
return windStore.getWindTrack(trackedEvent, this, windSource, millisecondsOverWhichToAverageWind, getMillisecondsOverWhichToAverageWind()/2);
|
||||
return windStore.getWindTrack(trackedEvent, this, windSource, millisecondsOverWhichToAverageWind,
|
||||
getMillisecondsOverWhichToAverageWind() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -643,7 +743,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
|
||||
@Override
|
||||
public Wind getWind(Position p, TimePoint at, Iterable<WindSource> windSourcesToExclude) {
|
||||
final WindWithConfidence<Pair<Position, TimePoint>> windWithConfidence = getWindWithConfidence(p, at, windSourcesToExclude);
|
||||
final WindWithConfidence<Pair<Position, TimePoint>> windWithConfidence = getWindWithConfidence(p, at,
|
||||
windSourcesToExclude);
|
||||
return windWithConfidence == null ? null : windWithConfidence.getObject();
|
||||
}
|
||||
|
||||
@@ -651,12 +752,12 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
public WindWithConfidence<Pair<Position, TimePoint>> getWindWithConfidence(Position p, TimePoint at) {
|
||||
return getWindWithConfidence(p, at, getWindSourcesToExclude());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterable<WindSource> getWindSourcesToExclude() {
|
||||
return Collections.unmodifiableCollection(windSourcesToExclude);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setWindSourcesToExclude(Iterable<WindSource> windSourcesToExclude) {
|
||||
this.windSourcesToExclude.clear();
|
||||
@@ -670,7 +771,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
Iterable<WindSource> windSourcesToExclude) {
|
||||
boolean canUseSpeedOfAtLeastOneWindSource = false;
|
||||
Weigher<Pair<Position, TimePoint>> timeWeigherThatPretendsToAlsoWeighPositions = new PositionAndTimePointWeigher(
|
||||
/* halfConfidenceAfterMilliseconds */10000l);
|
||||
/* halfConfidenceAfterMilliseconds */10000l);
|
||||
ConfidenceBasedWindAverager<Pair<Position, TimePoint>> averager = ConfidenceFactory.INSTANCE
|
||||
.createWindAverager(timeWeigherThatPretendsToAlsoWeighPositions);
|
||||
List<WindWithConfidence<Pair<Position, TimePoint>>> windFixesWithConfidences = new ArrayList<WindWithConfidence<Pair<Position, TimePoint>>>();
|
||||
@@ -689,10 +790,9 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
|
||||
}
|
||||
HasConfidence<ScalableWind, Wind, Pair<Position, TimePoint>> average = averager.getAverage(
|
||||
windFixesWithConfidences, new Pair<Position, TimePoint>(p, at));
|
||||
WindWithConfidence<Pair<Position, TimePoint>> result = average == null ? null :
|
||||
new WindWithConfidenceImpl<Pair<Position, TimePoint>>(
|
||||
average.getObject(), average.getConfidence(), new Pair<Position, TimePoint>(p, at),
|
||||
canUseSpeedOfAtLeastOneWindSource);
|
||||
WindWithConfidence<Pair<Position, TimePoint>> result = average == null ? null
|
||||
: new WindWithConfidenceImpl<Pair<Position, TimePoint>>(average.getObject(), average.getConfidence(),
|
||||
new Pair<Position, TimePoint>(p, at), canUseSpeedOfAtLeastOneWindSource);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -15,6 +15,7 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
|
||||
private RaceTimesInfoProvider raceTimesInfoProvider;
|
||||
private RaceIdentifier selectedRace;
|
||||
private boolean autoAdjustPlayMode;
|
||||
private RaceTimesInfoDTO lastRaceTimesInfo;
|
||||
|
||||
public RaceTimePanel(Timer timer, StringMessages stringMessages, RaceTimesInfoProvider raceTimesInfoProvider) {
|
||||
super(timer, stringMessages);
|
||||
@@ -48,7 +49,7 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
|
||||
// in case the race is not tracked anymore we reset the timer
|
||||
reset();
|
||||
} else {
|
||||
if (raceTimesInfo.startOfTracking != null && raceTimesInfo.timePointOfNewestEvent != null) {
|
||||
if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) && raceTimesInfo.timePointOfNewestEvent != null) {
|
||||
// we set here the min and max of the time slider, the start and end of the race as well as the known
|
||||
// leg markers
|
||||
boolean liveModeToBeMadePossible = isLiveModeToBeMadePossible();
|
||||
@@ -204,13 +205,26 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
|
||||
|
||||
private void updateLegMarkers(RaceTimesInfoDTO newRaceTimesInfo) {
|
||||
List<LegTimesInfoDTO> legTimepoints = newRaceTimesInfo.getLegTimes();
|
||||
if (sliderBar.isMinMaxInitialized()) {
|
||||
boolean requiresMarkerUpdate = true;
|
||||
|
||||
// updating the sliderbar markers requires a lot of time, therefore we need to do this only if required
|
||||
if(lastRaceTimesInfo != null && lastRaceTimesInfo.legTimes.size() == newRaceTimesInfo.legTimes.size()) {
|
||||
requiresMarkerUpdate = false;
|
||||
for(int i = 0; i < newRaceTimesInfo.legTimes.size(); i++) {
|
||||
if(newRaceTimesInfo.legTimes.get(i).firstPassingDate.getTime() != lastRaceTimesInfo.legTimes.get(i).firstPassingDate.getTime()) {
|
||||
requiresMarkerUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (requiresMarkerUpdate && sliderBar.isMinMaxInitialized()) {
|
||||
sliderBar.clearMarkers();
|
||||
for (LegTimesInfoDTO legTimepointDTO : legTimepoints) {
|
||||
sliderBar.addMarker(legTimepointDTO.name, new Double(legTimepointDTO.firstPassingDate.getTime()));
|
||||
}
|
||||
sliderBar.redraw();
|
||||
}
|
||||
lastRaceTimesInfo = newRaceTimesInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-43
@@ -95,7 +95,6 @@ import com.sap.sailing.domain.tracking.GPSFixTrack;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
import com.sap.sailing.domain.tracking.MarkPassing;
|
||||
import com.sap.sailing.domain.tracking.RacesHandle;
|
||||
import com.sap.sailing.domain.tracking.TrackedLeg;
|
||||
import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
@@ -923,48 +922,15 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
|
||||
raceTimesInfo.endOfRace = trackedRace.getAssumedEnd() == null ? null : trackedRace.getAssumedEnd().asDate();
|
||||
List<LegTimesInfoDTO> legTimes = new ArrayList<LegTimesInfoDTO>();
|
||||
raceTimesInfo.setLegTimes(legTimes);
|
||||
Iterable<TrackedLeg> trackedLegs = trackedRace.getTrackedLegs();
|
||||
int i = 1;
|
||||
// Remark: sometimes it can happen that a mark passing with a wrong time stamp breaks the right time order of the leg times
|
||||
Date lastLegPassingTime = null;
|
||||
for (TrackedLeg trackedLeg : trackedLegs) {
|
||||
if (i == 1) {
|
||||
if (raceTimesInfo.startOfRace != null) {
|
||||
// For the race start, we'd like to show the race start date as provided by TrackedRace.getStart().
|
||||
// Therefore, the use of "firstPassingDate" is not exactly correct for the race start.
|
||||
LegTimesInfoDTO legTimepointDTO = new LegTimesInfoDTO("S");
|
||||
legTimepointDTO.firstPassingDate = raceTimesInfo.startOfRace;
|
||||
legTimepointDTO.lastPassingDate = raceTimesInfo.startOfRace;
|
||||
legTimes.add(legTimepointDTO);
|
||||
lastLegPassingTime = raceTimesInfo.startOfRace;
|
||||
}
|
||||
}
|
||||
Waypoint to = trackedLeg.getLeg().getTo();
|
||||
Iterable<MarkPassing> markPassings = trackedRace.getMarkPassingsInOrder(to);
|
||||
if (markPassings != null && Util.size(markPassings) > 0) {
|
||||
// ensure the passing date is in the right time order; there may perhaps be left-overs for marks to be reached later that
|
||||
// claim it has been passed in the past which may have been an accidental tracker read-out;
|
||||
// the results of getMarkPassingsInOrder(to) has by definition an ascending time-point ordering
|
||||
synchronized (markPassings) {
|
||||
boolean isFirstValidPassing = true;
|
||||
LegTimesInfoDTO legTimepointDTO = new LegTimesInfoDTO("L" + i++);
|
||||
for (MarkPassing currentMarkPassing : markPassings) {
|
||||
Date currentPassingDate = currentMarkPassing.getTimePoint().asDate();
|
||||
if(lastLegPassingTime == null || currentPassingDate.after(lastLegPassingTime)) {
|
||||
if(isFirstValidPassing) {
|
||||
legTimes.add(legTimepointDTO);
|
||||
legTimepointDTO.firstPassingDate = currentPassingDate;
|
||||
legTimepointDTO.lastPassingDate = currentPassingDate;
|
||||
lastLegPassingTime = currentPassingDate;
|
||||
isFirstValidPassing = false;
|
||||
} else {
|
||||
if (currentPassingDate.after(legTimepointDTO.lastPassingDate)) {
|
||||
legTimepointDTO.lastPassingDate = currentPassingDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Iterable<TimePoint> startTimesOfTrackedLegs = trackedRace.getStartTimesOfTrackedLegs();
|
||||
synchronized(startTimesOfTrackedLegs) {
|
||||
int legNumber = 1;
|
||||
for(TimePoint legStartTime: startTimesOfTrackedLegs) {
|
||||
LegTimesInfoDTO legTimepointDTO = new LegTimesInfoDTO(legNumber);
|
||||
legTimepointDTO.name = (legNumber == 1) ? "S" : "L" + legNumber;
|
||||
legTimepointDTO.firstPassingDate = legStartTime.asDate();
|
||||
legTimes.add(legTimepointDTO);
|
||||
legNumber++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -12,12 +12,13 @@ public class LegTimesInfoDTO extends NamedDTO implements IsSerializable {
|
||||
*/
|
||||
public Date firstPassingDate;
|
||||
|
||||
public Date lastPassingDate;
|
||||
// public Date lastPassingDate;
|
||||
|
||||
public int legNumber;
|
||||
|
||||
public LegTimesInfoDTO() {}
|
||||
|
||||
public LegTimesInfoDTO(String name) {
|
||||
super(name);
|
||||
public LegTimesInfoDTO(int legNumber) {
|
||||
this.legNumber = legNumber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+47
-4
@@ -1,19 +1,31 @@
|
||||
package com.sap.sailing.gwt.ui.shared.racemap;
|
||||
|
||||
import com.google.gwt.maps.client.geom.LatLng;
|
||||
import com.google.gwt.maps.client.geom.LatLngBounds;
|
||||
import com.google.gwt.maps.client.geom.Point;
|
||||
import com.google.gwt.maps.client.geom.Size;
|
||||
import com.google.gwt.maps.client.overlay.Overlay;
|
||||
import com.sap.sailing.gwt.ui.shared.CompetitorDTO;
|
||||
import com.sap.sailing.gwt.ui.shared.GPSFixDTO;
|
||||
|
||||
/**
|
||||
* A google map overlay based on a HTML5 canvas for drawing boats (images)
|
||||
* The boats will be zoomed/scaled according to the current map state and rotated according to the bearing of the boat.
|
||||
*/
|
||||
public class BoatCanvasOverlay extends CanvasOverlay {
|
||||
|
||||
/**
|
||||
* The competitor the boat belongs too.
|
||||
*/
|
||||
private final CompetitorDTO competitorDTO;
|
||||
|
||||
private final RaceMapImageManager raceMapImageManager;
|
||||
|
||||
/**
|
||||
* The current GPS fix used to draw the boat.
|
||||
*/
|
||||
private GPSFixDTO boatFix;
|
||||
|
||||
private final RaceMapImageManager raceMapImageManager;
|
||||
|
||||
public BoatCanvasOverlay(CompetitorDTO competitorDTO, RaceMapImageManager raceMapImageManager) {
|
||||
super();
|
||||
this.competitorDTO = competitorDTO;
|
||||
@@ -29,8 +41,7 @@ public class BoatCanvasOverlay extends CanvasOverlay {
|
||||
protected void redraw(boolean force) {
|
||||
if (boatFix != null) {
|
||||
ImageTransformer boatImageTransformer = raceMapImageManager.getBoatImageTransformer(boatFix, isSelected());
|
||||
double realBoatSizeScaleFactor = raceMapImageManager.getRealBoatSizeScaleFactor(boatImageTransformer
|
||||
.getImageSize());
|
||||
double realBoatSizeScaleFactor = getRealBoatSizeScaleFactor(boatImageTransformer.getImageSize());
|
||||
boatImageTransformer.drawToCanvas(getCanvas(), boatFix.speedWithBearing.bearingInDegrees, realBoatSizeScaleFactor);
|
||||
LatLng latLngPosition = LatLng.newInstance(boatFix.position.latDeg, boatFix.position.lngDeg);
|
||||
Point boatPositionInPx = getMap().convertLatLngToDivPixel(latLngPosition);
|
||||
@@ -46,4 +57,36 @@ public class BoatCanvasOverlay extends CanvasOverlay {
|
||||
public void setBoatFix(GPSFixDTO boatFix) {
|
||||
this.boatFix = boatFix;
|
||||
}
|
||||
|
||||
public double getRealBoatSizeScaleFactor(Size imageSize) {
|
||||
// the possible zoom level range is 0 to 21 (zoom level 0 would show the whole world)
|
||||
int zoomLevel = map == null ? 1 : map.getZoomLevel();
|
||||
double minScaleFactor = 0.45;
|
||||
double maxScaleFactor = 2.0;
|
||||
double realBoatSizeScaleFactor = minScaleFactor;
|
||||
// here it would be better to get the boat length from the boat class -> for now we assume a length of 5m
|
||||
double boatLengthInMeter = 5.0;
|
||||
// to scale the boats to a realistic size we need the length of the boat in pixel,
|
||||
// but it does not work to just take the image size, because the images for the different boat states can be different
|
||||
int boatLengthInPixel = 50;
|
||||
if (zoomLevel > 5) {
|
||||
LatLngBounds bounds = map.getBounds();
|
||||
if (bounds != null) {
|
||||
LatLng upperRight = bounds.getNorthEast();
|
||||
LatLng bottomLeft = bounds.getSouthWest();
|
||||
LatLng upperLeft = LatLng.newInstance(upperRight.getLatitude(), bottomLeft.getLongitude());
|
||||
double distXInMeters = upperLeft.distanceFrom(upperRight);
|
||||
int widthInPixel = map.getSize().getWidth();
|
||||
double realBoatSizeInPixel = (widthInPixel * boatLengthInMeter) / distXInMeters;
|
||||
realBoatSizeScaleFactor = realBoatSizeInPixel / (double) boatLengthInPixel;
|
||||
if (realBoatSizeScaleFactor < minScaleFactor) {
|
||||
realBoatSizeScaleFactor = minScaleFactor;
|
||||
}
|
||||
if (realBoatSizeScaleFactor > maxScaleFactor) {
|
||||
realBoatSizeScaleFactor = maxScaleFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return realBoatSizeScaleFactor;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -7,17 +7,36 @@ import com.google.gwt.maps.client.MapWidget;
|
||||
import com.google.gwt.maps.client.geom.LatLng;
|
||||
import com.google.gwt.maps.client.overlay.Overlay;
|
||||
|
||||
/**
|
||||
* This class provides an google map overlay based on a HTML5 canvas.
|
||||
* See {@link com.google.gwt.maps.client.overlay.Overlay} how to implement an overlay.
|
||||
*/
|
||||
public abstract class CanvasOverlay extends Overlay {
|
||||
|
||||
private final Canvas canvas;
|
||||
/**
|
||||
* The HTML5 canvas which can be used to draw arbitrary shapes on a google map.
|
||||
*/
|
||||
protected final Canvas canvas;
|
||||
|
||||
private boolean isSelected;
|
||||
/**
|
||||
* Indicates whether the canvas has been selected or not.
|
||||
*/
|
||||
protected boolean isSelected;
|
||||
|
||||
private MapWidget map;
|
||||
/**
|
||||
* The reference to the actual Google map.
|
||||
*/
|
||||
protected MapWidget map;
|
||||
|
||||
private MapPane pane;
|
||||
/**
|
||||
* The pane of the Google map containing the HTML DIV element of the canvas.
|
||||
*/
|
||||
protected MapPane pane;
|
||||
|
||||
private LatLng latLngPosition;
|
||||
/**
|
||||
* The position of the canvas as a Latitude/Longitude position
|
||||
*/
|
||||
protected LatLng latLngPosition;
|
||||
|
||||
public CanvasOverlay() {
|
||||
canvas = Canvas.createIfSupported();
|
||||
@@ -72,5 +91,4 @@ public abstract class CanvasOverlay extends Overlay {
|
||||
protected MapPane getPane() {
|
||||
return pane;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+85
-11
@@ -87,7 +87,7 @@ import com.sap.sailing.gwt.ui.shared.racemap.RaceMapZoomSettings.ZoomTypes;
|
||||
|
||||
public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener,
|
||||
Component<RaceMapSettings>, RequiresDataInitialization, RequiresResize {
|
||||
protected MapWidget map;
|
||||
private MapWidget map;
|
||||
|
||||
private final SailingServiceAsync sailingService;
|
||||
private final ErrorReporter errorReporter;
|
||||
@@ -98,15 +98,22 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
private final Map<CompetitorDTO, Polyline> tails;
|
||||
|
||||
/**
|
||||
* Polyline for the start line (connecting two buoys forming the start gate).
|
||||
* Polyline for the start line (connecting two buoys representing the start gate).
|
||||
*/
|
||||
private Polyline startLine;
|
||||
|
||||
/**
|
||||
* Polyline for the finish line (connecting two buoys forming the finish gate).
|
||||
* Polyline for the finish line (connecting two buoys representing the finish gate).
|
||||
*/
|
||||
private Polyline finishLine;
|
||||
|
||||
/**
|
||||
* Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat).
|
||||
*/
|
||||
private Polyline advantageLine;
|
||||
|
||||
private WindTrackInfoDTO lastCombinedWindTrackInfoDTO;
|
||||
|
||||
/**
|
||||
* Key set is equal to that of {@link #tails} and tells what the index in in {@link #fixes} of the first fix shown
|
||||
* in {@link #tails} is .
|
||||
@@ -148,11 +155,11 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
* markers displayed in response to
|
||||
* {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)}
|
||||
*/
|
||||
protected Set<Marker> maneuverMarkers;
|
||||
private Set<Marker> maneuverMarkers;
|
||||
|
||||
protected Map<CompetitorDTO, List<ManeuverDTO>> lastManeuverResult;
|
||||
private Map<CompetitorDTO, List<ManeuverDTO>> lastManeuverResult;
|
||||
|
||||
protected Map<CompetitorDTO, List<GPSFixDTO>> lastDouglasPeuckerResult;
|
||||
private Map<CompetitorDTO, List<GPSFixDTO>> lastDouglasPeuckerResult;
|
||||
|
||||
private LatLng lastMousePosition;
|
||||
|
||||
@@ -238,7 +245,6 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
Maps.loadMapsApi(mapsAPIKey, "2", false, new Runnable() {
|
||||
public void run() {
|
||||
map = new MapWidget();
|
||||
raceMapImageManager.setMap(map);
|
||||
map.addControl(new LargeMapControl3D(), new ControlPosition(ControlAnchor.TOP_RIGHT, /* offsetX */ 0, /* offsetY */ 30));
|
||||
map.addControl(new MenuMapTypeControl());
|
||||
map.addControl(new ScaleControl(), new ControlPosition(ControlAnchor.BOTTOM_RIGHT, /* offsetX */ 10, /* offsetY */ 20));
|
||||
@@ -247,7 +253,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
map.setContinuousZoom(true);
|
||||
RaceMap.this.add(map, 0, 0);
|
||||
RaceMap.this.add(combinedWindPanel, 10, 10);
|
||||
|
||||
RaceMap.this.raceMapImageManager.loadMapIcons(map);
|
||||
map.setSize("100%", "100%");
|
||||
map.addMapZoomEndHandler(new MapZoomEndHandler() {
|
||||
@Override
|
||||
@@ -344,11 +350,13 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
if (maneuverMarkers != null) {
|
||||
removeAllManeuverMarkers();
|
||||
}
|
||||
|
||||
// Do mark specific actions
|
||||
showMarksOnMap(raceMapDataDTO.coursePositions);
|
||||
showHelpLines(raceMapDataDTO.coursePositions);
|
||||
showStartAndFinishLines(raceMapDataDTO.coursePositions);
|
||||
showAdvantageLine();
|
||||
// Rezoom the map
|
||||
// TODO make this a loop across the LatLongBoundsCalculators, pulling them from a collection updated in updateSettings
|
||||
// TODO make this a loop across the LatLngBoundsCalculators, pulling them from a collection updated in updateSettings
|
||||
if (!getSettings().getZoomSettings().contains(ZoomTypes.NONE)) { // Auto zoom if setting is not manual
|
||||
LatLngBounds bounds = getSettings().getZoomSettings().getNewBounds(RaceMap.this);
|
||||
zoomMapToNewBounds(bounds);
|
||||
@@ -394,6 +402,9 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
break;
|
||||
case COMBINED:
|
||||
showCombinedWindOnMap(windSource, windTrackInfoDTO);
|
||||
if(windTrackInfoDTO != null) {
|
||||
lastCombinedWindTrackInfoDTO = windTrackInfoDTO;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -589,7 +600,70 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelpLines(CourseDTO courseDTO) {
|
||||
/*
|
||||
* This algorithm is limited to distances such that dlon < pi/2, i.e those that extend around less than one quarter of the circumference
|
||||
* of the earth in longitude. A completely general, but more complicated algorithm is necessary if greater distances are allowed.
|
||||
*/
|
||||
public LatLng calculatePositionAlongRhumbline(LatLng position, double bearingDeg, double distanceInKm) {
|
||||
double distianceRad = distanceInKm / 6371.0; // r = 6371 means earth's radius in km
|
||||
double lat1 = position.getLatitudeRadians();
|
||||
double lon1 = position.getLongitudeRadians();
|
||||
double bearingRad = bearingDeg / 180. * Math.PI;
|
||||
|
||||
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distianceRad) +
|
||||
Math.cos(lat1) * Math.sin(distianceRad) * Math.cos(bearingRad));
|
||||
double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distianceRad)*Math.cos(lat1),
|
||||
Math.cos(distianceRad)-Math.sin(lat1)*Math.sin(lat2));
|
||||
lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalize to -180..+180º
|
||||
|
||||
return LatLng.newInstance(lat2 / Math.PI * 180., lon2 / Math.PI * 180.);
|
||||
}
|
||||
|
||||
private void showAdvantageLine() {
|
||||
if(map != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null && lastCombinedWindTrackInfoDTO.windFixes.size() > 0) {
|
||||
final CompetitorDTO leadingCompetitorDTO = quickRanks.get(0).competitor;
|
||||
if (leadingCompetitorDTO != null && lastShownFix.containsKey(leadingCompetitorDTO) && lastShownFix.get(leadingCompetitorDTO) != -1) {
|
||||
GPSFixDTO lastBoatFix = getBoatFix(leadingCompetitorDTO);
|
||||
|
||||
double advantageLineLengthInKm = 1.0;
|
||||
// implement and use Position.translateRhumb()
|
||||
double bearingOfBoatInDeg = lastBoatFix.speedWithBearing.bearingInDegrees;
|
||||
LatLng boatPosition = LatLng.newInstance(lastBoatFix.position.latDeg, lastBoatFix.position.lngDeg);
|
||||
LatLng posAheadOfFirstBoat = calculatePositionAlongRhumbline(boatPosition, bearingOfBoatInDeg, 0.05);
|
||||
double bearingOfCombinedWindInDeg = lastCombinedWindTrackInfoDTO.windFixes.get(0).trueWindBearingDeg;
|
||||
double rotatedBearingDeg = bearingOfCombinedWindInDeg + 90.0;
|
||||
if(rotatedBearingDeg >= 360.0)
|
||||
rotatedBearingDeg -= 360.0;
|
||||
LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, rotatedBearingDeg, advantageLineLengthInKm / 2.0);
|
||||
rotatedBearingDeg = bearingOfCombinedWindInDeg - 90.0;
|
||||
if(rotatedBearingDeg < 0.0)
|
||||
rotatedBearingDeg += 360.0;
|
||||
LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, rotatedBearingDeg, advantageLineLengthInKm / 2.0);
|
||||
|
||||
LatLng[] advantageLinePoints = new LatLng[2];
|
||||
advantageLinePoints[0] = LatLng.newInstance(advantageLinePos1.getLatitude(), advantageLinePos1.getLongitude());
|
||||
advantageLinePoints[1] = LatLng.newInstance(advantageLinePos2.getLatitude(), advantageLinePos2.getLongitude());;
|
||||
if(advantageLine == null) {
|
||||
PolylineOptions options = PolylineOptions.newInstance(/* clickable */false, /* geodesic */true);
|
||||
advantageLine = new Polyline(advantageLinePoints, /* color */ "#000000", /* width */ 1, /* opacity */0.5, options);
|
||||
map.addOverlay(advantageLine);
|
||||
} else {
|
||||
advantageLine.deleteVertex(1);
|
||||
advantageLine.deleteVertex(0);
|
||||
advantageLine.insertVertex(0, advantageLinePoints[0]);
|
||||
advantageLine.insertVertex(1, advantageLinePoints[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(advantageLine != null) {
|
||||
advantageLine.deleteVertex(1);
|
||||
advantageLine.deleteVertex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showStartAndFinishLines(final CourseDTO courseDTO) {
|
||||
if(map != null && courseDTO != null) {
|
||||
if(courseDTO.startGate != null) {
|
||||
LatLng[] startGatePoints = new LatLng[2];
|
||||
|
||||
+63
-100
@@ -5,10 +5,7 @@ import java.util.Map;
|
||||
|
||||
import com.google.gwt.core.client.GWT;
|
||||
import com.google.gwt.maps.client.MapWidget;
|
||||
import com.google.gwt.maps.client.geom.LatLng;
|
||||
import com.google.gwt.maps.client.geom.LatLngBounds;
|
||||
import com.google.gwt.maps.client.geom.Point;
|
||||
import com.google.gwt.maps.client.geom.Size;
|
||||
import com.google.gwt.maps.client.overlay.Icon;
|
||||
import com.sap.sailing.domain.common.LegType;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
@@ -71,17 +68,15 @@ public class RaceMapImageManager {
|
||||
|
||||
protected Map<Pair<ManeuverType, Tack>, Icon> maneuverIconsForTypeAndTargetTack;
|
||||
|
||||
private MapWidget map;
|
||||
|
||||
private static RaceMapResources resources = GWT.create(RaceMapResources.class);
|
||||
|
||||
public RaceMapImageManager() {
|
||||
maneuverIconsForTypeAndTargetTack = new HashMap<Pair<ManeuverType, Tack>, Icon>();
|
||||
|
||||
boatIconDownwindPortTransformer = new ImageTransformer(resources.lowlightedBoatIconDW_Port());
|
||||
boatIconHighlightedDownwindPortTransformer = new ImageTransformer(resources.highlightedBoatIconDW_Port());
|
||||
boatIconDownwindStarboardTransformer = new ImageTransformer(resources.lowlightedBoatIconDW_Starboard());
|
||||
boatIconHighlightedDownwindStarboardTransformer = new ImageTransformer(resources
|
||||
.highlightedBoatIconDW_Starboard());
|
||||
boatIconHighlightedDownwindStarboardTransformer = new ImageTransformer(resources.highlightedBoatIconDW_Starboard());
|
||||
boatIconPortTransformer = new ImageTransformer(resources.lowlightedBoatIcon_Port());
|
||||
boatIconHighlightedPortTransformer = new ImageTransformer(resources.highlightedBoatIcon_Port());
|
||||
boatIconStarboardTransformer = new ImageTransformer(resources.lowlightedBoatIcon_Starboard());
|
||||
@@ -90,68 +85,69 @@ public class RaceMapImageManager {
|
||||
expeditionWindIconTransformer = new ImageTransformer(resources.expeditionWindIcon());
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when the map API has finished loading. Up to this point, {@link #buoyIcon} and the {@link #maneuverIconsForTypeAndTargetTack}
|
||||
* are not propertly initialized.
|
||||
/*
|
||||
* Loads the map overlay icons
|
||||
* The method can only be called after the map is loaded
|
||||
*/
|
||||
public void setMap(MapWidget map) {
|
||||
this.map = map;
|
||||
buoyIcon = Icon.newInstance(resources.buoyIcon().getSafeUri().asString());
|
||||
buoyIcon.setIconAnchor(Point.newInstance(4, 4));
|
||||
public void loadMapIcons(MapWidget map) {
|
||||
if(map != null) {
|
||||
buoyIcon = Icon.newInstance(resources.buoyIcon().getSafeUri().asString());
|
||||
buoyIcon.setIconAnchor(Point.newInstance(4, 4));
|
||||
|
||||
Icon tackToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|00FF00|000000");
|
||||
tackToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.TACK, Tack.STARBOARD), tackToStarboardIcon);
|
||||
Icon tackToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|FF0000|000000");
|
||||
tackToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.TACK, Tack.PORT), tackToPortIcon);
|
||||
Icon jibeToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|00FF00|000000");
|
||||
jibeToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.JIBE, Tack.STARBOARD), jibeToStarboardIcon);
|
||||
Icon jibeToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|FF0000|000000");
|
||||
jibeToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.JIBE, Tack.PORT), jibeToPortIcon);
|
||||
Icon headUpOnStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|00FF00|000000");
|
||||
headUpOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.HEAD_UP, Tack.STARBOARD), headUpOnStarboardIcon);
|
||||
Icon headUpOnPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|FF0000|000000");
|
||||
headUpOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.HEAD_UP, Tack.PORT), headUpOnPortIcon);
|
||||
Icon bearAwayOnStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|00FF00|000000");
|
||||
bearAwayOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.BEAR_AWAY, Tack.STARBOARD), bearAwayOnStarboardIcon);
|
||||
Icon bearAwayOnPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|FF0000|000000");
|
||||
bearAwayOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.BEAR_AWAY, Tack.PORT), bearAwayOnPortIcon);
|
||||
Icon markPassingToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|00FF00|000000");
|
||||
markPassingToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.MARK_PASSING, Tack.STARBOARD), markPassingToStarboardIcon);
|
||||
Icon markPassingToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|FF0000|000000");
|
||||
markPassingToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.MARK_PASSING, Tack.PORT), markPassingToPortIcon);
|
||||
Icon unknownManeuverIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=?|FFFFFF|000000");
|
||||
unknownManeuverIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.UNKNOWN, Tack.STARBOARD), unknownManeuverIcon);
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.UNKNOWN, Tack.PORT), unknownManeuverIcon);
|
||||
Icon penaltyCircleToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|00FF00|000000");
|
||||
penaltyCircleToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.PENALTY_CIRCLE, Tack.STARBOARD), penaltyCircleToStarboardIcon);
|
||||
Icon penaltyCircleToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|FF0000|000000");
|
||||
penaltyCircleToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.PENALTY_CIRCLE, Tack.PORT), penaltyCircleToPortIcon);
|
||||
Icon tackToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|00FF00|000000");
|
||||
tackToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.TACK, Tack.STARBOARD), tackToStarboardIcon);
|
||||
Icon tackToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|FF0000|000000");
|
||||
tackToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.TACK, Tack.PORT), tackToPortIcon);
|
||||
Icon jibeToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|00FF00|000000");
|
||||
jibeToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.JIBE, Tack.STARBOARD), jibeToStarboardIcon);
|
||||
Icon jibeToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|FF0000|000000");
|
||||
jibeToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.JIBE, Tack.PORT), jibeToPortIcon);
|
||||
Icon headUpOnStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|00FF00|000000");
|
||||
headUpOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.HEAD_UP, Tack.STARBOARD), headUpOnStarboardIcon);
|
||||
Icon headUpOnPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|FF0000|000000");
|
||||
headUpOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.HEAD_UP, Tack.PORT), headUpOnPortIcon);
|
||||
Icon bearAwayOnStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|00FF00|000000");
|
||||
bearAwayOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.BEAR_AWAY, Tack.STARBOARD), bearAwayOnStarboardIcon);
|
||||
Icon bearAwayOnPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|FF0000|000000");
|
||||
bearAwayOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.BEAR_AWAY, Tack.PORT), bearAwayOnPortIcon);
|
||||
Icon markPassingToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|00FF00|000000");
|
||||
markPassingToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.MARK_PASSING, Tack.STARBOARD), markPassingToStarboardIcon);
|
||||
Icon markPassingToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|FF0000|000000");
|
||||
markPassingToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.MARK_PASSING, Tack.PORT), markPassingToPortIcon);
|
||||
Icon unknownManeuverIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=?|FFFFFF|000000");
|
||||
unknownManeuverIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.UNKNOWN, Tack.STARBOARD), unknownManeuverIcon);
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.UNKNOWN, Tack.PORT), unknownManeuverIcon);
|
||||
Icon penaltyCircleToStarboardIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|00FF00|000000");
|
||||
penaltyCircleToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.PENALTY_CIRCLE, Tack.STARBOARD), penaltyCircleToStarboardIcon);
|
||||
Icon penaltyCircleToPortIcon = Icon
|
||||
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|FF0000|000000");
|
||||
penaltyCircleToPortIcon.setIconAnchor(Point.newInstance(10, 33));
|
||||
maneuverIconsForTypeAndTargetTack.put(new Pair<ManeuverType, Tack>(ManeuverType.PENALTY_CIRCLE, Tack.PORT), penaltyCircleToPortIcon);
|
||||
}
|
||||
}
|
||||
|
||||
public ImageTransformer getBoatImageTransformer(GPSFixDTO boatFix, boolean highlighted) {
|
||||
@@ -193,37 +189,4 @@ public class RaceMapImageManager {
|
||||
public ImageTransformer getExpeditionWindIconTransformer() {
|
||||
return expeditionWindIconTransformer;
|
||||
}
|
||||
|
||||
public double getRealBoatSizeScaleFactor(Size imageSize) {
|
||||
// the possible zoom level range is 0 to 21 (zoom level 0 would show the whole world)
|
||||
int zoomLevel = map == null ? 1 : map.getZoomLevel();
|
||||
double minScaleFactor = 0.45;
|
||||
double maxScaleFactor = 2.0;
|
||||
double realBoatSizeScaleFactor = minScaleFactor;
|
||||
// here it would be better to get the boat length from the boat class -> for now we assume a length of 5m
|
||||
double boatLengthInMeter = 5.0;
|
||||
// to scale the boats to a realistic size we need the length of the boat in pixel,
|
||||
// but it does not work to just take the image size, because the images for the different boat states can be different
|
||||
// int boatLengthInPixel = 40;
|
||||
int boatLengthInPixel = 50;
|
||||
if (zoomLevel > 5) {
|
||||
LatLngBounds bounds = map.getBounds();
|
||||
if (bounds != null) {
|
||||
LatLng upperRight = bounds.getNorthEast();
|
||||
LatLng bottomLeft = bounds.getSouthWest();
|
||||
LatLng upperLeft = LatLng.newInstance(upperRight.getLatitude(), bottomLeft.getLongitude());
|
||||
double distXInMeters = upperLeft.distanceFrom(upperRight);
|
||||
int widthInPixel = map.getSize().getWidth();
|
||||
double realBoatSizeInPixel = (widthInPixel * boatLengthInMeter) / distXInMeters;
|
||||
realBoatSizeScaleFactor = realBoatSizeInPixel / (double) boatLengthInPixel;
|
||||
if (realBoatSizeScaleFactor < minScaleFactor) {
|
||||
realBoatSizeScaleFactor = minScaleFactor;
|
||||
}
|
||||
if (realBoatSizeScaleFactor > maxScaleFactor) {
|
||||
realBoatSizeScaleFactor = maxScaleFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return realBoatSizeScaleFactor;
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -10,12 +10,22 @@ import com.sap.sailing.gwt.ui.shared.PositionDTO;
|
||||
import com.sap.sailing.gwt.ui.shared.WindDTO;
|
||||
import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO;
|
||||
|
||||
/**
|
||||
* A google map overlay based on a HTML5 canvas for drawing a wind sensor (as an rotating arrow)
|
||||
* The wind sensor symbol will be rotated according to the wind data.
|
||||
*/
|
||||
public class WindSensorOverlay extends CanvasOverlay {
|
||||
|
||||
private final RaceMapImageManager raceMapImageManager;
|
||||
|
||||
/**
|
||||
* The current wind track used to draw the wind sensor.
|
||||
*/
|
||||
private WindTrackInfoDTO windTrackInfoDTO;
|
||||
|
||||
/**
|
||||
* The current wind source used to draw the wind sensor.
|
||||
*/
|
||||
private WindSource windSource;
|
||||
|
||||
private final ImageTransformer transformer;
|
||||
|
||||
Reference in New Issue
Block a user