///|
fn print_usage() -> Unit {
println("MoonSatKit - satellite propagation and pass prediction CLI")
println("")
println("Usage:")
println(" moonsatkit tle validate ")
println(" moonsatkit time parse ")
println(" moonsatkit propagate --at ")
println(
" moonsatkit passes --station --from --hours --min-elevation [--format json|csv]",
)
println(
" moonsatkit track --from --seconds --step ",
)
println(" moonsatkit eclipse --at ")
}
///|
fn fail(message : String) -> Unit {
println("error: \{message}")
}
///|
fn parse_decimal(text : StringView) -> Double? {
if text.is_empty() {
return None
}
let mut sign = 1.0
let mut value = 0.0
let mut fraction_scale = 0.1
let mut seen_digit = false
let mut seen_decimal = false
let mut valid = true
let mut index = 0
text
.iter()
.each(char => {
if index == 0 && char == '-' {
sign = -1.0
} else if index == 0 && char == '+' {
()
} else if char >= '0' && char <= '9' {
seen_digit = true
if seen_decimal {
value = value +
(char.to_int() - '0'.to_int()).to_double() * fraction_scale
fraction_scale = fraction_scale * 0.1
} else {
value = value * 10.0 + (char.to_int() - '0'.to_int()).to_double()
}
} else if char == '.' && !seen_decimal {
seen_decimal = true
} else {
valid = false
}
index = index + 1
})
if valid && seen_digit {
Some(sign * value)
} else {
None
}
}
///|
fn parse_integer(text : StringView) -> Int? {
if text.is_empty() {
return None
}
let mut sign = 1
let mut value = 0
let mut seen_digit = false
let mut valid = true
let mut index = 0
text
.iter()
.each(char => {
if index == 0 && char == '-' {
sign = -1
} else if index == 0 && char == '+' {
()
} else if char >= '0' && char <= '9' {
seen_digit = true
value = value * 10 + char.to_int() - '0'.to_int()
} else {
valid = false
}
index = index + 1
})
if valid && seen_digit {
Some(sign * value)
} else {
None
}
}
///|
fn option_value(args : Array[String], name : String) -> String? {
let mut index = 0
let mut result : String? = None
while index + 1 < args.length() {
if args[index] == name {
result = Some(args[index + 1])
}
index = index + 1
}
result
}
///|
fn parse_station(text : String) -> @lib.GroundStation? {
let parts = text.split(",").to_array()
if parts.length() != 3 {
return None
}
let latitude = match parse_decimal(parts[0]) {
Some(value) => value
None => return None
}
let longitude = match parse_decimal(parts[1]) {
Some(value) => value
None => return None
}
let altitude = match parse_decimal(parts[2]) {
Some(value) => value
None => return None
}
Some({
latitude_deg: latitude,
longitude_deg: longitude,
altitude_m: altitude,
})
}
///|
fn parse_tle_args(args : Array[String]) -> Result[@lib.Tle, String] {
if args.length() < 4 {
return Err("expected two TLE lines")
}
match @lib.validate_tle(args[2], args[3]) {
Ok(tle) => Ok(tle)
Err(error) => Err(error.to_string())
}
}
///|
fn run_propagate(args : Array[String]) -> Unit {
if args.length() != 6 || args[4] != "--at" {
fail("expected: propagate --at ")
return
}
let tle = match parse_tle_args(args) {
Ok(value) => value
Err(message) => {
fail(message)
return
}
}
let instant = match @lib.parse_utc_datetime(args[5]) {
Ok(value) => value
Err(error) => {
fail(error.to_string())
return
}
}
match @lib.propagate_sgp4(tle, instant) {
Ok(state) => println(@lib.orbit_state_to_json(state))
Err(error) => fail(error.to_string())
}
}
///|
fn run_passes(args : Array[String]) -> Unit {
let tle = match parse_tle_args(args) {
Ok(value) => value
Err(message) => {
fail(message)
return
}
}
let station_text = match option_value(args, "--station") {
Some(value) => value
None => {
fail("missing --station ")
return
}
}
let station = match parse_station(station_text) {
Some(value) => value
None => {
fail("invalid station; expected latitude,longitude,altitude_m")
return
}
}
let from_text = match option_value(args, "--from") {
Some(value) => value
None => {
fail("missing --from ")
return
}
}
let from = match @lib.parse_utc_datetime(from_text) {
Ok(value) => value
Err(error) => {
fail(error.to_string())
return
}
}
let hours_text = match option_value(args, "--hours") {
Some(value) => value
None => {
fail("missing --hours ")
return
}
}
let hours = match parse_integer(hours_text) {
Some(value) => value
None => {
fail("hours must be an integer")
return
}
}
let minimum_text = match option_value(args, "--min-elevation") {
Some(value) => value
None => {
fail("missing --min-elevation ")
return
}
}
let minimum = match parse_decimal(minimum_text) {
Some(value) => value
None => {
fail("minimum elevation must be a number")
return
}
}
let format = match option_value(args, "--format") {
Some(value) => value
None => "json"
}
let passes = match
@lib.predict_passes_sgp4(tle, station, from, hours, minimum) {
Ok(value) => value
Err(error) => {
fail(error.to_string())
return
}
}
match format {
"json" => println(@lib.pass_windows_to_json(passes))
"csv" => println(@lib.pass_windows_to_csv(passes))
_ => fail("format must be json or csv")
}
}
///|
fn run_track(args : Array[String]) -> Unit {
let tle = match parse_tle_args(args) {
Ok(value) => value
Err(message) => {
fail(message)
return
}
}
let from_text = match option_value(args, "--from") {
Some(value) => value
None => {
fail("missing --from ")
return
}
}
let from = match @lib.parse_utc_datetime(from_text) {
Ok(value) => value
Err(error) => {
fail(error.to_string())
return
}
}
let seconds_text = match option_value(args, "--seconds") {
Some(value) => value
None => {
fail("missing --seconds ")
return
}
}
let seconds = match parse_integer(seconds_text) {
Some(value) => value
None => {
fail("seconds must be an integer")
return
}
}
let step_text = match option_value(args, "--step") {
Some(value) => value
None => {
fail("missing --step ")
return
}
}
let step = match parse_integer(step_text) {
Some(value) => value
None => {
fail("step must be an integer")
return
}
}
match @lib.track_geojson_sgp4(tle, from, seconds, step) {
Ok(geojson) => println(geojson)
Err(error) => fail(error.to_string())
}
}
///|
fn run_eclipse(args : Array[String]) -> Unit {
if args.length() != 6 || args[4] != "--at" {
fail("expected: eclipse --at ")
return
}
let tle = match parse_tle_args(args) {
Ok(value) => value
Err(message) => {
fail(message)
return
}
}
let instant = match @lib.parse_utc_datetime(args[5]) {
Ok(value) => value
Err(error) => {
fail(error.to_string())
return
}
}
match @lib.eclipse_report_to_json(tle, instant) {
Ok(value) => println(value)
Err(error) => fail(error.to_string())
}
}
///|
fn main {
let args = @env.args()
if args.length() < 2 {
print_usage()
return
}
match args[1] {
"help" | "--help" | "-h" => print_usage()
"tle" => {
if args.length() != 5 || args[2] != "validate" {
fail("expected: tle validate ")
return
}
match @lib.validate_tle(args[3], args[4]) {
Ok(_) => println("valid")
Err(error) => fail(error.to_string())
}
}
"time" => {
if args.length() != 4 || args[2] != "parse" {
fail("expected: time parse ")
return
}
match @lib.parse_utc_datetime(args[3]) {
Ok(value) => println(value.to_string())
Err(error) => fail(error.to_string())
}
}
"propagate" => run_propagate(args)
"passes" => run_passes(args)
"track" => run_track(args)
"eclipse" => run_eclipse(args)
command => {
fail("unknown command '\{command}'")
print_usage()
}
}
}