Some checks failed
nigig-map / test (push) Has been cancelled
- 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
611 lines
20 KiB
Rust
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());
|
|
}
|
|
}
|