Added MessageDecoder interface. All decoder implement this.
Added DecoderFactory. This creates an appropriate decoder based on a MessageType. BoatActionDecoder implements MessageDecoder. BoatLocationDecoder implements MessageDecoder. HeartBeatDecoder implements MessageDecoder. JoinAcceptance implements MessageDecoder. RaceStatusDecoder implements MessageDecoder. RequestToJoinDecoder implements MessageDecoder. XMLMessageDecoder implements MessageDecoder. Refactored CourseWind decoder/encoder. CourseWind decoder/encoder is for an individual CourseWind. CourseWinds decoder/encoder is for the combined message from the API. Documented BoatAction, and it now contains a BoatActionEnum instead of a byte. Refactored CourseWind and CourseWinds classes. They now expose correct units, instead of packed units. Added CourseWindDecoderTest, and updated CourseWindsDecoderTest. issue #35 #36 #story[1095]
This commit is contained in:
@@ -2,6 +2,7 @@ package network;
|
||||
|
||||
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.Exceptions.InvalidMessageTypeException;
|
||||
import network.MessageDecoders.*;
|
||||
import network.Messages.*;
|
||||
import network.Messages.Enums.MessageType;
|
||||
@@ -130,16 +131,26 @@ public class BinaryMessageDecoder {
|
||||
//Now we create the message object based on what is actually in the message body.
|
||||
MessageType mType = MessageType.fromByte(headerMessageType);
|
||||
|
||||
/*MessageDecoder decoder = null;
|
||||
try {
|
||||
decoder = DecoderFactory.create(mType);
|
||||
} catch (InvalidMessageTypeException e) {
|
||||
throw new InvalidMessageException("Could not create decoder for MessageType: " + mType, e);
|
||||
}
|
||||
|
||||
return decoder.decode(messageBody);*/
|
||||
|
||||
|
||||
switch(mType) {
|
||||
case HEARTBEAT:
|
||||
//System.out.println("Decoding HeartBeat Message!");
|
||||
HeartBeatDecoder heartBeatDecoder = new HeartBeatDecoder(messageBody);
|
||||
return heartBeatDecoder.getMessage();
|
||||
HeartBeatDecoder heartBeatDecoder = new HeartBeatDecoder();
|
||||
return heartBeatDecoder.decode(messageBody);
|
||||
|
||||
case RACESTATUS:
|
||||
//System.out.println("Race Status Message");
|
||||
RaceStatusDecoder rsdecoder = new RaceStatusDecoder(messageBody);
|
||||
return rsdecoder.getMessage();
|
||||
RaceStatusDecoder rsdecoder = new RaceStatusDecoder();
|
||||
return rsdecoder.decode(messageBody);
|
||||
|
||||
case DISPLAYTEXTMESSAGE:
|
||||
//System.out.println("Display Text Message");
|
||||
@@ -148,8 +159,8 @@ public class BinaryMessageDecoder {
|
||||
|
||||
case XMLMESSAGE:
|
||||
//System.out.println("XML Message!");
|
||||
XMLMessageDecoder xmdecoder = new XMLMessageDecoder(messageBody);
|
||||
return xmdecoder.getMessage();
|
||||
XMLMessageDecoder xmdecoder = new XMLMessageDecoder();
|
||||
return xmdecoder.decode(messageBody);
|
||||
|
||||
case RACESTARTSTATUS:
|
||||
//System.out.println("Race Start Status Message");
|
||||
@@ -173,8 +184,8 @@ public class BinaryMessageDecoder {
|
||||
|
||||
case BOATLOCATION:
|
||||
//System.out.println("Boat Location Message!");
|
||||
BoatLocationDecoder blDecoder = new BoatLocationDecoder(messageBody);
|
||||
return blDecoder.getMessage();
|
||||
BoatLocationDecoder blDecoder = new BoatLocationDecoder();
|
||||
return blDecoder.decode(messageBody);
|
||||
|
||||
case MARKROUNDING:
|
||||
//System.out.println("Mark Rounding Message!");
|
||||
@@ -183,8 +194,8 @@ public class BinaryMessageDecoder {
|
||||
|
||||
case COURSEWIND:
|
||||
//System.out.println("Course Wind Message!");
|
||||
CourseWindDecoder cwDecoder = new CourseWindDecoder(messageBody);
|
||||
return new CourseWinds(cwDecoder.getMessageVersionNumber(), cwDecoder.getByteWindID(), cwDecoder.getLoopMessages());
|
||||
CourseWindsDecoder cwDecoder = new CourseWindsDecoder();
|
||||
return cwDecoder.decode(messageBody);
|
||||
|
||||
case AVGWIND:
|
||||
//System.out.println("Average Wind Message!");
|
||||
@@ -192,8 +203,8 @@ public class BinaryMessageDecoder {
|
||||
return awDecoder.getAverageWind();
|
||||
|
||||
case BOATACTION:
|
||||
BoatActionDecoder baDecoder = new BoatActionDecoder(messageBody);
|
||||
return new BoatAction(baDecoder.getBoatAction());
|
||||
BoatActionDecoder baDecoder = new BoatActionDecoder();
|
||||
return baDecoder.decode(messageBody);
|
||||
|
||||
default:
|
||||
//System.out.println("Broken Message!");
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatAction;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class BoatActionDecoder {
|
||||
byte byteBoatAction;
|
||||
BoatActionEnum boatAction;
|
||||
/**
|
||||
* Decodes {@link BoatAction} messages.
|
||||
*/
|
||||
public class BoatActionDecoder implements MessageDecoder {
|
||||
|
||||
public BoatActionDecoder(byte[] encodedBoatAction) {
|
||||
byteBoatAction = encodedBoatAction[0];
|
||||
/**
|
||||
* The encoded message.
|
||||
*/
|
||||
private byte[] encodedMessage;
|
||||
|
||||
boatAction = BoatActionEnum.fromByte(byteBoatAction);
|
||||
/**
|
||||
* The decoded message.
|
||||
*/
|
||||
private BoatAction message;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
*/
|
||||
public BoatActionDecoder() {
|
||||
}
|
||||
|
||||
public BoatActionEnum getBoatAction() {
|
||||
return boatAction;
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
BoatActionEnum boatActionEnum = BoatActionEnum.fromByte(encodedMessage[0]);
|
||||
|
||||
message = new BoatAction(boatActionEnum);
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the decoded message.
|
||||
* @return The decoded message.
|
||||
*/
|
||||
public BoatAction getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatLocation;
|
||||
import network.Messages.Enums.BoatLocationDeviceEnum;
|
||||
import shared.model.Azimuth;
|
||||
@@ -15,7 +16,7 @@ import static network.Utils.ByteConverter.*;
|
||||
/**
|
||||
* Decodes {@link BoatLocation} messages.
|
||||
*/
|
||||
public class BoatLocationDecoder {
|
||||
public class BoatLocationDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -32,20 +33,14 @@ public class BoatLocationDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public BoatLocationDecoder(byte[] encodedMessage) {
|
||||
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public BoatLocationDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
byte[] messageVersionNumberBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
|
||||
byte messageVersionNumber = messageVersionNumberBytes[0];
|
||||
@@ -152,6 +147,8 @@ public class BoatLocationDecoder {
|
||||
currentSet,
|
||||
rudderAngle );
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,19 +30,17 @@ public class BoatStatusDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public BoatStatusDecoder(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public BoatStatusDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
private void decode() {
|
||||
public BoatStatus decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
|
||||
byte[] sourceIDBytes = Arrays.copyOfRange(encodedMessage, 0, 4);
|
||||
@@ -75,6 +73,8 @@ public class BoatStatusDecoder {
|
||||
estTimeAtNextMark,
|
||||
estTimeAtFinish );
|
||||
|
||||
return message;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,63 +2,93 @@ package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.CourseWind;
|
||||
import shared.model.Bearing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static network.Utils.ByteConverter.bytesToInt;
|
||||
import static network.Utils.ByteConverter.bytesToLong;
|
||||
import static network.Utils.AC35UnitConverter.*;
|
||||
import static network.Utils.ByteConverter.*;
|
||||
|
||||
|
||||
/**
|
||||
* Created by hba56 on 23/04/17.
|
||||
* Decodes {@link CourseWind} messages.
|
||||
*/
|
||||
public class CourseWindDecoder {
|
||||
byte messageVersionNumber;
|
||||
byte byteWindID;
|
||||
byte loopCount;
|
||||
ArrayList<CourseWind> loopMessages = new ArrayList();
|
||||
|
||||
public CourseWindDecoder(byte[] encodedCourseWind) {
|
||||
final int lengthInBytesOfMessages = 20;
|
||||
/**
|
||||
* The encoded message.
|
||||
*/
|
||||
private byte[] encodedMessage;
|
||||
|
||||
messageVersionNumber = encodedCourseWind[0];
|
||||
byteWindID = encodedCourseWind[1];
|
||||
loopCount = encodedCourseWind[2];
|
||||
byte[] loopMessagesBytes = Arrays.copyOfRange(encodedCourseWind, 3, lengthInBytesOfMessages*loopCount+3);
|
||||
int messageLoopIndex = 0;
|
||||
/**
|
||||
* The decoded message.
|
||||
*/
|
||||
private CourseWind message;
|
||||
|
||||
for (int i=0; i < loopCount; i++) {
|
||||
byte[] messageBytes = Arrays.copyOfRange(loopMessagesBytes, messageLoopIndex, messageLoopIndex+20);
|
||||
ArrayList test = new ArrayList();
|
||||
byte[] windId = Arrays.copyOfRange(messageBytes, 0, 1);
|
||||
byte[] time = Arrays.copyOfRange(messageBytes, 1, 7);
|
||||
byte[] raceID = Arrays.copyOfRange(messageBytes, 7, 11);
|
||||
byte[] windDirection = Arrays.copyOfRange(messageBytes, 11, 13);
|
||||
byte[] windSpeed = Arrays.copyOfRange(messageBytes, 13, 15);
|
||||
byte[] bestUpwindAngle = Arrays.copyOfRange(messageBytes, 15, 17);
|
||||
byte[] bestDownwindAngle = Arrays.copyOfRange(messageBytes, 17, 19);
|
||||
byte[] flags = Arrays.copyOfRange(messageBytes, 19, 20);
|
||||
|
||||
CourseWind message = new CourseWind(windId[0], bytesToLong(time),
|
||||
bytesToInt(raceID), bytesToInt(windDirection),
|
||||
bytesToInt(windSpeed), bytesToInt(bestUpwindAngle),
|
||||
bytesToInt(bestDownwindAngle), flags[0]);
|
||||
|
||||
loopMessages.add(message);
|
||||
messageLoopIndex += 20;
|
||||
}
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
*/
|
||||
public CourseWindDecoder() {
|
||||
}
|
||||
|
||||
public ArrayList<CourseWind> getLoopMessages() {
|
||||
return loopMessages;
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public CourseWind decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
byte[] windId = Arrays.copyOfRange(encodedMessage, 0, 1);
|
||||
|
||||
byte[] timeBytes = Arrays.copyOfRange(encodedMessage, 1, 7);
|
||||
long time = bytesToLong(timeBytes);
|
||||
|
||||
byte[] raceIDBytes = Arrays.copyOfRange(encodedMessage, 7, 11);
|
||||
int raceIDInt = bytesToInt(raceIDBytes);
|
||||
|
||||
byte[] windDirectionBytes = Arrays.copyOfRange(encodedMessage, 11, 13);
|
||||
int windDirectionInt = bytesToInt(windDirectionBytes);
|
||||
Bearing windDirection = Bearing.fromDegrees(unpackHeading(windDirectionInt));
|
||||
|
||||
byte[] windSpeedBytes = Arrays.copyOfRange(encodedMessage, 13, 15);
|
||||
int windSpeedInt = bytesToInt(windSpeedBytes);
|
||||
double windSpeedKnots = unpackMMperSecToKnots(windSpeedInt);
|
||||
|
||||
byte[] bestUpwindAngleBytes = Arrays.copyOfRange(encodedMessage, 15, 17);
|
||||
int bestUpwindAngleInt = bytesToInt(bestUpwindAngleBytes);
|
||||
Bearing bestUpwindAngle = Bearing.fromDegrees(unpackHeading(bestUpwindAngleInt));
|
||||
|
||||
byte[] bestDownwindAngleBytes = Arrays.copyOfRange(encodedMessage, 17, 19);
|
||||
int bestDownwindAngleInt = bytesToInt(bestDownwindAngleBytes);
|
||||
Bearing bestDownwindAngle = Bearing.fromDegrees(unpackHeading(bestDownwindAngleInt));
|
||||
|
||||
byte[] flags = Arrays.copyOfRange(encodedMessage, 19, 20);
|
||||
|
||||
|
||||
|
||||
message = new CourseWind(
|
||||
windId[0],
|
||||
time,
|
||||
raceIDInt,
|
||||
windDirection,
|
||||
windSpeedKnots,
|
||||
bestUpwindAngle,
|
||||
bestDownwindAngle,
|
||||
flags[0] );
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public byte getMessageVersionNumber() {
|
||||
return messageVersionNumber;
|
||||
|
||||
/**
|
||||
* Returns the decoded message.
|
||||
* @return The decoded message.
|
||||
*/
|
||||
public CourseWind getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public byte getByteWindID() {
|
||||
return byteWindID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.CourseWind;
|
||||
import network.Messages.CourseWinds;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static network.Utils.ByteConverter.bytesToInt;
|
||||
import static network.Utils.ByteConverter.bytesToLong;
|
||||
|
||||
|
||||
/**
|
||||
* Decodes {@link CourseWinds} messages.
|
||||
*/
|
||||
public class CourseWindsDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
*/
|
||||
private byte[] encodedMessage;
|
||||
|
||||
/**
|
||||
* The decoded message.
|
||||
*/
|
||||
private CourseWinds message;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
*/
|
||||
public CourseWindsDecoder() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
//The header is three bytes.
|
||||
byte messageVersionNumber = encodedMessage[0];
|
||||
byte byteWindID = encodedMessage[1];
|
||||
byte loopCount = encodedMessage[2];
|
||||
|
||||
|
||||
//A CourseWind object is 20 bytes.
|
||||
final int courseWindByteLength = 20;
|
||||
|
||||
List<CourseWind> loopMessages = new ArrayList();
|
||||
|
||||
//The header is 3 bytes, so we need the remaining bytes.
|
||||
byte[] loopMessagesBytes = Arrays.copyOfRange(encodedMessage, 3, courseWindByteLength * loopCount + 3);
|
||||
|
||||
for (int messageLoopIndex = 0; messageLoopIndex < (loopCount * courseWindByteLength); messageLoopIndex += courseWindByteLength) {
|
||||
|
||||
byte[] messageBytes = Arrays.copyOfRange(loopMessagesBytes, messageLoopIndex, messageLoopIndex + courseWindByteLength);
|
||||
|
||||
CourseWindDecoder courseWindDecoder = new CourseWindDecoder();
|
||||
CourseWind courseWind = courseWindDecoder.decode(messageBytes);
|
||||
|
||||
loopMessages.add(courseWind);
|
||||
}
|
||||
|
||||
|
||||
|
||||
message = new CourseWinds(
|
||||
messageVersionNumber,
|
||||
byteWindID,
|
||||
loopMessages );
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the decoded message.
|
||||
* @return The decoded message.
|
||||
*/
|
||||
public CourseWinds getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Exceptions.InvalidMessageTypeException;
|
||||
import network.Messages.Enums.MessageType;
|
||||
|
||||
/**
|
||||
* Factory to create the appropriate decoder for a given message.
|
||||
*/
|
||||
public class DecoderFactory {
|
||||
|
||||
|
||||
/**
|
||||
* Private constructor. Currently doesn't need to be constructed.
|
||||
*/
|
||||
private DecoderFactory(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the correct type of decoder for a given message type.
|
||||
* @param type Type of message you want a decoder for.
|
||||
* @return The decoder.
|
||||
* @throws InvalidMessageTypeException If you pass in a {@link MessageType} that isn't recognised.
|
||||
*/
|
||||
public static MessageDecoder create(MessageType type) throws InvalidMessageTypeException {
|
||||
|
||||
|
||||
switch (type) {
|
||||
|
||||
case HEARTBEAT: return new HeartBeatDecoder();
|
||||
|
||||
case RACESTATUS: return new RaceStatusDecoder();
|
||||
|
||||
//case DISPLAYTEXTMESSAGE: return new DisplayTextMessageDecoder();//TODO
|
||||
|
||||
case XMLMESSAGE: return new XMLMessageDecoder();
|
||||
|
||||
//case RACESTARTSTATUS: return new RaceStartStatusDecoder();//TODO
|
||||
|
||||
//case YACHTEVENTCODE: return new YachtEventCodeDecoder();//TODO
|
||||
|
||||
//case YACHTACTIONCODE: return new YachtActionCodeDecoder();//TODO
|
||||
|
||||
//case CHATTERTEXT: return new ChatterTextDecoder();//TODO
|
||||
|
||||
case BOATLOCATION: return new BoatLocationDecoder();
|
||||
|
||||
//case MARKROUNDING: return new MarkRoundingDecoder()//TODO;
|
||||
|
||||
case COURSEWIND: return new CourseWindsDecoder();
|
||||
|
||||
//case AVGWIND: return new AverageWindDecoder()//TODO;
|
||||
|
||||
case REQUEST_TO_JOIN: return new RequestToJoinDecoder();
|
||||
|
||||
case JOIN_ACCEPTANCE: return new JoinAcceptanceDecoder();
|
||||
|
||||
case BOATACTION: return new BoatActionDecoder();
|
||||
|
||||
|
||||
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
import network.Messages.HeartBeat;
|
||||
|
||||
@@ -8,7 +9,7 @@ import static network.Utils.ByteConverter.bytesToLong;
|
||||
/**
|
||||
* Decodes {@link network.Messages.HeartBeat} messages.
|
||||
*/
|
||||
public class HeartBeatDecoder {
|
||||
public class HeartBeatDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -24,21 +25,17 @@ public class HeartBeatDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public HeartBeatDecoder(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public HeartBeatDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
message = new HeartBeat(bytesToLong(encodedMessage));
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.JoinAcceptanceEnum;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import network.Utils.ByteConverter;
|
||||
@@ -10,7 +11,7 @@ import java.util.Arrays;
|
||||
/**
|
||||
* Decoder for {@link JoinAcceptance} messages.
|
||||
*/
|
||||
public class JoinAcceptanceDecoder {
|
||||
public class JoinAcceptanceDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -25,19 +26,14 @@ public class JoinAcceptanceDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public JoinAcceptanceDecoder(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public JoinAcceptanceDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
//SourceID is first four bytes.
|
||||
byte[] sourceIdBytes = Arrays.copyOfRange(encodedMessage, 0, 4);
|
||||
@@ -55,6 +51,8 @@ public class JoinAcceptanceDecoder {
|
||||
|
||||
|
||||
message = new JoinAcceptance(acceptanceType, sourceID);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
|
||||
|
||||
/**
|
||||
* This is the interface that all message decoders must implement.
|
||||
* It allows for {@link #decode(byte[])}ing messages.
|
||||
*/
|
||||
public interface MessageDecoder {
|
||||
|
||||
|
||||
/**
|
||||
* Decodes a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
* @return The decoded message.
|
||||
*/
|
||||
public AC35Data decode(byte[] encodedMessage);
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatStatus;
|
||||
import network.Messages.Enums.RaceStatusEnum;
|
||||
import network.Messages.Enums.RaceTypeEnum;
|
||||
@@ -20,7 +21,7 @@ import static network.Utils.ByteConverter.bytesToShort;
|
||||
/**
|
||||
* Decodes {@link RaceStatus} messages.
|
||||
*/
|
||||
public class RaceStatusDecoder {
|
||||
public class RaceStatusDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -36,19 +37,14 @@ public class RaceStatusDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public RaceStatusDecoder(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public RaceStatusDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
|
||||
byte[] versionNumBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
|
||||
@@ -84,15 +80,18 @@ public class RaceStatusDecoder {
|
||||
List<BoatStatus> boatStatuses = new ArrayList<>();
|
||||
|
||||
|
||||
//BoatStatus is 20 bytes.
|
||||
int boatStatusByteLength = 20;
|
||||
|
||||
|
||||
//Decode each BoatStatus.
|
||||
for (int boatLoopIndex=0; boatLoopIndex < (numberOfBoats * 20); boatLoopIndex += 20) {
|
||||
for (int boatLoopIndex = 0; boatLoopIndex < (numberOfBoats * boatStatusByteLength); boatLoopIndex += boatStatusByteLength) {
|
||||
|
||||
byte[] boatStatusBytes = Arrays.copyOfRange(boatStatusesBytes, boatLoopIndex, boatLoopIndex + 20);
|
||||
byte[] boatStatusBytes = Arrays.copyOfRange(boatStatusesBytes, boatLoopIndex, boatLoopIndex + boatStatusByteLength);
|
||||
|
||||
BoatStatusDecoder boatStatusDecoder = new BoatStatusDecoder(boatStatusBytes);
|
||||
BoatStatusDecoder boatStatusDecoder = new BoatStatusDecoder();
|
||||
|
||||
boatStatuses.add(boatStatusDecoder.getMessage());
|
||||
boatStatuses.add(boatStatusDecoder.decode(boatStatusBytes));
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +105,8 @@ public class RaceStatusDecoder {
|
||||
windSpeedKnots,
|
||||
raceType,
|
||||
boatStatuses );
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.RequestToJoinEnum;
|
||||
import network.Messages.RequestToJoin;
|
||||
import network.Utils.ByteConverter;
|
||||
@@ -10,7 +11,7 @@ import java.util.Arrays;
|
||||
/**
|
||||
* Decoder for {@link network.Messages.RequestToJoin} messages.
|
||||
*/
|
||||
public class RequestToJoinDecoder {
|
||||
public class RequestToJoinDecoder implements MessageDecoder{
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -24,19 +25,14 @@ public class RequestToJoinDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedRequest The message to decode.
|
||||
*/
|
||||
public RequestToJoinDecoder(byte[] encodedRequest) {
|
||||
this.encodedRequest = encodedRequest;
|
||||
|
||||
decode();
|
||||
public RequestToJoinDecoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedRequest) {
|
||||
this.encodedRequest = encodedRequest;
|
||||
|
||||
//Request type is first four bytes.
|
||||
byte[] requestTypeBytes = Arrays.copyOfRange(encodedRequest, 0, 4);
|
||||
@@ -47,6 +43,8 @@ public class RequestToJoinDecoder {
|
||||
|
||||
|
||||
message = new RequestToJoin(requestType);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.XMLMessageType;
|
||||
import network.Messages.XMLMessage;
|
||||
|
||||
@@ -12,7 +13,7 @@ import static network.Utils.ByteConverter.bytesToShort;
|
||||
/**
|
||||
* Decodes {@link network.Messages.XMLMessage} messages.
|
||||
*/
|
||||
public class XMLMessageDecoder {
|
||||
public class XMLMessageDecoder implements MessageDecoder {
|
||||
|
||||
/**
|
||||
* The encoded message.
|
||||
@@ -28,18 +29,14 @@ public class XMLMessageDecoder {
|
||||
|
||||
/**
|
||||
* Constructs a decoder to decode a given message.
|
||||
* @param encodedMessage The message to decode.
|
||||
*/
|
||||
public XMLMessageDecoder(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
decode();
|
||||
public XMLMessageDecoder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the contained message.
|
||||
*/
|
||||
private void decode() {
|
||||
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) {
|
||||
this.encodedMessage = encodedMessage;
|
||||
|
||||
byte[] messageVersionNumberBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
|
||||
byte[] ackNumberBytes = Arrays.copyOfRange(encodedMessage, 1, 3);
|
||||
@@ -66,6 +63,8 @@ public class XMLMessageDecoder {
|
||||
xmlMsgSubType,
|
||||
sequenceNumber,
|
||||
xmlMessage );
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class BoatActionEncoder implements MessageEncoder {
|
||||
//Message is 1 byte.
|
||||
ByteBuffer boatActionMessage = ByteBuffer.allocate(1);
|
||||
|
||||
boatActionMessage.put(intToBytes(boatAction.getBoatAction(), 1));
|
||||
boatActionMessage.put(intToBytes(boatAction.getBoatAction().getValue(), 1));
|
||||
|
||||
byte [] result = boatActionMessage.array();
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package network.MessageEncoders;
|
||||
|
||||
|
||||
import network.Messages.CourseWind;
|
||||
import shared.model.Bearing;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static network.Utils.AC35UnitConverter.*;
|
||||
import static network.Utils.ByteConverter.*;
|
||||
import static network.Utils.ByteConverter.bytesToInt;
|
||||
|
||||
/**
|
||||
* This encoder can encode a {@link CourseWind} message.
|
||||
*/
|
||||
public class CourseWindEncoder {
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public CourseWindEncoder() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes a given CourseWind message.
|
||||
* @param message The message to encode.
|
||||
* @return The encoded message.
|
||||
*/
|
||||
public byte[] encode(CourseWind message) {
|
||||
|
||||
CourseWind courseWind = message;
|
||||
|
||||
|
||||
//CourseWind is 20 bytes.
|
||||
ByteBuffer courseWindBuffer = ByteBuffer.allocate(20);
|
||||
|
||||
|
||||
byte[] windId = intToBytes(courseWind.getID(), 1);
|
||||
|
||||
byte[] timeBytes = longToBytes(courseWind.getTime(), 6);
|
||||
|
||||
byte[] raceIDBytes = intToBytes(courseWind.getRaceID(), 4);
|
||||
|
||||
int windDirectionInt = packHeading(courseWind.getWindDirection().degrees());
|
||||
byte[] windDirectionBytes = intToBytes(windDirectionInt, 2);
|
||||
|
||||
int windSpeedInt = packKnotsToMMperSec(courseWind.getWindSpeedKnots());
|
||||
byte[] windSpeedBytes = intToBytes(windSpeedInt, 2);
|
||||
|
||||
int bestUpwindAngleInt = packHeading(courseWind.getBestUpwindAngle().degrees());
|
||||
byte[] bestUpwindAngleBytes = intToBytes(bestUpwindAngleInt, 2);
|
||||
|
||||
int bestDownwindAngleInt = packHeading(courseWind.getBestDownwindAngle().degrees());
|
||||
byte[] bestDownwindAngleBytes = intToBytes(bestDownwindAngleInt, 2);
|
||||
|
||||
byte[] flags = intToBytes(courseWind.getFlags(), 1);
|
||||
|
||||
courseWindBuffer.put(windId);
|
||||
courseWindBuffer.put(timeBytes);
|
||||
courseWindBuffer.put(raceIDBytes);
|
||||
courseWindBuffer.put(windDirectionBytes);
|
||||
courseWindBuffer.put(windSpeedBytes);
|
||||
courseWindBuffer.put(bestUpwindAngleBytes);
|
||||
courseWindBuffer.put(bestDownwindAngleBytes);
|
||||
courseWindBuffer.put(flags);
|
||||
|
||||
return courseWindBuffer.array();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package network.MessageEncoders;
|
||||
|
||||
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatAction;
|
||||
import network.Messages.CourseWind;
|
||||
import network.Messages.CourseWinds;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static network.Utils.ByteConverter.intToBytes;
|
||||
|
||||
/**
|
||||
* This encoder can encode a {@link CourseWinds} message.
|
||||
*/
|
||||
public class CourseWindsEncoder implements MessageEncoder {
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public CourseWindsEncoder() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] encode(AC35Data message) {
|
||||
|
||||
//Downcast.
|
||||
CourseWinds courseWinds = (CourseWinds) message;
|
||||
|
||||
|
||||
byte messageVersionNumber = CourseWinds.currentMessageVersionNumber;
|
||||
|
||||
byte byteWindID = courseWinds.getSelectedWindID();
|
||||
|
||||
byte[] loopcount = intToBytes(courseWinds.getCourseWinds().size(), 1);
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(3 + 20 * courseWinds.getCourseWinds().size());
|
||||
|
||||
result.put(messageVersionNumber);
|
||||
result.put(byteWindID);
|
||||
result.put(loopcount);
|
||||
|
||||
//Encode each CourseWind.
|
||||
for (CourseWind wind: courseWinds.getCourseWinds()){
|
||||
|
||||
CourseWindEncoder courseWindEncoder = new CourseWindEncoder();
|
||||
byte[] encodedCourseWind = courseWindEncoder.encode(wind);
|
||||
|
||||
result.put(encodedCourseWind);
|
||||
}
|
||||
return result.array();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public class EncoderFactory {
|
||||
|
||||
//case MARKROUNDING: return new MarkRoundingEncoder();//TODO
|
||||
|
||||
//case COURSEWIND: return new CourseWindEncoder();//TODO
|
||||
case COURSEWIND: return new CourseWindsEncoder();
|
||||
|
||||
//case AVGWIND: return new AverageWindEncoder();//TODO
|
||||
|
||||
|
||||
@@ -143,26 +143,7 @@ public class RaceVisionByteEncoder {
|
||||
return result.array();
|
||||
}
|
||||
|
||||
public static byte[] courseWind(byte windID, ArrayList<CourseWind> courseWinds){
|
||||
int messageVersionNumber = 0b1;
|
||||
byte byteWindID = windID;
|
||||
byte[] loopcount = intToBytes(courseWinds.size(), 1);
|
||||
ByteBuffer result = ByteBuffer.allocate(3 + 20 * courseWinds.size());
|
||||
result.put(intToBytes(messageVersionNumber, 1));
|
||||
result.put(byteWindID);
|
||||
result.put(loopcount);
|
||||
for (CourseWind wind: courseWinds){
|
||||
result.put(intToBytes(wind.getID(), 1));
|
||||
result.put(longToBytes(wind.getTime(), 6));
|
||||
result.put(intToBytes(wind.getRaceID(), 4));
|
||||
result.put(intToBytes(wind.getWindDirection(), 2));
|
||||
result.put(intToBytes(wind.getWindSpeed(), 2));
|
||||
result.put(intToBytes(wind.getBestUpwindAngle(), 2));
|
||||
result.put(intToBytes(wind.getBestDownwindAngle(), 2));
|
||||
result.put(intToBytes(wind.getFlags(), 1));
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
|
||||
public static byte[] averageWind(int time, int rawPeriod, int rawSampleSpeed, int period2, int speed2, int period3, int speed3, int period4, int speed4){
|
||||
int messageVersionNumber = 0b1;
|
||||
|
||||
@@ -4,19 +4,30 @@ import network.Messages.Enums.BoatActionEnum;
|
||||
import network.Messages.Enums.MessageType;
|
||||
|
||||
/**
|
||||
* Created by David on 10/07/2017.
|
||||
* Represents a BoatAction message.
|
||||
*/
|
||||
public class BoatAction extends AC35Data {
|
||||
|
||||
private byte boatAction;
|
||||
/**
|
||||
* The action for this message.
|
||||
*/
|
||||
private BoatActionEnum boatAction;
|
||||
|
||||
/**
|
||||
* Constructs a BoatActon message with a given action.
|
||||
* @param boatAction Action to use.
|
||||
*/
|
||||
public BoatAction(BoatActionEnum boatAction){
|
||||
super(MessageType.BOATACTION);
|
||||
this.boatAction = boatAction.getValue();
|
||||
this.boatAction = boatAction;
|
||||
}
|
||||
|
||||
public byte getBoatAction() {
|
||||
/**
|
||||
* Returns the action for this message.
|
||||
* @return The action for this message.
|
||||
*/
|
||||
public BoatActionEnum getBoatAction() {
|
||||
return boatAction;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,57 +2,132 @@ package network.Messages;
|
||||
|
||||
|
||||
import network.Messages.Enums.MessageType;
|
||||
import shared.model.Bearing;
|
||||
|
||||
/**
|
||||
* Created by fwy13 on 21/04/17.
|
||||
* Contains a single CourseWind record.
|
||||
* A CourseWinds message contains one or more CourseWind messages.
|
||||
*/
|
||||
public class CourseWind extends AC35Data {
|
||||
|
||||
private int ID, raceID, windDirection, windSpeed, bestUpwindAngle, bestDownwindAngle, flags;
|
||||
/**
|
||||
* The ID for this wind source.
|
||||
*/
|
||||
private int ID;
|
||||
|
||||
/**
|
||||
* The time the wind was captured at. Milliseconds since unix epoch.
|
||||
*/
|
||||
private long time;
|
||||
|
||||
public CourseWind(int ID, long time, int raceID, int windDirection, int windSpeed, int bestUpwindAngle, int bestDownwindAngle,
|
||||
int flags){
|
||||
/**
|
||||
* The ID of the race this applies to.
|
||||
* 0 means it isn't race specific.
|
||||
*/
|
||||
private int raceID;
|
||||
|
||||
/**
|
||||
* Direction of the wind.
|
||||
*/
|
||||
private Bearing windDirection;
|
||||
|
||||
/**
|
||||
* The speed of the wind, in knots.
|
||||
*/
|
||||
private double windSpeedKnots;
|
||||
|
||||
/**
|
||||
* Optimum upwind sailing angle.
|
||||
*/
|
||||
private Bearing bestUpwindAngle;
|
||||
|
||||
/**
|
||||
* Optimum downwind sailing angle.
|
||||
*/
|
||||
private Bearing bestDownwindAngle;
|
||||
|
||||
/**
|
||||
* Various flags which determine which values are valid.
|
||||
*/
|
||||
private short flags;
|
||||
|
||||
|
||||
|
||||
public CourseWind(int ID, long time, int raceID, Bearing windDirection, double windSpeedKnots, Bearing bestUpwindAngle, Bearing bestDownwindAngle, short flags) {
|
||||
super(MessageType.COURSEWIND);
|
||||
this.ID = ID;
|
||||
this.time = time;
|
||||
this.raceID = raceID;
|
||||
this.windDirection = windDirection;
|
||||
this.windSpeed = windSpeed;
|
||||
this.windSpeedKnots = windSpeedKnots;
|
||||
this.bestUpwindAngle = bestUpwindAngle;
|
||||
this.bestDownwindAngle = bestDownwindAngle;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ID of the wind source.
|
||||
* @return ID of the wind source.
|
||||
*/
|
||||
public int getID() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time that this was captured at. Milliseconds since unix epoch.
|
||||
* @return Time this wind was captured at.
|
||||
*/
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the race this wind source belongs to. 0 means any race.
|
||||
* @return ID of the race this belongs to.
|
||||
*/
|
||||
public int getRaceID() {
|
||||
return raceID;
|
||||
}
|
||||
|
||||
public int getWindDirection() {
|
||||
/**
|
||||
* Returns the direction of the wind.
|
||||
* @return The direction of the wind.
|
||||
*/
|
||||
public Bearing getWindDirection() {
|
||||
return windDirection;
|
||||
}
|
||||
|
||||
public int getWindSpeed() {
|
||||
return windSpeed;
|
||||
/**
|
||||
* Returns the wind speed, in knots.
|
||||
* @return Wind speed, in knots.
|
||||
*/
|
||||
public double getWindSpeedKnots() {
|
||||
return windSpeedKnots;
|
||||
}
|
||||
|
||||
public int getBestUpwindAngle() {
|
||||
/**
|
||||
* Returns the best upwind sailing angle.
|
||||
* @return Best upwind sailing angle.
|
||||
*/
|
||||
public Bearing getBestUpwindAngle() {
|
||||
return bestUpwindAngle;
|
||||
}
|
||||
|
||||
public int getBestDownwindAngle() {
|
||||
/**
|
||||
* Returns the best downwind sailing angle.
|
||||
* @return The best downwind sailing angle.
|
||||
*/
|
||||
public Bearing getBestDownwindAngle() {
|
||||
return bestDownwindAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns various flags which determine which values are valid.
|
||||
* @return Flag which determines which values are valid.
|
||||
*/
|
||||
public int getFlags() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,67 @@ import network.Messages.Enums.MessageType;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by fwy13 on 25/04/17.
|
||||
* Represents the information in a CourseWind message (AC streaming spec: 4.11).
|
||||
*/
|
||||
public class CourseWinds extends AC35Data {
|
||||
|
||||
private int msgVerNum;
|
||||
private int selectedWindID;
|
||||
/**
|
||||
* The current version number for this message type.
|
||||
*/
|
||||
public static final byte currentMessageVersionNumber = 1;
|
||||
|
||||
|
||||
/**
|
||||
* The version number of this message.
|
||||
*/
|
||||
private byte messageVersionNumber;
|
||||
|
||||
/**
|
||||
* The ID of the wind source currently selected.
|
||||
*/
|
||||
private byte selectedWindID;
|
||||
|
||||
/**
|
||||
* A list of wind sources.
|
||||
*/
|
||||
private List<CourseWind> courseWinds;
|
||||
|
||||
public CourseWinds(int msgVerNum, int selectedWindID, List<CourseWind> courseWinds){
|
||||
|
||||
/**
|
||||
* Constructs a CourseWinds with given parameters.
|
||||
* @param messageVersionNumber The version number of the message.
|
||||
* @param selectedWindID The selected wind ID.
|
||||
* @param courseWinds A list of wind sources.
|
||||
*/
|
||||
public CourseWinds(byte messageVersionNumber, byte selectedWindID, List<CourseWind> courseWinds) {
|
||||
super(MessageType.COURSEWIND);
|
||||
this.msgVerNum = msgVerNum;
|
||||
this.messageVersionNumber = messageVersionNumber;
|
||||
this.selectedWindID = selectedWindID;
|
||||
this.courseWinds = courseWinds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the version number of this message.
|
||||
* @return Version number of this message.
|
||||
*/
|
||||
public byte getMessageVersionNumber() {
|
||||
return messageVersionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the selected wind source.
|
||||
* @return ID of the selected wind source.
|
||||
*/
|
||||
public byte getSelectedWindID() {
|
||||
return selectedWindID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of wind sources.
|
||||
* @return List of wind sources.
|
||||
*/
|
||||
public List<CourseWind> getCourseWinds() {
|
||||
return courseWinds;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user