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