nigig-org/crates/apps/map/src/overpass_parser.rs
andodeki 08d3e9a7fb
Some checks failed
nigig-map / test (push) Has been cancelled
fix(map): increase MAX_ELEMENTS_PER_TILE to 250k and add CI workflow
- Increased MAX_ELEMENTS_PER_TILE from 100,000 to 250,000 to handle
  Kenya MBTiles that contain 101k-140k elements per tile
- Updated security limit test to use valid JSON with 260k elements
- Added .forgejo/workflows/nigig-map.yml CI workflow to catch
  map-related regressions in tile parsing, style compilation,
  and tessellation

Fixes runtime errors:
- 'failed to triangulate local mbtile: too many elements (N > 100000)'
- Tiles with 101k-140k elements now process successfully
2026-07-28 20:41:02 +00:00

611 lines
20 KiB
Rust

//! 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<OverpassElement>,
}
/// Single element (node or way) from Overpass API
#[derive(DeJson)]
pub struct OverpassElement {
#[rename(type)]
pub kind: String,
pub id: i64,
pub lat: Option<f64>,
pub lon: Option<f64>,
pub nodes: Option<Vec<i64>>,
pub tags: Option<HashMap<String, String>>,
}
/// Internal representation of a way during parsing
#[derive(Debug)]
pub struct WayData {
pub nodes: Vec<i64>,
pub tags: HashMap<String, String>,
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<TileBuffers, String> {
// 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<TileBuffers, String> {
// 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::<i64, (f64, f64)>::new();
let mut ways = Vec::<WayData>::new();
let mut labels = Vec::<TileLabel>::new();
let mut pois = Vec::<PoiFeature>::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<TileBuffers, String> {
// 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::<i64, (f64, f64)>::new();
let mut ways = Vec::<WayData>::new();
let mut labels = Vec::<TileLabel>::new();
let mut pois = Vec::<PoiFeature>::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<i64, (f64, f64)>,
ways: &mut Vec<WayData>,
labels: &mut Vec<TileLabel>,
pois: &mut Vec<PoiFeature>,
) -> 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<i64, (f64, f64)>,
ways: &mut Vec<WayData>,
labels: &mut Vec<TileLabel>,
pois: &mut Vec<PoiFeature>,
) -> 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<OverpassResponse, String> {
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());
}
}