//! Overpass API JSON parser. //! //! This module handles parsing of Overpass API JSON responses into structured //! data that can be tessellated for rendering. use super::geometry::TileKey; use super::label::TileLabel; use super::sprite::{classify_poi_icon, poi_priority, PoiFeature}; use super::style::CompiledMapTheme; use super::tile::TileBuffers; use makepad_widgets::makepad_platform::makepad_micro_serde::*; use std::collections::HashMap; /// Security limit for JSON parsing (Phase 4) const MAX_JSON_SIZE: usize = 50_000_000; // 50MB const MAX_ELEMENTS_PER_TILE: usize = 250_000; const MAX_TAGS_PER_ELEMENT: usize = 100; const MAX_NODES_PER_WAY: usize = 50_000; /// Overpass API response structure #[derive(DeJson)] pub struct OverpassResponse { pub elements: Vec, } /// Single element (node or way) from Overpass API #[derive(DeJson)] pub struct OverpassElement { #[rename(type)] pub kind: String, pub id: i64, pub lat: Option, pub lon: Option, pub nodes: Option>, pub tags: Option>, } /// Internal representation of a way during parsing #[derive(Debug)] pub struct WayData { pub nodes: Vec, pub tags: HashMap, pub closed: bool, } /// Parse Overpass JSON and build tile buffers pub fn build_tile_buffers_from_body( tile_key: TileKey, body: &str, theme: &CompiledMapTheme, ) -> Result { // Security: validate JSON size if body.len() > MAX_JSON_SIZE { return Err(format!( "JSON too large ({} > {} bytes)", body.len(), MAX_JSON_SIZE )); } let parsed = OverpassResponse::deserialize_json_lenient(body) .map_err(|e| format!("json error at line {} col {}: {}", e.line, e.col, e.msg))?; build_tile_buffers_from_response_owned(tile_key, parsed, theme) } /// Build tile buffers from parsed Overpass response pub fn build_tile_buffers_from_response( tile_key: TileKey, response: &OverpassResponse, theme: &CompiledMapTheme, ) -> Result { // Security: validate element count if response.elements.len() > MAX_ELEMENTS_PER_TILE { return Err(format!( "too many elements ({} > {})", response.elements.len(), MAX_ELEMENTS_PER_TILE )); } let mut nodes = HashMap::::new(); let mut ways = Vec::::new(); let mut labels = Vec::::new(); let mut pois = Vec::::new(); for element in &response.elements { process_element(tile_key, element, &mut nodes, &mut ways, &mut labels, &mut pois)?; } super::tessellation::tessellate_tile_buffers(tile_key, theme, nodes, ways, labels, pois) } /// Build tile buffers from owned Overpass response pub fn build_tile_buffers_from_response_owned( tile_key: TileKey, response: OverpassResponse, theme: &CompiledMapTheme, ) -> Result { // Security: validate element count if response.elements.len() > MAX_ELEMENTS_PER_TILE { return Err(format!( "too many elements ({} > {})", response.elements.len(), MAX_ELEMENTS_PER_TILE )); } let mut nodes = HashMap::::new(); let mut ways = Vec::::new(); let mut labels = Vec::::new(); let mut pois = Vec::::new(); for element in response.elements { process_element_owned(tile_key, element, &mut nodes, &mut ways, &mut labels, &mut pois)?; } super::tessellation::tessellate_tile_buffers(tile_key, theme, nodes, ways, labels, pois) } fn process_element( tile_key: TileKey, element: &OverpassElement, nodes: &mut HashMap, ways: &mut Vec, labels: &mut Vec, pois: &mut Vec, ) -> Result<(), String> { match element.kind.as_str() { "node" => { if let (Some(lat), Some(lon)) = (element.lat, element.lon) { nodes.insert(element.id, (lon, lat)); if let Some(tags) = &element.tags { // Security: validate tag count if tags.len() > MAX_TAGS_PER_ELEMENT { return Err(format!( "element has too many tags ({} > {})", tags.len(), MAX_TAGS_PER_ELEMENT )); } let world = super::geometry::lon_lat_to_world(lon, lat, tile_key.z); if let Some(label) = super::label::extract_point_label(tags, (world.x as f32, world.y as f32)) { labels.push(label); } if let Some(icon) = classify_poi_icon(tags) { let name = tags.get("name").cloned().unwrap_or_default(); pois.push(PoiFeature { world_x: world.x, world_y: world.y, icon, priority: poi_priority(icon), name, }); } } } } "way" => { if let Some(node_ids) = &element.nodes { // Security: validate node count if node_ids.len() > MAX_NODES_PER_WAY { return Err(format!( "way has too many nodes ({} > {})", node_ids.len(), MAX_NODES_PER_WAY )); } if let Some(tags) = &element.tags { if tags.len() > MAX_TAGS_PER_ELEMENT { return Err(format!( "element has too many tags ({} > {})", tags.len(), MAX_TAGS_PER_ELEMENT )); } } let closed = node_ids.len() > 2 && node_ids.first().copied() == node_ids.last().copied(); ways.push(WayData { nodes: node_ids.clone(), tags: element.tags.clone().unwrap_or_default(), closed, }); } } _ => {} } Ok(()) } fn process_element_owned( tile_key: TileKey, element: OverpassElement, nodes: &mut HashMap, ways: &mut Vec, labels: &mut Vec, pois: &mut Vec, ) -> Result<(), String> { match element.kind.as_str() { "node" => { if let (Some(lat), Some(lon)) = (element.lat, element.lon) { nodes.insert(element.id, (lon, lat)); if let Some(tags) = element.tags { // Security: validate tag count if tags.len() > MAX_TAGS_PER_ELEMENT { return Err(format!( "element has too many tags ({} > {})", tags.len(), MAX_TAGS_PER_ELEMENT )); } let world = super::geometry::lon_lat_to_world(lon, lat, tile_key.z); if let Some(label) = super::label::extract_point_label(&tags, (world.x as f32, world.y as f32)) { labels.push(label); } if let Some(icon) = classify_poi_icon(&tags) { let name = tags.get("name").cloned().unwrap_or_default(); pois.push(PoiFeature { world_x: world.x, world_y: world.y, icon, priority: poi_priority(icon), name, }); } } } } "way" => { if let Some(node_ids) = element.nodes { // Security: validate node count if node_ids.len() > MAX_NODES_PER_WAY { return Err(format!( "way has too many nodes ({} > {})", node_ids.len(), MAX_NODES_PER_WAY )); } if let Some(tags) = &element.tags { if tags.len() > MAX_TAGS_PER_ELEMENT { return Err(format!( "element has too many tags ({} > {})", tags.len(), MAX_TAGS_PER_ELEMENT )); } } let closed = node_ids.len() > 2 && node_ids.first().copied() == node_ids.last().copied(); ways.push(WayData { nodes: node_ids, tags: element.tags.unwrap_or_default(), closed, }); } } _ => {} } Ok(()) } /// Convert MVT tile to Overpass response (for MBTiles support) pub fn mbtiles_tile_to_overpass_response( tile_key: TileKey, raw_tile_data: &[u8], ) -> Result { use super::mvt_parser::{decode_vector_tile_payload, parse_mvt_tile, MvtTileJsonBuilder}; let pbf_data = decode_vector_tile_payload(raw_tile_data)?; let mut builder = MvtTileJsonBuilder::default(); parse_mvt_tile(&pbf_data, tile_key, &mut builder)?; Ok(builder.to_overpass_response()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_build_tile_buffers_from_body_empty() { let body = r#"{"elements": []}"#; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_body(tile_key, body, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert_eq!(buffers.feature_count, 0); assert!(buffers.labels.is_empty()); assert!(buffers.pois.is_empty()); } #[test] fn test_build_tile_buffers_from_body_with_node() { let body = r#"{ "elements": [ { "type": "node", "id": 123456, "lat": -1.2921, "lon": 36.8219, "tags": { "name": "Nairobi Station", "railway": "station" } } ] }"#; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_body(tile_key, body, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert_eq!(buffers.feature_count, 0); // Nodes don't create features assert!(!buffers.pois.is_empty()); // Should have POI } #[test] fn test_build_tile_buffers_from_body_with_way() { let body = r#"{ "elements": [ { "type": "node", "id": 1, "lat": -1.2921, "lon": 36.8219 }, { "type": "node", "id": 2, "lat": -1.2922, "lon": 36.8220 }, { "type": "way", "id": 123, "nodes": [1, 2], "tags": { "highway": "primary", "name": "Main Street" } } ] }"#; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_body(tile_key, body, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert!(buffers.feature_count > 0); // Should have stroke features } #[test] fn test_build_tile_buffers_from_body_malformed_json() { let body = r#"{"elements": ["#; // Malformed JSON let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_body(tile_key, body, &theme); assert!(result.is_err()); } #[test] fn test_build_tile_buffers_from_body_missing_elements() { let body = r#"{}"#; // Missing elements field let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_body(tile_key, body, &theme); assert!(result.is_err()); } #[test] fn test_build_tile_buffers_from_response_empty() { let response = OverpassResponse { elements: vec![], }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_response(tile_key, &response, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert_eq!(buffers.feature_count, 0); } #[test] fn test_build_tile_buffers_from_response_with_node() { let response = OverpassResponse { elements: vec![OverpassElement { kind: "node".to_string(), id: 123456, lat: Some(-1.2921), lon: Some(36.8219), nodes: None, tags: Some({ let mut tags = HashMap::new(); tags.insert("name".to_string(), "Nairobi Station".to_string()); tags.insert("railway".to_string(), "station".to_string()); tags }), }], }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_response(tile_key, &response, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert!(!buffers.pois.is_empty()); } #[test] fn test_build_tile_buffers_from_response_owned_empty() { let response = OverpassResponse { elements: vec![], }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_response_owned(tile_key, response, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert_eq!(buffers.feature_count, 0); } #[test] fn test_build_tile_buffers_from_response_owned_with_node() { let response = OverpassResponse { elements: vec![OverpassElement { kind: "node".to_string(), id: 123456, lat: Some(-1.2921), lon: Some(36.8219), nodes: None, tags: Some({ let mut tags = HashMap::new(); tags.insert("name".to_string(), "Nairobi Station".to_string()); tags.insert("railway".to_string(), "station".to_string()); tags }), }], }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let theme = CompiledMapTheme::default(); let result = build_tile_buffers_from_response_owned(tile_key, response, &theme); assert!(result.is_ok()); let buffers = result.unwrap(); assert!(!buffers.pois.is_empty()); } #[test] fn test_process_element_node() { let mut nodes = HashMap::new(); let mut ways = Vec::new(); let mut labels = Vec::new(); let mut pois = Vec::new(); let element = OverpassElement { kind: "node".to_string(), id: 123456, lat: Some(-1.2921), lon: Some(36.8219), nodes: None, tags: Some({ let mut tags = HashMap::new(); tags.insert("name".to_string(), "Nairobi Station".to_string()); tags.insert("railway".to_string(), "station".to_string()); tags }), }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let result = process_element(tile_key, &element, &mut nodes, &mut ways, &mut labels, &mut pois); assert!(result.is_ok()); assert_eq!(nodes.len(), 1); assert!(!pois.is_empty()); } #[test] fn test_process_element_way() { let mut nodes = HashMap::new(); nodes.insert(1, (36.8219, -1.2921)); nodes.insert(2, (36.8220, -1.2922)); let mut ways = Vec::new(); let mut labels = Vec::new(); let mut pois = Vec::new(); let element = OverpassElement { kind: "way".to_string(), id: 123, lat: None, lon: None, nodes: Some(vec![1, 2]), tags: Some({ let mut tags = HashMap::new(); tags.insert("highway".to_string(), "primary".to_string()); tags.insert("name".to_string(), "Main Street".to_string()); tags }), }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let result = process_element(tile_key, &element, &mut nodes, &mut ways, &mut labels, &mut pois); assert!(result.is_ok()); assert_eq!(ways.len(), 1); } #[test] fn test_process_element_unknown_type() { let mut nodes = HashMap::new(); let mut ways = Vec::new(); let mut labels = Vec::new(); let mut pois = Vec::new(); let element = OverpassElement { kind: "relation".to_string(), id: 123, lat: None, lon: None, nodes: None, tags: None, }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let result = process_element(tile_key, &element, &mut nodes, &mut ways, &mut labels, &mut pois); assert!(result.is_ok()); assert_eq!(nodes.len(), 0); assert_eq!(ways.len(), 0); } #[test] fn test_process_element_owned_node() { let mut nodes = HashMap::new(); let mut ways = Vec::new(); let mut labels = Vec::new(); let mut pois = Vec::new(); let element = OverpassElement { kind: "node".to_string(), id: 123456, lat: Some(-1.2921), lon: Some(36.8219), nodes: None, tags: Some({ let mut tags = HashMap::new(); tags.insert("name".to_string(), "Nairobi Station".to_string()); tags.insert("railway".to_string(), "station".to_string()); tags }), }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let result = process_element_owned(tile_key, element, &mut nodes, &mut ways, &mut labels, &mut pois); assert!(result.is_ok()); assert_eq!(nodes.len(), 1); assert!(!pois.is_empty()); } #[test] fn test_process_element_owned_way() { let mut nodes = HashMap::new(); nodes.insert(1, (36.8219, -1.2921)); nodes.insert(2, (36.8220, -1.2922)); let mut ways = Vec::new(); let mut labels = Vec::new(); let mut pois = Vec::new(); let element = OverpassElement { kind: "way".to_string(), id: 123, lat: None, lon: None, nodes: Some(vec![1, 2]), tags: Some({ let mut tags = HashMap::new(); tags.insert("highway".to_string(), "primary".to_string()); tags.insert("name".to_string(), "Main Street".to_string()); tags }), }; let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let result = process_element_owned(tile_key, element, &mut nodes, &mut ways, &mut labels, &mut pois); assert!(result.is_ok()); assert_eq!(ways.len(), 1); } #[test] fn test_mbtiles_tile_to_overpass_response_invalid_data() { let tile_key = TileKey { z: 14, x: 9250, y: 8247 }; let raw_tile_data = vec![0x00, 0x01, 0x02]; // Invalid MVT data let result = mbtiles_tile_to_overpass_response(tile_key, &raw_tile_data); assert!(result.is_err()); } }