Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d1127c6b5 | |||
| 7a54e424cb | |||
| 065f840941 | |||
| 74aedf6122 | |||
| 7c81beb1ba | |||
| 02b639d4ed | |||
| fc4142cfec | |||
| a515f31726 | |||
| e0a0dc2f0d | |||
| 02d6c4f412 | |||
| e129aba2fe | |||
| 9d65323052 | |||
| 0b13a8c5f1 | |||
| 3bcdc95978 | |||
| 4b7035e6be | |||
| 02758a6ee8 | |||
| 111df89033 |
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -114,7 +114,7 @@ def makeTrench(base=None):
|
||||
|
||||
try:
|
||||
folder = FreeCAD.ActiveDocument.Trenches
|
||||
except:
|
||||
except AttributeError:
|
||||
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Trenches')
|
||||
folder.Label = "Trenches"
|
||||
folder.addObject(obj)
|
||||
|
||||
+2
-1
@@ -971,7 +971,8 @@ class _PVPlantExportDXF(QtGui.QWidget):
|
||||
if FreeCAD.ActiveDocument.Transport:
|
||||
for road in FreeCAD.ActiveDocument.Transport.Group:
|
||||
base = exporter.createPolyline(road, "CIVIL External Roads")
|
||||
base.dxf.const_width = road.Width
|
||||
if hasattr(road, 'Width'):
|
||||
base.dxf.const_width = road.Width
|
||||
|
||||
axis = exporter.createPolyline(road, "CIVIL External Roads Axis")
|
||||
axis.dxf.const_width = .2
|
||||
|
||||
+149
-45
@@ -43,53 +43,74 @@ class OSMImporter:
|
||||
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
def transform_from_latlon(self, coordinates):
|
||||
"""Transforma coordenadas lat/lon a coordenadas FreeCAD"""
|
||||
if not coordinates:
|
||||
return []
|
||||
|
||||
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=160).read()
|
||||
""" Obtiene datos de OpenStreetMap """
|
||||
# Modificar la consulta en get_osm_data para incluir más tipos de agua:
|
||||
query = f"""[out:xml][bbox:{bbox}];
|
||||
(
|
||||
way["building"];
|
||||
way["highway"];
|
||||
way["railway"];
|
||||
way["power"="line"];
|
||||
way["power"="substation"];
|
||||
way["natural"="water"];
|
||||
way["waterway"];
|
||||
way["waterway"="river"];
|
||||
way["waterway"="stream"];
|
||||
way["waterway"="canal"];
|
||||
way["landuse"="basin"];
|
||||
way["landuse"="reservoir"];
|
||||
node["natural"="tree"];
|
||||
way["landuse"="forest"];
|
||||
way["landuse"="farmland"];
|
||||
);
|
||||
(._;>;);
|
||||
out body;
|
||||
"""
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.overpass_url,
|
||||
data=query.encode('utf-8'),
|
||||
#headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
|
||||
method='POST'
|
||||
)
|
||||
|
||||
response = urllib.request.urlopen(req, context=self.ssl_context, timeout=160)
|
||||
return response.read()
|
||||
except Exception as e:
|
||||
print(f"Error obteniendo datos OSM: {str(e)}")
|
||||
return None
|
||||
|
||||
def create_layer(self, name):
|
||||
"""Crea o obtiene una capa en el documento"""
|
||||
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):
|
||||
"""Procesa los datos XML de OSM"""
|
||||
if not osm_data:
|
||||
print("No hay datos OSM para procesar")
|
||||
return
|
||||
|
||||
root = ET.fromstring(osm_data)
|
||||
|
||||
# Primero, recolectar todos los nodos
|
||||
print(f"Procesando {len(root.findall('node'))} nodos...")
|
||||
|
||||
# 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'])
|
||||
)'''
|
||||
self.nodes[node.attrib['id']] = coordinates[i]
|
||||
|
||||
# Procesar ways
|
||||
for way in root.findall('way'):
|
||||
@@ -166,7 +187,7 @@ class OSMImporter:
|
||||
def create_buildings(self):
|
||||
building_layer = self.create_layer("Buildings")
|
||||
for way_id, data in self.ways_data.items():
|
||||
print(data)
|
||||
#print(data)
|
||||
if 'building' not in data['tags']:
|
||||
continue
|
||||
|
||||
@@ -226,11 +247,11 @@ class OSMImporter:
|
||||
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
|
||||
|
||||
if 'power' in tags:
|
||||
print("\n\n")
|
||||
print(tags)
|
||||
#print("\n\n")
|
||||
#print(tags)
|
||||
feature_type = tags['power']
|
||||
if feature_type == 'line':
|
||||
print("3.1. Create Power Lines")
|
||||
#print("3.1. Create Power Lines")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_power_line(
|
||||
nodes=nodes,
|
||||
@@ -239,7 +260,7 @@ class OSMImporter:
|
||||
)
|
||||
|
||||
elif feature_type == 'substation':
|
||||
print("3.1. Create substations")
|
||||
#print("3.1. Create substations")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_substation(
|
||||
way_id=way_id,
|
||||
@@ -249,7 +270,7 @@ class OSMImporter:
|
||||
)
|
||||
|
||||
elif feature_type == 'tower':
|
||||
print("3.1. Create power towers")
|
||||
#print("3.1. Create power towers")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_power_tower(
|
||||
position=nodes[0] if nodes else None,
|
||||
@@ -562,13 +583,15 @@ class OSMImporter:
|
||||
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 = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Base")
|
||||
layer.addObject(base_obj)
|
||||
base_obj.Shape = base_extrude
|
||||
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
|
||||
except Exception as e:
|
||||
@@ -583,7 +606,8 @@ class OSMImporter:
|
||||
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 = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Fence")
|
||||
layer.addObject(fence_obj)
|
||||
fence_obj.Shape = fence_extrude
|
||||
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
|
||||
except Exception as e:
|
||||
@@ -599,14 +623,15 @@ class OSMImporter:
|
||||
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 = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Building")
|
||||
layer.addObject(building_obj)
|
||||
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:
|
||||
'''try:
|
||||
num_transformers = int(tags.get('transformers', 1))
|
||||
for i in range(num_transformers):
|
||||
transformer_pos = self.calculate_equipment_position(
|
||||
@@ -618,11 +643,11 @@ class OSMImporter:
|
||||
transformer = self.create_transformer(
|
||||
position=transformer_pos,
|
||||
voltage=voltage,
|
||||
tech_type=tags.get('substation:type', 'outdoor')
|
||||
technology=tags.get('substation:type', 'outdoor')
|
||||
)
|
||||
layer.addObject(transformer)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")
|
||||
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:
|
||||
@@ -637,7 +662,8 @@ class OSMImporter:
|
||||
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
|
||||
|
||||
# 8. Propiedades técnicas
|
||||
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
|
||||
substation_data = FreeCAD.ActiveDocument.addObject("App::FeaturePython", f"{name}_Data")
|
||||
layer.addObject(substation_data)
|
||||
props = {
|
||||
"Voltage": voltage,
|
||||
"Type": substation_type,
|
||||
@@ -651,7 +677,8 @@ class OSMImporter:
|
||||
else:
|
||||
substation_data.addProperty(
|
||||
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
|
||||
prop, "Technical").setValue(value)
|
||||
prop, "Technical")
|
||||
setattr(substation_data, prop, value)
|
||||
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
|
||||
@@ -900,9 +927,9 @@ class OSMImporter:
|
||||
if face.isInside(rand_point, 0.1, True):
|
||||
return rand_point
|
||||
|
||||
def create_water_bodies(self):
|
||||
def create_water_bodies_old(self):
|
||||
water_layer = self.create_layer("Water")
|
||||
|
||||
print(self.ways_data)
|
||||
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]
|
||||
@@ -915,3 +942,80 @@ class OSMImporter:
|
||||
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
|
||||
water.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
|
||||
def create_water_bodies(self):
|
||||
|
||||
water_layer = self.create_layer("Water")
|
||||
|
||||
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
|
||||
|
||||
# ===== 1) RÍOS / CANALES (líneas) =====
|
||||
name = self.get_osm_name(tags, tags["waterway"])
|
||||
if 'waterway' in tags:
|
||||
if len(nodes) < 2:
|
||||
continue
|
||||
|
||||
try:
|
||||
width = self.parse_width(tags, default=2.0)
|
||||
points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
|
||||
wire = Draft.make_wire(points, closed=False, face=False)
|
||||
wire.Label = f"{name} ({tags['waterway']})"
|
||||
|
||||
wire.ViewObject.LineWidth = max(1, int(width * 0.5))
|
||||
wire.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
|
||||
water_layer.addObject(wire)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creando waterway {way_id}: {e}")
|
||||
|
||||
continue # importante
|
||||
|
||||
# ===== 2) LAGOS / EMBALSES (polígonos) =====
|
||||
is_area_water = (
|
||||
tags.get('natural') == 'water' or
|
||||
tags.get('landuse') in ['reservoir', 'basin'] or
|
||||
tags.get('water') is not None
|
||||
)
|
||||
|
||||
if not is_area_water or len(nodes) < 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
polygon_points = [FreeCAD.Vector(n.x, n.y, n.z) 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)
|
||||
|
||||
water = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Water_{way_id}")
|
||||
water.Shape = face
|
||||
water.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
water.Label = f"{name} ({tags['waterway']})"
|
||||
|
||||
water_layer.addObject(water)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creando área de agua {way_id}: {e}")
|
||||
|
||||
def get_osm_name(self, tags, fallback=""):
|
||||
for key in ["name", "name:es", "name:en", "alt_name", "ref"]:
|
||||
if key in tags and tags[key].strip():
|
||||
return tags[key]
|
||||
return fallback
|
||||
|
||||
def parse_width(self, tags, default=2.0):
|
||||
for key in ["width", "est_width"]:
|
||||
if key in tags:
|
||||
try:
|
||||
w = tags[key].replace("m", "").strip()
|
||||
return float(w)
|
||||
except:
|
||||
pass
|
||||
return default
|
||||
|
||||
+11
-4
@@ -39,9 +39,11 @@ class PVPlantWorkbench(Workbench):
|
||||
ToolTip = "Workbench for PV design"
|
||||
Icon = str(os.path.join(DirIcons, "icon.svg"))
|
||||
|
||||
def __init__(self):
|
||||
''' init '''
|
||||
|
||||
def Initialize(self):
|
||||
|
||||
#sys.path.append(r"C:\Users\javie\AppData\Roaming\FreeCAD\Mod")
|
||||
sys.path.append(os.path.join(FreeCAD.getUserAppDataDir(), 'Mod'))
|
||||
import PVPlantTools, reload
|
||||
|
||||
@@ -144,7 +146,10 @@ class PVPlantWorkbench(Workbench):
|
||||
from widgets import CountSelection
|
||||
|
||||
def Activated(self):
|
||||
"This function is executed when the workbench is activated"
|
||||
"""This function is executed when the workbench is activated"""
|
||||
|
||||
FreeCAD.Console.PrintLog("Road workbench activated.\n")
|
||||
|
||||
import SelectionObserver
|
||||
import FreeCADGui
|
||||
|
||||
@@ -153,7 +158,9 @@ class PVPlantWorkbench(Workbench):
|
||||
return
|
||||
|
||||
def Deactivated(self):
|
||||
"This function is executed when the workbench is deactivated"
|
||||
"""This function is executed when the workbench is deactivated"""
|
||||
|
||||
FreeCAD.Console.PrintLog("Road workbench deactivated.\n")
|
||||
|
||||
#FreeCADGui.Selection.removeObserver(self.observer)
|
||||
return
|
||||
@@ -201,4 +208,4 @@ class PVPlantWorkbench(Workbench):
|
||||
return "Gui::PythonWorkbench"
|
||||
|
||||
|
||||
Gui.addWorkbench(PVPlantWorkbench())
|
||||
FreeCADGui.addWorkbench(PVPlantWorkbench())
|
||||
|
||||
@@ -760,7 +760,7 @@ class EarthWorksTaskPanel:
|
||||
if len(section) > 0:
|
||||
try:
|
||||
boundary.add(Part.makePolygon(section))
|
||||
except:
|
||||
except Part.OCCError:
|
||||
pass
|
||||
Part.show(boundary)'''
|
||||
#mesh.smooth("Laplace", 3)
|
||||
|
||||
+90
-29
@@ -58,8 +58,9 @@ class MapWindow(QtGui.QWidget):
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
from PySide2.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
# Intentar cargar QtWebEngine (no siempre disponible, ej: FreeCAD flatpak)
|
||||
QWebEngineView, QWebChannel = self._load_webengine()
|
||||
self._webengine_available = QWebEngineView is not None
|
||||
|
||||
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
|
||||
|
||||
@@ -86,36 +87,54 @@ class MapWindow(QtGui.QWidget):
|
||||
self.layout.addWidget(RightWidget)
|
||||
|
||||
# Left Widgets:
|
||||
# -- Search Bar:
|
||||
self.valueSearch = QtGui.QLineEdit(self)
|
||||
self.valueSearch.setPlaceholderText("Search")
|
||||
self.valueSearch.returnPressed.connect(self.onSearch)
|
||||
if self._webengine_available:
|
||||
# -- Search Bar:
|
||||
self.valueSearch = QtGui.QLineEdit(self)
|
||||
self.valueSearch.setPlaceholderText("Search")
|
||||
self.valueSearch.returnPressed.connect(self.onSearch)
|
||||
|
||||
searchbutton = QtGui.QPushButton('Search')
|
||||
searchbutton.setFixedWidth(80)
|
||||
searchbutton.clicked.connect(self.onSearch)
|
||||
searchbutton = QtGui.QPushButton('Search')
|
||||
searchbutton.setFixedWidth(80)
|
||||
searchbutton.clicked.connect(self.onSearch)
|
||||
|
||||
SearchBarLayout = QtGui.QHBoxLayout(self)
|
||||
SearchBarLayout.addWidget(self.valueSearch)
|
||||
SearchBarLayout.addWidget(searchbutton)
|
||||
LeftLayout.addLayout(SearchBarLayout)
|
||||
SearchBarLayout = QtGui.QHBoxLayout(self)
|
||||
SearchBarLayout.addWidget(self.valueSearch)
|
||||
SearchBarLayout.addWidget(searchbutton)
|
||||
LeftLayout.addLayout(SearchBarLayout)
|
||||
|
||||
# -- Webbroser:
|
||||
self.view = QWebEngineView()
|
||||
self.channel = QWebChannel(self.view.page())
|
||||
self.view.page().setWebChannel(self.channel)
|
||||
self.channel.registerObject("MyApp", self)
|
||||
file = os.path.join(DirResources, "webs", "main.html")
|
||||
self.view.page().loadFinished.connect(self.onLoadFinished)
|
||||
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
|
||||
LeftLayout.addWidget(self.view)
|
||||
# self.layout.addWidget(self.view, 1, 0, 1, 3)
|
||||
# -- Web browser:
|
||||
self.view = QWebEngineView()
|
||||
self.channel = QWebChannel(self.view.page())
|
||||
self.view.page().setWebChannel(self.channel)
|
||||
self.channel.registerObject("MyApp", self)
|
||||
file = os.path.join(DirResources, "webs", "main.html")
|
||||
self.view.page().loadFinished.connect(self.onLoadFinished)
|
||||
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
|
||||
LeftLayout.addWidget(self.view)
|
||||
else:
|
||||
# -- Modo manual: entrada de coordenadas sin mapa web
|
||||
self.valueSearch = QtGui.QLineEdit(self)
|
||||
self.valueSearch.setPlaceholderText("Latitud, Longitud (ej: 40.4168, -3.7038)")
|
||||
self.valueSearch.returnPressed.connect(self.onManualCoords)
|
||||
|
||||
searchbutton = QtGui.QPushButton('Ir')
|
||||
searchbutton.setFixedWidth(80)
|
||||
searchbutton.clicked.connect(self.onManualCoords)
|
||||
|
||||
SearchBarLayout = QtGui.QHBoxLayout(self)
|
||||
SearchBarLayout.addWidget(self.valueSearch)
|
||||
SearchBarLayout.addWidget(searchbutton)
|
||||
LeftLayout.addLayout(SearchBarLayout)
|
||||
|
||||
info = QtGui.QLabel("Mapa web no disponible. Introduce coordenadas manualmente.")
|
||||
info.setStyleSheet("color: #888; font-style: italic; padding: 20px;")
|
||||
info.setAlignment(QtCore.Qt.AlignCenter)
|
||||
LeftLayout.addWidget(info)
|
||||
|
||||
# -- Latitud y longitud:
|
||||
self.labelCoordinates = QtGui.QLabel()
|
||||
self.labelCoordinates.setFixedHeight(21)
|
||||
LeftLayout.addWidget(self.labelCoordinates)
|
||||
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
|
||||
|
||||
# Right Widgets:
|
||||
labelKMZ = QtGui.QLabel()
|
||||
@@ -139,9 +158,6 @@ class MapWindow(QtGui.QWidget):
|
||||
radio3 = QtGui.QRadioButton("Datos GPS")
|
||||
radio1.setChecked(True)
|
||||
|
||||
# buttonDialog = QtGui.QPushButton('...')
|
||||
# buttonDialog.setEnabled(False)
|
||||
|
||||
vbox = QtGui.QVBoxLayout(self)
|
||||
vbox.addWidget(radio1)
|
||||
vbox.addWidget(radio2)
|
||||
@@ -149,7 +165,6 @@ class MapWindow(QtGui.QWidget):
|
||||
|
||||
self.groupbox.setLayout(vbox)
|
||||
RightLayout.addWidget(self.groupbox)
|
||||
# ------------------------
|
||||
|
||||
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
|
||||
RightLayout.addWidget(self.checkboxImportGis)
|
||||
@@ -174,6 +189,52 @@ class MapWindow(QtGui.QWidget):
|
||||
with open(file, 'r') as f:
|
||||
frame.runJavaScript(f.read())
|
||||
|
||||
def _load_webengine(self):
|
||||
"""Intenta cargar QWebEngineView desde cualquier versión de PySide.
|
||||
Retorna (QWebEngineView_class, QWebChannel_class) o (None, None)."""
|
||||
for modpath in [
|
||||
'PySide6.QtWebEngineWidgets',
|
||||
'PySide6.QtWebEngineCore',
|
||||
'PySide6.QtWebEngineQuick',
|
||||
'PySide2.QtWebEngineWidgets',
|
||||
'PySide.QtWebEngineWidgets',
|
||||
]:
|
||||
try:
|
||||
parts = modpath.split('.')
|
||||
mod = __import__(parts[0], fromlist=parts[1:])
|
||||
for p in parts[1:]:
|
||||
mod = getattr(mod, p)
|
||||
View = getattr(mod, 'QWebEngineView', None)
|
||||
Channel = getattr(mod, 'QWebChannel', None)
|
||||
if View is not None:
|
||||
return View, Channel
|
||||
except (ImportError, AttributeError):
|
||||
continue
|
||||
# Fallback: intentar por separado QtWebChannel (sí existe en flatpak)
|
||||
try:
|
||||
from PySide6.QtWebChannel import QWebChannel as Channel
|
||||
except ImportError:
|
||||
Channel = None
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"PVPlantGeoreferencing: QtWebEngine no disponible. "
|
||||
"Usando modo manual de coordenadas.\n")
|
||||
return None, Channel
|
||||
|
||||
def onManualCoords(self):
|
||||
"""Procesa entrada manual de latitud,longitud"""
|
||||
text = self.valueSearch.text().strip()
|
||||
if not text:
|
||||
return
|
||||
try:
|
||||
parts = text.replace(',', ' ').split()
|
||||
lat = float(parts[0])
|
||||
lon = float(parts[1])
|
||||
self.georeference_coordinates = {'lat': lat, 'lon': lon}
|
||||
self.labelCoordinates.setText(f"{lat:.6f}, {lon:.6f}")
|
||||
FreeCAD.Console.PrintMessage(f"Coordenadas: {lat:.6f}, {lon:.6f}\n")
|
||||
except (ValueError, IndexError):
|
||||
FreeCAD.Console.PrintError("Formato inválido. Usa: latitud, longitud\n")
|
||||
|
||||
def onSearch(self):
|
||||
if self.valueSearch.text() == "":
|
||||
return
|
||||
@@ -265,7 +326,7 @@ class MapWindow(QtGui.QWidget):
|
||||
for item in items['features']:
|
||||
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
|
||||
coord = item['geometry']['coordinates']
|
||||
point = ImportElevation.getElevationFromOE([[coord[0], coord[1]],])
|
||||
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
|
||||
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
|
||||
if item['properties'].get('radius'):
|
||||
r = round(item['properties']['radius'] * 1000, 0)
|
||||
|
||||
+58
-23
@@ -39,6 +39,51 @@ import os
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
import PVPlantSite
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adaptador UTM: emula la API de la librería 'utm' usando pyproj
|
||||
# La librería 'utm' dejó de usarse en favor de pyproj (más completa y mantenida).
|
||||
# from_latlon(lat, lon) -> (easting, northing, zone_number, zone_letter)
|
||||
# to_latlon(easting, northing, zone_number, zone_letter) -> (lat, lon)
|
||||
# ---------------------------------------------------------------------------
|
||||
_utm_cache = {}
|
||||
|
||||
def _get_transformer(lat, lon):
|
||||
"""Obtiene o crea un transformador UTM para las coordenadas dadas."""
|
||||
from pyproj import Transformer
|
||||
zone = int((lon + 180) / 6) + 1
|
||||
hem = 'S' if lat < 0 else 'N'
|
||||
key = (zone, hem)
|
||||
if key not in _utm_cache:
|
||||
crs_utm = f'+proj=utm +zone={zone} +{hem.lower()} +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
|
||||
_utm_cache[key] = Transformer.from_crs('EPSG:4326', crs_utm, always_xy=True)
|
||||
return _utm_cache[key], zone, hem
|
||||
|
||||
def from_latlon(lat, lon):
|
||||
"""Convierte (lat, lon) a UTM. Retorna (easting, northing, zone_number, zone_letter)."""
|
||||
transformer, zone, hem = _get_transformer(lat, lon)
|
||||
easting, northing = transformer.transform(lon, lat)
|
||||
return (easting, northing, zone, hem)
|
||||
|
||||
def to_latlon(easting, northing, zone_number, zone_letter):
|
||||
"""Convierte UTM a (lat, lon)."""
|
||||
from pyproj import Transformer
|
||||
hem = zone_letter.upper()
|
||||
key = (zone_number, hem)
|
||||
if key not in _utm_cache:
|
||||
crs_utm = f'+proj=utm +zone={zone_number} +{hem.lower()} +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
|
||||
_utm_cache[key] = Transformer.from_crs(crs_utm, 'EPSG:4326', always_xy=True)
|
||||
lon, lat = _utm_cache[key].transform(easting, northing)
|
||||
return (lat, lon)
|
||||
|
||||
# Parche: reemplazar el módulo 'utm' por nuestro adaptador
|
||||
import sys
|
||||
class _UTMWrapper:
|
||||
"""Wrapper para que 'import utm' devuelva nuestras funciones."""
|
||||
from_latlon = staticmethod(from_latlon)
|
||||
to_latlon = staticmethod(to_latlon)
|
||||
sys.modules['utm'] = _UTMWrapper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_elevation_from_oe(coordinates): # v1 deepseek
|
||||
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
|
||||
@@ -52,7 +97,6 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
|
||||
return []
|
||||
|
||||
import requests
|
||||
import utm
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Construcción más eficiente de parámetros
|
||||
@@ -68,7 +112,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
|
||||
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx
|
||||
|
||||
except RequestException as e:
|
||||
print(f"Error en la solicitud: {str(e)}")
|
||||
print(f"Error en la solicitud: {e}")
|
||||
return []
|
||||
|
||||
try:
|
||||
@@ -95,7 +139,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
|
||||
round(result["elevation"])) * 1000)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error procesando coordenadas: {str(e)}")
|
||||
print(f"Error procesando coordenadas: {e}")
|
||||
continue
|
||||
|
||||
return points
|
||||
@@ -110,7 +154,6 @@ def getElevationFromOE(coordinates):
|
||||
return None
|
||||
|
||||
from requests import get
|
||||
import utm
|
||||
|
||||
locations_str=""
|
||||
total = len(coordinates) - 1
|
||||
@@ -119,21 +162,9 @@ def getElevationFromOE(coordinates):
|
||||
if i != total:
|
||||
locations_str += '|'
|
||||
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
|
||||
points = []
|
||||
try:
|
||||
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
|
||||
except RequestException as e:
|
||||
print(f"Error en la solicitud: {str(e)}")
|
||||
points = []
|
||||
for i, point in enumerate(coordinates):
|
||||
c = utm.from_latlon(point[0], point[1])
|
||||
points.append(FreeCAD.Vector(round(c[0], 0),
|
||||
round(c[1], 0),
|
||||
0) * 1000)
|
||||
return points
|
||||
|
||||
# Only get the json response in case of 200 or 201
|
||||
points = []
|
||||
if r.status_code == 200 or r.status_code == 201:
|
||||
results = r.json()
|
||||
for point in results["results"]:
|
||||
c = utm.from_latlon(point["latitude"], point["longitude"])
|
||||
@@ -141,11 +172,18 @@ def getElevationFromOE(coordinates):
|
||||
round(c[1], 0),
|
||||
round(point["elevation"], 0)) * 1000
|
||||
points.append(v)
|
||||
except RequestException as e:
|
||||
# print(f"Error en la solicitud: {str(e)}")
|
||||
for i, point in enumerate(coordinates):
|
||||
c = utm.from_latlon(point[0], point[1])
|
||||
points.append(FreeCAD.Vector(round(c[0], 0),
|
||||
round(c[1], 0),
|
||||
0) * 1000)
|
||||
|
||||
return points
|
||||
|
||||
def getSinglePointElevationFromBing(lat, lng):
|
||||
#http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey}
|
||||
import utm
|
||||
|
||||
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
|
||||
source += str(lat) + "," + str(lng)
|
||||
@@ -170,7 +208,6 @@ def getSinglePointElevationFromBing(lat, lng):
|
||||
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
|
||||
#http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=35.89431,-110.72522,35.89393,-110.72578,35.89374,-110.72606,35.89337,-110.72662
|
||||
# &heights=ellipsoid&samples=10&key={BingMapsAPIKey}
|
||||
import utm
|
||||
import math
|
||||
import requests
|
||||
|
||||
@@ -315,7 +352,6 @@ def getSinglePointElevationUtm(lat, lon):
|
||||
res = s['results']
|
||||
print (res)
|
||||
|
||||
import utm
|
||||
for r in res:
|
||||
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
@@ -327,7 +363,6 @@ def getSinglePointElevationUtm(lat, lon):
|
||||
|
||||
def getElevationUTM(polygon, lat, lng, resolution = 10000):
|
||||
|
||||
import utm
|
||||
geo = utm.from_latlon(lat, lng)
|
||||
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
|
||||
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
|
||||
@@ -396,7 +431,7 @@ def getElevation1(polygon,resolution=10):
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
except:
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
continue
|
||||
|
||||
#points = []
|
||||
@@ -530,7 +565,7 @@ class _ImportPointsTaskPanel:
|
||||
|
||||
try:
|
||||
PointGroups = FreeCAD.ActiveDocument.Point_Groups
|
||||
except:
|
||||
except AttributeError:
|
||||
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
|
||||
PointGroups.Label = "Point Groups"
|
||||
|
||||
|
||||
+2
-2
@@ -1164,8 +1164,8 @@ from scipy.interpolate import LinearNDInterpolator
|
||||
import Part
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide2 import QtCore, QtGui
|
||||
from PySide2.QtWidgets import QListWidgetItem
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtWidgets import QListWidgetItem
|
||||
import os
|
||||
import PVPlantResources
|
||||
|
||||
|
||||
+35
-41
@@ -182,16 +182,14 @@ def makeSolarDiagram(longitude, latitude, scale=1, complete=False, tz=None):
|
||||
import ladybug
|
||||
from ladybug import location
|
||||
from ladybug import sunpath
|
||||
except:
|
||||
# TODO - remove pysolar dependency
|
||||
# FreeCAD.Console.PrintWarning("Ladybug module not found, using pysolar instead. Warning, this will be deprecated in the future\n")
|
||||
except ImportError:
|
||||
ladybug = False
|
||||
try:
|
||||
import pysolar
|
||||
except:
|
||||
except ImportError:
|
||||
try:
|
||||
import Pysolar as pysolar
|
||||
except:
|
||||
except ImportError:
|
||||
FreeCAD.Console.PrintError("The pysolar module was not found. Unable to generate solar diagrams\n")
|
||||
return None
|
||||
else:
|
||||
@@ -361,7 +359,7 @@ def makeWindRose(epwfile, scale=1, sectors=24):
|
||||
try:
|
||||
import ladybug
|
||||
from ladybug import epw
|
||||
except:
|
||||
except ImportError:
|
||||
FreeCAD.Console.PrintError("The ladybug module was not found. Unable to generate solar diagrams\n")
|
||||
return None
|
||||
if not epwfile:
|
||||
@@ -667,23 +665,22 @@ class _PVPlantSite(ArchSite._Site):
|
||||
self.computeAreas(obj)
|
||||
|
||||
def computeAreas(self, obj):
|
||||
"""
|
||||
Compute areas, perimeter and volumes.
|
||||
Override to add custom logic after parent computation.
|
||||
"""
|
||||
ArchSite._Site.computeAreas(self, obj)
|
||||
return
|
||||
|
||||
if not obj.Shape:
|
||||
return
|
||||
|
||||
if obj.Shape.isNull():
|
||||
if obj.Shape.isNull() or not obj.Shape.isValid() or not obj.Shape.Faces:
|
||||
return
|
||||
if not obj.Shape.isValid():
|
||||
return
|
||||
if not obj.Shape.Faces:
|
||||
return
|
||||
if not hasattr(obj, "Perimeter"): # check we have a latest version site
|
||||
if not hasattr(obj, "Perimeter"):
|
||||
return
|
||||
if not obj.Terrain:
|
||||
return
|
||||
# compute area
|
||||
|
||||
# Compute projected area (horizontal projection of all near-horizontal faces)
|
||||
fset = []
|
||||
for f in obj.Shape.Faces:
|
||||
if f.normalAt(0, 0).getAngle(FreeCAD.Vector(0, 0, 1)) < 1.5707:
|
||||
@@ -694,13 +691,11 @@ class _PVPlantSite(ArchSite._Site):
|
||||
for f in fset:
|
||||
try:
|
||||
pf = Part.Face(Part.Wire(Drawing.project(f, FreeCAD.Vector(0, 0, 1))[0].Edges))
|
||||
except Part.OCCError:
|
||||
# error in computing the area. Better set it to zero than show a wrong value
|
||||
if obj.ProjectedArea.Value != 0:
|
||||
print("Error computing areas for ", obj.Label)
|
||||
obj.ProjectedArea = 0
|
||||
else:
|
||||
pset.append(pf)
|
||||
except Part.OCCError:
|
||||
if getattr(obj, 'ProjectedArea', None) and obj.ProjectedArea.Value != 0:
|
||||
FreeCAD.Console.PrintWarning(f"Error computing projected area for {obj.Label}\n")
|
||||
obj.ProjectedArea = 0
|
||||
if pset:
|
||||
self.flatarea = pset.pop()
|
||||
for f in pset:
|
||||
@@ -708,28 +703,27 @@ class _PVPlantSite(ArchSite._Site):
|
||||
self.flatarea = self.flatarea.removeSplitter()
|
||||
if obj.ProjectedArea.Value != self.flatarea.Area:
|
||||
obj.ProjectedArea = self.flatarea.Area
|
||||
# compute perimeter
|
||||
|
||||
# Compute perimeter (border edges only)
|
||||
lut = {}
|
||||
for e in obj.Shape.Edges:
|
||||
lut.setdefault(e.hashCode(), []).append(e)
|
||||
l = 0
|
||||
for e in lut.values():
|
||||
if len(e) == 1: # keep only border edges
|
||||
l += e[0].Length
|
||||
if l:
|
||||
if obj.Perimeter.Value != l:
|
||||
obj.Perimeter = l
|
||||
# compute volumes
|
||||
if obj.Terrain.Shape.Solids:
|
||||
shapesolid = obj.Terrain.Shape.copy()
|
||||
else:
|
||||
shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector)
|
||||
addvol = 0
|
||||
subvol = 0
|
||||
for sub in obj.Subtractions:
|
||||
subvol += sub.Shape.common(shapesolid).Volume
|
||||
for sub in obj.Additions:
|
||||
addvol += sub.Shape.cut(shapesolid).Volume
|
||||
perimeter = sum(e[0].Length for e in lut.values() if len(e) == 1)
|
||||
if perimeter and obj.Perimeter.Value != perimeter:
|
||||
obj.Perimeter = perimeter
|
||||
|
||||
# Compute cut/fill volumes relative to terrain
|
||||
try:
|
||||
if obj.Terrain.Shape.Solids:
|
||||
shapesolid = obj.Terrain.Shape.copy()
|
||||
else:
|
||||
shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
subvol = sum(sub.Shape.common(shapesolid).Volume for sub in obj.Subtractions)
|
||||
addvol = sum(sub.Shape.cut(shapesolid).Volume for sub in obj.Additions)
|
||||
|
||||
if obj.SubtractionVolume.Value != subvol:
|
||||
obj.SubtractionVolume = subvol
|
||||
if obj.AdditionVolume.Value != addvol:
|
||||
@@ -1056,7 +1050,7 @@ class _ViewProviderSite:
|
||||
if hasattr(vobj.Object,"EPWFile") and vobj.Object.EPWFile:
|
||||
try:
|
||||
import ladybug
|
||||
except:
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
self.windrosenode = makeWindRose(vobj.Object.EPWFile,vobj.SolarDiagramScale)
|
||||
|
||||
+147
-71
@@ -129,8 +129,14 @@ class Terrain(ArchComponent.Component):
|
||||
# obj.IfcType = "Fence"
|
||||
# obj.MoveWithHost = False
|
||||
|
||||
self.site = PVPlantSite.get()
|
||||
self.site.Terrain = obj
|
||||
try:
|
||||
self.site = PVPlantSite.get()
|
||||
except Exception:
|
||||
self.site = None
|
||||
if self.site:
|
||||
self.site.Terrain = obj
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning('Terrain: No se encontró Site, algunas funciones DEM requerirán Site.\n')
|
||||
obj.ViewObject.ShapeColor = (0.0000, 0.6667, 0.4980)
|
||||
obj.ViewObject.LineColor = (0.0000, 0.6000, 0.4392)
|
||||
|
||||
@@ -192,6 +198,12 @@ class Terrain(ArchComponent.Component):
|
||||
|
||||
if prop == "InitialMesh":
|
||||
obj.mesh = obj.InitialMesh.copy()
|
||||
# Forzar actualización visual
|
||||
obj.publishProperty("Mesh")
|
||||
|
||||
if prop == "mesh":
|
||||
# La propiedad mesh cambió → forzar recompute para que updateData se dispare
|
||||
pass
|
||||
|
||||
if prop == "DEM" or prop == "CuttingBoundary":
|
||||
from datetime import datetime
|
||||
@@ -237,7 +249,7 @@ class Terrain(ArchComponent.Component):
|
||||
del templist
|
||||
|
||||
# create xy coordinates
|
||||
offset = self.site.Origin
|
||||
offset = self.site.Origin if self.site else FreeCAD.Vector(0, 0, 0)
|
||||
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
|
||||
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
|
||||
datavals = datavals * 1000 # Ajuste de altura
|
||||
@@ -269,35 +281,95 @@ class Terrain(ArchComponent.Component):
|
||||
stepx = math.ceil(nx / stepsize)
|
||||
stepy = math.ceil(ny / stepsize)
|
||||
|
||||
# Malla completa primero como numpy y filtramos todo de una
|
||||
from datetime import datetime
|
||||
t_start = datetime.now()
|
||||
|
||||
# Crear grid completo de coordenadas
|
||||
XX, YY = np.meshgrid(x, y)
|
||||
ZZ = datavals.copy()
|
||||
|
||||
# Enmascarar nodata
|
||||
mask_valida = ZZ != nodata_value
|
||||
|
||||
# Enmascarar cutting boundary si existe
|
||||
if obj.CuttingBoundary:
|
||||
from FreeCAD import Base
|
||||
shape = obj.CuttingBoundary.Shape
|
||||
mask_boundary = np.zeros_like(ZZ, dtype=bool)
|
||||
# Sampling: revisar solo puntos estratégicos para boundary grande
|
||||
stride = max(1, min(nx, ny) // 200)
|
||||
for i in range(0, ny, stride):
|
||||
for j in range(0, nx, stride):
|
||||
if mask_valida[i, j]:
|
||||
if shape.isInside(FreeCAD.Vector(x[j], y[i], 0), 0, True):
|
||||
mask_boundary[i, j] = True
|
||||
mask_valida = mask_valida & mask_boundary
|
||||
|
||||
# Extraer puntos válidos como lista plana
|
||||
pts_validos = np.column_stack([
|
||||
XX[mask_valida].ravel(),
|
||||
YY[mask_valida].ravel(),
|
||||
ZZ[mask_valida].ravel()
|
||||
])
|
||||
|
||||
del XX, YY, ZZ, mask_valida
|
||||
|
||||
# Triangulación completa de una vez (no por parches)
|
||||
mesh = Mesh.Mesh()
|
||||
for indx in range(stepx):
|
||||
inix = indx * stepsize - 1
|
||||
finx = min([stepsize * (indx + 1), len(x)-1])
|
||||
for indy in range(stepy):
|
||||
iniy = indy * stepsize - 1
|
||||
finy = min([stepsize * (indy + 1), len(y) - 1])
|
||||
pts = []
|
||||
for i in range(inix, finx):
|
||||
for j in range(iniy, finy):
|
||||
if datavals[j][i] != nodata_value:
|
||||
if obj.CuttingBoundary:
|
||||
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
|
||||
pts.append([x[i], y[j], datavals[j][i]])
|
||||
else:
|
||||
pts.append([x[i], y[j], datavals[j][i]])
|
||||
if len(pts) > 3:
|
||||
if len(pts_validos) > 3:
|
||||
# Si hay muchos puntos, triangulamos por parches para evitar OOM
|
||||
patch_size = 50000
|
||||
n_patches = max(1, math.ceil(len(pts_validos) / patch_size))
|
||||
for p in range(n_patches):
|
||||
patch = pts_validos[p * patch_size:(p + 1) * patch_size].tolist()
|
||||
if len(patch) > 3:
|
||||
try:
|
||||
triangulated = Triangulation.Triangulate(pts)
|
||||
triangulated = Triangulation.Triangulate(patch)
|
||||
mesh.addMesh(triangulated)
|
||||
except TypeError:
|
||||
print(f"Error al procesar {len(pts)} puntos: {str(e)}")
|
||||
except TypeError as e:
|
||||
print(f"Patch {p}: error al procesar {len(patch)} puntos: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"Patch {p}: error inesperado: {str(e)}")
|
||||
|
||||
print(f'Terraín DEM: {len(pts_validos)} pts válidos, {n_patches} parches, {datetime.now()-t_start}')
|
||||
del pts_validos
|
||||
|
||||
mesh.removeDuplicatedPoints()
|
||||
mesh.removeFoldsOnSurface()
|
||||
obj.InitialMesh = mesh.copy()
|
||||
Mesh.show(mesh)
|
||||
# Limpiar objetos mesh huérfanos previos si existen
|
||||
for o in FreeCAD.ActiveDocument.Objects:
|
||||
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
|
||||
FreeCAD.ActiveDocument.removeObject(o.Name)
|
||||
mesh_obj = Mesh.show(mesh)
|
||||
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
|
||||
elif suffix in ['.xyz']:
|
||||
data = open_xyz_mmap(obj.DEM)
|
||||
pts_array = open_xyz_mmap(obj.DEM)
|
||||
if pts_array is not None and len(pts_array) > 3:
|
||||
import MeshTools.Triangulation as Triangulation
|
||||
import Mesh
|
||||
if obj.CuttingBoundary:
|
||||
mask = []
|
||||
for pt in pts_array:
|
||||
mask.append(obj.CuttingBoundary.Shape.isInside(
|
||||
FreeCAD.Vector(pt[0], pt[1], 0), 0, True))
|
||||
pts_array = pts_array[mask]
|
||||
if len(pts_array) > 3:
|
||||
from datetime import datetime
|
||||
t0 = datetime.now()
|
||||
pts_list = pts_array.tolist()
|
||||
mesh = Triangulation.Triangulate(pts_list)
|
||||
mesh.removeDuplicatedPoints()
|
||||
mesh.removeFoldsOnSurface()
|
||||
obj.InitialMesh = mesh.copy()
|
||||
# Limpiar objetos mesh huérfanos previos
|
||||
for o in FreeCAD.ActiveDocument.Objects:
|
||||
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
|
||||
FreeCAD.ActiveDocument.removeObject(o.Name)
|
||||
mesh_obj = Mesh.show(mesh)
|
||||
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
|
||||
print(f'XYZ import: {len(pts_array)} puntos en {datetime.now()-t0}')
|
||||
|
||||
|
||||
|
||||
@@ -329,6 +401,11 @@ class Terrain(ArchComponent.Component):
|
||||
if obj.DEM:
|
||||
obj.DEM = None
|
||||
obj.mesh = mesh
|
||||
# Forzar actualización visual llamando a publishProperty
|
||||
try:
|
||||
obj.publishProperty("Mesh")
|
||||
except:
|
||||
pass
|
||||
|
||||
def execute(self, obj):
|
||||
''''''
|
||||
@@ -547,47 +624,47 @@ class ViewProviderTerrain:
|
||||
offset.factor = -2.0
|
||||
|
||||
# Boundary features.
|
||||
'''self.boundary_color = coin.SoBaseColor()
|
||||
self.boundary_color = coin.SoBaseColor()
|
||||
self.boundary_coords = coin.SoGeoCoordinate()
|
||||
self.boundary_lines = coin.SoLineSet()
|
||||
self.boundary_style = coin.SoDrawStyle()
|
||||
self.boundary_style.style = coin.SoDrawStyle.LINES'''
|
||||
self.boundary_style.style = coin.SoDrawStyle.LINES
|
||||
|
||||
# Boundary root.
|
||||
'''boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
|
||||
boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
|
||||
boundaries.style = 'EMISSIVE_DIFFUSE'
|
||||
boundaries.addChild(self.boundary_color)
|
||||
boundaries.addChild(self.boundary_style)
|
||||
boundaries.addChild(self.boundary_coords)
|
||||
boundaries.addChild(self.boundary_lines)'''
|
||||
boundaries.addChild(self.boundary_lines)
|
||||
|
||||
# Major Contour features.
|
||||
'''self.major_color = coin.SoBaseColor()
|
||||
self.major_color = coin.SoBaseColor()
|
||||
self.major_coords = coin.SoGeoCoordinate()
|
||||
self.major_lines = coin.SoLineSet()
|
||||
self.major_style = coin.SoDrawStyle()
|
||||
self.major_style.style = coin.SoDrawStyle.LINES'''
|
||||
self.major_style.style = coin.SoDrawStyle.LINES
|
||||
|
||||
# Major Contour root.
|
||||
'''major_contours = coin.SoSeparator()
|
||||
major_contours = coin.SoSeparator()
|
||||
major_contours.addChild(self.major_color)
|
||||
major_contours.addChild(self.major_style)
|
||||
major_contours.addChild(self.major_coords)
|
||||
major_contours.addChild(self.major_lines)'''
|
||||
major_contours.addChild(self.major_lines)
|
||||
|
||||
# Minor Contour features.
|
||||
'''self.minor_color = coin.SoBaseColor()
|
||||
self.minor_color = coin.SoBaseColor()
|
||||
self.minor_coords = coin.SoGeoCoordinate()
|
||||
self.minor_lines = coin.SoLineSet()
|
||||
self.minor_style = coin.SoDrawStyle()
|
||||
self.minor_style.style = coin.SoDrawStyle.LINES'''
|
||||
self.minor_style.style = coin.SoDrawStyle.LINES
|
||||
|
||||
# Minor Contour root.
|
||||
'''minor_contours = coin.SoSeparator()
|
||||
minor_contours = coin.SoSeparator()
|
||||
minor_contours.addChild(self.minor_color)
|
||||
minor_contours.addChild(self.minor_style)
|
||||
minor_contours.addChild(self.minor_coords)
|
||||
minor_contours.addChild(self.minor_lines)'''
|
||||
minor_contours.addChild(self.minor_lines)
|
||||
|
||||
# Highlight for selection.
|
||||
highlight = coin.SoType.fromName('SoFCSelection').createInstance()
|
||||
@@ -596,7 +673,7 @@ class ViewProviderTerrain:
|
||||
highlight.addChild(mat_binding)
|
||||
highlight.addChild(self.geo_coords)
|
||||
highlight.addChild(self.triangles)
|
||||
#highlight.addChild(boundaries)
|
||||
highlight.addChild(boundaries)
|
||||
|
||||
# Face root.
|
||||
face = coin.SoSeparator()
|
||||
@@ -609,19 +686,19 @@ class ViewProviderTerrain:
|
||||
edge.addChild(self.edge_style)
|
||||
edge.addChild(highlight)
|
||||
|
||||
# Surface root.
|
||||
# Surface root - con contour lines visibles.
|
||||
surface_root = coin.SoSeparator()
|
||||
surface_root.addChild(face)
|
||||
surface_root.addChild(offset)
|
||||
surface_root.addChild(edge)
|
||||
#surface_root.addChild(major_contours)
|
||||
#surface_root.addChild(minor_contours)
|
||||
surface_root.addChild(major_contours)
|
||||
surface_root.addChild(minor_contours)
|
||||
vobj.addDisplayMode(surface_root, "Surface")
|
||||
|
||||
# Boundary root.
|
||||
#boundary_root = coin.SoSeparator()
|
||||
#boundary_root.addChild(boundaries)
|
||||
#vobj.addDisplayMode(boundary_root, "Boundary")
|
||||
boundary_root = coin.SoSeparator()
|
||||
boundary_root.addChild(boundaries)
|
||||
vobj.addDisplayMode(boundary_root, "Boundary")
|
||||
|
||||
# Elevation/Shaded root.
|
||||
'''shaded_root = coin.SoSeparator()
|
||||
@@ -648,52 +725,50 @@ class ViewProviderTerrain:
|
||||
self.onChanged(vobj, "ShapeColor")
|
||||
self.onChanged(vobj, "LineColor")
|
||||
self.onChanged(vobj, "LineWidth")
|
||||
#self.onChanged(vobj, "BoundaryColor")
|
||||
#self.onChanged(vobj, "BoundaryWidth")
|
||||
#self.onChanged(vobj, "BoundaryPattern")
|
||||
#self.onChanged(vobj, "PatternScale")
|
||||
#self.onChanged(vobj, "MajorColor")
|
||||
#self.onChanged(vobj, "MajorWidth")
|
||||
#self.onChanged(vobj, "MinorColor")
|
||||
#self.onChanged(vobj, "MinorWidth")
|
||||
self.onChanged(vobj, "BoundaryColor")
|
||||
self.onChanged(vobj, "BoundaryWidth")
|
||||
self.onChanged(vobj, "BoundaryPattern")
|
||||
self.onChanged(vobj, "PatternScale")
|
||||
self.onChanged(vobj, "MajorColor")
|
||||
self.onChanged(vobj, "MajorWidth")
|
||||
self.onChanged(vobj, "MinorColor")
|
||||
self.onChanged(vobj, "MinorWidth")
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
''' Update Object visuals when a data property changed. '''
|
||||
|
||||
# Set geosystem.
|
||||
geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
|
||||
try:
|
||||
utm_zone = FreeCAD.ActiveDocument.Site.UtmZone
|
||||
except:
|
||||
utm_zone = "30"
|
||||
geo_system = ["UTM", utm_zone, "FLAT"]
|
||||
self.geo_coords.geoSystem.setValues(geo_system)
|
||||
'''
|
||||
self.boundary_coords.geoSystem.setValues(geo_system)
|
||||
self.major_coords.geoSystem.setValues(geo_system)
|
||||
self.minor_coords.geoSystem.setValues(geo_system)
|
||||
'''
|
||||
|
||||
if prop == "Mesh":
|
||||
if prop == "mesh" or prop == "Mesh":
|
||||
if obj.mesh:
|
||||
print("Mostrar mesh")
|
||||
|
||||
mesh = obj.mesh
|
||||
vertices = [tuple(v) for v in mesh.Topology[0]]
|
||||
faces = []
|
||||
for face in mesh.Topology[1]:
|
||||
faces.extend(face)
|
||||
faces.append(-1)
|
||||
try:
|
||||
vertices = [tuple(v) for v in mesh.Topology[0]]
|
||||
faces = []
|
||||
for face in mesh.Topology[1]:
|
||||
faces.extend(face)
|
||||
faces.append(-1)
|
||||
|
||||
# Asignar a los nodos de visualización
|
||||
self.geo_coords.point.values = vertices # <-- ¡Clave!
|
||||
self.triangles.coordIndex.values = faces # <-- ¡Clave!
|
||||
# Asignar a los nodos de visualización
|
||||
self.geo_coords.point.values = vertices
|
||||
self.triangles.coordIndex.values = faces
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(f"Error actualizando mesh visual: {e}\n")
|
||||
|
||||
def getDisplayModes(self, vobj):
|
||||
''' Return a list of display modes. '''
|
||||
modes = ["Surface", "Boundary"]
|
||||
|
||||
return modes
|
||||
return ["Surface", "Boundary", "Flat Lines", "Wireframe"]
|
||||
|
||||
def getDefaultDisplayMode(self):
|
||||
'''
|
||||
Return the name of the default display mode.
|
||||
'''
|
||||
return "Surface"
|
||||
|
||||
def claimChildren(self):
|
||||
@@ -736,3 +811,4 @@ class ViewProviderTerrain:
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
FreeCADGui.addCommand('Terrain', _CommandTerrain())'''
|
||||
|
||||
|
||||
+10
-30
@@ -450,35 +450,18 @@ class ContourTaskPanel():
|
||||
starttime = datetime.now()
|
||||
|
||||
if self.land is None:
|
||||
print("No hay objetos para procesar")
|
||||
FreeCAD.Console.PrintWarning("No hay objetos para procesar\n")
|
||||
return False
|
||||
else:
|
||||
minor = FreeCAD.Units.Quantity(self.inputMinorContourMargin.currentText()).Value
|
||||
mayor = FreeCAD.Units.Quantity(self.inputMayorContourMargin.currentText()).Value
|
||||
|
||||
i = 2
|
||||
if i == 0:
|
||||
makeContours(self.land, minor, mayor, self.MinorColor, self.MayorColor,
|
||||
self.inputMinorContourThickness.value(), self.inputMayorContourThickness.value())
|
||||
elif i == 1:
|
||||
import multiprocessing
|
||||
p = multiprocessing.Process(target=makeContours,
|
||||
args=(self.land, minor, mayor,
|
||||
self.MinorColor, self.MayorColor,
|
||||
self.inputMinorContourThickness.value(),
|
||||
self.inputMayorContourThickness.value(), ))
|
||||
p.start()
|
||||
p.join()
|
||||
|
||||
else:
|
||||
import threading
|
||||
hilo = threading.Thread(target = makeContours,
|
||||
args = (self.land, minor, mayor,
|
||||
self.MinorColor, self.MayorColor,
|
||||
self.inputMinorContourThickness.value(),
|
||||
self.inputMayorContourThickness.value()))
|
||||
hilo.daemon = True
|
||||
hilo.start()
|
||||
makeContours(
|
||||
self.land, minor, mayor,
|
||||
self.MinorColor, self.MayorColor,
|
||||
self.inputMinorContourThickness.value(),
|
||||
self.inputMayorContourThickness.value()
|
||||
)
|
||||
|
||||
total_time = datetime.now() - starttime
|
||||
print(" -- Tiempo tardado:", total_time)
|
||||
@@ -569,7 +552,7 @@ class SlopeTaskPanel(_generalTaskPanel):
|
||||
land.ViewObject.DiffuseColor = colorlist
|
||||
|
||||
# TODO: check this code:
|
||||
elif obj.isDerivedFrom("Mesh::Feature"):
|
||||
elif hasattr(land, 'Mesh') and land.isDerivedFrom("Mesh::Feature"):
|
||||
fMesh = Mest2FemMesh(land)
|
||||
import math
|
||||
setColors = []
|
||||
@@ -602,10 +585,7 @@ class SlopeTaskPanel(_generalTaskPanel):
|
||||
print("Everything OK (", datetime.now() - starttime, ")")
|
||||
|
||||
def accept(self):
|
||||
# self.getPointSlope()
|
||||
import threading
|
||||
hilo = threading.Thread(target=self.getPointSlope(self.ranges))
|
||||
hilo.start()
|
||||
self.getPointSlope(self.ranges)
|
||||
return True
|
||||
|
||||
# Orientation Analisys: ---------------------------------------------------------------------------------
|
||||
@@ -809,4 +789,4 @@ if FreeCAD.GuiUp:
|
||||
FreeCADGui.addCommand('SlopeAnalisys', _CommandSlopeAnalisys())
|
||||
FreeCADGui.addCommand('HeightAnalisys', _CommandHeightAnalisys())
|
||||
FreeCADGui.addCommand('OrientationAnalisys', _CommandOrientationAnalisys())
|
||||
FreeCADGui.addCommand('TerrainAnalisys', CommandTerrainAnalisysGroup())'''
|
||||
FreeCADGui.addCommand('TerrainAnalisys', CommandTerrainAnalisysGroup())'''
|
||||
|
||||
+6
-1
@@ -652,6 +652,9 @@ if FreeCAD.GuiUp:
|
||||
from Civil.Fence import PVPlantFence
|
||||
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
|
||||
|
||||
import docgenerator
|
||||
FreeCADGui.addCommand('GenerateDocuments', docgenerator.generateDocuments())
|
||||
|
||||
projectlist = [ # "Reload",
|
||||
"PVPlantSite",
|
||||
"ProjectSetup",
|
||||
@@ -687,4 +690,6 @@ pv_mechanical = [
|
||||
]
|
||||
|
||||
objectlist = ['PVPlantTree',
|
||||
'PVPlantFenceGroup',]
|
||||
'PVPlantFenceGroup',
|
||||
'GenerateDocuments',
|
||||
]
|
||||
@@ -4,14 +4,17 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
|
||||
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw.css">
|
||||
<script src="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw-src.js"></script>
|
||||
<!-- 1. Core Leaflet library -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/togeojson@0.16.0"></script>
|
||||
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
|
||||
<!-- 2. Leaflet.draw Plugin (MUST be loaded AFTER Leaflet) -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
|
||||
<!-- 3. Other plugins -->
|
||||
<script src="https://unpkg.com/togeojson@0.16.0"></script>
|
||||
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
|
||||
|
||||
<!--script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script-->
|
||||
<script type="text/javascript" src="./qwebchannel.js"></script>
|
||||
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
# Script para FreeCAD - Procesador de Documentos Word con Carátula
|
||||
import os
|
||||
import glob
|
||||
from PySide import QtWidgets, QtCore
|
||||
from PySide.QtWidgets import (QFileDialog, QMessageBox, QProgressDialog,
|
||||
QApplication, QVBoxLayout, QWidget, QPushButton,
|
||||
QLabel, QTextEdit)
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
import PVPlantResources
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
DOCX_AVAILABLE = True
|
||||
except ImportError:
|
||||
DOCX_AVAILABLE = False
|
||||
FreeCAD.Console.PrintError("Error: python-docx no está instalado. Instala con: pip install python-docx\n")
|
||||
|
||||
|
||||
class DocumentProcessor(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(DocumentProcessor, self).__init__(parent)
|
||||
self.caratula_path = ""
|
||||
self.carpeta_path = ""
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Procesador de Documentos Word")
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(500)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Título
|
||||
title = QLabel("<h2>Procesador de Documentos Word</h2>")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Información
|
||||
info_text = QLabel(
|
||||
"Este script buscará recursivamente todos los archivos .docx en una carpeta,\n"
|
||||
"insertará una carátula y aplicará formato estándar a todos los documentos."
|
||||
)
|
||||
info_text.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(info_text)
|
||||
|
||||
# Botón seleccionar carátula
|
||||
self.btn_caratula = QPushButton("1. Seleccionar Carátula")
|
||||
self.btn_caratula.clicked.connect(self.seleccionar_caratula)
|
||||
layout.addWidget(self.btn_caratula)
|
||||
|
||||
self.label_caratula = QLabel("No se ha seleccionado carátula")
|
||||
self.label_caratula.setWordWrap(True)
|
||||
layout.addWidget(self.label_caratula)
|
||||
|
||||
# Botón seleccionar carpeta
|
||||
self.btn_carpeta = QPushButton("2. Seleccionar Carpeta de Documentos")
|
||||
self.btn_carpeta.clicked.connect(self.seleccionar_carpeta)
|
||||
layout.addWidget(self.btn_carpeta)
|
||||
|
||||
self.label_carpeta = QLabel("No se ha seleccionado carpeta")
|
||||
self.label_carpeta.setWordWrap(True)
|
||||
layout.addWidget(self.label_carpeta)
|
||||
|
||||
# Botón procesar
|
||||
self.btn_procesar = QPushButton("3. Procesar Documentos")
|
||||
self.btn_procesar.clicked.connect(self.procesar_documentos)
|
||||
self.btn_procesar.setEnabled(False)
|
||||
layout.addWidget(self.btn_procesar)
|
||||
|
||||
# Área de log
|
||||
self.log_area = QTextEdit()
|
||||
self.log_area.setReadOnly(True)
|
||||
layout.addWidget(self.log_area)
|
||||
|
||||
# Botón cerrar
|
||||
self.btn_cerrar = QPushButton("Cerrar")
|
||||
self.btn_cerrar.clicked.connect(self.close)
|
||||
layout.addWidget(self.btn_cerrar)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def log(self, mensaje):
|
||||
"""Agrega un mensaje al área de log"""
|
||||
self.log_area.append(mensaje)
|
||||
QApplication.processEvents() # Para actualizar la UI
|
||||
|
||||
def seleccionar_caratula(self):
|
||||
"""Abre un diálogo para seleccionar el archivo de carátula"""
|
||||
archivo, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Seleccionar archivo de carátula",
|
||||
"",
|
||||
"Word documents (*.docx);;All files (*.*)"
|
||||
)
|
||||
|
||||
if archivo and os.path.exists(archivo):
|
||||
self.caratula_path = archivo
|
||||
self.label_caratula.setText(f"Carátula: {os.path.basename(archivo)}")
|
||||
self.verificar_estado()
|
||||
self.log(f"✓ Carátula seleccionada: {archivo}")
|
||||
|
||||
def seleccionar_carpeta(self):
|
||||
"""Abre un diálogo para seleccionar la carpeta de documentos"""
|
||||
carpeta = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Seleccionar carpeta con documentos"
|
||||
)
|
||||
|
||||
if carpeta:
|
||||
self.carpeta_path = carpeta
|
||||
self.label_carpeta.setText(f"Carpeta: {carpeta}")
|
||||
self.verificar_estado()
|
||||
self.log(f"✓ Carpeta seleccionada: {carpeta}")
|
||||
|
||||
def verificar_estado(self):
|
||||
"""Habilita el botón procesar si ambos paths están seleccionados"""
|
||||
if self.caratula_path and self.carpeta_path:
|
||||
self.btn_procesar.setEnabled(True)
|
||||
|
||||
def buscar_docx_recursivamente(self, carpeta):
|
||||
"""Busca recursivamente todos los archivos .docx en una carpeta"""
|
||||
archivos_docx = []
|
||||
patron = os.path.join(carpeta, "**", "*.docx")
|
||||
|
||||
for archivo in glob.glob(patron, recursive=True):
|
||||
archivos_docx.append(archivo)
|
||||
|
||||
return archivos_docx
|
||||
|
||||
def aplicar_formato_estandar(self, doc):
|
||||
"""Aplica formato estándar al documento"""
|
||||
try:
|
||||
# Configurar estilos por defecto
|
||||
style = doc.styles['Normal']
|
||||
font = style.font
|
||||
font.name = 'Arial'
|
||||
font.size = Pt(11)
|
||||
font.color.rgb = RGBColor(0, 0, 0) # Negro
|
||||
|
||||
# Configurar encabezados
|
||||
try:
|
||||
heading_style = doc.styles['Heading 1']
|
||||
heading_font = heading_style.font
|
||||
heading_font.name = 'Arial'
|
||||
heading_font.size = Pt(14)
|
||||
heading_font.bold = True
|
||||
heading_font.color.rgb = RGBColor(0, 51, 102) # Azul oscuro
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ⚠ Advertencia en formato: {str(e)}")
|
||||
|
||||
def aplicar_formato_avanzado(self, doc):
|
||||
"""Aplica formato más avanzado y personalizado"""
|
||||
try:
|
||||
# Configurar márgenes
|
||||
sections = doc.sections
|
||||
for section in sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
|
||||
# Configurar estilos de párrafo
|
||||
for paragraph in doc.paragraphs:
|
||||
paragraph.paragraph_format.space_after = Pt(6)
|
||||
paragraph.paragraph_format.space_before = Pt(0)
|
||||
paragraph.paragraph_format.line_spacing = 1.15
|
||||
|
||||
# Alinear párrafos justificados
|
||||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
|
||||
# Aplicar fuente específica a cada run
|
||||
for run in paragraph.runs:
|
||||
run.font.name = 'Arial'
|
||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Arial')
|
||||
run.font.size = Pt(11)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ⚠ Advertencia en formato avanzado: {str(e)}")
|
||||
|
||||
def insertar_caratula_y_formatear(self, archivo_docx, archivo_caratula):
|
||||
"""Inserta la carátula y aplica formato al documento"""
|
||||
try:
|
||||
# Abrir el documento de carátula
|
||||
doc_caratula = Document(archivo_caratula)
|
||||
|
||||
# Abrir el documento destino
|
||||
doc_destino = Document(archivo_docx)
|
||||
|
||||
# Crear un nuevo documento que contendrá la carátula + contenido original
|
||||
nuevo_doc = Document()
|
||||
|
||||
# Copiar todo el contenido de la carátula
|
||||
for elemento in doc_caratula.element.body:
|
||||
print(elemento)
|
||||
nuevo_doc.element.body.append(elemento)
|
||||
|
||||
# Agregar un salto de página después de la carátula
|
||||
nuevo_doc.add_page_break()
|
||||
|
||||
# Copiar todo el contenido del documento original
|
||||
for elemento in doc_destino.element.body:
|
||||
nuevo_doc.element.body.append(elemento)
|
||||
|
||||
# Aplicar formatos
|
||||
self.aplicar_formato_estandar(nuevo_doc)
|
||||
self.aplicar_formato_avanzado(nuevo_doc)
|
||||
|
||||
# Guardar el documento (sobrescribir el original)
|
||||
nombre_base = os.path.splitext(os.path.basename(archivo_docx))[0]
|
||||
extension = os.path.splitext(archivo_docx)[1]
|
||||
name = f"{nombre_base}{extension}"
|
||||
nuevo_docx = os.path.join(self.output_carpeta, name)
|
||||
nuevo_doc.save(nuevo_docx)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def procesar_documentos(self):
|
||||
"""Función principal que orquesta todo el proceso"""
|
||||
if not DOCX_AVAILABLE:
|
||||
QMessageBox.critical(self, "Error",
|
||||
"La biblioteca python-docx no está disponible.\n\n"
|
||||
"Instala con: pip install python-docx")
|
||||
return
|
||||
|
||||
# Verificar paths
|
||||
if not os.path.exists(self.caratula_path):
|
||||
QMessageBox.warning(self, "Error", "El archivo de carátula no existe.")
|
||||
return
|
||||
|
||||
if not os.path.exists(self.carpeta_path):
|
||||
QMessageBox.warning(self, "Error", "La carpeta de documentos no existe.")
|
||||
return
|
||||
|
||||
self.log("\n=== INICIANDO PROCESAMIENTO ===")
|
||||
self.log(f"Carátula: {self.caratula_path}")
|
||||
self.log(f"Carpeta: {self.carpeta_path}")
|
||||
|
||||
directorio_padre = os.path.dirname(self.carpeta_path)
|
||||
self.output_carpeta = os.path.join(directorio_padre, "03.Outputs")
|
||||
os.makedirs(self.output_carpeta, exist_ok=True)
|
||||
|
||||
# Buscar archivos .docx
|
||||
self.log("Buscando archivos .docx...")
|
||||
archivos_docx = self.buscar_docx_recursivamente(self.carpeta_path)
|
||||
|
||||
if not archivos_docx:
|
||||
self.log("No se encontraron archivos .docx en la carpeta seleccionada.")
|
||||
QMessageBox.information(self, "Información",
|
||||
"No se encontraron archivos .docx en la carpeta seleccionada.")
|
||||
return
|
||||
|
||||
self.log(f"Se encontraron {len(archivos_docx)} archivos .docx")
|
||||
|
||||
# Crear diálogo de progreso
|
||||
progress = QProgressDialog("Procesando documentos...", "Cancelar", 0, len(archivos_docx), self)
|
||||
progress.setWindowTitle("Procesando")
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.show()
|
||||
|
||||
# Procesar cada archivo
|
||||
exitosos = 0
|
||||
fallidos = 0
|
||||
errores_detallados = []
|
||||
|
||||
for i, archivo_docx in enumerate(archivos_docx):
|
||||
if progress.wasCanceled():
|
||||
self.log("Proceso cancelado por el usuario.")
|
||||
break
|
||||
|
||||
progress.setValue(i)
|
||||
progress.setLabelText(f"Procesando {i + 1}/{len(archivos_docx)}: {os.path.basename(archivo_docx)}")
|
||||
QApplication.processEvents()
|
||||
|
||||
self.log(f"Procesando: {os.path.basename(archivo_docx)}")
|
||||
|
||||
success, error_msg = self.insertar_caratula_y_formatear(archivo_docx, self.caratula_path)
|
||||
|
||||
if success:
|
||||
self.log(f" ✓ Completado")
|
||||
exitosos += 1
|
||||
else:
|
||||
self.log(f" ✗ Error: {error_msg}")
|
||||
fallidos += 1
|
||||
errores_detallados.append(f"{os.path.basename(archivo_docx)}: {error_msg}")
|
||||
|
||||
progress.setValue(len(archivos_docx))
|
||||
|
||||
# Mostrar resumen
|
||||
self.log("\n=== RESUMEN ===")
|
||||
self.log(f"Documentos procesados exitosamente: {exitosos}")
|
||||
self.log(f"Documentos con errores: {fallidos}")
|
||||
self.log(f"Total procesados: {exitosos + fallidos}")
|
||||
|
||||
# Mostrar mensaje final
|
||||
mensaje = (f"Procesamiento completado:\n"
|
||||
f"✓ Exitosos: {exitosos}\n"
|
||||
f"✗ Fallidos: {fallidos}\n"
|
||||
f"Total: {len(archivos_docx)}")
|
||||
|
||||
if fallidos > 0:
|
||||
mensaje += f"\n\nErrores encontrados:\n" + "\n".join(
|
||||
errores_detallados[:5]) # Mostrar solo primeros 5 errores
|
||||
if len(errores_detallados) > 5:
|
||||
mensaje += f"\n... y {len(errores_detallados) - 5} más"
|
||||
|
||||
QMessageBox.information(self, "Proceso Completado", mensaje)
|
||||
|
||||
|
||||
# Función para ejecutar desde FreeCAD
|
||||
def run_document_processor():
|
||||
"""Función principal para ejecutar el procesador desde FreeCAD"""
|
||||
# Verificar si python-docx está disponible
|
||||
if not DOCX_AVAILABLE:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setText("Biblioteca python-docx no encontrada")
|
||||
msg.setInformativeText(
|
||||
"Para usar este script necesitas instalar python-docx:\n\n"
|
||||
"1. Abre la consola de FreeCAD\n"
|
||||
"2. Ejecuta: import subprocess, sys\n"
|
||||
"3. Ejecuta: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-docx'])\n\n"
|
||||
"O instala desde una terminal externa con: pip install python-docx"
|
||||
)
|
||||
msg.setWindowTitle("Dependencia faltante")
|
||||
msg.exec_()
|
||||
return
|
||||
|
||||
# Crear y mostrar la interfaz
|
||||
dialog = DocumentProcessor(FreeCADGui.getMainWindow())
|
||||
dialog.exec_()
|
||||
|
||||
class generateDocuments:
|
||||
def GetResources(self):
|
||||
return {'Pixmap': str(os.path.join(DirIcons, "house.svg")),
|
||||
'MenuText': "DocumentGenerator",
|
||||
'Accel': "D, G",
|
||||
'ToolTip': "Creates a Building object from setup dialog."}
|
||||
|
||||
def IsActive(self):
|
||||
return not FreeCAD.ActiveDocument is None
|
||||
|
||||
|
||||
def Activated(self):
|
||||
run_document_processor()
|
||||
@@ -159,7 +159,7 @@ def calculate_incenter(facet):
|
||||
"""Calcula el incentro usando la función nativa de FreeCAD"""
|
||||
try:
|
||||
return facet.InCircle[0] # (x, y, z)
|
||||
except:
|
||||
except (IndexError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+20
-6
@@ -2,13 +2,27 @@
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>PVPlant</name>
|
||||
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
|
||||
<version>2025.11.20</version>
|
||||
<date>2025.11.20</date>
|
||||
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
|
||||
<version>2026.02.12</version>
|
||||
<date>2026.02.15</date>
|
||||
|
||||
<maintainer email="javier.branagutierrez@gmail.com">
|
||||
Javier Braña
|
||||
</maintainer>
|
||||
|
||||
<license file="LICENSE">LGPL-2.1-or-later</license>
|
||||
<url type="repository" branch="developed">https://homehud.duckdns.org/javier/PVPlant</url>
|
||||
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
|
||||
<url type="readme">https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md</url>
|
||||
|
||||
<url type="repository" branch="main">
|
||||
https://homehud.duckdns.org/javier/PVPlant
|
||||
</url>
|
||||
|
||||
<url type="bugtracker">
|
||||
https://homehud.duckdns.org/javier/PVPlant/issues
|
||||
</url>
|
||||
|
||||
<url type="readme">
|
||||
https://homehud.duckdns.org/javier/PVPlant/raw/branch/main/README.md
|
||||
</url>
|
||||
|
||||
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>
|
||||
|
||||
<content>
|
||||
|
||||
@@ -55,6 +55,10 @@ class _CommandReload:
|
||||
import hydro.hydrological as hydro
|
||||
import Importer.importOSM as iOSM
|
||||
|
||||
import docgenerator
|
||||
|
||||
importlib.reload(docgenerator)
|
||||
|
||||
importlib.reload(ProjectSetup)
|
||||
importlib.reload(PVPlantPlacement)
|
||||
importlib.reload(PVPlantImportGrid)
|
||||
|
||||
+2
-3
@@ -2,8 +2,6 @@ numpy~=1.26.2
|
||||
opencv-python~=4.8.1
|
||||
matplotlib~=3.8.2
|
||||
openpyxl~=3.1.2
|
||||
utm~=0.7.0
|
||||
PySide2~=5.15.8
|
||||
requests~=2.31.0
|
||||
setuptools~=68.2.2
|
||||
laspy~=2.5.3
|
||||
@@ -17,4 +15,5 @@ certifi~=2023.11.17
|
||||
SciPy~=1.11.4
|
||||
pycollada~=0.7.2
|
||||
shapely
|
||||
rtree
|
||||
rtree
|
||||
pandas
|
||||
|
||||
Reference in New Issue
Block a user