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 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): 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')] } self.create_transportation() self.create_buildings() self.create_power_infrastructure() self.create_vegetation() 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) # Vías férreas if 'railway' in tags: self.create_railway(nodes, transport_layer) def create_road(self, nodes, width, road_type, layer): points = [n for n in nodes] polyline = Draft.make_wire(points, closed=False, face=False) polyline.Label = f"Road_{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): points = [n for n in nodes] rail_line = Draft.make_wire(points, closed=False, face=False) rail_line.Label = "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 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: feature_type = tags['power'] if feature_type == 'line': self.create_power_line( nodes=nodes, tags=tags, layer=power_layer ) elif feature_type == 'substation': self.create_substation( way_id=way_id, tags=tags, nodes=nodes, layer=power_layer ) elif feature_type == 'tower': self.create_power_tower( position=nodes[0] if nodes else None, tags=tags, 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_{voltage}V" # Propiedades visuales wire.ViewObject.LineWidth = 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 = layer.addObject("Part::Feature", "Power_Tower") 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_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") # Árboles individuales for node_id, coords in self.nodes.items(): # Verificar si es un árbol # (Necesitarías procesar los tags de los nodos, implementación simplificada) cylinder = Part.makeCylinder(0.5, 5.0, FreeCAD.Vector(coords[0], coords[1], 0))# * scale - self.Origin ) tree = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree") vegetation_layer.addObject(tree) tree.Shape = cylinder tree.ViewObject.ShapeColor = self.feature_colors['vegetation'] # Áreas verdes for way_id, data in self.ways_data.items(): if 'natural' in data['tags'] or 'landuse' in data['tags']: 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) area = vegetation_layer.addObject("Part::Feature", "GreenArea") area.Shape = face area.ViewObject.ShapeColor = self.feature_colors['vegetation'] 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 = water_layer.addObject("Part::Feature", "WaterBody") water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin ) water.ViewObject.ShapeColor = self.feature_colors['water']