Merge remote-tracking branch 'server/master'

This commit is contained in:
Axel Uhl
2012-04-17 18:00:48 +02:00
12 changed files with 468 additions and 249 deletions
@@ -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)} &lt;= 0</code>
*/
@@ -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);
@@ -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;
}
@@ -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
@@ -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++;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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];
@@ -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,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;