diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java index 651588a941e..93c22f0dd84 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java @@ -1,683 +1,683 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Extremes; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; -import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; - -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.EventAndRaceIdentifier; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; -import com.sap.sailing.gwt.ui.actions.GetCompetitorsRaceDataAction; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.TimeListener; -import com.sap.sailing.gwt.ui.client.Timer; -import com.sap.sailing.gwt.ui.client.Timer.PlayModes; -import com.sap.sailing.gwt.ui.shared.CompetitorDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.MultiCompetitorRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.components.Component; -import com.sap.sailing.gwt.ui.shared.panels.BusyIndicator; -import com.sap.sailing.gwt.ui.shared.panels.SimpleBusyIndicator; - -/** - * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward distance to - * leader) for different races in a chart. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author Benjamin Ebling (D056866), Axel Uhl (d043530) - * - */ -public abstract class AbstractChartPanel extends SimpleChartPanel -implements CompetitorSelectionChangeListener, RaceSelectionChangeListener, TimeListener, RequiresResize { - private static final int LINE_WIDTH = 1; - private MultiCompetitorRaceDataDTO chartData; - private final SailingServiceAsync sailingService; - private final AsyncActionsExecutor asyncActionsExecutor; - private final ErrorReporter errorReporter; - private Chart chart; - private boolean compactChart; - private final AbsolutePanel busyIndicatorPanel; - private final Label noCompetitorsSelectedLabel; - private final Map dataSeriesByCompetitor; - private final Map markPassingSeriesByCompetitor; - private Series timeLineSeries; - private boolean timeLineNeedsUpdate = true; - private final boolean allowTimeAdjust; - private boolean ignoreTimeAdjustOnce; - private final RaceSelectionProvider raceSelectionProvider; - private long stepSize = 5000; - protected final StringMessages stringMessages; - private final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); - private DetailType dataToShow; - private final CompetitorSelectionProvider competitorSelectionProvider; - - public AbstractChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, - Timer timer, final StringMessages stringMessages, ErrorReporter errorReporter, DetailType dataToShow, - boolean compactChart, boolean allowTimeAdjust) { - super(timer); - this.stringMessages = stringMessages; - dataSeriesByCompetitor = new HashMap(); - markPassingSeriesByCompetitor = new HashMap(); - this.timer.addTimeListener(this); - this.competitorSelectionProvider = competitorSelectionProvider; - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - this.errorReporter = errorReporter; - this.dataToShow = dataToShow; - chartData = null; - this.compactChart = compactChart; - this.sailingService = sailingService; - this.asyncActionsExecutor = asyncActionsExecutor; - this.raceSelectionProvider = raceSelectionProvider; - this.allowTimeAdjust = allowTimeAdjust; - raceSelectionProvider.addRaceSelectionChangeListener(this); - setSize("100%", "100%"); - - noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); - noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); - - chart = createChart(dataToShow); - if (allowTimeAdjust) { - timeLineSeries = createTimeLineSeries(); - } - - busyIndicatorPanel = new AbsolutePanel(); - final BusyIndicator busyIndicator = new SimpleBusyIndicator(/*busy*/ true, /*scale*/ 1); - //Adding the busyIndicator with an scheduler, to be sure that the busyIndicatorPanel has a width and a height - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - busyIndicatorPanel.setSize("100%", "100%"); - busyIndicatorPanel.add(busyIndicator, busyIndicatorPanel.getOffsetWidth() / 2, busyIndicatorPanel.getOffsetHeight() / 2); - } - }); - - List selectedRaces = raceSelectionProvider.getSelectedRaces(); - if(!selectedRaces.isEmpty()) { - loadData(true); - } - timer.addTimeListener(this); - } - - protected void selectRace(final RaceIdentifier selectedRace) { - } - - /** - * Creates a new chart for the given {@link DetailType} dataToShow and also - * clears the {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}. - * @param dataToShow The detail type for the new chart. - * @return A chart for the given detail Type - */ - private Chart createChart(DetailType dataToShow) { - Chart chart = new Chart().setZoomType(Chart.ZoomType.X) - .setWidth100() - .setHeight100() - .setMarginLeft(65) - .setMarginRight(65) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) - .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker(new Marker().setEnabled(false).setHoverState( - new Marker().setEnabled(true).setRadius(4))).setShadow(false) - .setHoverStateLineWidth(LINE_WIDTH)); - useCheckboxesToShowAndHide(chart); - chart.setChartTitle(new ChartTitle().setText(DetailTypeFormatter.format(dataToShow, stringMessages))); - - if (allowTimeAdjust) { - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - if (ignoreTimeAdjustOnce) { - ignoreTimeAdjustOnce = false; - } else { - timer.setPlayMode(PlayModes.Replay); - timer.setTime(chartClickEvent.getXAxisValueAsLong()); - } - return true; - } - }); - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - try { - chartSelectionEvent.getXAxisMaxAsLong(); - chartSelectionEvent.getXAxisMinAsLong(); - ignoreTimeAdjustOnce = true; - } catch (Throwable t) { - // Redrawing, or the chart wouldn't rezoom - AbstractChartPanel.this.chart.redraw(); - } - return true; - } - }); - } - final String unit = getUnit(); - if(!compactChart) - chart.getYAxis().setAxisTitleText(DetailTypeFormatter.format(dataToShow, stringMessages) + " ["+unit+"]"); - else - chart.getYAxis().setAxisTitleText("["+unit+"]"); - chart.getYAxis().setStartOnTick(false).setShowFirstLabel(false); - chart.getYAxis().setReversed((dataToShow == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || - dataToShow == DetailType.GAP_TO_LEADER_IN_SECONDS) ? true : false); - chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(10000) // ten seconds - .setAxisTitleText(stringMessages.time()); - String decimalPlaces = ""; - for (int i = 0; i < dataToShow.getPrecision(); i++) { - if (i == 0) { - decimalPlaces += "."; - } - decimalPlaces += "0"; - } - final NumberFormat numberFormat = NumberFormat.getFormat("0" + decimalPlaces); - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - String seriesName = toolTipData.getSeriesName(); - - if (seriesName.equals(stringMessages.time())) { - return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) - + "
(" + stringMessages.clickChartToSetTime() + ")"; - } else { - return "" + seriesName - + (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "") - + "
" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " - + numberFormat.format(toolTipData.getYAsDouble()) + unit; - } - } - })); - - if (compactChart) { - chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) - .setOption("legend/margin", 2) - .setOption("title/margin", 5) - .setChartSubtitle(null) - .getXAxis().setAxisTitleText(null); - } - - setChartData(null); - dataSeriesByCompetitor.clear(); - markPassingSeriesByCompetitor.clear(); - return chart; - } - - protected abstract Component getComponent(); - - /** - * Loads the needed data (data which isn't in the {@link #chartData cache}) for the {@link #getVisibleCompetitors() - * visible competitors} via - * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After - * loading is the method {@link #drawChartData()} called.
- * If no data needs to be {@link #needsDataLoading() loaded}, the no competitors selected label is displayed. - * - * @param showBusyIndicator If true is the busy indicator shown while loading the data from the server. - */ - protected void loadData(boolean showBusyIndicator) { - if (needsDataLoading()) { - if (showBusyIndicator) { - setWidget(busyIndicatorPanel); - } - if (chartData == null || chartData.getDetailType() != getDataToShow()) { - chartData = new MultiCompetitorRaceDataDTO(getDataToShow()); - } - - Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); - final ArrayList> dataQuery = new ArrayList>(); - for (CompetitorDTO competitor : getVisibleCompetitors()) { - Date chartDataDateOfNewestData = chartData.getDateOfNewestData(); - Date competitorDateOfNewestData = chartData.contains(competitor) ? chartData.getCompetitorData(competitor).getDateOfNewestData() : null; - if (!chartData.contains(competitor)) { - dataQuery.add(new Pair(new Date(0), competitor)); - } else if (competitorDateOfNewestData.before(chartDataDateOfNewestData) || competitorDateOfNewestData.before(toDate)) { - dataQuery.add(new Pair(new Date(competitorDateOfNewestData.getTime() + getStepSize()), competitor)); - } - } - - GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, - getSelectedRace(), dataQuery, toDate, getStepSize(), getDataToShow(), - new AsyncCallback() { - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(getStringMessages().failedToLoadRaceData() + ": " + caught.toString(), - timer.getPlayMode() == PlayModes.Live); - } - - @Override - public void onSuccess(MultiCompetitorRaceDataDTO result) { - if (result != null) { - for (CompetitorRaceDataDTO competitorData : result.getAllRaceData()) { - if (chartData.contains(competitorData.getCompetitor())) { - chartData.addCompetitorRaceData(competitorData); - chartData.setCompetitorMarkPassingsData(competitorData); - } else { - chartData.setCompetitorData(competitorData.getCompetitor(), competitorData); - } - } - } - drawChartData(); - setWidget(chart); - } - }); - asyncActionsExecutor.execute(getCompetitorsRaceDataAction); - } else { - setWidget(noCompetitorsSelectedLabel); - } - } - - private RaceIdentifier getSelectedRace() { - RaceIdentifier result = null; - List selectedRaces = raceSelectionProvider.getSelectedRaces(); - if (selectedRaces != null && !selectedRaces.isEmpty()) { - result = selectedRaces.iterator().next(); - } - return result; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - timeLineNeedsUpdate = true; - loadData(true); - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - Series competitorSeries = getCompetitorSeries(competitor); - if (competitorSeries != null) { - chart.removeSeries(competitorSeries); - } - Series competitorMarkPassingSeries = getCompetitorMarkPassingSeries(competitor); - if (competitorMarkPassingSeries != null) { - chart.removeSeries(competitorMarkPassingSeries); - } - if (!hasVisibleCompetitors()) { - setWidget(noCompetitorsSelectedLabel); - } - } - - /** - * Creates the series for all selected competitors if these aren't created yet.
- * Fills the series for the selected competitors with the data in {@link AbstractChartPanel#chartData}.
- * Removes series of competitors, which aren't selected and adds series for competitors, which are newly selected.
- * Also {@link #forceTimeLineUpdate() updates} the {@link #timeLineSeries} if {@link #timeLineNeedsUpdate} is true.

- * - * The data for all {@link #getVisibleCompetitors() visible competitors} needs to be filled before calling this method. - */ - private synchronized void drawChartData() { - //Make sure the busy indicator is removed at this point, or plotting the data results in an exception - setWidget(chart); - Iterable competitors = competitorSelectionProvider.getAllCompetitors(); - if (chartData != null && competitors != null && competitors.iterator().hasNext()) { - List chartSeries = Arrays.asList(chart.getSeries()); - for (CompetitorDTO competitor : competitors) { - Series compSeries = getCompetitorSeries(competitor); - Series markSeries = getCompetitorMarkPassingSeries(competitor); - if (isCompetitorVisible(competitor)) { - CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); - if (competitorData != null) { - Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); - - List> markPassingsData = competitorData.getMarkPassingsData(); - List markPassingPoints = new ArrayList(); - for (Triple markPassingData : markPassingsData) { - if (markPassingData.getB() != null && markPassingData.getC() != null) { - if (markPassingData.getB().before(toDate)) { - Point markPassingPoint = new Point(markPassingData.getB().getTime(), - markPassingData.getC()); - markPassingPoint.setName(markPassingData.getA()); - markPassingPoints.add(markPassingPoint); - } - } - } - markSeries.setPoints(markPassingPoints.toArray(new Point[0])); - - Point[] compSeriesPoints = compSeries.getPoints(); - Date dateOfNewestSeriesPoint = compSeriesPoints.length == 0 ? null : - new Date(compSeriesPoints[compSeriesPoints.length - 1].getX().longValue()); - List> raceData = dateOfNewestSeriesPoint == null ? competitorData - .getRaceData() : competitorData.getRaceDataAfterDate(dateOfNewestSeriesPoint); - for (Pair data : raceData) { - if (data.getA() != null && data.getB() != null) { - if (data.getA().before(toDate)) { - Point competitorPoint = new Point(data.getA().getTime(), data.getB()); - compSeries.addPoint(competitorPoint); - } - } - } - - //Adding the series if chart doesn't contain it - if (!chartSeries.contains(compSeries)) { - chart.addSeries(compSeries); - chart.addSeries(markSeries); - } - } - } else { - //Removing the series if chart contains it - if (chartSeries.contains(compSeries)) { - chart.removeSeries(compSeries); - chart.removeSeries(markSeries); - } - } - } - if (timeLineNeedsUpdate) { - forceTimeLineUpdate(); - } - } - } - - private Iterable getVisibleCompetitors() { - return competitorSelectionProvider.getSelectedCompetitors(); - } - - private boolean hasVisibleCompetitors() { - return getVisibleCompetitors().iterator().hasNext(); - } - - private boolean isCompetitorVisible(CompetitorDTO competitor) { - return competitorSelectionProvider.isSelected(competitor); - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the data of a specific competitor. - */ - private Series getCompetitorSeries(final CompetitorDTO competitor){ - Series result = dataSeriesByCompetitor.get(competitor); - if (result == null) { - result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name); - result.setPlotOptions(new LinePlotOptions() - .setLineWidth(LINE_WIDTH) - .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) - .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) - .setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); - dataSeriesByCompetitor.put(competitor, result); - } - return result; - } - - private Series createTimeLineSeries() { - return chart - .createSeries() - .setType(Series.Type.LINE) - .setName(stringMessages.time()) - .setPlotOptions(new LinePlotOptions().setShowInLegend(false).setHoverStateEnabled(false).setLineWidth(2)); - } - - private String getUnit() { - String unit = ""; - switch (getDataToShow()) { - case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: - unit = getStringMessages().currentSpeedOverGroundInKnotsUnit(); - break; - case DISTANCE_TRAVELED: - unit = getStringMessages().distanceInMetersUnit(); - break; - case GAP_TO_LEADER_IN_SECONDS: - unit = getStringMessages().gapToLeaderInSecondsUnit(); - break; - case VELOCITY_MADE_GOOD_IN_KNOTS: - unit = getStringMessages().velocityMadeGoodInKnotsUnit(); - break; - case WINDWARD_DISTANCE_TO_OVERALL_LEADER: - unit = getStringMessages().windwardDistanceToGoInMetersUnit(); - } - return unit; - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the mark passings. - */ - private Series getCompetitorMarkPassingSeries(CompetitorDTO competitor){ - Series result = markPassingSeriesByCompetitor.get(competitor); - if (result == null) { - result = chart.createSeries().setType(Series.Type.SCATTER).setName(stringMessages.markPassing()+" "+competitor.name); - result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); - markPassingSeriesByCompetitor.put(competitor, result); - } - return result; - } - - /** - * Clears the whole chart and empties cached data. - */ - protected void clearChart() { - setChartData(null); - dataSeriesByCompetitor.clear(); - markPassingSeriesByCompetitor.clear(); - chart.removeAllSeries(); - } - - public String getLocalizedShortName() { - return DetailTypeFormatter.format(getDataToShow(), getStringMessages()); - } - - public Widget getEntryWidget() { - return this; - } - - public boolean hasSettings() { - return true; - } - - public ChartSettings getAbstractSettings() { - return new ChartSettings(getStepSize()); - } - - /** - * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided - * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #loadData(boolean)}, if this method returns true; - * - * @return true if the settings had been changed and a clearing and loading is needed. - */ - protected boolean updateSettingsOnly(ChartSettings newSettings) { - boolean settingsChanged = false; - if (getStepSize() != newSettings.getStepSize()) { - setStepSize(newSettings.getStepSize()); - settingsChanged = true; - } - return settingsChanged; - } - - protected StringMessages getStringMessages() { - return stringMessages; - } - - protected long getStepSize() { - return stepSize; - } - - protected void setStepSize(long stepSize) { - this.stepSize = stepSize; - } - - protected DetailType getDataToShow() { - return this.dataToShow; - } - - /** - * Updates the {@link #dataToShow} field, creates a new chart for the new dataToShow and clears the {@link #chartData}.
- * Doesn't {@link #loadData(boolean) load the data}. - * - * @return true if the data to show changed - */ - protected boolean setDataToShow(DetailType dataToShow) { - if (dataToShow != this.dataToShow) { - this.dataToShow = dataToShow; - setChartData(null); - chart = createChart(dataToShow); - setWidget(chart); - - return true; - } else { - return false; - } - } - - protected MultiCompetitorRaceDataDTO getChartData() { - return chartData; - } - - protected void setChartData(MultiCompetitorRaceDataDTO chartData) { - this.chartData = chartData; - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - setChartData(null); - clearChart(); - timeLineNeedsUpdate = true; - loadData(true); - } - - /** - * Checks the relation of the mark passings to the selection range. - * - * @param markPassingInRange - * A Boolean matrix filled by - * {@link AbstractChartPanel#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) - * fillPotentialXValues(...)} - * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the - * selection range needs to be refactored. Value B returns true if two passings are in range before the - * error happened or false, if the error happens before two passings were in the selection. B can be - * null. - */ - public Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { - boolean everyPassingInRange = true; - Boolean twoPassingsInRangeBeforeError = null; - ArrayList competitorPassings = markPassingInRange.get(0); - for (int i = 0; i < competitorPassings.size(); i++) { - Boolean passingInRange = competitorPassings.get(i); - for (int j = 1; j < markPassingInRange.size(); j++) { - Boolean passingToCompare = markPassingInRange.get(j).get(i); - if (passingInRange != null) { - if (passingToCompare != null && everyPassingInRange) { - everyPassingInRange = passingInRange.equals(passingToCompare); - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = true; - } - } else if (passingToCompare != null) { - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = false; - } - } - } else { - passingInRange = passingToCompare; - } - } - } - - return new Pair(everyPassingInRange, twoPassingsInRangeBeforeError); - } - - /** - * Forces the chart to {@link #updateTimeLine(Date) update} the position and to {@link #ensureTimeLineIsVisible() - * ensure}, that the time line is visible. - */ - public void forceTimeLineUpdate() { - updateTimeLine(timer.getTime()); - } - - /** - * Updates the position of the time line for the given {@link Date}, if {@link #allowTimeAdjust} is true.
- * @param date Defines the x-Value of the line points - */ - private void updateTimeLine(Date date) { - if (allowTimeAdjust && chart != null) { - raceSelectionProvider.getSelectedRaces().get(0); - - Long x = date.getTime(); - Extremes extremes = chart.getYAxis(0).getExtremes(); - Point[] points = new Point[2]; - points[0] = new Point(x, extremes.getDataMin()); - points[1] = new Point(x, extremes.getDataMax()); - timeLineSeries.setPoints(points); - ensureTimeLineIsVisible(); - timeLineNeedsUpdate = false; - } - } - - /** - * Checks if the series of the chart contain the {@link #timeLineSeries}. If not, it is added to the chart.
- * Throws a NPE if the chart is null. - */ - private void ensureTimeLineIsVisible() { - if (!Arrays.asList(chart.getSeries()).contains(timeLineSeries)) { - chart.addSeries(timeLineSeries); - } - } - - @Override - public void timeChanged(Date date) { - if (getChartData() != null) { - Date newestEvent = getChartData().getOldestDateOfNewestData(); - updateTimeLine(date); - if ((newestEvent == null || (newestEvent.before(date) && (date.getTime() - newestEvent.getTime()) >= getStepSize()))) { - loadData(false); - } - } - } - - private boolean needsDataLoading() { - return hasVisibleCompetitors() && isVisible(); - } - - @Override - public void onResize() { - if(getChartData() != null) { - chart.setSizeToMatchContainer(); - } - } - - @Override - public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime()); - } - - public void triggerDataLoading() { - loadData(true); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Extremes; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.EventAndRaceIdentifier; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; +import com.sap.sailing.gwt.ui.actions.GetCompetitorsRaceDataAction; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.TimeListener; +import com.sap.sailing.gwt.ui.client.Timer; +import com.sap.sailing.gwt.ui.client.Timer.PlayModes; +import com.sap.sailing.gwt.ui.shared.CompetitorDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.MultiCompetitorRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.components.Component; +import com.sap.sailing.gwt.ui.shared.panels.BusyIndicator; +import com.sap.sailing.gwt.ui.shared.panels.SimpleBusyIndicator; + +/** + * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward distance to + * leader) for different races in a chart. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author Benjamin Ebling (D056866), Axel Uhl (d043530) + * + */ +public abstract class AbstractChartPanel extends SimpleChartPanel +implements CompetitorSelectionChangeListener, RaceSelectionChangeListener, TimeListener, RequiresResize { + private static final int LINE_WIDTH = 1; + private MultiCompetitorRaceDataDTO chartData; + private final SailingServiceAsync sailingService; + private final AsyncActionsExecutor asyncActionsExecutor; + private final ErrorReporter errorReporter; + private Chart chart; + private boolean compactChart; + private final AbsolutePanel busyIndicatorPanel; + private final Label noCompetitorsSelectedLabel; + private final Map dataSeriesByCompetitor; + private final Map markPassingSeriesByCompetitor; + private Series timeLineSeries; + private boolean timeLineNeedsUpdate = true; + private final boolean allowTimeAdjust; + private boolean ignoreTimeAdjustOnce; + private final RaceSelectionProvider raceSelectionProvider; + private long stepSize = 5000; + protected final StringMessages stringMessages; + private final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); + private DetailType dataToShow; + private final CompetitorSelectionProvider competitorSelectionProvider; + + public AbstractChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, + Timer timer, final StringMessages stringMessages, ErrorReporter errorReporter, DetailType dataToShow, + boolean compactChart, boolean allowTimeAdjust) { + super(timer); + this.stringMessages = stringMessages; + dataSeriesByCompetitor = new HashMap(); + markPassingSeriesByCompetitor = new HashMap(); + this.timer.addTimeListener(this); + this.competitorSelectionProvider = competitorSelectionProvider; + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + this.errorReporter = errorReporter; + this.dataToShow = dataToShow; + chartData = null; + this.compactChart = compactChart; + this.sailingService = sailingService; + this.asyncActionsExecutor = asyncActionsExecutor; + this.raceSelectionProvider = raceSelectionProvider; + this.allowTimeAdjust = allowTimeAdjust; + raceSelectionProvider.addRaceSelectionChangeListener(this); + setSize("100%", "100%"); + + noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); + noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); + + chart = createChart(dataToShow); + if (allowTimeAdjust) { + timeLineSeries = createTimeLineSeries(); + } + + busyIndicatorPanel = new AbsolutePanel(); + final BusyIndicator busyIndicator = new SimpleBusyIndicator(/*busy*/ true, /*scale*/ 1); + //Adding the busyIndicator with an scheduler, to be sure that the busyIndicatorPanel has a width and a height + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + busyIndicatorPanel.setSize("100%", "100%"); + busyIndicatorPanel.add(busyIndicator, busyIndicatorPanel.getOffsetWidth() / 2, busyIndicatorPanel.getOffsetHeight() / 2); + } + }); + + List selectedRaces = raceSelectionProvider.getSelectedRaces(); + if(!selectedRaces.isEmpty()) { + loadData(true); + } + timer.addTimeListener(this); + } + + protected void selectRace(final RaceIdentifier selectedRace) { + } + + /** + * Creates a new chart for the given {@link DetailType} dataToShow and also + * clears the {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}. + * @param dataToShow The detail type for the new chart. + * @return A chart for the given detail Type + */ + private Chart createChart(DetailType dataToShow) { + Chart chart = new Chart().setZoomType(Chart.ZoomType.X) + .setWidth100() + .setHeight100() + .setMarginLeft(65) + .setMarginRight(65) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) + .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker(new Marker().setEnabled(false).setHoverState( + new Marker().setEnabled(true).setRadius(4))).setShadow(false) + .setHoverStateLineWidth(LINE_WIDTH)); + useCheckboxesToShowAndHide(chart); + chart.setChartTitle(new ChartTitle().setText(DetailTypeFormatter.format(dataToShow, stringMessages))); + + if (allowTimeAdjust) { + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + if (ignoreTimeAdjustOnce) { + ignoreTimeAdjustOnce = false; + } else { + timer.setPlayMode(PlayModes.Replay); + timer.setTime(chartClickEvent.getXAxisValueAsLong()); + } + return true; + } + }); + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + try { + chartSelectionEvent.getXAxisMaxAsLong(); + chartSelectionEvent.getXAxisMinAsLong(); + ignoreTimeAdjustOnce = true; + } catch (Throwable t) { + // Redrawing, or the chart wouldn't rezoom + AbstractChartPanel.this.chart.redraw(); + } + return true; + } + }); + } + final String unit = getUnit(); + if(!compactChart) + chart.getYAxis().setAxisTitleText(DetailTypeFormatter.format(dataToShow, stringMessages) + " ["+unit+"]"); + else + chart.getYAxis().setAxisTitleText("["+unit+"]"); + chart.getYAxis().setStartOnTick(false).setShowFirstLabel(false); + chart.getYAxis().setReversed((dataToShow == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || + dataToShow == DetailType.GAP_TO_LEADER_IN_SECONDS) ? true : false); + chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(10000) // ten seconds + .setAxisTitleText(stringMessages.time()); + String decimalPlaces = ""; + for (int i = 0; i < dataToShow.getPrecision(); i++) { + if (i == 0) { + decimalPlaces += "."; + } + decimalPlaces += "0"; + } + final NumberFormat numberFormat = NumberFormat.getFormat("0" + decimalPlaces); + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + String seriesName = toolTipData.getSeriesName(); + + if (seriesName.equals(stringMessages.time())) { + return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) + + "
(" + stringMessages.clickChartToSetTime() + ")"; + } else { + return "" + seriesName + + (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "") + + "
" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(toolTipData.getYAsDouble()) + unit; + } + } + })); + + if (compactChart) { + chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) + .setOption("legend/margin", 2) + .setOption("title/margin", 5) + .setChartSubtitle(null) + .getXAxis().setAxisTitleText(null); + } + + setChartData(null); + dataSeriesByCompetitor.clear(); + markPassingSeriesByCompetitor.clear(); + return chart; + } + + protected abstract Component getComponent(); + + /** + * Loads the needed data (data which isn't in the {@link #chartData cache}) for the {@link #getVisibleCompetitors() + * visible competitors} via + * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After + * loading is the method {@link #drawChartData()} called.
+ * If no data needs to be {@link #needsDataLoading() loaded}, the no competitors selected label is displayed. + * + * @param showBusyIndicator If true is the busy indicator shown while loading the data from the server. + */ + protected void loadData(boolean showBusyIndicator) { + if (needsDataLoading()) { + if (showBusyIndicator) { + setWidget(busyIndicatorPanel); + } + if (chartData == null || chartData.getDetailType() != getDataToShow()) { + chartData = new MultiCompetitorRaceDataDTO(getDataToShow()); + } + + Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); + final ArrayList> dataQuery = new ArrayList>(); + for (CompetitorDTO competitor : getVisibleCompetitors()) { + Date chartDataDateOfNewestData = chartData.getDateOfNewestData(); + Date competitorDateOfNewestData = chartData.contains(competitor) ? chartData.getCompetitorData(competitor).getDateOfNewestData() : null; + if (!chartData.contains(competitor)) { + dataQuery.add(new Pair(new Date(0), competitor)); + } else if (competitorDateOfNewestData.before(chartDataDateOfNewestData) || competitorDateOfNewestData.before(toDate)) { + dataQuery.add(new Pair(new Date(competitorDateOfNewestData.getTime() + getStepSize()), competitor)); + } + } + + GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, + getSelectedRace(), dataQuery, toDate, getStepSize(), getDataToShow(), + new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(getStringMessages().failedToLoadRaceData() + ": " + caught.toString(), + timer.getPlayMode() == PlayModes.Live); + } + + @Override + public void onSuccess(MultiCompetitorRaceDataDTO result) { + if (result != null) { + for (CompetitorRaceDataDTO competitorData : result.getAllRaceData()) { + if (chartData.contains(competitorData.getCompetitor())) { + chartData.addCompetitorRaceData(competitorData); + chartData.setCompetitorMarkPassingsData(competitorData); + } else { + chartData.setCompetitorData(competitorData.getCompetitor(), competitorData); + } + } + } + drawChartData(); + setWidget(chart); + } + }); + asyncActionsExecutor.execute(getCompetitorsRaceDataAction); + } else { + setWidget(noCompetitorsSelectedLabel); + } + } + + private RaceIdentifier getSelectedRace() { + RaceIdentifier result = null; + List selectedRaces = raceSelectionProvider.getSelectedRaces(); + if (selectedRaces != null && !selectedRaces.isEmpty()) { + result = selectedRaces.iterator().next(); + } + return result; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + timeLineNeedsUpdate = true; + loadData(true); + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + Series competitorSeries = getCompetitorSeries(competitor); + if (competitorSeries != null) { + chart.removeSeries(competitorSeries); + } + Series competitorMarkPassingSeries = getCompetitorMarkPassingSeries(competitor); + if (competitorMarkPassingSeries != null) { + chart.removeSeries(competitorMarkPassingSeries); + } + if (!hasVisibleCompetitors()) { + setWidget(noCompetitorsSelectedLabel); + } + } + + /** + * Creates the series for all selected competitors if these aren't created yet.
+ * Fills the series for the selected competitors with the data in {@link AbstractChartPanel#chartData}.
+ * Removes series of competitors, which aren't selected and adds series for competitors, which are newly selected.
+ * Also {@link #forceTimeLineUpdate() updates} the {@link #timeLineSeries} if {@link #timeLineNeedsUpdate} is true.

+ * + * The data for all {@link #getVisibleCompetitors() visible competitors} needs to be filled before calling this method. + */ + private synchronized void drawChartData() { + //Make sure the busy indicator is removed at this point, or plotting the data results in an exception + setWidget(chart); + Iterable competitors = competitorSelectionProvider.getAllCompetitors(); + if (chartData != null && competitors != null && competitors.iterator().hasNext()) { + List chartSeries = Arrays.asList(chart.getSeries()); + for (CompetitorDTO competitor : competitors) { + Series compSeries = getCompetitorSeries(competitor); + Series markSeries = getCompetitorMarkPassingSeries(competitor); + if (isCompetitorVisible(competitor)) { + CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); + if (competitorData != null) { + Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); + + List> markPassingsData = competitorData.getMarkPassingsData(); + List markPassingPoints = new ArrayList(); + for (Triple markPassingData : markPassingsData) { + if (markPassingData.getB() != null && markPassingData.getC() != null) { + if (markPassingData.getB().before(toDate)) { + Point markPassingPoint = new Point(markPassingData.getB().getTime(), + markPassingData.getC()); + markPassingPoint.setName(markPassingData.getA()); + markPassingPoints.add(markPassingPoint); + } + } + } + markSeries.setPoints(markPassingPoints.toArray(new Point[0])); + + Point[] compSeriesPoints = compSeries.getPoints(); + Date dateOfNewestSeriesPoint = compSeriesPoints.length == 0 ? null : + new Date(compSeriesPoints[compSeriesPoints.length - 1].getX().longValue()); + List> raceData = dateOfNewestSeriesPoint == null ? competitorData + .getRaceData() : competitorData.getRaceDataAfterDate(dateOfNewestSeriesPoint); + for (Pair data : raceData) { + if (data.getA() != null && data.getB() != null) { + if (data.getA().before(toDate)) { + Point competitorPoint = new Point(data.getA().getTime(), data.getB()); + compSeries.addPoint(competitorPoint); + } + } + } + + //Adding the series if chart doesn't contain it + if (!chartSeries.contains(compSeries)) { + chart.addSeries(compSeries); + chart.addSeries(markSeries); + } + } + } else { + //Removing the series if chart contains it + if (chartSeries.contains(compSeries)) { + chart.removeSeries(compSeries); + chart.removeSeries(markSeries); + } + } + } + if (timeLineNeedsUpdate) { + forceTimeLineUpdate(); + } + } + } + + private Iterable getVisibleCompetitors() { + return competitorSelectionProvider.getSelectedCompetitors(); + } + + private boolean hasVisibleCompetitors() { + return getVisibleCompetitors().iterator().hasNext(); + } + + private boolean isCompetitorVisible(CompetitorDTO competitor) { + return competitorSelectionProvider.isSelected(competitor); + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the data of a specific competitor. + */ + private Series getCompetitorSeries(final CompetitorDTO competitor){ + Series result = dataSeriesByCompetitor.get(competitor); + if (result == null) { + result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name); + result.setPlotOptions(new LinePlotOptions() + .setLineWidth(LINE_WIDTH) + .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) + .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) + .setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); + dataSeriesByCompetitor.put(competitor, result); + } + return result; + } + + private Series createTimeLineSeries() { + return chart + .createSeries() + .setType(Series.Type.LINE) + .setName(stringMessages.time()) + .setPlotOptions(new LinePlotOptions().setShowInLegend(false).setHoverStateEnabled(false).setLineWidth(2)); + } + + private String getUnit() { + String unit = ""; + switch (getDataToShow()) { + case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: + unit = getStringMessages().currentSpeedOverGroundInKnotsUnit(); + break; + case DISTANCE_TRAVELED: + unit = getStringMessages().distanceInMetersUnit(); + break; + case GAP_TO_LEADER_IN_SECONDS: + unit = getStringMessages().gapToLeaderInSecondsUnit(); + break; + case VELOCITY_MADE_GOOD_IN_KNOTS: + unit = getStringMessages().velocityMadeGoodInKnotsUnit(); + break; + case WINDWARD_DISTANCE_TO_OVERALL_LEADER: + unit = getStringMessages().windwardDistanceToGoInMetersUnit(); + } + return unit; + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the mark passings. + */ + private Series getCompetitorMarkPassingSeries(CompetitorDTO competitor){ + Series result = markPassingSeriesByCompetitor.get(competitor); + if (result == null) { + result = chart.createSeries().setType(Series.Type.SCATTER).setName(stringMessages.markPassing()+" "+competitor.name); + result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); + markPassingSeriesByCompetitor.put(competitor, result); + } + return result; + } + + /** + * Clears the whole chart and empties cached data. + */ + protected void clearChart() { + setChartData(null); + dataSeriesByCompetitor.clear(); + markPassingSeriesByCompetitor.clear(); + chart.removeAllSeries(); + } + + public String getLocalizedShortName() { + return DetailTypeFormatter.format(getDataToShow(), getStringMessages()); + } + + public Widget getEntryWidget() { + return this; + } + + public boolean hasSettings() { + return true; + } + + public ChartSettings getAbstractSettings() { + return new ChartSettings(getStepSize()); + } + + /** + * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided + * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #loadData(boolean)}, if this method returns true; + * + * @return true if the settings had been changed and a clearing and loading is needed. + */ + protected boolean updateSettingsOnly(ChartSettings newSettings) { + boolean settingsChanged = false; + if (getStepSize() != newSettings.getStepSize()) { + setStepSize(newSettings.getStepSize()); + settingsChanged = true; + } + return settingsChanged; + } + + protected StringMessages getStringMessages() { + return stringMessages; + } + + protected long getStepSize() { + return stepSize; + } + + protected void setStepSize(long stepSize) { + this.stepSize = stepSize; + } + + protected DetailType getDataToShow() { + return this.dataToShow; + } + + /** + * Updates the {@link #dataToShow} field, creates a new chart for the new dataToShow and clears the {@link #chartData}.
+ * Doesn't {@link #loadData(boolean) load the data}. + * + * @return true if the data to show changed + */ + protected boolean setDataToShow(DetailType dataToShow) { + if (dataToShow != this.dataToShow) { + this.dataToShow = dataToShow; + setChartData(null); + chart = createChart(dataToShow); + setWidget(chart); + + return true; + } else { + return false; + } + } + + protected MultiCompetitorRaceDataDTO getChartData() { + return chartData; + } + + protected void setChartData(MultiCompetitorRaceDataDTO chartData) { + this.chartData = chartData; + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + setChartData(null); + clearChart(); + timeLineNeedsUpdate = true; + loadData(true); + } + + /** + * Checks the relation of the mark passings to the selection range. + * + * @param markPassingInRange + * A Boolean matrix filled by + * {@link AbstractChartPanel#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) + * fillPotentialXValues(...)} + * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the + * selection range needs to be refactored. Value B returns true if two passings are in range before the + * error happened or false, if the error happens before two passings were in the selection. B can be + * null. + */ + public Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { + boolean everyPassingInRange = true; + Boolean twoPassingsInRangeBeforeError = null; + ArrayList competitorPassings = markPassingInRange.get(0); + for (int i = 0; i < competitorPassings.size(); i++) { + Boolean passingInRange = competitorPassings.get(i); + for (int j = 1; j < markPassingInRange.size(); j++) { + Boolean passingToCompare = markPassingInRange.get(j).get(i); + if (passingInRange != null) { + if (passingToCompare != null && everyPassingInRange) { + everyPassingInRange = passingInRange.equals(passingToCompare); + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = true; + } + } else if (passingToCompare != null) { + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = false; + } + } + } else { + passingInRange = passingToCompare; + } + } + } + + return new Pair(everyPassingInRange, twoPassingsInRangeBeforeError); + } + + /** + * Forces the chart to {@link #updateTimeLine(Date) update} the position and to {@link #ensureTimeLineIsVisible() + * ensure}, that the time line is visible. + */ + public void forceTimeLineUpdate() { + updateTimeLine(timer.getTime()); + } + + /** + * Updates the position of the time line for the given {@link Date}, if {@link #allowTimeAdjust} is true.
+ * @param date Defines the x-Value of the line points + */ + private void updateTimeLine(Date date) { + if (allowTimeAdjust && chart != null) { + raceSelectionProvider.getSelectedRaces().get(0); + + Long x = date.getTime(); + Extremes extremes = chart.getYAxis(0).getExtremes(); + Point[] points = new Point[2]; + points[0] = new Point(x, extremes.getDataMin()); + points[1] = new Point(x, extremes.getDataMax()); + timeLineSeries.setPoints(points); + ensureTimeLineIsVisible(); + timeLineNeedsUpdate = false; + } + } + + /** + * Checks if the series of the chart contain the {@link #timeLineSeries}. If not, it is added to the chart.
+ * Throws a NPE if the chart is null. + */ + private void ensureTimeLineIsVisible() { + if (!Arrays.asList(chart.getSeries()).contains(timeLineSeries)) { + chart.addSeries(timeLineSeries); + } + } + + @Override + public void timeChanged(Date date) { + if (getChartData() != null) { + Date newestEvent = getChartData().getOldestDateOfNewestData(); + updateTimeLine(date); + if ((newestEvent == null || (newestEvent.before(date) && (date.getTime() - newestEvent.getTime()) >= getStepSize()))) { + loadData(false); + } + } + } + + private boolean needsDataLoading() { + return hasVisibleCompetitors() && isVisible(); + } + + @Override + public void onResize() { + if(getChartData() != null) { + chart.setSizeToMatchContainer(); + } + } + + @Override + public void competitorsListChanged(Iterable competitors) { + timeChanged(timer.getTime()); + } + + public void triggerDataLoading() { + loadData(true); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartSettingsComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartSettingsComponent.java index 6d6f60070e4..57fec4d6ccb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartSettingsComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartSettingsComponent.java @@ -1,65 +1,65 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.google.gwt.user.client.ui.DoubleBox; -import com.google.gwt.user.client.ui.FocusWidget; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.sap.sailing.gwt.ui.client.DataEntryDialog; -import com.sap.sailing.gwt.ui.client.DataEntryDialog.Validator; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -public abstract class AbstractChartSettingsComponent implements SettingsDialogComponent { - - private DoubleBox stepSizeBox; - private final ChartSettings settings; - private final StringMessages stringMessages; - - public AbstractChartSettingsComponent(ChartSettings settings, StringMessages stringMessages) { - this.settings = settings; - this.stringMessages = stringMessages; - } - - @Override - public VerticalPanel getAdditionalWidget(DataEntryDialog dialog) { - VerticalPanel panel = new VerticalPanel(); - panel.add(new Label(getStringMessages().stepSizeInSeconds())); - stepSizeBox = dialog.createDoubleBox(((double) settings.getStepSize()) / 1000, 5); - panel.add(stepSizeBox); - return panel; - } - - protected DoubleBox getStepSizeBox() { - return stepSizeBox; - } - - @Override - public Validator getValidator() { - return new Validator() { - @Override - public String getErrorMessage(SettingsType valueToValidate) { - String errorMessage = null; - if (valueToValidate.getStepSize() < 1) { - errorMessage = getStringMessages().stepSizeMustBeGreaterThanNull(); - } - return errorMessage; - } - }; - } - - @Override - public FocusWidget getFocusWidget() { - return stepSizeBox; - } - - protected StringMessages getStringMessages() { - return stringMessages; - } - - public ChartSettings getAbstractResult() { - Double valueInSeconds = getStepSizeBox().getValue(); - Long value = valueInSeconds == null ? 0 : (long) (getStepSizeBox().getValue() * 1000); - return new ChartSettings(value); - } - -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.google.gwt.user.client.ui.DoubleBox; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.gwt.ui.client.DataEntryDialog; +import com.sap.sailing.gwt.ui.client.DataEntryDialog.Validator; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +public abstract class AbstractChartSettingsComponent implements SettingsDialogComponent { + + private DoubleBox stepSizeBox; + private final ChartSettings settings; + private final StringMessages stringMessages; + + public AbstractChartSettingsComponent(ChartSettings settings, StringMessages stringMessages) { + this.settings = settings; + this.stringMessages = stringMessages; + } + + @Override + public VerticalPanel getAdditionalWidget(DataEntryDialog dialog) { + VerticalPanel panel = new VerticalPanel(); + panel.add(new Label(getStringMessages().stepSizeInSeconds())); + stepSizeBox = dialog.createDoubleBox(((double) settings.getStepSize()) / 1000, 5); + panel.add(stepSizeBox); + return panel; + } + + protected DoubleBox getStepSizeBox() { + return stepSizeBox; + } + + @Override + public Validator getValidator() { + return new Validator() { + @Override + public String getErrorMessage(SettingsType valueToValidate) { + String errorMessage = null; + if (valueToValidate.getStepSize() < 1) { + errorMessage = getStringMessages().stepSizeMustBeGreaterThanNull(); + } + return errorMessage; + } + }; + } + + @Override + public FocusWidget getFocusWidget() { + return stepSizeBox; + } + + protected StringMessages getStringMessages() { + return stringMessages; + } + + public ChartSettings getAbstractResult() { + Double valueInSeconds = getStepSizeBox().getValue(); + Long value = valueInSeconds == null ? 0 : (long) (getStepSizeBox().getValue() * 1000); + return new ChartSettings(value); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartPanel.java old mode 100644 new mode 100755 index 5319a887b2e..0e699effba2 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartPanel.java @@ -1,56 +1,56 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.Timer; -import com.sap.sailing.gwt.ui.shared.components.Component; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -/** - * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward distance to - * leader) for different races in a chart. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author D056866 Benjamin Ebling - * - */ -public class ChartPanel extends AbstractChartPanel implements Component { - - public ChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, CompetitorSelectionProvider competitorSelectionProvider, - RaceSelectionProvider raceSelectionProvider, Timer timer, DetailType dataToShow, StringMessages stringMessages, - ErrorReporter errorReporter, boolean compactChart, boolean allowTimeAdjust) { - super(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, timer, stringMessages, errorReporter, - dataToShow, compactChart, allowTimeAdjust); - } - - @Override - protected Component getComponent() { - return this; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new ChartSettingsComponent(getAbstractSettings(), getStringMessages()); - } - - @Override - public void updateSettings(ChartSettings newSettings) { - updateSettingsOnly(newSettings); - clearChart(); - loadData(true); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.competitorCharts(); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.Timer; +import com.sap.sailing.gwt.ui.shared.components.Component; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +/** + * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward distance to + * leader) for different races in a chart. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author D056866 Benjamin Ebling + * + */ +public class ChartPanel extends AbstractChartPanel implements Component { + + public ChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, CompetitorSelectionProvider competitorSelectionProvider, + RaceSelectionProvider raceSelectionProvider, Timer timer, DetailType dataToShow, StringMessages stringMessages, + ErrorReporter errorReporter, boolean compactChart, boolean allowTimeAdjust) { + super(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, timer, stringMessages, errorReporter, + dataToShow, compactChart, allowTimeAdjust); + } + + @Override + protected Component getComponent() { + return this; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new ChartSettingsComponent(getAbstractSettings(), getStringMessages()); + } + + @Override + public void updateSettings(ChartSettings newSettings) { + updateSettingsOnly(newSettings); + clearChart(); + loadData(true); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.competitorCharts(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettings.java old mode 100644 new mode 100755 index 172d6bb9252..aafdaefcd10 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettings.java @@ -1,20 +1,20 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -public class ChartSettings { - private final long stepSize; - - public ChartSettings(long stepSize) { - this.stepSize = stepSize; - } - - /** - * Copy-constructor - */ - public ChartSettings(ChartSettings superResult) { - this(superResult.getStepSize()); - } - - public long getStepSize() { - return stepSize; - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +public class ChartSettings { + private final long stepSize; + + public ChartSettings(long stepSize) { + this.stepSize = stepSize; + } + + /** + * Copy-constructor + */ + public ChartSettings(ChartSettings superResult) { + this(superResult.getStepSize()); + } + + public long getStepSize() { + return stepSize; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettingsComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettingsComponent.java old mode 100644 new mode 100755 index 7be7e52e5af..0a372253da0 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettingsComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/ChartSettingsComponent.java @@ -1,15 +1,15 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.sap.sailing.gwt.ui.client.StringMessages; - -public class ChartSettingsComponent extends AbstractChartSettingsComponent { - - public ChartSettingsComponent(ChartSettings settings, StringMessages stringMessages) { - super(settings, stringMessages); - } - - @Override - public ChartSettings getResult() { - return getAbstractResult(); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.sap.sailing.gwt.ui.client.StringMessages; + +public class ChartSettingsComponent extends AbstractChartSettingsComponent { + + public ChartSettingsComponent(ChartSettings settings, StringMessages stringMessages) { + super(settings, stringMessages); + } + + @Override + public ChartSettings getResult() { + return getAbstractResult(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartPanel.java old mode 100644 new mode 100755 index 72310fdff36..2cd7afe73d0 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartPanel.java @@ -1,59 +1,59 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.Timer; -import com.sap.sailing.gwt.ui.shared.components.Component; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -/** - * MultiChartPanel is a GWT panel that can show competitor data (e.g. current speed over ground, windward distance to - * leader) for different races in a chart. The chart type can be selected from the settings. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author Benjamin Ebling (D056866), Axel Uhl (d043530) - * - */ -public class MultiChartPanel extends AbstractChartPanel implements Component { - public MultiChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, - Timer timer, final StringMessages stringMessages, ErrorReporter errorReporter, boolean compactChart, - boolean allowTimeAdjust) { - super(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, timer, stringMessages, errorReporter, - /*show initially*/ DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER, compactChart, allowTimeAdjust); - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new MultiChartSettingsComponent(new MultiChartSettings(getAbstractSettings(), getDataToShow()), getStringMessages()); - } - - @Override - public void updateSettings(MultiChartSettings newSettings) { - boolean settingsChanged = updateSettingsOnly(newSettings); - boolean dataToShowChanged = setDataToShow(newSettings.getDataToShow()); - if (dataToShowChanged || settingsChanged) { - clearChart(); - loadData(true); - } - } - - @Override - protected Component getComponent() { - return this; - } - - @Override - public String getLocalizedShortName() { - return stringMessages.competitorCharts(); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.Timer; +import com.sap.sailing.gwt.ui.shared.components.Component; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +/** + * MultiChartPanel is a GWT panel that can show competitor data (e.g. current speed over ground, windward distance to + * leader) for different races in a chart. The chart type can be selected from the settings. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author Benjamin Ebling (D056866), Axel Uhl (d043530) + * + */ +public class MultiChartPanel extends AbstractChartPanel implements Component { + public MultiChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, + Timer timer, final StringMessages stringMessages, ErrorReporter errorReporter, boolean compactChart, + boolean allowTimeAdjust) { + super(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, timer, stringMessages, errorReporter, + /*show initially*/ DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER, compactChart, allowTimeAdjust); + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new MultiChartSettingsComponent(new MultiChartSettings(getAbstractSettings(), getDataToShow()), getStringMessages()); + } + + @Override + public void updateSettings(MultiChartSettings newSettings) { + boolean settingsChanged = updateSettingsOnly(newSettings); + boolean dataToShowChanged = setDataToShow(newSettings.getDataToShow()); + if (dataToShowChanged || settingsChanged) { + clearChart(); + loadData(true); + } + } + + @Override + protected Component getComponent() { + return this; + } + + @Override + public String getLocalizedShortName() { + return stringMessages.competitorCharts(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettings.java old mode 100644 new mode 100755 index d77f8dd41d3..9752c6a56ab --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettings.java @@ -1,16 +1,16 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.sap.sailing.domain.common.DetailType; - -public class MultiChartSettings extends ChartSettings { - private final DetailType dataToShow; - - public MultiChartSettings(ChartSettings superResult, DetailType dataToShow) { - super(superResult); - this.dataToShow = dataToShow; - } - - public DetailType getDataToShow() { - return dataToShow; - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.sap.sailing.domain.common.DetailType; + +public class MultiChartSettings extends ChartSettings { + private final DetailType dataToShow; + + public MultiChartSettings(ChartSettings superResult, DetailType dataToShow) { + super(superResult); + this.dataToShow = dataToShow; + } + + public DetailType getDataToShow() { + return dataToShow; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettingsComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettingsComponent.java old mode 100644 new mode 100755 index 820709a1a2b..4bf7c847f10 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettingsComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/MultiChartSettingsComponent.java @@ -1,54 +1,54 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.ListBox; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.gwt.ui.client.DataEntryDialog; -import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -public class MultiChartSettingsComponent extends AbstractChartSettingsComponent implements - SettingsDialogComponent { - private ListBox dataSelection; - private final DetailType initialDataToShow; - - public MultiChartSettingsComponent(MultiChartSettings settings, StringMessages stringMessages) { - super(settings, stringMessages); - this.initialDataToShow = settings.getDataToShow(); - } - - @Override - public VerticalPanel getAdditionalWidget(DataEntryDialog dialog) { - VerticalPanel configPanel = super.getAdditionalWidget(dialog); - configPanel.setSpacing(5); - Label lblChart = new Label(getStringMessages().chooseChart()); - configPanel.add(lblChart); - dataSelection = dialog.createListBox(/* isMultiSelect */ false); - int i=0; - for (DetailType detailType : new DetailType[] { DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER, DetailType.DISTANCE_TRAVELED, - DetailType.VELOCITY_MADE_GOOD_IN_KNOTS, DetailType.GAP_TO_LEADER_IN_SECONDS, DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS, - DetailType.RACE_RANK }) { - dataSelection.addItem(DetailTypeFormatter.format(detailType, getStringMessages()), detailType.toString()); - if (detailType == initialDataToShow) { - dataSelection.setSelectedIndex(i); - } - i++; - } - configPanel.add(dataSelection); - return configPanel; - } - - @Override - public MultiChartSettings getResult() { - DetailType dataToShow = null; - for (DetailType dt : DetailType.values()){ - if (dt.toString().equals(dataSelection.getValue(dataSelection.getSelectedIndex()))){ - dataToShow = dt; - } - } - return new MultiChartSettings(getAbstractResult(), dataToShow); - } - -} +package com.sap.sailing.gwt.ui.shared.charts; + +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.gwt.ui.client.DataEntryDialog; +import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +public class MultiChartSettingsComponent extends AbstractChartSettingsComponent implements + SettingsDialogComponent { + private ListBox dataSelection; + private final DetailType initialDataToShow; + + public MultiChartSettingsComponent(MultiChartSettings settings, StringMessages stringMessages) { + super(settings, stringMessages); + this.initialDataToShow = settings.getDataToShow(); + } + + @Override + public VerticalPanel getAdditionalWidget(DataEntryDialog dialog) { + VerticalPanel configPanel = super.getAdditionalWidget(dialog); + configPanel.setSpacing(5); + Label lblChart = new Label(getStringMessages().chooseChart()); + configPanel.add(lblChart); + dataSelection = dialog.createListBox(/* isMultiSelect */ false); + int i=0; + for (DetailType detailType : new DetailType[] { DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER, DetailType.DISTANCE_TRAVELED, + DetailType.VELOCITY_MADE_GOOD_IN_KNOTS, DetailType.GAP_TO_LEADER_IN_SECONDS, DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS, + DetailType.RACE_RANK }) { + dataSelection.addItem(DetailTypeFormatter.format(detailType, getStringMessages()), detailType.toString()); + if (detailType == initialDataToShow) { + dataSelection.setSelectedIndex(i); + } + i++; + } + configPanel.add(dataSelection); + return configPanel; + } + + @Override + public MultiChartSettings getResult() { + DetailType dataToShow = null; + for (DetailType dt : DetailType.values()){ + if (dt.toString().equals(dataSelection.getValue(dataSelection.getSelectedIndex()))){ + dataToShow = dt; + } + } + return new MultiChartSettings(getAbstractResult(), dataToShow); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/SimpleChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/SimpleChartPanel.java old mode 100644 new mode 100755 index 7560d08cb16..fdca259f019 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/SimpleChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/SimpleChartPanel.java @@ -1,50 +1,50 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.Legend; -import org.moxieapps.gwt.highcharts.client.events.SeriesCheckboxClickEvent; -import org.moxieapps.gwt.highcharts.client.events.SeriesCheckboxClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.SeriesLegendItemClickEvent; -import org.moxieapps.gwt.highcharts.client.events.SeriesLegendItemClickEventHandler; -import org.moxieapps.gwt.highcharts.client.plotOptions.PlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.SeriesPlotOptions; - -import com.google.gwt.user.client.ui.SimplePanel; -import com.sap.sailing.gwt.ui.client.Timer; - -public abstract class SimpleChartPanel extends SimplePanel { - - protected final Timer timer; - - public SimpleChartPanel(Timer timer) { - this.timer = timer; - } - - /** - * When using this method to enable the use of checkboxes only for hiding / showing a series, callers need to ensure - * that all series have {@link PlotOptions#setSelected(boolean)} set to true for all series that are - * added to the chart and hence visible. Otherwise, the checkbox won't initially be in sync with the series' - * visibility state. - */ - protected void useCheckboxesToShowAndHide(final Chart chart) { - chart.setLegend(new Legend().setEnabled(true).setBorderWidth(0).setSymbolPadding(20)); // make room for checkbox - chart.setSeriesPlotOptions(new SeriesPlotOptions().setSeriesCheckboxClickEventHandler(new SeriesCheckboxClickEventHandler() { - @Override - public boolean onClick(SeriesCheckboxClickEvent seriesCheckboxClickEvent) { - if (seriesCheckboxClickEvent.isChecked()) { - chart.getSeries(seriesCheckboxClickEvent.getSeriesId()).show(); - } else { - chart.getSeries(seriesCheckboxClickEvent.getSeriesId()).hide(); - } - return false; // don't toggle the select state of the series - } - }).setShowCheckbox(true). - setSeriesLegendItemClickEventHandler(new SeriesLegendItemClickEventHandler() { - @Override - public boolean onClick(SeriesLegendItemClickEvent seriesLegendItemClickEvent) { - // disable toggling visibility by clicking the legend item; force user to use checkbox instead - return false; - } - })); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.Legend; +import org.moxieapps.gwt.highcharts.client.events.SeriesCheckboxClickEvent; +import org.moxieapps.gwt.highcharts.client.events.SeriesCheckboxClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.SeriesLegendItemClickEvent; +import org.moxieapps.gwt.highcharts.client.events.SeriesLegendItemClickEventHandler; +import org.moxieapps.gwt.highcharts.client.plotOptions.PlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.SeriesPlotOptions; + +import com.google.gwt.user.client.ui.SimplePanel; +import com.sap.sailing.gwt.ui.client.Timer; + +public abstract class SimpleChartPanel extends SimplePanel { + + protected final Timer timer; + + public SimpleChartPanel(Timer timer) { + this.timer = timer; + } + + /** + * When using this method to enable the use of checkboxes only for hiding / showing a series, callers need to ensure + * that all series have {@link PlotOptions#setSelected(boolean)} set to true for all series that are + * added to the chart and hence visible. Otherwise, the checkbox won't initially be in sync with the series' + * visibility state. + */ + protected void useCheckboxesToShowAndHide(final Chart chart) { + chart.setLegend(new Legend().setEnabled(true).setBorderWidth(0).setSymbolPadding(20)); // make room for checkbox + chart.setSeriesPlotOptions(new SeriesPlotOptions().setSeriesCheckboxClickEventHandler(new SeriesCheckboxClickEventHandler() { + @Override + public boolean onClick(SeriesCheckboxClickEvent seriesCheckboxClickEvent) { + if (seriesCheckboxClickEvent.isChecked()) { + chart.getSeries(seriesCheckboxClickEvent.getSeriesId()).show(); + } else { + chart.getSeries(seriesCheckboxClickEvent.getSeriesId()).hide(); + } + return false; // don't toggle the select state of the series + } + }).setShowCheckbox(true). + setSeriesLegendItemClickEventHandler(new SeriesLegendItemClickEventHandler() { + @Override + public boolean onClick(SeriesLegendItemClickEvent seriesLegendItemClickEvent) { + // disable toggling visibility by clicking the legend item; force user to use checkbox instead + return false; + } + })); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChart.java old mode 100644 new mode 100755 index 3abbef5a3b3..6da4dcdc111 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChart.java @@ -1,581 +1,581 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Color; -import org.moxieapps.gwt.highcharts.client.PlotLine; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.XAxis; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; -import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; - -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.EventAndRaceIdentifier; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; -import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; -import com.sap.sailing.gwt.ui.client.ColorMap; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesCalculationUtil; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.TimeListener; -import com.sap.sailing.gwt.ui.client.Timer; -import com.sap.sailing.gwt.ui.client.Timer.PlayModes; -import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sailing.gwt.ui.shared.components.Component; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -public class WindChart extends SimpleChartPanel implements Component, RaceSelectionChangeListener, - RaceTimesInfoProviderListener, TimeListener, RequiresResize { - public static final long DEFAULT_RESOLUTION_IN_MILLISECONDS = 10000; - - private static final int LINE_WIDTH = 1; - private final StringMessages stringMessages; - private final Set windSourceTypesToDisplay; - private long resolutionInMilliseconds; - private boolean showWindSpeedSeries; - private boolean showWindDirectionsSeries; - - /** - * Holds one series for each wind source for which data has been received. - */ - private final Map windSourceDirectionSeries; - private final Map windSourceSpeedSeries; - - private final ErrorReporter errorReporter; - private final AsyncActionsExecutor asyncActionsExecutor; - private final SailingServiceAsync sailingService; - private final Chart chart; - private boolean ignoreClickOnce; - private final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); - - private Long timeOfEarliestRequestInMillis; - private Long timeOfLatestRequestInMillis; - - private Date minTimepoint; - private Date maxTimepoint; - private Date minSelectedTimepoint; - private Date maxSelectedTimepoint; - - private RaceIdentifier selectedRaceIdentifier; - private RaceTimesInfoDTO lastRaceTimesInfo; - - private final ColorMap colorMap; - private boolean isZoomed = false; - - /** - * @param raceSelectionProvider - * if null, this chart won't update its contents automatically upon race selection change; - * otherwise, whenever the selection changes, the wind data of the race selected now is loaded from the - * server and displayed in this chart. If no race is selected, the chart is cleared. - */ - public WindChart(SailingServiceAsync sailingService, RaceSelectionProvider raceSelectionProvider, Timer timer, - WindChartSettings settings, final StringMessages stringMessages, AsyncActionsExecutor asyncActionsExecutor, - ErrorReporter errorReporter, boolean compactChart) { - super(timer); - this.sailingService = sailingService; - this.asyncActionsExecutor = asyncActionsExecutor; - this.stringMessages = stringMessages; - this.errorReporter = errorReporter; - this.windSourceDirectionSeries = new HashMap(); - this.windSourceSpeedSeries = new HashMap(); - this.colorMap = new ColorMap(); - this.windSourceTypesToDisplay = new HashSet(); - this.showWindSpeedSeries = false; - this.showWindDirectionsSeries = true; - chart = new Chart() - .setZoomType(Chart.ZoomType.X) - .setMarginLeft(65) - .setMarginRight(65) - .setWidth100() - .setHeight100() - .setChartTitle(new ChartTitle().setText(stringMessages.wind())) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) - .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker( - new Marker().setEnabled(false).setHoverState( - new Marker().setEnabled(true).setRadius(4))).setShadow(false) - .setHoverStateLineWidth(LINE_WIDTH)); - useCheckboxesToShowAndHide(chart); - final NumberFormat numberFormat = NumberFormat.getFormat("0"); - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - String seriesName = toolTipData.getSeriesName(); - if (seriesName.equals(WindChart.this.stringMessages.time())) { - return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) - + "
(" + stringMessages.clickChartToSetTime() + ")"; - } else if (seriesName.startsWith(stringMessages.fromDeg()+" ")) { - double value = toolTipData.getYAsDouble() % 360; - return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") - + "
" + - dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + - numberFormat.format(value < 0 ? value + 360 : value) + stringMessages.degreesShort(); - } else { - return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") - + "
" + - dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + - numberFormat.format(toolTipData.getYAsDouble()) + stringMessages.averageSpeedInKnotsUnit(); - } - } - })); - - chart.setBackgroundColor(new Color("#ebebec")); - - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - if (ignoreClickOnce) { - ignoreClickOnce = false; - } else { - WindChart.this.timer.setPlayMode(PlayModes.Replay); - WindChart.this.timer.setTime(chartClickEvent.getXAxisValueAsLong()); - } - return true; - } - }); - - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - try { - long xAxisMin = chartSelectionEvent.getXAxisMinAsLong(); - long xAxisMax = chartSelectionEvent.getXAxisMaxAsLong(); - WindChart.this.minSelectedTimepoint = new Date(xAxisMin); - WindChart.this.maxSelectedTimepoint = new Date(xAxisMax); - WindChart.this.isZoomed = true; - ignoreClickOnce = true; - } catch (Throwable t) { - WindChart.this.minSelectedTimepoint = null; - WindChart.this.maxSelectedTimepoint = null; - WindChart.this.isZoomed = false; - //Redrawing, or the chart wouldn't rezoom - chart.redraw(); - } - return true; - } - }); - - chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(10000) // ten seconds - .setAxisTitleText(stringMessages.time()); - chart.getYAxis(0).setAxisTitleText(stringMessages.fromDeg()).setStartOnTick(false).setShowFirstLabel(false) - .setLabels(new YAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - long value = axisLabelsData.getValueAsLong() % 360; - return new Long(value < 0 ? value + 360 : value).toString(); - } - })); - - chart.getYAxis(1).setOpposite(true).setAxisTitleText(stringMessages.speed()+" ("+stringMessages.averageSpeedInKnotsUnit()+")") - .setStartOnTick(false).setShowFirstLabel(false).setGridLineWidth(0).setMinorGridLineWidth(0); - - if (compactChart) { - chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) - .setOption("legend/margin", 2) - .setOption("title/margin", 5) - .setChartSubtitle(null) - .getXAxis().setAxisTitle(null); - } - - setWidget(chart); - setSize("100%", "100%"); - updateSettings(settings); - if (raceSelectionProvider != null) { - raceSelectionProvider.addRaceSelectionChangeListener(this); - onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - } - showVisibleSeries(); - timer.addTimeListener(this); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.wind(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - private void showVisibleSeries() { - Set visibleSeries = new HashSet(Arrays.asList(chart.getSeries())); - - if(showWindDirectionsSeries) { - for (Map.Entry e : windSourceDirectionSeries.entrySet()) { - Series series = e.getValue(); - if (windSourceTypesToDisplay.contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); - series.select(true); // ensures that the checkbox will be ticked - } - } else { - if(visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - } - - if(showWindSpeedSeries) { - for (Map.Entry e : windSourceSpeedSeries.entrySet()) { - Series series = e.getValue(); - if (windSourceTypesToDisplay.contains(e.getKey().getType())) { - if (!visibleSeries.contains(series)) { - chart.addSeries(series, false, false); - series.select(true); // ensures that the checkbox will be ticked - } - } else { - if(visibleSeries.contains(series)) { - chart.removeSeries(series, false); - } - } - } - } - chart.redraw(); - } - - /** - * Creates the series for the windSource specified. If the series is created and needs to be visible - * based on the {@link #windSourceTypesToDisplay}, it is added to the chart. - */ - private Series getOrCreateSpeedSeries(WindSource windSource) { - Series result = windSourceSpeedSeries.get(windSource); - if (result == null) { - result = createSpeedSeries(windSource); - windSourceSpeedSeries.put(windSource, result); - if (showWindSpeedSeries && windSourceTypesToDisplay.contains(windSource.getType())) { - chart.addSeries(result); - } - } - return result; - } - - /** - * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateDirectionSeries(WindSource)} and - * {@link #showVisibleSeries()} - */ - private Series createDirectionSeries(WindSource windSource) { - Series newSeries = chart - .createSeries() - .setType(Series.Type.LINE) - .setName(stringMessages.fromDeg()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) - .setYAxis(0) - .setPlotOptions(new LinePlotOptions().setColor(colorMap.getColorByID(windSource)).setSelected(true)); - return newSeries; - } - - /** - * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateSpeedSeries(WindSource)} and - * {@link #showVisibleSeries()} - */ - private Series createSpeedSeries(WindSource windSource) { - Series newSeries = chart - .createSeries() - .setType(Series.Type.LINE) - .setName(stringMessages.windSpeed()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) - .setYAxis(1) // use the second Y-axis - .setPlotOptions(new LinePlotOptions().setDashStyle(PlotLine.DashStyle.SHORT_DOT) - .setLineWidth(3).setHoverStateLineWidth(3) - .setColor(colorMap.getColorByID(windSource)).setSelected(true)); // show only the markers, not the connecting lines - return newSeries; - } - - /** - * Updates the wind charts with the wind data from result. If append is true, previously - * existing points in the chart are left unchanged. Otherwise, the existing wind series are replaced. - */ - public void updateStripChartSeries(WindInfoForRaceDTO result, boolean append) { - final NumberFormat numberFormat = NumberFormat.getFormat("0"); - for (Map.Entry e : result.windTrackInfoByWindSource.entrySet()) { - WindSource windSource = e.getKey(); - Series directionSeries = getOrCreateDirectionSeries(windSource); - Series speedSeries = null; - if (windSource.getType().useSpeed()) { - speedSeries = getOrCreateSpeedSeries(windSource); - } - WindTrackInfoDTO windTrackInfo = e.getValue(); - Double directionMin = null; - Double directionMax = null; - Point[] directionPoints = new Point[windTrackInfo.windFixes.size()]; - Point[] speedPoints = new Point[windTrackInfo.windFixes.size()]; - int i=0; - for (WindDTO wind : windTrackInfo.windFixes) { - if (timeOfEarliestRequestInMillis == null || wind.timepointtimeOfLatestRequestInMillis) { - timeOfLatestRequestInMillis = wind.originTimepoint; - } - - Point newDirectionPoint = new Point(wind.originTimepoint, wind.dampenedTrueWindFromDeg); - if (wind.dampenedTrueWindSpeedInKnots != null) { - String name = numberFormat.format(wind.dampenedTrueWindSpeedInKnots)+ stringMessages.averageSpeedInKnotsUnit(); - // name += " Confidence:" + wind.confidence; - newDirectionPoint.setName(name); - } - newDirectionPoint = recalculateDirectionPoint(directionMax, directionMin, newDirectionPoint); - directionPoints[i] = newDirectionPoint; - - double direction = newDirectionPoint.getY().doubleValue(); - if (directionMax == null || direction > directionMax) { - directionMax = direction; - } - if (directionMin == null || direction < directionMin) { - directionMin = direction; - } - - Point newSpeedPoint = new Point(wind.originTimepoint, wind.dampenedTrueWindSpeedInKnots); - speedPoints[i++] = newSpeedPoint; - } - Point[] newDirectionPoints; - Point[] newSpeedPoints = null; - if (append) { - Point[] oldDirectionPoints = directionSeries.getPoints(); - newDirectionPoints = new Point[oldDirectionPoints.length + directionPoints.length]; - System.arraycopy(oldDirectionPoints, 0, newDirectionPoints, 0, oldDirectionPoints.length); - System.arraycopy(directionPoints, 0, newDirectionPoints, oldDirectionPoints.length, directionPoints.length); - if (windSource.getType().useSpeed()) { - Point[] oldSpeedPoints = speedSeries.getPoints(); - newSpeedPoints = new Point[oldSpeedPoints.length + speedPoints.length]; - System.arraycopy(oldSpeedPoints, 0, newSpeedPoints, 0, oldSpeedPoints.length); - System.arraycopy(speedPoints, 0, newSpeedPoints, oldSpeedPoints.length, speedPoints.length); - } - } else { - newDirectionPoints = directionPoints; - newSpeedPoints = speedPoints; - } - directionSeries.setPoints(newDirectionPoints); - if (windSource.getType().useSpeed()) { - speedSeries.setPoints(newSpeedPoints); - } - } - } - - private Point recalculateDirectionPoint(Double yMax, Double yMin, Point directionPoint) { - double y = directionPoint.getY().doubleValue(); - boolean recalculated = false; - - if (yMax != null && yMin != null && (y < yMin || yMax < y)) { - double deltaMin = Math.abs(yMin - y); - double deltaMax = Math.abs(yMax - y); - - double yDown = y - 360; - double deltaMinDown = Math.abs(yMin - Math.abs(yDown)); - - double yUp = y + 360; - double deltaMaxUp = Math.abs(yMax - Math.abs(yUp)); - - if (!(deltaMin <= deltaMinDown && deltaMin <= deltaMaxUp) - && !(deltaMax <= deltaMinDown && deltaMax <= deltaMaxUp)) { - y = deltaMaxUp <= deltaMinDown ? yUp : yDown; - recalculated = true; - } - } - - return recalculated ? new Point(directionPoint.getX(), y) : directionPoint; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new WindChartSettingsDialogComponent(new WindChartSettings(showWindSpeedSeries, showWindDirectionsSeries, windSourceTypesToDisplay, resolutionInMilliseconds), stringMessages); - } - - /** - * Sets the visibilities of the wind source series based on the new settings. Note that this does not - * re-load any wind data. This has to happen by calling {@link #updateStripChartSeries(WindInfoForRaceDTO, boolean)}. - */ - @Override - public void updateSettings(WindChartSettings newSettings) { - if (newSettings.getResolutionInMilliseconds() != resolutionInMilliseconds) { - resolutionInMilliseconds = newSettings.getResolutionInMilliseconds(); - clearCacheAndReload(); - } - showWindDirectionsSeries = newSettings.isShowWindDirectionsSeries(); - showWindSpeedSeries = newSettings.isShowWindSpeedSeries(); - windSourceTypesToDisplay.clear(); - windSourceTypesToDisplay.addAll(newSettings.getWindSourceTypesToDisplay()); - showVisibleSeries(); - } - - private void clearCacheAndReload() { - timeOfEarliestRequestInMillis = null; - timeOfLatestRequestInMillis = null; - - loadData(selectedRaceIdentifier, minTimepoint, maxTimepoint, /* append */false); - } - - /** - * Creates the series for the windSource specified. If the series is created and needs to be visible - * based on the {@link #windSourceTypesToDisplay}, it is added to the chart. - */ - private Series getOrCreateDirectionSeries(WindSource windSource) { - Series result = windSourceDirectionSeries.get(windSource); - if (result == null) { - result = createDirectionSeries(windSource); - windSourceDirectionSeries.put(windSource, result); - if (showWindDirectionsSeries && windSourceTypesToDisplay.contains(windSource.getType())) { - chart.addSeries(result); - } - } - return result; - } - - /** - * @param append - * if true, the results retrieved from the server will be appended to the wind chart instead - * of overwriting the existing series. - */ - private void loadData(final RaceIdentifier raceIdentifier, final Date from, final Date to, final boolean append) { - if (raceIdentifier == null) { - clearChart(); - } else if (needsDataLoading() && from != null && to != null) { - chart.showLoading("Loading wind data..."); - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, raceIdentifier, - // TODO Time interval should be determined by a selection in the chart but be at most 60s. See bug #121. - // Consider incremental updates for new data only. - from, to, resolutionInMilliseconds, - null, new AsyncCallback() { - @Override - public void onSuccess(WindInfoForRaceDTO result) { - if (result != null) { - XAxis xAxis = chart.getXAxis(); - - xAxis.setMin(minTimepoint.getTime()); - xAxis.setMax(maxTimepoint.getTime()); - xAxis.setStartOnTick(false); - xAxis.setEndOnTick(false); - - updateStripChartSeries(result, append); - } else { - if (!append) { - clearChart(); // no wind known for untracked race - } - } - chart.hideLoading(); - } - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorFetchingWindInformationForRace() + " " - + raceIdentifier + ": " + caught.getMessage(), timer.getPlayMode() == PlayModes.Live); - chart.hideLoading(); - } - }); - asyncActionsExecutor.execute(getWindInfoAction); - } - } - - private void clearChart() { - chart.removeAllSeries(); - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - // show wind of first selected race - selectedRaceIdentifier = selectedRaces.iterator().next(); - clearCacheAndReload(); - } else { - clearChart(); - } - } - - @Override - public void raceTimesInfosReceived(Map raceTimesInfos) { - this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaceIdentifier); - - Pair raceMinMax = RaceTimesCalculationUtil.caluclateRaceMinMax(timer, this.lastRaceTimesInfo); - - this.minTimepoint = raceMinMax.getA(); - this.maxTimepoint = raceMinMax.getB(); - } - - /** - * If in live mode, fetches what's missing since the last fix and date. If nothing has been loaded yet, - * loads from the beginning up to date. If in replay mode, checks if anything has been loaded at all. If not, - * everything for the currently selected race is loaded; otherwise, no-op. - */ - @Override - public void timeChanged(Date date) { - if (timer.getPlayMode() == PlayModes.Live) { - // is date before first cache entry or is cache empty? - if (timeOfEarliestRequestInMillis == null || timeOfEarliestRequestInMillis > date.getTime()) { - loadData(selectedRaceIdentifier, minTimepoint, date, /* append */ true); - } else if (timeOfLatestRequestInMillis < date.getTime()) { - loadData(selectedRaceIdentifier, new Date(timeOfLatestRequestInMillis), maxTimepoint, /* append */true); - } - // otherwise the cache spans across date and so we don't need to load anything - } else { - // assuming play mode is replay / non-live - if (timeOfLatestRequestInMillis == null) { - loadData(selectedRaceIdentifier, minTimepoint, maxTimepoint, /* append */false); // replace old series - } - } - } - - @Override - public void onResize() { - if(isVisible()) { - chart.setSizeToMatchContainer(); - } - } - - private boolean needsDataLoading() { - return isVisible(); - } - - public Date getMinSelectedTimepoint() { - return minSelectedTimepoint; - } - - public Date getMaxSelectedTimepoint() { - return maxSelectedTimepoint; - } - - public boolean isZoomed() { - return isZoomed; - } - -} +package com.sap.sailing.gwt.ui.shared.charts; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Color; +import org.moxieapps.gwt.highcharts.client.PlotLine; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.XAxis; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; +import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.EventAndRaceIdentifier; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor; +import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; +import com.sap.sailing.gwt.ui.client.ColorMap; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesCalculationUtil; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.TimeListener; +import com.sap.sailing.gwt.ui.client.Timer; +import com.sap.sailing.gwt.ui.client.Timer.PlayModes; +import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; +import com.sap.sailing.gwt.ui.shared.components.Component; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +public class WindChart extends SimpleChartPanel implements Component, RaceSelectionChangeListener, + RaceTimesInfoProviderListener, TimeListener, RequiresResize { + public static final long DEFAULT_RESOLUTION_IN_MILLISECONDS = 10000; + + private static final int LINE_WIDTH = 1; + private final StringMessages stringMessages; + private final Set windSourceTypesToDisplay; + private long resolutionInMilliseconds; + private boolean showWindSpeedSeries; + private boolean showWindDirectionsSeries; + + /** + * Holds one series for each wind source for which data has been received. + */ + private final Map windSourceDirectionSeries; + private final Map windSourceSpeedSeries; + + private final ErrorReporter errorReporter; + private final AsyncActionsExecutor asyncActionsExecutor; + private final SailingServiceAsync sailingService; + private final Chart chart; + private boolean ignoreClickOnce; + private final DateTimeFormat dateFormat = DateTimeFormat.getFormat("HH:mm:ss"); + + private Long timeOfEarliestRequestInMillis; + private Long timeOfLatestRequestInMillis; + + private Date minTimepoint; + private Date maxTimepoint; + private Date minSelectedTimepoint; + private Date maxSelectedTimepoint; + + private RaceIdentifier selectedRaceIdentifier; + private RaceTimesInfoDTO lastRaceTimesInfo; + + private final ColorMap colorMap; + private boolean isZoomed = false; + + /** + * @param raceSelectionProvider + * if null, this chart won't update its contents automatically upon race selection change; + * otherwise, whenever the selection changes, the wind data of the race selected now is loaded from the + * server and displayed in this chart. If no race is selected, the chart is cleared. + */ + public WindChart(SailingServiceAsync sailingService, RaceSelectionProvider raceSelectionProvider, Timer timer, + WindChartSettings settings, final StringMessages stringMessages, AsyncActionsExecutor asyncActionsExecutor, + ErrorReporter errorReporter, boolean compactChart) { + super(timer); + this.sailingService = sailingService; + this.asyncActionsExecutor = asyncActionsExecutor; + this.stringMessages = stringMessages; + this.errorReporter = errorReporter; + this.windSourceDirectionSeries = new HashMap(); + this.windSourceSpeedSeries = new HashMap(); + this.colorMap = new ColorMap(); + this.windSourceTypesToDisplay = new HashSet(); + this.showWindSpeedSeries = false; + this.showWindDirectionsSeries = true; + chart = new Chart() + .setZoomType(Chart.ZoomType.X) + .setMarginLeft(65) + .setMarginRight(65) + .setWidth100() + .setHeight100() + .setChartTitle(new ChartTitle().setText(stringMessages.wind())) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) + .setLinePlotOptions(new LinePlotOptions().setLineWidth(LINE_WIDTH).setMarker( + new Marker().setEnabled(false).setHoverState( + new Marker().setEnabled(true).setRadius(4))).setShadow(false) + .setHoverStateLineWidth(LINE_WIDTH)); + useCheckboxesToShowAndHide(chart); + final NumberFormat numberFormat = NumberFormat.getFormat("0"); + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + String seriesName = toolTipData.getSeriesName(); + if (seriesName.equals(WindChart.this.stringMessages.time())) { + return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) + + "
(" + stringMessages.clickChartToSetTime() + ")"; + } else if (seriesName.startsWith(stringMessages.fromDeg()+" ")) { + double value = toolTipData.getYAsDouble() % 360; + return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") + + "
" + + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(value < 0 ? value + 360 : value) + stringMessages.degreesShort(); + } else { + return "" + seriesName + (toolTipData.getPointName() != null ? " "+toolTipData.getPointName() : "") + + "
" + + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(toolTipData.getYAsDouble()) + stringMessages.averageSpeedInKnotsUnit(); + } + } + })); + + chart.setBackgroundColor(new Color("#ebebec")); + + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + if (ignoreClickOnce) { + ignoreClickOnce = false; + } else { + WindChart.this.timer.setPlayMode(PlayModes.Replay); + WindChart.this.timer.setTime(chartClickEvent.getXAxisValueAsLong()); + } + return true; + } + }); + + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + try { + long xAxisMin = chartSelectionEvent.getXAxisMinAsLong(); + long xAxisMax = chartSelectionEvent.getXAxisMaxAsLong(); + WindChart.this.minSelectedTimepoint = new Date(xAxisMin); + WindChart.this.maxSelectedTimepoint = new Date(xAxisMax); + WindChart.this.isZoomed = true; + ignoreClickOnce = true; + } catch (Throwable t) { + WindChart.this.minSelectedTimepoint = null; + WindChart.this.maxSelectedTimepoint = null; + WindChart.this.isZoomed = false; + //Redrawing, or the chart wouldn't rezoom + chart.redraw(); + } + return true; + } + }); + + chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(10000) // ten seconds + .setAxisTitleText(stringMessages.time()); + chart.getYAxis(0).setAxisTitleText(stringMessages.fromDeg()).setStartOnTick(false).setShowFirstLabel(false) + .setLabels(new YAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + long value = axisLabelsData.getValueAsLong() % 360; + return new Long(value < 0 ? value + 360 : value).toString(); + } + })); + + chart.getYAxis(1).setOpposite(true).setAxisTitleText(stringMessages.speed()+" ("+stringMessages.averageSpeedInKnotsUnit()+")") + .setStartOnTick(false).setShowFirstLabel(false).setGridLineWidth(0).setMinorGridLineWidth(0); + + if (compactChart) { + chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) + .setOption("legend/margin", 2) + .setOption("title/margin", 5) + .setChartSubtitle(null) + .getXAxis().setAxisTitle(null); + } + + setWidget(chart); + setSize("100%", "100%"); + updateSettings(settings); + if (raceSelectionProvider != null) { + raceSelectionProvider.addRaceSelectionChangeListener(this); + onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + } + showVisibleSeries(); + timer.addTimeListener(this); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.wind(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + private void showVisibleSeries() { + Set visibleSeries = new HashSet(Arrays.asList(chart.getSeries())); + + if(showWindDirectionsSeries) { + for (Map.Entry e : windSourceDirectionSeries.entrySet()) { + Series series = e.getValue(); + if (windSourceTypesToDisplay.contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + series.select(true); // ensures that the checkbox will be ticked + } + } else { + if(visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + } + + if(showWindSpeedSeries) { + for (Map.Entry e : windSourceSpeedSeries.entrySet()) { + Series series = e.getValue(); + if (windSourceTypesToDisplay.contains(e.getKey().getType())) { + if (!visibleSeries.contains(series)) { + chart.addSeries(series, false, false); + series.select(true); // ensures that the checkbox will be ticked + } + } else { + if(visibleSeries.contains(series)) { + chart.removeSeries(series, false); + } + } + } + } + chart.redraw(); + } + + /** + * Creates the series for the windSource specified. If the series is created and needs to be visible + * based on the {@link #windSourceTypesToDisplay}, it is added to the chart. + */ + private Series getOrCreateSpeedSeries(WindSource windSource) { + Series result = windSourceSpeedSeries.get(windSource); + if (result == null) { + result = createSpeedSeries(windSource); + windSourceSpeedSeries.put(windSource, result); + if (showWindSpeedSeries && windSourceTypesToDisplay.contains(windSource.getType())) { + chart.addSeries(result); + } + } + return result; + } + + /** + * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateDirectionSeries(WindSource)} and + * {@link #showVisibleSeries()} + */ + private Series createDirectionSeries(WindSource windSource) { + Series newSeries = chart + .createSeries() + .setType(Series.Type.LINE) + .setName(stringMessages.fromDeg()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) + .setYAxis(0) + .setPlotOptions(new LinePlotOptions().setColor(colorMap.getColorByID(windSource)).setSelected(true)); + return newSeries; + } + + /** + * Only creates the series but doesn't add it to the chart. See also {@link #getOrCreateSpeedSeries(WindSource)} and + * {@link #showVisibleSeries()} + */ + private Series createSpeedSeries(WindSource windSource) { + Series newSeries = chart + .createSeries() + .setType(Series.Type.LINE) + .setName(stringMessages.windSpeed()+" "+WindSourceTypeFormatter.format(windSource, stringMessages)) + .setYAxis(1) // use the second Y-axis + .setPlotOptions(new LinePlotOptions().setDashStyle(PlotLine.DashStyle.SHORT_DOT) + .setLineWidth(3).setHoverStateLineWidth(3) + .setColor(colorMap.getColorByID(windSource)).setSelected(true)); // show only the markers, not the connecting lines + return newSeries; + } + + /** + * Updates the wind charts with the wind data from result. If append is true, previously + * existing points in the chart are left unchanged. Otherwise, the existing wind series are replaced. + */ + public void updateStripChartSeries(WindInfoForRaceDTO result, boolean append) { + final NumberFormat numberFormat = NumberFormat.getFormat("0"); + for (Map.Entry e : result.windTrackInfoByWindSource.entrySet()) { + WindSource windSource = e.getKey(); + Series directionSeries = getOrCreateDirectionSeries(windSource); + Series speedSeries = null; + if (windSource.getType().useSpeed()) { + speedSeries = getOrCreateSpeedSeries(windSource); + } + WindTrackInfoDTO windTrackInfo = e.getValue(); + Double directionMin = null; + Double directionMax = null; + Point[] directionPoints = new Point[windTrackInfo.windFixes.size()]; + Point[] speedPoints = new Point[windTrackInfo.windFixes.size()]; + int i=0; + for (WindDTO wind : windTrackInfo.windFixes) { + if (timeOfEarliestRequestInMillis == null || wind.timepointtimeOfLatestRequestInMillis) { + timeOfLatestRequestInMillis = wind.originTimepoint; + } + + Point newDirectionPoint = new Point(wind.originTimepoint, wind.dampenedTrueWindFromDeg); + if (wind.dampenedTrueWindSpeedInKnots != null) { + String name = numberFormat.format(wind.dampenedTrueWindSpeedInKnots)+ stringMessages.averageSpeedInKnotsUnit(); + // name += " Confidence:" + wind.confidence; + newDirectionPoint.setName(name); + } + newDirectionPoint = recalculateDirectionPoint(directionMax, directionMin, newDirectionPoint); + directionPoints[i] = newDirectionPoint; + + double direction = newDirectionPoint.getY().doubleValue(); + if (directionMax == null || direction > directionMax) { + directionMax = direction; + } + if (directionMin == null || direction < directionMin) { + directionMin = direction; + } + + Point newSpeedPoint = new Point(wind.originTimepoint, wind.dampenedTrueWindSpeedInKnots); + speedPoints[i++] = newSpeedPoint; + } + Point[] newDirectionPoints; + Point[] newSpeedPoints = null; + if (append) { + Point[] oldDirectionPoints = directionSeries.getPoints(); + newDirectionPoints = new Point[oldDirectionPoints.length + directionPoints.length]; + System.arraycopy(oldDirectionPoints, 0, newDirectionPoints, 0, oldDirectionPoints.length); + System.arraycopy(directionPoints, 0, newDirectionPoints, oldDirectionPoints.length, directionPoints.length); + if (windSource.getType().useSpeed()) { + Point[] oldSpeedPoints = speedSeries.getPoints(); + newSpeedPoints = new Point[oldSpeedPoints.length + speedPoints.length]; + System.arraycopy(oldSpeedPoints, 0, newSpeedPoints, 0, oldSpeedPoints.length); + System.arraycopy(speedPoints, 0, newSpeedPoints, oldSpeedPoints.length, speedPoints.length); + } + } else { + newDirectionPoints = directionPoints; + newSpeedPoints = speedPoints; + } + directionSeries.setPoints(newDirectionPoints); + if (windSource.getType().useSpeed()) { + speedSeries.setPoints(newSpeedPoints); + } + } + } + + private Point recalculateDirectionPoint(Double yMax, Double yMin, Point directionPoint) { + double y = directionPoint.getY().doubleValue(); + boolean recalculated = false; + + if (yMax != null && yMin != null && (y < yMin || yMax < y)) { + double deltaMin = Math.abs(yMin - y); + double deltaMax = Math.abs(yMax - y); + + double yDown = y - 360; + double deltaMinDown = Math.abs(yMin - Math.abs(yDown)); + + double yUp = y + 360; + double deltaMaxUp = Math.abs(yMax - Math.abs(yUp)); + + if (!(deltaMin <= deltaMinDown && deltaMin <= deltaMaxUp) + && !(deltaMax <= deltaMinDown && deltaMax <= deltaMaxUp)) { + y = deltaMaxUp <= deltaMinDown ? yUp : yDown; + recalculated = true; + } + } + + return recalculated ? new Point(directionPoint.getX(), y) : directionPoint; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new WindChartSettingsDialogComponent(new WindChartSettings(showWindSpeedSeries, showWindDirectionsSeries, windSourceTypesToDisplay, resolutionInMilliseconds), stringMessages); + } + + /** + * Sets the visibilities of the wind source series based on the new settings. Note that this does not + * re-load any wind data. This has to happen by calling {@link #updateStripChartSeries(WindInfoForRaceDTO, boolean)}. + */ + @Override + public void updateSettings(WindChartSettings newSettings) { + if (newSettings.getResolutionInMilliseconds() != resolutionInMilliseconds) { + resolutionInMilliseconds = newSettings.getResolutionInMilliseconds(); + clearCacheAndReload(); + } + showWindDirectionsSeries = newSettings.isShowWindDirectionsSeries(); + showWindSpeedSeries = newSettings.isShowWindSpeedSeries(); + windSourceTypesToDisplay.clear(); + windSourceTypesToDisplay.addAll(newSettings.getWindSourceTypesToDisplay()); + showVisibleSeries(); + } + + private void clearCacheAndReload() { + timeOfEarliestRequestInMillis = null; + timeOfLatestRequestInMillis = null; + + loadData(selectedRaceIdentifier, minTimepoint, maxTimepoint, /* append */false); + } + + /** + * Creates the series for the windSource specified. If the series is created and needs to be visible + * based on the {@link #windSourceTypesToDisplay}, it is added to the chart. + */ + private Series getOrCreateDirectionSeries(WindSource windSource) { + Series result = windSourceDirectionSeries.get(windSource); + if (result == null) { + result = createDirectionSeries(windSource); + windSourceDirectionSeries.put(windSource, result); + if (showWindDirectionsSeries && windSourceTypesToDisplay.contains(windSource.getType())) { + chart.addSeries(result); + } + } + return result; + } + + /** + * @param append + * if true, the results retrieved from the server will be appended to the wind chart instead + * of overwriting the existing series. + */ + private void loadData(final RaceIdentifier raceIdentifier, final Date from, final Date to, final boolean append) { + if (raceIdentifier == null) { + clearChart(); + } else if (needsDataLoading() && from != null && to != null) { + chart.showLoading("Loading wind data..."); + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, raceIdentifier, + // TODO Time interval should be determined by a selection in the chart but be at most 60s. See bug #121. + // Consider incremental updates for new data only. + from, to, resolutionInMilliseconds, + null, new AsyncCallback() { + @Override + public void onSuccess(WindInfoForRaceDTO result) { + if (result != null) { + XAxis xAxis = chart.getXAxis(); + + xAxis.setMin(minTimepoint.getTime()); + xAxis.setMax(maxTimepoint.getTime()); + xAxis.setStartOnTick(false); + xAxis.setEndOnTick(false); + + updateStripChartSeries(result, append); + } else { + if (!append) { + clearChart(); // no wind known for untracked race + } + } + chart.hideLoading(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorFetchingWindInformationForRace() + " " + + raceIdentifier + ": " + caught.getMessage(), timer.getPlayMode() == PlayModes.Live); + chart.hideLoading(); + } + }); + asyncActionsExecutor.execute(getWindInfoAction); + } + } + + private void clearChart() { + chart.removeAllSeries(); + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + if (selectedRaces != null && !selectedRaces.isEmpty()) { + // show wind of first selected race + selectedRaceIdentifier = selectedRaces.iterator().next(); + clearCacheAndReload(); + } else { + clearChart(); + } + } + + @Override + public void raceTimesInfosReceived(Map raceTimesInfos) { + this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaceIdentifier); + + Pair raceMinMax = RaceTimesCalculationUtil.caluclateRaceMinMax(timer, this.lastRaceTimesInfo); + + this.minTimepoint = raceMinMax.getA(); + this.maxTimepoint = raceMinMax.getB(); + } + + /** + * If in live mode, fetches what's missing since the last fix and date. If nothing has been loaded yet, + * loads from the beginning up to date. If in replay mode, checks if anything has been loaded at all. If not, + * everything for the currently selected race is loaded; otherwise, no-op. + */ + @Override + public void timeChanged(Date date) { + if (timer.getPlayMode() == PlayModes.Live) { + // is date before first cache entry or is cache empty? + if (timeOfEarliestRequestInMillis == null || timeOfEarliestRequestInMillis > date.getTime()) { + loadData(selectedRaceIdentifier, minTimepoint, date, /* append */ true); + } else if (timeOfLatestRequestInMillis < date.getTime()) { + loadData(selectedRaceIdentifier, new Date(timeOfLatestRequestInMillis), maxTimepoint, /* append */true); + } + // otherwise the cache spans across date and so we don't need to load anything + } else { + // assuming play mode is replay / non-live + if (timeOfLatestRequestInMillis == null) { + loadData(selectedRaceIdentifier, minTimepoint, maxTimepoint, /* append */false); // replace old series + } + } + } + + @Override + public void onResize() { + if(isVisible()) { + chart.setSizeToMatchContainer(); + } + } + + private boolean needsDataLoading() { + return isVisible(); + } + + public Date getMinSelectedTimepoint() { + return minSelectedTimepoint; + } + + public Date getMaxSelectedTimepoint() { + return maxSelectedTimepoint; + } + + public boolean isZoomed() { + return isZoomed; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettings.java old mode 100644 new mode 100755 index b2eda005d0f..54d5c1de91a --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettings.java @@ -1,45 +1,45 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import java.util.Set; - -import com.sap.sailing.domain.common.WindSourceType; - -public class WindChartSettings { - private final Set windSourceTypesToDisplay; - - private final long resolutionInMilliseconds; - - private final boolean showWindSpeedSeries; - private final boolean showWindDirectionsSeries; - - public WindChartSettings(boolean showWindSpeedSeries, boolean showWindDirectionsSeries, Set windSourceTypesToDisplay, long resolutionInMilliseconds) { - this.windSourceTypesToDisplay = windSourceTypesToDisplay; - this.resolutionInMilliseconds = resolutionInMilliseconds; - this.showWindDirectionsSeries = showWindDirectionsSeries; - this.showWindSpeedSeries = showWindSpeedSeries; - } - - /** - * Uses {@link WindChart#DEFAULT_RESOLUTION_IN_MILLISECONDS} as resolution - */ - public WindChartSettings(boolean showWindSpeedSeries, boolean showWindDirectionsSeries, Set windSourceTypesToDisplay) { - this(showWindSpeedSeries, showWindDirectionsSeries, windSourceTypesToDisplay, WindChart.DEFAULT_RESOLUTION_IN_MILLISECONDS); - } - - public Set getWindSourceTypesToDisplay() { - return windSourceTypesToDisplay; - } - - public long getResolutionInMilliseconds() { - return resolutionInMilliseconds; - } - - public boolean isShowWindSpeedSeries() { - return showWindSpeedSeries; - } - - public boolean isShowWindDirectionsSeries() { - return showWindDirectionsSeries; - } - -} +package com.sap.sailing.gwt.ui.shared.charts; + +import java.util.Set; + +import com.sap.sailing.domain.common.WindSourceType; + +public class WindChartSettings { + private final Set windSourceTypesToDisplay; + + private final long resolutionInMilliseconds; + + private final boolean showWindSpeedSeries; + private final boolean showWindDirectionsSeries; + + public WindChartSettings(boolean showWindSpeedSeries, boolean showWindDirectionsSeries, Set windSourceTypesToDisplay, long resolutionInMilliseconds) { + this.windSourceTypesToDisplay = windSourceTypesToDisplay; + this.resolutionInMilliseconds = resolutionInMilliseconds; + this.showWindDirectionsSeries = showWindDirectionsSeries; + this.showWindSpeedSeries = showWindSpeedSeries; + } + + /** + * Uses {@link WindChart#DEFAULT_RESOLUTION_IN_MILLISECONDS} as resolution + */ + public WindChartSettings(boolean showWindSpeedSeries, boolean showWindDirectionsSeries, Set windSourceTypesToDisplay) { + this(showWindSpeedSeries, showWindDirectionsSeries, windSourceTypesToDisplay, WindChart.DEFAULT_RESOLUTION_IN_MILLISECONDS); + } + + public Set getWindSourceTypesToDisplay() { + return windSourceTypesToDisplay; + } + + public long getResolutionInMilliseconds() { + return resolutionInMilliseconds; + } + + public boolean isShowWindSpeedSeries() { + return showWindSpeedSeries; + } + + public boolean isShowWindDirectionsSeries() { + return showWindDirectionsSeries; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettingsDialogComponent.java old mode 100644 new mode 100755 index 0c0c7531866..18eb5f298ba --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/WindChartSettingsDialogComponent.java @@ -1,98 +1,98 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import com.google.gwt.user.client.ui.CheckBox; -import com.google.gwt.user.client.ui.FocusWidget; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.IntegerBox; -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.WindSourceType; -import com.sap.sailing.gwt.ui.client.DataEntryDialog; -import com.sap.sailing.gwt.ui.client.DataEntryDialog.Validator; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; -import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; - -public class WindChartSettingsDialogComponent implements SettingsDialogComponent { - private final WindChartSettings initialSettings; - private IntegerBox resolutionInSecondsBox; - private final Map checkboxes; - private CheckBox showWindSpeedSeriesCheckbox; - private CheckBox showWindDirectionsSeriesCheckbox; - - private final StringMessages stringMessages; - - public WindChartSettingsDialogComponent(WindChartSettings initialSettings, StringMessages stringMessages) { - super(); - this.stringMessages = stringMessages; - this.initialSettings = initialSettings; - checkboxes = new LinkedHashMap(); - } - - @Override - public Widget getAdditionalWidget(DataEntryDialog dialog) { - VerticalPanel vp = new VerticalPanel(); - resolutionInSecondsBox = dialog.createIntegerBox((int) (initialSettings.getResolutionInMilliseconds()/1000), /* visibleLength */ 5); - - HorizontalPanel hp = new HorizontalPanel(); - hp.add(new Label(stringMessages.stepSizeInSeconds())); - hp.add(resolutionInSecondsBox); - vp.add(hp); - - showWindSpeedSeriesCheckbox = dialog.createCheckbox(stringMessages.showWindSpeedSeries()); - showWindSpeedSeriesCheckbox.setValue(initialSettings.isShowWindSpeedSeries()); - vp.add(showWindSpeedSeriesCheckbox); - - showWindDirectionsSeriesCheckbox = dialog.createCheckbox(stringMessages.showWindDirectionSeries()); - showWindDirectionsSeriesCheckbox.setValue(initialSettings.isShowWindDirectionsSeries()); - vp.add(showWindDirectionsSeriesCheckbox); - - Label createHeadlineLabel = dialog.createHeadlineLabel(stringMessages.windSourcesUsed()); - vp.add(createHeadlineLabel); - for (WindSourceType windSourceType : WindSourceType.values()) { - CheckBox checkbox = dialog.createCheckbox(WindSourceTypeFormatter.format(windSourceType, stringMessages)); - checkboxes.put(windSourceType, checkbox); - checkbox.setValue(initialSettings.getWindSourceTypesToDisplay().contains(windSourceType)); - vp.add(checkbox); - } - return vp; - } - - @Override - public WindChartSettings getResult() { - Set windSourceTypesToDisplay = new HashSet(); - for (Map.Entry e : checkboxes.entrySet()) { - if (e.getValue().getValue()) { - windSourceTypesToDisplay.add(e.getKey()); - } - } - return new WindChartSettings(showWindSpeedSeriesCheckbox.getValue(), showWindDirectionsSeriesCheckbox.getValue(), - windSourceTypesToDisplay, resolutionInSecondsBox.getValue() == null ? -1 : resolutionInSecondsBox.getValue()*1000); - } - - @Override - public Validator getValidator() { - return new Validator() { - @Override - public String getErrorMessage(WindChartSettings valueToValidate) { - String errorMessage = null; - if (valueToValidate.getResolutionInMilliseconds() < 1) { - errorMessage = stringMessages.stepSizeMustBeGreaterThanNull(); - } - return errorMessage; - } - }; - } - - @Override - public FocusWidget getFocusWidget() { - return resolutionInSecondsBox; - } - -} +package com.sap.sailing.gwt.ui.shared.charts; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.IntegerBox; +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.WindSourceType; +import com.sap.sailing.gwt.ui.client.DataEntryDialog; +import com.sap.sailing.gwt.ui.client.DataEntryDialog.Validator; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; +import com.sap.sailing.gwt.ui.shared.components.SettingsDialogComponent; + +public class WindChartSettingsDialogComponent implements SettingsDialogComponent { + private final WindChartSettings initialSettings; + private IntegerBox resolutionInSecondsBox; + private final Map checkboxes; + private CheckBox showWindSpeedSeriesCheckbox; + private CheckBox showWindDirectionsSeriesCheckbox; + + private final StringMessages stringMessages; + + public WindChartSettingsDialogComponent(WindChartSettings initialSettings, StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + this.initialSettings = initialSettings; + checkboxes = new LinkedHashMap(); + } + + @Override + public Widget getAdditionalWidget(DataEntryDialog dialog) { + VerticalPanel vp = new VerticalPanel(); + resolutionInSecondsBox = dialog.createIntegerBox((int) (initialSettings.getResolutionInMilliseconds()/1000), /* visibleLength */ 5); + + HorizontalPanel hp = new HorizontalPanel(); + hp.add(new Label(stringMessages.stepSizeInSeconds())); + hp.add(resolutionInSecondsBox); + vp.add(hp); + + showWindSpeedSeriesCheckbox = dialog.createCheckbox(stringMessages.showWindSpeedSeries()); + showWindSpeedSeriesCheckbox.setValue(initialSettings.isShowWindSpeedSeries()); + vp.add(showWindSpeedSeriesCheckbox); + + showWindDirectionsSeriesCheckbox = dialog.createCheckbox(stringMessages.showWindDirectionSeries()); + showWindDirectionsSeriesCheckbox.setValue(initialSettings.isShowWindDirectionsSeries()); + vp.add(showWindDirectionsSeriesCheckbox); + + Label createHeadlineLabel = dialog.createHeadlineLabel(stringMessages.windSourcesUsed()); + vp.add(createHeadlineLabel); + for (WindSourceType windSourceType : WindSourceType.values()) { + CheckBox checkbox = dialog.createCheckbox(WindSourceTypeFormatter.format(windSourceType, stringMessages)); + checkboxes.put(windSourceType, checkbox); + checkbox.setValue(initialSettings.getWindSourceTypesToDisplay().contains(windSourceType)); + vp.add(checkbox); + } + return vp; + } + + @Override + public WindChartSettings getResult() { + Set windSourceTypesToDisplay = new HashSet(); + for (Map.Entry e : checkboxes.entrySet()) { + if (e.getValue().getValue()) { + windSourceTypesToDisplay.add(e.getKey()); + } + } + return new WindChartSettings(showWindSpeedSeriesCheckbox.getValue(), showWindDirectionsSeriesCheckbox.getValue(), + windSourceTypesToDisplay, resolutionInSecondsBox.getValue() == null ? -1 : resolutionInSecondsBox.getValue()*1000); + } + + @Override + public Validator getValidator() { + return new Validator() { + @Override + public String getErrorMessage(WindChartSettings valueToValidate) { + String errorMessage = null; + if (valueToValidate.getResolutionInMilliseconds() < 1) { + errorMessage = stringMessages.stepSizeMustBeGreaterThanNull(); + } + return errorMessage; + } + }; + } + + @Override + public FocusWidget getFocusWidget() { + return resolutionInSecondsBox; + } + +}