use crate::{
parser::{parse_subset, Parser},
tokenizer::Tokenizer,
types::{custom::UserDefinedTag, note::Note, source::citation::Citation},
GedcomError,
};
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct Place {
pub value: Option<String>,
pub form: Option<String>,
pub map: Option<MapCoordinates>,
pub phonetic: Vec<PlaceVariation>,
pub romanized: Vec<PlaceVariation>,
pub notes: Vec<Note>,
pub external_ids: Vec<String>,
pub citations: Vec<Citation>,
pub custom_data: Vec<Box<UserDefinedTag>>,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct MapCoordinates {
pub latitude: Option<String>,
pub longitude: Option<String>,
}
impl MapCoordinates {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<MapCoordinates, GedcomError> {
let mut map = MapCoordinates::default();
map.parse(tokenizer, level)?;
Ok(map)
}
#[must_use]
pub fn with_coordinates(latitude: &str, longitude: &str) -> Self {
MapCoordinates {
latitude: Some(latitude.to_string()),
longitude: Some(longitude.to_string()),
}
}
#[must_use]
pub fn latitude_decimal(&self) -> Option<f64> {
self.latitude.as_ref().and_then(|lat| parse_coordinate(lat))
}
#[must_use]
pub fn longitude_decimal(&self) -> Option<f64> {
self.longitude
.as_ref()
.and_then(|lon| parse_coordinate(lon))
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.latitude.is_some() && self.longitude.is_some()
}
}
fn parse_coordinate(coord: &str) -> Option<f64> {
let trimmed = coord.trim();
if trimmed.is_empty() {
return None;
}
let first_char = trimmed.chars().next()?;
match first_char {
'N' | 'E' => trimmed[1..].parse().ok(),
'S' | 'W' => trimmed[1..].parse::<f64>().ok().map(|v| -v),
_ => trimmed.parse().ok(),
}
}
impl Parser for MapCoordinates {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
tokenizer.next_token()?;
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"LATI" => self.latitude = Some(tokenizer.take_line_value()?),
"LONG" => self.longitude = Some(tokenizer.take_line_value()?),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled MapCoordinates Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct PlaceVariation {
pub value: String,
pub variation_type: Option<String>,
}
impl PlaceVariation {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<PlaceVariation, GedcomError> {
let mut variation = PlaceVariation {
value: tokenizer.take_line_value()?,
variation_type: None,
};
variation.parse(tokenizer, level)?;
Ok(variation)
}
#[must_use]
pub fn with_type(value: &str, variation_type: &str) -> Self {
PlaceVariation {
value: value.to_string(),
variation_type: Some(variation_type.to_string()),
}
}
}
impl Parser for PlaceVariation {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"TYPE" => self.variation_type = Some(tokenizer.take_line_value()?),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled PlaceVariation Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
impl Place {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<Place, GedcomError> {
let mut place = Place {
value: Some(tokenizer.take_line_value()?),
..Default::default()
};
place.parse(tokenizer, level)?;
Ok(place)
}
#[must_use]
pub fn with_value(value: &str) -> Self {
Place {
value: Some(value.to_string()),
..Default::default()
}
}
pub fn set_coordinates(&mut self, latitude: &str, longitude: &str) {
self.map = Some(MapCoordinates::with_coordinates(latitude, longitude));
}
#[must_use]
pub fn latitude(&self) -> Option<f64> {
self.map.as_ref().and_then(MapCoordinates::latitude_decimal)
}
#[must_use]
pub fn longitude(&self) -> Option<f64> {
self.map
.as_ref()
.and_then(MapCoordinates::longitude_decimal)
}
#[must_use]
pub fn has_coordinates(&self) -> bool {
self.map.as_ref().is_some_and(MapCoordinates::is_complete)
}
pub fn add_phonetic(&mut self, variation: PlaceVariation) {
self.phonetic.push(variation);
}
pub fn add_romanized(&mut self, variation: PlaceVariation) {
self.romanized.push(variation);
}
#[must_use]
pub fn jurisdictions(&self) -> Vec<&str> {
self.value
.as_ref()
.map(|v| v.split(',').map(str::trim).collect())
.unwrap_or_default()
}
}
impl Parser for Place {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"FORM" => self.form = Some(tokenizer.take_line_value()?),
"MAP" => self.map = Some(MapCoordinates::new(tokenizer, level + 1)?),
"FONE" => self
.phonetic
.push(PlaceVariation::new(tokenizer, level + 1)?),
"ROMN" => self
.romanized
.push(PlaceVariation::new(tokenizer, level + 1)?),
"NOTE" => self.notes.push(Note::new(tokenizer, level + 1)?),
"SOUR" => self.citations.push(Citation::new(tokenizer, level + 1)?),
"EXID" => self.external_ids.push(tokenizer.take_line_value()?),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled Place Tag: {tag}"),
})
}
}
Ok(())
};
self.custom_data = parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_coordinate_north() {
assert!((parse_coordinate("N50.8333333").unwrap() - 50.833_333_3).abs() < 0.0001);
}
#[test]
fn test_parse_coordinate_south() {
assert!((parse_coordinate("S25.0667").unwrap() - (-25.0667)).abs() < 0.0001);
}
#[test]
fn test_parse_coordinate_east() {
assert!((parse_coordinate("E4.3333").unwrap() - 4.3333).abs() < 0.0001);
}
#[test]
fn test_parse_coordinate_west() {
assert!((parse_coordinate("W122.4194").unwrap() - (-122.4194)).abs() < 0.0001);
}
#[test]
fn test_parse_coordinate_decimal() {
assert!((parse_coordinate("50.8333").unwrap() - 50.8333).abs() < 0.0001);
assert!((parse_coordinate("-25.0667").unwrap() - (-25.0667)).abs() < 0.0001);
}
#[test]
fn test_map_coordinates_is_complete() {
let complete = MapCoordinates::with_coordinates("N50.0", "E4.0");
assert!(complete.is_complete());
let incomplete = MapCoordinates {
latitude: Some("N50.0".to_string()),
longitude: None,
};
assert!(!incomplete.is_complete());
}
#[test]
fn test_place_with_value() {
let place = Place::with_value("New York, New York, USA");
assert_eq!(place.value, Some("New York, New York, USA".to_string()));
}
#[test]
fn test_place_jurisdictions() {
let place = Place::with_value("City, County, State, Country");
let jurisdictions = place.jurisdictions();
assert_eq!(jurisdictions, vec!["City", "County", "State", "Country"]);
}
#[test]
fn test_place_set_coordinates() {
let mut place = Place::with_value("Paris, France");
place.set_coordinates("N48.8566", "E2.3522");
assert!(place.has_coordinates());
assert!((place.latitude().unwrap() - 48.8566).abs() < 0.0001);
assert!((place.longitude().unwrap() - 2.3522).abs() < 0.0001);
}
#[test]
fn test_place_variation_with_type() {
let variation = PlaceVariation::with_type("東京", "kana");
assert_eq!(variation.value, "東京");
assert_eq!(variation.variation_type, Some("kana".to_string()));
}
}