refactoring of RaceMapPanel into RaceMapPanel, RaceMap und RaceMapData

This commit is contained in:
fmittag committed 2012-01-10 14:59:14 +01:00
1 parent 38374491d4
commit 04784c9e18
4 files changed
+1103 -971

No files matched your search

@@ -71,7 +71,7 @@ public class ImageRotator {
context.translate(canvasRadius, canvasRadius);
context.rotate(angleInRadians);
context.scale(scaleFactor, scaleFactor);
context.drawImage(imageElement, -imageWidth/2, -imageHeight/2);
context.drawImage(imageElement, (-imageWidth/2) * scaleFactor, (-imageHeight/2) * scaleFactor);
result = canvas.toDataUrl("image/png");
context.restore();
}
@@ -0,0 +1,844 @@
package com.sap.sailing.gwt.ui.adminconsole;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gwt.core.client.GWT;
import com.google.gwt.maps.client.InfoWindowContent;
import com.google.gwt.maps.client.MapType;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.Maps;
import com.google.gwt.maps.client.control.LargeMapControl3D;
import com.google.gwt.maps.client.control.MenuMapTypeControl;
import com.google.gwt.maps.client.control.ScaleControl;
import com.google.gwt.maps.client.event.MapDragEndHandler;
import com.google.gwt.maps.client.event.MapMouseMoveHandler;
import com.google.gwt.maps.client.event.MapZoomEndHandler;
import com.google.gwt.maps.client.event.MarkerClickHandler;
import com.google.gwt.maps.client.event.MarkerMouseOutHandler;
import com.google.gwt.maps.client.event.MarkerMouseOverHandler;
import com.google.gwt.maps.client.event.PolylineClickHandler;
import com.google.gwt.maps.client.event.PolylineMouseOutHandler;
import com.google.gwt.maps.client.event.PolylineMouseOverHandler;
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.Projection;
import com.google.gwt.maps.client.overlay.Icon;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.MarkerOptions;
import com.google.gwt.maps.client.overlay.Polyline;
import com.google.gwt.maps.client.overlay.PolylineOptions;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.Tack;
import com.sap.sailing.domain.common.Util.Triple;
import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.client.Timer;
import com.sap.sailing.gwt.ui.shared.CompetitorDAO;
import com.sap.sailing.gwt.ui.shared.EventDAO;
import com.sap.sailing.gwt.ui.shared.GPSFixDAO;
import com.sap.sailing.gwt.ui.shared.ManeuverDAO;
import com.sap.sailing.gwt.ui.shared.MarkDAO;
import com.sap.sailing.gwt.ui.shared.RaceDAO;
import com.sap.sailing.gwt.ui.shared.RegattaDAO;
import com.sap.sailing.server.api.EventNameAndRaceName;
public class RaceMap {
protected MapWidget map;
private final SailingServiceAsync sailingService;
private final ErrorReporter errorReporter;
/**
* Two sails on downwind leg, wind from port (sails on starboard); no highlighting
*/
protected ImageRotator boatIconDownwindPortRotator;
/**
* Two sails on downwind leg, wind from port (sails on starboard); with highlighting
*/
protected ImageRotator boatIconHighlightedDownwindPortRotator;
/**
* Two sails on downwind leg, wind from starboard (sails on port); no highlighting
*/
protected ImageRotator boatIconDownwindStarboardRotator;
/**
* Two sails on downwind leg, wind from starboard (sails on port); with highlighting
*/
protected ImageRotator boatIconHighlightedDownwindStarboardRotator;
/**
* One sail, wind from port (sails on starboard); no highlighting
*/
protected ImageRotator boatIconPortRotator;
/**
* One sail, wind from port (sails on starboard); with highlighting
*/
protected ImageRotator boatIconHighlightedPortRotator;
/**
* One sail, wind from starboard (sails on port); no highlighting
*/
protected ImageRotator boatIconStarboardRotator;
/**
* One sail, wind from starboard (sails on port); with highlighting
*/
protected ImageRotator boatIconHighlightedStarboardRotator;
protected Icon buoyIcon;
protected Icon tackToStarboardIcon;
protected Icon tackToPortIcon;
protected Icon jibeToStarboardIcon;
protected Icon jibeToPortIcon;
protected Icon markPassingToStarboardIcon;
protected Icon markPassingToPortIcon;
protected Icon headUpOnStarboardIcon;
protected Icon headUpOnPortIcon;
protected Icon bearAwayOnStarboardIcon;
protected Icon bearAwayOnPortIcon;
protected Icon unknownManeuverIcon;
protected Icon penaltyCircleToStarboardIcon;
protected Icon penaltyCircleToPortIcon;
protected LatLng lastMousePosition;
protected long tailLengthInMilliSeconds = 30000l;
protected List<Triple<EventDAO, RegattaDAO, RaceDAO>> selectedEventAndRace;
/**
* If the user explicitly zoomed or panned the map, don't adjust zoom/pan unless a new race is selected
*/
protected boolean mapZoomedOrPannedSinceLastRaceSelectionChange = false;
/**
* Used to check if the first initial zoom to the buoy markers was already done.
*/
protected boolean mapFirstZoomDone = false;
// key for domain web4sap.com
protected final String mapsAPIKey = "ABQIAAAAmvjPh3ZpHbnwuX3a66lDqRRLCigyC_gRDASMpyomD2do5awpNhRCyD_q-27hwxKe_T6ivSZ_0NgbUg";
protected static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class);
protected final RaceMapData data;
private final Timer timer;
protected boolean showDouglasPeuckerPoints = false;
protected boolean showManeuverHeadUp = false;
protected boolean showManeuverBearAway = false;
protected boolean showManeuverTack = false;
protected boolean showManeuverJibe = false;
protected boolean showManeuverPenaltyCircle = false;
protected boolean showManeuverMarkPassing = false;
protected boolean showManeuverOther = false;
public RaceMap(SailingServiceAsync sailingService, ErrorReporter errorReporter, Timer timer) {
this.sailingService = sailingService;
this.errorReporter = errorReporter;
this.timer = timer;
data = new RaceMapData();
}
public void loadMapsAPI(final Grid grid, final int gridRow, final int gridColumn) {
Maps.loadMapsApi(mapsAPIKey, "2", false, new Runnable() {
public void run() {
map = new MapWidget();
map.addControl(new LargeMapControl3D());
map.addControl(new MenuMapTypeControl());
map.addControl(new ScaleControl());
// Add the map to the HTML host page
map.setSize("100%", "100%");
map.setScrollWheelZoomEnabled(true);
map.setContinuousZoom(true);
MapType currentMapType = map.getCurrentMapType();
Projection projection = currentMapType.getProjection();
grid.setWidget(gridRow, gridColumn, map);
map.addMapZoomEndHandler(new MapZoomEndHandler() {
@Override
public void onZoomEnd(MapZoomEndEvent event) {
mapZoomedOrPannedSinceLastRaceSelectionChange = true;
}
});
map.addMapDragEndHandler(new MapDragEndHandler() {
@Override
public void onDragEnd(MapDragEndEvent event) {
mapZoomedOrPannedSinceLastRaceSelectionChange = true;
}
});
map.addMapMouseMoveHandler(new MapMouseMoveHandler() {
@Override
public void onMouseMove(MapMouseMoveEvent event) {
lastMousePosition = event.getLatLng();
}
});
boatIconDownwindPortRotator = new ImageRotator(resources.lowlightedBoatIconDW_Port());
boatIconHighlightedDownwindPortRotator = new ImageRotator(resources.highlightedBoatIconDW_Port());
boatIconDownwindStarboardRotator = new ImageRotator(resources.lowlightedBoatIconDW_Starboard());
boatIconHighlightedDownwindStarboardRotator = new ImageRotator(resources
.highlightedBoatIconDW_Starboard());
boatIconPortRotator = new ImageRotator(resources.lowlightedBoatIcon_Port());
boatIconHighlightedPortRotator = new ImageRotator(resources.highlightedBoatIcon_Port());
boatIconStarboardRotator = new ImageRotator(resources.lowlightedBoatIcon_Starboard());
boatIconHighlightedStarboardRotator = new ImageRotator(resources.highlightedBoatIcon_Starboard());
buoyIcon = Icon.newInstance(resources.buoyIcon().getSafeUri().asString());
buoyIcon.setIconAnchor(Point.newInstance(4, 4));
tackToStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|00FF00|000000");
tackToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
tackToPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=T|FF0000|000000");
tackToPortIcon.setIconAnchor(Point.newInstance(10, 33));
jibeToStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|00FF00|000000");
jibeToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
jibeToPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=J|FF0000|000000");
jibeToPortIcon.setIconAnchor(Point.newInstance(10, 33));
headUpOnStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|00FF00|000000");
headUpOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
headUpOnPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=H|FF0000|000000");
headUpOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
bearAwayOnStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|00FF00|000000");
bearAwayOnStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
bearAwayOnPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=B|FF0000|000000");
bearAwayOnPortIcon.setIconAnchor(Point.newInstance(10, 33));
markPassingToStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|00FF00|000000");
markPassingToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
markPassingToPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=M|FF0000|000000");
markPassingToPortIcon.setIconAnchor(Point.newInstance(10, 33));
unknownManeuverIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=?|FFFFFF|000000");
unknownManeuverIcon.setIconAnchor(Point.newInstance(10, 33));
penaltyCircleToStarboardIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|00FF00|000000");
penaltyCircleToStarboardIcon.setIconAnchor(Point.newInstance(10, 33));
penaltyCircleToPortIcon = Icon
.newInstance("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=P|FF0000|000000");
penaltyCircleToPortIcon.setIconAnchor(Point.newInstance(10, 33));
}
});
}
/**
* From {@link #data.fixes} as well as the selection of {@link #getCompetitorsToShow competitors to show}, computes the
* from/to times for which to request GPS raceMapData.fixes from the server. No update is performed here to {@link #data.fixes}. The
* result guarantees that, when used in
* {@link SailingServiceAsync#getBoatPositions(String, String, Map, Map, boolean, AsyncCallback)}, for each
* competitor from {@link #competitorsToShow} there are all raceMapData.fixes known by the server for that competitor starting
* at <code>upTo-{@link #tailLengthInMilliSeconds}</code> and ending at <code>upTo</code> (exclusive).
*
* @return a triple whose {@link Triple#getA() first} component contains the "from", and whose {@link Triple#getB()
* second} component contains the "to" times for the competitors whose trails / positions to show; the
* {@link Triple#getC() third} component tells whether the existing raceMapData.fixes can remain and be augmented by
* those requested or need to be replaced
*/
protected Triple<Map<CompetitorDAO, Date>, Map<CompetitorDAO, Date>, Map<CompetitorDAO, Boolean>> computeFromAndTo(
Date upTo, Collection<CompetitorDAO> competitorsToShow) {
Date tailstart = new Date(upTo.getTime() - tailLengthInMilliSeconds);
Map<CompetitorDAO, Date> from = new HashMap<CompetitorDAO, Date>();
Map<CompetitorDAO, Date> to = new HashMap<CompetitorDAO, Date>();
Map<CompetitorDAO, Boolean> overlapWithKnownFixes = new HashMap<CompetitorDAO, Boolean>();
for (CompetitorDAO competitor : competitorsToShow) {
List<GPSFixDAO> fixesForCompetitor = data.fixes.get(competitor);
Date fromDate;
Date toDate;
Date timepointOfLastKnownFix = fixesForCompetitor == null ? null
: data.getTimepointOfLastNonExtrapolated(fixesForCompetitor);
Date timepointOfFirstKnownFix = fixesForCompetitor == null ? null
: data.getTimepointOfFirstNonExtrapolated(fixesForCompetitor);
boolean overlap = false;
if (fixesForCompetitor != null && timepointOfFirstKnownFix != null
&& !tailstart.before(timepointOfFirstKnownFix) && timepointOfLastKnownFix != null
&& !tailstart.after(timepointOfLastKnownFix)) {
// the beginning of what we need is contained in the interval we already have; skip what we already have
fromDate = timepointOfLastKnownFix;
overlap = true;
} else {
fromDate = tailstart;
}
if (fixesForCompetitor != null && timepointOfFirstKnownFix != null
&& !upTo.before(timepointOfFirstKnownFix) && timepointOfLastKnownFix != null
&& !upTo.after(timepointOfLastKnownFix)) {
// the end of what we need is contained in the interval we already have; skip what we already have
toDate = timepointOfFirstKnownFix;
overlap = true;
} else {
toDate = upTo;
}
// only request something for the competitor if we're missing information at all
if (fromDate.before(toDate) || fromDate.equals(toDate)) {
from.put(competitor, fromDate);
to.put(competitor, toDate);
overlapWithKnownFixes.put(competitor, overlap);
}
}
return new Triple<Map<CompetitorDAO, Date>, Map<CompetitorDAO, Date>, Map<CompetitorDAO, Boolean>>(from, to,
overlapWithKnownFixes);
}
/**
* Adds the raceMapData.fixes received in <code>result</code> to {@link #data.fixes} and ensures they are still contiguous for each
* competitor. If <code>overlapsWithKnownraceMapData.fixes</code> indicates that the raceMapData.fixes received in <code>result</code>
* overlap with those already known, the raceMapData.fixes are merged into the list of already known raceMapData.fixes for the competitor.
* Otherwise, the raceMapData.fixes received in <code>result</code> replace those known so far for the respective competitor.
*/
protected void updateFixes(Map<CompetitorDAO, List<GPSFixDAO>> result,
Map<CompetitorDAO, Boolean> overlapsWithKnownFixes) {
for (Map.Entry<CompetitorDAO, List<GPSFixDAO>> e : result.entrySet()) {
if (e.getValue() != null && !e.getValue().isEmpty()) {
List<GPSFixDAO> fixesForCompetitor = data.fixes.get(e.getKey());
if (fixesForCompetitor == null) {
fixesForCompetitor = new ArrayList<GPSFixDAO>();
data.fixes.put(e.getKey(), fixesForCompetitor);
}
if (!overlapsWithKnownFixes.get(e.getKey())) {
fixesForCompetitor.clear();
// to re-establish the invariants for raceMapData.tails, raceMapData.firstShownFix and raceMapData.lastShownFix, we now need to remove
// all
// points from the competitor's polyline and clear the entries in raceMapData.firstShownFix and raceMapData.lastShownFix
if (map != null && data.tails.containsKey(e.getKey())) {
map.removeOverlay(data.tails.remove(e.getKey()));
}
data.firstShownFix.remove(e.getKey());
data.lastShownFix.remove(e.getKey());
fixesForCompetitor.addAll(e.getValue());
} else {
data.mergeFixes(e.getKey(), e.getValue());
}
}
}
}
protected void showMarksOnMap(List<MarkDAO> result) {
if (map != null) {
Set<MarkDAO> toRemove = new HashSet<MarkDAO>(data.buoyMarkers.keySet());
for (MarkDAO markDAO : result) {
Marker buoyMarker = data.buoyMarkers.get(markDAO);
if (buoyMarker == null) {
buoyMarker = createBuoyMarker(markDAO);
data.buoyMarkers.put(markDAO, buoyMarker);
map.addOverlay(buoyMarker);
} else {
buoyMarker.setLatLng(LatLng.newInstance(markDAO.position.latDeg, markDAO.position.lngDeg));
toRemove.remove(markDAO);
}
}
for (MarkDAO toRemoveMarkDAO : toRemove) {
Marker marker = data.buoyMarkers.remove(toRemoveMarkDAO);
map.removeOverlay(marker);
}
zoomMapFirstTimeToMarks(data.buoyMarkers.keySet());
}
}
/**
* Zooms the map to the given marks if the map was not zoomed yet. If the map zoom to the marks was successful one
* time, the initial zoom to these marks can not be done again except the race selection is changed via {@link RaceMapPanel#onRaceSelectionChange(List).
*
* @param marksToZoomAt
* the marks to zoom at
*/
protected void zoomMapFirstTimeToMarks(Set<MarkDAO> marksToZoomAt) {
if (!mapZoomedOrPannedSinceLastRaceSelectionChange && !mapFirstZoomDone) {
LatLngBounds newBounds = null;
if (marksToZoomAt != null && !marksToZoomAt.isEmpty()) {
for (MarkDAO markDAO : marksToZoomAt) {
LatLng latLngZoomFirstTime = LatLng.newInstance(markDAO.position.latDeg, markDAO.position.lngDeg);
LatLngBounds bounds = LatLngBounds.newInstance(latLngZoomFirstTime, latLngZoomFirstTime);
if (newBounds == null) {
newBounds = bounds;
} else {
newBounds.extend(bounds.getNorthEast());
newBounds.extend(bounds.getSouthWest());
}
}
}
if (newBounds != null) {
map.setZoomLevel(map.getBoundsZoomLevel(newBounds));
map.setCenter(newBounds.getCenter());
map.checkResizeAndCenter();
mapFirstZoomDone = true;
/*
* Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that the map was just zoomed
* to the bounds of the buoys, it was not a zoom or pan triggered by the user. As a consequence the
* mapZoomedOrPannedSinceLastRaceSelection option has to reset again.
*/
mapZoomedOrPannedSinceLastRaceSelectionChange = false;
}
}
}
/**
* @param from
* time point for first fix to show in raceMapData.tails
* @param to
* time point for last fix to show in raceMapData.tails
*/
protected void showBoatsOnMap(Date from, Date to, Collection<CompetitorDAO> competitorsToShow,
Set<CompetitorDAO> competitorsSelectedInMap) {
if (map != null) {
LatLngBounds newMapBounds = null;
Set<CompetitorDAO> competitorDAOsOfUnusedTails = new HashSet<CompetitorDAO>(data.tails.keySet());
Set<CompetitorDAO> competitorDAOsOfUnusedMarkers = new HashSet<CompetitorDAO>(data.boatMarkers.keySet());
for (CompetitorDAO competitorDAO : competitorsToShow) {
if (data.fixes.containsKey(competitorDAO)) {
Polyline tail = data.tails.get(competitorDAO);
if (tail == null) {
tail = createTailAndUpdateIndices(competitorDAO, from, to);
map.addOverlay(tail);
} else {
data.updateTail(tail, competitorDAO, from, to);
competitorDAOsOfUnusedTails.remove(competitorDAO);
}
LatLngBounds bounds = tail.getBounds();
if (newMapBounds == null) {
newMapBounds = bounds;
} else {
newMapBounds.extend(bounds.getNorthEast());
newMapBounds.extend(bounds.getSouthWest());
}
if (data.lastShownFix.containsKey(competitorDAO) && data.lastShownFix.get(competitorDAO) != -1) {
GPSFixDAO lastPos = data.getBoatFix(competitorDAO);
Marker boatMarker = data.boatMarkers.get(competitorDAO);
if (boatMarker == null) {
boatMarker = createBoatMarker(competitorDAO, false);
map.addOverlay(boatMarker);
data.boatMarkers.put(competitorDAO, boatMarker);
} else {
competitorDAOsOfUnusedMarkers.remove(competitorDAO);
// check if anchors match; re-use marker with setImage only if anchors match
Point newAnchor = getBoatImageRotator(lastPos,
competitorsSelectedInMap.contains(competitorDAO)).getAnchor();
Point oldAnchor = boatMarker.getIcon().getIconAnchor();
if (oldAnchor.getX() == newAnchor.getX() && oldAnchor.getY() == newAnchor.getY()) {
boatMarker.setLatLng(LatLng.newInstance(lastPos.position.latDeg,
lastPos.position.lngDeg));
boatMarker.setImage(getBoatImageURL(lastPos,
competitorsSelectedInMap.contains(competitorDAO)));
} else {
// anchors don't match; replace marker
map.removeOverlay(boatMarker);
boatMarker = createBoatMarker(competitorDAO,
competitorsSelectedInMap.contains(competitorDAO));
map.addOverlay(boatMarker);
data.boatMarkers.put(competitorDAO, boatMarker);
}
}
}
}
}
if (!mapZoomedOrPannedSinceLastRaceSelectionChange && newMapBounds != null) {
map.setZoomLevel(map.getBoundsZoomLevel(newMapBounds));
map.setCenter(newMapBounds.getCenter());
mapFirstZoomDone = true;
}
for (CompetitorDAO unusedMarkerCompetitorDAO : competitorDAOsOfUnusedMarkers) {
map.removeOverlay(data.boatMarkers.remove(unusedMarkerCompetitorDAO));
}
for (CompetitorDAO unusedTailCompetitorDAO : competitorDAOsOfUnusedTails) {
map.removeOverlay(data.tails.remove(unusedTailCompetitorDAO));
}
}
}
private String getBoatImageURL(GPSFixDAO boatFix, boolean highlighted) {
return getBoatImageURL(getBoatImageRotator(boatFix, highlighted), boatFix);
}
private String getBoatImageURL(ImageRotator boatImageRotator, GPSFixDAO boatFix) {
// the possible zoom level range is 0 to 21 (zoom level 0 would show the whole world)
int zoomLevel = map.getZoomLevel();
double minScaleFactor = 0.1;
double maxScaleFactor = 1.0;
double scaleDiffPerZoomLevel = (maxScaleFactor - minScaleFactor) / 21.0;
double scaleFactor = minScaleFactor + scaleDiffPerZoomLevel * zoomLevel;
return boatImageRotator.getRotatedImageURL(boatFix.speedWithBearing.bearingInDegrees, scaleFactor);
}
private Icon getBoatImageIcon(GPSFixDAO boatFix, boolean highlighted) {
ImageRotator boatImageRotator = getBoatImageRotator(boatFix, highlighted);
Icon icon = Icon.newInstance(getBoatImageURL(boatImageRotator, boatFix));
icon.setIconAnchor(boatImageRotator.getAnchor());
return icon;
}
private ImageRotator getBoatImageRotator(GPSFixDAO boatFix, boolean highlighted) {
if (boatFix.tack == Tack.PORT) {
if (LegType.DOWNWIND == boatFix.legType) {
if (highlighted) {
return boatIconHighlightedDownwindStarboardRotator;
} else {
return boatIconDownwindStarboardRotator;
}
} else {
if (highlighted) {
return boatIconHighlightedStarboardRotator;
} else {
return boatIconStarboardRotator;
}
}
} else {
if (LegType.DOWNWIND == boatFix.legType) {
if (highlighted) {
return boatIconHighlightedDownwindPortRotator;
} else {
return boatIconDownwindPortRotator;
}
} else {
if (highlighted) {
return boatIconHighlightedPortRotator;
} else {
return boatIconPortRotator;
}
}
}
}
protected Marker createBuoyMarker(final MarkDAO markDAO) {
MarkerOptions options = MarkerOptions.newInstance();
if (buoyIcon != null) {
options.setIcon(buoyIcon);
}
options.setTitle(markDAO.name);
final Marker buoyMarker = new Marker(LatLng.newInstance(markDAO.position.latDeg, markDAO.position.lngDeg),
options);
buoyMarker.addMarkerClickHandler(new MarkerClickHandler() {
@Override
public void onClick(MarkerClickEvent event) {
LatLng latlng = buoyMarker.getLatLng();
showMarkInfoWindow(markDAO, latlng);
}
});
return buoyMarker;
}
protected Marker createBoatMarker(final CompetitorDAO competitorDAO, boolean highlighted) {
GPSFixDAO boatFix = data.getBoatFix(competitorDAO);
double latDeg = boatFix.position.latDeg;
double lngDeg = boatFix.position.lngDeg;
MarkerOptions options = MarkerOptions.newInstance();
Icon icon = getBoatImageIcon(boatFix, highlighted);
options.setIcon(icon);
options.setTitle(competitorDAO.name);
final Marker boatMarker = new Marker(LatLng.newInstance(latDeg, lngDeg), options);
boatMarker.addMarkerClickHandler(new MarkerClickHandler() {
@Override
public void onClick(MarkerClickEvent event) {
LatLng latlng = boatMarker.getLatLng();
showCompetitorInfoWindow(competitorDAO, latlng);
}
});
boatMarker.addMarkerMouseOverHandler(new MarkerMouseOverHandler() {
@Override
public void onMouseOver(MarkerMouseOverEvent event) {
map.setTitle(competitorDAO.name);
// setSelectedInMap(competitorDAO, true);
// quickRanksBox.setItemSelected(quickRanksList.indexOf(competitorDAO), true);
}
});
boatMarker.addMarkerMouseOutHandler(new MarkerMouseOutHandler() {
@Override
public void onMouseOut(MarkerMouseOutEvent event) {
map.setTitle("");
}
});
return boatMarker;
}
private void showMarkInfoWindow(MarkDAO markDAO, LatLng latlng) {
map.getInfoWindow().open(latlng, new InfoWindowContent(getInfoWindowContent(markDAO)));
}
private void showCompetitorInfoWindow(final CompetitorDAO competitorDAO, LatLng where) {
GPSFixDAO latestFixForCompetitor = data.getBoatFix(competitorDAO);
map.getInfoWindow().open(where,
new InfoWindowContent(getInfoWindowContent(competitorDAO, latestFixForCompetitor)));
}
private Widget getInfoWindowContent(MarkDAO markDAO) {
VerticalPanel result = new VerticalPanel();
result.add(new Label("Mark " + markDAO.name));
result.add(new Label("" + markDAO.position));
return result;
}
private Widget getInfoWindowContent(CompetitorDAO competitorDAO, GPSFixDAO lastFix) {
final VerticalPanel result = new VerticalPanel();
result.add(new Label("Competitor " + competitorDAO.name));
result.add(new Label("" + lastFix.position));
result.add(new Label(lastFix.speedWithBearing.speedInKnots + "kts " + lastFix.speedWithBearing.bearingInDegrees
+ "deg"));
result.add(new Label("Tack: " + lastFix.tack.name()));
if (!selectedEventAndRace.isEmpty()) {
EventDAO event = selectedEventAndRace.get(selectedEventAndRace.size() - 1).getA();
RaceDAO race = selectedEventAndRace.get(selectedEventAndRace.size() - 1).getC();
if (event != null && race != null) {
Map<CompetitorDAO, Date> from = new HashMap<CompetitorDAO, Date>();
from.put(competitorDAO, data.fixes.get(competitorDAO).get(data.firstShownFix.get(competitorDAO)).timepoint);
Map<CompetitorDAO, Date> to = new HashMap<CompetitorDAO, Date>();
to.put(competitorDAO, data.getBoatFix(competitorDAO).timepoint);
/* currently not showing Douglas-Peucker points; TODO use checkboxes to select what to show (Bug #6) */
sailingService.getDouglasPoints(new EventNameAndRaceName(event.name, race.name), from, to, 3,
new AsyncCallback<Map<CompetitorDAO, List<GPSFixDAO>>>() {
@Override
public void onFailure(Throwable caught) {
errorReporter.reportError("Error obtaining douglas positions: " + caught.getMessage());
}
@Override
public void onSuccess(Map<CompetitorDAO, List<GPSFixDAO>> result) {
data.lastDouglasPeuckerResult = result;
if (data.douglasMarkers != null) {
removeAllMarkDouglasPeuckerpoints();
}
if (!timer.isPlaying()) {
if (showDouglasPeuckerPoints) {
showMarkDouglasPeuckerPoints(result);
}
}
}
});
sailingService.getManeuvers(new EventNameAndRaceName(event.name, race.name), from, to,
new AsyncCallback<Map<CompetitorDAO, List<ManeuverDAO>>>() {
@Override
public void onFailure(Throwable caught) {
errorReporter.reportError("Error obtaining maneuvers: " + caught.getMessage());
}
@Override
public void onSuccess(Map<CompetitorDAO, List<ManeuverDAO>> result) {
data.lastManeuverResult = result;
if (data.maneuverMarkers != null) {
removeAllManeuverMarkers();
}
if (!timer.isPlaying()) {
showManeuvers(result);
}
}
});
}
}
return result;
}
private String getColorString(CompetitorDAO competitorDAO) {
// TODO green no more than 70, red no less than 120
return "#" + (Integer.toHexString(competitorDAO.hashCode()) + "000000").substring(0, 4).toUpperCase() + "00";
}
/**
* Creates a polyline for the competitor represented by <code>competitorDAO</code>, taking the raceMapData.fixes from
* {@link #data.fixes raceMapData.fixes.get(competitorDAO)} and using the raceMapData.fixes starting at time point <code>from</code> (inclusive)
* up to the last fix with time point before <code>to</code>. The polyline is returned. Updates are applied to
* {@link #data.lastShownFix}, {@link #data.firstShownFix} and {@link #data.tails}.
*/
protected Polyline createTailAndUpdateIndices(final CompetitorDAO competitorDAO, Date from, Date to) {
List<LatLng> points = new ArrayList<LatLng>();
List<GPSFixDAO> fixesForCompetitor = data.fixes.get(competitorDAO);
int indexOfFirst = -1;
int indexOfLast = -1;
int i = 0;
for (Iterator<GPSFixDAO> fixIter = fixesForCompetitor.iterator(); fixIter.hasNext() && indexOfLast == -1;) {
GPSFixDAO fix = fixIter.next();
if (!fix.timepoint.before(to)) {
indexOfLast = i;
} else {
LatLng point = null;
if (indexOfFirst == -1) {
if (!fix.timepoint.before(from)) {
indexOfFirst = i;
point = LatLng.newInstance(fix.position.latDeg, fix.position.lngDeg);
}
} else {
point = LatLng.newInstance(fix.position.latDeg, fix.position.lngDeg);
}
if (point != null) {
points.add(point);
}
}
i++;
}
if (indexOfLast == -1) {
indexOfLast = i - 1;
}
if (indexOfFirst != -1 && indexOfLast != -1) {
data.firstShownFix.put(competitorDAO, indexOfFirst);
data.lastShownFix.put(competitorDAO, indexOfLast);
}
PolylineOptions options = PolylineOptions.newInstance(
/* clickable */true, /* geodesic */true);
Polyline result = new Polyline(points.toArray(new LatLng[0]), getColorString(competitorDAO), /* width */3,
/* opacity */0.5, options);
result.addPolylineClickHandler(new PolylineClickHandler() {
@Override
public void onClick(PolylineClickEvent event) {
showCompetitorInfoWindow(competitorDAO, lastMousePosition);
}
});
result.addPolylineMouseOverHandler(new PolylineMouseOverHandler() {
@Override
public void onMouseOver(PolylineMouseOverEvent event) {
map.setTitle(competitorDAO.name);
}
});
result.addPolylineMouseOutHandler(new PolylineMouseOutHandler() {
@Override
public void onMouseOut(PolylineMouseOutEvent event) {
map.setTitle("");
}
});
data.tails.put(competitorDAO, result);
return result;
}
protected void removeAllMarkDouglasPeuckerpoints() {
if (data.douglasMarkers != null) {
for (Marker marker : data.douglasMarkers) {
map.removeOverlay(marker);
}
}
data.douglasMarkers = null;
}
protected void removeAllManeuverMarkers() {
if (data.maneuverMarkers != null) {
for (Marker marker : data.maneuverMarkers) {
map.removeOverlay(marker);
}
data.maneuverMarkers = null;
}
}
protected void showMarkDouglasPeuckerPoints(Map<CompetitorDAO, List<GPSFixDAO>> gpsFixPointMapForCompetitors) {
data.douglasMarkers = new HashSet<Marker>();
if (map != null && gpsFixPointMapForCompetitors != null) {
Set<CompetitorDAO> keySet = gpsFixPointMapForCompetitors.keySet();
Iterator<CompetitorDAO> iter = keySet.iterator();
while (iter.hasNext()) {
CompetitorDAO competitorDAO = iter.next();
List<GPSFixDAO> gpsFix = gpsFixPointMapForCompetitors.get(competitorDAO);
for (GPSFixDAO fix : gpsFix) {
LatLng latLng = LatLng.newInstance(fix.position.latDeg, fix.position.lngDeg);
MarkerOptions options = MarkerOptions.newInstance();
options.setTitle(fix.timepoint+": "+fix.position+", "+fix.speedWithBearing.toString());
Marker marker = new Marker(latLng, options);
data.douglasMarkers.add(marker);
map.addOverlay(marker);
}
}
}
}
protected void showManeuvers(Map<CompetitorDAO, List<ManeuverDAO>> maneuvers) {
data.maneuverMarkers = new HashSet<Marker>();
if (map != null && maneuvers != null) {
Set<CompetitorDAO> keySet = maneuvers.keySet();
Iterator<CompetitorDAO> iter = keySet.iterator();
while (iter.hasNext()) {
CompetitorDAO competitorDAO = iter.next();
List<ManeuverDAO> maneuversForCompetitor = maneuvers.get(competitorDAO);
for (ManeuverDAO maneuver : maneuversForCompetitor) {
boolean showThisManeuver = true;
LatLng latLng = LatLng.newInstance(maneuver.position.latDeg, maneuver.position.lngDeg);
MarkerOptions options = MarkerOptions.newInstance();
options.setTitle("" + maneuver.timepoint + ": " + maneuver.type + " "
+ maneuver.directionChangeInDegrees + "deg from " + maneuver.speedWithBearingBefore
+ " to " + maneuver.speedWithBearingAfter);
if (maneuver.type.equals("TACK") && showManeuverTack) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(tackToPortIcon);
} else {
options.setIcon(tackToStarboardIcon);
}
} else if (maneuver.type.equals("JIBE") && showManeuverJibe) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(jibeToPortIcon);
} else {
options.setIcon(jibeToStarboardIcon);
}
} else if (maneuver.type.equals("HEAD_UP") && showManeuverHeadUp) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(headUpOnPortIcon);
} else {
options.setIcon(headUpOnStarboardIcon);
}
} else if (maneuver.type.equals("BEAR_AWAY") && showManeuverBearAway) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(bearAwayOnPortIcon);
} else {
options.setIcon(bearAwayOnStarboardIcon);
}
} else if (maneuver.type.equals("PENALTY_CIRCLE") && showManeuverPenaltyCircle) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(penaltyCircleToPortIcon);
} else {
options.setIcon(penaltyCircleToStarboardIcon);
}
} else if (maneuver.type.equals("MARK_PASSING") && showManeuverMarkPassing) {
if (maneuver.newTack.equals("PORT")) {
options.setIcon(markPassingToPortIcon);
} else {
options.setIcon(markPassingToStarboardIcon);
}
} else {
if (maneuver.type.equals("UNKNOWN") && showManeuverOther) {
options.setIcon(unknownManeuverIcon);
} else {
showThisManeuver = false;
}
}
if (showThisManeuver) {
Marker marker = new Marker(latLng, options);
data.maneuverMarkers.add(marker);
map.addOverlay(marker);
}
}
}
}
}
}
@@ -0,0 +1,197 @@
package com.sap.sailing.gwt.ui.adminconsole;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.Polyline;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.shared.CompetitorDAO;
import com.sap.sailing.gwt.ui.shared.GPSFixDAO;
import com.sap.sailing.gwt.ui.shared.ManeuverDAO;
import com.sap.sailing.gwt.ui.shared.MarkDAO;
public class RaceMapData {
/**
* Tails of competitors currently displayed as overlays on the map.
*/
protected final Map<CompetitorDAO, Polyline> tails;
/**
* 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 .
*/
protected final Map<CompetitorDAO, Integer> firstShownFix;
/**
* Key set is equal to that of {@link #tails} and tells what the index in in {@link #fixes} of the last fix shown in
* {@link #tails} is .
*/
protected final Map<CompetitorDAO, Integer> lastShownFix;
/**
* Fixes of each competitors tail. If a list is contained for a competitor, the list contains a timely "contiguous"
* list of fixes for the competitor. This means the server has no more data for the time interval covered, unless
* the last fix was {@link GPSFixDAO#extrapolated obtained by extrapolation}.
*/
protected final Map<CompetitorDAO, List<GPSFixDAO>> fixes;
/**
* Markers used as boat display on the map
*/
protected final Map<CompetitorDAO, Marker> boatMarkers;
protected final Map<MarkDAO, Marker> buoyMarkers;
/**
* markers displayed in response to
* {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)}
*/
protected Set<Marker> douglasMarkers;
/**
* markers displayed in response to
* {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)}
*/
protected Set<Marker> maneuverMarkers;
protected Map<CompetitorDAO, List<ManeuverDAO>> lastManeuverResult;
protected Map<CompetitorDAO, List<GPSFixDAO>> lastDouglasPeuckerResult;
public RaceMapData() {
tails = new HashMap<CompetitorDAO, Polyline>();
firstShownFix = new HashMap<CompetitorDAO, Integer>();
lastShownFix = new HashMap<CompetitorDAO, Integer>();
buoyMarkers = new HashMap<MarkDAO, Marker>();
boatMarkers = new HashMap<CompetitorDAO, Marker>();
fixes = new HashMap<CompetitorDAO, List<GPSFixDAO>>();
}
protected Date getTimepointOfFirstNonExtrapolated(List<GPSFixDAO> fixesForCompetitor) {
for (GPSFixDAO fix : fixesForCompetitor) {
if (!fix.extrapolated) {
return fix.timepoint;
}
}
return null;
}
protected Date getTimepointOfLastNonExtrapolated(List<GPSFixDAO> fixesForCompetitor) {
if (!fixesForCompetitor.isEmpty()) {
for (ListIterator<GPSFixDAO> fixIter = fixesForCompetitor.listIterator(fixesForCompetitor.size() - 1); fixIter
.hasPrevious();) {
GPSFixDAO fix = fixIter.previous();
if (!fix.extrapolated) {
return fix.timepoint;
}
}
}
return null;
}
/**
* While updating the {@link #fixes} for <code>competitorDAO</code>, the invariants for {@link #tails} and
* {@link #firstShownFix} and {@link #lastShownFix} are maintained: each time a fix is inserted, the
* {@link #firstShownFix}/{@link #lastShownFix} records for <code>competitorDAO</code> are incremented if they are
* greater or equal to the insertion index and we have a tail in {@link #tails} for <code>competitorDAO</code>.
* Additionally, if the fix is in between the fixes shown in the competitor's tail, the tail is adjusted by
* inserting the corresponding fix.
*/
protected void mergeFixes(CompetitorDAO competitorDAO, List<GPSFixDAO> mergeThis) {
List<GPSFixDAO> intoThis = fixes.get(competitorDAO);
int indexOfFirstShownFix = firstShownFix.get(competitorDAO) == null ? -1 : firstShownFix.get(competitorDAO);
int indexOfLastShownFix = lastShownFix.get(competitorDAO) == null ? -1 : lastShownFix.get(competitorDAO);
Polyline tail = tails.get(competitorDAO);
int intoThisIndex = 0;
for (GPSFixDAO mergeThisFix : mergeThis) {
while (intoThisIndex < intoThis.size()
&& intoThis.get(intoThisIndex).timepoint.before(mergeThisFix.timepoint)) {
intoThisIndex++;
}
if (intoThisIndex < intoThis.size() && intoThis.get(intoThisIndex).timepoint.equals(mergeThisFix.timepoint)) {
// exactly same time point; replace with fix from mergeThis
intoThis.set(intoThisIndex, mergeThisFix);
} else {
intoThis.add(intoThisIndex, mergeThisFix);
if (indexOfFirstShownFix >= intoThisIndex) {
indexOfFirstShownFix++;
}
if (indexOfLastShownFix >= intoThisIndex) {
indexOfLastShownFix++;
}
if (tail != null && intoThisIndex >= indexOfFirstShownFix && intoThisIndex <= indexOfLastShownFix) {
tail.insertVertex(intoThisIndex - indexOfFirstShownFix,
LatLng.newInstance(mergeThisFix.position.latDeg, mergeThisFix.position.lngDeg));
}
}
intoThisIndex++;
}
}
protected GPSFixDAO getBoatFix(CompetitorDAO competitorDAO) {
return fixes.get(competitorDAO).get(lastShownFix.get(competitorDAO));
}
/**
* If the tail starts before <code>from</code>, removes leading vertices from <code>tail</code> that are before
* <code>from</code>. This is determined by using the {@link #firstShownFix} index which tells us where in
* {@link #fixes} we find the sequence of fixes currently represented in the tail.
* <p>
*
* If the tail starts after <code>from</code>, vertices for those {@link #fixes} for <code>competitorDAO</code> at
* or after time point <code>from</code> and before the time point of the first fix displayed so far in the tail and
* before <code>to</code> are prepended to the tail.
* <p>
*
* Now to the end of the tail: if the existing tail's end exceeds <code>to</code>, the vertices in excess are
* removed (aided by {@link #lastShownFix}). Otherwise, for the competitor's fixes starting at the tail's end up to
* <code>to</code> are appended to the tail.
* <p>
*
* When this method returns, {@link #firstShownFix} and {@link #lastShownFix} have been updated accordingly.
*/
protected void updateTail(Polyline tail, CompetitorDAO competitorDAO, Date from, Date to) {
List<GPSFixDAO> fixesForCompetitor = fixes.get(competitorDAO);
int indexOfFirstShownFix = firstShownFix.get(competitorDAO) == null ? -1 : firstShownFix.get(competitorDAO);
while (indexOfFirstShownFix != -1 && tail.getVertexCount() > 0
&& fixesForCompetitor.get(indexOfFirstShownFix).timepoint.before(from)) {
tail.deleteVertex(0);
indexOfFirstShownFix++;
}
// now the polyline contains no more vertices representing fixes before "from";
// go back in time starting at indexOfFirstShownFix while the fixes are still at or after "from"
// and insert corresponding vertices into the polyline
while (indexOfFirstShownFix > 0 && !fixesForCompetitor.get(indexOfFirstShownFix - 1).timepoint.before(from)) {
indexOfFirstShownFix--;
GPSFixDAO fix = fixesForCompetitor.get(indexOfFirstShownFix);
tail.insertVertex(0, LatLng.newInstance(fix.position.latDeg, fix.position.lngDeg));
}
// now adjust the polylines tail: remove excess vertices that are after "to"
int indexOfLastShownFix = lastShownFix.get(competitorDAO) == null ? -1 : lastShownFix.get(competitorDAO);
while (indexOfLastShownFix != -1 && tail.getVertexCount() > 0
&& fixesForCompetitor.get(indexOfLastShownFix).timepoint.after(to)) {
tail.deleteVertex(tail.getVertexCount() - 1);
indexOfLastShownFix--;
}
// now the polyline contains no more vertices representing fixes after "to";
// go forward in time starting at indexOfLastShownFix while the fixes are still at or before "to"
// and insert corresponding vertices into the polyline
while (indexOfLastShownFix < fixesForCompetitor.size() - 1
&& !fixesForCompetitor.get(indexOfLastShownFix + 1).timepoint.after(to)) {
indexOfLastShownFix++;
GPSFixDAO fix = fixesForCompetitor.get(indexOfLastShownFix);
tail.insertVertex(tail.getVertexCount(), LatLng.newInstance(fix.position.latDeg, fix.position.lngDeg));
}
firstShownFix.put(competitorDAO, indexOfFirstShownFix);
lastShownFix.put(competitorDAO, indexOfLastShownFix);
}
}