Skip to content

Instantly share code, notes, and snippets.

@alwarren
Last active April 5, 2023 18:29
Show Gist options
  • Save alwarren/3214e29752ae6fa808b671cac7117442 to your computer and use it in GitHub Desktop.
Save alwarren/3214e29752ae6fa808b671cac7117442 to your computer and use it in GitHub Desktop.
Converting Metar data in CSV format to JSON using NOAA's Text Data Server
object Api {
private const val endpoint = "https://www.aviationweather.gov/adds/dataserver_current/"
private val scalarConverter by lazy { ScalarsConverterFactory.create() }
private val jsonConverter by lazy { GsonConverterFactory.create() }
private val client by lazy {
OkHttpClient.Builder()
.addInterceptor { chain ->
val original: Request = chain.request()
val originalHttpUrl: HttpUrl = original.url()
val url = originalHttpUrl.newBuilder()
.addQueryParameter("dataSource", "metars")
.addQueryParameter("requestType", "retrieve")
.addQueryParameter("format", "csv")
.build()
val requestBuilder: Request.Builder = original.newBuilder()
.url(url)
val request: Request = requestBuilder.build()
chain.proceed(request)
}
.addInterceptor(MetarServiceInterceptor())
.build()
}
fun <T> service(clazz: Class<T>): T =
Retrofit.Builder()
.baseUrl(endpoint)
.client(client)
.addConverterFactory(scalarConverter)
.addConverterFactory(jsonConverter)
.build().create(clazz)
}
object Messages {
private const val BUNDLE_NAME = "internationalization.messages" //$NON-NLS-1$
private var fResourceBundle: ResourceBundle
init {
fResourceBundle = ResourceBundle.getBundle(BUNDLE_NAME)
}
fun setLocale(locale: Locale) {
Locale.setDefault(locale)
ResourceBundle.clearCache()
fResourceBundle = ResourceBundle.getBundle(BUNDLE_NAME, locale)
}
fun getString(message: String): String {
return fResourceBundle.getString(message)
}
fun getString(message: String, vararg arguments: Any?): String? {
return MessageFormat.format(getString(message), *arguments)
}
}
# Allowed values: SKC|CLR|CAVOK|FEW|SCT|BKN|OVC|OVX
CloudQuantity.SKC=Clear
CloudQuantity.CLR=Clear
CloudQuantity.CAVOK=Ceiling and visibility OK
CloudQuantity.FEW=Few
CloudQuantity.SCT=Scattered
CloudQuantity.BKN=Broken
CloudQuantity.NSC=No significant clouds.
CloudQuantity.OVC=Overcast
CloudQuantity.OVX=Obscured
SkyCondition.message={0} at {1, number, integer}
SkyCondition.title=Sky Condition
MetarView.title=Station {0}
MetarView.noStation=No Station
Message.noData=No Data
data class Metar(
val rawText: String?,
val stationId: String?,
val observationTime: String?,
val latitude: Float?, // float
val longitude: Float?, // float
val tempC: Float?, // float
val dewpointC: Float?, // float
val windDirDegrees: Int?, // int
val windSpeedKt: Int?, // int
val windGustKt: Int?, // int
val visibilityStatuteMi: Float?, // float
val altimInHg: Float?, // float
val seaLevelPressureMb: Float?, // float
val qualityControlFlags: QualityControlFlags?,
val wxString: String?,
val skyCondition: List<SkyCondition>?,
val flightCategory: String?,
val threeHrPressureTendencyMb: Float?, // float
val maxTC: Float?, // float
val minTC: Float?, // float
val maxT24hrC: Float?, // float
val minT24hrC: Float?, // float
val precipIn: Float?, // float
val pcp3hrIn: Float?, // float
val pcp6hrIn: Float?, // float
val pcp24hrIn: Float?, // float
val snowIn: Float?, // float
val vertVisFt: Float?, // float
val metarType: String?,
val elevationM: Float? // float
) {
override fun toString(): String {
return rawText ?: "No Data"
}
}
object MetarJsonConverter {
fun encode(responseText: String): String {
val lines = responseText.trim().split("\n")
val gson = GsonBuilder().setPrettyPrinting().create()
validateResponseText(lines)
val json = getJson(lines)
return gson.toJson(json).toString()
}
private fun getJson(lines: List<String>): JsonObject {
val json = JsonObject()
json.addProperty("errors", lines[0])
json.addProperty("warnings", lines[1])
json.addProperty("timeTaken", lines[2])
json.addProperty("dataSource", lines[3])
json.addProperty("numResults", lines[4])
json.add("data", getData(lines))
return json
}
private fun getData(lines: List<String>): JsonArray {
val metars = getMetars(lines)
val jsonArray = JsonArray()
metars.forEach { metar ->
jsonArray.add(metar(metar))
}
return jsonArray
}
private fun getMetars(lines: List<String>): List<Metar> {
val indices = 6 until lines.size
val metars = mutableListOf<Metar>()
for (i in indices)
metars.add(CsvMapper.decode(lines[i]))
return metars
}
private fun metar(metar: Metar): JsonObject {
val jsonObject = JsonObject()
jsonObject.addProperty("rawText", metar.rawText)
jsonObject.addProperty("stationId", metar.stationId)
jsonObject.addProperty("observationTime", metar.observationTime)
jsonObject.addProperty("latitude", metar.latitude)
jsonObject.addProperty("longitude", metar.longitude)
jsonObject.addProperty("tempC", metar.tempC)
jsonObject.addProperty("dewpointC", metar.dewpointC)
jsonObject.addProperty("windDirDegrees", metar.windDirDegrees)
jsonObject.addProperty("windSpeedKt", metar.windSpeedKt)
jsonObject.addProperty("windGustKt", metar.windGustKt)
jsonObject.addProperty("visibilityStatuteMi", metar.visibilityStatuteMi)
jsonObject.addProperty("altimInHg", metar.altimInHg)
jsonObject.addProperty("seaLevelPressureMb", metar.seaLevelPressureMb)
jsonObject.add("qualityControlFlags", qualityControlFlags(metar.qualityControlFlags))
jsonObject.addProperty("wxString", metar.wxString)
jsonObject.add("skyCondition", skyConditions(metar))
jsonObject.addProperty("flightCategory", metar.flightCategory)
jsonObject.addProperty("threeHrPressureTendencyMb", metar.threeHrPressureTendencyMb)
jsonObject.addProperty("maxTC", metar.maxTC)
jsonObject.addProperty("minTC", metar.minTC)
jsonObject.addProperty("maxT24hrC", metar.maxT24hrC)
jsonObject.addProperty("minT24hrC", metar.minT24hrC)
jsonObject.addProperty("precipIn", metar.precipIn)
jsonObject.addProperty("pcp3hrIn", metar.pcp3hrIn)
jsonObject.addProperty("pcp6hrIn", metar.pcp6hrIn)
jsonObject.addProperty("pcp24hrIn", metar.pcp24hrIn)
jsonObject.addProperty("snowIn", metar.snowIn)
jsonObject.addProperty("vertVisFt", metar.vertVisFt)
jsonObject.addProperty("metarType", metar.metarType)
jsonObject.addProperty("elevationM", metar.elevationM)
return jsonObject
}
private fun qualityControlFlags(flags: QualityControlFlags?): JsonObject {
val jsonObject = JsonObject()
jsonObject.addProperty("autoStation", flags?.autoStation)
jsonObject.addProperty("auto", flags?.auto)
jsonObject.addProperty("presentWeatherSensorOff", flags?.presentWeatherSensorOff)
jsonObject.addProperty("noSignal", flags?.noSignal)
jsonObject.addProperty("maintenanceIndicatorOn", flags?.maintenanceIndicatorOn)
jsonObject.addProperty("lightningSensorOff", flags?.lightningSensorOff)
jsonObject.addProperty("freezingRainSensorOff", flags?.freezingRainSensorOff)
jsonObject.addProperty("corrected", flags?.corrected)
return jsonObject
}
private fun skyConditions(metar: Metar): JsonArray {
val conditions = JsonArray()
metar.skyCondition?.forEach { condition ->
val jsonObject = JsonObject()
jsonObject.addProperty("skyCover", condition.skyCover)
jsonObject.addProperty("cloudBaseFtAgl", condition.cloudBaseFtAgl)
conditions.add(jsonObject)
}
return conditions
}
private fun validateResponseText(lines: List<String>) {
when {
lines.isEmpty() -> throw FileParseException("Empty server response")
lines.size < 5 -> throw FileParseException("Invalid server response")
lines.size == 6 -> throw FileParseException("Missing server data")
}
}
class FileParseException(message:String): Exception(message)
}
class MetarServiceInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val source = response.body()!!.source()
source.request(Long.MAX_VALUE) // Buffer the entire body.
val buffer: Buffer = source.buffer()
val responseBodyString: String = buffer.clone().readString(Charset.forName("UTF-8"))
if (response.code() == 200) {
val metarResponse = MetarJsonConverter.encode(responseBodyString)
val contentType = MediaType.parse("application/json; charset=utf-8")
val body = ResponseBody.create(contentType, metarResponse)
return response.newBuilder().body(body).build()
}
return response
}
}
data class QualityControlFlags (
val autoStation: String?,
val auto: String?,
val presentWeatherSensorOff: String?,
val noSignal: String?,
val maintenanceIndicatorOn: String?,
val lightningSensorOff: String?,
val freezingRainSensorOff: String?,
val corrected: String?
)
data class SkyCondition (
val skyCover: String,
val cloudBaseFtAgl: Int? = null
) {
override fun toString(): String {
val cover = Messages.getString("CloudQuantity.$skyCover")
return if (cloudBaseFtAgl == null)
cover
else
return Messages.getString("SkyCondition.message", cover, cloudBaseFtAgl) ?: "Bad Message Format"
}
}
{
"errors": "No errors",
"warnings": "No warnings",
"timeTaken": "5 ms",
"dataSource": "data source\u003dmetars",
"numResults": "2 results",
"data": [
{
"rawText": "KDAL 201853Z 19012G21KT 10SM SCT037TCU BKN060 29/19 A2995 RMK AO2 SLP136 TCU N NE S-SW T02940194",
"stationId": "KDAL",
"observationTime": "2020-10-20T18:53:00Z",
"latitude": 32.85,
"longitude": -96.85,
"tempC": 29.4,
"dewpointC": 19.4,
"windDirDegrees": 190,
"windSpeedKt": 12,
"windGustKt": 21,
"visibilityStatuteMi": 10.0,
"altimInHg": 29.949802,
"seaLevelPressureMb": 1013.6,
"qualityControlFlags": {
"autoStation": "",
"auto": "",
"presentWeatherSensorOff": "TRUE",
"noSignal": "",
"maintenanceIndicatorOn": "",
"lightningSensorOff": "",
"freezingRainSensorOff": "",
"corrected": ""
},
"wxString": "",
"skyCondition": [
{
"skyCover": "SCT",
"cloudBaseFtAgl": 3700
},
{
"skyCover": "BKN",
"cloudBaseFtAgl": 6000
}
],
"flightCategory": "",
"pcp3hrIn": 3700.0,
"pcp24hrIn": 6000.0,
"metarType": ""
},
{
"rawText": "KDFW 201853Z 18018KT 10SM SCT038 SCT050 29/20 A2994 RMK AO2 PK WND 18026/1813 SLP130 T02940200",
"stationId": "KDFW",
"observationTime": "2020-10-20T18:53:00Z",
"latitude": 32.9,
"longitude": -97.02,
"tempC": 29.4,
"dewpointC": 20.0,
"windDirDegrees": 180,
"windSpeedKt": 18,
"visibilityStatuteMi": 10.0,
"altimInHg": 29.940945,
"seaLevelPressureMb": 1013.0,
"qualityControlFlags": {
"autoStation": "",
"auto": "",
"presentWeatherSensorOff": "TRUE",
"noSignal": "",
"maintenanceIndicatorOn": "",
"lightningSensorOff": "",
"freezingRainSensorOff": "",
"corrected": ""
},
"wxString": "",
"skyCondition": [
{
"skyCover": "SCT",
"cloudBaseFtAgl": 3800
},
{
"skyCover": "SCT",
"cloudBaseFtAgl": 5000
}
],
"flightCategory": "",
"pcp3hrIn": 3800.0,
"pcp24hrIn": 5000.0,
"metarType": ""
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment