Parse APRS-style location out of beacons #10

Open
opened 2025-10-14 02:46:46 +00:00 by w1cdn · 7 comments
Owner

Separating this from #5.

Separating this from #5.
Author
Owner

Otherwise, N6CTA's mobile BBS won't show up in the right place. :-)

Otherwise, N6CTA's mobile BBS won't show up in the right place. :-)
Author
Owner
https://rust-lang-nursery.github.io/rust-cookbook/text/regex.html
Author
Owner

https://www.aprs.org/doc/APRS101.PDF

In generic format examples, the latitude is shown as the 8-character string
ddmm.hhN (i.e. degrees, minutes and hundredths of a minute north).
In generic format examples, the longitude is shown as the 9-character string
dddmm.hhW (i.e. degrees, minutes and hundredths of a minute west).

Should probably split these into two regex statements.

image.png https://regex101.com/
https://www.aprs.org/doc/APRS101.PDF > In generic format examples, the latitude is shown as the 8-character string `ddmm.hhN` (i.e. degrees, minutes and hundredths of a minute north). > In generic format examples, the longitude is shown as the 9-character string `dddmm.hhW` (i.e. degrees, minutes and hundredths of a minute west). Should probably split these into two regex statements. <img width="984" alt="image.png" src="attachments/7c9eed88-2c85-469f-9fe5-c78f462b8f6c"> https://regex101.com/
342 KiB
Author
Owner

Made a ton of progress tonight. This should be able to be turned into a function. Not committed to this repo yet. I learned a lot about capture groups!

Currently it does account for the position ambiguity in the APRS spec (you can put spaces in), but I ran out of time to

  • fix the values if you put in spaces for mm and
  • apply the same ambiguity to lon if ambiguity is not defined with spaces
  • properly error out if it can't parse any location

In some instances — for example, where the exact position is not known — the sending station may wish to reduce the number of digits of precision in the latitude and longitude. In this case, the mm and hh digits in the latitude may be progressively replaced by a V (space) character as the amount of imprecision increases. [...]

use regex::Regex;

fn main() {
    
    //let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN";
    let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !4756.48N/09701.61WB -1/B -7/N EN17lw Net105&FARPN";
    
    let re_loc = Regex::new(r"(?P<latd>\d\d)(?P<latm>[\d ][\d ]\.[\d ][\d ])(?P<ns>[NS])/(?P<lond>\d\d\d)(?P<lonm>[\d ][\d ]\.[\d ][\d ])(?P<ew>[EW])").unwrap();
    let loc = re_loc.captures(&packet).unwrap();
    //println!("latd {} {} lond {} {}", &loc["latd"], &loc["ns"], &loc["lond"], &loc["ew"]);
    
    
    // Oh no, I have to convert these to decimal degrees...
    /*
    https://www.aprs.org/doc/APRS101.PDF
    - In generic format examples, the latitude is shown as the 8-character string
    ddmm.hhN (i.e. degrees, minutes and hundredths of a minute north).
    - In generic format examples, the longitude is shown as the 9-character string
    dddmm.hhW (i.e. degrees, minutes and hundredths of a minute west).
    */
    
    // Convert to decimal degrees
    let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].trim().parse().expect("Expects a number!") / 60.0);
    // If south, make negative
    if &loc["ns"] == "S" {
        lat_dec = lat_dec * -1.0
    }
    //println!("lat {}", lat_dec);
    let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].trim().parse().expect("Expects a number!") / 60.0);
    // If west, make negative
    if &loc["ew"] == "W" {
        lon_dec = lon_dec * -1.0
    }
    //println!("lon {}", lon_dec);
    
    // String to paste into map for testing
    println!("{}, {}", lat_dec, lon_dec);
    
}
image.png
Made a ton of progress tonight. This should be able to be turned into a function. Not committed to this repo yet. I learned a lot about capture groups! Currently it *does* account for the position ambiguity in the APRS spec (you can put spaces in), but I ran out of time to - [x] fix the values if you put in spaces for `mm` and - [ ] apply the same ambiguity to `lon` if ambiguity is not defined with spaces - [x] properly error out if it can't parse any location > In some instances — for example, where the exact position is not known — the sending station may wish to reduce the number of digits of precision in the latitude and longitude. In this case, the mm and hh digits in the latitude may be progressively replaced by a V (space) character as the amount of imprecision increases. [...] ``` use regex::Regex; fn main() { //let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN"; let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !4756.48N/09701.61WB -1/B -7/N EN17lw Net105&FARPN"; let re_loc = Regex::new(r"(?P<latd>\d\d)(?P<latm>[\d ][\d ]\.[\d ][\d ])(?P<ns>[NS])/(?P<lond>\d\d\d)(?P<lonm>[\d ][\d ]\.[\d ][\d ])(?P<ew>[EW])").unwrap(); let loc = re_loc.captures(&packet).unwrap(); //println!("latd {} {} lond {} {}", &loc["latd"], &loc["ns"], &loc["lond"], &loc["ew"]); // Oh no, I have to convert these to decimal degrees... /* https://www.aprs.org/doc/APRS101.PDF - In generic format examples, the latitude is shown as the 8-character string ddmm.hhN (i.e. degrees, minutes and hundredths of a minute north). - In generic format examples, the longitude is shown as the 9-character string dddmm.hhW (i.e. degrees, minutes and hundredths of a minute west). */ // Convert to decimal degrees let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].trim().parse().expect("Expects a number!") / 60.0); // If south, make negative if &loc["ns"] == "S" { lat_dec = lat_dec * -1.0 } //println!("lat {}", lat_dec); let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].trim().parse().expect("Expects a number!") / 60.0); // If west, make negative if &loc["ew"] == "W" { lon_dec = lon_dec * -1.0 } //println!("lon {}", lon_dec); // String to paste into map for testing println!("{}, {}", lat_dec, lon_dec); } ``` <img width="989" alt="image.png" src="attachments/35e61d56-ee7b-47fb-bc79-93969ebfa083">
356 KiB
Author
Owner

I guess I could just replace the spaces with either

  • zeros
  • random digits

before converting string to numeric.

I guess I could just replace the spaces with either - zeros - random digits before converting string to numeric.
Author
Owner

Another snapshot. This replaces ambiguation spaces with a random integer, but I didn't carry the same pattern over yet.

  • check that all capture groups have values
use regex::Regex;
use rand::Rng;

fn main() {
    
    //let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN";
    let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !47  .  n/09701.61WB -1/B -7/N EN17lw Net105&FARPN";
    
    // Capture different pieces of the location stringf
    let re_loc = Regex::new(r"(?P<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[ewEW])").unwrap();
    let loc = re_loc.captures(&packet).unwrap();
    //println!("latd {} {} lond {} {}", &loc["latd"], &loc["ns"], &loc["lond"], &loc["ew"]);
    
    // TODO check that each capture group has information, otherwise return nothing
    
    // Oh no, I have to convert these to decimal degrees...
    /*
    https://www.aprs.org/doc/APRS101.PDF
    - In generic format examples, the latitude is shown as the 8-character string
    ddmm.hhN (i.e. degrees, minutes and hundredths of a minute north).
    - In generic format examples, the longitude is shown as the 9-character string
    dddmm.hhW (i.e. degrees, minutes and hundredths of a minute west).
    */
    
    // Initiate randomness for ambiguity..
    let mut rng = rand::rng();
    
    // Convert to decimal degrees. If ambiguity spaces are included (see APRS spec), replace them with 1s.
    let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0);
    // If south, make negative
    if &loc["ns"] == "S" {
        lat_dec = lat_dec * -1.0
    }
    
    // TODO if there are spaces in loc["latm"], the same spaces need to be in loc["lonm"] for proper ambiguity according to APRS spec
    
    
    let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0);
    // If west, make negative
    if &loc["ew"] == "W" {
        lon_dec = lon_dec * -1.0
    }
    
    // String to paste into map for testing
    println!("{}, {}", lat_dec, lon_dec);
    
}

Another snapshot. This replaces ambiguation spaces with a random integer, but I didn't carry the same pattern over yet. - [x] check that all capture groups have values ``` use regex::Regex; use rand::Rng; fn main() { //let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN"; let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !47 . n/09701.61WB -1/B -7/N EN17lw Net105&FARPN"; // Capture different pieces of the location stringf let re_loc = Regex::new(r"(?P<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[ewEW])").unwrap(); let loc = re_loc.captures(&packet).unwrap(); //println!("latd {} {} lond {} {}", &loc["latd"], &loc["ns"], &loc["lond"], &loc["ew"]); // TODO check that each capture group has information, otherwise return nothing // Oh no, I have to convert these to decimal degrees... /* https://www.aprs.org/doc/APRS101.PDF - In generic format examples, the latitude is shown as the 8-character string ddmm.hhN (i.e. degrees, minutes and hundredths of a minute north). - In generic format examples, the longitude is shown as the 9-character string dddmm.hhW (i.e. degrees, minutes and hundredths of a minute west). */ // Initiate randomness for ambiguity.. let mut rng = rand::rng(); // Convert to decimal degrees. If ambiguity spaces are included (see APRS spec), replace them with 1s. let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); // If south, make negative if &loc["ns"] == "S" { lat_dec = lat_dec * -1.0 } // TODO if there are spaces in loc["latm"], the same spaces need to be in loc["lonm"] for proper ambiguity according to APRS spec let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); // If west, make negative if &loc["ew"] == "W" { lon_dec = lon_dec * -1.0 } // String to paste into map for testing println!("{}, {}", lat_dec, lon_dec); } ```
Author
Owner

More progress. I got really slowed down looking for a way to return a None tuple that was also f64.

use regex::Regex;
use rand::Rng;

fn main() {
    
//let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN";  // full location
let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN";     // missing capture group N
//let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !47  .  n/09701.61WB -1/B -7/N EN17lw Net105&FARPN";                 // ambiguity
//let packet = "	1:Fm K7SKI To BEACON <UI pid=F0 Len=58 > K7SKI-1/mail K7SKI-7/K-node and hf-vhf gateway Boise, ID";         // no location

let (a, b) = aprs_loc(packet);
println!("{}, {}", a, b);

// Mimic checks that need to be done on the function result
if a == -9999.0_f64 {
    println!("lat is null, don't include it in the spot")
}
if b == -9999.0_f64 {
    println!("lon is null, don't include it in the spot")
}
   
    
}

fn aprs_loc(packet: &str) -> (f64, f64) {
    
    // Capture different pieces of the location stringf
    let re_loc = Regex::new(r"(?P<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[ewEW])").unwrap();
    
    // Only proceed if there were captures
    match re_loc.captures(&packet) {
    Some(_caps) => {
        
        let loc = re_loc.captures(&packet).unwrap();
                        
        // Initiate randomness for ambiguity..
        let mut rng = rand::rng();
        
        // Convert to decimal degrees. If ambiguity spaces are included (see APRS spec), replace them with 1s.
        let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0);
        // If south, make negative
        if &loc["ns"] == "S" {
            lat_dec = lat_dec * -1.0
        }
        
        // TODO if there are spaces in loc["latm"], the same spaces need to be in loc["lonm"] for proper ambiguity according to APRS spec
        
        let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0);
        // If west, make negative
        if &loc["ew"] == "W" {
            lon_dec = lon_dec * -1.0
        }
        
        // String to paste into map for testing
        //println!("{}, {}", lat_dec, lon_dec);
      
        // Return
       (lat_dec, lon_dec)
        
    }
    // Otherwise if there were no captures, return bad data
    None => {
        // The regex did not match. Deal with it here!
        (-9999.0_f64, -9999.0_f64)
    }
}
}
More progress. I got really slowed down looking for a way to return a None tuple that was also f64. ``` use regex::Regex; use rand::Rng; fn main() { //let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08N/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN"; // full location let packet = "1:Fm KC2IHX-7 To BEACON <UI pid=F0 Len=64 > !4603.08/11819.83WB -1/B -7/N 14105/7140LSB DN06ub Net105&FARPN"; // missing capture group N //let packet = "1:Fm W1CDN-7 To BEACON <UI pid=F0 Len=50 > !47 . n/09701.61WB -1/B -7/N EN17lw Net105&FARPN"; // ambiguity //let packet = " 1:Fm K7SKI To BEACON <UI pid=F0 Len=58 > K7SKI-1/mail K7SKI-7/K-node and hf-vhf gateway Boise, ID"; // no location let (a, b) = aprs_loc(packet); println!("{}, {}", a, b); // Mimic checks that need to be done on the function result if a == -9999.0_f64 { println!("lat is null, don't include it in the spot") } if b == -9999.0_f64 { println!("lon is null, don't include it in the spot") } } fn aprs_loc(packet: &str) -> (f64, f64) { // Capture different pieces of the location stringf let re_loc = Regex::new(r"(?P<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[ewEW])").unwrap(); // Only proceed if there were captures match re_loc.captures(&packet) { Some(_caps) => { let loc = re_loc.captures(&packet).unwrap(); // Initiate randomness for ambiguity.. let mut rng = rand::rng(); // Convert to decimal degrees. If ambiguity spaces are included (see APRS spec), replace them with 1s. let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); // If south, make negative if &loc["ns"] == "S" { lat_dec = lat_dec * -1.0 } // TODO if there are spaces in loc["latm"], the same spaces need to be in loc["lonm"] for proper ambiguity according to APRS spec let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); // If west, make negative if &loc["ew"] == "W" { lon_dec = lon_dec * -1.0 } // String to paste into map for testing //println!("{}, {}", lat_dec, lon_dec); // Return (lat_dec, lon_dec) } // Otherwise if there were no captures, return bad data None => { // The regex did not match. Deal with it here! (-9999.0_f64, -9999.0_f64) } } } ```
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: w1cdn/mwtchahrd#10
No description provided.