tiles-client 0.12.0

Client for downloading tiles.
Documentation
use std::{
  path::PathBuf,
  sync::{Arc, mpsc},
  thread,
};

use crate::{Tile, math};

/// Client for accessing tiles
pub struct Client
{
  cache_dir: PathBuf,
  sender: mpsc::Sender<(u32, u32, u32, PathBuf)>,
}

/// Client configuration
pub struct ClientBuilder
{
  user_agent: String,
  url: String,
  qualifier: String,
  organization: String,
  application: String,
  tiles_cache: String,
  on_download_complted: Box<dyn Fn() + Sync + Send>,
}

impl ClientBuilder
{
  /// Create a new client for the given user agent
  /// url template, for instance "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
  pub fn new(user_agent: impl Into<String>, url: impl Into<String>) -> Self
  {
    Self {
      user_agent: user_agent.into(),
      url: url.into(),
      qualifier: "com".into(),
      organization: "cyloncore".into(),
      application: "tiles-client".into(),
      tiles_cache: "tiles_cache".into(),
      on_download_complted: Box::new(|| {}),
    }
  }
  /// Set application metainfo
  pub fn application_metainfo(
    mut self,
    qualifier: impl Into<String>,
    organization: impl Into<String>,
    application: impl Into<String>,
  ) -> Self
  {
    self.qualifier = qualifier.into();
    self.organization = organization.into();
    self.application = application.into();
    self
  }
  /// Set the directory used for storing cache
  pub fn tiles_cache(mut self, tiles_cache: String) -> Self
  {
    self.tiles_cache = tiles_cache;
    self
  }
  /// Build the client
  pub fn build(self) -> Client
  {
    let (sender, receiver) = mpsc::channel::<(u32, u32, u32, PathBuf)>();
    let user_agent = self.user_agent;
    let on_download_completed = Arc::new(self.on_download_complted);
    thread::spawn(move || {
      while let Ok(msg) = receiver.recv()
      {
        let url = self
          .url
          .replace("{x}", &msg.0.to_string())
          .replace("{y}", &msg.1.to_string())
          .replace("{z}", &msg.2.to_string());

        let mut request = ehttp::Request::get(url);
        request.headers.insert("User-Agent", user_agent.to_owned());
        let on_download_completed = on_download_completed.clone();
        ehttp::fetch(
          request,
          move |result: ehttp::Result<ehttp::Response>| match result
          {
            Ok(result) =>
            {
              if result.ok
              {
                if let Some(parent) = msg.3.parent()
                {
                  std::fs::create_dir_all(parent).unwrap();
                }
                std::fs::write(msg.3, &result.bytes).expect("Failed to write file");
                on_download_completed();
              }
              else
              {
                log::error!(
                  "Response: {:?} ({:?}) for {}",
                  result.status_text,
                  result.status,
                  result.url
                );
              }
            }
            Err(e) =>
            {
              log::error!("Error: {:?}", e);
            }
          },
        );
      }
    });
    let cache_dir =
      directories::ProjectDirs::from(&self.qualifier, &self.organization, &self.application)
        .unwrap()
        .cache_dir()
        .join(&self.tiles_cache);
    Client { cache_dir, sender }
  }
}

impl Client
{
  /// This function compute the filename cache
  fn tile_filename(&self, x: u32, y: u32, z: u32) -> PathBuf
  {
    self
      .cache_dir
      .join(z.to_string())
      .join(y.to_string())
      .join(format!("{}.png", x))
  }

  fn clamp_tile_index(index: Option<u32>, max_index: u32) -> u32
  {
    index.unwrap_or(0).clamp(0, max_index - 1)
  }

  /// Return the tiles corresponding to the rectangular area
  pub fn tiles(&self, rect: geo::Rect, zoom_level: u32, download: bool) -> Vec<Tile>
  {
    let max_index = 2u32.pow(zoom_level);

    let x_min = Self::clamp_tile_index(math::lon_to_tile_x(rect.min().x, zoom_level), max_index);
    let x_max = Self::clamp_tile_index(math::lon_to_tile_x(rect.max().x, zoom_level), max_index);
    let y_min = Self::clamp_tile_index(math::lat_to_tile_y(rect.max().y, zoom_level), max_index);
    let y_max = Self::clamp_tile_index(math::lat_to_tile_y(rect.min().y, zoom_level), max_index);

    let mut tiles = Vec::new();
    for tile_x in x_min..=x_max
    {
      for tile_y in y_min..=y_max
      {
        let min_lon = math::tile_x_to_lon(tile_x, zoom_level);
        let max_lon = math::tile_x_to_lon(tile_x + 1, zoom_level);
        let min_lat = math::tile_y_to_lat(tile_y, zoom_level);
        let max_lat = math::tile_y_to_lat(tile_y + 1, zoom_level);
        let tile = Tile {
          tile_x,
          tile_y,
          tile_z: zoom_level,
          filename: self.tile_filename(tile_x, tile_y, zoom_level),
          bounding_box: geo::Rect::new(
            geo::Coord {
              x: min_lon,
              y: min_lat,
            },
            geo::Coord {
              x: max_lon,
              y: max_lat,
            },
          ),
          image: Default::default(),
        };
        if !tile.is_cached() && download
        {
          self
            .sender
            .send((
              tile.tile_x(),
              tile.tile_y(),
              tile.tile_z(),
              tile.filename().to_owned(),
            ))
            .unwrap();
        }
        tiles.push(tile);
      }
    }
    tiles
  }
}