1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
 * Copyright (C) 2018 Kubos Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use chrono::{DateTime, NaiveDateTime, Utc};
use nom::simple_errors::Context;
use nom::{
    float, map_res, multispace, tag, take_until, take_until_and_consume, Err, ErrorKind, IResult,
};
use std::str::from_utf8;

#[derive(Debug, PartialEq)]
/// Struct for storing geo-records.
pub struct GeoRecord {
    /// Modem' longitude at the time of the last connection
    pub lon: f32,
    /// Modem's latitude at the time of the last connection
    pub lat: f32,
    /// Time of modem's last connection
    pub time: i64,
    /// Approximate error of location data (<300m to <100km)
    pub max_error: u32,
}

fn parse_coord(input: &[u8]) -> IResult<&[u8], f32> {
    let (input, _) = multispace(input)?;
    let (input, d) = float(input)?;
    let (input, _) = multispace(input)?;
    let (input, m) = float(input)?;
    let (input, _) = multispace(input)?;
    let (input, s) = float(input)?;
    Ok((input, d + m / 60.0 + s / 3600.0))
}

fn parse_date(input: &[u8]) -> IResult<&[u8], i64> {
    let (input, _) = take_until_and_consume!(input, "TIME:")?;
    let (input, date) = map_res!(input, take_until!("\n"), from_utf8)?;
    let dt = DateTime::<Utc>::from_utc(
        NaiveDateTime::parse_from_str(date, "%d %m %Y %H:%M:%S")
            .or_else(|_| Err(Err::Error(Context::Code(input, ErrorKind::Tag))))?,
        Utc,
    );
    Ok((input, dt.timestamp()))
}

impl GeoRecord {
    // Fields are left justified and the entire record is end padded with spaces to a fixed 90 byte length.
    // N:DDD MM SS
    // W:DDD MM SS
    // TIME: DD MM YYYY HH:MM:SS
    // ERR:<300m to <100km
    /// Parse GeoRecord
    pub fn parse(input: &[u8]) -> IResult<&[u8], GeoRecord> {
        let (input, _) = take_until_and_consume!(input, "GU")?;
        let (input, _) = take_until_and_consume!(input, "N:")?;
        let (input, n) = parse_coord(input)?;
        let (input, _) = take_until_and_consume!(input, "W:")?;
        let (input, w) = parse_coord(input)?;
        let (input, time) = parse_date(input)?;
        let (input, _) = take_until_and_consume!(input, "ERR: < ")?;
        let (input, max_error) = float(input)?;
        let max_error = max_error as u32;
        let (input, _) = multispace(input)?;
        let (input, unit) = take_until!(input, "\n")?;
        let (input, _) = multispace(input)?;
        let (input, _) = tag!(input, "OK")?;
        let (input, _) = multispace(input)?;
        println!("unit {:?}", unit);
        let max_error = match unit {
            b"km" => max_error * 1000,
            _ => max_error,
        };

        Ok((
            input,
            GeoRecord {
                lon: -w,
                lat: n,
                time,
                max_error,
            },
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_parses() {
        assert_eq!(
            Ok((
                &b"extra"[..],
                GeoRecord {
                    lat: 40.482502,
                    lon: -85.49389,
                    time: 1514898642,
                    max_error: 5000,
                },
            )),
            GeoRecord::parse(b"GU\nN: 040 28 57\nW: 085 29 38\nTIME: 02 01 2018 13:10:42\nERR: < 5 km\n\nOK\n                     extra")
        )
    }
}