Compare commits

...

7 Commits

Author SHA1 Message Date
louis-e
a86e23129b Address code review feedback 2026-02-01 15:52:15 +01:00
louis-e
69b30ef59f Fix underground building:part and cargo fmt 2026-02-01 15:51:21 +01:00
louis-e
1733f5d664 Enhance urban density grid 2026-02-01 15:39:01 +01:00
louis-e
e6b6de27ff Determine boundary validity 2026-02-01 14:12:33 +01:00
louis-e
ac0fc275dc Enhance OSM query 2026-02-01 13:46:52 +01:00
louis-e
de1f52bfaf Fix power base 2026-02-01 13:15:13 +01:00
louis-e
03cc86f3e2 Add more element processors 2026-02-01 03:14:45 +01:00
11 changed files with 1166 additions and 22 deletions

View File

@@ -277,6 +277,19 @@ impl Block {
196 => "damaged_anvil",
197 => "large_fern",
198 => "large_fern",
199 => "chain",
200 => "end_rod",
201 => "lightning_rod",
202 => "gold_block",
203 => "sea_lantern",
204 => "orange_concrete",
205 => "orange_wool",
206 => "blue_wool",
207 => "green_concrete",
208 => "brick_wall",
209 => "redstone_block",
210 => "chain",
211 => "chain",
_ => panic!("Invalid id"),
}
}
@@ -505,6 +518,17 @@ impl Block {
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
210 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("axis".to_string(), Value::String("x".to_string()));
map
})),
211 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("axis".to_string(), Value::String("z".to_string()));
map
})),
_ => None,
}
}
@@ -752,6 +776,19 @@ pub const CHIPPED_ANVIL: Block = Block::new(195);
pub const DAMAGED_ANVIL: Block = Block::new(196);
pub const LARGE_FERN_LOWER: Block = Block::new(197);
pub const LARGE_FERN_UPPER: Block = Block::new(198);
pub const CHAIN: Block = Block::new(199);
pub const END_ROD: Block = Block::new(200);
pub const LIGHTNING_ROD: Block = Block::new(201);
pub const GOLD_BLOCK: Block = Block::new(202);
pub const SEA_LANTERN: Block = Block::new(203);
pub const ORANGE_CONCRETE: Block = Block::new(204);
pub const ORANGE_WOOL: Block = Block::new(205);
pub const BLUE_WOOL: Block = Block::new(206);
pub const GREEN_CONCRETE: Block = Block::new(207);
pub const BRICK_WALL: Block = Block::new(208);
pub const REDSTONE_BLOCK: Block = Block::new(209);
pub const CHAIN_X: Block = Block::new(210);
pub const CHAIN_Z: Block = Block::new(211);
/// Maps a block to its corresponding stair variant
#[inline]

View File

@@ -85,6 +85,14 @@ pub fn generate_world_with_options(
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
// Collect urban coverage to determine if boundary areas are truly urbanized
// This helps avoid placing stone ground in rural areas within city boundaries
let urban_coverage = flood_fill_cache.collect_urban_coverage(&elements, &xzbbox);
// Build urban density grid for efficient per-coordinate urban checks with rounded edges
let urban_density_grid =
crate::floodfill_cache::UrbanDensityGrid::from_coverage(&urban_coverage, &xzbbox);
// Partition elements: separate boundary elements for deferred processing
// This avoids cloning by moving elements instead of copying them
let (boundary_elements, other_elements): (Vec<_>, Vec<_>) = elements
@@ -182,6 +190,8 @@ pub fn generate_world_with_options(
highways::generate_siding(&mut editor, way);
} else if way.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, &element, args);
} else if way.tags.contains_key("power") {
power::generate_power(&mut editor, &element);
}
// Release flood fill cache entry for this way
flood_fill_cache.remove_way(way.id);
@@ -215,6 +225,14 @@ pub fn generate_world_with_options(
tourisms::generate_tourisms(&mut editor, node);
} else if node.tags.contains_key("man_made") {
man_made::generate_man_made_nodes(&mut editor, node);
} else if node.tags.contains_key("power") {
power::generate_power_nodes(&mut editor, node);
} else if node.tags.contains_key("historic") {
historic::generate_historic(&mut editor, node);
} else if node.tags.contains_key("emergency") {
emergency::generate_emergency(&mut editor, node);
} else if node.tags.contains_key("advertising") {
advertising::generate_advertising(&mut editor, node);
}
}
ProcessedElement::Relation(rel) => {
@@ -276,7 +294,13 @@ pub fn generate_world_with_options(
for element in boundary_elements.into_iter() {
match &element {
ProcessedElement::Way(way) => {
boundaries::generate_boundary(&mut editor, way, args, &flood_fill_cache);
boundaries::generate_boundary(
&mut editor,
way,
args,
&flood_fill_cache,
&urban_density_grid,
);
// Clean up cache entry for consistency with other element processing
flood_fill_cache.remove_way(way.id);
}
@@ -286,6 +310,7 @@ pub fn generate_world_with_options(
rel,
args,
&flood_fill_cache,
&urban_density_grid,
&xzbbox,
);
// Clean up cache entries for consistency with other element processing

View File

@@ -0,0 +1,120 @@
//! Processing of advertising elements.
//!
//! This module handles advertising-related OSM elements including:
//! - `advertising=column` - Cylindrical advertising columns (Litfaßsäule)
//! - `advertising=flag` - Advertising flags on poles
//! - `advertising=poster_box` - Illuminated poster display boxes
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
use rand::Rng;
/// Generate advertising structures from node elements
pub fn generate_advertising(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(advertising_type) = node.tags.get("advertising") {
match advertising_type.as_str() {
"column" => generate_advertising_column(editor, node),
"flag" => generate_advertising_flag(editor, node),
"poster_box" => generate_poster_box(editor, node),
_ => {}
}
}
}
/// Generate an advertising column (Litfaßsäule)
///
/// Creates a simple advertising column.
fn generate_advertising_column(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Two green concrete blocks stacked
editor.set_block(GREEN_CONCRETE, x, 1, z, None, None);
editor.set_block(GREEN_CONCRETE, x, 2, z, None, None);
// Stone brick slab on top
editor.set_block(STONE_BRICK_SLAB, x, 3, z, None, None);
}
/// Generate an advertising flag
///
/// Creates a flagpole with a banner/flag for advertising.
fn generate_advertising_flag(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Use deterministic RNG for flag color
let mut rng = element_rng(node.id);
// Get height from tags or default
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(6)
.clamp(4, 12);
// Flagpole
for y in 1..=height {
editor.set_block(IRON_BARS, x, y, z, None, None);
}
// Flag/banner at top (using colored wool)
// Random bright advertising colors
let flag_colors = [
RED_WOOL,
YELLOW_WOOL,
BLUE_WOOL,
GREEN_WOOL,
ORANGE_WOOL,
WHITE_WOOL,
];
let flag_block = flag_colors[rng.gen_range(0..flag_colors.len())];
// Flag extends to one side (2-3 blocks)
let flag_length = 3;
for dx in 1..=flag_length {
editor.set_block(flag_block, x + dx, height, z, None, None);
editor.set_block(flag_block, x + dx, height - 1, z, None, None);
}
// Finial at top
editor.set_block(IRON_BLOCK, x, height + 1, z, None, None);
}
/// Generate a poster box (city light / lollipop display)
///
/// Creates an illuminated poster display box on a pole.
fn generate_poster_box(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Y=1: Two iron bars next to each other
editor.set_block(IRON_BARS, x, 1, z, None, None);
editor.set_block(IRON_BARS, x + 1, 1, z, None, None);
// Y=2 and Y=3: Two sea lanterns
editor.set_block(SEA_LANTERN, x, 2, z, None, None);
editor.set_block(SEA_LANTERN, x + 1, 2, z, None, None);
editor.set_block(SEA_LANTERN, x, 3, z, None, None);
editor.set_block(SEA_LANTERN, x + 1, 3, z, None, None);
// Y=4: Two polished stone brick slabs
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 4, z, None, None);
}

View File

@@ -6,12 +6,22 @@
//! Boundaries are processed last but only fill empty areas, allowing more specific
//! landuse areas (parks, residential, etc.) to take priority over the general
//! urban ground.
//!
//! # Urban Density Grid
//!
//! To avoid placing stone ground in rural areas that happen to fall within city
//! administrative boundaries, we use a grid-based density check. The world is divided
//! into cells (32×32 blocks = 1 Minecraft chunk), and each cell's urban density is
//! pre-calculated.
//!
//! Stone ground is only placed in cells that exceed the density threshold, with
//! rounded corners where urban cells meet rural cells for natural-looking transitions.
use crate::args::Args;
use crate::block_definitions::*;
use crate::clipping::clip_way_to_bbox;
use crate::coordinate_system::cartesian::XZBBox;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill_cache::{FloodFillCache, UrbanDensityGrid};
use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
@@ -52,11 +62,15 @@ fn is_urban_boundary(tags: &std::collections::HashMap<String, String>) -> bool {
}
/// Generate ground blocks for an urban boundary way.
///
/// Uses the urban density grid to determine which coordinates should receive stone ground.
/// Only places stone in sufficiently urban cells, with rounded corners at urban/rural borders.
pub fn generate_boundary(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
urban_density_grid: &UrbanDensityGrid,
) {
// Check if this is an urban boundary
if !is_urban_boundary(&element.tags) {
@@ -67,10 +81,13 @@ pub fn generate_boundary(
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Fill the area with smooth stone as ground block
// Fill urban areas with smooth stone as ground block
// The grid handles density checks and corner rounding
// Use None, None to only set where no block exists yet - don't overwrite anything
for (x, z) in floor_area {
editor.set_block(SMOOTH_STONE, x, 0, z, None, None);
if urban_density_grid.should_place_stone(x, z) {
editor.set_block(SMOOTH_STONE, x, 0, z, None, None);
}
}
}
@@ -80,6 +97,7 @@ pub fn generate_boundary_from_relation(
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
urban_density_grid: &UrbanDensityGrid,
xzbbox: &XZBBox,
) {
// Check if this is an urban boundary
@@ -122,6 +140,12 @@ pub fn generate_boundary_from_relation(
};
// Generate boundary area from clipped way
generate_boundary(editor, &clipped_way, args, flood_fill_cache);
generate_boundary(
editor,
&clipped_way,
args,
flood_fill_cache,
urban_density_grid,
);
}
}

View File

@@ -138,16 +138,17 @@ pub fn generate_buildings(
];
let accent_block = accent_blocks[rng.gen_range(0..accent_blocks.len())];
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
// Skip building:part if 'layer' or 'level' is -1 or lower (underground parts)
if element.tags.contains_key("building:part") {
if let Some(layer) = element.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) <= -1 {
return;
}
}
}
if let Some(level) = element.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
if let Some(level) = element.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) <= -1 {
return;
}
}
}
@@ -1516,6 +1517,20 @@ pub fn generate_building_from_relation(
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
// Skip building:part relations if layer or level is -1 or lower (underground parts)
if relation.tags.contains_key("building:part") {
if let Some(layer) = relation.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) <= -1 {
return;
}
}
if let Some(level) = relation.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) <= -1 {
return;
}
}
}
// Extract levels from relation tags
let relation_levels = relation
.tags

View File

@@ -0,0 +1,55 @@
//! Processing of emergency infrastructure elements.
//!
//! This module handles emergency-related OSM elements including:
//! - `emergency=fire_hydrant` - Fire hydrants
use crate::block_definitions::*;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
/// Generate emergency infrastructure from node elements
pub fn generate_emergency(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(emergency_type) = node.tags.get("emergency") {
if emergency_type.as_str() == "fire_hydrant" {
generate_fire_hydrant(editor, node)
}
}
}
/// Generate a fire hydrant
///
/// Creates a simple fire hydrant structure using brick wall with redstone block on top.
/// Skips underground, wall-mounted, and pond hydrant types.
fn generate_fire_hydrant(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Get hydrant type - skip underground, wall, and pond types
let hydrant_type = node
.tags
.get("fire_hydrant:type")
.map(|s| s.as_str())
.unwrap_or("pillar");
// Skip non-visible hydrant types
if matches!(hydrant_type, "underground" | "wall" | "pond") {
return;
}
// Simple hydrant: brick wall with redstone block on top
editor.set_block(BRICK_WALL, x, 1, z, None, None);
editor.set_block(REDSTONE_BLOCK, x, 2, z, None, None);
}

View File

@@ -0,0 +1,203 @@
//! Processing of historic elements.
//!
//! This module handles historic OSM elements including:
//! - `historic=memorial` - Memorials, monuments, and commemorative structures
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
use rand::Rng;
/// Generate historic structures from node elements
pub fn generate_historic(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(historic_type) = node.tags.get("historic") {
match historic_type.as_str() {
"memorial" => generate_memorial(editor, node),
"monument" => generate_monument(editor, node),
"wayside_cross" => generate_wayside_cross(editor, node),
_ => {}
}
}
}
/// Generate a memorial structure
///
/// Memorials come in many forms. We determine the type from the `memorial` tag:
/// - plaque: Simple wall-mounted or standing plaque
/// - statue: A statue on a pedestal
/// - sculpture: Artistic sculpture
/// - stone/stolperstein: Memorial stone
/// - bench: Memorial bench (already handled by amenity=bench typically)
/// - cross: Memorial cross
/// - obelisk: Tall pointed pillar
/// - stele: Upright stone slab
/// - bust: Bust on a pedestal
/// - Default: A general monument/pillar
fn generate_memorial(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Use deterministic RNG for consistent results
let mut rng = element_rng(node.id);
// Get memorial subtype
let memorial_type = node
.tags
.get("memorial")
.map(|s| s.as_str())
.unwrap_or("yes");
match memorial_type {
"plaque" => {
// Simple plaque on a small stand
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x, 2, z, None, None);
}
"statue" | "sculpture" | "bust" => {
// Statue on a pedestal
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
editor.set_block(CHISELED_STONE_BRICKS, x, 2, z, None, None);
// Use polished andesite for bronze/metal statue appearance
let statue_block = if rng.gen_bool(0.5) {
POLISHED_ANDESITE
} else {
POLISHED_DIORITE
};
editor.set_block(statue_block, x, 3, z, None, None);
editor.set_block(statue_block, x, 4, z, None, None);
editor.set_block(STONE_BRICK_WALL, x, 5, z, None, None);
}
"stone" | "stolperstein" => {
// Simple memorial stone embedded in ground
let stone_block = if memorial_type == "stolperstein" {
GOLD_BLOCK // Stolpersteine are brass/gold colored
} else {
STONE
};
editor.set_block(stone_block, x, 0, z, None, None);
}
"cross" | "war_memorial" => {
// Memorial cross
generate_cross(editor, x, z, 5);
}
"obelisk" => {
// Tall pointed pillar with fixed height
// Base layer at Y=1
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
}
}
// Second base layer at Y=2
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
}
}
// Stone brick slabs on the 4 corners at Y=3 (on top of corner blocks)
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z - 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z - 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z + 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z + 1, None, None);
// Main shaft, fixed height of 4 blocks (Y=3 to Y=6)
for y in 3..=6 {
editor.set_block(SMOOTH_QUARTZ, x, y, z, None, None);
}
editor.set_block(STONE_BRICK_SLAB, x, 7, z, None, None);
}
"stele" => {
// Upright stone slab
// Base
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
// Upright slab (using wall blocks for thin appearance)
for y in 2..=4 {
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
}
editor.set_block(STONE_BRICK_SLAB, x, 5, z, None, None);
}
_ => {
// Default: simple stone pillar monument
editor.set_block(STONE_BRICKS, x, 2, z, None, None);
editor.set_block(CHISELED_STONE_BRICKS, x, 3, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
}
}
}
/// Generate a monument (larger than memorial)
fn generate_monument(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Monuments are typically larger structures
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(10)
.clamp(5, 20);
// Large base platform
for dx in -2..=2 {
for dz in -2..=2 {
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
}
}
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
}
}
// Main structure
for y in 3..height {
editor.set_block(POLISHED_ANDESITE, x, y, z, None, None);
}
// Decorative top
editor.set_block(CHISELED_STONE_BRICKS, x, height, z, None, None);
}
/// Generate a wayside cross
fn generate_wayside_cross(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Simple roadside cross
generate_cross(editor, x, z, 4);
}
/// Helper function to generate a cross structure
fn generate_cross(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
// Base
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
// Vertical beam
for y in 2..=height {
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
}
// Horizontal beam (cross arm) at 2/3 height, but at least one block below top
let arm_y = (2 + (height * 2 / 3)).min(height - 1);
editor.set_block(STONE_BRICK_WALL, x - 1, arm_y, z, None, None);
editor.set_block(STONE_BRICK_WALL, x + 1, arm_y, z, None, None);
}

View File

@@ -1,14 +1,18 @@
pub mod advertising;
pub mod amenities;
pub mod barriers;
pub mod boundaries;
pub mod bridges;
pub mod buildings;
pub mod doors;
pub mod emergency;
pub mod highways;
pub mod historic;
pub mod landuse;
pub mod leisure;
pub mod man_made;
pub mod natural;
pub mod power;
pub mod railways;
pub mod subprocessor;
pub mod tourisms;

View File

@@ -0,0 +1,354 @@
//! Processing of power infrastructure elements.
//!
//! This module handles power-related OSM elements including:
//! - `power=tower` - Large electricity pylons
//! - `power=pole` - Smaller wooden/concrete poles
//! - `power=line` - Power lines connecting towers/poles
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::osm_parser::{ProcessedElement, ProcessedNode, ProcessedWay};
use crate::world_editor::WorldEditor;
/// Generate power infrastructure from way elements (power lines)
pub fn generate_power(editor: &mut WorldEditor, element: &ProcessedElement) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = element.tags().get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Skip underground power infrastructure
if element
.tags()
.get("location")
.map(|v| v == "underground" || v == "underwater")
.unwrap_or(false)
{
return;
}
if element
.tags()
.get("tunnel")
.map(|v| v == "yes")
.unwrap_or(false)
{
return;
}
if let Some(power_type) = element.tags().get("power") {
match power_type.as_str() {
"line" | "minor_line" => {
if let ProcessedElement::Way(way) = element {
generate_power_line(editor, way);
}
}
"tower" => generate_power_tower(editor, element),
"pole" => generate_power_pole(editor, element),
_ => {}
}
}
}
/// Generate power infrastructure from node elements
pub fn generate_power_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Skip underground power infrastructure
if node
.tags
.get("location")
.map(|v| v == "underground" || v == "underwater")
.unwrap_or(false)
{
return;
}
if node.tags.get("tunnel").map(|v| v == "yes").unwrap_or(false) {
return;
}
if let Some(power_type) = node.tags.get("power") {
let element = ProcessedElement::Node(node.clone());
match power_type.as_str() {
"tower" => generate_power_tower(editor, &element),
"pole" => generate_power_pole(editor, &element),
_ => {}
}
}
}
/// Generate a high-voltage transmission tower (pylon)
///
/// Creates a realistic lattice tower structure using iron bars and iron blocks.
/// The design is a tapered lattice tower with cross-bracing and insulators.
fn generate_power_tower(editor: &mut WorldEditor, element: &ProcessedElement) {
let Some(first_node) = element.nodes().next() else {
return;
};
let x = first_node.x;
let z = first_node.z;
// Extract height from tags, default to 25 blocks (represents ~25-40m real towers)
let height = element
.tags()
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(25)
.clamp(15, 40);
// Tower design constants
let base_width = 3; // Half-width at base (so 7x7 footprint)
let top_width = 1; // Half-width at top (so 3x3)
let arm_height = height - 4; // Height where arms extend
let arm_length = 5; // How far arms extend horizontally
// Build the four corner legs with tapering
for y in 1..=height {
// Calculate taper: legs get closer together as we go up
let progress = y as f32 / height as f32;
let current_width = base_width - ((base_width - top_width) as f32 * progress) as i32;
// Four corner positions
let corners = [
(x - current_width, z - current_width),
(x + current_width, z - current_width),
(x - current_width, z + current_width),
(x + current_width, z + current_width),
];
for (cx, cz) in corners {
editor.set_block(IRON_BLOCK, cx, y, cz, None, None);
}
// Add horizontal cross-bracing every 5 blocks
if y % 5 == 0 && y < height - 2 {
// Connect corners horizontally
for dx in -current_width..=current_width {
editor.set_block(IRON_BLOCK, x + dx, y, z - current_width, None, None);
editor.set_block(IRON_BLOCK, x + dx, y, z + current_width, None, None);
}
for dz in -current_width..=current_width {
editor.set_block(IRON_BLOCK, x - current_width, y, z + dz, None, None);
editor.set_block(IRON_BLOCK, x + current_width, y, z + dz, None, None);
}
}
// Add diagonal bracing between cross-brace levels
if y % 5 >= 1 && y % 5 <= 4 && y > 1 && y < height - 2 {
let prev_width = base_width
- ((base_width - top_width) as f32 * ((y - 1) as f32 / height as f32)) as i32;
// Only add center vertical support if the width changed
if current_width != prev_width || y % 5 == 2 {
editor.set_block(IRON_BARS, x, y, z, None, None);
}
}
}
// Create the cross-arms at arm_height for holding power lines
// These extend outward in two directions (perpendicular to typical line direction)
for arm_offset in [-arm_length, arm_length] {
// Main arm beam (iron blocks for strength)
for dx in 0..=arm_length {
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
editor.set_block(IRON_BLOCK, arm_x, arm_height, z, None, None);
// Add second arm perpendicular
editor.set_block(
IRON_BLOCK,
x,
arm_height,
z + if arm_offset < 0 { -dx } else { dx },
None,
None,
);
}
// Insulators hanging from arm ends (end rods to simulate ceramic insulators)
let end_x = if arm_offset < 0 {
x - arm_length
} else {
x + arm_length
};
editor.set_block(END_ROD, end_x, arm_height - 1, z, None, None);
editor.set_block(END_ROD, x, arm_height - 1, z + arm_offset, None, None);
}
// Add a second, smaller arm set lower for additional circuits
let lower_arm_height = arm_height - 6;
if lower_arm_height > 5 {
let lower_arm_length = arm_length - 1;
for arm_offset in [-lower_arm_length, lower_arm_length] {
for dx in 0..=lower_arm_length {
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
editor.set_block(IRON_BLOCK, arm_x, lower_arm_height, z, None, None);
}
let end_x = if arm_offset < 0 {
x - lower_arm_length
} else {
x + lower_arm_length
};
editor.set_block(END_ROD, end_x, lower_arm_height - 1, z, None, None);
}
}
// Top finial/lightning rod
editor.set_block(IRON_BLOCK, x, height, z, None, None);
editor.set_block(LIGHTNING_ROD, x, height + 1, z, None, None);
// Concrete foundation at base
for dx in -3..=3 {
for dz in -3..=3 {
editor.set_block(GRAY_CONCRETE, x + dx, 0, z + dz, None, None);
}
}
}
/// Generate a wooden/concrete power pole
///
/// Creates a simpler single-pole structure for lower voltage distribution lines.
fn generate_power_pole(editor: &mut WorldEditor, element: &ProcessedElement) {
let Some(first_node) = element.nodes().next() else {
return;
};
let x = first_node.x;
let z = first_node.z;
// Extract height from tags, default to 10 blocks
let height = element
.tags()
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(10)
.clamp(6, 15);
// Determine pole material from tags
let pole_material = element
.tags()
.get("material")
.map(|m| m.as_str())
.unwrap_or("wood");
let pole_block = match pole_material {
"concrete" => LIGHT_GRAY_CONCRETE,
"steel" | "metal" => IRON_BLOCK,
_ => OAK_LOG, // Default to wood
};
// Build the main pole
for y in 1..=height {
editor.set_block(pole_block, x, y, z, None, None);
}
// Cross-arm at top (perpendicular beam for wires)
let arm_length = 2;
for dx in -arm_length..=arm_length {
editor.set_block(OAK_FENCE, x + dx, height, z, None, None);
}
// Insulators at arm ends
editor.set_block(END_ROD, x - arm_length, height + 1, z, None, None);
editor.set_block(END_ROD, x + arm_length, height + 1, z, None, None);
editor.set_block(END_ROD, x, height + 1, z, None, None); // Center insulator
}
/// Generate power lines connecting towers/poles
///
/// Creates a catenary-like curve (simplified) between nodes to simulate
/// the natural sag of power cables.
fn generate_power_line(editor: &mut WorldEditor, way: &ProcessedWay) {
if way.nodes.len() < 2 {
return;
}
// Determine line height based on voltage (higher voltage = taller structures)
let base_height = way
.tags
.get("voltage")
.and_then(|v| v.parse::<i32>().ok())
.map(|voltage| {
if voltage >= 220000 {
22 // High voltage transmission
} else if voltage >= 110000 {
18
} else if voltage >= 33000 {
14
} else {
10 // Distribution lines
}
})
.unwrap_or(15);
// Process consecutive node pairs
for i in 1..way.nodes.len() {
let start = &way.nodes[i - 1];
let end = &way.nodes[i];
// Calculate distance between nodes
let dx = (end.x - start.x) as f64;
let dz = (end.z - start.z) as f64;
let distance = (dx * dx + dz * dz).sqrt();
// Calculate sag based on span length (longer spans = more sag)
let max_sag = (distance / 15.0).clamp(1.0, 6.0) as i32;
// Determine chain orientation based on line direction
// If the line runs more along X-axis, use CHAIN_X; if more along Z-axis, use CHAIN_Z
let chain_block = if dx.abs() >= dz.abs() {
CHAIN_X // Line runs primarily along X-axis
} else {
CHAIN_Z // Line runs primarily along Z-axis
};
// Generate points along the line using Bresenham
let line_points = bresenham_line(start.x, 0, start.z, end.x, 0, end.z);
for (idx, (lx, _, lz)) in line_points.iter().enumerate() {
// Calculate position along the span (0.0 to 1.0)
let t = idx as f64 / line_points.len().max(1) as f64;
// Catenary approximation: sag is maximum at center, zero at ends
// Using parabola: sag = 4 * max_sag * t * (1 - t)
let sag = (4.0 * max_sag as f64 * t * (1.0 - t)) as i32;
let wire_y = base_height - sag;
// Place the wire block (chain aligned with line direction)
editor.set_block(chain_block, *lx, wire_y, *lz, None, None);
// For high voltage lines, add parallel wires offset to sides
if base_height >= 18 {
// Three-phase power: 3 parallel lines
// Offset perpendicular to the line direction
if dx.abs() >= dz.abs() {
// Line runs along X, offset in Z
editor.set_block(chain_block, *lx, wire_y, *lz + 1, None, None);
editor.set_block(chain_block, *lx, wire_y, *lz - 1, None, None);
} else {
// Line runs along Z, offset in X
editor.set_block(chain_block, *lx + 1, wire_y, *lz, None, None);
editor.set_block(chain_block, *lx - 1, wire_y, *lz, None, None);
}
}
}
}
}

View File

@@ -11,13 +11,13 @@ use fnv::FnvHashMap;
use rayon::prelude::*;
use std::time::Duration;
/// A memory-efficient bitmap for storing building footprint coordinates.
/// A memory-efficient bitmap for storing coordinates.
///
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
///
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
pub struct BuildingFootprintBitmap {
pub struct CoordinateBitmap {
/// The bitmap data, where each bit represents one (x, z) coordinate
bits: Vec<u8>,
/// Minimum x coordinate (offset for indexing)
@@ -27,12 +27,13 @@ pub struct BuildingFootprintBitmap {
/// Width of the world (max_x - min_x + 1)
width: usize,
/// Height of the world (max_z - min_z + 1)
#[allow(dead_code)]
height: usize,
/// Number of coordinates marked as building footprints
/// Number of coordinates marked
count: usize,
}
impl BuildingFootprintBitmap {
impl CoordinateBitmap {
/// Creates a new empty bitmap covering the given world bounds.
pub fn new(xzbbox: &XZBBox) -> Self {
let min_x = xzbbox.min_x();
@@ -44,7 +45,7 @@ impl BuildingFootprintBitmap {
// Calculate number of bytes needed (round up to nearest byte)
let total_bits = width
.checked_mul(height)
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
.expect("CoordinateBitmap: world size too large (width * height overflowed)");
let num_bytes = total_bits.div_ceil(8);
Self {
@@ -79,7 +80,7 @@ impl BuildingFootprintBitmap {
Some(local_z * self.width + local_x)
}
/// Sets a coordinate as part of a building footprint.
/// Sets a coordinate.
#[inline]
pub fn set(&mut self, x: i32, z: i32) {
if let Some(bit_index) = self.coord_to_index(x, z) {
@@ -96,7 +97,7 @@ impl BuildingFootprintBitmap {
}
}
/// Checks if a coordinate is part of a building footprint.
/// Checks if a coordinate is set.
#[inline]
pub fn contains(&self, x: i32, z: i32) -> bool {
if let Some(bit_index) = self.coord_to_index(x, z) {
@@ -111,10 +112,193 @@ impl BuildingFootprintBitmap {
/// Returns true if no coordinates are marked.
#[must_use]
#[allow(dead_code)] // Standard API method for collection-like types
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.count == 0
}
/// Returns the number of coordinates that are set.
#[inline]
#[allow(dead_code)]
pub fn count(&self) -> usize {
self.count
}
/// Counts how many coordinates from the given iterator are set in this bitmap.
#[inline]
#[allow(dead_code)]
pub fn count_contained<'a, I>(&self, coords: I) -> usize
where
I: Iterator<Item = &'a (i32, i32)>,
{
coords.filter(|(x, z)| self.contains(*x, *z)).count()
}
}
/// Type alias for building footprint bitmap (for backwards compatibility).
pub type BuildingFootprintBitmap = CoordinateBitmap;
/// Bitmap tracking urban coverage (buildings, roads, paved areas, etc.)
/// Used to determine if a boundary area is actually urbanized.
pub type UrbanCoverageBitmap = CoordinateBitmap;
/// Grid-based urban density map for efficient per-coordinate urban checks.
///
/// Divides the world into cells and pre-calculates the urban density of each cell.
/// Uses distance-based smoothing to create organic boundaries around urban areas
/// instead of blocky grid edges.
pub struct UrbanDensityGrid {
/// Density value (0.0 to 1.0) for each cell, stored in row-major order
cells: Vec<f32>,
/// Size of each cell in blocks
cell_size: i32,
/// Minimum x coordinate of the grid (world coordinates)
min_x: i32,
/// Minimum z coordinate of the grid (world coordinates)
min_z: i32,
/// Number of cells in the x direction
width: usize,
/// Number of cells in the z direction
height: usize,
/// Density threshold for considering a cell "urban"
threshold: f32,
/// Buffer distance in blocks around urban areas
buffer_radius: i32,
}
impl UrbanDensityGrid {
/// Cell size in blocks
const DEFAULT_CELL_SIZE: i32 = 64;
/// Default density threshold (25%)
const DEFAULT_THRESHOLD: f32 = 0.25;
/// Buffer radius around urban areas in blocks
const DEFAULT_BUFFER_RADIUS: i32 = 20;
/// Creates a new urban density grid from the urban coverage bitmap.
pub fn from_coverage(coverage: &UrbanCoverageBitmap, xzbbox: &XZBBox) -> Self {
let cell_size = Self::DEFAULT_CELL_SIZE;
let min_x = xzbbox.min_x();
let min_z = xzbbox.min_z();
// Calculate grid dimensions (round up to cover entire bbox)
let world_width = xzbbox.max_x() - min_x + 1;
let world_height = xzbbox.max_z() - min_z + 1;
let width = ((world_width + cell_size - 1) / cell_size) as usize;
let height = ((world_height + cell_size - 1) / cell_size) as usize;
// Calculate density for each cell
let mut cells = vec![0.0f32; width * height];
for cell_z in 0..height {
for cell_x in 0..width {
let cell_min_x = min_x + (cell_x as i32) * cell_size;
let cell_min_z = min_z + (cell_z as i32) * cell_size;
let cell_max_x = (cell_min_x + cell_size - 1).min(xzbbox.max_x());
let cell_max_z = (cell_min_z + cell_size - 1).min(xzbbox.max_z());
let mut urban_count = 0;
let mut total_count = 0;
for z in cell_min_z..=cell_max_z {
for x in cell_min_x..=cell_max_x {
total_count += 1;
if coverage.contains(x, z) {
urban_count += 1;
}
}
}
let density = if total_count > 0 {
urban_count as f32 / total_count as f32
} else {
0.0
};
cells[cell_z * width + cell_x] = density;
}
}
Self {
cells,
cell_size,
min_x,
min_z,
width,
height,
threshold: Self::DEFAULT_THRESHOLD,
buffer_radius: Self::DEFAULT_BUFFER_RADIUS,
}
}
/// Converts world coordinates to cell coordinates.
#[inline]
fn coord_to_cell(&self, x: i32, z: i32) -> (i32, i32) {
let cell_x = (x - self.min_x) / self.cell_size;
let cell_z = (z - self.min_z) / self.cell_size;
(cell_x, cell_z)
}
/// Checks if a cell is considered urban (above density threshold).
#[inline]
fn is_urban_cell(&self, cell_x: i32, cell_z: i32) -> bool {
if cell_x < 0 || cell_z < 0 {
return false;
}
let cx = cell_x as usize;
let cz = cell_z as usize;
if cx >= self.width || cz >= self.height {
return false;
}
self.cells[cz * self.width + cx] >= self.threshold
}
/// Determines if a coordinate should have stone ground placed.
///
/// Uses distance-based smoothing: a point gets stone if it's within
/// `buffer_radius` blocks of any urban cell's edge.
#[inline]
pub fn should_place_stone(&self, x: i32, z: i32) -> bool {
let (cell_x, cell_z) = self.coord_to_cell(x, z);
// If this cell is urban, always place stone
if self.is_urban_cell(cell_x, cell_z) {
return true;
}
// Check distance to nearby urban cells
// We only need to check cells within buffer_radius distance
let cells_to_check = (self.buffer_radius / self.cell_size) + 2;
let buffer_sq = self.buffer_radius * self.buffer_radius;
for dz in -cells_to_check..=cells_to_check {
for dx in -cells_to_check..=cells_to_check {
let check_x = cell_x + dx;
let check_z = cell_z + dz;
if self.is_urban_cell(check_x, check_z) {
// Calculate distance from point to nearest edge of this urban cell
let cell_min_x = self.min_x + check_x * self.cell_size;
let cell_max_x = cell_min_x + self.cell_size - 1;
let cell_min_z = self.min_z + check_z * self.cell_size;
let cell_max_z = cell_min_z + self.cell_size - 1;
// Distance to nearest point on the cell's bounding box
let nearest_x = x.clamp(cell_min_x, cell_max_x);
let nearest_z = z.clamp(cell_min_z, cell_max_z);
let dist_x = x - nearest_x;
let dist_z = z - nearest_z;
let dist_sq = dist_x * dist_x + dist_z * dist_z;
if dist_sq <= buffer_sq {
return true;
}
}
}
}
false
}
}
/// A cache of pre-computed flood fill results, keyed by element ID.
@@ -283,6 +467,120 @@ impl FloodFillCache {
footprints
}
/// Collects all urban coverage coordinates from the pre-computed cache.
///
/// Urban coverage includes buildings, roads (as line areas), and urban landuse types.
/// This is used to determine if a boundary area is truly urbanized or just rural
/// land that happens to be within administrative city limits.
///
/// # Coverage includes:
/// - Buildings and building:parts
/// - Urban landuse types: residential, commercial, industrial, retail, etc.
/// - Amenities with areas (parking lots, schools, etc.)
///
/// # Note on highways:
/// Linear highways are NOT included because they use bresenham lines, not flood fill.
/// However, urban areas typically have enough buildings + urban landuse to provide
/// adequate coverage signal.
pub fn collect_urban_coverage(
&self,
elements: &[ProcessedElement],
xzbbox: &XZBBox,
) -> UrbanCoverageBitmap {
let mut coverage = UrbanCoverageBitmap::new(xzbbox);
for element in elements {
match element {
ProcessedElement::Way(way) => {
// Check if this is an urban element
if Self::is_urban_coverage_element(way) {
if let Some(cached) = self.way_cache.get(&way.id) {
for &(x, z) in cached {
coverage.set(x, z);
}
}
}
}
ProcessedElement::Relation(rel) => {
// Check buildings
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
if let Some(cached) = self.way_cache.get(&member.way.id) {
for &(x, z) in cached {
coverage.set(x, z);
}
}
}
}
}
// Check urban landuse relations
else if let Some(landuse) = rel.tags.get("landuse") {
if Self::is_urban_landuse(landuse) {
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
if let Some(cached) = self.way_cache.get(&member.way.id) {
for &(x, z) in cached {
coverage.set(x, z);
}
}
}
}
}
}
}
_ => {}
}
}
coverage
}
/// Checks if a way element contributes to urban coverage.
fn is_urban_coverage_element(way: &ProcessedWay) -> bool {
// Buildings are always urban
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
return true;
}
// Urban landuse types
if let Some(landuse) = way.tags.get("landuse") {
if Self::is_urban_landuse(landuse) {
return true;
}
}
// Amenities with areas (parking, schools, etc.)
if way.tags.contains_key("amenity") {
return true;
}
// Highway areas (pedestrian plazas, etc.)
if way.tags.contains_key("highway")
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false)
{
return true;
}
false
}
/// Checks if a landuse type is considered urban.
fn is_urban_landuse(landuse: &str) -> bool {
matches!(
landuse,
"residential"
| "commercial"
| "industrial"
| "retail"
| "railway"
| "construction"
| "education"
| "religious"
| "military"
)
}
/// Removes a way's cached flood fill result, freeing memory.
///
/// Call this after processing an element to release its cached data.

View File

@@ -124,6 +124,7 @@ pub fn fetch_data_from_overpass(
r#"[out:json][timeout:360][bbox:{},{},{},{}];
(
nwr["building"];
nwr["building:part"];
nwr["highway"];
nwr["landuse"];
nwr["natural"];
@@ -134,10 +135,18 @@ pub fn fetch_data_from_overpass(
nwr["tourism"];
nwr["bridge"];
nwr["railway"];
nwr["roller_coaster"];
nwr["barrier"];
nwr["entrance"];
nwr["door"];
nwr["boundary"];
nwr["power"];
nwr["historic"];
nwr["emergency"];
nwr["advertising"];
nwr["man_made"];
nwr["aeroway"];
nwr["area:aeroway"];
way;
)->.relsinbbox;
(