Merge branch 'master' into foiling

Conflicts:
	java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties
	java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties
This commit is contained in:
Steffen Schaefer committed 2016-06-22 10:10:52 +02:00
commit 6450f3db42
129 files changed
+724 -807

No files matched your search

@@ -1,8 +1,10 @@
package com.sap.sailing.domain.ranking;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -29,6 +31,7 @@ import com.sap.sailing.domain.tracking.WindPositionMode;
import com.sap.sailing.domain.tracking.impl.AbstractRaceRankComparator;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
public abstract class AbstractRankingMetric implements RankingMetric {
private static final long serialVersionUID = -3671039530564696392L;
@@ -664,24 +667,32 @@ public abstract class AbstractRankingMetric implements RankingMetric {
if (firstAroundMark != null) {
result = firstAroundMark;
} else {
Iterable<MarkPassing> markPassingsForLegStart = getTrackedRace().getMarkPassingsInOrder(trackedLeg.getLeg().getFrom());
List<MarkPassing> copyOfMarkPassingsForLegStart = new ArrayList<>();
{ // scope the markPassingsForLegStart, so it is no longer used outside this block
final Iterable<MarkPassing> markPassingsForLegStart = getTrackedRace().getMarkPassingsInOrder(trackedLeg.getLeg().getFrom());
// See bug 3728: obtaining lock for mark passings in order for a waypoint before code potentially called from here,
// e.g., through getWindwardDistanceTraveled(...), tries to obtain the read lock for mark passings for a competitor can
// result in a deadlock. Therefore, we copy the mark passings for the leg start under the lock, then release it again
// before calling into a deep stack with getWindwardDistanceTraveled(...) which may well obtain a read lock on
// the mark passings for a competitor.
getTrackedRace().lockForRead(markPassingsForLegStart);
try {
Util.addAll(markPassingsForLegStart, copyOfMarkPassingsForLegStart);
} finally {
getTrackedRace().unlockAfterRead(markPassingsForLegStart);
}
}
Distance maxWindwardDistanceTraveled = new MeterDistance(Double.MIN_VALUE);
Competitor competitorFarthestAlong = null;
getTrackedRace().lockForRead(markPassingsForLegStart);
try {
for (MarkPassing mp : markPassingsForLegStart) {
if (mp.getTimePoint().after(timePoint)) {
break;
}
final Distance windwardDistanceTraveled = getWindwardDistanceTraveled(mp.getCompetitor(), mp.getWaypoint(),
timePoint, cache);
if (windwardDistanceTraveled.compareTo(maxWindwardDistanceTraveled) > 0) {
maxWindwardDistanceTraveled = windwardDistanceTraveled;
competitorFarthestAlong = mp.getCompetitor();
}
for (MarkPassing mp : copyOfMarkPassingsForLegStart) {
if (mp.getTimePoint().after(timePoint)) {
break;
}
final Distance windwardDistanceTraveled = getWindwardDistanceTraveled(mp.getCompetitor(), mp.getWaypoint(), timePoint, cache);
if (windwardDistanceTraveled.compareTo(maxWindwardDistanceTraveled) > 0) {
maxWindwardDistanceTraveled = windwardDistanceTraveled;
competitorFarthestAlong = mp.getCompetitor();
}
} finally {
getTrackedRace().unlockAfterRead(markPassingsForLegStart);
}
result = competitorFarthestAlong;
}
@@ -416,6 +416,10 @@ public interface TrackedRace extends Serializable, IsManagedByCache<SharedDomain
* first obtains the mark passings read lock and later, while holding on to that lock, asks for the course's read
* lock, a deadlock may result.<p>
*
* Furthermore, when trying to acquire both, a lock for the {@link #getMarkPassings(Competitor) mark passings for a competitor}
* and a lock for the {@link #getMarkPassingsInOrder(Waypoint) mark passings for a waypoint, this needs to happen in exactly
* this order, or a deadlock may result.<p>
*
* The {@link #unlockAfterRead(Iterable)} method will symmetrically unlock the course's read lock after releasing the
* read lock for the mark passings.
*/
@@ -451,16 +451,24 @@ public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor {
if (leaderLeg == null || leg != leaderLeg.getLeg()) {
// add distance to next mark
Position nextMarkPosition = getTrackedRace().getApproximatePosition(leg.getTo(), timePoint);
Distance distanceToNextMark = getTrackedRace().getTrackedLeg(leg)
.getAbsoluteWindwardDistance(currentPosition, nextMarkPosition, timePoint, windPositionMode, cache);
result = new MeterDistance(result.getMeters() + distanceToNextMark.getMeters());
if (nextMarkPosition == null) {
result = null;
break;
} else {
Distance distanceToNextMark = getTrackedRace().getTrackedLeg(leg)
.getAbsoluteWindwardDistance(currentPosition, nextMarkPosition, timePoint, windPositionMode, cache);
result = new MeterDistance(result.getMeters() + distanceToNextMark.getMeters());
}
currentPosition = nextMarkPosition;
} else {
// we're now in the same leg with leader; compute windward distance to leader
result = new MeterDistance(result.getMeters()
+ getTrackedRace().getTrackedLeg(leg)
.getAbsoluteWindwardDistance(currentPosition, leaderPosition, timePoint, windPositionMode, cache)
.getMeters());
final Distance absoluteWindwardDistance = getTrackedRace().getTrackedLeg(leg)
.getAbsoluteWindwardDistance(currentPosition, leaderPosition, timePoint, windPositionMode, cache);
if (absoluteWindwardDistance != null) {
result = new MeterDistance(result.getMeters() + absoluteWindwardDistance.getMeters());
} else {
result = null;
}
break;
}
}
@@ -38,11 +38,11 @@ import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO;
import com.sap.sse.common.settings.Settings;
import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback;
import com.sap.sse.gwt.client.event.LocaleChangeEvent;
import com.sap.sse.gwt.client.sapheader.SAPHeader;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeLifecycleTabbedSettingsDialog;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveLifecycle;
import com.sap.sse.gwt.client.shared.perspective.PerspectiveLifecycleWithAllSettings;
import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication;
public class DesktopStartView extends Composite implements StartView {
private static StartPageViewUiBinder uiBinder = GWT.create(StartPageViewUiBinder.class);
@@ -50,7 +50,7 @@ public class DesktopStartView extends Composite implements StartView {
interface StartPageViewUiBinder extends UiBinder<Widget, DesktopStartView> {
}
@UiField(provided=true) SAPHeaderWithAuthentication sapHeader;
@UiField(provided=true) SAPHeader sapHeader;
@UiField(provided=true) ListBox localeSelectionBox;
@UiField(provided=true) ListBox eventSelectionBox;
@UiField(provided=true) ListBox leaderboardSelectionBox;
@@ -79,7 +79,9 @@ public class DesktopStartView extends Composite implements StartView {
this.eventBus = eventBus;
this.events = new ArrayList<EventDTO>();
sapHeader = new SAPHeaderWithAuthentication(StringMessages.INSTANCE.sapSailingAnalytics(), StringMessages.INSTANCE.autoplayConfiguration());
// sapHeader = new SAPHeaderWithAuthentication(StringMessages.INSTANCE.sapSailingAnalytics(), StringMessages.INSTANCE.autoplayConfiguration());
sapHeader = new SAPHeader(StringMessages.INSTANCE.sapSailingAnalytics());
sapHeader.setHeaderTitle(StringMessages.INSTANCE.autoplayConfiguration());
eventSelectionBox = new ListBox();
eventSelectionBox.setMultipleSelect(false);
leaderboardSelectionBox = new ListBox();
@@ -1,6 +1,6 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:s="urn:import:com.sap.sse.security.ui.authentication.generic.sapheader"
xmlns:s="urn:import:com.sap.sse.gwt.client.sapheader"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:with field="res" type="com.sap.sailing.gwt.common.client.SharedResources" />
<ui:with field="i18n" type="com.sap.sailing.gwt.ui.client.StringMessages" />
@@ -19,7 +19,7 @@
}
</ui:style>
<g:HTMLPanel>
<s:SAPHeaderWithAuthentication ui:field="sapHeader" />
<s:SAPHeader ui:field="sapHeader" />
<div style="height:1em;" />
<div class="{res.mediaCss.grid} {style.row}">
<div class="{res.mediaCss.small12} {res.mediaCss.medium6} {res.mediaCss.large3} {res.mediaCss.columns}">
@@ -7,12 +7,21 @@ import java.util.List;
import com.google.gwt.i18n.client.LocaleInfo;
import com.sap.sse.common.Util;
/**
* Utilities for working with GWT client locales.
*/
public final class GWTLocaleUtil {
private static final String NO_LOCALE_TEXT = "---";
private GWTLocaleUtil() {
}
/**
* Returns all available client locales. As GWT works with a placeholder locale "default", this ensures that only
* real locales are returned.
*
* @return all available client locales
*/
public static Iterable<String> getAvailableLocales() {
List<String> result = new ArrayList<>();
result.addAll(Arrays.asList(LocaleInfo.getAvailableLocaleNames()));
@@ -20,6 +29,12 @@ public final class GWTLocaleUtil {
return result;
}
/**
* Returns all available client locales plus a null entry. This is useful where you need to populate UI elements for
* all locales plus a fallback entry.
*
* @return all available client locales plus a null entry
*/
public static Iterable<String> getAvailableLocalesAndDefault() {
List<String> result = new ArrayList<>();
result.add(null);
@@ -27,10 +42,20 @@ public final class GWTLocaleUtil {
return result;
}
/**
* @return the count of available client locales (including a default entry)
*/
public static int getLanguageCountWithDefault() {
return Util.size(getAvailableLocalesAndDefault());
}
/**
* Renders a locale with it's name in the language itself plus the technical locale name.
* For the fallback entry (null) a placeholder is returned instead
*
* @param localeNameOrNull the locale's name or null for the fallback entry
* @return a decorated representation of the locale to be shown on the UI
*/
public static String getDecoratedLanguageDisplayNameWithDefaultLocaleSupport(String localeNameOrNull) {
if (localeNameOrNull == null || localeNameOrNull.isEmpty()) {
return NO_LOCALE_TEXT;
@@ -5,6 +5,16 @@ import java.util.logging.Logger;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Window.Navigator;
/**
* Helps to detect different form factors/device categories by analyzing the browser's user agent string. Based on the
* browser itself or the name of the operating system it's easy to detect mobile OSes as iOS or Android. Distinguishing
* smartphones and tablets is a little bit more complex because you need to analyze common device ID patterns for known
* tablets of various companies as Samsung or Sony.
*
* By now, it's not possible to ask the browser about the device or even the physical size of the device or browser
* window. It's also not possible to render an element with CSS size "1cm" or "1in" and read its pixel size in order to
* calculate the real size of the screen. This is caused by the fact that at least mobile browsers always fake 96 dpi.
*/
public class DeviceDetector {
private static Logger LOG = Logger.getLogger(DeviceDetector.class.getName());
@@ -13,6 +23,7 @@ public class DeviceDetector {
private static final RegExp tabletBlacklistRegExp = RegExp.compile(
"iPad|" // Apple ;-)
+ "Nexus 7|Nexus 10|Nexus 9|" // Google Tablets
+ "Ryu|" // Google Pixel C
+ "PlayBook|" // Blackberry
+ "KFAPWI|" // Kindle Fire HD
+ "FZ-B|" // Panasonic ToughPad
@@ -1,32 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.imagegallery;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
public class Image extends Composite {
private static ImageUiBinder uiBinder = GWT.create(ImageUiBinder.class);
interface ImageUiBinder extends UiBinder<Widget, Image> {
}
private final ImageDescriptor imageDescriptor;
@UiField DivElement title;
@UiField ImageElement image;
public Image(ImageDescriptor imageDescriptor) {
this.imageDescriptor = imageDescriptor;
ImageGalleryResources.INSTANCE.css().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
title.setInnerText(this.imageDescriptor.getTitle());
image.setSrc(this.imageDescriptor.getImageURL());
}
}
@@ -1,12 +0,0 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:with field="res" type="com.sap.sailing.gwt.common.client.SharedResources" />
<ui:with field="local_res" type="com.sap.sailing.gwt.common.theme.component.imagegallery.ImageGalleryResources" />
<g:HTMLPanel styleName="{local_res.css.sampleCssClass}">
<div>
<img ui:field="image" />
</div>
<div ui:field='title'></div>
</g:HTMLPanel>
</ui:UiBinder>
@@ -1,102 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.imagegallery;
import java.util.Date;
public class ImageDescriptor {
private String title;
private String subtitle;
private Date createdAtDate;
private String author;
private int widthInPx;
private int heightInPx;
private String imageURL;
private String thumbnailURL;
private int thumbnailWidthInPx;
private int thumbnailHeightInPx;
public ImageDescriptor(String imageURL) {
this.imageURL = imageURL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Date getCreatedAtDate() {
return createdAtDate;
}
public void setCreatedAtDate(Date createdAtDate) {
this.createdAtDate = createdAtDate;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getWidthInPx() {
return widthInPx;
}
public void setWidthInPx(int widthInPx) {
this.widthInPx = widthInPx;
}
public int getHeightInPx() {
return heightInPx;
}
public void setHeightInPx(int heightInPx) {
this.heightInPx = heightInPx;
}
public String getThumbnailURL() {
return thumbnailURL;
}
public void setThumbnailURL(String thumbnailURL) {
this.thumbnailURL = thumbnailURL;
}
public String getImageURL() {
return imageURL;
}
public void setImageURL(String imageURL) {
this.imageURL = imageURL;
}
public int getThumbnailWidthInPx() {
return thumbnailWidthInPx;
}
public void setThumbnailWidthInPx(int thumbnailWidthInPx) {
this.thumbnailWidthInPx = thumbnailWidthInPx;
}
public int getThumbnailHeightInPx() {
return thumbnailHeightInPx;
}
public void setThumbnailHeightInPx(int thumbnailHeightInPx) {
this.thumbnailHeightInPx = thumbnailHeightInPx;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
}
@@ -1,42 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.imagegallery;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Widget;
public class ImageGallery extends Composite {
private static ImageGalleryUiBinder uiBinder = GWT.create(ImageGalleryUiBinder.class);
interface ImageGalleryUiBinder extends UiBinder<Widget, ImageGallery> {
}
private ImageGalleryData imageGalleryData;
@UiField DivElement titleUi;
@UiField HTMLPanel imagesPanel;
public ImageGallery() {
ImageGalleryResources.INSTANCE.css().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
}
public ImageGallery(ImageGalleryData data) {
this();
setData(data);
}
public void setData(ImageGalleryData data) {
this.imageGalleryData = data;
titleUi.setInnerText(this.imageGalleryData.getName() != null ? this.imageGalleryData.getName() : "");
for(ImageDescriptor imageDesc: imageGalleryData.getImages()) {
Image image = new Image(imageDesc);
imagesPanel.add(image);
}
}
}
@@ -1,10 +0,0 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:with field="res" type="com.sap.sailing.gwt.common.client.SharedResources" />
<ui:with field="local_res" type="com.sap.sailing.gwt.common.theme.component.imagegallery.ImageGalleryResources" />
<g:HTMLPanel styleName="{local_res.css.sampleCssClass}">
<div ui:field="titleUi" />
<g:HTMLPanel ui:field='imagesPanel' />
</g:HTMLPanel>
</ui:UiBinder>
@@ -1,38 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.imagegallery;
import java.util.ArrayList;
import java.util.List;
public class ImageGalleryData {
private String name;
private List<ImageDescriptor> images;
public ImageGalleryData() {
this("", new ArrayList<ImageDescriptor>());
}
public ImageGalleryData(String name) {
this(name, new ArrayList<ImageDescriptor>());
}
public ImageGalleryData(String name, List<ImageDescriptor> images) {
this.name = name;
this.images = images;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<ImageDescriptor> getImages() {
return images;
}
public void setImages(List<ImageDescriptor> images) {
this.images = images;
}
}
@@ -1,16 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.imagegallery;
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
public interface ImageGalleryResources extends ClientBundle {
public static final ImageGalleryResources INSTANCE = GWT.create(ImageGalleryResources.class);
@Source("ImageGallery.gss")
LocalCss css();
public interface LocalCss extends CssResource {
String sampleCssClass();
}
}
@@ -1,55 +0,0 @@
/**
* Website footer styling
*/
.sitefooter {
font-size: 1rem;
background: #333;
border-top: 5px solid #fff;
color: #fff;
line-height: 5em;
width: 100%;
padding: 0 1.333333333333333em;
}
.sitefooter_copyright {
font-weight: 600;
margin-right: 2em;
}
@media (min-width: 45em) {
.sitefooter_copyright {
float: left;
}
}
.sitefooter_links {
text-align: left;
font-size: 1em;
font-weight: 600;
text-transform: uppercase;
}
.sitefooter_links_link {
display: block;
color: #fff;
text-decoration: none;
margin-right: 2em;
}
.sitefooter_links_link:last-child {
margin-right: 0;
}
@media (min-width: 45em) {
.sitefooter_links_link {
display: inline-block;
}
}
.sitefooter_language {
text-align: left;
}
@media (min-width: 45em) {
.sitefooter_language {
text-align: right;
}
}
.sitefooter_language_link {
color: #f0ab00;
display: inline-block;
cursor: pointer;
margin-left: 0.666666666666667em;
}
@@ -1,68 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.sapfooter;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.http.client.UrlBuilder;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.Location;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
public class SAPFooter extends Composite {
private static SAPFooterPanelUiBinder uiBinder = GWT.create(SAPFooterPanelUiBinder.class);
interface SAPFooterPanelUiBinder extends UiBinder<Widget, SAPFooter> {
}
@UiField Anchor changeLanguageLink;
@UiField DivElement languageSelectionDiv;
private final Map<String, String> localeAndLanguages;
public SAPFooter() {
localeAndLanguages = new HashMap<String, String>();
localeAndLanguages.put("default", "English");
localeAndLanguages.put("de", "Deutsch");
SAPFooterResources.INSTANCE.css().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
updateUI();
}
@UiHandler("changeLanguageLink")
public void changeLanguage(ClickEvent e) {
String selectedLanguage = changeLanguageLink.getText();
String selectedLocale = null;
for (Entry<String, String> entry: localeAndLanguages.entrySet()) {
if (entry.getValue().equals(selectedLanguage)) {
selectedLocale = entry.getKey();
}
}
if(selectedLocale != null) {
UrlBuilder builder = Location.createUrlBuilder().setParameter("locale", selectedLocale);
Window.Location.replace(builder.buildString());
}
}
private void updateUI() {
LocaleInfo currentLocale = LocaleInfo.getCurrentLocale();
HashMap<String, String> languagesToSelectFrom = new HashMap<String, String>(localeAndLanguages);
languagesToSelectFrom.remove(currentLocale.getLocaleName());
if(languagesToSelectFrom.size() == 1) {
changeLanguageLink.setText(languagesToSelectFrom.values().iterator().next());
}
}
}
@@ -1,47 +0,0 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:with field="i18n"
type="com.sap.sailing.gwt.common.client.i18n.TextMessages" />
<ui:with field="res"
type="com.sap.sailing.gwt.common.client.SharedResources" />
<ui:with field="local_res"
type="com.sap.sailing.gwt.common.theme.component.sapfooter.SAPFooterResources" />
<g:HTMLPanel>
<!-- Website footer -->
<footer class="{local_res.css.sitefooter}">
<div class="{res.mediaCss.grid}">
<div
class="{res.mediaCss.small12} {res.mediaCss.medium8} {res.mediaCss.columns}">
<div class="{local_res.css.sitefooter_copyright}">
<ui:text from='{i18n.footerCopyright}' />
</div>
<div class="{local_res.css.sitefooter_links}">
<a href="http://www.sap.com/about/legal/impressum.html" target="_blank"
class="{local_res.css.sitefooter_links_link}">
<ui:text from='{i18n.footerLegal}' />
</a>
<a href="http://www.sap.com/about/legal/privacy.html" target="_blank"
class="{local_res.css.sitefooter_links_link}">
<ui:text from='{i18n.footerPrivacy}' />
</a>
<a
href="mailto:sailing_analytics%40sap.com?subject=[SAP Sailing] Feedback"
target="_blank" class="{local_res.css.sitefooter_links_link}">
<ui:text from='{i18n.footerFeedback}' />
</a>
</div>
</div>
<div
class="{res.mediaCss.small12} {res.mediaCss.medium4} {res.mediaCss.columns} {local_res.css.sitefooter_language}">
<div ui:field="languageSelectionDiv">
<ui:text from='{i18n.footerLanguage}' />
:
<g:Anchor ui:field="changeLanguageLink"
styleName="{local_res.css.sitefooter_language_link}" />
</div>
</div>
</div>
</footer>
</g:HTMLPanel>
</ui:UiBinder>
@@ -1,21 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.sapfooter;
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
public interface SAPFooterResources extends ClientBundle {
public static final SAPFooterResources INSTANCE = GWT.create(SAPFooterResources.class);
@Source("SAPFooter.gss")
LocalCss css();
public interface LocalCss extends CssResource {
String sitefooter();
String sitefooter_copyright();
String sitefooter_links();
String sitefooter_links_link();
String sitefooter_language();
String sitefooter_language_link();
}
}
@@ -1,102 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.videogallery;
import java.util.Date;
public class VideoDescriptor {
private String title;
private String subtitle;
private Date createdAtDate;
private String author;
private int widthInPx;
private int heightInPx;
private String videoURL;
private String thumbnailURL;
private int thumbnailWidthInPx;
private int thumbnailHeightInPx;
public VideoDescriptor(String videoURL) {
this.videoURL = videoURL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Date getCreatedAtDate() {
return createdAtDate;
}
public void setCreatedAtDate(Date createdAtDate) {
this.createdAtDate = createdAtDate;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getWidthInPx() {
return widthInPx;
}
public void setWidthInPx(int widthInPx) {
this.widthInPx = widthInPx;
}
public int getHeightInPx() {
return heightInPx;
}
public void setHeightInPx(int heightInPx) {
this.heightInPx = heightInPx;
}
public String getThumbnailURL() {
return thumbnailURL;
}
public void setThumbnailURL(String thumbnailURL) {
this.thumbnailURL = thumbnailURL;
}
public String getVideoURL() {
return videoURL;
}
public void setVideoURL(String videoURL) {
this.videoURL = videoURL;
}
public int getThumbnailWidthInPx() {
return thumbnailWidthInPx;
}
public void setThumbnailWidthInPx(int thumbnailWidthInPx) {
this.thumbnailWidthInPx = thumbnailWidthInPx;
}
public int getThumbnailHeightInPx() {
return thumbnailHeightInPx;
}
public void setThumbnailHeightInPx(int thumbnailHeightInPx) {
this.thumbnailHeightInPx = thumbnailHeightInPx;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
}
@@ -1,36 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.videogallery;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
public class VideoGallery extends Composite {
private static VideoGalleryUiBinder uiBinder = GWT.create(VideoGalleryUiBinder.class);
interface VideoGalleryUiBinder extends UiBinder<Widget, VideoGallery> {
}
private VideoGalleryData videoGalleryData;
@UiField DivElement galleryTitle;
public VideoGallery() {
VideoGalleryResources.INSTANCE.css().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
}
public VideoGallery(VideoGalleryData data) {
this();
setData(data);
}
public void setData(VideoGalleryData data) {
this.videoGalleryData = data;
galleryTitle.setInnerText(this.videoGalleryData.getName() != null ? this.videoGalleryData.getName() : "");
}
}
@@ -1,14 +0,0 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:with field="i18n"
type="com.sap.sailing.gwt.common.client.i18n.TextMessages" />
<ui:with field="res"
type="com.sap.sailing.gwt.common.client.SharedResources" />
<ui:with field="local_res"
type="com.sap.sailing.gwt.common.theme.component.videogallery.VideoGalleryResources" />
<g:HTMLPanel styleName="{local_res.css.sampleCssClass}">
Video gallery...
<div ui:field='galleryTitle'></div>
</g:HTMLPanel>
</ui:UiBinder>
@@ -1,38 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.videogallery;
import java.util.ArrayList;
import java.util.List;
public class VideoGalleryData {
private String name;
private List<VideoDescriptor> videos;
public VideoGalleryData() {
this("", new ArrayList<VideoDescriptor>());
}
public VideoGalleryData(String name) {
this(name, new ArrayList<VideoDescriptor>());
}
public VideoGalleryData(String name, List<VideoDescriptor> videos) {
this.name = name;
this.videos = videos;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<VideoDescriptor> getVideos() {
return videos;
}
public void setVideos(List<VideoDescriptor> videos) {
this.videos = videos;
}
}
@@ -1,16 +0,0 @@
package com.sap.sailing.gwt.common.theme.component.videogallery;
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
public interface VideoGalleryResources extends ClientBundle {
public static final VideoGalleryResources INSTANCE = GWT.create(VideoGalleryResources.class);
@Source("VideoGallery.gss")
LocalCss css();
public interface LocalCss extends CssResource {
String sampleCssClass();
}
}
@@ -11,6 +11,7 @@ import com.sap.sailing.gwt.home.desktop.app.DesktopActivityManager;
import com.sap.sailing.gwt.home.desktop.app.DesktopActivityMapper;
import com.sap.sailing.gwt.home.desktop.app.DesktopClientFactory;
import com.sap.sailing.gwt.home.desktop.app.TabletAndDesktopApplicationClientFactory;
import com.sap.sailing.gwt.home.shared.SwitchingEntryPoint;
import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper;
import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants;
import com.sap.sailing.gwt.ui.client.ServerConfigurationService;
@@ -21,6 +22,10 @@ import com.sap.sse.gwt.client.mvp.AbstractMvpEntryPoint;
import com.sap.sse.gwt.resources.CommonControlsCSS;
import com.sap.sse.gwt.resources.Highcharts;
/**
* Desktop/tablet EntryPoint for the Home module. Be aware that this EntryPoint isn't directly started but instead the
* {@link SwitchingEntryPoint} decides about which version of the application will be shown.
*/
public class DesktopEntryPoint extends AbstractMvpEntryPoint<StringMessages, DesktopClientFactory> {
@@ -5,6 +5,10 @@ import com.google.gwt.activity.shared.ActivityMapper;
import com.google.web.bindery.event.shared.EventBus;
import com.sap.sailing.gwt.home.shared.app.SailingActivityManager;
/**
* Ensures that {@link DesktopResettableNavigationPathDisplay} will be correcly informed about the Activity type when
* the current activity changes.
*/
public class DesktopActivityManager extends SailingActivityManager<DesktopResettableNavigationPathDisplay> {
public DesktopActivityManager(ActivityMapper mapper, EventBus eventBus) {
@@ -1,7 +1,13 @@
package com.sap.sailing.gwt.home.desktop.app;
import com.sap.sailing.gwt.home.shared.app.NavigationPathDisplay;
import com.sap.sailing.gwt.home.shared.app.ResettableNavigationPathDisplay;
/**
* In sailing there are two kinds of activities: Ones with header and ones without. Because the header has a gray
* background, the {@link NavigationPathDisplay} needs to be set to the exact same background. Activities without header
* have white background, so we need to switch this based on the Activity type.
*/
public interface DesktopResettableNavigationPathDisplay extends ResettableNavigationPathDisplay {
void setWithHeader(boolean withHeader);
}
@@ -22,6 +22,9 @@ import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.media.GalleryImageHolder;
import com.sap.sailing.gwt.ui.client.media.VideoThumbnail;
/**
* Desktop page to show videos and images as a gallery.
*/
public class MediaPage extends Composite {
private static MediaPageUiBinder uiBinder = GWT.create(MediaPageUiBinder.class);
@@ -65,6 +68,8 @@ public class MediaPage extends Composite {
photoSectionUi.getStyle().clearDisplay();
String photoCss = null;
// To make the image gallery look good, we use different styling if there are only few (<7) images
// available. If there are more images, always 4 images are shown in a row.
switch (photosCount) {
case 1:
photoCss = res.mediaCss().medium12();
@@ -139,6 +144,12 @@ public class MediaPage extends Composite {
}
}
/**
* Shows a selected video in the big viewer.
*
* @param video the video to show in the big viewer
* @param autoplay true, if the video should play automatically, false otherwise
*/
private void putVideoOnDisplay(final SailingVideoDTO video, boolean autoplay) {
videoDisplayUi = new VideoWithLowerThird(true, autoplay);
videoDisplayUi.setVideo(video);
@@ -9,6 +9,10 @@ import com.sap.sailing.gwt.home.communication.media.SailingImageDTO;
import com.sap.sailing.gwt.home.shared.partials.fullscreen.FullscreenContainer;
import com.sap.sse.gwt.client.controls.carousel.ImageCarousel.FullscreenViewer;
/**
* Fullscreen viewer for the image gallery that shows the current image in a big view and lists all images on the bottom
* as small slider.
*/
public class SailingFullscreenViewer extends FullscreenContainer<SailingGalleryPlayer> implements
FullscreenViewer<SailingImageDTO> {
@@ -40,6 +40,12 @@ import com.sap.sailing.gwt.ui.raceboard.RaceBoardPerspectiveSettings;
import com.sap.sse.gwt.client.player.Timer;
import com.sap.sse.gwt.client.player.Timer.PlayModes;
/**
* Base Activity for all desktop event pages. This includes the event page itself for multi-regatta events as well as
* regatta/single-regatta-event pages.
*
* @param <PLACE> The concrete {@link AbstractEventPlace} subclass, this instance is bound to.
*/
public abstract class AbstractEventActivity<PLACE extends AbstractEventPlace> extends AbstractActivity implements
EventView.Presenter {
@@ -57,6 +63,12 @@ public abstract class AbstractEventActivity<PLACE extends AbstractEventPlace> ex
protected final EventViewDTO eventDTO;
/**
* @param place the place of the tab to be activated
* @param eventDTO the basic event data
* @param clientFactory the {@link EventClientFactory} to use
* @param homePlacesNavigator the {@link DesktopPlacesNavigator} to use to navigate to other places in the application
*/
public AbstractEventActivity(PLACE place, EventViewDTO eventDTO, EventClientFactory clientFactory, DesktopPlacesNavigator homePlacesNavigator) {
this.currentPlace = place;
this.eventDTO = eventDTO;
@@ -12,6 +12,11 @@ import com.sap.sailing.gwt.home.shared.app.NavigationPathDisplay;
import com.sap.sailing.gwt.home.shared.app.NavigationPathDisplay.NavigationItem;
import com.sap.sailing.gwt.ui.client.StringMessages;
/**
* Base Activity for all desktop multi-regatta-event pages.
*
* @param <PLACE> The concrete {@link AbstractMultiregattaEventPlace} subclass, this instance is bound to.
*/
public class EventMultiregattaActivity extends AbstractEventActivity<AbstractMultiregattaEventPlace> implements EventMultiregattaView.Presenter {
private EventMultiregattaView currentView = new TabletAndDesktopMultiRegattaEventView();
@@ -2,12 +2,12 @@ package com.sap.sailing.gwt.home.desktop.places.event.multiregatta.mediatab;
import com.google.gwt.place.shared.Prefix;
import com.sap.sailing.gwt.home.desktop.places.event.multiregatta.AbstractMultiregattaEventPlace;
import com.sap.sailing.gwt.home.shared.app.MobileSupport;
import com.sap.sailing.gwt.home.shared.app.HasMobileVersion;
import com.sap.sailing.gwt.home.shared.places.PlaceTokenPrefixes;
import com.sap.sailing.gwt.home.shared.places.event.AbstractEventPlace;
import com.sap.sailing.gwt.home.shared.places.event.EventContext;
public class MultiregattaMediaPlace extends AbstractMultiregattaEventPlace implements MobileSupport {
public class MultiregattaMediaPlace extends AbstractMultiregattaEventPlace implements HasMobileVersion {
public MultiregattaMediaPlace(String id) {
super(id);
}
@@ -23,9 +23,4 @@ public class MultiregattaMediaPlace extends AbstractMultiregattaEventPlace imple
return new MultiregattaMediaPlace(context);
}
}
@Override
public boolean hasMobileVersion() {
return true;
}
}
@@ -8,11 +8,11 @@ import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.sap.sailing.gwt.home.communication.event.EventReferenceDTO;
import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO;
import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO.EventType;
import com.sap.sailing.gwt.home.communication.eventview.HasRegattaMetadata;
import com.sap.sailing.gwt.home.communication.eventview.HasRegattaMetadata.RegattaState;
import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO;
import com.sap.sailing.gwt.home.communication.eventview.RegattaReferenceDTO;
import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO.EventType;
import com.sap.sailing.gwt.home.communication.eventview.HasRegattaMetadata.RegattaState;
import com.sap.sailing.gwt.home.desktop.app.DesktopPlacesNavigator;
import com.sap.sailing.gwt.home.desktop.places.event.AbstractEventActivity;
import com.sap.sailing.gwt.home.desktop.places.event.EventClientFactory;
@@ -20,8 +20,8 @@ import com.sap.sailing.gwt.home.desktop.places.event.EventView;
import com.sap.sailing.gwt.home.desktop.places.event.EventView.PlaceCallback;
import com.sap.sailing.gwt.home.desktop.places.event.regatta.overviewtab.RegattaOverviewPlace;
import com.sap.sailing.gwt.home.shared.app.NavigationPathDisplay;
import com.sap.sailing.gwt.home.shared.app.PlaceNavigation;
import com.sap.sailing.gwt.home.shared.app.NavigationPathDisplay.NavigationItem;
import com.sap.sailing.gwt.home.shared.app.PlaceNavigation;
import com.sap.sailing.gwt.home.shared.places.event.EventContext;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sse.gwt.client.async.AsyncActionsExecutor;
@@ -30,6 +30,13 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes;
import com.sap.sse.gwt.client.player.Timer.PlayStates;
import com.sap.sse.gwt.client.useragent.UserAgentDetails;
/**
* Base Activity for all desktop single-regatta-event/series-event pages as well as the pages for one regatta of a
* multi-regatta-event.
*
* @param <PLACE>
* The concrete {@link AbstractEventRegattaPlace} subclass, this instance is bound to.
*/
public class EventRegattaActivity extends AbstractEventActivity<AbstractEventRegattaPlace> implements EventRegattaView.Presenter {
private EventRegattaView currentView = new TabletAndDesktopRegattaEventView();
private final UserAgentDetails userAgent = new UserAgentDetails(Window.Navigator.getUserAgent());
@@ -2,12 +2,12 @@ package com.sap.sailing.gwt.home.desktop.places.event.regatta.mediatab;
import com.google.gwt.place.shared.Prefix;
import com.sap.sailing.gwt.home.desktop.places.event.regatta.AbstractEventRegattaPlace;
import com.sap.sailing.gwt.home.shared.app.MobileSupport;
import com.sap.sailing.gwt.home.shared.app.HasMobileVersion;
import com.sap.sailing.gwt.home.shared.places.PlaceTokenPrefixes;
import com.sap.sailing.gwt.home.shared.places.event.AbstractEventPlace;
import com.sap.sailing.gwt.home.shared.places.event.EventContext;
public class RegattaMediaPlace extends AbstractEventRegattaPlace implements MobileSupport {
public class RegattaMediaPlace extends AbstractEventRegattaPlace implements HasMobileVersion {
public RegattaMediaPlace(String id, String regattaId) {
super(id, regattaId);
}
@@ -29,9 +29,4 @@ public class RegattaMediaPlace extends AbstractEventRegattaPlace implements Mobi
return new RegattaMediaPlace(context);
}
}
@Override
public boolean hasMobileVersion() {
return true;
}
}
@@ -2,12 +2,12 @@ package com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab;
import com.google.gwt.place.shared.Prefix;
import com.sap.sailing.gwt.home.desktop.places.event.regatta.AbstractEventRegattaPlace;
import com.sap.sailing.gwt.home.shared.app.MobileSupport;
import com.sap.sailing.gwt.home.shared.app.HasMobileVersion;
import com.sap.sailing.gwt.home.shared.places.PlaceTokenPrefixes;
import com.sap.sailing.gwt.home.shared.places.event.AbstractEventPlace;
import com.sap.sailing.gwt.home.shared.places.event.EventContext;
public class RegattaRacesPlace extends AbstractEventRegattaPlace implements MobileSupport {
public class RegattaRacesPlace extends AbstractEventRegattaPlace implements HasMobileVersion {
public RegattaRacesPlace(String id, String leaderboardName) {
super(id, leaderboardName);
}
@@ -28,9 +28,4 @@ public class RegattaRacesPlace extends AbstractEventRegattaPlace implements Mobi
return new RegattaRacesPlace(context);
}
}
@Override
public boolean hasMobileVersion() {
return true;
}
}
@@ -9,6 +9,7 @@ import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.sap.sailing.gwt.common.client.SharedResources;
import com.sap.sailing.gwt.home.mobile.app.MobileActivityMapper;
import com.sap.sailing.gwt.home.mobile.app.MobileApplicationClientFactory;
import com.sap.sailing.gwt.home.shared.SwitchingEntryPoint;
import com.sap.sailing.gwt.home.shared.app.ApplicationHistoryMapper;
import com.sap.sailing.gwt.home.shared.app.ResettableNavigationPathDisplay;
import com.sap.sailing.gwt.home.shared.app.SailingActivityManager;
@@ -20,6 +21,10 @@ import com.sap.sse.gwt.client.EntryPointHelper;
import com.sap.sse.gwt.client.mvp.AbstractMvpEntryPoint;
import com.sap.sse.gwt.resources.CommonControlsCSS;
/**
* Smartphone EntryPoint for the Home module. Be aware that this EntryPoint isn't directly started but instead the
* {@link SwitchingEntryPoint} decides about which version of the application will be shown.
*/
public class MobileEntryPoint extends AbstractMvpEntryPoint<StringMessages, MobileApplicationClientFactory> {
@@ -30,6 +30,9 @@ import com.sap.sailing.gwt.home.mobile.app.MobilePlacesNavigator;
import com.sap.sailing.gwt.home.shared.SwitchingEntryPoint;
import com.sap.sse.common.Util.Pair;
/**
* Mobile page footer with several links and the ability to switch the language.
*/
public class Footer extends Composite {
private static FooterPanelUiBinder uiBinder = GWT.create(FooterPanelUiBinder.class);
@@ -26,6 +26,9 @@ import com.sap.sse.gwt.client.LinkUtil;
import com.sap.sse.security.ui.authentication.AuthenticationContextEvent;
import com.sap.sse.security.ui.authentication.AuthenticationSignOutRequestEvent;
/**
* Mobile page header with SAP logo title and a dropdown menu (burger button) on the right.
*/
public class Header extends Composite {
// @UiField TextBox searchText;
@@ -11,6 +11,9 @@ import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Widget;
/**
* Represents an entry in the header's popup menu.
*/
public class HeaderNavigationItem extends Widget implements HasClickHandlers {
@UiField
@@ -24,8 +24,8 @@ import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException;
/**
* This is a convenience class, which e.g. provides static methods to iterate over leaderboards or races of an entire
* event or the races of a single regatta. These methods provide a {@link LeaderboardContext} or {@link RaceContext}
* instance to the given {@link LeaderboardCallback} or {@link RaceCallback}, respectively.
* event or the races of a single regatta while keeping their orders. These methods provide a {@link LeaderboardContext}
* or {@link RaceContext} instance to the given {@link LeaderboardCallback} or {@link RaceCallback}, respectively.
*
* Also methods to calculate the {@link Duration time to live} based on the event state(non-live) are provided, which
* are used to define the refresh interval in a {@link ResultWithTTL} for different UI components.
@@ -6,6 +6,10 @@ import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
import com.sap.sse.gwt.dispatch.shared.commands.ResultWithTTL;
import com.sap.sse.gwt.dispatch.shared.commands.SortedSetResult;
/**
* {@link RaceCallback} implementation, which collects the set of {@link RaceContext#getLiveRaceOrNull() live races}
* and calculates a overall "time to live" for them using a {@link RaceRefreshCalculator}.
*/
@GwtIncompatible
public final class LiveRaceCalculator implements RaceCallback {
private final RaceRefreshCalculator refreshCalculator = new RaceRefreshCalculator();
@@ -17,6 +21,9 @@ public final class LiveRaceCalculator implements RaceCallback {
refreshCalculator.doForRace(rc);
}
/**
* @return the {@link ResultWithTTL} containing the live races and their calculated overall "time to live".
*/
public ResultWithTTL<SortedSetResult<LiveRaceDTO>> getResult() {
return new ResultWithTTL<>(refreshCalculator.getTTL(), result);
}
@@ -3,6 +3,11 @@ package com.sap.sailing.gwt.home.server;
import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO.RaceViewState;
import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
/**
* {@link RaceCallback} implementation, which calculates flags representing to regatta state based on the
* {@link RaceContext#getLiveRaceViewState() state} of the {@link RaceContext races} passed in the
* {@link #doForRace(RaceContext)} method. The aggregated values can be accessed by their respective getter method.
*/
public class OverallRacesStateCalculator implements RaceCallback {
private boolean hasLiveRace = false;
private boolean hasFinishedRace = false;
@@ -18,18 +23,34 @@ public class OverallRacesStateCalculator implements RaceCallback {
hasUnfinishedRace |= raceViewState != RaceViewState.FINISHED;
}
/**
* @return <code>true</code> if at least one currently running race was passed to the
* {@link #doForRace(RaceContext)} method, <code>false</code> otherwise
*/
public boolean hasLiveRace() {
return hasLiveRace;
}
/**
* @return <code>true</code> if at least one finished race was passed to the {@link #doForRace(RaceContext)} method,
* <code>false</code> otherwise
*/
public boolean hasFinishedRace() {
return hasFinishedRace;
}
/**
* @return <code>true</code> if at least one race was passed to the {@link #doForRace(RaceContext)} method which
* wasn't finished yet, <code>false</code> otherwise
*/
public boolean hasUnfinishedRace() {
return hasUnfinishedRace;
}
/**
* @return <code>true</code> if at least one race was passed to the {@link #doForRace(RaceContext)} method which
* was abandoned or postponed, <code>false</code> otherwise
*/
public boolean hasAbandonedOrPostponedRace() {
return hasAbandonedOrPostponedRace;
}
@@ -16,6 +16,11 @@ import com.sap.sailing.gwt.home.communication.race.FleetMetadataDTO;
import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO;
import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
/**
* {@link RaceCallback} implementation, which prepares races information to be displayed in the competition format.
* Therefore, it builds a hierarchy of DTOs representing the {@link RaceCompetitionFormatSeriesDTO series},
* {@link RaceCompetitionFormatFleetDTO fleets} and {@link SimpleRaceMetadataDTO races} of a regatta.
*/
public class RaceCompetitionFormatDataCalculator implements RaceCallback {
private final Map<String, Map<FleetMetadataDTO, SeriesFleetData>> dataPerSeriesAndFleet = new LinkedHashMap<>();
@@ -39,6 +44,10 @@ public class RaceCompetitionFormatDataCalculator implements RaceCallback {
return seriesFleetData;
}
/**
* @return collection of {@link RaceCompetitionFormatSeriesDTO}s representing the regattas hierarchy, containing the
* various series, fleets and races.
*/
public Collection<RaceCompetitionFormatSeriesDTO> getResult() {
Set<RaceCompetitionFormatSeriesDTO> result = new LinkedHashSet<>();
for (Entry<String, Map<FleetMetadataDTO, SeriesFleetData>> seriesData : dataPerSeriesAndFleet.entrySet()) {
@@ -7,6 +7,10 @@ import com.google.gwt.core.shared.GwtIncompatible;
import com.sap.sailing.gwt.home.communication.event.RaceListRaceDTO;
import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
/**
* {@link RaceCallback} implementation, which collects the set of {@link RaceContext#getFinishedRaceOrNull() finished
* races} to show in the respective UI sections.
*/
@GwtIncompatible
public class RaceListDataCalculator implements RaceCallback {
private final TreeSet<RaceListRaceDTO> races = new TreeSet<>();
@@ -19,6 +23,9 @@ public class RaceListDataCalculator implements RaceCallback {
}
}
/**
* @return the collected finished races
*/
public Collection<RaceListRaceDTO> getResult() {
return races;
}
@@ -7,6 +7,12 @@ import com.sap.sse.common.Duration;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
/**
* {@link RaceCallback} implementation, which calculates a "time to live" for the given {@link RaceContext} based on its
* {@link RaceContext#getLiveRaceViewState() view state}. The TTL for this {@link RaceRefreshCalculator} instance will
* potentially be recalculated, when {@link #doForRace(RaceContext)} is called repeatedly. The final {@link #getTTL()
* TTL} can be used for as refresh interval of "Live races" sections.
*/
@GwtIncompatible
final class RaceRefreshCalculator implements RaceCallback {
private long ttl = Duration.ONE_MINUTE.times(3).asMillis();
@@ -24,6 +30,11 @@ final class RaceRefreshCalculator implements RaceCallback {
}
}
/**
* Returns the "time to live" calculated for the maybe repeated calls to {@link #doForRace(RaceContext)}.
*
* @return the calculated {@link Duration time to live}
*/
public Duration getTTL() {
return new MillisecondsDurationImpl(ttl);
}
@@ -9,7 +9,13 @@ import com.sap.sailing.gwt.home.communication.race.FleetMetadataDTO;
import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressDTO;
import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressFleetDTO;
import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressSeriesDTO;
import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
/**
* {@link RaceCallback} implementation, which prepares races information to display the {@link RegattaProgressDTO
* progress of a regatta} including its {@link RegattaProgressSeriesDTO series} and {@link RegattaProgressFleetDTO
* fleets}.
*/
public class RegattaProgressCalculator implements EventActionUtil.RaceCallback {
private Map<String, Map<String, FleetInfo>> infoPerSeriesAndFleet = new LinkedHashMap<>();
@@ -40,6 +46,9 @@ public class RegattaProgressCalculator implements EventActionUtil.RaceCallback {
return fleetInfo;
}
/**
* @return the {@link RegattaProgressDTO} instance, including the progresses for series and fleets.
*/
public RegattaProgressDTO getResult() {
RegattaProgressDTO progress = new RegattaProgressDTO();
for (Entry<String, Map<String, FleetInfo>> seriesInfo : infoPerSeriesAndFleet.entrySet()) {
@@ -1,9 +1,14 @@
package com.sap.sailing.gwt.home.server;
import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO.RaceDataInfo;
import com.sap.sailing.gwt.home.server.EventActionUtil.RaceCallback;
/**
* {@link RaceCallback} implementation, which calculates the {@link RaceDataInfo} for a regatta by aggregating the
* information of the {@link RaceContext races} passed to the {@link #doForRace(RaceContext)} method.
*/
public class RegattaRaceDataInfoCalculator implements RaceCallback {
public class RegattaRaceDataInfoCalculator implements EventActionUtil.RaceCallback {
private boolean hasGPSData, hasWindData, hasVideoData, hasAudioData;
@Override
@@ -14,7 +19,10 @@ public class RegattaRaceDataInfoCalculator implements EventActionUtil.RaceCallba
hasVideoData |= raceDataInfo.hasVideoData();
hasAudioData |= raceDataInfo.hasAudioData();
}
/**
* @return the aggregated {@link RaceDataInfo}
*/
public RaceDataInfo getRaceDataInfo() {
return new RaceDataInfo(hasGPSData, hasWindData, hasVideoData, hasAudioData);
}
@@ -30,6 +30,16 @@ import com.sap.sse.common.Util.Triple;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.gwt.dispatch.shared.commands.ResultWithTTL;
/**
* {@link LeaderboardCallback} implementation, which calculates the statistics for the regatta(s) passed to the
* {@link #doForLeaderboard(LeaderboardContext)} method. Because this calculations can be costly and time consuming,
* several information can be disabled/enabled:
* <ul>
* <li>Maximum speed (of a competitor) - by static flag {@link #CALCULATE_MAX_SPEED} within this class</li>
* <li>Total of sailed miles - by static flag {@link #CALCULATE_SAILED_MILES} within this class</li>
* <li>Information from tracked races - by environment variable <code>DISABLE_STATS</code></li>
* </ul>
*/
public class StatisticsCalculator implements LeaderboardCallback {
private static final Logger logger = Logger.getLogger(StatisticsCalculator.class.getName());
@@ -137,6 +147,9 @@ public class StatisticsCalculator implements LeaderboardCallback {
}
}
/**
* @return the {@link ResultWithTTL} containing the calculated statistics.
*/
public ResultWithTTL<EventStatisticsDTO> getResult() {
totalDistanceTraveled = totalDistanceTraveled == Distance.NULL ? null : totalDistanceTraveled;
return new ResultWithTTL<EventStatisticsDTO>(Duration.ONE_MINUTE.times(5), new EventStatisticsDTO(regattas,
@@ -29,6 +29,17 @@ import com.sap.sailing.gwt.home.shared.app.ApplicationPlaceUpdater;
import com.sap.sailing.gwt.home.shared.app.HasMobileVersion;
import com.sap.sailing.gwt.home.shared.app.MobileSupport;
/**
* Wrapper EntryPoint for the Home module that decides if the desktop or mobile page will be shown and internally starts
* the respective EntryPoint.
*
* The decision is made on the following aspects:
* <ul>
* <li>the type of device (desktop, phone, table)</li>
* <li>the place to be shown and if the place is available for one or the other form factor</li>
* <li>the user preference that is being triggered by the user clicking "desktop version" or "mobile version" links in the page footer</li>
* </ul>
*/
public class SwitchingEntryPoint implements EntryPoint {
private static Logger LOG = Logger.getLogger(SwitchingEntryPoint.class.getName());
private static final String SAPSAILING_MOBILE = "sapsailing_mobile";
@@ -21,8 +21,7 @@ import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesContext;
import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace;
/**
* Helper class to support converting old places to new ones.
*
* Helper class to support converting old places to new ones and relace inconsistent places with working ones.
*/
public class ApplicationPlaceUpdater {
@@ -2,6 +2,10 @@ package com.sap.sailing.gwt.home.shared.app;
import com.sap.sailing.gwt.home.communication.SailingDispatchSystem;
/**
* Definition of ClientFactories that provide access to the dispatch based remote framework through
* {@link SailingDispatchSystem}.
*/
public interface ClientFactoryWithDispatch {
SailingDispatchSystem getDispatch();
}
@@ -1,5 +1,9 @@
package com.sap.sailing.gwt.home.shared.app;
/**
* Allows places to define a custom page title. This title is shown as page title on desktop and in the header on
* mobile.
*/
public interface HasLocationTitle {
String getLocationTitle();
}
@@ -1,5 +1,9 @@
package com.sap.sailing.gwt.home.shared.app;
/**
* Marker for Places that have a mobile version. This is used to decide which page version has to be shown for a
* specific place.
*/
public interface HasMobileVersion {
}
@@ -1,5 +1,10 @@
package com.sap.sailing.gwt.home.shared.app;
/**
* Marker for Places that have a mobile version depending on some place state. This is commonly uses in combination with
* {@link com.sap.sailing.gwt.home.shared.ExperimentalFeatures} for new mobile places that should be able to be
* deactivated during active development.
*/
public interface MobileSupport {
boolean hasMobileVersion();
@@ -1,5 +1,18 @@
package com.sap.sailing.gwt.home.shared.app;
/**
* Abstraction of a hierarchy regarding navigation. This concept is introduced to allow activities to define its
* navigation path/hierarchy using a UI element that is not part of the activity itself. This allows the central
* definition of the navigation UI without losing the ability to control the concrete navigation path for an activity in
* a fine grained way.
*
* {@link NavigationPathDisplay} uses a async mechanism to let activities set their concrete navigation path because
* some parts of the navigation path can rely on dynamic information (e.g. event/series names) that isn't available on
* construction of the activity.
*
* The abtraction also allows different implementations/visualizations. So it's possible to render breadcrumbs on
* desktop, while on mobile devices a popup menu is used.
*/
public interface NavigationPathDisplay {
void showNavigationPath(NavigationItem... navigationPath);
@@ -1,5 +1,8 @@
package com.sap.sailing.gwt.home.shared.app;
/**
* {@link NavigationPathDisplay} that can be reset to clear the currently shown navigation path.
*/
public interface ResettableNavigationPathDisplay extends NavigationPathDisplay {
void reset();
@@ -5,6 +5,13 @@ import com.google.gwt.activity.shared.ActivityMapper;
import com.google.web.bindery.event.shared.EventBus;
import com.sap.sse.gwt.client.mvp.CustomActivityManager;
/**
* Specific {@link CustomActivityManager} that controls a {@link ResettableNavigationPathDisplay} by resetting the
* navigation path before activation of an activity. With that, there is no wrong path in the navigation path if an
* acitivity defines a navigation path by itself.
*
* @param <NPD> the concrete {@link ResettableNavigationPathDisplay} used by this ActivityManager.
*/
public class SailingActivityManager<NPD extends ResettableNavigationPathDisplay> extends CustomActivityManager {
private NPD navigationPathDisplay;
@@ -26,6 +26,12 @@ import com.google.gwt.user.client.ui.SimpleLayoutPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Base class for fullscreen viewer UIs. This viewer has an extensible header area with a close button and a content
* area that takes the rest of the available space.
*
* @param <T> The type of the content widget.
*/
public class FullscreenContainer<T extends Widget> {
private static FullscreenContainerUiBinder uiBinder = GWT.create(FullscreenContainerUiBinder.class);
@@ -56,9 +62,15 @@ public class FullscreenContainer<T extends Widget> {
popup.addStyleName(style.popup());
}
/**
* To be overwritten by subclasses to hook into the fullscreen viewer's lifecycle.
*/
protected void onShow() {
}
/**
* To be overwritten by subclasses to hook into the fullscreen viewer's lifecycle.
*/
protected void onClose() {
}
@@ -72,6 +84,9 @@ public class FullscreenContainer<T extends Widget> {
headerContentUi.getElement().getStyle().setMarginLeft(5, Unit.EM);
}
/**
* Should be called by subclasses if the content needs a border as optical guidance for the user.
*/
protected void showBorder() {
contentUi.addStyleName(style.contentBorder());
}
@@ -85,6 +100,11 @@ public class FullscreenContainer<T extends Widget> {
return popup.addCloseHandler(handler);
}
/**
* To be called by subclasses to add more actions (in addition to the close button) to the upper right corner of the fullscreen veiwer.
*
* @param widget the action widget to add to the toolbar in the upper right corner.
*/
public void addToolbarAction(Widget widget) {
widget.addStyleName(style.toolbarAction());
toolbarUi.add(widget);
@@ -10,6 +10,9 @@ import com.sap.sailing.gwt.ui.client.media.PlayEvent;
import com.sap.sailing.gwt.ui.client.media.VideoJSPlayer;
import com.sap.sse.gwt.client.media.VideoDTO;
/**
* Video player with custom play button.
*/
public class VideoPlayer extends Composite {
private final VideoPlayerResources.LocalCss style = VideoPlayerResources.INSTANCE.css();
private VideoJSPlayer videoJSPlayer;
@@ -4,6 +4,9 @@ import com.sap.sailing.gwt.home.communication.event.LabelType;
import com.sap.sailing.gwt.home.shared.partials.lowerthird.LowerThird;
import com.sap.sse.gwt.client.media.VideoDTO;
/**
* Video player with a lower third containing title, subtitle and an optional {@link LabelType}.
*/
public class VideoWithLowerThird extends VideoPlayer {
private final LowerThird lowerThird = new LowerThird();
@@ -2,10 +2,20 @@ package com.sap.sailing.gwt.home.shared.refresh;
import com.sap.sailing.gwt.home.communication.SailingAction;
/**
* Provides a SailingAction instance that is used by {@link RefreshManager} to load data consumed by a
* {@link RefreshableWidget}.SailingAction
*
* @param <A>
* The type of action given by the {@link ActionProvider}.
*/
public interface ActionProvider<A extends SailingAction<?>> {
A getAction();
/**
* @return true if this ActionProvider is active and a refresh may occur, false otherwise.
*/
boolean isActive();
public abstract class AbstractActionProvider<A extends SailingAction<?>> implements ActionProvider<A> {
@@ -21,6 +31,12 @@ public interface ActionProvider<A extends SailingAction<?>> {
}
}
/**
* Default implementation of {@link ActionProvider} that returns a non-changing action instance.
*
* @param <A>
* The type of action given by the {@link ActionProvider}.
*/
public class DefaultActionProvider<A extends SailingAction<?>> extends AbstractActionProvider<A> {
public DefaultActionProvider(A action) {
@@ -8,6 +8,12 @@ import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.gwt.home.communication.SailingDispatchSystem;
/**
* Specialized {@link LifecycleRefreshManager} that is bound to the lifecycle of a given {@link AcceptsOneWidget} and
* ensures that a content {@link Widget} is being attached when the first refresh is successfully finished. This enures
* that no uninitialized {@link RefreshableWidget} are initially shown. Instead you can explicitly set a placeholder
* widget to the container that is automatically replaced with the actual content when the data is available.
*/
public class AutoAttachingRefreshManager extends LifecycleRefreshManager {
private static final Logger LOG = Logger.getLogger(AutoAttachingRefreshManager.class.getName());
@@ -7,9 +7,23 @@ import com.google.gwt.event.logical.shared.AttachEvent.Handler;
import com.google.gwt.event.logical.shared.HasAttachHandlers;
import com.sap.sailing.gwt.home.communication.SailingDispatchSystem;
/**
* Specialized {@link RefreshManager} that is being bound to the lifecycle of a given widget. This is typically a
* container that holds the layout of the inner refreshable parts.
*
* This should always be used when refresh functionality is used by a UI part that can disappear sometime. This is e.g
* the case for Activities that are replaced by another Activity when a new Place fires.
*
* The implementation ensures that no memory leak is produced by the inner Timer instance by cleanly stopping all work
* when the lifecycle widget is detached from the DOM tree.
*/
public class LifecycleRefreshManager extends RefreshManager {
private final HasAttachHandlers lifecycleWidget;
/**
* @param lifecycleWidget the widget that defines the lifecycle of this {@link RefreshManager}.
* @param actionExecutor the {@link SailingDispatchSystem} to use for remote communication
*/
public LifecycleRefreshManager(HasAttachHandlers lifecycleWidget, SailingDispatchSystem actionExecutor) {
super(actionExecutor);
this.lifecycleWidget = lifecycleWidget;
@@ -24,6 +38,9 @@ public class LifecycleRefreshManager extends RefreshManager {
}
});
// if the widget is already attached, the listener above won't fire, so we trigger the first refresh directly.
// scheduleFinally is used to wait until all RefreshableWidgets are added.
// Otherwise, there would be no RefreshableWidget and the initial refresh would be skipped, so that not refresh would occur at all.
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
@@ -36,6 +53,8 @@ public class LifecycleRefreshManager extends RefreshManager {
@Override
protected boolean canExecute() {
// Refreshes can only occur if the lifecycleWidget is actually attached to the DOM tree.
// Other wise we would produce a memory consuming zombie that eats the browser's resources and unnecessarily fetches data from the server.
return lifecycleWidget.isAttached();
}
}
@@ -16,9 +16,25 @@ import com.sap.sse.common.Duration;
import com.sap.sse.gwt.dispatch.shared.commands.DTO;
import com.sap.sse.gwt.dispatch.shared.commands.ResultWithTTL;
/**
* Provides automatic refresh functionality for widgets based on the {@link SailingDispatchSystem}.
*
* A widget can be registered for automatic refresh if it implements {@link RefreshableWidget} and it's data is provided
* by a dispatch action of type {@link SailingAction}. This action needs to have it's result wrapped by a
* {@link ResultWithTTL} to control refresh intervals by the server.
*
* The {@link RefreshManager} ensures that the next refresh will occur in about the time defined by the returned
* {@link ResultWithTTL}. While doing this it tries to batch refreshes that are scheduled for a similar time point.
*
* If an error occurs while loading new data on a refresh, the next refresh is automatically scheduled in 30 seconds to
* not kill the server by immediate repetitive remote calls.
*/
public abstract class RefreshManager {
private static final Logger LOG = Logger.getLogger(RefreshManager.class.getName());
/**
* When an error occurs, the next refresh is delayed by 30 seconds to not kill the server by doing immediate repetitive calls.
*/
private static final long PAUSE_ON_ERROR = Duration.ONE_SECOND.times(30).asMillis();
private List<RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>>> refreshables = new ArrayList<>();
@@ -34,30 +50,53 @@ public abstract class RefreshManager {
boolean started = false;
/**
* @param actionExecutor the {@link SailingDispatchSystem} to use for remote communication
*/
public RefreshManager(SailingDispatchSystem actionExecutor) {
this.actionExecutor = actionExecutor;
}
/**
* Initially starts the auto-refresh. Needs to be called once by sublasses when at least one {@link RefreshableWidget} is added.
* This triggers the first refresh. Subsequent refreshes are automatically triggered until {@link #cancel()} is called.
*/
protected void start() {
LOG.log(Level.FINE, "Starting auto refresh");
started = true;
reschedule();
}
/**
* Must be called by subclasses to stop the auto-refresh. Ensures that all resources are disposed that would
* otherwise lead to memory leaks or zombie processes.
*/
protected void cancel() {
LOG.log(Level.FINE, "Cancelling auto refresh");
started = false;
timer.cancel();
}
/**
* @return true if a refresh may occur in the current state, false otherwise.
*/
protected abstract boolean canExecute();
/**
* Is called whenever a refresh was successful. Useful for subclasses that need to hook into the lifecycle of the {@link RefreshManager}.
*/
protected void onSuccessfulUpdate() {
}
/**
* Is called whenever a refresh failed. Useful for subclasses that need to hook into the lifecycle of the {@link RefreshManager}.
*/
protected void onFailedUpdate(Throwable errorCause) {
}
/**
* Triggers an update for all {@link RefreshableWidget}s that will have a timeout in the next 5 seconds.
*/
private void update() {
for (final RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>> refreshable : refreshables) {
@@ -94,6 +133,9 @@ public abstract class RefreshManager {
}
}
/**
* Ensures that the next update is scheduled by a timer.
*/
private void reschedule() {
if (scheduled) {
return;
@@ -129,6 +171,7 @@ public abstract class RefreshManager {
}
}
if(nextUpdate == null) {
// This can occur, if there is already a call running for all RefreshableWidgets
LOG.log(Level.FINE, "Nothing to auto update");
} else {
int delayMillis = (int) (nextUpdate - System.currentTimeMillis());
@@ -151,6 +194,16 @@ public abstract class RefreshManager {
reschedule();
}
/**
* Add a {@link RefreshableWidget} to the set of refreshables controlled by this {@link RefreshManager}.
* The refresh intervals are controlled by the ResultWithTTL returned from the server.
*
* On each refresh, the given {@link ActionProvider} is called to get the action to send to the server.
* This is useful if some state-dependent data needs to be sent so that the action can't be a fix instance.
*
* @param widget the {@link RefreshableWidget} to start auto refresh for
* @param provider the {@link ActionProvider} to get the {@link SailingAction} instance from when fetching new data for a refresh
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget,
ActionProvider<A> provider) {
@@ -158,6 +211,15 @@ public abstract class RefreshManager {
reschedule();
}
/**
* Add a {@link RefreshableWidget} to the set of refreshables controlled by this {@link RefreshManager}.
* The refresh intervals are controlled by the ResultWithTTL returned from the server.
*
* On each refresh, the given action is sent to the server to get new data.
*
* @param widget the {@link RefreshableWidget} to start auto refresh for
* @param action the {@link SailingAction} instance to use when fetching new data for a refresh
*/
public <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget, A action) {
add(widget, new DefaultActionProvider<>(action));
}
@@ -166,12 +228,24 @@ public abstract class RefreshManager {
return actionExecutor;
}
/**
* Internal structure for holding the state for a {@link RefreshableWidget} controlled by the {@link RefreshManager}.
*
* @param <D> The Type of the Data returned from the server (wrapped by a {@link ResultWithTTL})
* @param <A> The type of the dispatch action for fetching new data
*/
private static class RefreshHolder<D extends DTO, A extends SailingAction<ResultWithTTL<D>>> {
private final RefreshableWidget<D> widget;
private final ActionProvider<A> provider;
// initial update is now
/**
* Defines the timeout when the next refresh will occur.
* Initial update is now to trigger an immediate update.
*/
private long timeout = System.currentTimeMillis();
/**
* When there is currently a call running for refreshing this {@link RefreshableWidget}, callRunning is set to true.
*/
private boolean callRunning = false;
public RefreshHolder(RefreshableWidget<D> widget, ActionProvider<A> provider) {
@@ -6,6 +6,12 @@ import com.sap.sailing.gwt.home.communication.SailingDispatchSystem;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory;
/**
* Specialized {@link AutoAttachingRefreshManager} that initially shows a busy widget that is replayced with the actual
* content when the first refresh is finished. In the case of an error while doing the first refresh, an error widget is
* shown. When an error occurs on a further refresh, the old content simply isn't exchanged an a refresh is triggered as
* described for {@link RefreshManager}.
*/
public class RefreshManagerWithErrorAndBusy extends AutoAttachingRefreshManager {
private final AcceptsOneWidget container;
@@ -2,6 +2,13 @@ package com.sap.sailing.gwt.home.shared.refresh;
import com.sap.sse.gwt.dispatch.shared.commands.DTO;
/**
* To be implemented by widgets that are automatically managed by a {@link RefreshManager} that (re)loads the data
* needed by this widget.
*
* @param <D>
* The type of data accepted by this widget
*/
public interface RefreshableWidget<D extends DTO> {
void setData(D data);
}
@@ -685,8 +685,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component
if (isInfoBefore) {
additionalInformation.append(" / ");
}
additionalInformation.append("Finished at: "
+ (timeFormatter.format(entryDTO.raceInfo.finishedTime)));
additionalInformation.append(stringMessages.finishTime((timeFormatter.format(entryDTO.raceInfo.finishedTime))));
isInfoBefore = true;
}
return additionalInformation.toString();
@@ -678,6 +678,14 @@ TrackedRaceChangedListener, LeaderboardsDisplayer {
@Override
protected void leaderboardSelectionChanged() {
if (raceColumnTableSelectionModel.getSelectedSet() != null
&& !raceColumnTableSelectionModel.getSelectedSet().isEmpty()) {
// we can be sure that raceColumnTableSelectionModel is single-selection only because
// this class's constructor passes "false" to the super-class constructor's multi-selection
// constructor parameter
raceColumnTableSelectionModel.setSelected(raceColumnTableSelectionModel.getSelectedSet().iterator().next(), false);
}
leaderboardRemoveButton.setEnabled(!leaderboardSelectionModel.getSelectedSet().isEmpty());
StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard();
if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) {
@@ -246,7 +246,9 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
Date max = raceMinMax.getB();
// never reduce max if it was already set
if (min != null && max != null && (getToTime() == null || getToTime().before(max))) {
if (min != null && max != null &&
(getToTime() == null || getToTime().before(max) ||
getFromTime() == null || getFromTime().after(min))) {
setMinMax(min, max, /* fireEvent */ false); // no event because we guarantee time to be between min and max
}
}
@@ -1611,4 +1611,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String noRacesAvailable();
String loadingRegattasAndRaces();
String selectFromRacesWithOverlappingTimeRange();
String finishTime(String timeAsString);
String showCompetitorCharts();
String showWindChart();
}
@@ -444,7 +444,7 @@ scoringSchemeHighPointFirstGetsTen=High Point, winner gets 10 points
scoringSchemeHighPointFirstGetsTenOrEight=High Point, winner gets 10 points (or 8)
scoringSchemeHighPointFirstGetsTenOrEightDescription=Score in points. Winner scores 10 points, 2nd - 9 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored.
scoringSchemeHighPointFirstGetsTwelveOrEight=High Point, winner gets 12 points (or 8)
scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored.
scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor with the highest score, if tie remains then tie is broken by the rank in the last race scored.
scoringSystem=Scoring system
createFlexibleLeaderboard=Create flexible leaderboard
createRegattaLeaderboard=Create regatta leaderboard
@@ -1578,6 +1578,9 @@ noTracksFound=No tracks found
noRacesAvailable=No races available
loadingRegattasAndRaces=Loading Regattas and Races
selectFromRacesWithOverlappingTimeRange=Select from the races whose time range overlaps with media track those to which the track shall be linked
finishTime=Finish at: {0}
showCompetitorCharts=Show competitor chart
showWindChart=Show wind chart
currentRideHeightInMeters=Ride Height
currentRideHeightInMetersTooltip=The current Foiling ride height
currentOrAverageRideHeightInMeters=Ride Height (\u2205 at end)
@@ -1132,7 +1132,7 @@ leaderboardNotContainedInLeaderboardGroup=Ergebnisliste {0} nicht in Wettfahrtgr
leaderboardGroupNotContainedInEvent=Die Wettfahrtgruppe {0} gehört nicht zur Veranstaltung {1}.
raceIsInLiveTimePanelMode=Rennen ist live
backToLiveTimePanelMode=Zurück zu live
windChart=Wind Chart
windChart=Winddiagramm
mediaNoVideosCaption=Keine Videos
mediaShowVideoCaption=Video ein
mediaShowVideoTooltip=Video abspielen: {0}
@@ -1563,6 +1563,9 @@ noTracksFound=Keine Tracks gefunden
noRacesAvailable=Keine Rennen verfügbar
loadingRegattasAndRaces=Regatten und Rennen werden geladen
selectFromRacesWithOverlappingTimeRange=Das oder die Rennen aus der Liste derer zum Verknüpfen auswählen, die zeitlich mit dem Medien-Track überlappen
finishTime=Ende um: {0}
showCompetitorCharts=Teilnehmer-Diagramme zeigen
showWindChart=Windiagramm zeigen
currentRideHeightInMeters=Fahrhöhe
currentRideHeightInMetersTooltip=Die aktuelle Fahrhöhe (Foiling).
currentOrAverageRideHeightInMeters=Fahrhöhe (\u2205 nach Abschluss)
@@ -14,6 +14,9 @@ import com.sap.sse.common.media.MediaSubType;
import com.sap.sse.common.media.MediaType;
import com.sap.sse.common.media.MimeType;
/**
* video.js (http://videojs.com/) wrapper as GWT widget.
*/
public class VideoJSPlayer extends Widget {
private static VideoJSPlayerUiBinder uiBinder = GWT.create(VideoJSPlayerUiBinder.class);
@@ -2,5 +2,9 @@ package com.sap.sailing.gwt.ui.client.refresh;
import com.google.gwt.user.client.ui.IsWidget;
/**
* View that shows a loading indicator.
*
*/
public interface BusyView extends IsWidget {
}
@@ -2,6 +2,9 @@ package com.sap.sailing.gwt.ui.client.refresh;
import com.sap.sse.gwt.client.mvp.ErrorView;
/**
* ClientFactory definition to create error and busy (loading indicator) views.
*/
public interface ErrorAndBusyClientFactory {
BusyView createBusyView();
@@ -36,6 +36,16 @@ public class DateAndTimeFormatterUtil {
return defaultDateFormatter.render(startDate) + " - " + defaultDateFormatter.render(endDate);
}
/**
* Formats a duration in a compact format so that hours and minutes are only shown if the duration is >1h to avoid
* unnecessary 00:yy or 00:xx:yy values.
*
* Be aware that this method doesn't work for durations >= 24h.
*
* @param timeInMilliseconds
* the duration in milliseconds to format
* @return the formatted duration
*/
public static String formatElapsedTime(long timeInMilliseconds) {
String result = "";
int seconds = (int) (timeInMilliseconds / 1000) % 60 ;
@@ -26,15 +26,15 @@ public class RaceBoardPerspectiveSettingsDialogComponent implements SettingsDial
public Widget getAdditionalWidget(DataEntryDialog<?> dialog) {
VerticalPanel vp = new VerticalPanel();
showLeaderboardCheckBox = dialog.createCheckbox(stringMessages.show() + " " + stringMessages.leaderboard());
showLeaderboardCheckBox = dialog.createCheckbox(stringMessages.showLeaderboard());
showLeaderboardCheckBox.setValue(initialSettings.isShowLeaderboard());
vp.add(showLeaderboardCheckBox);
showWindChartCheckBox = dialog.createCheckbox(stringMessages.show() + " " + stringMessages.windChart());
showWindChartCheckBox = dialog.createCheckbox(stringMessages.showWindChart());
showWindChartCheckBox.setValue(initialSettings.isShowWindChart());
vp.add(showWindChartCheckBox);
showCompetitorsChartCheckBox = dialog.createCheckbox(stringMessages.show() + " " + stringMessages.competitorCharts());
showCompetitorsChartCheckBox = dialog.createCheckbox(stringMessages.showCompetitorCharts());
showCompetitorsChartCheckBox.setValue(initialSettings.isShowCompetitorsChart());
vp.add(showCompetitorsChartCheckBox);
@@ -40,6 +40,7 @@ public class ConfigurationJsonGetServlet extends AbstractJsonHttpServlet {
if (configuration != null) {
JsonSerializer<DeviceConfiguration> serializer = DeviceConfigurationJsonSerializer.create();
JSONObject json = serializer.serialize(configuration);
response.setCharacterEncoding("UTF-8");
json.writeJSONString(response.getWriter());
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "No configuration for given identifier.");
@@ -22,6 +22,20 @@ public class DefaultWindTrackJsonSerializer implements WindTrackJsonSerializer {
private TimePoint toTime;
private WindSource windSource;
/**
* -1 means unlimited.
*/
private final int maxNumberOfFixes;
public DefaultWindTrackJsonSerializer(int maxNumberOfFixes) {
super();
this.maxNumberOfFixes = maxNumberOfFixes;
}
public DefaultWindTrackJsonSerializer() {
this(-1);
}
public JSONObject serialize(WindTrack windTrack) {
JSONObject result = new JSONObject();
JSONArray jsonWindFixes = new JSONArray();
@@ -31,7 +45,8 @@ public class DefaultWindTrackJsonSerializer implements WindTrackJsonSerializer {
windTrack.lockForRead();
try {
Iterator<Wind> windIter = windTrack.getFixesIterator(fromTime, /* inclusive */true, toTime, /* inclusive */ false);
while (windIter.hasNext()) {
int count = 0;
while ((maxNumberOfFixes == -1 || count<maxNumberOfFixes) && windIter.hasNext()) {
fixes.add(windIter.next());
}
} finally {
@@ -687,8 +687,12 @@ public class RegattasResource extends AbstractSailingServerResource {
Regatta regatta = findRegattaByName(regattaName);
if (regatta == null) {
response = Response.status(Status.NOT_FOUND)
.entity("Could not find a regatta with name '" + StringEscapeUtils.escapeHtml(regattaName) + "'.").type(MediaType.TEXT_PLAIN)
.build();
.entity("Could not find a regatta with name '" + StringEscapeUtils.escapeHtml(regattaName) + "'.").type(MediaType.TEXT_PLAIN)
.build();
} else if (!((fromtime != null && totime != null) || (fromtimeasmillis != null && totimeasmillis != null))) {
response = Response.status(Status.NOT_FOUND)
.entity("Either the 'fromtime' and 'totime' or the 'fromtimeasmillis' and 'totimeasmillis' parameter must be set.").type(MediaType.TEXT_PLAIN)
.build();
} else {
RaceDefinition race = findRaceByName(regatta, raceName);
if (race == null) {
@@ -719,7 +723,7 @@ public class RegattasResource extends AbstractSailingServerResource {
from = Util.getLatestOfTimePoints(from, trackedRace.getStartOfTracking());
to = Util.getEarliestOfTimePoints(to, Util.getEarliestOfTimePoints(trackedRace.getEndOfTracking(), trackedRace.getTimePointOfNewestEvent()));
TrackedRaceJsonSerializer serializer = new TrackedRaceJsonSerializer(
new DefaultWindTrackJsonSerializer());
new DefaultWindTrackJsonSerializer(/* maxNumberOfFixes */ 10000));
serializer.setWindSource(windSource);
serializer.setWindSourceId(windSourceId);
serializer.setFromTime(from);
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/competitors/{id}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/competitors/{id}/team</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/competitors/{id}/team/image</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/regattas/{name}/entries</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/events/{eventId}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/events/{eventId}/racestates</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/events</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/gps_fixes</h2>
<b>Description:</b>
@@ -74,7 +74,11 @@ name to make sure you get to the event you're interested in.
</tr>
<tr>
<td><a href="raceCompetitorPositionsGetDoc.html">/api/v1/regattas/{regattaname}/races/{racename}/competitors/positions</a></td>
<td>Gets all GPS positions of the competitors of a race</td>
<td>Gets the GPS positions of the competitors of a race</td>
</tr>
<tr>
<td><a href="raceCompetitorLiveGetDoc.html">/api/v1/regattas/{regattaname}/races/{racename}/competitors/live</a></td>
<td>Gets some competitor information (e.g current rank, current leg) for a live race</td>
</tr>
<tr>
<td><a href="raceCompetitorLegsGetDoc.html">/api/v1/regattas/{regattaname}/races/{racename}/competitors/legs</a></td>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/device_mappings/start</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/device_mappings/end</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/competitors/{competitorId}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/marks/{id}/gps_fixes</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/racecolumnfactors?race_column={racecolumnname}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/racecolumnfactors?race_column={racecolumnname}&explicit_factor=2.0</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboards/{name}/marks?race_column={name}&amp;fleet={name}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboardgroups/{name}</h2>
<b>Description:</b>
@@ -5,10 +5,10 @@
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0 Beta</title>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0 Beta</h1>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboardgroups</h2>
<b>Description:</b>
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more