package grib import cats.effect._ import fs2.io.file.{Files, Path} import java.nio.ByteBuffer import java.time.temporal.ChronoUnit import java.time.{ZoneOffset, ZonedDateTime} object GribParser { def parseFile(path: Path): IO[List[Grib]] = { def loop(ptr: Long, acc: List[Grib]): IO[List[Grib]] = { for { fileSize <- Files[IO].size(path) result <- if (ptr >= fileSize) { IO.pure(acc.reverse) } else { for { grib <- parseGrib(path, ptr) nextResult <- loop(ptr + grib.length, grib :: acc) } yield nextResult } } yield result } loop(0L, List.empty) } private def parseGrib(path: Path, ptr: Long): IO[Grib] = { for { res0 <- parse0(path, ptr) (version, discipline, gribLength, len0) = res0 ptr1 = ptr + len0 res1 <- parse1(path, ptr1) (referenceTime, len1) = res1 ptr3 = ptr1 + len1 res3 <- parse3(path, ptr3) (grid, len3) = res3 ptr4 = ptr3 + len3 res4 <- parse4(path, ptr4, discipline, referenceTime) (meteo, time, len4) = res4 // _ <- IO.println(time) ptr5 = ptr4 + len4 res5 <- parse5(path, ptr5) (conversion, bitsPerDataPoint, len5) = res5 ptr6 = ptr5 + len5 len6 <- parse6(path, ptr6) ptr7 = ptr6 + len6 len7 <- parse7(path, ptr7) sections = List( GribSection(0, ptr, len0), GribSection(1, ptr1, len1), GribSection(3, ptr3, len3), GribSection(4, ptr4, len4), GribSection(5, ptr5, len5), GribSection(6, ptr6, len6), GribSection(7, ptr7, len7), ) title = Codes.codesToString(meteo.discipline, meteo.category, meteo.product) grib = Grib(version, gribLength, title, grid, meteo, time, conversion, bitsPerDataPoint, sections) } yield grib } private def parse0(path: Path, ptr: Long): IO[(Int, Int, Long, Int)] = { val length = 16 for { bytes <- readBytes(path, ptr, length) identifier = new String(bytes.take(4), "ASCII") // GRIB discipline = bytes(6) version = bytes(7) gribLength = ByteBuffer.wrap(bytes.slice(8, 16)).getLong } yield (version, discipline, gribLength, length) } private def parse1(path: Path, ptr: Long): IO[(ZonedDateTime, Int)] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt year = ByteBuffer.wrap(bytes.slice(12, 14)).getShort month = bytes(14) day = bytes(15) hour = bytes(16) minute = bytes(17) second = bytes(18) referenceTime = ZonedDateTime.of( year, month, day, hour, minute, second, 0, // last 0 is nanos ZoneOffset.UTC // This is what 'Z' represents - UTC/Zero offset ) // result = (f"$year%04d-$month%02d-$day%02dT$hour%02d:$minute%02d:$second%02dZ", length) } yield (referenceTime, length) } private def parse3(path: Path, ptr: Long): IO[(GribGrid, Int)] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt template = ByteBuffer.wrap(bytes.slice(12, 14)).getShort cols = ByteBuffer.wrap(bytes.slice(30, 34)).getInt rows = ByteBuffer.wrap(bytes.slice(34, 38)).getInt } yield (GribGrid(template, cols, rows), length) } private def parse4(path: Path, ptr: Long, discipline: Int, referenceTime: ZonedDateTime): IO[(MeteoParam, GribTime, Int)] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt category = bytes(9) & 0xFF // to uint product = bytes(10) & 0xFF subtype = if (bytes(8) == 1) "now" else "period" levelType = bytes(22) & 0xFF levelValue = bytes(27) & 0xFF meteoParam = MeteoParam(discipline, category, product, subtype, levelType, levelValue) forecastTime = if (bytes(8) == 1) { val leadTime = ByteBuffer.wrap(bytes.slice(20, 22)).getShort referenceTime.toInstant.plus(leadTime, ChronoUnit.HOURS) } else { val year = ByteBuffer.wrap(bytes.slice(37, 39)).getShort val month = bytes(39) val day = bytes(40) val hour = bytes(41) val minute = bytes(42) val second = bytes(43) ZonedDateTime.of( year, month, day, hour, minute, second, 0, // last 0 is nanos ZoneOffset.UTC // This is what 'Z' represents - UTC/Zero offset ).toInstant } time = GribTime(referenceTime.toString, forecastTime.toString) } yield (meteoParam, time, length) } private def parse5(path: Path, ptr: Long): IO[(MeteoConversion, Int, Int)] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt bitsPerDataPoint = bytes(19).toInt reference = ByteBuffer.wrap(bytes.slice(11, 15)).getFloat binaryScale = -(bytes(16) & 0xFF) decimalScale = bytes(18) & 0xFF conversion = MeteoConversion(reference, binaryScale, decimalScale) } yield (conversion, bitsPerDataPoint, length) } private def parse6(path: Path, ptr: Long): IO[Int] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt } yield length } private def parse7(path: Path, ptr: Long): IO[Int] = { for { bytes <- readBytes(path, ptr, 64) length = ByteBuffer.wrap(bytes.slice(0, 4)).getInt } yield length } private def readBytes(path: Path, ptr: Long, len: Int): IO[Array[Byte]] = { Files[IO].readRange(path, 1024, ptr, ptr + len) .compile .to(Array) } }