Files
PVPlant/Importer/importOSM.py
2025-07-06 01:12:08 +02:00

915 lines
38 KiB
Python

import FreeCAD
import FreeCADGui
import Part
import Draft
from xml.etree import ElementTree as ET
import ssl
import certifi
import urllib.request
import math
import utm
from collections import defaultdict
import PVPlantImportGrid as ImportElevation
scale = 1000.0
class OSMImporter:
def __init__(self, origin):
self.Origin = origin if origin else FreeCAD.Vector(0, 0, 0)
self.overpass_url = "https://overpass-api.de/api/interpreter"
self.nodes = {}
self.ways_data = defaultdict(dict)
self.feature_colors = {
'building': (0.8, 0.8, 0.6),
'highway': {
'motorway': (1.0, 0.4, 0.4),
'trunk': (1.0, 0.6, 0.4),
'primary': (1.0, 0.8, 0.4),
'secondary': (1.0, 1.0, 0.4),
'tertiary': (0.8, 1.0, 0.4),
'residential': (0.6, 0.6, 0.6)
},
'railway': (0.4, 0.4, 0.4),
'power': {
'line': (0.0, 0.0, 0.0),
'tower': (0.3, 0.3, 0.3),
'substation': (0.8, 0.0, 0.0)
},
'vegetation': (0.4, 0.8, 0.4),
'water': (0.4, 0.6, 1.0)
}
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transform_from_latlon(self, lat, lon):
point = ImportElevation.getElevationFromOE([[lat, lon], ])
return FreeCAD.Vector(point[0].x, point[0].y, point[0].z) * scale - self.Origin
'''x, y, _, _ = utm.from_latlon(lat, lon)
return FreeCAD.Vector(x, y, .0) * scale - self.Origin'''
def get_osm_data(self, bbox):
query = f"""
[out:xml][bbox:{bbox}];
(
way["building"];
way["highway"];
way["railway"];
way["power"="line"];
way["power"="substation"];
way["natural"="water"];
way["landuse"="forest"];
node["natural"="tree"];
);
(._;>;);
out body;
"""
req = urllib.request.Request(
self.overpass_url,
data=query.encode('utf-8'),
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
method='POST'
)
return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read()
def create_layer(self, name):
if not FreeCAD.ActiveDocument.getObject(name):
return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
return FreeCAD.ActiveDocument.getObject(name)
def process_osm_data(self, osm_data):
root = ET.fromstring(osm_data)
# Almacenar nodos transformados
for node in root.findall('node'):
self.nodes[node.attrib['id']] = self.transform_from_latlon(
float(node.attrib['lat']),
float(node.attrib['lon'])
)
# Procesar ways
for way in root.findall('way'):
way_id = way.attrib['id']
self.ways_data[way_id] = {
'tags': {tag.attrib['k']: tag.attrib['v'] for tag in way.findall('tag')},
'nodes': [nd.attrib['ref'] for nd in way.findall('nd')]
}
print("1. Create Transportations")
FreeCADGui.updateGui()
self.create_transportation()
print("2. Create Buildings")
FreeCADGui.updateGui()
self.create_buildings()
print("3. Create Power Infrastructure")
FreeCADGui.updateGui()
self.create_power_infrastructure()
print("4. Create Vegetation")
FreeCADGui.updateGui()
self.create_vegetation()
print("5. Create Water Bodies")
FreeCADGui.updateGui()
self.create_water_bodies()
def create_transportation(self):
transport_layer = self.create_layer("Transport")
for way_id, data in self.ways_data.items():
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) < 2:
continue
# Carreteras
if 'highway' in tags:
highway_type = tags['highway']
width = {
'motorway': 10.0,
'trunk': 8.0,
'primary': 6.0,
'secondary': 5.0,
'tertiary': 4.0
}.get(highway_type, 3.0)
self.create_road(nodes, width, highway_type, transport_layer, tags['name'] if 'name' in tags else "")
# Vías férreas
if 'railway' in tags:
self.create_railway(nodes, transport_layer, tags['name'] if 'name' in tags else "")
def create_road(self, nodes, width, road_type, layer, name=""):
points = [n for n in nodes]
polyline = Draft.make_wire(points, closed=False, face=False)
polyline.Label = f"Road_{name if (name != '') else road_type}"
polyline.ViewObject.LineWidth = 2.0
polyline.ViewObject.ShapeColor = self.feature_colors['highway'].get(road_type, (0.5, 0.5, 0.5))
polyline.addProperty("App::PropertyString", "OSMType", "Metadata", "Tipo de vía").OSMType = road_type
polyline.addProperty("App::PropertyFloat", "Width", "Metadata", "Ancho de la vía").Width = width
layer.addObject(polyline)
def create_railway(self, nodes, layer, name=""):
points = [n for n in nodes]
rail_line = Draft.make_wire(points, closed=False, face=False)
rail_line.Label = f"{name if (name != '') else 'Railway'}"
rail_line.ViewObject.LineWidth = 1.5
rail_line.ViewObject.ShapeColor = self.feature_colors['railway']
layer.addObject(rail_line)
def create_buildings(self):
building_layer = self.create_layer("Buildings")
for way_id, data in self.ways_data.items():
if 'building' not in data['tags']:
continue
print(data)
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) < 3:
continue
# Calcular altura
height = self.get_building_height(tags)
# Crear polígono base
polygon_points = [n for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
try:
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
extruded = face.extrude(FreeCAD.Vector(0, 0, height))# * scale - self.Origin )
building = building_layer.addObject("Part::Feature", f"Building_{way_id}")
building.Shape = extruded
building.Label = f"Building ({height}m)"
building.ViewObject.ShapeColor = self.feature_colors['building']
# Metadatos
building.addProperty("App::PropertyFloat", "Height", "Metadata", "Altura del edificio").Height = height
if 'building:levels' in tags:
building.addProperty("App::PropertyInteger", "Levels", "Metadata",
"Niveles del edificio").Levels = int(tags['building:levels'])
except Exception as e:
print(f"Error en edificio {way_id}: {str(e)}")
@staticmethod
def get_building_height(tags):
# Lógica de cálculo de altura
if 'height' in tags:
try:
return float(tags['height'].split()[0])
except:
pass
if 'building:levels' in tags:
try:
return float(tags['building:levels']) * 3.0
except:
pass
return 5.0 # Altura por defecto
def create_power_infrastructure(self):
power_layer = self.create_layer("Power_Infrastructure")
for way_id, data in self.ways_data.items():
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if 'power' in tags:
print("\n\n")
print(tags)
feature_type = tags['power']
if feature_type == 'line':
print("3.1. Create Power Lines")
FreeCADGui.updateGui()
self.create_power_line(
nodes=nodes,
tags=tags,
layer=power_layer
)
elif feature_type == 'substation':
print("3.1. Create substations")
FreeCADGui.updateGui()
self.create_substation(
way_id=way_id,
tags=tags,
nodes=nodes,
layer=power_layer
)
elif feature_type == 'tower':
print("3.1. Create power towers")
FreeCADGui.updateGui()
self.create_power_tower(
position=nodes[0] if nodes else None,
tags=tags,
layer=power_layer
)
'''self.create_power_tower_1(
way=way_id,
layer=power_layer
)'''
def create_power_line(self, nodes, tags, layer):
"""Crea líneas de transmisión eléctrica con propiedades técnicas"""
try:
# Configuración basada en tags
line_type = tags.get('line', 'overhead')
voltage = self.parse_voltage(tags.get('voltage', '0'))
cables = int(tags.get('cables', '3'))
material = tags.get('material', 'aluminum')
# Crear geometría
points = [FreeCAD.Vector(*n) for n in nodes]
if len(points) < 2:
return
wire = Draft.make_wire(points, closed=False, face=False)
wire.Label = f"Power_Line_{int(voltage/1000000)}kV"
# Propiedades visuales
wire.ViewObject.LineWidth = 6 #'''1 + (voltage / 100000)'''
color = self.feature_colors['power']['line']
wire.ViewObject.ShapeColor = color
# Propiedades técnicas
wire.addProperty("App::PropertyFloat", "Voltage", "PowerLine", "Voltage in volts").Voltage = voltage
wire.addProperty("App::PropertyInteger", "Cables", "PowerLine", "Number of conductors").Cables = cables
wire.addProperty("App::PropertyString", "Material", "PowerLine", "Conductor material").Material = material
wire.addProperty("App::PropertyString", "Type", "PowerLine", "Line type").Type = line_type
layer.addObject(wire)
# Añadir torres si es overhead
'''if line_type == 'overhead':
distance_between_towers = 150 # metros por defecto
if 'distance_between_towers' in tags:
try:
distance_between_towers = float(tags['distance_between_towers'])
except:
pass
self.create_power_towers_along_line(
points=points,
voltage=voltage,
distance=distance_between_towers,
layer=layer
)'''
except Exception as e:
FreeCAD.Console.PrintError(f"Error creating power line: {str(e)}\n")
def create_power_towers_along_line(self, points, voltage, distance, layer):
"""Crea torres de transmisión a lo largo de la línea"""
total_length = 0
previous_point = None
for point in points:
if previous_point is not None:
segment_length = (point - previous_point).Length
num_towers = int(segment_length / distance)
if num_towers > 0:
step = segment_length / num_towers
direction = (point - previous_point).normalize()
for i in range(num_towers):
tower_pos = previous_point + (direction * (i * step))
self.create_power_tower(
position=tower_pos,
tags={'voltage': str(voltage)},
layer=layer
)
previous_point = point
def create_power_tower(self, position, tags, layer):
"""Crea una torre de transmisión individual"""
try:
voltage = self.parse_voltage(tags.get('voltage', '0'))
# Dimensiones basadas en voltaje
base_size = 2.0 + (voltage / 100000)
height = 25.0 + (voltage / 10000)
# Geometría de la torre
base = Part.makeBox(base_size, base_size, 3.0,
FreeCAD.Vector(position.x - base_size / 2, position.y - base_size / 2, 0))
mast = Part.makeCylinder(0.5, height, FreeCAD.Vector(position.x, position.y, 3.0))
# Unir componentes
tower = base.fuse(mast)
tower_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Power_Tower")
layer.addObject(tower_obj)
tower_obj.Shape = tower
tower_obj.ViewObject.ShapeColor = self.feature_colors['power']['tower']
# Añadir propiedades
tower_obj.addProperty("App::PropertyFloat", "Voltage", "Technical", "Design voltage").Voltage = voltage
tower_obj.addProperty("App::PropertyFloat", "Height", "Technical", "Tower height").Height = height
# Añadir crossarms (crucetas)
for i in range(3):
crossarm_height = 3.0 + (i * 5.0)
crossarm = Part.makeBox(
4.0, 0.2, 0.2,
FreeCAD.Vector(position.x - 2.0, position.y - 0.1, crossarm_height)
)
tower_obj.Shape = tower_obj.Shape.fuse(crossarm)
except Exception as e:
FreeCAD.Console.PrintError(f"Error creating power tower: {str(e)}\n")
def create_power_line_simple(self, nodes, layer):
# Torres de alta tensión
for node in nodes:
cylinder = Part.makeCylinder(1.0, 20.0, FreeCAD.Vector(node[0], node[1], 0))# * scale - self.Origin )
pole = FreeCAD.ActiveDocument.addObject("Part::Feature", "PowerPole")
layer.addObject(pole)
pole.Shape = cylinder
pole.ViewObject.ShapeColor = self.feature_colors['power']['tower']
# Líneas eléctricas
points = [n for n in nodes]
cable = Draft.make_wire(points, closed=False, face=False)
cable.ViewObject.LineWidth = 3.0
cable.ViewObject.ShapeColor = self.feature_colors['power']['line']
layer.addObject(cable)
def create_power_tower_1(self, way, layer):
"""Crea una torre eléctrica según especificaciones OSM"""
tags = way['tags']
nodes = [self.nodes[ref] for ref in way['nodes'] if ref in self.nodes]
if not nodes:
return
try:
# Obtener parámetros principales
structure_type = tags.get('structure', 'lattice')
material = tags.get('material', 'steel')
height = float(tags.get('height', 30.0)) # Altura en metros
voltage = self.parse_voltage(tags.get('voltage', '132000')) # 132kV por defecto
lines = int(tags.get('lines', '3'))
operator = tags.get('operator', '')
tower_name = tags.get('name', f"Tower_{way['id']}")
# Calcular posición (usar primer nodo)
position = FreeCAD.Vector(*nodes[0])
# Configurar dimensiones basadas en parámetros
base_size = self.calculate_base_size(structure_type, height)
crossarm_length = self.calculate_crossarm_length(voltage, lines)
color = self.get_material_color(material)
# Crear geometría según tipo de estructura
if structure_type == 'lattice':
tower = self.create_lattice_tower(height, base_size, crossarm_length)
elif structure_type == 'tubular':
tower = self.create_tubular_tower(height, base_size, crossarm_length)
elif structure_type == 'portal':
tower = self.create_portal_tower(height, base_size, crossarm_length)
else:
tower = self.create_default_tower(height, base_size, crossarm_length)
# Crear objeto en FreeCAD
tower_obj = layer.addObject("Part::Feature", "Power_Tower")
tower_obj.Shape = tower
tower_obj.ViewObject.ShapeColor = color
tower_obj.Placement.Base = position
# Añadir propiedades técnicas
tower_obj.addProperty("App::PropertyFloat", "Voltage", "Technical", "Voltaje nominal (V)").Voltage = voltage
tower_obj.addProperty("App::PropertyFloat", "Height", "Technical", "Altura total (m)").Height = height
tower_obj.addProperty("App::PropertyString", "StructureType", "Technical",
"Tipo de estructura").StructureType = structure_type
tower_obj.addProperty("App::PropertyString", "Material", "Technical",
"Material de construcción").Material = material
tower_obj.addProperty("App::PropertyInteger", "Lines", "Technical", "Número de circuitos").Lines = lines
if operator:
tower_obj.addProperty("App::PropertyString", "Operator", "General", "Operador").Operator = operator
# Añadir cables si existen nodos de conexión
if len(nodes) >= 2:
connection_points = [FreeCAD.Vector(*n) for n in nodes]
self.create_power_lines_between_towers(connection_points, voltage, layer)
except Exception as e:
FreeCAD.Console.PrintError(f"Error creando torre {way['id']}: {str(e)}\n")
def create_lattice_tower(self, height, base_size, crossarm_length):
"""Crea torre de celosía tipo armazón"""
# Base
base = Part.makeBox(base_size, base_size, 3.0)
# Patas principales
leg_profile = Part.makeBox(0.5, 0.5, height)
legs = []
for x in [-base_size / 2, base_size / 2]:
for y in [-base_size / 2, base_size / 2]:
leg = leg_profile.copy()
leg.translate(FreeCAD.Vector(x, y, 3.0))
legs.append(leg)
# Travesaños horizontales
horizontal_bars = []
for z in [10.0, height / 2, height - 5.0]:
bar = Part.makeBox(base_size + 1.0, 0.3, 0.3, FreeCAD.Vector(-(base_size + 1) / 2, -0.15, z))
horizontal_bars.append(bar)
# Crucetas
crossarms = self.create_crossarms(height, crossarm_length)
# Unir todas las partes
tower = base.multiFuse(legs + horizontal_bars + crossarms)
return tower.removeSplitter()
def create_crossarms(self, height, length):
"""Crea crucetas para líneas eléctricas"""
crossarms = []
positions = [
(height - 5.0, 0), # Superior
(height - 15.0, 22.5), # Media con ángulo
(height - 25.0, -22.5) # Inferior con ángulo
]
for z, angle in positions:
crossarm = Part.makeBox(length, 0.3, 0.3)
crossarm.rotate(FreeCAD.Vector(0, 0, z), FreeCAD.Vector(0, 0, 1), angle)
crossarm.translate(FreeCAD.Vector(-length / 2, -0.15, z))
crossarms.append(crossarm)
return crossarms
def calculate_base_size(self, structure_type, height):
"""Calcula tamaño de base según tipo de estructura y altura"""
base_sizes = {
'lattice': 4.0 + (height * 0.1),
'tubular': 3.0 + (height * 0.05),
'portal': 6.0,
'default': 3.0
}
return base_sizes.get(structure_type, base_sizes['default'])
def calculate_crossarm_length(self, voltage, lines):
"""Calcula longitud de crucetas según voltaje y número de circuitos"""
return (voltage / 100000) * 8.0 + (lines * 2.0)
def get_material_color(self, material):
"""Devuelve color según material de construcción"""
colors = {
'steel': (0.65, 0.65, 0.7),
'concrete': (0.8, 0.8, 0.8),
'wood': (0.5, 0.3, 0.2),
'aluminum': (0.9, 0.9, 0.9),
'default': (0.5, 0.5, 0.5)
}
return colors.get(material.lower(), colors['default'])
def create_power_lines_between_towers(self, points, voltage, layer):
"""Crea cables entre torres"""
cable_thickness = 0.1 + (voltage / 500000)
for i in range(len(points) - 1):
start = points[i]
end = points[i + 1]
# Crear cable curvo (catenaria)
cable = self.create_catenary(start, end, sag=5.0)
cable_obj = layer.addObject("Part::Feature", f"Power_Line_{i}")
cable_obj.Shape = cable
cable_obj.ViewObject.LineWidth = cable_thickness * 1000 # Convertir a mm
cable_obj.ViewObject.ShapeColor = (0.1, 0.1, 0.1)
def create_catenary(self, start, end, sag=5.0):
"""Crea curva de catenaria para cables eléctricos"""
mid_point = (start + end) / 2
mid_point.z -= sag
arc = Part.Arc(
start,
mid_point,
end
)
return Part.Edge(arc.toShape())
def create_substation(self, way_id, tags, nodes, layer):
"""Crea subestaciones con todos los componentes detallados"""
try:
# 1. Parámetros base
voltage = self.parse_voltage(tags.get('voltage', '0'))
substation_type = tags.get('substation', 'distribution')
name = tags.get('name', f"Substation_{way_id}")
if len(nodes) < 3:
FreeCAD.Console.PrintWarning(f"Subestación {way_id} ignorada: polígono inválido\n")
return
# 2. Geometría base
polygon_points = [n for n in nodes if isinstance(n, FreeCAD.Vector)]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
# 3. Base del terreno
base_height = 0.3
try:
base_shape = Part.makePolygon(polygon_points)
base_face = Part.Face(base_shape)
base_extrude = base_face.extrude(FreeCAD.Vector(0, 0, base_height))
base_obj = layer.addObject("Part::Feature", f"{name}_Base")
base_obj.Shape = base_extrude
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
except Exception as e:
FreeCAD.Console.PrintError(f"Error base {way_id}: {str(e)}\n")
# 4. Cercado perimetral
if tags.get('fence', 'no') == 'yes':
try:
fence_offset = -0.8 # metros hacia adentro
fence_points = self.offset_polygon(polygon_points, fence_offset)
if len(fence_points) > 2:
fence_shape = Part.makePolygon(fence_points)
fence_face = Part.Face(fence_shape)
fence_extrude = fence_face.extrude(FreeCAD.Vector(0, 0, 2.8))
fence_obj = layer.addObject("Part::Feature", f"{name}_Fence")
fence_obj.Shape = fence_extrude
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error cerca {way_id}: {str(e)}\n")
# 5. Edificio principal
if tags.get('building', 'no') == 'yes':
try:
building_offset = -2.0 # metros hacia adentro
building_height = 4.5 + (voltage / 100000)
building_points = self.offset_polygon(polygon_points, building_offset)
if len(building_points) > 2:
building_shape = Part.makePolygon(building_points)
building_face = Part.Face(building_shape)
building_extrude = building_face.extrude(FreeCAD.Vector(0, 0, building_height))
building_obj = layer.addObject("Part::Feature", f"{name}_Building")
building_obj.Shape = building_extrude
building_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error edificio {way_id}: {str(e)}\n")
# 6. Transformadores
try:
num_transformers = int(tags.get('transformers', 1))
for i in range(num_transformers):
transformer_pos = self.calculate_equipment_position(
polygon_points,
index=i,
total=num_transformers,
offset=3.0
)
transformer = self.create_transformer(
position=transformer_pos,
voltage=voltage,
tech_type=tags.get('substation:type', 'outdoor')
)
layer.addObject(transformer)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")
# 7. Torre de seccionamiento para alta tensión
if substation_type == 'transmission' and voltage >= 110000:
try:
tower_pos = self.calculate_tower_position(polygon_points)
tower = self.create_circuit_breaker_tower(
position=tower_pos,
voltage=voltage
)
layer.addObject(tower)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
# 8. Propiedades técnicas
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
props = {
"Voltage": voltage,
"Type": substation_type,
"Components": ['Base'] + (['Fence'] if 'fence' in tags else []) +
(['Building'] if 'building' in tags else []) +
[f'Transformer_{n + 1}' for n in range(num_transformers)]
}
for prop, value in props.items():
if isinstance(value, list):
substation_data.addProperty("App::PropertyStringList", prop, "Technical").Components = value
else:
substation_data.addProperty(
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
prop, "Technical").setValue(value)
except Exception as e:
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
def add_substation_fence(self, parent_obj, polygon_points):
"""Añade cerca perimetral"""
try:
offset_points = self.offset_polygon(polygon_points, -0.5)
fence = Part.makePolygon(offset_points).extrude(FreeCAD.Vector(0, 0, 2.5))
fence_obj = parent_obj.Document.addObject("Part::Feature", "Fence")
fence_obj.Shape = fence
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
fence_obj.Placement.Base = parent_obj.Placement.Base
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error en cerca: {str(e)}\n")
def parse_voltage(self, voltage_str):
# Convertir valores comunes de voltaje a numéricos
voltage_map = {
'low': 400,
'medium': 20000,
'high': 110000,
'extra high': 380000,
'ultra high': 765000
}
try:
return float(''.join(filter(str.isdigit, voltage_str.split()[0]))) * 1000
except:
return 20000 # Valor por defecto
def offset_polygon(self, points, offset):
"""Versión corregida sin error de sintaxis"""
if not points:
return []
sum_x = sum(p.x for p in points)
sum_y = sum(p.y for p in points)
centroid = FreeCAD.Vector(sum_x / len(points), sum_y / len(points), 0)
return [p + (centroid - p).normalize() * offset for p in points]
def create_transformer(self, position, voltage, technology):
# Crear transformador básico según características
height = 2.0 + (voltage / 100000)
radius = 0.5 + (voltage / 500000)
transformer = Part.makeCylinder(radius, height, FreeCAD.Vector(position))
transformer_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Transformer")
transformer_obj.Shape = transformer
transformer_obj.ViewObject.ShapeColor = (0.1, 0.1, 0.5) if 'oil' in technology.lower() else (0.5, 0.5, 0.5)
return transformer_obj
def calculate_equipment_position(self, polygon_points, index, total, offset=0.0):
"""Calcula posición equidistante alrededor del perímetro con offset"""
perimeter = sum((p2 - p1).Length for p1, p2 in zip(polygon_points, polygon_points[1:]))
target_dist = (perimeter / total) * index
accumulated = 0.0
for i in range(len(polygon_points) - 1):
p1 = polygon_points[i]
p2 = polygon_points[i + 1]
segment_length = (p2 - p1).Length
if accumulated + segment_length >= target_dist:
direction = (p2 - p1).normalize()
return p1 + direction * (target_dist - accumulated) + direction.cross(FreeCAD.Vector(0, 0, 1)) * offset
accumulated += segment_length
return polygon_points[0]
def create_circuit_breaker_tower(self, position, voltage):
"""Crea torre de seccionamiento especializada"""
tower = Part.makeCompound([
Part.makeCylinder(0.8, 12, position), # Poste principal
Part.makeBox(3, 0.3, 0.3, position + FreeCAD.Vector(-1.5, -0.15, 12)), # Cruceta superior
Part.makeBox(2.5, 0.3, 0.3, position + FreeCAD.Vector(-1.25, -0.15, 9)) # Cruceta media
])
tower_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "CircuitBreakerTower")
tower_obj.Shape = tower
tower_obj.ViewObject.ShapeColor = (0.1, 0.1, 0.1)
tower_obj.addProperty("App::PropertyFloat", "Voltage", "Technical").Voltage = voltage
return tower_obj
def calculate_tower_position(self, polygon_points):
# Colocar torre en el punto medio del lado más largo
# (Implementación compleja que requeriría cálculo geométrico)
return FreeCAD.Vector(polygon_points[0].x, polygon_points[0].y, 0)
def create_vegetation(self):
vegetation_layer = self.create_layer("Vegetation")
# Procesar nodos de vegetación individual
for way_id, tags in self.ways_data.items():
coords = self.nodes.get(way_id)
if not coords:
continue
pos = FreeCAD.Vector(*coords)
if tags.get('natural') == 'tree':
self.create_tree(pos, tags, vegetation_layer)
elif tags.get('natural') == 'shrub':
self.create_shrub(pos, tags, vegetation_layer)
"""elif tags.get('natural') == 'tree_stump':
self.create_tree_stump(pos, vegetation_layer)"""
# Procesar áreas vegetales
for way_id, data in self.ways_data.items():
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if not nodes or len(nodes) < 3:
continue
if tags.get('natural') == 'wood' or tags.get('landuse') == 'forest':
self.create_forest(nodes, tags, vegetation_layer)
elif tags.get('natural') == 'grassland':
self.create_grassland(nodes, vegetation_layer)
elif tags.get('natural') == 'heath':
self.create_heathland(nodes, vegetation_layer)
elif tags.get('natural') == 'scrub':
self.create_scrub_area(nodes, vegetation_layer)
def create_tree(self, position, tags, layer):
"""Crea un árbol individual con propiedades basadas en etiquetas OSM"""
height = float(tags.get('height', 10.0))
trunk_radius = float(tags.get('circumference', 1.0)) / (2 * math.pi)
canopy_radius = float(tags.get('diameter_crown', 4.0)) / 2
# Crear tronco
trunk = Part.makeCylinder(trunk_radius, height, position)
# Crear copa (forma cónica)
canopy_center = position + FreeCAD.Vector(0, 0, height)
canopy = Part.makeCone(canopy_radius, canopy_radius * 0.7, canopy_radius * 1.5, canopy_center)
tree = trunk.fuse(canopy)
tree_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree")
layer.addObject(tree_obj)
tree_obj.Shape = tree
tree_obj.ViewObject.ShapeColor = (0.3, 0.6, 0.2) # Verde bosque
# Añadir metadatos
for prop in ['genus', 'species', 'leaf_type', 'height']:
if prop in tags:
tree_obj.addProperty("App::PropertyString", prop.capitalize(), "Botany",
"Botanical property").__setattr__(prop.capitalize(), tags[prop])
def create_forest(self, nodes, tags, layer):
"""Crea un área boscosa con densidad variable"""
polygon_points = [FreeCAD.Vector(*n) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
# Crear base del bosque
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
forest_base = FreeCAD.ActiveDocument.addObject("Part::Feature", "Forest_Base")
layer.addObject(forest_base)
forest_base.Shape = face
forest_base.ViewObject.ShapeColor = (0.15, 0.4, 0.1) # Verde oscuro
# Generar árboles aleatorios dentro del polígono
density = float(tags.get('density', 0.5)) # Árboles por m²
area = face.Area
num_trees = int(area * density)
for _ in range(num_trees):
rand_point = self.random_point_in_polygon(polygon_points)
self.create_tree(rand_point, {}, layer)
def create_grassland(self, nodes, layer):
"""Crea un área de pastizales"""
polygon_points = [FreeCAD.Vector(*n) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
grassland = FreeCAD.ActiveDocument.addObject("Part::Feature", "Grassland")
layer.addObject(grassland)
grassland.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))
grassland.ViewObject.ShapeColor = (0.5, 0.7, 0.3) # Verde pasto
def create_heathland(self, nodes, layer):
"""Crea un área de brezales con vegetación baja"""
polygon_points = [FreeCAD.Vector(*n) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
heath = FreeCAD.ActiveDocument.addObject("Part::Feature", "Heathland")
layer.addObject(heath)
heath.Shape = face
heath.ViewObject.ShapeColor = (0.6, 0.5, 0.4) # Color terroso
# Añadir arbustos dispersos
for _ in range(int(face.Area * 0.1)): # 1 arbusto cada 10m²
rand_point = self.random_point_in_polygon(polygon_points)
self.create_shrub(rand_point, {}, layer)
def create_shrub(self, position, tags, layer):
"""Crea un arbusto individual"""
height = float(tags.get('height', 1.5))
radius = float(tags.get('diameter_crown', 1.0)) / 2
# Crear forma de arbusto (cono invertido)
base_center = position + FreeCAD.Vector(0, 0, height / 2)
shrub = Part.makeCone(radius, radius * 1.5, height, base_center)
shrub_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Shrub")
layer.addObject(shrub_obj)
shrub_obj.Shape = shrub
shrub_obj.ViewObject.ShapeColor = (0.4, 0.5, 0.3) # Verde arbusto
# Añadir metadatos si existen
if 'genus' in tags:
shrub_obj.addProperty("App::PropertyString", "Genus", "Botany", "Plant genus").Genus = tags['genus']
def create_tree_stump(self, position, layer):
"""Crea un tocón de árbol"""
height = 0.4
radius = 0.5
stump = Part.makeCylinder(radius, height, position)
stump_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree_Stump")
layer.addObject(stump_obj)
stump_obj.Shape = stump
stump_obj.ViewObject.ShapeColor = (0.3, 0.2, 0.1) # Marrón madera
def random_point_in_polygon(self, polygon_points):
"""Genera un punto aleatorio dentro de un polígono"""
min_x = min(p.x for p in polygon_points)
max_x = max(p.x for p in polygon_points)
min_y = min(p.y for p in polygon_points)
max_y = max(p.y for p in polygon_points)
while True:
rand_x = random.uniform(min_x, max_x)
rand_y = random.uniform(min_y, max_y)
rand_point = FreeCAD.Vector(rand_x, rand_y, 0)
# Verificar si el punto está dentro del polígono
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
if face.isInside(rand_point, 0.1, True):
return rand_point
def create_water_bodies(self):
water_layer = self.create_layer("Water")
for way_id, data in self.ways_data.items():
if 'natural' in data['tags'] and data['tags']['natural'] == 'water':
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) > 2:
polygon_points = [n for n in nodes]
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
water = FreeCAD.ActiveDocument.addObject("Part::Feature", "WaterBody")
water_layer.addObject(water)
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
water.ViewObject.ShapeColor = self.feature_colors['water']