use crate::geo::types::Coordinate;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OsmElementType {
Node,
Way,
Relation,
}
impl OsmElementType {
#[allow(dead_code)]
#[must_use]
pub fn to_code(self) -> &'static str {
match self {
OsmElementType::Node => "N",
OsmElementType::Way => "W",
OsmElementType::Relation => "R",
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct NamedPlace {
pub osm_id: i64,
pub osm_type: OsmElementType,
pub name: String,
pub lat: f64,
pub lon: f64,
pub tags: HashMap<String, String>,
}
impl NamedPlace {
#[allow(dead_code)]
#[must_use]
pub fn new(osm_id: i64, osm_type: OsmElementType, name: String, lat: f64, lon: f64) -> Self {
Self {
osm_id,
osm_type,
name,
lat,
lon,
tags: HashMap::new(),
}
}
#[allow(dead_code)]
#[must_use]
pub fn with_tag(mut self, key: String, value: String) -> Self {
self.tags.insert(key, value);
self
}
#[allow(dead_code)]
#[must_use]
pub fn with_tags(mut self, tags: HashMap<String, String>) -> Self {
self.tags.extend(tags);
self
}
#[allow(dead_code)]
#[must_use]
pub fn osm_key(&self) -> Option<&str> {
["amenity", "shop", "tourism", "historic", "leisure", "office",
"public_transport", "highway", "building", "place", "natural", "landuse"]
.iter()
.find(|&&key| self.tags.contains_key(key))
.copied()
}
#[allow(dead_code)]
pub fn osm_value(&self) -> Option<&str> {
if let Some(key) = self.osm_key() {
self.tags.get(key).map(String::as_str)
} else {
None
}
}
#[allow(dead_code)]
pub fn city(&self) -> Option<&str> {
self.tags.get("addr:city")
.or_else(|| self.tags.get("city"))
.or_else(|| self.tags.get("town"))
.or_else(|| self.tags.get("village"))
.map(String::as_str)
}
#[allow(dead_code)]
pub fn street(&self) -> Option<&str> {
self.tags.get("addr:street")
.or_else(|| self.tags.get("street"))
.map(String::as_str)
}
#[allow(dead_code)]
pub fn housenumber(&self) -> Option<&str> {
self.tags.get("addr:housenumber")
.or_else(|| self.tags.get("housenumber"))
.map(String::as_str)
}
#[allow(dead_code)]
pub fn postcode(&self) -> Option<&str> {
self.tags.get("addr:postcode")
.or_else(|| self.tags.get("postcode"))
.map(String::as_str)
}
#[allow(dead_code)]
pub fn countrycode(&self) -> Option<&str> {
self.tags.get("addr:country")
.or_else(|| self.tags.get("country_code"))
.or_else(|| self.tags.get("country"))
.map(String::as_str)
}
}
#[allow(dead_code)]
const SEARCHABLE_TAGS: &[&str] = &[
"name",
"addr:housenumber",
"addr:street",
"addr:city",
"addr:postcode",
"amenity",
"shop",
"tourism",
"historic",
"leisure",
"office",
"craft",
"emergency",
"healthcare",
"public_transport",
"highway",
"place",
"natural",
"waterway",
"landuse",
"building",
"aeroway",
"railway",
"boundary",
];
#[allow(dead_code)]
fn has_searchable_tags(tags: &HashMap<String, String>) -> bool {
if tags.contains_key("name") {
return true;
}
if tags.contains_key("addr:housenumber") && tags.contains_key("addr:street") {
return true;
}
if tags.contains_key("place") {
return true;
}
false
}
#[allow(dead_code)]
fn extract_name(tags: &HashMap<String, String>) -> Option<String> {
if let Some(name) = tags.get("name") {
return Some(name.clone());
}
for alt_key in &["alt_name", "official_name", "loc_name", "short_name"] {
if let Some(name) = tags.get(*alt_key) {
return Some(name.clone());
}
}
if let (Some(housenumber), Some(street)) =
(tags.get("addr:housenumber"), tags.get("addr:street")) {
return Some(format!("{housenumber} {street}"));
}
if let Some(place_type) = tags.get("place") {
return Some(format!("Unnamed {place_type}"));
}
None
}
#[allow(dead_code)]
pub fn extract_named_places<P: AsRef<Path>>(
_file_path: P,
_bbox: Option<(f64, f64, f64, f64)>,
) -> Result<Vec<NamedPlace>> {
anyhow::bail!(
"OSM PBF extraction requires the osmpbfreader crate. \
Vendor it and enable the 'osm' feature to use this function."
)
}
#[allow(dead_code)]
#[allow(clippy::cast_precision_loss)]
fn centroid(coords: &[Coordinate]) -> (f64, f64) {
if coords.is_empty() {
return (0.0, 0.0);
}
let sum_lat: f64 = coords.iter().map(|c| c.lat).sum();
let sum_lon: f64 = coords.iter().map(|c| c.lon).sum();
let n = coords.len() as f64;
(sum_lat / n, sum_lon / n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_searchable_tags() {
let mut tags = HashMap::new();
tags.insert("name".to_string(), "Test".to_string());
assert!(has_searchable_tags(&tags));
let mut tags2 = HashMap::new();
tags2.insert("addr:housenumber".to_string(), "123".to_string());
tags2.insert("addr:street".to_string(), "Main St".to_string());
assert!(has_searchable_tags(&tags2));
let mut tags3 = HashMap::new();
tags3.insert("building".to_string(), "yes".to_string());
assert!(!has_searchable_tags(&tags3));
}
#[test]
fn test_extract_name() {
let mut tags = HashMap::new();
tags.insert("name".to_string(), "Cafe Example".to_string());
assert_eq!(extract_name(&tags), Some("Cafe Example".to_string()));
let mut tags2 = HashMap::new();
tags2.insert("addr:housenumber".to_string(), "42".to_string());
tags2.insert("addr:street".to_string(), "Oak Street".to_string());
assert_eq!(extract_name(&tags2), Some("42 Oak Street".to_string()));
}
#[test]
fn test_named_place_accessors() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "cafe".to_string());
tags.insert("addr:city".to_string(), "Montreal".to_string());
tags.insert("addr:street".to_string(), "Rue Saint-Denis".to_string());
tags.insert("addr:housenumber".to_string(), "123".to_string());
let place = NamedPlace::new(12345, OsmElementType::Node, "Cafe".to_string(), 45.5, -73.6)
.with_tags(tags);
assert_eq!(place.osm_key(), Some("amenity"));
assert_eq!(place.osm_value(), Some("cafe"));
assert_eq!(place.city(), Some("Montreal"));
assert_eq!(place.street(), Some("Rue Saint-Denis"));
assert_eq!(place.housenumber(), Some("123"));
}
#[test]
fn test_centroid() {
let coords = vec![
Coordinate::new(45.0, -73.0),
Coordinate::new(46.0, -74.0),
];
let (lat, lon) = centroid(&coords);
assert!((lat - 45.5).abs() < 1e-6);
assert!((lon - (-73.5)).abs() < 1e-6);
}
}