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, coordinates): points = ImportElevation.getElevationFromOE(coordinates) pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points] return pts 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 coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')] coordinates = self.transform_from_latlon(coordinates) for i, node in enumerate(root.findall('node')): self. nodes[node.attrib['id']] = coordinates[i] '''return 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(): print(data) if 'building' not in data['tags']: continue 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']