Skip to content

Commit e60db6f

Browse files
committed
Merge remote-tracking branch 'eclipse/main' into eclipse-main
2 parents f5041c0 + 7304bae commit e60db6f

11 files changed

Lines changed: 240 additions & 51 deletions

File tree

java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/confidence/impl/ScalableWind.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
* Wind values are scaled by separately scaling their speed and bearing, and separately scaling their time point, and
1414
* separately scaling their position. For the separate speed/bearing scaling see also {@link ScalableSpeedWithBearing}.<p>
1515
*
16-
* Internally, the
17-
*
1816
* @author Axel Uhl (d043530)
1917
*
2018
*/

java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ <h5 class="articleSubheadline">August 2026</h5>
1414
year's episode of that event.</li>
1515
<li>Show one decimal place in wind chart tool-tips, so, e.g., a wind speed series tool-tip
1616
may read "16.6kts" now.</li>
17+
<li>New API method on end point /sailingserver/api/v1/regattas/{regattaname}/races/{racename}/wind_at_position
18+
allows clients with the <tt>TRACKED_RACE:EXPORT</tt> permission for the race in question
19+
to obtain position-based interpolated wind data taking all non-excluded wind sources available
20+
for that race into account, computing a weighted average that matches what is being used for
21+
competitor-specific VMG and TWA calculations alike.</li>
1722
</ul>
1823
<h5 class="articleSubheadline">July 2026</h5>
1924
<ul class="bulletList">

java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ Iterable<DetailType> determineDetailTypesForCompetitorChart(String leaderboardGr
443443
* is thrown if the leaderboard is not found by name
444444
*/
445445
PairingListTemplateDTO calculatePairingListTemplate(final int flightCount, final int groupCount,
446-
final int competitorCount, final int flightMultiplier, final int tolerance)
446+
final int competitorCount, final int flightMultiplier, final int boatChangeFactor)
447447
throws UnauthorizedException, NotFoundException, IllegalArgumentException;
448448

449449
PairingListDTO getPairingListFromTemplate(String leaderboardName, int flightMultiplier,

java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Colorline.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,19 @@ public void setPath(final MVCArray<LatLng> path) {
171171
polylines.add(createPolyline(path, 0));
172172
break;
173173
case POLYCHROMATIC:
174-
for (int i = 0; i < path.getLength() - 1; i++) {
175-
MVCArray<LatLng> subPath = MVCArray.newInstance();
176-
subPath.push(path.get(i));
177-
subPath.push(path.get(i + 1));
178-
polylines.add(createPolyline(subPath, i));
174+
if (path.getLength() == 1) {
175+
// A single vertex cannot form a segment; represent it as one Polyline holding just that vertex,
176+
// mirroring the interim single-vertex representation insertAt builds up and getLength's case 1 expects
177+
final MVCArray<LatLng> subPath = MVCArray.newInstance();
178+
subPath.push(path.get(/* i */ 0));
179+
polylines.add(createPolyline(subPath, /* fixIndexInTail */ 0));
180+
} else {
181+
for (int i = 0; i < path.getLength() - 1; i++) {
182+
final MVCArray<LatLng> subPath = MVCArray.newInstance();
183+
subPath.push(path.get(i));
184+
subPath.push(path.get(i + 1));
185+
polylines.add(createPolyline(subPath, i));
186+
}
179187
}
180188
break;
181189
}

java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5050,10 +5050,10 @@ public List<ExpeditionDeviceConfiguration> getExpeditionDeviceConfigurations() {
50505050

50515051
@Override
50525052
public PairingListTemplateDTO calculatePairingListTemplate(final int flightCount, final int groupCount,
5053-
final int competitorCount, final int flightMultiplier, final int tolerance) {
5053+
final int competitorCount, final int flightMultiplier, final int boatChangeFactor) {
50545054
PairingListTemplate template = getService().createPairingListTemplate(flightCount, groupCount, competitorCount,
5055-
flightMultiplier, tolerance);
5056-
return new PairingListTemplateDTO(flightCount, groupCount, competitorCount, flightMultiplier, tolerance,
5055+
flightMultiplier, boatChangeFactor);
5056+
return new PairingListTemplateDTO(flightCount, groupCount, competitorCount, flightMultiplier, boatChangeFactor,
50575057
template.getBoatChanges(), template.getPairingListTemplate(), template.getQuality(), template.getBoatAssignmentsQuality());
50585058
}
50595059

java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/DefaultWindTrackJsonSerializer.java

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.sap.sailing.server.gateway.serialization.impl;
22

3-
import java.util.ArrayList;
4-
import java.util.Iterator;
3+
import java.util.function.Function;
54

65
import org.json.simple.JSONArray;
76
import org.json.simple.JSONObject;
@@ -36,39 +35,48 @@ public DefaultWindTrackJsonSerializer(int maxNumberOfFixes, TimePoint fromTime,
3635
}
3736

3837
public JSONObject serialize(final WindTrack windTrack) {
39-
JSONObject result = new JSONObject();
40-
JSONArray jsonWindFixes = new JSONArray();
41-
// quickly extract relevant fixes to hold locks as shortly as possible; process later
42-
ArrayList<Wind> fixes = new ArrayList<>();
4338
windTrack.lockForRead();
4439
try {
45-
Iterator<Wind> windIter = windTrack.getFixesIterator(fromTime, /* inclusive */true, toTime, /* inclusive */ false);
46-
int count = 0;
47-
while ((maxNumberOfFixes == -1 || count++<maxNumberOfFixes) && windIter.hasNext()) {
48-
fixes.add(windIter.next());
49-
}
40+
return serializeFixesFromWindSource(windSource, ()->windTrack.getFixesIterator(fromTime, /* inclusive */true, toTime, /* inclusive */ false),
41+
/* averager */ wind->windTrack.getAveragedWind(wind.getPosition(), wind.getTimePoint()), maxNumberOfFixes);
5042
} finally {
5143
windTrack.unlockAfterRead();
5244
}
53-
for (Wind wind : fixes) {
54-
JSONObject jsonWind = new JSONObject();
55-
jsonWind.put("trueBearing-deg", RoundingUtil.bearingDecimalFormatter.format(wind.getBearing().getDegrees()));
56-
jsonWind.put("speed-kts", RoundingUtil.speedDecimalFormatter.format(wind.getKnots()));
57-
jsonWind.put("speed-m/s", RoundingUtil.speedDecimalFormatter.format(wind.getMetersPerSecond()));
58-
if (wind.getTimePoint() != null) {
59-
jsonWind.put("timepoint-ms", wind.getTimePoint().asMillis());
60-
final Wind averagedWind = windTrack.getAveragedWind(wind.getPosition(), wind.getTimePoint());
61-
jsonWind.put("dampenedTrueBearing-deg", RoundingUtil.bearingDecimalFormatter.format(averagedWind.getBearing().getDegrees()));
62-
jsonWind.put("dampenedSpeed-kts", RoundingUtil.speedDecimalFormatter.format(averagedWind.getKnots()));
63-
jsonWind.put("dampenedSpeed-m/s", RoundingUtil.speedDecimalFormatter.format(averagedWind.getMetersPerSecond()));
64-
}
65-
if (wind.getPosition() != null) {
66-
jsonWind.put("lat-deg", RoundingUtil.latLngDecimalFormatter.format(wind.getPosition().getLatDeg()));
67-
jsonWind.put("lng-deg", RoundingUtil.latLngDecimalFormatter.format(wind.getPosition().getLngDeg()));
45+
}
46+
47+
public static JSONObject serializeFixesFromWindSource(final WindSource windSource, final Iterable<Wind> fixes, final Function<Wind, Wind> averager, int maxNumberOfFixes) {
48+
final JSONObject result = new JSONObject();
49+
final JSONArray jsonWindFixes = new JSONArray();
50+
int count = 0;
51+
for (final Wind wind : fixes) {
52+
if (maxNumberOfFixes == 1 || count++ >= maxNumberOfFixes) {
53+
break;
6854
}
55+
final Wind averagedWind = wind.getTimePoint() == null ? null : averager.apply(wind);
56+
JSONObject jsonWind = getWindAsJson(wind, averagedWind);
6957
jsonWindFixes.add(jsonWind);
7058
}
7159
result.put(windSource.getType() + (windSource.getId() != null ? "-"+windSource.getId().toString() : ""), jsonWindFixes);
7260
return result;
7361
}
62+
63+
public static JSONObject getWindAsJson(Wind wind, final Wind averagedWind) {
64+
final JSONObject jsonWind = new JSONObject();
65+
jsonWind.put("trueBearing-deg", RoundingUtil.bearingDecimalFormatter.format(wind.getBearing().getDegrees()));
66+
jsonWind.put("speed-kts", RoundingUtil.speedDecimalFormatter.format(wind.getKnots()));
67+
jsonWind.put("speed-m/s", RoundingUtil.speedDecimalFormatter.format(wind.getMetersPerSecond()));
68+
if (wind.getTimePoint() != null) {
69+
jsonWind.put("timepoint-ms", wind.getTimePoint().asMillis());
70+
}
71+
if (averagedWind != null) {
72+
jsonWind.put("dampenedTrueBearing-deg", RoundingUtil.bearingDecimalFormatter.format(averagedWind.getBearing().getDegrees()));
73+
jsonWind.put("dampenedSpeed-kts", RoundingUtil.speedDecimalFormatter.format(averagedWind.getKnots()));
74+
jsonWind.put("dampenedSpeed-m/s", RoundingUtil.speedDecimalFormatter.format(averagedWind.getMetersPerSecond()));
75+
}
76+
if (wind.getPosition() != null) {
77+
jsonWind.put("lat-deg", RoundingUtil.latLngDecimalFormatter.format(wind.getPosition().getLatDeg()));
78+
jsonWind.put("lng-deg", RoundingUtil.latLngDecimalFormatter.format(wind.getPosition().getLngDeg()));
79+
}
80+
return jsonWind;
81+
}
7482
}

java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/TrackedRaceJsonSerializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ public class TrackedRaceJsonSerializer implements JsonSerializer<TrackedRace> {
2626
private final String windSourceIdToSerialize;
2727
private final Function<WindSource, WindTrackJsonSerializer> windTrackSerializerProducer;
2828

29-
public TrackedRaceJsonSerializer(Function<WindSource, WindTrackJsonSerializer> windTrackSerializer, String windSourceToSerialize, String windSourceIdToSerialize) {
29+
public TrackedRaceJsonSerializer(final Function<WindSource, WindTrackJsonSerializer> windTrackSerializer,
30+
final String windSourceToSerialize, final String windSourceIdToSerialize) {
3031
this.windTrackSerializerProducer = windTrackSerializer;
3132
this.windSourceToSerialize = windSourceToSerialize;
3233
this.windSourceIdToSerialize = windSourceIdToSerialize;
33-
windSourceToSerialize = WindSourceType.COMBINED.name();
3434
}
3535

3636
public JSONObject serialize(TrackedRace trackedRace) {

java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,17 @@
9191
import com.sap.sailing.domain.common.RegattaNameAndRaceName;
9292
import com.sap.sailing.domain.common.Tack;
9393
import com.sap.sailing.domain.common.TargetTimeInfo;
94+
import com.sap.sailing.domain.common.Wind;
9495
import com.sap.sailing.domain.common.WindSource;
96+
import com.sap.sailing.domain.common.WindSourceType;
9597
import com.sap.sailing.domain.common.abstractlog.NotRevokableException;
9698
import com.sap.sailing.domain.common.dto.CompetitorDTO;
9799
import com.sap.sailing.domain.common.dto.FleetDTO;
98100
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
99101
import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO;
100102
import com.sap.sailing.domain.common.dto.LeaderboardRowDTO;
101103
import com.sap.sailing.domain.common.dto.LegEntryDTO;
104+
import com.sap.sailing.domain.common.impl.WindImpl;
102105
import com.sap.sailing.domain.common.polars.NotEnoughDataHasBeenAddedException;
103106
import com.sap.sailing.domain.common.security.SecuredDomainType;
104107
import com.sap.sailing.domain.common.security.SecuredDomainType.LeaderboardActions;
@@ -178,6 +181,7 @@
178181
import com.sap.sse.common.Color;
179182
import com.sap.sse.common.Distance;
180183
import com.sap.sse.common.Duration;
184+
import com.sap.sse.common.Position;
181185
import com.sap.sse.common.Speed;
182186
import com.sap.sse.common.SpeedWithBearing;
183187
import com.sap.sse.common.TimePoint;
@@ -186,6 +190,7 @@
186190
import com.sap.sse.common.Util.Pair;
187191
import com.sap.sse.common.Util.Triple;
188192
import com.sap.sse.common.WithID;
193+
import com.sap.sse.common.impl.DegreePosition;
189194
import com.sap.sse.common.impl.MillisecondsDurationImpl;
190195
import com.sap.sse.common.impl.MillisecondsTimePoint;
191196
import com.sap.sse.common.impl.RGBColor;
@@ -209,7 +214,7 @@
209214
@Path("/v1/regattas")
210215
public class RegattasResource extends AbstractSailingServerResource {
211216
private static final String SECONDARY_USER_BEARER_TOKEN = "secondaryuserbearertoken";
212-
217+
private static final int MAX_NUMBER_OF_WIND_FIXES_PER_REQUEST = 10000;
213218
private static final Logger logger = Logger.getLogger(RegattasResource.class.getName());
214219

215220
private DataMiningResource dataMiningResource;
@@ -1775,6 +1780,11 @@ public Response getHighQualityWindFixes(@PathParam("regattaname") String regatta
17751780
return response;
17761781
}
17771782

1783+
@FunctionalInterface
1784+
private static interface WindForTrackedRaceSerializerFactory {
1785+
JsonSerializer<TrackedRace> getSerializer(TimePoint finalFrom, TimePoint finalTo);
1786+
}
1787+
17781788
@GET
17791789
@Produces("application/json;charset=UTF-8")
17801790
@Path("{regattaname}/races/{racename}/wind")
@@ -1785,6 +1795,93 @@ public Response getWind(@PathParam("regattaname") String regattaName, @PathParam
17851795
@QueryParam("totimeasmillis") Long totimeasmillis,
17861796
@HeaderParam(SECONDARY_USER_BEARER_TOKEN) String secondaryUserBearerToken,
17871797
@Context HttpServletRequest request) {
1798+
return getWindWithSerializer(regattaName, raceName, fromtime, fromtimeasmillis, totime, totimeasmillis, secondaryUserBearerToken, request,
1799+
(finalFrom, finalTo)->new TrackedRaceJsonSerializer(
1800+
ws -> new DefaultWindTrackJsonSerializer(MAX_NUMBER_OF_WIND_FIXES_PER_REQUEST, finalFrom, finalTo,ws), windSource, windSourceId));
1801+
}
1802+
1803+
@GET
1804+
@Produces("application/json;charset=UTF-8")
1805+
@Path("{regattaname}/races/{racename}/wind_at_position")
1806+
public Response getWindAtPosition(@PathParam("regattaname") String regattaName, @PathParam("racename") String raceName,
1807+
@QueryParam("fromtime") String fromtime,
1808+
@QueryParam("fromtimeasmillis") Long fromtimeasmillis, @QueryParam("totime") String totime,
1809+
@QueryParam("totimeasmillis") Long totimeasmillis,
1810+
@QueryParam("intervalasmillis") Long intervalasmillis,
1811+
@QueryParam("latDeg[]") List<Double> latitudesInDegrees,
1812+
@QueryParam("lngDeg[]") List<Double> longitudesInDegrees,
1813+
@HeaderParam(SECONDARY_USER_BEARER_TOKEN) String secondaryUserBearerToken,
1814+
@Context HttpServletRequest request) {
1815+
final Response response;
1816+
if (latitudesInDegrees == null || longitudesInDegrees == null) {
1817+
response = Response.serverError().entity("Latitudes and longitudes must be provided as query parameters latDeg[] and lngDeg[]").build();
1818+
} else if (latitudesInDegrees.size() != longitudesInDegrees.size()) {
1819+
response = Response.serverError().entity("Latitudes and longitudes arrays in query parameters latDeg[] and lngDeg[] must have equal count").build();
1820+
} else if (intervalasmillis == null) {
1821+
response = Response.serverError().entity("intervalasmillis query parameter is missing").build();
1822+
} else if (intervalasmillis <= 0) {
1823+
response = Response.serverError().entity("intervalasmillis query parameter must be a positive value").build();
1824+
} else {
1825+
response = getWindWithSerializer(regattaName, raceName, fromtime, fromtimeasmillis, totime, totimeasmillis, secondaryUserBearerToken, request,
1826+
new WindForTrackedRaceSerializerFactory() {
1827+
@Override
1828+
public JsonSerializer<TrackedRace> getSerializer(TimePoint finalFrom, TimePoint finalTo) {
1829+
return new JsonSerializer<TrackedRace>() {
1830+
@Override
1831+
public JSONObject serialize(TrackedRace trackedRace) {
1832+
return serializeWindAtPositionsAndTimePoints(trackedRace, finalFrom, finalTo,
1833+
new MillisecondsDurationImpl(intervalasmillis), latitudesInDegrees,
1834+
longitudesInDegrees);
1835+
}
1836+
};
1837+
}
1838+
});
1839+
}
1840+
return response;
1841+
}
1842+
1843+
private JSONObject serializeWindAtPositionsAndTimePoints(TrackedRace trackedRace, TimePoint from,
1844+
TimePoint to, Duration interval, List<Double> latitudesInDegrees, List<Double> longitudesInDegrees) {
1845+
final Iterable<Wind> windIterable = new Iterable<Wind>() {
1846+
@Override
1847+
public Iterator<Wind> iterator() {
1848+
return new Iterator<Wind>() {
1849+
private TimePoint timePoint = from;
1850+
private int count = 0;
1851+
1852+
@Override
1853+
public boolean hasNext() {
1854+
return timePoint != null && (to == null || !timePoint.after(to));
1855+
}
1856+
1857+
@Override
1858+
public Wind next() {
1859+
final Position position = new DegreePosition(latitudesInDegrees.get(count), longitudesInDegrees.get(count));
1860+
// TODO bug6281: the position gets lost when asking the tracked race for wind at that position because we get an average from all wind sources,
1861+
// including the averaged position. We may want to pass the requested position into the response, too, but how?
1862+
final Wind windFromRace = trackedRace.getWind(position, timePoint);
1863+
final Wind result = new WindImpl(position, timePoint, windFromRace);
1864+
count++;
1865+
if (count >= latitudesInDegrees.size()) {
1866+
timePoint = timePoint.plus(interval);
1867+
count = 0;
1868+
}
1869+
return result;
1870+
}
1871+
};
1872+
}
1873+
};
1874+
return DefaultWindTrackJsonSerializer.serializeFixesFromWindSource(trackedRace.getWindSources(WindSourceType.COMBINED).iterator().next(),
1875+
windIterable, wind->wind, MAX_NUMBER_OF_WIND_FIXES_PER_REQUEST);
1876+
}
1877+
1878+
private Response getWindWithSerializer(final String regattaName, final String raceName,
1879+
final String fromtime,
1880+
final Long fromtimeasmillis, final String totime,
1881+
final Long totimeasmillis,
1882+
final String secondaryUserBearerToken,
1883+
final HttpServletRequest request,
1884+
final WindForTrackedRaceSerializerFactory serializerFactory) {
17881885
Response response;
17891886
final String clientIP = HttpRequestUtils.getClientIP(request);
17901887
final String userAgent = HttpRequestUtils.getUserAgent(request);
@@ -1830,12 +1927,7 @@ public Response getWind(@PathParam("regattaname") String regattaName, @PathParam
18301927
final TimePoint finalFrom = Util.getLatestOfTimePoints(from, trackedRace.getStartOfTracking());
18311928
final TimePoint finalTo = Util.getEarliestOfTimePoints(to, Util.getEarliestOfTimePoints(
18321929
trackedRace.getEndOfTracking(), trackedRace.getTimePointOfNewestEvent()));
1833-
final TrackedRaceJsonSerializer serializer = new TrackedRaceJsonSerializer(
1834-
ws -> new DefaultWindTrackJsonSerializer(/* maxNumberOfFixes */ 10000, finalFrom, finalTo,
1835-
ws),
1836-
windSource, windSourceId);
1837-
1838-
final JSONObject jsonWindTracks = serializer.serialize(trackedRace);
1930+
final JSONObject jsonWindTracks = serializerFactory.getSerializer(finalFrom, finalTo).serialize(trackedRace);
18391931
response = Response.ok(streamingOutput(jsonWindTracks)).build();
18401932
}
18411933
}

0 commit comments

Comments
 (0)