geolite 0.0.7

open source geocode solution
use std::{fs, path::PathBuf};

use geozero::ToWkt;
use rusqlite::Connection;
use serde::Deserialize;

use super::{http_client, repository};

#[derive(Deserialize)]
struct geofabrik_index {
  features: Vec<geofabrik_feature>,
}

#[derive(Deserialize)]
struct geofabrik_feature {
  properties: geofabrik_properties,
  #[serde(default)]
  geometry: Option<serde_json::Value>,
}

#[derive(Deserialize)]
struct geofabrik_properties {
  id: String,
  name: String,
  #[serde(default)]
  urls: Option<geofabrik_urls>,
}

#[derive(Deserialize)]
struct geofabrik_urls {
  pbf: Option<String>,
}

pub const GEOFABRIK_ENDPOINT: &str = "https://download.geofabrik.de/index-v1.json";

#[derive(Copy, Clone, PartialEq, Eq)]
pub enum source {
  geofabrik,
  local,
}

impl source {
  pub fn default_endpoint(self) -> Option<&'static str> {
    match self {
      source::geofabrik => Some(GEOFABRIK_ENDPOINT),
      source::local => None,
    }
  }
}

pub struct geofabrik_entry {
  pub id: String,
  pub name: String,
  pub url: String,
}

pub struct local_pbf {
  pub path: PathBuf,
  pub size_bytes: u64,
}

pub enum listing {
  geofabrik(Vec<geofabrik_entry>),
  local(Vec<local_pbf>),
}

fn coverage_wkt(geometry: Option<&serde_json::Value>) -> Option<String> {
  geometry.and_then(|shape| geozero::geojson::GeoJson(&shape.to_string()).to_wkt().ok())
}

pub(super) fn geofabrik(
  conn: &Connection,
  recreate_cache: bool,
  endpoint: &str,
) -> Vec<geofabrik_entry> {
  if !recreate_cache {
    let cached = repository::list_geofabrik_index(conn);
    if !cached.is_empty() {
      return cached
        .into_iter()
        .map(|(id, name, url)| geofabrik_entry { id, name, url })
        .collect();
    }
  }
  let body = http_client::agent()
    .get(endpoint)
    .call()
    .expect("failed to fetch geofabrik index")
    .into_body()
    .read_to_string()
    .expect("failed to read response body");
  let index: geofabrik_index =
    serde_json::from_str(&body).expect("failed to parse geofabrik index");
  let tx = conn
    .unchecked_transaction()
    .expect("failed to begin transaction");
  for f in &index.features {
    let url = f
      .properties
      .urls
      .as_ref()
      .and_then(|u| u.pbf.as_deref())
      .unwrap_or("-");
    let coverage = coverage_wkt(f.geometry.as_ref());
    repository::upsert_geofabrik_index_item(
      &tx,
      &f.properties.id,
      &f.properties.name,
      url,
      coverage.as_deref(),
    );
  }
  tx.commit().expect("failed to commit transaction");
  repository::list_geofabrik_index(conn)
    .into_iter()
    .map(|(id, name, url)| geofabrik_entry { id, name, url })
    .collect()
}

pub(super) fn resolve_geofabrik_url(conn: &Connection, id: &str, endpoint: &str) -> Option<String> {
  if let Some(url) = repository::get_geofabrik_url(conn, id)
    && url != "-"
  {
    return Some(url);
  }
  geofabrik(conn, false, endpoint)
    .into_iter()
    .find(|i| i.id == id)
    .and_then(|i| if i.url == "-" { None } else { Some(i.url) })
}

pub(super) fn list_local(data_path: &str) -> Vec<local_pbf> {
  let pbf_dir = PathBuf::from(data_path);
  let Ok(entries) = fs::read_dir(&pbf_dir) else {
    return vec![];
  };
  let mut result: Vec<local_pbf> = entries
    .flatten()
    .filter(|e| {
      e.path()
        .extension()
        .map(|ext| ext == "pbf")
        .unwrap_or(false)
    })
    .filter_map(|e| {
      let size_bytes = e.metadata().ok()?.len();
      Some(local_pbf {
        path: e.path(),
        size_bytes,
      })
    })
    .collect();
  result.sort_by(|a, b| a.path.cmp(&b.path));
  result
}

#[cfg(test)]
#[path = "catalog.test.rs"]
mod tests;