915 lines
38 KiB
Python
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']
|
|
|