Last active
April 5, 2023 18:29
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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