use crate::proto::osmformat::PrimitiveBlock;
use chrono::{DateTime, Utc};
pub struct FieldCodec {
date_granularity: i32,
granularity: i32,
lat_offset: i64,
lon_offset: i64,
string_table: Vec<String>,
}
impl FieldCodec {
pub fn new(granularity: i32, date_granularity: i32) -> Self {
Self {
date_granularity,
granularity,
lat_offset: 0,
lon_offset: 0,
string_table: Vec::new(),
}
}
pub fn new_with_block(block: &PrimitiveBlock) -> anyhow::Result<Self> {
let granularity = block.get_granularity();
let date_granularity = block.get_date_granularity();
if granularity <= 0 {
bail!("invalid block granularity: {}", granularity);
}
if date_granularity <= 0 {
bail!("invalid block date_granularity: {}", date_granularity);
}
let bytes_array = block.get_stringtable().get_s();
let string_table = if bytes_array.is_empty() {
Vec::with_capacity(0)
} else {
bytes_array
.iter()
.map(|bytes| match String::from_utf8(bytes.clone()) {
Ok(str) => str,
Err(err) => {
eprintln!("{}", err);
String::new()
}
})
.collect::<Vec<String>>()
};
Ok(Self {
date_granularity,
granularity,
lat_offset: block.get_lat_offset(),
lon_offset: block.get_lon_offset(),
string_table,
})
}
pub fn encode_latitude(&self, latitude: i64) -> i64 {
(latitude - self.lat_offset) / self.granularity as i64
}
pub fn decode_latitude(&self, raw_latitude: i64) -> anyhow::Result<i64> {
let scaled = (self.granularity as i64)
.checked_mul(raw_latitude)
.ok_or_else(|| anyhow!("latitude value overflows i64: {}", raw_latitude))?;
self.lat_offset
.checked_add(scaled)
.ok_or_else(|| anyhow!("latitude offset overflows i64"))
}
pub fn encode_longitude(&self, longitude: i64) -> i64 {
(longitude - self.lon_offset) / self.granularity as i64
}
pub fn decode_longitude(&self, raw_longitude: i64) -> anyhow::Result<i64> {
let scaled = (self.granularity as i64)
.checked_mul(raw_longitude)
.ok_or_else(|| anyhow!("longitude value overflows i64: {}", raw_longitude))?;
self.lon_offset
.checked_add(scaled)
.ok_or_else(|| anyhow!("longitude offset overflows i64"))
}
pub fn encode_timestamp(&self, time: DateTime<Utc>) -> i64 {
time.timestamp_millis() / self.date_granularity as i64
}
pub fn decode_timestamp(&self, raw_timestamp: i64) -> anyhow::Result<DateTime<Utc>> {
let timestamp = (self.date_granularity as i64)
.checked_mul(raw_timestamp)
.ok_or_else(|| anyhow!("timestamp value overflows i64: {}", raw_timestamp))?;
DateTime::from_timestamp_millis(timestamp)
.ok_or_else(|| anyhow!("timestamp out of range: {}", timestamp))
}
pub fn decode_string(&self, string_id: usize) -> String {
match self.string_table.get(string_id) {
None => {
eprintln!("no matched string table id: {}", string_id);
String::new()
}
Some(s) => s.to_owned(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::osmformat::PrimitiveBlock;
fn block_with(granularity: i32, date_granularity: i32) -> PrimitiveBlock {
let mut block = PrimitiveBlock::new();
block.set_granularity(granularity);
block.set_date_granularity(date_granularity);
block
}
#[test]
fn new_with_block_rejects_zero_or_negative_granularity() {
assert!(FieldCodec::new_with_block(&block_with(0, 1000)).is_err());
assert!(FieldCodec::new_with_block(&block_with(-100, 1000)).is_err());
assert!(FieldCodec::new_with_block(&block_with(100, 0)).is_err());
assert!(FieldCodec::new_with_block(&block_with(100, -1000)).is_err());
}
#[test]
fn new_with_block_accepts_valid_granularity() {
assert!(FieldCodec::new_with_block(&block_with(100, 1000)).is_ok());
}
#[test]
fn decode_latitude_overflow_is_an_error_not_a_panic() {
let codec = FieldCodec::new_with_block(&block_with(100, 1000)).unwrap();
assert!(codec.decode_latitude(i64::MAX).is_err());
assert!(codec.decode_latitude(i64::MIN).is_err());
let mut block = block_with(100, 1000);
block.set_lat_offset(i64::MAX);
let codec = FieldCodec::new_with_block(&block).unwrap();
assert!(codec.decode_latitude(1).is_err());
}
#[test]
fn decode_timestamp_overflow_is_an_error_not_a_panic() {
let codec = FieldCodec::new_with_block(&block_with(100, 1000)).unwrap();
assert!(codec.decode_timestamp(i64::MAX).is_err());
assert!(codec.decode_timestamp(i64::MIN).is_err());
}
#[test]
fn roundtrip_scalars() {
let codec = FieldCodec::new_with_block(&block_with(100, 1000)).unwrap();
let lat = 1_234_567_800;
let lon = -987_654_300;
assert_eq!(
codec.decode_latitude(codec.encode_latitude(lat)).unwrap(),
lat
);
assert_eq!(
codec.decode_longitude(codec.encode_longitude(lon)).unwrap(),
lon
);
}
}