1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct MapPosition {
7 pub lat: f64,
8 pub lng: f64,
9 pub zoom: f64,
10}
11
12impl MapPosition {
13 pub fn new(lat: f64, lng: f64, zoom: f64) -> Self {
15 Self { lat, lng, zoom }
16 }
17}
18
19impl Default for MapPosition {
20 fn default() -> Self {
21 Self::new(51.505, -0.09, 13.0)
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct MapMarker {
28 pub lat: f64,
29 pub lng: f64,
30 pub title: String,
31 pub description: Option<String>,
32 pub icon: Option<MarkerIcon>,
33 pub popup_options: Option<PopupOptions>,
34 pub custom_data: Option<HashMap<String, String>>,
35}
36
37impl MapMarker {
38 pub fn new(lat: f64, lng: f64, title: impl Into<String>) -> Self {
40 Self {
41 lat,
42 lng,
43 title: title.into(),
44 description: None,
45 icon: None,
46 popup_options: None,
47 custom_data: None,
48 }
49 }
50
51 pub fn with_description(mut self, description: impl Into<String>) -> Self {
53 self.description = Some(description.into());
54 self
55 }
56
57 pub fn with_icon(mut self, icon: MarkerIcon) -> Self {
59 self.icon = Some(icon);
60 self
61 }
62
63 pub fn with_popup_options(mut self, options: PopupOptions) -> Self {
65 self.popup_options = Some(options);
66 self
67 }
68
69 pub fn with_custom_data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
71 if self.custom_data.is_none() {
72 self.custom_data = Some(HashMap::new());
73 }
74 if let Some(ref mut data) = self.custom_data {
75 data.insert(key.into(), value.into());
76 }
77 self
78 }
79}
80
81impl Default for MapMarker {
82 fn default() -> Self {
83 Self {
84 lat: 0.0,
85 lng: 0.0,
86 title: String::new(),
87 description: None,
88 icon: None,
89 popup_options: None,
90 custom_data: None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct MarkerIcon {
98 pub icon_url: String,
99 pub icon_size: Option<(u32, u32)>,
100 pub icon_anchor: Option<(u32, u32)>,
101 pub popup_anchor: Option<(i32, i32)>,
102 pub shadow_url: Option<String>,
103 pub shadow_size: Option<(u32, u32)>,
104}
105
106impl MarkerIcon {
107 pub fn new(icon_url: impl Into<String>) -> Self {
109 Self {
110 icon_url: icon_url.into(),
111 icon_size: None,
112 icon_anchor: None,
113 popup_anchor: None,
114 shadow_url: None,
115 shadow_size: None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct PopupOptions {
123 pub max_width: Option<u32>,
124 pub min_width: Option<u32>,
125 pub max_height: Option<u32>,
126 pub auto_pan: Option<bool>,
127 pub keep_in_view: Option<bool>,
128 pub close_button: Option<bool>,
129 pub auto_close: Option<bool>,
130 pub close_on_escape_key: Option<bool>,
131 pub class_name: Option<String>,
132}
133
134impl Default for PopupOptions {
135 fn default() -> Self {
136 Self {
137 max_width: Some(300),
138 min_width: Some(50),
139 max_height: None,
140 auto_pan: Some(true),
141 keep_in_view: Some(false),
142 close_button: Some(true),
143 auto_close: Some(true),
144 close_on_escape_key: Some(true),
145 class_name: None,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct MapOptions {
153 pub zoom_control: bool,
154 pub scroll_wheel_zoom: bool,
155 pub double_click_zoom: bool,
156 pub touch_zoom: bool,
157 pub dragging: bool,
158 pub keyboard: bool,
159 pub attribution_control: bool,
160 pub tile_layer: TileLayer,
161 pub leaflet_resources: LeafletResources,
162}
163
164impl Default for MapOptions {
165 fn default() -> Self {
166 Self {
167 zoom_control: true,
168 scroll_wheel_zoom: true,
169 double_click_zoom: true,
170 touch_zoom: true,
171 dragging: true,
172 keyboard: true,
173 attribution_control: true,
174 tile_layer: TileLayer::default(),
175 leaflet_resources: LeafletResources::default(),
176 }
177 }
178}
179
180impl MapOptions {
181 pub fn minimal() -> Self {
183 Self {
184 zoom_control: false,
185 scroll_wheel_zoom: false,
186 double_click_zoom: false,
187 touch_zoom: false,
188 dragging: false,
189 keyboard: false,
190 attribution_control: false,
191 tile_layer: TileLayer::default(),
192 leaflet_resources: LeafletResources::default(),
193 }
194 }
195
196 pub fn with_zoom_control(mut self, enabled: bool) -> Self {
198 self.zoom_control = enabled;
199 self
200 }
201
202 pub fn with_scroll_wheel_zoom(mut self, enabled: bool) -> Self {
204 self.scroll_wheel_zoom = enabled;
205 self
206 }
207
208 pub fn with_double_click_zoom(mut self, enabled: bool) -> Self {
210 self.double_click_zoom = enabled;
211 self
212 }
213
214 pub fn with_touch_zoom(mut self, enabled: bool) -> Self {
216 self.touch_zoom = enabled;
217 self
218 }
219
220 pub fn with_dragging(mut self, enabled: bool) -> Self {
222 self.dragging = enabled;
223 self
224 }
225
226 pub fn with_keyboard(mut self, enabled: bool) -> Self {
228 self.keyboard = enabled;
229 self
230 }
231
232 pub fn with_attribution_control(mut self, enabled: bool) -> Self {
234 self.attribution_control = enabled;
235 self
236 }
237
238 pub fn with_tile_layer(mut self, tile_layer: TileLayer) -> Self {
240 self.tile_layer = tile_layer;
241 self
242 }
243
244 pub fn with_leaflet_resources(mut self, resources: LeafletResources) -> Self {
246 self.leaflet_resources = resources;
247 self
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub struct TileLayer {
254 pub url: String,
255 pub attribution: String,
256 pub max_zoom: u8,
257 pub subdomains: Vec<String>,
258}
259
260impl TileLayer {
261 pub fn openstreetmap() -> Self {
263 Self {
264 url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png".to_string(),
265 attribution: "© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors".to_string(),
266 max_zoom: 19,
267 subdomains: vec!["a".to_string(), "b".to_string(), "c".to_string()],
268 }
269 }
270
271 pub fn satellite() -> Self {
273 Self {
274 url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}".to_string(),
275 attribution: "Tiles © Esri".to_string(),
276 max_zoom: 18,
277 subdomains: vec![],
278 }
279 }
280}
281
282impl Default for TileLayer {
283 fn default() -> Self {
284 Self::openstreetmap()
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub enum LeafletResources {
291 Cdn {
293 version: String,
294 base_url: Option<String>, },
296 Local {
298 css_path: String,
299 js_path: String,
300 },
301}
302
303impl LeafletResources {
304 pub fn cdn(version: impl Into<String>) -> Self {
306 Self::Cdn {
307 version: version.into(),
308 base_url: None,
309 }
310 }
311
312 pub fn cdn_with_base_url(version: impl Into<String>, base_url: impl Into<String>) -> Self {
314 Self::Cdn {
315 version: version.into(),
316 base_url: Some(base_url.into()),
317 }
318 }
319
320 pub fn local(css_path: impl Into<String>, js_path: impl Into<String>) -> Self {
322 Self::Local {
323 css_path: css_path.into(),
324 js_path: js_path.into(),
325 }
326 }
327
328 pub fn css_url(&self) -> String {
330 match self {
331 Self::Cdn { version, base_url } => {
332 let base = base_url.as_deref().unwrap_or("https://unpkg.com");
333 format!("{}/leaflet@{}/dist/leaflet.css", base, version)
334 }
335 Self::Local { css_path, .. } => css_path.clone(),
336 }
337 }
338
339 pub fn js_url(&self) -> String {
341 match self {
342 Self::Cdn { version, base_url } => {
343 let base = base_url.as_deref().unwrap_or("https://unpkg.com");
344 format!("{}/leaflet@{}/dist/leaflet.js", base, version)
345 }
346 Self::Local { js_path, .. } => js_path.clone(),
347 }
348 }
349
350 pub fn css_integrity(&self) -> Option<String> {
352 match self {
353 Self::Cdn { version, base_url } if base_url.is_none() => {
354 match version.as_str() {
356 "1.9.4" => Some("sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=".to_string()),
357 "1.9.3" => Some("sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=".to_string()),
358 "1.9.2" => Some("sha256-sA+zWATbFveLLNqWO2gtiw3HL/lh1giY/Inf1BJ0z14=".to_string()),
359 _ => None,
360 }
361 }
362 _ => None,
363 }
364 }
365
366 pub fn js_integrity(&self) -> Option<String> {
368 match self {
369 Self::Cdn { version, base_url } if base_url.is_none() => {
370 match version.as_str() {
372 "1.9.4" => Some("sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=".to_string()),
373 "1.9.3" => Some("sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=".to_string()),
374 "1.9.2" => Some("sha256-o9N4PsYA2zOcVD5OHEHviWpTGQ4Q1jEzU7oJiE+zRCE=".to_string()),
375 _ => None,
376 }
377 }
378 _ => None,
379 }
380 }
381}
382
383impl Default for LeafletResources {
384 fn default() -> Self {
385 Self::cdn("1.9.4")
386 }
387}