1use crate::io::{HttpClient, HttpRequest, HttpResponse};
48use crate::mvt::{decode_mvt, MvtDecodeOptions};
49use crate::tile_source::{
50 RevalidationHint, TileData, TileError, TileFreshness, TileResponse, TileSource, VectorTileData,
51};
52use crate::tilejson::TileJson;
53use rustial_math::TileId;
54use std::collections::HashMap;
55use std::sync::Mutex;
56use std::time::{Duration, SystemTime};
57
58fn parse_cache_control_max_age(value: &str) -> Option<u64> {
63 for directive in value.split(',') {
64 let directive = directive.trim();
65 if let Some(rest) = directive.strip_prefix("max-age=") {
66 if let Ok(seconds) = rest.trim_matches('"').parse::<u64>() {
67 return Some(seconds);
68 }
69 }
70 }
71 None
72}
73
74fn parse_age_seconds(response: &HttpResponse) -> u64 {
75 response
76 .header("age")
77 .and_then(|value| value.parse::<u64>().ok())
78 .unwrap_or(0)
79}
80
81fn parse_http_freshness(response: &HttpResponse) -> TileFreshness {
82 let now = SystemTime::now();
83 let age = parse_age_seconds(response);
84
85 let expires_at = response
86 .header("cache-control")
87 .and_then(parse_cache_control_max_age)
88 .map(|max_age| max_age.saturating_sub(age))
89 .map(Duration::from_secs)
90 .and_then(|ttl| now.checked_add(ttl))
91 .or_else(|| {
92 response
93 .header("expires")
94 .and_then(|value| httpdate::parse_http_date(value).ok())
95 });
96
97 TileFreshness {
98 expires_at,
99 etag: response.header("etag").map(ToOwned::to_owned),
100 last_modified: response.header("last-modified").map(ToOwned::to_owned),
101 }
102}
103
104pub struct HttpVectorTileSource {
114 url_template: String,
116
117 client: Box<dyn HttpClient>,
119
120 default_headers: Vec<(String, String)>,
122
123 decode_options: MvtDecodeOptions,
125
126 tilejson: Option<TileJson>,
128
129 pending: Mutex<HashMap<String, TileId>>,
131
132 deferred_decode: bool,
138}
139
140impl std::fmt::Debug for HttpVectorTileSource {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let pending_count = self.pending.lock().map(|p| p.len()).unwrap_or(0);
143 f.debug_struct("HttpVectorTileSource")
144 .field("url_template", &self.url_template)
145 .field("has_tilejson", &self.tilejson.is_some())
146 .field("pending", &pending_count)
147 .finish()
148 }
149}
150
151impl HttpVectorTileSource {
152 pub fn new(url_template: impl Into<String>, client: Box<dyn HttpClient>) -> Self {
157 Self {
158 url_template: url_template.into(),
159 client,
160 default_headers: Vec::new(),
161 decode_options: MvtDecodeOptions::default(),
162 tilejson: None,
163 pending: Mutex::new(HashMap::new()),
164 deferred_decode: false,
165 }
166 }
167
168 pub fn with_deferred_decode(mut self, deferred: bool) -> Self {
176 self.deferred_decode = deferred;
177 self
178 }
179
180 #[inline]
182 pub fn deferred_decode(&self) -> bool {
183 self.deferred_decode
184 }
185
186 pub fn with_tilejson(mut self, tilejson: TileJson) -> Self {
189 self.tilejson = Some(tilejson);
190 self
191 }
192
193 pub fn with_decode_options(mut self, options: MvtDecodeOptions) -> Self {
195 self.decode_options = options;
196 self
197 }
198
199 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
201 self.default_headers.push((name.into(), value.into()));
202 self
203 }
204
205 pub fn tile_url(&self, id: &TileId) -> String {
207 self.url_template
208 .replace("{z}", &id.zoom.to_string())
209 .replace("{x}", &id.x.to_string())
210 .replace("{y}", &id.y.to_string())
211 }
212
213 #[inline]
215 pub fn url_template(&self) -> &str {
216 &self.url_template
217 }
218
219 pub fn pending_count(&self) -> usize {
221 self.pending.lock().map(|p| p.len()).unwrap_or(0)
222 }
223
224 pub fn tilejson(&self) -> Option<&TileJson> {
226 self.tilejson.as_ref()
227 }
228
229 fn decode_tile_bytes(
231 &self,
232 bytes: &[u8],
233 tile_id: &TileId,
234 ) -> Result<VectorTileData, TileError> {
235 let layers = decode_mvt(bytes, tile_id, &self.decode_options)
236 .map_err(|e| TileError::Decode(format!("MVT decode: {e}")))?;
237
238 Ok(VectorTileData { layers })
239 }
240}
241
242impl TileSource for HttpVectorTileSource {
243 fn request(&self, id: TileId) {
244 let url = self.tile_url(&id);
245
246 if let Ok(mut pending) = self.pending.lock() {
247 pending.insert(url.clone(), id);
248 }
249
250 let mut req = HttpRequest::get(&url);
251 for (name, value) in &self.default_headers {
252 req = req.with_header(name.clone(), value.clone());
253 }
254
255 self.client.send(req);
256 }
257
258 fn request_revalidate(&self, id: TileId, hint: RevalidationHint) {
259 let url = self.tile_url(&id);
260
261 if let Ok(mut pending) = self.pending.lock() {
262 pending.insert(url.clone(), id);
263 }
264
265 let mut req = HttpRequest::get(&url);
266 for (name, value) in &self.default_headers {
267 req = req.with_header(name.clone(), value.clone());
268 }
269
270 if let Some(etag) = &hint.etag {
271 req = req.with_header("If-None-Match", etag.clone());
272 }
273 if let Some(last_modified) = &hint.last_modified {
274 req = req.with_header("If-Modified-Since", last_modified.clone());
275 }
276
277 self.client.send(req);
278 }
279
280 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
281 let responses = self.client.poll();
282 if responses.is_empty() {
283 return Vec::new();
284 }
285
286 let mut pending = match self.pending.lock() {
287 Ok(p) => p,
288 Err(_) => return Vec::new(),
289 };
290
291 let mut results = Vec::with_capacity(responses.len());
292
293 for (url, response) in responses {
294 let tile_id = match pending.remove(&url) {
295 Some(id) => id,
296 None => continue,
297 };
298
299 match response {
300 Ok(resp) if resp.status == 304 => {
301 let freshness = parse_http_freshness(&resp);
302 results.push((tile_id, Ok(TileResponse::not_modified(freshness))));
303 }
304 Ok(resp) if resp.is_success() => {
305 let freshness = parse_http_freshness(&resp);
306 if self.deferred_decode {
307 let raw = crate::tile_source::RawVectorPayload {
308 tile_id,
309 bytes: std::sync::Arc::new(resp.body),
310 decode_options: self.decode_options.clone(),
311 };
312 results.push((
313 tile_id,
314 Ok(TileResponse {
315 data: TileData::RawVector(raw),
316 freshness,
317 not_modified: false,
318 }),
319 ));
320 } else {
321 let tile_result =
322 self.decode_tile_bytes(&resp.body, &tile_id)
323 .map(|vector_data| TileResponse {
324 data: TileData::Vector(vector_data),
325 freshness,
326 not_modified: false,
327 });
328 results.push((tile_id, tile_result));
329 }
330 }
331 Ok(resp) if resp.status == 404 => {
332 results.push((tile_id, Err(TileError::NotFound(tile_id))));
333 }
334 Ok(resp) => {
335 results.push((
336 tile_id,
337 Err(TileError::Network(format!("HTTP {}", resp.status))),
338 ));
339 }
340 Err(err) => {
341 results.push((tile_id, Err(TileError::Network(err))));
342 }
343 }
344 }
345
346 results
347 }
348
349 fn cancel(&self, id: TileId) {
350 if let Ok(mut pending) = self.pending.lock() {
351 let url = self.tile_url(&id);
352 pending.remove(&url);
353 }
354 }
355}
356
357#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::io::HttpResponse;
365 use std::sync::{Arc, Mutex as StdMutex};
366
367 struct MockClient {
368 sent: StdMutex<Vec<HttpRequest>>,
369 responses: StdMutex<Vec<(String, Result<HttpResponse, String>)>>,
370 }
371
372 impl MockClient {
373 fn new() -> Self {
374 Self {
375 sent: StdMutex::new(Vec::new()),
376 responses: StdMutex::new(Vec::new()),
377 }
378 }
379
380 fn queue_response(&self, url: &str, status: u16, body: Vec<u8>) {
381 self.responses.lock().unwrap().push((
382 url.to_string(),
383 Ok(HttpResponse {
384 status,
385 body,
386 headers: vec![],
387 }),
388 ));
389 }
390 }
391
392 impl HttpClient for MockClient {
393 fn send(&self, request: HttpRequest) {
394 self.sent.lock().unwrap().push(request);
395 }
396
397 fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
398 std::mem::take(&mut *self.responses.lock().unwrap())
399 }
400 }
401
402 const TEMPLATE: &str = "https://tiles.example.com/{z}/{x}/{y}.pbf";
403
404 fn build_test_mvt() -> Vec<u8> {
406 fn encode_varint(mut val: u64) -> Vec<u8> {
407 let mut buf = Vec::new();
408 loop {
409 let mut byte = (val & 0x7F) as u8;
410 val >>= 7;
411 if val != 0 {
412 byte |= 0x80;
413 }
414 buf.push(byte);
415 if val == 0 {
416 break;
417 }
418 }
419 buf
420 }
421 fn encode_tag(field: u32, wt: u8) -> Vec<u8> {
422 encode_varint(((field as u64) << 3) | wt as u64)
423 }
424 fn encode_ld(field: u32, data: &[u8]) -> Vec<u8> {
425 let mut b = encode_tag(field, 2);
426 b.extend(encode_varint(data.len() as u64));
427 b.extend_from_slice(data);
428 b
429 }
430 fn encode_vi(field: u32, val: u64) -> Vec<u8> {
431 let mut b = encode_tag(field, 0);
432 b.extend(encode_varint(val));
433 b
434 }
435 fn zigzag(n: i32) -> u32 {
436 ((n << 1) ^ (n >> 31)) as u32
437 }
438
439 let mut geom = Vec::new();
441 geom.extend(encode_varint(((1u64) << 3) | 1)); geom.extend(encode_varint(zigzag(2048) as u64));
443 geom.extend(encode_varint(zigzag(2048) as u64));
444
445 let mut feat = Vec::new();
447 feat.extend(encode_vi(3, 1)); feat.extend(encode_ld(4, &geom));
449
450 let mut layer = Vec::new();
452 layer.extend(encode_ld(1, b"test"));
453 layer.extend(encode_ld(2, &feat));
454 layer.extend(encode_vi(5, 4096));
455 layer.extend(encode_vi(15, 2));
456
457 encode_ld(3, &layer)
459 }
460
461 #[test]
462 fn url_template_substitution() {
463 let client = MockClient::new();
464 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(client));
465 let url = source.tile_url(&TileId::new(10, 512, 340));
466 assert_eq!(url, "https://tiles.example.com/10/512/340.pbf");
467 }
468
469 #[test]
470 fn request_sends_http_get() {
471 let _client = MockClient::new();
472 let sent = Arc::new(StdMutex::new(Vec::new()));
473 let sent_clone = sent.clone();
474
475 struct TrackingClient {
476 sent: Arc<StdMutex<Vec<String>>>,
477 }
478 impl HttpClient for TrackingClient {
479 fn send(&self, request: HttpRequest) {
480 self.sent.lock().unwrap().push(request.url);
481 }
482 fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
483 Vec::new()
484 }
485 }
486
487 let source =
488 HttpVectorTileSource::new(TEMPLATE, Box::new(TrackingClient { sent: sent_clone }));
489 source.request(TileId::new(5, 10, 20));
490
491 let urls = sent.lock().unwrap().clone();
492 assert_eq!(urls, vec!["https://tiles.example.com/5/10/20.pbf"]);
493 }
494
495 #[test]
496 fn successful_fetch_decodes_mvt() {
497 let client = MockClient::new();
498 let url = "https://tiles.example.com/0/0/0.pbf";
499 let mvt_bytes = build_test_mvt();
500 client.queue_response(url, 200, mvt_bytes);
501
502 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(client));
503 source.request(TileId::new(0, 0, 0));
504
505 let results = source.poll();
506 assert_eq!(results.len(), 1);
507 let (id, result) = &results[0];
508 assert_eq!(*id, TileId::new(0, 0, 0));
509
510 let response = result.as_ref().expect("should succeed");
511 match &response.data {
512 TileData::Vector(vt) => {
513 assert!(vt.layers.contains_key("test"));
514 assert_eq!(vt.layers["test"].len(), 1);
515 }
516 other => panic!("expected Vector tile data, got {:?}", other),
517 }
518 }
519
520 #[test]
521 fn http_404_returns_not_found() {
522 let client = MockClient::new();
523 client.queue_response("https://tiles.example.com/0/0/0.pbf", 404, vec![]);
524
525 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(client));
526 source.request(TileId::new(0, 0, 0));
527
528 let results = source.poll();
529 assert_eq!(results.len(), 1);
530 assert!(matches!(results[0].1, Err(TileError::NotFound(_))));
531 }
532
533 #[test]
534 fn cancel_removes_pending() {
535 let client = MockClient::new();
536 client.queue_response("https://tiles.example.com/0/0/0.pbf", 200, build_test_mvt());
537
538 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(client));
539 source.request(TileId::new(0, 0, 0));
540 assert_eq!(source.pending_count(), 1);
541
542 source.cancel(TileId::new(0, 0, 0));
543 assert_eq!(source.pending_count(), 0);
544
545 let results = source.poll();
546 assert!(results.is_empty());
547 }
548
549 #[test]
550 fn with_tilejson_attaches_metadata() {
551 let client = MockClient::new();
552 let tj = TileJson::with_tiles(vec!["https://example.com/{z}/{x}/{y}.pbf".into()]);
553 let source =
554 HttpVectorTileSource::new(TEMPLATE, Box::new(client)).with_tilejson(tj.clone());
555 assert!(source.tilejson().is_some());
556 assert_eq!(source.tilejson().unwrap().tiles.len(), 1);
557 }
558
559 #[test]
560 fn default_headers_are_sent() {
561 #[derive(Clone)]
562 struct HeaderCapture {
563 last_headers: Arc<StdMutex<Vec<(String, String)>>>,
564 }
565 impl HttpClient for HeaderCapture {
566 fn send(&self, request: HttpRequest) {
567 *self.last_headers.lock().unwrap() = request.headers;
568 }
569 fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
570 Vec::new()
571 }
572 }
573
574 let capture = HeaderCapture {
575 last_headers: Arc::new(StdMutex::new(Vec::new())),
576 };
577 let headers_ref = capture.last_headers.clone();
578
579 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(capture))
580 .with_header("Authorization", "Bearer tok");
581
582 source.request(TileId::new(0, 0, 0));
583
584 let headers = headers_ref.lock().unwrap().clone();
585 assert_eq!(headers.len(), 1);
586 assert_eq!(headers[0].0, "Authorization");
587 }
588
589 #[test]
590 fn debug_impl() {
591 let client = MockClient::new();
592 let source = HttpVectorTileSource::new(TEMPLATE, Box::new(client));
593 let dbg = format!("{source:?}");
594 assert!(dbg.contains("HttpVectorTileSource"));
595 }
596
597 #[test]
598 fn deferred_decode_returns_raw_vector() {
599 let client = MockClient::new();
600 let url = "https://tiles.example.com/0/0/0.pbf";
601 let mvt_bytes = build_test_mvt();
602 client.queue_response(url, 200, mvt_bytes.clone());
603
604 let source =
605 HttpVectorTileSource::new(TEMPLATE, Box::new(client)).with_deferred_decode(true);
606 assert!(source.deferred_decode());
607
608 source.request(TileId::new(0, 0, 0));
609 let results = source.poll();
610 assert_eq!(results.len(), 1);
611
612 let (id, result) = &results[0];
613 assert_eq!(*id, TileId::new(0, 0, 0));
614 let response = result.as_ref().expect("should succeed");
615 assert!(
616 response.data.is_raw_vector(),
617 "deferred mode should return RawVector"
618 );
619
620 let raw = response.data.as_raw_vector().expect("should be RawVector");
621 assert_eq!(raw.tile_id, TileId::new(0, 0, 0));
622 assert_eq!(raw.bytes.len(), mvt_bytes.len());
623 }
624
625 #[test]
626 fn deferred_decode_raw_bytes_can_be_decoded_later() {
627 let client = MockClient::new();
628 let url = "https://tiles.example.com/0/0/0.pbf";
629 client.queue_response(url, 200, build_test_mvt());
630
631 let source =
632 HttpVectorTileSource::new(TEMPLATE, Box::new(client)).with_deferred_decode(true);
633 source.request(TileId::new(0, 0, 0));
634 let results = source.poll();
635 let response = results[0].1.as_ref().expect("should succeed");
636 let raw = response.data.as_raw_vector().expect("should be RawVector");
637
638 let layers = crate::mvt::decode_mvt(&raw.bytes, &raw.tile_id, &raw.decode_options)
640 .expect("should decode");
641 assert!(layers.contains_key("test"));
642 assert_eq!(layers["test"].len(), 1);
643 }
644
645 #[test]
646 fn deferred_decode_off_returns_vector() {
647 let client = MockClient::new();
648 let url = "https://tiles.example.com/0/0/0.pbf";
649 client.queue_response(url, 200, build_test_mvt());
650
651 let source =
652 HttpVectorTileSource::new(TEMPLATE, Box::new(client)).with_deferred_decode(false);
653 assert!(!source.deferred_decode());
654
655 source.request(TileId::new(0, 0, 0));
656 let results = source.poll();
657 let response = results[0].1.as_ref().expect("should succeed");
658 assert!(
659 response.data.is_vector(),
660 "non-deferred mode should return Vector"
661 );
662 }
663}