Skip to main content

tiles_client/
client.rs

1use std::{
2  path::PathBuf,
3  sync::{Arc, mpsc},
4  thread,
5};
6
7use crate::{Tile, math};
8
9/// Client for accessing tiles
10pub struct Client
11{
12  cache_dir: PathBuf,
13  sender: mpsc::Sender<(u32, u32, u32, PathBuf)>,
14}
15
16/// Client configuration
17pub struct ClientBuilder
18{
19  user_agent: String,
20  url: String,
21  qualifier: String,
22  organization: String,
23  application: String,
24  tiles_cache: String,
25  on_download_complted: Box<dyn Fn() + Sync + Send>,
26}
27
28impl ClientBuilder
29{
30  /// Create a new client for the given user agent
31  /// url template, for instance "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
32  pub fn new(user_agent: impl Into<String>, url: impl Into<String>) -> Self
33  {
34    Self {
35      user_agent: user_agent.into(),
36      url: url.into(),
37      qualifier: "com".into(),
38      organization: "cyloncore".into(),
39      application: "tiles-client".into(),
40      tiles_cache: "tiles_cache".into(),
41      on_download_complted: Box::new(|| {}),
42    }
43  }
44  /// Set application metainfo
45  pub fn application_metainfo(
46    mut self,
47    qualifier: impl Into<String>,
48    organization: impl Into<String>,
49    application: impl Into<String>,
50  ) -> Self
51  {
52    self.qualifier = qualifier.into();
53    self.organization = organization.into();
54    self.application = application.into();
55    self
56  }
57  /// Set the directory used for storing cache
58  pub fn tiles_cache(mut self, tiles_cache: String) -> Self
59  {
60    self.tiles_cache = tiles_cache;
61    self
62  }
63  /// Build the client
64  pub fn build(self) -> Client
65  {
66    let (sender, receiver) = mpsc::channel::<(u32, u32, u32, PathBuf)>();
67    let user_agent = self.user_agent;
68    let on_download_completed = Arc::new(self.on_download_complted);
69    thread::spawn(move || {
70      while let Ok(msg) = receiver.recv()
71      {
72        let url = self
73          .url
74          .replace("{x}", &msg.0.to_string())
75          .replace("{y}", &msg.1.to_string())
76          .replace("{z}", &msg.2.to_string());
77
78        let mut request = ehttp::Request::get(url);
79        request.headers.insert("User-Agent", user_agent.to_owned());
80        let on_download_completed = on_download_completed.clone();
81        ehttp::fetch(
82          request,
83          move |result: ehttp::Result<ehttp::Response>| match result
84          {
85            Ok(result) =>
86            {
87              if result.ok
88              {
89                if let Some(parent) = msg.3.parent()
90                {
91                  std::fs::create_dir_all(parent).unwrap();
92                }
93                std::fs::write(msg.3, &result.bytes).expect("Failed to write file");
94                on_download_completed();
95              }
96              else
97              {
98                log::error!(
99                  "Response: {:?} ({:?}) for {}",
100                  result.status_text,
101                  result.status,
102                  result.url
103                );
104              }
105            }
106            Err(e) =>
107            {
108              log::error!("Error: {:?}", e);
109            }
110          },
111        );
112      }
113    });
114    let cache_dir =
115      directories::ProjectDirs::from(&self.qualifier, &self.organization, &self.application)
116        .unwrap()
117        .cache_dir()
118        .join(&self.tiles_cache);
119    Client { cache_dir, sender }
120  }
121}
122
123impl Client
124{
125  /// This function compute the filename cache
126  fn tile_filename(&self, x: u32, y: u32, z: u32) -> PathBuf
127  {
128    self
129      .cache_dir
130      .join(z.to_string())
131      .join(y.to_string())
132      .join(format!("{}.png", x))
133  }
134
135  fn clamp_tile_index(index: Option<u32>, max_index: u32) -> u32
136  {
137    index.unwrap_or(0).clamp(0, max_index - 1)
138  }
139
140  /// Return the tiles corresponding to the rectangular area
141  pub fn tiles(&self, rect: geo::Rect, zoom_level: u32, download: bool) -> Vec<Tile>
142  {
143    let max_index = 2u32.pow(zoom_level);
144
145    // Get tile coordinate bounds
146    let x_min = Self::clamp_tile_index(math::lon_to_tile_x(rect.min().x, zoom_level), max_index);
147    let x_max = Self::clamp_tile_index(math::lon_to_tile_x(rect.max().x, zoom_level), max_index);
148    let y_min = Self::clamp_tile_index(math::lat_to_tile_y(rect.max().y, zoom_level), max_index);
149    let y_max = Self::clamp_tile_index(math::lat_to_tile_y(rect.min().y, zoom_level), max_index);
150
151    // Generate tiles
152    let mut tiles = Vec::new();
153    for tile_x in x_min..=x_max
154    {
155      for tile_y in y_min..=y_max
156      {
157        let min_lon = math::tile_x_to_lon(tile_x, zoom_level);
158        let max_lon = math::tile_x_to_lon(tile_x + 1, zoom_level);
159        let min_lat = math::tile_y_to_lat(tile_y, zoom_level);
160        let max_lat = math::tile_y_to_lat(tile_y + 1, zoom_level);
161        let tile = Tile {
162          tile_x,
163          tile_y,
164          tile_z: zoom_level,
165          filename: self.tile_filename(tile_x, tile_y, zoom_level),
166          bounding_box: geo::Rect::new(
167            geo::Coord {
168              x: min_lon,
169              y: min_lat,
170            },
171            geo::Coord {
172              x: max_lon,
173              y: max_lat,
174            },
175          ),
176          image: Default::default(),
177        };
178        if !tile.is_cached() && download
179        {
180          self
181            .sender
182            .send((
183              tile.tile_x(),
184              tile.tile_y(),
185              tile.tile_z(),
186              tile.filename().to_owned(),
187            ))
188            .unwrap();
189        }
190        tiles.push(tile);
191      }
192    }
193    tiles
194  }
195}