How `parse_osm_data` Transforms Raw OSM Data into Minecraft-Ready Objects in Arnis
parse_osm_data converts raw OpenStreetMap JSON into three internal structures—ProcessedNode, ProcessedWay, and ProcessedRelation—by deserializing the payload, splitting elements by type, converting geographic coordinates to Minecraft X/Z coordinates, and clipping geometries to the target bounding box.
The parse_osm_data function serves as the primary data ingestion pipeline for the Arnis project, transforming chaotic OSM API responses into a clean, typed hierarchy that the rest of the world-generation engine can consume. Located in [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs), this function bridges the gap between latitude/longitude geographic data and the block-based coordinate system of Minecraft.
Overview of the parse_osm_data Pipeline
The transformation occurs in four distinct stages. First, the raw JSON string is deserialized into lightweight temporary structs. Second, elements are categorized into nodes, ways, and relations. Third, geographic coordinates are converted to planar Minecraft coordinates and the three primary processed types are instantiated. Fourth, geometries are clipped to the user-defined bounding box and assembled into the final output vector.
The function produces three core data structures:
| Struct | Purpose |
|---|---|
ProcessedNode |
Represents a single OSM node with its latitude/longitude converted to Minecraft X/Z coordinates and its tag map preserved. |
ProcessedWay |
An ordered list of ProcessedNode objects forming a polyline or polygon, together with associated tags. |
ProcessedRelation |
A collection of ProcessedMember objects (each wrapping a ProcessedWay and a role such as outer, inner, or part) representing multipolygons, buildings, and other complex features. |
Step-by-Step Transformation Process
Step 1: Deserializing Raw OSM JSON
The process begins by parsing the raw JSON payload into the OsmData struct using serde_json. This struct mirrors the schema returned by the OSM Overpass API.
#[derive(Debug, Deserialize)]
pub struct OsmData {
elements: Vec<OsmElement>,
#[serde(default)]
pub remark: Option<String>,
}
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L33-L38), lines 33-38.
Step 2: Splitting Elements by Type
Once deserialized, the function iterates through the elements vector and splits them into separate buckets based on their type field. This categorization simplifies the subsequent processing passes and improves cache locality.
impl SplitOsmData {
fn from_raw_osm_data(osm_data: OsmData) -> Self {
let mut nodes = Vec::new();
let mut ways = Vec::new();
let mut relations = Vec::new();
let mut others = Vec::new();
for element in osm_data.elements {
match element.r#type.as_str() {
"node" => nodes.push(element),
"way" => ways.push(element),
"relation" => relations.push(element),
_ => others.push(element),
}
}
SplitOsmData { nodes, ways, relations, others }
}
}
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L58-L70), lines 58-70.
Step 3: Converting Nodes to ProcessedNode
The first transformation pass processes individual nodes. For each node, the function converts its geographic coordinates to Minecraft X/Z coordinates using a coord_transformer instance. The resulting ProcessedNode is stored in a lookup map (nodes_map) for later reference when building ways.
for element in data.nodes {
if let (Some(lat), Some(lon)) = (element.lat, element.lon) {
let llpoint = LLPoint::new(lat, lon).unwrap();
let xzpoint = coord_transformer.transform_point(llpoint);
let processed = ProcessedNode {
id: element.id,
tags: element.tags.clone().unwrap_or_default(),
x: xzpoint.x,
z: xzpoint.z,
};
nodes_map.insert(element.id, processed.clone());
// Keep only tagged nodes that lie inside (or near) the bbox
if !element.tags.as_ref().map(|t| t.is_empty()).unwrap_or(true) && xzbbox.contains(&xzpoint) {
processed_elements.push(ProcessedElement::Node(processed));
}
}
}
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L200-L227), lines 200-227.
The ProcessedNode struct definition includes the X and Z fields:
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L84-L92), lines 84-92.
Step 4: Building ProcessedWay Objects
The second pass constructs ways by resolving node references against the nodes_map built in the previous step. Each way is stored unclipped in ways_map because relations may need the full geometry later. However, before being added to the final processed_elements list, the way is clipped to the bounding box using clip_way_to_bbox.
for element in data.ways {
let mut nodes = Vec::new();
if let Some(node_ids) = &element.nodes {
for &node_id in node_ids {
if let Some(node) = nodes_map.get(&node_id) {
nodes.push(node.clone());
}
}
}
let way = Arc::new(ProcessedWay {
id: element.id,
tags: element.tags.clone().unwrap_or_default(),
nodes,
});
ways_map.insert(element.id, Arc::clone(&way));
// Clip to the bounding box before storing as a processed element
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
if clipped_nodes.is_empty() { continue; }
let processed = ProcessedWay {
id: element.id,
tags: way.tags.clone(),
nodes: clipped_nodes,
};
processed_elements.push(ProcessedElement::Way(processed));
}
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L232-L267), lines 232-267.
The ProcessedWay struct is defined at lines 104-108:
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L104-L108).
Step 5: Assembling ProcessedRelation Structures
The final pass handles relations, specifically multipolygons and building relations. For each member way, the function resolves the reference from ways_map, determines the role (outer, inner, or part), and applies conditional clipping logic. Water relations and building multipolygons retain their unclipped geometry because downstream processors (such as water_areas.rs) require complete rings for proper polygon reconstruction.
for element in data.relations {
let Some(tags) = &element.tags else { continue };
let relation_type = tags.get("type").map(|x| x.as_str());
// Only multipolygons or building relations are relevant
if relation_type != Some("multipolygon") && relation_type != Some("building") { continue; }
let is_water_relation = is_water_element(tags);
let is_building_multipolygon = (tags.contains_key("building") || tags.contains_key("building:part"))
&& relation_type == Some("multipolygon");
let keep_unclipped = is_water_relation || is_building_multipolygon;
let members: Vec<ProcessedMember> = element.members.iter()
.filter_map(|mem| {
if mem.r#type != "way" { return None; }
// Resolve role (outer/inner/part)
let role = match mem.role.trim().to_ascii_lowercase().as_str() {
"outer" | "outline" => ProcessedMemberRole::Outer,
"inner" => ProcessedMemberRole::Inner,
"part" if relation_type == Some("building") => ProcessedMemberRole::Part,
_ if is_building_relation => ProcessedMemberRole::Outer,
_ => return None,
};
// Retrieve the way (may have been filtered out earlier)
let way = ways_map.get(&mem.r#ref)?.clone();
// Clip unless we need the full geometry (water or building multipolygons)
let final_way = if keep_unclipped {
way
} else {
let clipped = clip_way_to_bbox(&way.nodes, &xzbbox);
if clipped.is_empty() { return None; }
Arc::new(ProcessedWay { id: way.id, tags: way.tags.clone(), nodes: clipped })
};
Some(ProcessedMember { role, way: final_way })
})
.collect();
if !members.is_empty() {
processed_elements.push(ProcessedElement::Relation(ProcessedRelation {
id: element.id,
members,
tags: tags.clone(),
}));
}
}
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L270-L364), lines 270-364.
The supporting struct definitions are located at lines 118-122 (ProcessedMember) and lines 124-128 (ProcessedRelation):
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L118-L122) and
Source: [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs#L124-L128).
Code Example: Using parse_osm_data in Practice
The following example demonstrates how to load raw OSM JSON, define a geographic bounding box, and invoke parse_osm_data to obtain Minecraft-ready elements.
use arnis::osm_parser::{parse_osm_data, OsmData};
use arnis::coordinate_system::geographic::LLBBox;
// 1. Load raw JSON (e.g., from a file or HTTP request)
let raw_json = std::fs::read_to_string("sample.osm.json")?;
let osm_data: OsmData = serde_json::from_str(&raw_json)?;
// 2. Define the area to generate (lat/lon bounding box)
let bbox = LLBBox::new(
LLPoint::new(48.8566, 2.3522).unwrap(), // top-left (Paris)
LLPoint::new(48.80, 2.40).unwrap(), // bottom-right
);
// 3. Parse – scale of 1.0 means 1 meter → 1 block
let (elements, xz_bbox) = parse_osm_data(osm_data, bbox, 1.0, false);
// 4. Iterate over the results
for elem in elements {
match elem {
ProcessedElement::Node(node) => {
println!("Node {} → ({}, {}) tags={}", node.id, node.x, node.z, node.tags.len());
}
ProcessedElement::Way(way) => {
println!("Way {} with {} nodes", way.id, way.nodes.len());
}
ProcessedElement::Relation(rel) => {
println!("Relation {} with {} members", rel.id, rel.members.len());
}
}
}
This example demonstrates the end-to-end flow: raw OSM → bounding box → processed objects ready for world generation.
Key Source Files and Implementation Details
| File | Relevant Content |
|---|---|
src/osm_parser.rs |
Core parsing logic, definitions of ProcessedNode, ProcessedWay, ProcessedRelation, and the parse_osm_data function (lines 58-70, 200-364). |
src/coordinate_system/ |
Helpers that turn latitude/longitude into Minecraft X/Z (CoordTransformer). |
src/clipping.rs |
Geometry clipping (clip_way_to_bbox) used heavily during way and relation processing. |
src/element_processing/water_areas.rs, src/element_processing/buildings.rs |
Consumers of ProcessedRelation that further merge/clip rings for specific feature types. |
src/main.rs |
Entry point that fetches OSM data, builds the bounding box, and calls parse_osm_data. |
Summary
parse_osm_datain [src/osm_parser.rs](https://github.com/louis-e/arnis/blob/main/src/osm_parser.rs) is the central entry point for converting raw OSM JSON into internal Minecraft-ready structures.- The function executes a four-stage pipeline: JSON deserialization, element splitting, coordinate transformation, and geometry clipping.
- ProcessedNode objects store X/Z coordinates converted from latitude/longitude using
coord_transformer.transform_point. - ProcessedWay objects are built by resolving node references from a lookup map, storing unclipped geometries for relation use while emitting clipped versions for final output.
- ProcessedRelation objects handle multipolygons and building relations, with special logic to preserve unclipped geometries for water features and building multipolygons that require full ring reconstruction downstream.
Frequently Asked Questions
What is the purpose of parse_osm_data in Arnis?
parse_osm_data functions as the data normalization layer for the Arnis Minecraft world generator. It takes the unstructured JSON returned by the OpenStreetMap Overpass API and converts it into strongly-typed, coordinate-transformed structs that subsequent processing modules—such as building generators and water area renderers—can consume efficiently.
How does parse_osm_data handle coordinate conversion?
The function utilizes a CoordTransformer instance (defined in the coordinate_system module) to convert each node's latitude and longitude into Minecraft X/Z coordinates. This transformation occurs during the node processing phase, where LLPoint structs are converted to XZPoint structs and stored in the x and z fields of each ProcessedNode.
Why are some relations kept unclipped during processing?
Water relations and building multipolygons retain their unclipped geometries because downstream processors—specifically those in water_areas.rs and buildings.rs—require complete ring structures to correctly reconstruct polygons and perform boolean operations. The parse_osm_data function identifies these cases using the is_water_relation and is_building_multipolygon flags, bypassing the clip_way_to_bbox call for their member ways.
What is the difference between ProcessedWay and ProcessedRelation?
A ProcessedWay represents a linear sequence of nodes forming a line or polygon outline, stored as an ordered Vec<ProcessedNode> with associated tags. A ProcessedRelation represents a higher-level collection of ways (and potentially other relations) with specific roles—such as "outer" rings defining boundaries and "inner" rings defining holes—enabling the representation of complex multipolygons like buildings with courtyards or lakes with islands.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →