mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-21 11:40:01 +00:00
- added support for loading addtional race information, e.g. time offset
- made time zone explicit wich is used to convert start time which comes without time zone information --> to be added from additional race information
This commit is contained in:
1 parent
0fb43c35a5
commit
6b275d49ba
8 files changed
+327
-99
No files matched your search
+24
@@ -0,0 +1,24 @@
|
||||
package com.sap.sailing.domain.swisstimingreplayadapter;
|
||||
|
||||
public class SwissTimingRaceConfig {
|
||||
|
||||
public final String latitude;
|
||||
public final String longitude;
|
||||
public final String country_code;
|
||||
public final String gmt_offset;
|
||||
public final String location;
|
||||
public final String event_name;
|
||||
public final String race_start_ts;
|
||||
|
||||
public SwissTimingRaceConfig(String latitude, String longitude, String country_code, String gmt_offset,
|
||||
String location, String event_name, String race_start_ts) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.country_code = country_code;
|
||||
this.gmt_offset = gmt_offset;
|
||||
this.location = location;
|
||||
this.event_name = event_name;
|
||||
this.race_start_ts = race_start_ts;
|
||||
}
|
||||
|
||||
}
|
||||
+70
-21
@@ -1,44 +1,93 @@
|
||||
package com.sap.sailing.domain.swisstimingreplayadapter;
|
||||
|
||||
/**
|
||||
* Implementations are passed to {@link SwissTimingReplayService#loadRaceData(String, SwissTimingReplayListener)} for getting notified of about content parsed from SwissTiming's binary replay file format.
|
||||
*
|
||||
* The protocol is
|
||||
* 1 x keyFrameIndex
|
||||
* n x keyFrameIndexPosition
|
||||
* 1 x eot
|
||||
* n x [
|
||||
* 1 x referenceTimestamp
|
||||
* 1 x referenceLocation
|
||||
* 1 x rsc_cid
|
||||
* 1 x competitorsCount
|
||||
* competitorsCount x competitor
|
||||
* n x mark
|
||||
* 1 x trackersCount
|
||||
* trackersCount x trackers
|
||||
* 1 x rankingsCount
|
||||
* rankingsCount x ranking [
|
||||
* n x rankingMark
|
||||
* ]
|
||||
* 1 x eot
|
||||
*
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* @author D047974 - Jens Rommel
|
||||
*
|
||||
*/
|
||||
public interface SwissTimingReplayListener {
|
||||
|
||||
void referenceTimestamp(long referenceTimestamp);
|
||||
|
||||
void referenceLocation(int latitude, int longitude);
|
||||
|
||||
void keyFrameIndex(int keyFrameIndex);
|
||||
|
||||
void keyFrameIndexPosition(int keyFrameIndexPosition);
|
||||
|
||||
void referenceTimestamp(long referenceTimestamp);
|
||||
|
||||
void referenceLocation(int latitude, int longitude);
|
||||
|
||||
void rsc_cid(String text);
|
||||
|
||||
void mark(MarkType markType, String identifier, byte index, String id1, String id2, short windSpeed, short windDirection);
|
||||
|
||||
void frameMetaData(byte cid, int raceTime, int startTime, int estimatedStartTime, RaceStatus raceStatus, short distanceToNextMark,
|
||||
Weather weather, short humidity, short temperature, String messageText, byte cFlag, byte rFlag,
|
||||
byte duration, short nm);
|
||||
|
||||
void competitorsCount(short competitorsCount);
|
||||
|
||||
void competitor(int hashValue, String nation, String sailNumber, String name, CompetitorStatus competitorStatus, BoatType boatType,
|
||||
short cRank_Bracket, short cnPoints_x10_Bracket, short ctPoints_x10_Winner);
|
||||
|
||||
void frame(byte cid, int raceTime, int startTime, int estimatedStartTime, RaceStatus raceStatus, short distanceToNextMark,
|
||||
Weather weather, short humidity, short temperature, String messageText, byte cFlag, byte rFlag,
|
||||
byte duration, short nm);
|
||||
|
||||
void mark(MarkType markType, String identifier, byte index, String id1, String id2, short windSpeed_Knots, short windDirection);
|
||||
|
||||
void trackersCount(short trackersCount);
|
||||
|
||||
void trackers(int hashValue, int latitude, int longitude, short cog, short sog_Knots_x10, short average_sog, short vmg_Knots_x10,
|
||||
CompetitorStatus competitorStatus, short rank, short distanceToLeader, short distanceToNextMark, short nextMark, short pRank, short ptPoints, short pnPoints);
|
||||
|
||||
void rankingsCount(short entriesCount);
|
||||
|
||||
void ranking(int hashValue, short rank, short rankIndex, short racePoints, CompetitorStatus competitorStatus, short finishRank,
|
||||
short finishRankIndex, int gap, int raceTime);
|
||||
|
||||
void rankingsCount(short entriesCount);
|
||||
|
||||
void rankingMark(short marksRank, short marksRankIndex, int marksGap, int marksRaceTime);
|
||||
|
||||
void trackersCount(short trackersCount);
|
||||
|
||||
void trackers(int hashValue, int latitude, int longitude, short cog, short sog, short average_sog, short vmg,
|
||||
CompetitorStatus competitorStatus, short rank, short dtl, short dtnm, short nm, short pRank, short ptPoints, short pnPoints);
|
||||
|
||||
void illegalState(String message);
|
||||
|
||||
void unknownMessageIdentificationCode(byte messageIdentificationCode);
|
||||
|
||||
void eot();
|
||||
|
||||
/**
|
||||
* Called when an unknown message identification codes has been encountered.
|
||||
* See {@link MessageIdentificationCodes} for supported codes.
|
||||
* @param messageIdentificationCode
|
||||
*/
|
||||
void unknownMessageIdentificationCode(byte messageIdentificationCode);
|
||||
|
||||
/**
|
||||
* Called when STX was expected but startByte has been encountered.
|
||||
* @param startByte
|
||||
*/
|
||||
void unexpectedStartByte(byte startByte);
|
||||
|
||||
/**
|
||||
* Called when the actual payload was smaller than announced in the message header.
|
||||
* @param remainingPayloadSize
|
||||
*/
|
||||
void payloadMismatch(short remainingPayloadSize);
|
||||
|
||||
/**
|
||||
* Called when the end of a message was expected but a non ETX/EOT byte has been encountered.
|
||||
* @param endByte
|
||||
*/
|
||||
void prematureEndOfData(byte endByte);
|
||||
|
||||
}
|
||||
+116
-59
@@ -7,89 +7,140 @@ import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.DateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
|
||||
public class SwissTimingReplayService {
|
||||
|
||||
|
||||
static final String RACE_CONFIG_URL_TEMPLATE = "http://live.ota.st-sportservice.com/configuration?_race={0}&effective=1&additional=config";
|
||||
|
||||
private static final int STX = 0x02;
|
||||
private static final int ETX = 0x03;
|
||||
private static final int EOT = 0x04;
|
||||
|
||||
|
||||
private static final Charset CHARSET = Charset.forName("ISO-8859-1");
|
||||
|
||||
public static final DateFormat SWISSTIMING_DATEFORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
|
||||
public static final String SWISSTIMING_DATEFORMAT_PATTERN = "dd.MM.yyyy HH:mm";
|
||||
public static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone("GMT");
|
||||
|
||||
public static List<SwissTimingReplayRace> listReplayRaces(String swissTimingUrlText) {
|
||||
URL url;
|
||||
URL raceListUrl;
|
||||
try {
|
||||
url = new URL(swissTimingUrlText);
|
||||
return parseJSONObject(url.openStream(), swissTimingUrlText);
|
||||
} catch (Exception e) { //MalformedURLException | IOException | ParseException | org.json.simple.parser.ParseException)
|
||||
raceListUrl = new URL(swissTimingUrlText);
|
||||
List<SwissTimingReplayRace> races = parseJSONObject(raceListUrl.openStream(), swissTimingUrlText);
|
||||
// loadRaceConfigs(races);
|
||||
return races;
|
||||
} catch (Exception e) { // MalformedURLException | IOException | ParseException |
|
||||
// org.json.simple.parser.ParseException)
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static List<SwissTimingReplayRace> parseJSONObject(InputStream inputStream, String swissTimingUrlText) throws IOException, ParseException, org.json.simple.parser.ParseException {
|
||||
public static SwissTimingRaceConfig loadRaceConfig(String raceId) {
|
||||
try {
|
||||
URL configUrl = new URL(MessageFormat.format(RACE_CONFIG_URL_TEMPLATE, raceId));
|
||||
InputStream configDataStream = configUrl.openStream();
|
||||
return loadRaceConfig(configDataStream);
|
||||
} catch (Exception e) { // MalformedURLException | ParseException | org.json.simple.parser.ParseException)
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static SwissTimingRaceConfig loadRaceConfig(InputStream configDataStream) throws IOException,
|
||||
org.json.simple.parser.ParseException {
|
||||
JSONObject jsonRaceConfig = (JSONObject) new JSONParser().parse(new InputStreamReader(configDataStream));
|
||||
JSONObject jsonConfigEntry = (JSONObject) jsonRaceConfig.get("config");
|
||||
String latitude = (String) jsonRaceConfig.get("latitude");
|
||||
String longitude = (String) jsonRaceConfig.get("longitude");
|
||||
String country_code = (String) jsonRaceConfig.get("country_code");
|
||||
String gmt_offset = (String) jsonRaceConfig.get("gmt_offset");
|
||||
String location = (String) jsonRaceConfig.get("location");
|
||||
String event_name = (String) (jsonConfigEntry != null ? jsonConfigEntry.get("event_name") : null);
|
||||
String race_start_ts = (String) (jsonConfigEntry != null ? jsonConfigEntry.get("race_start_ts") : null);
|
||||
SwissTimingRaceConfig raceConfig = new SwissTimingRaceConfig(latitude, longitude, country_code, gmt_offset,
|
||||
location, event_name, race_start_ts);
|
||||
return raceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param inputStream
|
||||
* The stream to read the JSON content from
|
||||
* @param swissTimingUrlText
|
||||
* The URL where the stream has been taken from. Only used as information for later reference.
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws ParseException
|
||||
* @throws org.json.simple.parser.ParseException
|
||||
*/
|
||||
static List<SwissTimingReplayRace> parseJSONObject(InputStream inputStream, String swissTimingUrlText)
|
||||
throws IOException, ParseException, org.json.simple.parser.ParseException {
|
||||
JSONArray json = (JSONArray) new JSONParser().parse(new InputStreamReader(inputStream));
|
||||
List<SwissTimingReplayRace> result = new ArrayList<SwissTimingReplayRace>();
|
||||
DateFormat startTimeFormat = getStartTimeFormat();
|
||||
for (Object raceEntry : json) {
|
||||
JSONObject jsonRaceEntry = (JSONObject) raceEntry;
|
||||
String startTimeText = (String) jsonRaceEntry.get("start");
|
||||
Date startTime = startTimeText == null ? null : SWISSTIMING_DATEFORMAT.parse(startTimeText);
|
||||
SwissTimingReplayRace replayRace = new SwissTimingReplayRace(
|
||||
swissTimingUrlText,
|
||||
(String) jsonRaceEntry.get("flight_number"),
|
||||
(String) jsonRaceEntry.get("race_id"),
|
||||
(String) jsonRaceEntry.get("rsc"),
|
||||
(String) jsonRaceEntry.get("name"),
|
||||
(String) jsonRaceEntry.get("class"),
|
||||
startTime ,
|
||||
Date startTime = startTimeText == null ? null : startTimeFormat.parse(startTimeText);
|
||||
|
||||
SwissTimingReplayRace replayRace = new SwissTimingReplayRace(swissTimingUrlText,
|
||||
(String) jsonRaceEntry.get("flight_number"), (String) jsonRaceEntry.get("race_id"),
|
||||
(String) jsonRaceEntry.get("rsc"), (String) jsonRaceEntry.get("name"),
|
||||
(String) jsonRaceEntry.get("class"), startTime,
|
||||
(String) jsonRaceEntry.get("link"));
|
||||
result.add(replayRace);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static DateFormat getStartTimeFormat() {
|
||||
DateFormat startTimeFormat = new SimpleDateFormat(SWISSTIMING_DATEFORMAT_PATTERN);
|
||||
startTimeFormat.setTimeZone(DEFAULT_TIMEZONE);
|
||||
return startTimeFormat;
|
||||
}
|
||||
|
||||
public static void loadRaceData(String link, SwissTimingReplayListener replayListener) {
|
||||
URL raceDataUrl;
|
||||
try {
|
||||
raceDataUrl = new URL("http://" + link);
|
||||
InputStream urlInputStream = raceDataUrl.openStream();
|
||||
readData(urlInputStream, replayListener);
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static void readData(InputStream urlInputStream, SwissTimingReplayListener replayListener) throws IOException {
|
||||
DataInputStream data = new DataInputStream(urlInputStream);
|
||||
|
||||
byte[] startByteBuffer = new byte[1];
|
||||
|
||||
byte[] startByteBuffer = new byte[1];
|
||||
int readSuccess = data.read(startByteBuffer);
|
||||
|
||||
|
||||
while (readSuccess != -1) {
|
||||
|
||||
|
||||
byte startByte = startByteBuffer[0];
|
||||
if (startByte != STX) {
|
||||
replayListener.illegalState("Expected STX (0x02), found: " + startByte);
|
||||
replayListener.unexpectedStartByte(startByte);
|
||||
}
|
||||
|
||||
|
||||
byte messageIdentificationCode = data.readByte();
|
||||
short messageLength = data.readShort();
|
||||
|
||||
|
||||
if (messageLength > 0) {
|
||||
short payloadSize = (short) (messageLength - 5); //payload is message minus STX, MIC, message length and ETX/EOT --> = 5 byte
|
||||
|
||||
short payloadSize = (short) (messageLength - 5); // payload is message minus STX, MIC, message length
|
||||
// and ETX/EOT --> = 5 byte
|
||||
|
||||
switch (messageIdentificationCode) {
|
||||
case MessageIdentificationCodes.MIC_00_Reference_Timestamp:
|
||||
while (payloadSize > 0) {
|
||||
@@ -113,7 +164,7 @@ public class SwissTimingReplayService {
|
||||
|
||||
replayListener.keyFrameIndex(keyFrameIndex);
|
||||
payloadSize -= 4;
|
||||
|
||||
|
||||
while (payloadSize > 0) {
|
||||
int keyFrameIndexPosition = data.readInt();
|
||||
|
||||
@@ -140,7 +191,8 @@ public class SwissTimingReplayService {
|
||||
short windSpeed = data.readShort();
|
||||
short windDirection = data.readShort();
|
||||
|
||||
replayListener.mark(new MarkType(markType), identifier, index, id1, id2, windSpeed, windDirection);
|
||||
replayListener.mark(new MarkType(markType), identifier, index, id1, id2, windSpeed,
|
||||
windDirection);
|
||||
payloadSize -= 1 + 8 + 1 + 8 + 8 + 2 + 2;
|
||||
}
|
||||
break;
|
||||
@@ -160,8 +212,10 @@ public class SwissTimingReplayService {
|
||||
short cRank_Bracket = data.readShort();
|
||||
short cnPoints_x10_Bracket = data.readShort();
|
||||
short ctPoints_x10_Winner = data.readShort();
|
||||
|
||||
replayListener.competitor(hashValue, nation, sailNumber, name, CompetitorStatus.byCode(competitorStatus), BoatType.byCode(boatType), cRank_Bracket, cnPoints_x10_Bracket, ctPoints_x10_Winner);
|
||||
|
||||
replayListener.competitor(hashValue, nation, sailNumber, name,
|
||||
CompetitorStatus.byCode(competitorStatus), BoatType.byCode(boatType), cRank_Bracket,
|
||||
cnPoints_x10_Bracket, ctPoints_x10_Winner);
|
||||
payloadSize -= 4 + 3 + 1 + sailNumberLength + 1 + nameLength + 1 + 1 + 2 + 2 + 2;
|
||||
}
|
||||
break;
|
||||
@@ -182,9 +236,10 @@ public class SwissTimingReplayService {
|
||||
byte rFlag = data.readByte();
|
||||
byte duration = data.readByte();
|
||||
short nm = data.readShort();
|
||||
|
||||
replayListener.frame(cid, raceTime, startTime, estimatedStartTime, RaceStatus.byCode(raceStatus), distanceToNextMark, Weather.byCode(weather), humidity, temperature,
|
||||
messageText, cFlag, rFlag, duration, nm);
|
||||
|
||||
replayListener.frameMetaData(cid, raceTime, startTime, estimatedStartTime,
|
||||
RaceStatus.byCode(raceStatus), distanceToNextMark, Weather.byCode(weather), humidity,
|
||||
temperature, messageText, cFlag, rFlag, duration, nm);
|
||||
payloadSize -= 1 + 3 + 3 + 3 + 1 + 2 + 1 + 2 + 2 + 1 + messageTextLength + 1 + 1 + 1 + 2;
|
||||
}
|
||||
break;
|
||||
@@ -203,7 +258,8 @@ public class SwissTimingReplayService {
|
||||
int gap = read3ByteInt(data);
|
||||
int raceTime = read3ByteInt(data);
|
||||
short marksCount = data.readShort();
|
||||
replayListener.ranking(hashValue, rank, rankIndex, racePoints, CompetitorStatus.byCode(competitorStatus), finishRank, finishRankIndex, gap, raceTime);
|
||||
replayListener.ranking(hashValue, rank, rankIndex, racePoints,
|
||||
CompetitorStatus.byCode(competitorStatus), finishRank, finishRankIndex, gap, raceTime);
|
||||
payloadSize -= 4 + 2 + 2 + 2 + 1 + 2 + 2 + 3 + 3 + 2;
|
||||
for (int i = 0; i < marksCount; i++) {
|
||||
short marksRank = data.readShort();
|
||||
@@ -213,7 +269,7 @@ public class SwissTimingReplayService {
|
||||
replayListener.rankingMark(marksRank, marksRankIndex, marksGap, marksRaceTime);
|
||||
payloadSize -= 2 + 2 + 3 + 3;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
break;
|
||||
case MessageIdentificationCodes.MIC_124_Trackers:
|
||||
@@ -236,8 +292,10 @@ public class SwissTimingReplayService {
|
||||
short pRank = data.readShort();
|
||||
short ptPoints = data.readShort();
|
||||
short pnPoints = data.readShort();
|
||||
|
||||
replayListener.trackers(hashValue, latitude, longitude, cog, sog, average_sog, vmg, CompetitorStatus.byCode(competitorStatus), rank, dtl, dtnm, nm, pRank, ptPoints, pnPoints);
|
||||
|
||||
replayListener.trackers(hashValue, latitude, longitude, cog, sog, average_sog, vmg,
|
||||
CompetitorStatus.byCode(competitorStatus), rank, dtl, dtnm, nm, pRank, ptPoints,
|
||||
pnPoints);
|
||||
|
||||
payloadSize -= 4 + 4 + 4 + 2 + 2 + 2 + 2 + 1 + 2 + 2 + 2 + 2 + 2 + 2 + 2;
|
||||
}
|
||||
@@ -248,35 +306,34 @@ public class SwissTimingReplayService {
|
||||
}
|
||||
|
||||
if (payloadSize != 0) {
|
||||
replayListener.illegalState("Payload mismatch.");
|
||||
replayListener.payloadMismatch(payloadSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
byte endByte = data.readByte();
|
||||
if (endByte == EOT) {
|
||||
replayListener.eot();
|
||||
} else if (endByte == ETX) {
|
||||
//observe whether ETX is interesting for anybody.
|
||||
} else {
|
||||
replayListener.illegalState("Premature end of data section.");
|
||||
}
|
||||
|
||||
readSuccess = data.read(startByteBuffer);
|
||||
|
||||
|
||||
byte endByte = data.readByte();
|
||||
if (endByte == EOT) {
|
||||
replayListener.eot();
|
||||
} else if (endByte == ETX) {
|
||||
// observe whether ETX is interesting for anybody.
|
||||
} else {
|
||||
replayListener.prematureEndOfData(endByte);
|
||||
}
|
||||
|
||||
readSuccess = data.read(startByteBuffer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static int read3ByteInt(DataInputStream data) throws IOException {
|
||||
byte highByte = data.readByte();
|
||||
byte midByte = data.readByte();
|
||||
byte highByte = data.readByte();
|
||||
byte midByte = data.readByte();
|
||||
byte lowByte = data.readByte();
|
||||
|
||||
//turn signed to unsigned, then shift left
|
||||
|
||||
// turn signed to unsigned, then shift left
|
||||
int highInt = (highByte & 0xFF) << 16;
|
||||
int midInt = (midByte & 0xFF) << 8;
|
||||
int lowInt = lowByte & 0xFF;
|
||||
|
||||
|
||||
return highInt | midInt | lowInt;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user