mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-01 09:53:54 -05:00
Compare commits
7 Commits
dependabot
...
more-proce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a86e23129b | ||
|
|
69b30ef59f | ||
|
|
1733f5d664 | ||
|
|
e6b6de27ff | ||
|
|
ac0fc275dc | ||
|
|
de1f52bfaf | ||
|
|
03cc86f3e2 |
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
120
src/element_processing/advertising.rs
Normal file
120
src/element_processing/advertising.rs
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
55
src/element_processing/emergency.rs
Normal file
55
src/element_processing/emergency.rs
Normal 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);
|
||||
}
|
||||
203
src/element_processing/historic.rs
Normal file
203
src/element_processing/historic.rs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
354
src/element_processing/power.rs
Normal file
354
src/element_processing/power.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user