diff --git a/DESCRIPTION b/DESCRIPTION index 4d86895..ad452f9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,6 +35,7 @@ Imports: curl, data.table, httr, + R6, stringi, XML Suggests: diff --git a/NAMESPACE b/NAMESPACE index c01f4c3..62160bf 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,12 +20,14 @@ export(nearest_stations_noaa) export(nearest_stations_ogimet) export(ogimet_daily) export(ogimet_hourly) +export(parser) export(sounding_wyoming) export(spheroid_dist) export(stations_hydro_imgw_telemetry) export(stations_meteo_imgw_telemetry) export(stations_ogimet) export(test_url) +import(R6) import(data.table) import(httr) importFrom(XML,readHTMLTable) diff --git a/R/parser.R b/R/parser.R new file mode 100644 index 0000000..bbcfd99 --- /dev/null +++ b/R/parser.R @@ -0,0 +1,2126 @@ +#' Parse SYNOP messages into structured lists +#' +#' This function wraps the SYNOP decoding logic that was previously distributed +#' with the package in `inst/extdata`. It parses one or more SYNOP messages and +#' returns their structured representation as generated by the `SYNOP` R6 +#' decoder. +#' +#' @param message Character vector with SYNOP messages. +#' @param country Optional single character value passed to the precipitation +#' indicator decoder to adjust country-specific behaviour (e.g. `"RU"`). +#' @param simplify Logical. If `TRUE` (default) and a single message is +#' provided, the function returns the decoded list directly instead of a +#' length-one list. +#' +#' @return A list of decoded SYNOP messages. When `simplify = TRUE` and a single +#' message is supplied, the corresponding decoded list is returned directly. +#' @examples +#' parser("AAXX 01004 88889 12782 61506 10094 20047 30111 40197 53007 60001 81541") +#' parser(rep("AAXX 01004 88889 12782 61506 10094 20047 30111 40197 53007 60001 81541", 2), simplify = FALSE) +#' @import R6 +#' @export +parser <- function(message, country = NULL, simplify = TRUE) { + if (missing(message) || length(message) == 0) { + stop("`message` must contain at least one SYNOP string.") + } + + if (!is.character(message)) { + stop("`message` must be a character vector.") + } + + if (!is.null(country) && !(is.character(country) && length(country) %in% c(1, length(message)))) { + stop("`country` must be NULL, a single string, or a character vector matching the length of `message`.") + } + + country_vec <- if (is.null(country)) rep(list(NULL), length(message)) else as.list(rep(country, length.out = length(message))) + + results <- mapply( + function(msg, cntry) { + msg <- trimws(msg) + if (nzchar(msg)) { + synop <- SYNOP$new() + synop$country <- cntry + synop$decode(msg) + } else { + warning("Empty SYNOP message supplied; returning NULL.") + NULL + } + }, + message, + country_vec, + SIMPLIFY = FALSE + ) + + if (simplify && length(results) == 1) { + return(results[[1]]) + } + + results +} + +################################################################################ +# observations.R +# +# Observation classes from SYNOP - R version +# +# This is an R port of pymetdecoder/synop/observations.py +# Adapted from Python to R using R6 classes and functional approach +################################################################################ + +################################################################################ +# BASE CLASSES +################################################################################ + +# Base Observation class +Observation <- R6Class("Observation", + public = list( + null_char = "/", + code_len = NULL, + code_table = NULL, + unit = NULL, + valid_range = NULL, + + initialize = function(null_char = "/") { + self$null_char <- null_char + }, + + # Check if value is available (not all null chars) + is_available = function(value, char = NULL) { + if (is.null(char)) char <- self$null_char + if (is.null(value)) return(FALSE) + value_str <- as.character(value) + !all(strsplit(value_str, "")[[1]] == char) + }, + + # Check if value is valid + is_valid = function(value, raise_exception = TRUE, name = NULL, ...) { + tryCatch({ + valid <- private$check_valid(value, ...) + if (!valid && raise_exception) { + stop(paste0(value, " is not a valid code for ", ifelse(is.null(name), class(self)[1], name))) + } + valid + }, error = function(e) { + if (raise_exception) { + stop(e) + } + FALSE + }, warning = function(w) { + if (raise_exception) { + stop(w) + } + FALSE + }) + }, + + # Decode raw value + decode = function(raw, ...) { + kwargs <- list(...) + + # Check if available + if (!self$is_available(raw)) { + return(NULL) + } + + # Check if valid + if (!self$is_valid(raw, raise_exception = FALSE, ...)) { + return(NULL) + } + + # Decode + tryCatch({ + self$decode_internal(raw, ...) + }, error = function(e) { + warning(paste("Unable to decode:", raw)) + NULL + }) + }, + + # Encode observation + encode = function(data, ...) { + kwargs <- list(...) + allow_none <- ifelse(is.null(kwargs$allow_none), FALSE, kwargs$allow_none) + + tryCatch({ + if (is.null(data) || (is.list(data) && is.null(data$value))) { + if (allow_none || !is.null(self$code_table)) { + self$encode_internal(data, ...) + } else { + paste(rep(self$null_char, self$code_len), collapse = "") + } + } else { + self$encode_internal(data, ...) + } + }, error = function(e) { + warning(paste("Unable to encode:", toString(data))) + paste(rep(self$null_char, self$code_len), collapse = "") + }) + }, + + # Internal decode method (to be overridden) + decode_internal = function(raw, ...) { + if (!is.null(self$components) && length(self$components) > 0) { + # Handle components + result <- list() + for (comp in self$components) { + comp_class <- comp[[4]] + comp_obj <- comp_class$new() + result[[comp[[1]]]] <- comp_obj$decode( + substr(raw, comp[[2]] + 1, comp[[2]] + comp[[3]]) + ) + } + result + } else { + self$decode_value(raw, ...) + } + }, + + # Internal encode method (to be overridden) + encode_internal = function(data, ...) { + if (!is.null(self$components)) { + # Handle components + result <- character(0) + for (comp in self$components) { + comp_class <- comp[[4]] + comp_obj <- comp_class$new() + result <- c(result, comp_obj$encode( + if (comp[[1]] %in% names(data)) data[[comp[[1]]]] else NULL + )) + } + paste(result, collapse = "") + } else { + self$encode_value(data, ...) + } + }, + + # Decode value (uses code table if available) + decode_value = function(val, ...) { + kwargs <- list(...) + + # Check if value is available + if (!self$is_available(val)) { + return(NULL) + } + + # Get unit + unit <- if (is.null(kwargs$unit)) self$unit else kwargs$unit + + # Get value from code table + if (!is.null(self$code_table)) { + out_val <- tryCatch({ + self$code_table$decode(val, ...) + }, error = function(e) { + warning(paste("Error decoding with code table:", val, "-", e$message)) + NULL + }, warning = function(w) { + warning(paste("Warning decoding with code table:", val, "-", w$message)) + NULL + }) + + if (!is.null(out_val) && !is.list(out_val)) { + out_val <- list(value = out_val) + } + if (!is.null(out_val) && !("_code" %in% names(out_val))) { + code_val <- suppressWarnings(as.integer(val)) + if (!is.na(code_val)) { + out_val[["_code"]] <- code_val + } + } + } else { + # No code table - just convert to integer + out_val <- tryCatch({ + code_val <- suppressWarnings(as.integer(val)) + if (is.na(code_val)) { + return(NULL) + } + code_val + }, warning = function(w) { + NULL + }, error = function(e) { + NULL + }) + + if (is.null(out_val)) { + return(NULL) + } + + out_val <- list(value = out_val) + } + + if (is.null(out_val)) return(NULL) + + # Convert to int if not a list + if (!is.list(out_val)) { + out_val <- list(value = as.integer(out_val)) + } + + # Perform post conversion + out_val <- self$decode_convert(out_val, ...) + + # Add unit if specified + if (!is.null(unit)) { + out_val$unit <- unit + } + + out_val + }, + + # Encode value + encode_value = function(data, ...) { + # Get value from code table or data + if (!is.null(self$code_table)) { + out_val <- self$code_table$encode(data) + } else { + out_val <- if ("value" %in% names(data)) data$value else data + } + + # Convert value + out_val <- self$encode_convert(out_val, ...) + + # Format code + if (is.null(self$code_len)) { + return(as.character(out_val)) + } + sprintf(paste0("%0", self$code_len, "d"), as.integer(out_val)) + }, + + # Conversion methods (to be overridden) + decode_convert = function(val, ...) { + val + }, + + encode_convert = function(val, ...) { + val + } + ), + + private = list( + check_valid = function(value, ...) { + tryCatch({ + # Check if value is available + if (!self$is_available(value)) { + return(TRUE) + } + + # Check valid range + if (!is.null(self$valid_range)) { + val_num <- suppressWarnings(as.numeric(value)) + if (is.na(val_num)) { + return(FALSE) + } + if (val_num >= self$valid_range[1] && val_num <= self$valid_range[2]) { + return(TRUE) + } + return(FALSE) + } + + # If we reach here, assume valid + TRUE + }, error = function(e) { + FALSE + }, warning = function(w) { + FALSE + }) + } + ) +) + +################################################################################ +# SHARED CLASSES +################################################################################ + +CloudCover <- R6Class("CloudCover", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$code_table <- CodeTable2700$new() + self$unit <- "okta" + } + ) +) + +CloudGenus <- R6Class("CloudGenus", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$code_table <- CodeTable0500$new() + } + ) +) + +Day <- R6Class("Day", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$valid_range <- c(1, 31) + } + ) +) + +DirectionCardinal <- R6Class("DirectionCardinal", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$code_table <- CodeTable0700$new() + } + ) +) + +DirectionDegrees <- R6Class("DirectionDegrees", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$code_table <- CodeTable0877$new() + self$unit <- "deg" + } + ) +) + +Hour <- R6Class("Hour", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$valid_range <- c(0, 24) + } + ) +) + +Minute <- R6Class("Minute", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$valid_range <- c(0, 59) + } + ) +) + +SignedTemperature <- R6Class("SignedTemperature", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + self$unit <- "Cel" + }, + + decode_internal = function(raw, ...) { + kwargs <- list(...) + sign <- kwargs$sign + + if (is.null(sign) || sign == "/") { + return(NULL) + } + + if (!sign %in% c("0", "1")) { + stop(paste(sign, "is not a valid temperature sign")) + } + + self$decode_value(raw, sign = sign) + }, + + decode_convert = function(val, ...) { + kwargs <- list(...) + sign <- kwargs$sign + if (is.null(sign)) return(val) + + factor <- ifelse(sign == "0", 10, -10) + val$value <- val$value / factor + val + }, + + encode_convert = function(val, ...) { + sign_char <- ifelse(val >= 0, "0", "1") + abs_val <- abs(val * 10) + paste0(sign_char, sprintf("%03d", as.integer(abs_val))) + } + ) +) + +Visibility <- R6Class("Visibility", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$code_table <- CodeTable4377$new() + self$unit <- "m" + }, + + encode_internal = function(data, ...) { + kwargs <- list(...) + use90 <- ifelse(is.null(kwargs$use90), + ifelse("use90" %in% names(data), data$use90, FALSE), + kwargs$use90) + self$encode_value(data, use90 = use90) + } + ) +) + +################################################################################ +# CODE TABLE CLASSES (simplified versions) +################################################################################ + +# Base CodeTable class +CodeTable <- R6Class("CodeTable", + public = list( + table_name = NULL, + + decode = function(value, ...) { + tryCatch({ + result <- self$decode_internal(value, ...) + if (!is.null(result)) { + result$`_table` <- self$table_name + } + result + }, error = function(e) { + warning(paste("Unable to decode", value, "in", class(self)[1])) + NULL + }) + }, + + encode = function(value, ...) { + if (is.null(value)) return(NULL) + if (is.list(value) && "_code" %in% names(value)) { + return(value$`_code`) + } + self$encode_internal(value, ...) + }, + + decode_internal = function(value, ...) { + stop("decode_internal must be implemented in subclass") + }, + + encode_internal = function(value, ...) { + stop("encode_internal must be implemented in subclass") + } + ) +) + +# CodeTable2700 - Total cloud cover +CodeTable2700 <- R6Class("CodeTable2700", + inherit = CodeTable, + public = list( + initialize = function() { + self$table_name <- "2700" + }, + + decode_internal = function(N, ...) { + n <- as.integer(N) + if (n == 9) { + list(value = NULL, obscured = TRUE, unit = "okta") + } else { + list(value = n, obscured = FALSE, unit = "okta") + } + }, + + encode_internal = function(data, ...) { + if (is.null(data$value)) { + if (data$obscured) return("9") + stop("Cannot encode cloud cover: value is NULL and obscured is FALSE") + } + as.character(data$value) + } + ) +) + +# CodeTable0500 - Genus of cloud +CodeTable0500 <- R6Class("CodeTable0500", + inherit = CodeTable, + public = list( + values = c("Ci", "Cc", "Cs", "Ac", "As", "Ns", "Sc", "St", "Cu", "Cb"), + + initialize = function() { + self$table_name <- "0500" + }, + + decode_internal = function(i, ...) { + idx <- as.integer(i) + 1 + if (idx >= 1 && idx <= length(self$values)) { + list(value = self$values[idx]) + } else { + stop(paste("Invalid cloud genus code:", i)) + } + }, + + encode_internal = function(data, ...) { + val <- if (is.list(data)) data$value else data + idx <- which(self$values == val) + if (length(idx) == 0) { + stop(paste("Invalid cloud genus:", val)) + } + as.character(idx - 1) + } + ) +) + +# CodeTable0700 - Direction or bearing in one figure +CodeTable0700 <- R6Class("CodeTable0700", + inherit = CodeTable, + public = list( + directions = c(NULL, "NE", "E", "SE", "S", "SW", "W", "NW", "N", NULL), + + initialize = function() { + self$table_name <- "0700" + }, + + decode_internal = function(D, ...) { + if (D == "/") { + return(list(value = NULL, isCalmOrStationary = NULL, allDirections = NULL)) + } + + d <- as.integer(D) + isCalmOrStationary <- (d == 0) + allDirections <- (d == 9) + + direction <- if (d >= 0 && d < length(self$directions)) { + self$directions[d + 1] + } else { + NULL + } + + list( + value = direction, + isCalmOrStationary = isCalmOrStationary, + allDirections = allDirections + ) + }, + + encode_internal = function(data, ...) { + if ("isCalmOrStationary" %in% names(data) && data$isCalmOrStationary) { + return("0") + } + if ("allDirections" %in% names(data) && data$allDirections) { + return("9") + } + if ("value" %in% names(data) && !is.null(data$value)) { + idx <- which(self$directions == data$value) - 1 + if (length(idx) > 0) { + return(as.character(idx)) + } + } + stop("Cannot encode direction") + } + ) +) + +# CodeTable0877 - True direction in tens of degrees +CodeTable0877 <- R6Class("CodeTable0877", + inherit = CodeTable, + public = list( + initialize = function() { + self$table_name <- "0877" + }, + + decode_internal = function(dd, ...) { + dd_int <- as.integer(dd) + calm <- (dd_int == 0) + varAllUnknown <- (dd_int == 99) + + if (calm) { + direction <- NULL + } else if (varAllUnknown) { + direction <- NULL + } else if (dd_int >= 1 && dd_int <= 36) { + direction <- dd_int * 10 + } else { + stop(paste("Invalid direction code:", dd)) + } + + list( + value = direction, + varAllUnknown = varAllUnknown, + calm = calm + ) + }, + + encode_internal = function(data, ...) { + val <- if (is.list(data)) data$value else data + if (is.null(val)) { + if ("calm" %in% names(data) && data$calm) return("00") + if ("varAllUnknown" %in% names(data) && data$varAllUnknown) return("99") + return("//") + } + code <- round(val / 10) + if (code < 1) code <- 0 + if (code > 36) code <- 36 + sprintf("%02d", code) + } + ) +) + +# CodeTable4377 - Horizontal visibility at surface +CodeTable4377 <- R6Class("CodeTable4377", + inherit = CodeTable, + public = list( + range90 = list( + c(0, 50), c(50, 200), c(200, 500), c(500, 1000), c(1000, 2000), + c(2000, 4000), c(4000, 10000), c(10000, 20000), c(20000, 50000), + c(50000, Inf) + ), + + initialize = function() { + self$table_name <- "4377" + }, + + decode_internal = function(VV, ...) { + vv <- as.integer(VV) + + if (vv >= 51 && vv <= 55) { + stop(paste("Invalid visibility code:", VV)) + } + + visibility <- NULL + quantifier <- NULL + + if (vv == 0) { + visibility <- 100 + quantifier <- "isLess" + } else if (vv <= 50) { + visibility <- vv * 100 + } else if (vv <= 80) { + visibility <- (vv - 50) * 1000 + } else if (vv <= 88) { + visibility <- (vv - 74) * 5000 + } else if (vv == 89) { + visibility <- 70000 + quantifier <- "isGreater" + } else if (vv == 90) { + visibility <- 50 + quantifier <- "isLess" + } else if (vv == 91) { + visibility <- 50 + } else if (vv == 92) { + visibility <- 200 + } else if (vv == 93) { + visibility <- 500 + } else if (vv == 94) { + visibility <- 1000 + } else if (vv == 95) { + visibility <- 2000 + } else if (vv == 96) { + visibility <- 4000 + } else if (vv == 97) { + visibility <- 10000 + } else if (vv == 98) { + visibility <- 20000 + } else if (vv == 99) { + visibility <- 50000 + quantifier <- "isGreaterOrEqual" + } else { + stop(paste("Invalid visibility code:", VV)) + } + + use90 <- (vv >= 90) + list( + value = visibility, + quantifier = quantifier, + use90 = use90 + ) + }, + + encode_internal = function(data, use90 = FALSE, ...) { + value <- if (is.list(data)) data$value else data + quantifier <- if (is.list(data) && "quantifier" %in% names(data)) data$quantifier else NULL + + if (use90) { + for (idx in seq_along(self$range90)) { + r <- self$range90[[idx]] + if (value >= r[1] && value < r[2]) { + return(sprintf("%02d", idx + 89)) + } + } + } else { + if (value < 100) { + code <- 0 + } else if (value <= 5000) { + code <- floor(value / 100) + } else if (value <= 30000) { + code <- floor(value / 1000) + 50 + } else if (value <= 70000 && is.null(quantifier)) { + code <- floor(value / 5000) + 74 + } else { + code <- 89 + } + return(sprintf("%02d", code)) + } + + stop(paste("Cannot encode visibility:", value)) + } + ) +) + +################################################################################ +# MAIN OBSERVATION CLASSES +################################################################################ + +# Temperature observation +Temperature <- R6Class("Temperature", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(group, ...) { + sn <- substr(group, 2, 2) + TTT <- substr(group, 3, 5) + + # Fix trailing "/" (issue #10) + if (TTT != "///") { + TTT <- sub("/$", "0", TTT) + } + + if (!sn %in% c("0", "1", "/")) { + warning(paste(group, "is an invalid temperature group")) + return(NULL) + } + + temp_obs <- SignedTemperature$new() + temp_obs$decode(TTT, sign = sn) + }, + + encode_internal = function(data, ...) { + temp_obs <- SignedTemperature$new() + temp_obs$encode(data) + } + ) +) + +# Pressure observation +Pressure <- R6Class("Pressure", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + self$unit <- "hPa" + }, + + decode_convert = function(val, ...) { + val_int <- as.integer(val$value) + val$value <- (val_int / 10) + ifelse(val_int > 5000, 0, 1000) + val + }, + + encode_convert = function(val, ...) { + abs(val * 10) - ifelse(val >= 1000, 10000, 0) + } + ) +) + +# Surface wind observation +SurfaceWind <- R6Class("SurfaceWind", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(ddff, ...) { + dd <- substr(ddff, 1, 2) + ff <- substr(ddff, 3, 4) + + dir_obs <- DirectionDegrees$new() + direction <- dir_obs$decode(dd) + + speed_obs <- WindSpeed$new() + speed <- speed_obs$decode(ff) + + # Sanity check: if wind is calm, it can't have a speed + if (!is.null(direction) && !is.null(direction$calm) && direction$calm && + !is.null(speed) && !is.null(speed$value) && speed$value > 0) { + warning(paste("Wind is calm, yet has a speed (dd:", dd, ", ff:", ff, ")")) + speed <- NULL + } + + list(direction = direction, speed = speed) + }, + + encode_internal = function(data, ...) { + dir_obs <- DirectionDegrees$new() + speed_obs <- WindSpeed$new() + + dd <- dir_obs$encode(if ("direction" %in% names(data)) data$direction else NULL, allow_none = TRUE) + ff <- speed_obs$encode(if ("speed" %in% names(data)) data$speed else NULL) + + paste0(dd, ff) + } + ) +) + +# Wind speed (simplified) +WindSpeed <- R6Class("WindSpeed", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + }, + + decode_internal = function(ff, ...) { + # Decode wind speed - ff is just a numeric value + # Use the base decode_value method which handles numeric conversion + self$decode_value(ff, ...) + }, + + encode_internal = function(data, ...) { + if (is.null(data)) { + return(paste(rep(self$null_char, self$code_len), collapse = "")) + } + value <- if (is.list(data)) data$value else data + if (!is.null(value) && value > 99) { + return(paste0("99 00", sprintf("%02d", value))) + } + sprintf("%02d", as.integer(value)) + } + ) +) + +################################################################################ +# SYNOP REPORT CLASS +################################################################################ + +# Base Report class +Report <- R6Class("Report", + public = list( + not_implemented = list(), + + decode = function(message) { + tryCatch({ + self$decode_internal(message) + }, error = function(e) { + stop(paste("Decode error:", e$message)) + }) + }, + + decode_internal = function(message) { + stop("decode_internal must be implemented in subclass") + } + ) +) + +# SYNOP class - main class for decoding SYNOP messages +SYNOP <- R6Class("SYNOP", + inherit = Report, + public = list( + country = NULL, + + initialize = function() { + self$not_implemented <- list() + self$country <- NULL + }, + + decode_internal = function(message) { + # Initialize data + data <- list() + + # Split message into groups + groups <- strsplit(message, " ")[[1]] + group_idx <- 1 + + # Helper function to get next group + get_next_group <- function() { + if (group_idx <= length(groups)) { + group <- groups[group_idx] + group_idx <<- group_idx + 1 + return(group) + } + return(NULL) + } + + # Alias for convenience + next_group <- get_next_group + + # SECTION 0: Station type, time, and identification + station_type <- next_group() + if (is.null(station_type)) { + stop("Invalid SYNOP: missing station type") + } + + # For simplicity, assume AAXX format + data$station_type <- list(value = station_type) + + # Get observation time and wind indicator (YYGGi) + yygii <- next_group() + if (is.null(yygii) || nchar(yygii) < 5) { + stop("Invalid SYNOP: missing YYGGi group") + } + + # Decode observation time + obs_time <- ObservationTime$new() + data$obs_time <- obs_time$decode(substr(yygii, 1, 4)) + + # Decode wind indicator + wind_ind <- WindIndicator$new() + data$wind_indicator <- wind_ind$decode(substr(yygii, 5, 5)) + + # Get station ID + station_id_group <- next_group() + if (is.null(station_id_group)) { + stop("Invalid SYNOP: missing station ID") + } + + data$station_id <- list(value = station_id_group) + + # Decode region + tryCatch({ + region <- Region$new() + result <- region$decode(station_id_group) + if (!is.null(result)) { + data$region <- result + } + }, error = function(e) { + warning(paste("Error decoding region:", e$message)) + }) + + # Check if next group is NIL (station did not send data) + next_check <- next_group() + if (!is.null(next_check) && (next_check == "NIL" || grepl("^NIL", next_check))) { + # Station did not send data - set remaining fields to NA + data$precipitation_indicator <- NA + data$weather_indicator <- NA + data$lowest_cloud_base <- NA + data$visibility <- NA + data$cloud_cover <- NA + data$surface_wind <- NA + data$air_temperature <- NA + data$dewpoint_temperature <- NA + data$relative_humidity <- NA + data$station_pressure <- NA + data$sea_level_pressure <- NA + data$pressure_tendency <- NA + data$precipitation_s1 <- NA + data$present_weather <- NA + data$past_weather <- NA + data$cloud_types <- NA + return(data) + } + + # SECTION 1: Main observations + section1 <- next_check # Use the group we already got + if (is.null(section1) || nchar(section1) < 5) { + # If section1 is invalid, try to continue anyway + warning("Invalid or missing section 1") + return(data) + } + + # Decode precipitation indicator, weather indicator, cloud base, visibility + tryCatch({ + precip_ind <- PrecipitationIndicator$new() + result <- precip_ind$decode(substr(section1, 1, 1), country = self$country) + if (!is.null(result)) { + data$precipitation_indicator <- result + } + }, error = function(e) { + warning(paste("Error decoding precipitation indicator:", e$message)) + }) + + tryCatch({ + weather_ind <- WeatherIndicator$new() + result <- weather_ind$decode(substr(section1, 2, 2)) + if (!is.null(result)) { + data$weather_indicator <- result + } + }, error = function(e) { + warning(paste("Error decoding weather indicator:", e$message)) + }) + + tryCatch({ + lowest_cloud <- LowestCloudBase$new() + result <- lowest_cloud$decode(substr(section1, 3, 3)) + if (!is.null(result)) { + data$lowest_cloud_base <- result + } + }, error = function(e) { + warning(paste("Error decoding lowest cloud base:", e$message)) + }) + + tryCatch({ + vis <- Visibility$new() + result <- vis$decode(substr(section1, 4, 5)) + if (!is.null(result)) { + data$visibility <- result + } + }, error = function(e) { + warning(paste("Error decoding visibility:", e$message)) + }) + + # Get cloud cover and wind (Nddff) + nddff <- next_group() + if (!is.null(nddff) && nchar(nddff) >= 5) { + tryCatch({ + cloud <- CloudCover$new() + result <- cloud$decode(substr(nddff, 1, 1)) + if (!is.null(result)) { + data$cloud_cover <- result + } + }, error = function(e) { + warning(paste("Error decoding cloud cover from:", nddff, "-", e$message)) + }) + + tryCatch({ + wind <- SurfaceWind$new() + wind_data <- wind$decode(substr(nddff, 2, 5)) + if (!is.null(wind_data)) { + if (!is.null(data$wind_indicator)) { + if (!is.null(wind_data$speed)) { + wind_data$speed$unit <- data$wind_indicator$unit + } + } + data$surface_wind <- wind_data + } + }, error = function(e) { + warning(paste("Error decoding surface wind from:", nddff, "-", e$message)) + }) + } + + # Parse section 1 groups (1sTTT, 2sTTT, 3P0P0P0, 4PPPP, etc.) + next_grp <- next_group() + while (!is.null(next_grp)) { + if (grepl("^333|^444|^555", next_grp)) { + # Start of next section + break + } + + # Try to get header, handle errors gracefully + header <- tryCatch({ + as.integer(substr(next_grp, 1, 1)) + }, error = function(e) { + warning(paste("Unable to parse header from group:", next_grp)) + next_grp <<- next_group() + return(NULL) + }, warning = function(w) { + warning(paste("Warning parsing header from group:", next_grp)) + next_grp <<- next_group() + return(NULL) + }) + + if (is.null(header) || is.na(header)) { + next_grp <- next_group() + # Skip to next iteration + if (is.null(next_grp)) break + next + } + + tryCatch({ + if (header == 1) { + # Air temperature + temp <- Temperature$new() + result <- temp$decode(next_grp) + if (!is.null(result)) { + data$air_temperature <- result + } + } else if (header == 2) { + # Dewpoint temperature or relative humidity + sn <- substr(next_grp, 2, 2) + if (sn == "9") { + rel_hum <- RelativeHumidity$new() + result <- rel_hum$decode(substr(next_grp, 3, 5)) + if (!is.null(result)) { + data$relative_humidity <- result + } + } else { + temp <- Temperature$new() + result <- temp$decode(next_grp) + if (!is.null(result)) { + data$dewpoint_temperature <- result + } + } + } else if (header == 3) { + # Station pressure + press <- Pressure$new() + result <- press$decode(substr(next_grp, 2, 5)) + if (!is.null(result)) { + data$station_pressure <- result + } + } else if (header == 4) { + # Sea level pressure + press <- Pressure$new() + result <- press$decode(substr(next_grp, 2, 5)) + if (!is.null(result)) { + data$sea_level_pressure <- result + } + } else if (header == 5) { + # Pressure tendency + press_tend <- PressureTendency$new() + result <- press_tend$decode(next_grp) + if (!is.null(result)) { + data$pressure_tendency <- result + } + } else if (header == 6) { + # Precipitation + if (!is.null(data$precipitation_indicator) && + data$precipitation_indicator$in_group_1) { + precip <- Precipitation$new() + result <- precip$decode(next_grp) + if (!is.null(result)) { + data$precipitation_s1 <- result + } + } + } else if (header == 7) { + # Present and past weather + if (nchar(next_grp) >= 5) { + ww <- Weather$new() + result <- ww$decode(substr(next_grp, 2, 3), + time_before = list(value = 6, unit = "h"), + type = "present", + weather_indicator = if (!is.null(data$weather_indicator)) data$weather_indicator$value else NULL) + if (!is.null(result)) { + data$present_weather <- result + } + result2 <- ww$decode(substr(next_grp, 4, 4), type = "past", + weather_indicator = if (!is.null(data$weather_indicator)) data$weather_indicator$value else NULL) + result3 <- ww$decode(substr(next_grp, 5, 5), type = "past", + weather_indicator = if (!is.null(data$weather_indicator)) data$weather_indicator$value else NULL) + if (!is.null(result2) || !is.null(result3)) { + data$past_weather <- list(result2, result3) + } + } + } else if (header == 8) { + # Cloud types + cloud_types <- CloudType$new() + result <- cloud_types$decode(next_grp) + if (!is.null(result)) { + data$cloud_types <- result + } + } + }, error = function(e) { + warning(paste("Error decoding group:", next_grp, "-", e$message)) + # Continue to next group + }, warning = function(w) { + warning(paste("Warning decoding group:", next_grp, "-", w$message)) + # Continue to next group + }) + + next_grp <- next_group() + } + + # SECTION 3: Additional observations + if (!is.null(next_grp) && next_grp == "333") { + next_grp <- next_group() + cloud_layers <- list() + highest_gusts <- list() + group_9 <- list() # Collect group 9 codes + + while (!is.null(next_grp) && !grepl("^444|^555", next_grp)) { + # Try to get header, handle errors gracefully + header <- tryCatch({ + as.integer(substr(next_grp, 1, 1)) + }, error = function(e) { + warning(paste("Unable to parse header from group:", next_grp)) + return(NULL) + }, warning = function(w) { + warning(paste("Warning parsing header from group:", next_grp)) + return(NULL) + }) + + if (is.null(header) || is.na(header)) { + next_grp <- next_group() + # Skip to next iteration + if (is.null(next_grp)) break + next + } + + tryCatch({ + # Check if it's a group 9 code (9xxxx) + if (header == 9) { + group_9[[length(group_9) + 1]] <- next_grp + } else if (header == 8) { + # Cloud layers + cloud_layer <- CloudLayer$new() + result <- cloud_layer$decode(next_grp) + if (!is.null(result)) { + cloud_layers[[length(cloud_layers) + 1]] <- result + } + } else if (header == 1) { + # Maximum temperature + temp <- Temperature$new() + result <- temp$decode(next_grp) + if (!is.null(result)) { + data$maximum_temperature <- result + } + } else if (header == 2) { + # Minimum temperature + temp <- Temperature$new() + result <- temp$decode(next_grp) + if (!is.null(result)) { + data$minimum_temperature <- result + } + } + }, error = function(e) { + warning(paste("Error decoding group:", next_grp, "-", e$message)) + # Continue to next group + }, warning = function(w) { + warning(paste("Warning decoding group:", next_grp, "-", w$message)) + # Continue to next group + }) + + next_grp <- next_group() + } + + # Parse group 9 codes (including highest gusts) + if (length(group_9) > 0) { + idx <- 1 + while (idx <= length(group_9)) { + g <- group_9[[idx]] + tryCatch({ + if (nchar(g) >= 3) { + j1 <- substr(g, 2, 2) # Second character + j2 <- substr(g, 3, 3) # Third character + + if (j1 == "1") { + # Group 91xx - highest gusts + if (j2 == "0") { + # 910ff - gust with 10 min period + if (is.null(data$highest_gust)) { + data$highest_gust <- list() + } + gust <- HighestGust$new() + gust_data <- gust$decode(g, + unit = if (!is.null(data$wind_indicator)) data$wind_indicator$unit else NULL, + measure_period = list(value = 10, unit = "min") + ) + if (!is.null(gust_data)) { + data$highest_gust[[length(data$highest_gust) + 1]] <- gust_data + } + idx <- idx + 1 + } else if (j2 == "1") { + # 911ff - gust with time before obs + # Check if next group is direction (915dd) + if (idx < length(group_9)) { + next_g <- group_9[[idx + 1]] + if (substr(next_g, 1, 3) == "915") { + gust_group <- paste(g, next_g, sep = " ") + idx <- idx + 2 # Skip next group + } else { + gust_group <- g + idx <- idx + 1 + } + } else { + gust_group <- g + idx <- idx + 1 + } + + if (is.null(data$highest_gust)) { + data$highest_gust <- list() + } + gust <- HighestGust$new() + gust_data <- gust$decode(gust_group, + unit = if (!is.null(data$wind_indicator)) data$wind_indicator$unit else NULL, + time_before = list(value = 6, unit = "h") # Default time before + ) + if (!is.null(gust_data)) { + data$highest_gust[[length(data$highest_gust) + 1]] <- gust_data + } + } else { + idx <- idx + 1 + } + } else { + idx <- idx + 1 + } + } else { + idx <- idx + 1 + } + }, error = function(e) { + warning(paste("Error decoding group 9 code:", g, "-", e$message)) + idx <<- idx + 1 + }, warning = function(w) { + warning(paste("Warning decoding group 9 code:", g, "-", w$message)) + idx <<- idx + 1 + }) + } + } + + if (length(cloud_layers) > 0) { + data$cloud_layer <- cloud_layers + } + } + + return(data) + } + ) +) + +################################################################################ +# ADDITIONAL CLASSES NEEDED FOR SYNOP +################################################################################ + +# ObservationTime +ObservationTime <- R6Class("ObservationTime", + inherit = Observation, + public = list( + components = list( + list("day", 0, 2, Day), + list("hour", 2, 2, Hour) + ), + + initialize = function() { + super$initialize() + self$code_len <- 4 + } + ) +) + +# WindIndicator +WindIndicator <- R6Class("WindIndicator", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$valid_range <- c(1, 7) + }, + + decode_internal = function(iw, ...) { + iw_int <- as.integer(iw) + if (iw == "/") { + list(value = NULL, unit = NULL, estimated = NULL) + } else { + list( + value = iw_int, + unit = ifelse(iw_int < 2, "m/s", "KT"), + estimated = (iw_int %in% c(0, 3)) + ) + } + } + ) +) + +# Region +Region <- R6Class("Region", + inherit = Observation, + public = list( + decode_internal = function(raw, ...) { + raw_int <- as.integer(raw) + + regions <- list( + I = list(c(60000, 69998)), + II = list(c(20000, 20099), c(20200, 21998), c(23001, 25998), + c(28001, 32998), c(35001, 36998), c(38001, 39998), + c(40350, 48599), c(48800, 49998), c(50001, 59998)), + III = list(c(80001, 88998)), + IV = list(c(70001, 79998)), + V = list(c(48600, 48799), c(90001, 98998)), + VI = list(c(1, 19998), c(20100, 20199), c(22001, 22998), + c(26001, 27998), c(33001, 34998), c(37001, 37998), + c(40001, 40349)), + Antarctic = list(c(89001, 89998)) + ) + + for (reg_name in names(regions)) { + for (range in regions[[reg_name]]) { + if (raw_int >= range[1] && raw_int <= range[2]) { + return(list(value = reg_name)) + } + } + } + + stop(paste("Invalid region code:", raw)) + } + ) +) + +# PrecipitationIndicator +PrecipitationIndicator <- R6Class("PrecipitationIndicator", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + }, + + decode_internal = function(i, ...) { + kwargs <- list(...) + country <- kwargs$country + i_int <- as.integer(i) + + list( + value = i_int, + in_group_1 = (i %in% c("0", "1")) || (i == "6" && !is.null(country) && country == "RU"), + in_group_3 = (i %in% c("0", "2")) || (i == "7" && !is.null(country) && country == "RU") + ) + } + ) +) + +# WeatherIndicator +WeatherIndicator <- R6Class("WeatherIndicator", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$valid_range <- c(1, 7) + }, + + decode_internal = function(ix, ...) { + ix_int <- ifelse(ix == "/", NULL, as.integer(ix)) + + list( + value = ix_int, + automatic = ifelse(is.null(ix_int) || ix_int < 3, FALSE, TRUE) + ) + } + ) +) + +# LowestCloudBase +LowestCloudBase <- R6Class("LowestCloudBase", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$code_table <- CodeTable1600$new() + self$unit <- "m" + } + ) +) + +# CodeTable1600 +CodeTable1600 <- R6Class("CodeTable1600", + inherit = CodeTable, + public = list( + ranges = list( + c(0, 50), c(50, 100), c(100, 200), c(200, 300), c(300, 600), + c(600, 1000), c(1000, 1500), c(1500, 2000), c(2000, 2500), c(2500, Inf) + ), + + initialize = function() { + self$table_name <- "1600" + }, + + decode_internal = function(h, ...) { + h_int <- as.integer(h) + if (h_int >= 0 && h_int < length(self$ranges)) { + range <- self$ranges[[h_int + 1]] + quantifier <- ifelse(is.infinite(range[2]), "isGreaterOrEqual", NULL) + list(min = range[1], max = ifelse(is.infinite(range[2]), NULL, range[2]), + quantifier = quantifier) + } else { + stop(paste("Invalid cloud base code:", h)) + } + } + ) +) + +# Precipitation +Precipitation <- R6Class("Precipitation", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(group, ...) { + kwargs <- list(...) + tenths <- ifelse(is.null(kwargs$tenths), FALSE, kwargs$tenths) + + if (tenths) { + rrrr <- substr(group, 2, 5) + amount <- Amount24$new() + list( + amount = amount$decode(rrrr), + time_before_obs = list(value = 24, unit = "h") + ) + } else { + rrr <- substr(group, 2, 4) + t <- substr(group, 5, 5) + amount <- Amount$new() + list( + amount = amount$decode(rrr), + time_before_obs = TimeBeforeObs$new()$decode(t) + ) + } + } + ) +) + +# Amount (simplified) +Amount <- R6Class("Amount", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 3 + self$code_table <- CodeTable3590$new() + self$unit <- "mm" + } + ) +) + +# Amount24 +Amount24 <- R6Class("Amount24", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + self$code_table <- CodeTable3590A$new() + self$unit <- "mm" + } + ) +) + +# CodeTable3590 (simplified) +CodeTable3590 <- R6Class("CodeTable3590", + inherit = CodeTable, + public = list( + initialize = function() { + self$table_name <- "3590" + }, + + decode_internal = function(RRR, ...) { + rrr_int <- as.integer(RRR) + if (rrr_int <= 988) { + list(value = rrr_int, quantifier = NULL, trace = FALSE) + } else if (rrr_int == 989) { + list(value = rrr_int, quantifier = "isGreaterOrEqual", trace = FALSE) + } else if (rrr_int == 990) { + list(value = 0, quantifier = NULL, trace = TRUE) + } else if (rrr_int >= 991 && rrr_int <= 999) { + list(value = (rrr_int - 990) / 10.0, quantifier = NULL, trace = FALSE) + } else { + stop(paste("Invalid precipitation code:", RRR)) + } + } + ) +) + +# CodeTable3590A (simplified) +CodeTable3590A <- R6Class("CodeTable3590A", + inherit = CodeTable, + public = list( + initialize = function() { + self$table_name <- "3590A" + }, + + decode_internal = function(RRRR, ...) { + rrrr_int <- as.integer(RRRR) + if (rrrr_int <= 9998) { + list(value = round(rrrr_int * 0.1, 1), quantifier = NULL, trace = FALSE) + } else if (rrrr_int == 9999) { + list(value = 0, quantifier = NULL, trace = TRUE) + } else { + stop(paste("Invalid precipitation code:", RRRR)) + } + } + ) +) + +# TimeBeforeObs (simplified) +TimeBeforeObs <- R6Class("TimeBeforeObs", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$code_table <- CodeTable4019$new() + self$unit <- "h" + } + ) +) + +# CodeTable4019 +CodeTable4019 <- R6Class("CodeTable4019", + inherit = CodeTable, + public = list( + values = c(NULL, 6, 12, 18, 24, 1, 2, 3, 9, 15), + + initialize = function() { + self$table_name <- "4019" + }, + + decode_internal = function(t, ...) { + t_int <- as.integer(t) + 1 + if (t_int >= 1 && t_int <= length(self$values)) { + val <- self$values[[t_int]] + if (!is.null(val)) { + list(value = val, unit = "h") + } else { + NULL + } + } else { + NULL + } + } + ) +) + +# PressureTendency +PressureTendency <- R6Class("PressureTendency", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(group, ...) { + a <- substr(group, 2, 2) + ppp <- substr(group, 3, 5) + + tendency <- Tendency$new() + change <- Change$new() + + list( + tendency = tendency$decode(a), + change = change$decode(ppp, tendency = tendency$decode(a)) + ) + } + ) +) + +# Tendency (simplified) +Tendency <- R6Class("Tendency", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + self$valid_range <- c(0, 8) + } + ) +) + +# Change (simplified) +Change <- R6Class("Change", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 3 + self$unit <- "hPa" + }, + + decode_convert = function(val, ...) { + kwargs <- list(...) + tendency <- kwargs$tendency + + if (is.list(tendency) && "value" %in% names(tendency)) { + factor <- ifelse(tendency$value < 5, 10.0, -10.0) + val$value <- val$value / factor + } + val + } + ) +) + +# Weather +Weather <- R6Class("Weather", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + }, + + decode_internal = function(group, ...) { + kwargs <- list(...) + w_type <- kwargs$type + ix <- kwargs$weather_indicator + + if (w_type == "present") { + table <- ifelse(!is.null(ix) && ix %in% c(5, 6, 7), "4680", "4677") + } else if (w_type == "past") { + table <- ifelse(!is.null(ix) && ix %in% c(5, 6, 7), "4531", "4561") + } else { + stop(paste("Invalid weather type:", w_type)) + } + + group_int <- as.integer(group) + if (is.na(group_int)) { + return(NULL) + } + + result <- list(value = group_int, `_table` = table) + if (!is.null(kwargs$time_before)) { + result$time_before_obs <- kwargs$time_before + } + + result + } + ) +) + +# CloudType +CloudType <- R6Class("CloudType", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(group, ...) { + nh <- substr(group, 2, 2) + cl <- substr(group, 3, 3) + cm <- substr(group, 4, 4) + ch <- substr(group, 5, 5) + + low_cloud <- LowCloud$new() + middle_cloud <- MiddleCloud$new() + high_cloud <- HighCloud$new() + cloud_cover <- CloudCover$new() + + result <- list( + low_cloud_type = low_cloud$decode(cl), + middle_cloud_type = middle_cloud$decode(cm), + high_cloud_type = high_cloud$decode(ch) + ) + + cover <- cloud_cover$decode(nh) + if (nh != "/") { + if (!is.null(result$low_cloud_type) && + result$low_cloud_type$value >= 1 && + result$low_cloud_type$value <= 9) { + result$low_cloud_amount <- cover + } else if (!is.null(result$middle_cloud_type) && + result$middle_cloud_type$value >= 0 && + result$middle_cloud_type$value <= 9) { + result$middle_cloud_amount <- cover + } else { + result$cloud_amount <- cover + } + } + + result + } + ) +) + +# LowCloud, MiddleCloud, HighCloud (simplified) +LowCloud <- R6Class("LowCloud", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + } + ) +) + +MiddleCloud <- R6Class("MiddleCloud", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + } + ) +) + +HighCloud <- R6Class("HighCloud", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 1 + } + ) +) + +# CloudLayer +CloudLayer <- R6Class("CloudLayer", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 4 + }, + + decode_internal = function(group, ...) { + n <- substr(group, 2, 2) + c <- substr(group, 3, 3) + hh <- substr(group, 4, 5) + + cloud_cover <- CloudCover$new() + cloud_genus <- CloudGenus$new() + height <- Height$new() + + list( + cloud_cover = cloud_cover$decode(n), + cloud_genus = cloud_genus$decode(c), + cloud_height = height$decode(hh) + ) + } + ) +) + +# Height (simplified) +Height <- R6Class("Height", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + self$code_table <- CodeTable1677$new() + self$unit <- "m" + } + ) +) + +# CodeTable1677 (simplified) +CodeTable1677 <- R6Class("CodeTable1677", + inherit = CodeTable, + public = list( + initialize = function() { + self$table_name <- "1677" + }, + + decode_internal = function(hh, ...) { + hh_int <- as.integer(hh) + quantifier <- NULL + + if (hh_int == 0) { + list(value = 30, quantifier = "isLess") + } else if (hh_int >= 1 && hh_int <= 50) { + list(value = hh_int * 30, quantifier = NULL) + } else if (hh_int >= 56 && hh_int <= 80) { + list(value = (hh_int - 50) * 300, quantifier = NULL) + } else if (hh_int >= 81 && hh_int <= 88) { + list(value = ((hh_int - 80) * 1500) + 9000, quantifier = NULL) + } else if (hh_int == 89) { + list(value = 21000, quantifier = "isGreater") + } else if (hh_int == 99) { + list(value = 21000, quantifier = "isGreater") + } else { + stop(paste("Invalid height code:", hh)) + } + } + ) +) + +# RelativeHumidity +RelativeHumidity <- R6Class("RelativeHumidity", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 3 + self$valid_range <- c(0, 100) + self$unit <- "%" + } + ) +) + +# HighestGust - Highest wind gust +HighestGust <- R6Class("HighestGust", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + }, + + decode_internal = function(group, ...) { + kwargs <- list(...) + + # Split group into separate groups if needed + groups <- strsplit(group, " ")[[1]] + + # Get type, speed and direction + # Format: 910ff or 911ff, optionally followed by 915dd + t <- NULL + ff <- NULL + dd <- NULL + + if (length(groups) > 0) { + # First group: 910ff or 911ff + first_group <- groups[1] + if (nchar(first_group) >= 5) { + t <- substr(first_group, 3, 3) + ff <- substr(first_group, 4, 5) + } + } + + # Second group: 915dd (direction) + if (length(groups) > 1) { + second_group <- groups[2] + if (nchar(second_group) >= 5 && substr(second_group, 1, 3) == "915") { + dd <- substr(second_group, 4, 5) + } + } + + # Return values + time_before <- kwargs$time_before + measure_period <- kwargs$measure_period + + gust_obs <- Gust$new() + dir_obs <- DirectionDegrees$new() + + data <- list( + speed = gust_obs$decode(ff, unit = kwargs$unit), + direction = dir_obs$decode(dd) + ) + + if (!is.null(time_before)) { + data$time_before_obs <- time_before + } + if (!is.null(measure_period)) { + data$measure_period <- measure_period + } + + data + }, + + encode_internal = function(data, ...) { + kwargs <- list(...) + time_before <- kwargs$time_before + measure_period <- kwargs$measure_period + output <- character(0) + + # Handle list of gusts or single gust + if (is.list(data) && "speed" %in% names(data)) { + data <- list(data) # Convert single gust to list + } + + for (d in data) { + # Convert time before obs, if required + if ("time_before_obs" %in% names(d)) { + if (is.null(time_before) || + (!is.null(time_before) && !identical(d$time_before_obs, time_before))) { + time_before_obs <- TimeBeforeObs$new() + tt <- time_before_obs$encode(d$time_before_obs) + if (tt != "//") { + output <- c(output, paste0("907", tt)) + } + } + prefix <- "911" + } else if ("measure_period" %in% names(d)) { + if (identical(d$measure_period, list(value = 10, unit = "min"))) { + prefix <- "910" + } else { + stop("Invalid value for measure_period") + } + } else { + prefix <- "910" # Default + } + + # Convert the gust + gust_obs <- Gust$new() + ff <- gust_obs$encode(if ("speed" %in% names(d)) d$speed else NULL) + output <- c(output, paste0(prefix, ff)) + + # Convert the direction + if ("direction" %in% names(d) && !is.null(d$direction)) { + dir_obs <- DirectionDegrees$new() + dd <- dir_obs$encode(d$direction) + output <- c(output, paste0("915", dd)) + } + } + + paste(output, collapse = " ") + } + ) +) + +# Gust - Wind gust speed (internal class for HighestGust) +Gust <- R6Class("Gust", + inherit = Observation, + public = list( + initialize = function() { + super$initialize() + self$code_len <- 2 + }, + + decode_internal = function(ff, ...) { + # Decode wind gust speed - same as WindSpeed + self$decode_value(ff, ...) + }, + + encode_internal = function(data, ...) { + # Encode wind gust speed - same as WindSpeed + if (is.null(data)) { + return(paste(rep(self$null_char, self$code_len), collapse = "")) + } + value <- if (is.list(data)) data$value else data + if (!is.null(value) && value > 99) { + return(paste0("99 00", sprintf("%02d", value))) + } + sprintf("%02d", as.integer(value)) + } + ) +) + +################################################################################ +# EXPORT FUNCTIONS +################################################################################ + +# Helper function to create observation instances +create_observation <- function(class_name, ...) { + class_map <- list( + "CloudCover" = CloudCover, + "CloudGenus" = CloudGenus, + "Day" = Day, + "DirectionCardinal" = DirectionCardinal, + "DirectionDegrees" = DirectionDegrees, + "Hour" = Hour, + "Minute" = Minute, + "SignedTemperature" = SignedTemperature, + "Visibility" = Visibility, + "Temperature" = Temperature, + "Pressure" = Pressure, + "SurfaceWind" = SurfaceWind, + "WindSpeed" = WindSpeed, + "SYNOP" = SYNOP + ) + + if (!class_name %in% names(class_map)) { + stop(paste("Unknown observation class:", class_name)) + } + + class_map[[class_name]]$new(...) +} + +# Example usage function +example_usage <- function() { + # Example: Decode temperature + # Format: 1sTTT where s=0 (positive) or s=1 (negative), TTT=temperature + temp <- Temperature$new() + result <- temp$decode("10094") # Sign=0 (positive), TTT=094 -> 9.4°C + print(result) + + # Negative temperature + result2 <- temp$decode("11094") # Sign=1 (negative), TTT=094 -> -9.4°C + print(result2) + + # Example: Encode temperature + encoded <- temp$encode(list(value = 19.4)) + print(encoded) + + # Example: Decode cloud cover + cloud <- CloudCover$new() + result <- cloud$decode("6") + print(result) + + # Example: Decode surface wind + wind <- SurfaceWind$new() + result <- wind$decode("1506") + print(result) + + # Example: Decode full SYNOP + synop <- SYNOP$new() + synop_msg <- "AAXX 01004 88889 12782 61506 10094 20047 30111 40197 53007 60001 81541 333 81656 86070" + output <- synop$decode(synop_msg) + print(output) +} + + diff --git a/man/parser.Rd b/man/parser.Rd new file mode 100644 index 0000000..7c8e110 --- /dev/null +++ b/man/parser.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/parser.R +\name{parser} +\alias{parser} +\title{Parse SYNOP messages into structured lists} +\usage{ +parser(message, country = NULL, simplify = TRUE) +} +\arguments{ +\item{message}{Character vector with SYNOP messages.} + +\item{country}{Optional single character value passed to the precipitation +indicator decoder to adjust country-specific behaviour (e.g. \code{"RU"}).} + +\item{simplify}{Logical. If \code{TRUE} (default) and a single message is +provided, the function returns the decoded list directly instead of a +length-one list.} +} +\value{ +A list of decoded SYNOP messages. When \code{simplify = TRUE} and a single +message is supplied, the corresponding decoded list is returned directly. +} +\description{ +This function wraps the SYNOP decoding logic that was previously distributed +with the package in \code{inst/extdata}. It parses one or more SYNOP messages and +returns their structured representation as generated by the \code{SYNOP} R6 +decoder. +} +\examples{ +parser("AAXX 01004 88889 12782 61506 10094 20047 30111 40197 53007 60001 81541") +parser(rep("AAXX 01004 88889 12782 61506 10094 20047 30111 40197 53007 60001 81541", 2), simplify = FALSE) +}