diff --git a/Export/exportDXF.py b/Export/exportDXF.py
index 7a8f975..1dbad34 100644
--- a/Export/exportDXF.py
+++ b/Export/exportDXF.py
@@ -27,256 +27,6 @@ from PVPlantResources import DirIcons as DirIcons
field = {"name": "", "width": 0, "heigth": 0}
-
-class LineTypeDelegate(QtWidgets.QStyledItemDelegate):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.line_types = [
- {"name": "Continuous", "pen": QtCore.Qt.SolidLine},
- {"name": "Dashed", "pen": QtCore.Qt.DashLine},
- {"name": "Dotted", "pen": QtCore.Qt.DotLine},
- {"name": "Dash-Dot", "pen": QtCore.Qt.DashDotLine},
- {"name": "Dash-Dot-Dot", "pen": QtCore.Qt.DashDotDotLine}
- ]
-
- def paint(self, painter, option, index):
- # Dibujar el fondo
- painter.save()
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- # Configuraciones comunes
- line_color = QtGui.QColor(0, 0, 0) # Color de la línea
- line_width = 2 # Grosor de línea
-
- # Dibujar el ítem base
- super().paint(painter, option, index)
-
- # Obtener el tipo de línea
- line_type = self.line_types[index.row()]
-
- # Configurar el área de dibujo
- text_rect = option.rect.adjusted(5, 0, -100, 0)
- line_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
-
- # Dibujar el texto
- painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type["name"])
-
- # Dibujar la línea de muestra
- pen = QtGui.QPen(line_color, line_width, line_type["pen"])
- painter.setPen(pen)
-
- start_y = line_rect.center().y()
- painter.drawLine(line_rect.left(), start_y, line_rect.right(), start_y)
-
- painter.restore()
-
- def sizeHint(self, option, index):
- size = super().sizeHint(option, index)
- size.setHeight(25) # Altura aumentada para mejor visualización
- return size
-
-
-class LineTypePreviewDelegate(QtWidgets.QStyledItemDelegate):
- def __init__(self, parent=None):
- super().__init__(parent)
- '''self.line_types = [
- ("Continuous", QtCore.Qt.SolidLine),
- ("Dashed", QtCore.Qt.DashLine),
- ("Dotted", QtCore.Qt.DotLine),
- ("Dash-Dot", QtCore.Qt.DashDotLine),
- ("Dash-Dot-Dot", QtCore.Qt.DashDotDotLine)
- ]'''
-
- self.line_types = [
- {"name": "Continuous", "style": QtCore.Qt.SolidLine},
- {"name": "Dashed", "style": QtCore.Qt.DashLine},
- {"name": "Dotted", "style": QtCore.Qt.DotLine},
- {"name": "Custom 1 (--0--0--)",
- "style": QtCore.Qt.CustomDashLine,
- "pattern": [8, 4, 2, 4]}, # Patrón personalizado
- {"name": "Custom 2 (- - -)",
- "style": QtCore.Qt.CustomDashLine,
- "pattern": [6, 3]} # Otro patrón
- ]
-
- def paint(self, painter, option, index):
- painter.save()
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- if index.row() >= len(self.line_types):
- painter.restore()
- return
-
- line_data = self.line_types[index.row()]
- line_name = line_data["name"]
- line_style = line_data.get("style", QtCore.Qt.SolidLine)
- dash_pattern = line_data.get("pattern", [])
-
- # Fondo para selección
- if option.state & QtGui.QStyle.State_Selected:
- painter.fillRect(option.rect, option.palette.highlight())
-
- # Áreas de dibujo
- text_rect = option.rect.adjusted(5, 0, -100, 0)
- preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
-
- # Texto
- painter.setPen(option.palette.color(QtGui.QPalette.Text))
- painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_name)
-
- # Línea personalizada
- pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
- pen.setStyle(line_style)
-
- if line_style == QtCore.Qt.CustomDashLine and dash_pattern:
- pen.setDashPattern(dash_pattern)
-
- painter.setPen(pen)
- painter.drawLine(preview_rect.left(), preview_rect.center().y(),
- preview_rect.right(), preview_rect.center().y())
-
- painter.restore()
-
- def sizeHint(self, option, index):
- return QtCore.QSize(200, 25)
-
-
-class LineTypeComboBox(QtWidgets.QComboBox):
- def __init__(self, parent=None):
- super().__init__(parent)
- self._delegate = LineTypePreviewDelegate(self)
- self.setItemDelegate(self._delegate)
- self.addItems(["Continuous", "Dashed", "Dotted", "Dash-Dot", "Dash-Dot-Dot"])
-
- def paintEvent(self, event):
- painter = QtGui.QPainter(self)
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- # Dibujar el fondo del combobox
- option = QtWidgets.QStyleOptionComboBox()
- self.initStyleOption(option)
- self.style().drawComplexControl(QtGui.QStyle.CC_ComboBox, option, painter, self)
-
- # Obtener el texto y estilo actual
- current_text = self.currentText()
- line_style = next(
- (style for name, style in self._delegate.line_types if name == current_text),
- QtCore.Qt.SolidLine
- )
-
- # Área de dibujo
- text_rect = self.rect().adjusted(5, 0, -30, 0)
- preview_rect = self.rect().adjusted(self.width() - 70, 2, -5, -2)
-
- # Dibujar texto
- painter.setPen(self.palette().color(QtGui.QPalette.Text))
- painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, current_text)
-
- # Dibujar línea de previsualización
- pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
- pen.setStyle(line_style)
- painter.setPen(pen)
-
- center_y = preview_rect.center().y()
- painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y)
-
-
-class exportDXF:
-
- def __init__(self, filename):
- ''' '''
-
- self.doc = None
- self.msp = None
- self.filename = filename
-
- '''
- doc.linetypes.add("GRENZE2",
- # linetype definition in acad.lin:
- # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1
- # replacing BOX by shape index 132 (got index from an AutoCAD file),
- # ezdxf can't get shape index from ltypeshp.shx
- pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
- description="Grenze eckig ----[]-----[]----[]-----[]----[]--",
- length=1.45, # required for complex line types
- })
-
- doc.linetypes.add("GRENZE2",
- # linetype definition in acad.lin:
- # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1
- # replacing BOX by shape index 132 (got index from an AutoCAD file),
- # ezdxf can't get shape index from ltypeshp.shx
- pattern = "A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
- description = "Límite1 ----0-----0----0-----0----0-----0--",
- length = 1.45, # required for complex line types
- })
- '''
-
- def createFile(self, version='R2018'):
- import ezdxf
- from ezdxf.tools.standards import linetypes
-
- # 1. Create a new document
- self.doc = ezdxf.new(version)
- # 2. Setup document:
- self.doc.header["$INSUNITS"] = 6
- self.doc.header['$MEASUREMENT'] = 1
- self.doc.header['$LUNITS'] = 2
- self.doc.header['$AUNITS'] = 0
-
- # 3. Add new entities to the modelspace:
- self.msp = self.doc.modelspace()
-
- print("linetypes: ", self.doc.linetypes)
- for name, desc, pattern in linetypes():
- if name not in self.doc.linetypes:
- self.doc.linetypes.add(name=name,
- pattern=pattern,
- description=desc,
- )
-
-
- self.doc.linetypes.add(name="FENCELINE1",
- pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
- description="Límite1 ----0-----0----0-----0----0-----0--",
- length=1.45) # required for complex line types
-
- def save(self):
- from os.path import exists
- file_exists = exists(self.filename)
- self.doc.saveas(self.filename)
- self.doc.save()
-
- def createLayer(self, layerName="newLayer", layerColor=(0,0,0), layerLineType='CONTINUOUS'):
- layer = self.doc.layers.add(name=layerName, linetype=layerLineType)
- layer.rgb = layerColor
- return layer
-
- def createBlock(self, blockName='newBlock'):
- # Create a block
- block = self.doc.blocks.new(name=blockName)
- return block
-
- def insertBlock(self, name, point=(0.0, 0.0), rotation=0.0, xscale=0.0, yscale=0.0):
- if name == "":
- return None
-
- return self.msp.add_blockref(name, point, dxfattribs={
- 'xscale': xscale,
- 'yscale': yscale,
- 'rotation': rotation
- })
-
- def createPolyline(self, wire):
- try:
- data = getWire(wire.Shape)
- lwp = self.msp.add_lwpolyline(data)
- return lwp
- except Exception as e:
- print("Error creating polyline:", e)
- return None
-
-
def getWire(wire, nospline=False, width=.0):
"""Return a list of DXF ready points and bulges from a wire.
@@ -321,7 +71,7 @@ def getWire(wire, nospline=False, width=.0):
--------
calcBulge
"""
- import Part
+
import DraftGeomUtils
import math
@@ -419,44 +169,559 @@ def getArcData(edge):
math.degrees(ang1), math.degrees(ang2))
+# =================================================================================
+# CLASE PARA EXPORTACIÓN DXF
+# =================================================================================
+
+class exportDXF:
+
+ def __init__(self, filename):
+
+ self.doc = None
+ self.msp = None
+ self.filename = filename
+ self.paper_layouts = []
+
+ '''
+ doc.linetypes.add("GRENZE2",
+ # linetype definition in acad.lin:
+ # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1
+ # replacing BOX by shape index 132 (got index from an AutoCAD file),
+ # ezdxf can't get shape index from ltypeshp.shx
+ pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
+ description="Grenze eckig ----[]-----[]----[]-----[]----[]--",
+ length=1.45, # required for complex line types
+ })
+
+ doc.linetypes.add("GRENZE2",
+ # linetype definition in acad.lin:
+ # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1
+ # replacing BOX by shape index 132 (got index from an AutoCAD file),
+ # ezdxf can't get shape index from ltypeshp.shx
+ pattern = "A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
+ description = "Límite1 ----0-----0----0-----0----0-----0--",
+ length = 1.45, # required for complex line types
+ })
+ '''
+
+ def createFile(self, version='R2018'):
+ import ezdxf
+ from ezdxf.tools.standards import linetypes
+
+ # 1. Create a new document
+ self.doc = ezdxf.new(version)
+
+ # 2. Setup document:
+ self.doc.header["$INSUNITS"] = 6
+ self.doc.header['$MEASUREMENT'] = 1
+ self.doc.header['$LUNITS'] = 2
+ self.doc.header['$AUNITS'] = 0
+
+ # 3. Add new entities to the modelspace:
+ self.msp = self.doc.modelspace()
+
+ for name, desc, pattern in linetypes():
+ if name not in self.doc.linetypes:
+ self.doc.linetypes.add(name=name,
+ pattern=pattern,
+ description=desc,
+ )
+
+
+ self.doc.linetypes.add(name="FENCELINE1",
+ pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1",
+ description="Límite1 ----0-----0----0-----0----0-----0--",
+ length=1.45) # required for complex line types
+
+ def save(self):
+ from os.path import exists
+ file_exists = exists(self.filename)
+ self.doc.saveas(self.filename)
+ self.doc.save()
+
+ def createLayer(self, layerName="newLayer", layerColor=(0,0,0), layerLineType='CONTINUOUS'):
+ layer = self.doc.layers.add(name=layerName, linetype=layerLineType)
+ layer.rgb = layerColor
+ return layer
+
+ def createBlock(self, blockName='newBlock'):
+ # Create a block
+ block = self.doc.blocks.new(name=blockName)
+ return block
+
+ def insertBlock(self, name, point=(0.0, 0.0), rotation=0.0, xscale=0.0, yscale=0.0):
+ if name == "":
+ return None
+
+ return self.msp.add_blockref(name, point, dxfattribs={
+ 'xscale': xscale,
+ 'yscale': yscale,
+ 'rotation': rotation
+ })
+
+ def createPolyline(self, wire):
+ try:
+ data = getWire(wire.Shape)
+ lwp = self.msp.add_lwpolyline(data)
+ return lwp
+ except Exception as e:
+ print("Error creating polyline:", e)
+ return None
+
+# =================================================================================
+# INTERFAZ DE USUARIO
+# =================================================================================
+class LineTypeEditorDelegate(QtWidgets.QStyledItemDelegate):
+ """Delegado que muestra el combobox personalizado solo durante la edición"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.line_styles = {
+ "CONTINUOUS": QtCore.Qt.SolidLine,
+ "DASHED": QtCore.Qt.DashLine,
+ "DOTTED": QtCore.Qt.DotLine,
+ "DASH-DOT": QtCore.Qt.DashDotLine,
+ "DASH-DOT-DOT": QtCore.Qt.DashDotDotLine,
+ "FENCELINE1": QtCore.Qt.CustomDashLine,
+ }
+ self.custom_patterns = {
+ "FENCELINE1": [4, 2, 1, 2]
+ }
+
+ # Tamaño mínimo para mostrar la previsualización
+ self.min_preview_width = 80
+
+ def createEditor(self, parent, option, index):
+ # SOLO se crea el editor cuando se inicia la edición
+ return LineTypeComboBox(parent)
+
+ def setEditorData(self, editor, index):
+ # Establecer el valor actual en el editor
+ value = index.data(QtCore.Qt.DisplayRole)
+ if value:
+ editor.setCurrentText(value)
+
+ def setModelData(self, editor, model, index):
+ # Guardar el valor seleccionado en el modelo
+ model.setData(index, editor.currentText(), QtCore.Qt.EditRole)
+
+ def paint0(self, painter, option, index):
+ """Pintado normal de la celda (sin editor)"""
+ # Configurar el fondo según selección
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+ else:
+ painter.fillRect(option.rect, option.palette.base())
+
+ # Dibujar el texto del tipo de línea
+ text = index.data(QtCore.Qt.DisplayRole) or ""
+ painter.setPen(option.palette.text().color())
+ painter.drawText(option.rect.adjusted(5, 0, -5, 0),
+ QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
+ text)
+
+ def paint1(self, painter, option, index):
+ # Fondo para selección
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+ else:
+ painter.fillRect(option.rect, option.palette.base())
+
+ # Obtener el tipo de línea
+ line_type = index.data(QtCore.Qt.DisplayRole) or "CONTINUOUS"
+ line_type = line_type.upper()
+
+ # Área de texto
+ text_rect = option.rect.adjusted(5, 0, -100, 0)
+ painter.setPen(option.palette.color(QtGui.QPalette.Text))
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type)
+
+ # Área de previsualización
+ preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
+
+ # Configurar el estilo de línea
+ pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
+
+ if line_type in self.line_styles:
+ pen.setStyle(self.line_styles[line_type])
+
+ if line_type in self.custom_patterns:
+ pen.setDashPattern(self.custom_patterns[line_type])
+ else:
+ pen.setStyle(QtCore.Qt.SolidLine)
+
+ painter.setPen(pen)
+
+ # Dibujar la línea
+ center_y = preview_rect.center().y()
+ painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y)
+
+ def paint(self, painter, option, index):
+ # Fondo para selección
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+ else:
+ painter.fillRect(option.rect, option.palette.base())
+
+ # Obtener el tipo de línea
+ line_type = index.data(QtCore.Qt.DisplayRole) or "CONTINUOUS"
+ line_type = line_type.upper()
+
+ # Calcular el espacio disponible
+ total_width = option.rect.width()
+
+ # Área de texto (ajustable)
+ text_rect = option.rect.adjusted(5, 0, -5, 0)
+
+ # Área de previsualización (derecha, tamaño fijo pero adaptable)
+ preview_width = min(self.min_preview_width, total_width - 50) # 50px para texto mínimo
+ if preview_width < 20: # No mostrar previsualización si es demasiado pequeña
+ preview_width = 0
+
+ # Ajustar rectángulos
+ if preview_width > 0:
+ text_rect.setRight(option.rect.right() - preview_width - 10)
+ preview_rect = option.rect.adjusted(
+ option.rect.width() - preview_width,
+ 2,
+ -5,
+ -2
+ )
+ else:
+ # Solo mostrar texto si no hay espacio para previsualización
+ preview_rect = None
+
+ # Dibujar texto del tipo de línea
+ painter.setPen(option.palette.color(QtGui.QPalette.Text))
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type)
+
+ # Dibujar previsualización si hay espacio suficiente
+ if preview_rect and preview_width > 20:
+ # Configurar el estilo de línea
+ pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
+ if line_type in self.line_styles:
+ pen.setStyle(self.line_styles[line_type])
+ if line_type in self.custom_patterns:
+ pen.setDashPattern(self.custom_patterns[line_type])
+ else:
+ pen.setStyle(QtCore.Qt.SolidLine)
+
+ painter.setPen(pen)
+
+ # Dibujar la línea centrada verticalmente
+ center_y = preview_rect.center().y()
+ painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y)
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(200, 25)
+
+
+class LineTypeDelegate(QtWidgets.QStyledItemDelegate):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.line_types = [
+ {"name": "Continuous", "pen": QtCore.Qt.SolidLine},
+ {"name": "Dashed", "pen": QtCore.Qt.DashLine},
+ {"name": "Dotted", "pen": QtCore.Qt.DotLine},
+ {"name": "Dash-Dot", "pen": QtCore.Qt.DashDotLine},
+ {"name": "Dash-Dot-Dot", "pen": QtCore.Qt.DashDotDotLine}
+ ]
+
+ def paint(self, painter, option, index):
+ # Dibujar el fondo
+ painter.save()
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
+
+ # Configuraciones comunes
+ line_color = QtGui.QColor(0, 0, 0) # Color de la línea
+ line_width = 2 # Grosor de línea
+
+ # Dibujar el ítem base
+ super().paint(painter, option, index)
+
+ # Obtener el tipo de línea
+ line_type = self.line_types[index.row()]
+
+ # Configurar el área de dibujo
+ text_rect = option.rect.adjusted(5, 0, -100, 0)
+ line_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
+
+ # Dibujar el texto
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type["name"])
+
+ # Dibujar la línea de muestra
+ pen = QtGui.QPen(line_color, line_width, line_type["pen"])
+ painter.setPen(pen)
+
+ start_y = line_rect.center().y()
+ painter.drawLine(line_rect.left(), start_y, line_rect.right(), start_y)
+
+ painter.restore()
+
+ def sizeHint(self, option, index):
+ size = super().sizeHint(option, index)
+ size.setHeight(25) # Altura aumentada para mejor visualización
+ return size
+
+
+class LineTypePreviewDelegate(QtWidgets.QStyledItemDelegate):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.line_types = [
+ {"name": "Continuous", "style": QtCore.Qt.SolidLine},
+ {"name": "Dashed", "style": QtCore.Qt.DashLine},
+ {"name": "Dotted", "style": QtCore.Qt.DotLine},
+ {"name": "Custom 1 (--0--0--)", "style": QtCore.Qt.CustomDashLine, "pattern": [8, 4, 2, 4]}, # Patrón personalizado
+ {"name": "Custom 2 (- - -)", "style": QtCore.Qt.CustomDashLine, "pattern": [6, 3]} # Otro patrón
+ ]
+
+ def paint(self, painter, option, index):
+
+ # SOLUCIÓN AL PROBLEMA: Usar un enfoque más simple sin painter.save/restore
+ try:
+ if index.row() >= len(self.line_types):
+ return
+
+ # Configurar el fondo para selección
+ if option.state & QtGui.QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+
+ # Determinar el tipo de línea
+ line_data = self.line_types[index.row()]
+ line_name = line_data["name"]
+ line_style = line_data["style"]
+
+ # Área de texto
+ text_rect = option.rect.adjusted(5, 0, -100, 0)
+ painter.setPen(option.palette.color(QtGui.QPalette.Text))
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_name)
+
+ # Área de previsualización
+ preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
+
+ # Dibujar la línea de muestra
+ pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
+ pen.setStyle(line_style)
+ painter.setPen(pen)
+
+ center_y = preview_rect.center().y()
+ painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y)
+
+ except Exception as e:
+ print("Error painting line type:", e)
+
+ '''
+ painter.save()
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
+
+ if index.row() >= len(self.line_types):
+ painter.restore()
+ return
+
+ line_data = self.line_types[index.row()]
+ line_name = line_data["name"]
+ line_style = line_data.get("style", QtCore.Qt.SolidLine)
+ dash_pattern = line_data.get("pattern", [])
+
+ # Fondo para selección
+ if option.state & QtGui.QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+
+ # Áreas de dibujo
+ text_rect = option.rect.adjusted(5, 0, -100, 0)
+ preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2)
+
+ # Texto
+ painter.setPen(option.palette.color(QtGui.QPalette.Text))
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_name)
+
+ # Línea personalizada
+ pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2)
+ pen.setStyle(line_style)
+
+ if line_style == QtCore.Qt.CustomDashLine and dash_pattern:
+ pen.setDashPattern(dash_pattern)
+
+ painter.setPen(pen)
+ painter.drawLine(preview_rect.left(), preview_rect.center().y(),
+ preview_rect.right(), preview_rect.center().y())
+
+ painter.restore()'''
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(200, 25)
+
+
+class LineTypeComboBox(QtWidgets.QComboBox):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._delegate = LineTypePreviewDelegate(self)
+ self.setItemDelegate(self._delegate)
+ self.addItems(["Continuous", "Dashed", "Dotted", "Dash-Dot", "Dash-Dot-Dot"])
+
+ def paintEvent(self, event):
+ """Pintado simplificado sin QPainter complejo"""
+ painter = QtGui.QPainter(self)
+ try:
+ # Dibujar fondo
+ option = QtWidgets.QStyleOptionComboBox()
+ self.initStyleOption(option)
+ self.style().drawComplexControl(QtGui.QStyle.CC_ComboBox, option, painter, self)
+
+ # Dibujar texto
+ text_rect = self.rect().adjusted(5, 0, -30, 0)
+ painter.setPen(self.palette().color(QtGui.QPalette.Text))
+ painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, self.currentText())
+ finally:
+ painter.end() # Asegurar que el painter se cierre correctamente
+
+
class _PVPlantExportDXF(QtGui.QWidget):
'''The editmode TaskPanel to select what you want to export'''
def __init__(self):
- # super(_PVPlantExportDXF, self).__init__()
- QtGui.QWidget.__init__(self)
-
import os
- from datetime import datetime
+ QtGui.QWidget.__init__(self)
# self.form:
self.form = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(os.path.realpath(__file__)), "exportDXF.ui"))
# setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg")))
# self.form.buttonTo.clicked.connect(self.addTo)
- self.form.tableLayers.setItemDelegateForColumn(3, LineTypeDelegate())
self.layout = QtGui.QHBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
self.layout.addWidget(self.form)
self.form.buttonAcept.clicked.connect(self.onAceptClick)
+ self.form.buttonCancel.clicked.connect(self.onCancelClick)
- self.add_row("Layer 1", QtGui.QColor(255, 0, 0), "Continua", "1")
- self.add_row("Layer 2", QtGui.QColor(255, 0, 0), "Continua", "1")
+ self.delegate = LineTypeEditorDelegate()
+ self.form.tableLayers.setItemDelegateForColumn(3, self.delegate)
- path = os.path.join(os.path.dirname(FreeCAD.ActiveDocument.FileName), "outputs", "autocad")
- if not os.path.exists(path):
- os.makedirs(path)
- name = datetime.now().strftime("%Y%m%d%H%M%S") + "-" + FreeCAD.ActiveDocument.Name
- self.filename = os.path.join(path, name) + ".dxf"
+ from datetime import datetime
+ import os
+ output_dir = os.path.join(os.path.dirname(FreeCAD.ActiveDocument.FileName), "outputs", "autocad")
+ os.makedirs(output_dir, exist_ok=True)
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
+ self.filename = os.path.join(output_dir, f"{timestamp}-{FreeCAD.ActiveDocument.Name}.dxf")
- def add_row(self, name, color, line_type, thickness):
+ # Limpiar la tabla
+ for row in range(self.form.tableLayers.rowCount() - 1, -1, -1):
+ self.form.tableLayers.removeRow(row)
+
+ # Configuración de las capas por defecto
+ self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True)
+ self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "CONTINUOUS", "1", True)
+ self.add_row("Internal_Roads", QtGui.QColor(128, 128, 128), "CONTINUOUS", "1", True)
+ self.add_row("Frames", QtGui.QColor(0, 255, 0), "CONTINUOUS", "1", True)
+
+ def save_project_settings(self):
+ """Guarda la configuración actual en el proyecto de FreeCAD"""
+ try:
+ # Guardar estado de la tabla
+ table_data = []
+ for row in range(self.form.tableLayers.rowCount()):
+ # Estado del checkbox
+ checked = self.form.tableLayers.cellWidget(row, 0).findChild(QtWidgets.QCheckBox).isChecked()
+
+ # Nombre de capa
+ name = self.form.tableLayers.item(row, 1).text()
+
+ # Color
+ color_btn = self.form.tableLayers.cellWidget(row, 2).findChild(QtWidgets.QPushButton)
+ color = color_btn.color
+ color_str = f"{color.red()},{color.green()},{color.blue()}"
+
+ # Tipo de línea
+ line_type = self.form.tableLayers.item(row, 3).text()
+
+ # Grosor
+ thickness_combo = self.form.tableLayers.cellWidget(row, 4)
+ thickness = thickness_combo.currentText()
+
+ table_data.append({
+ "name": name,
+ "color": color_str,
+ "line_type": line_type,
+ "thickness": thickness,
+ "checked": checked
+ })
+
+ # Crear o actualizar el objeto de configuración en el proyecto
+ config_obj = None
+ for obj in FreeCAD.ActiveDocument.Objects:
+ if obj.Name == "DXF_Export_Config":
+ config_obj = obj
+ break
+
+ if not config_obj:
+ config_obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "DXF_Export_Config")
+
+ # Guardar como propiedad del objeto
+ config_obj.addProperty("App::PropertyString", "TableSettings", "DXF Export", "Table configuration")
+ config_obj.TableSettings = json.dumps(table_data)
+
+ print("Configuración guardada en el proyecto")
+ except Exception as e:
+ print("Error guardando configuración en el proyecto:", e)
+
+ def load_project_settings(self):
+ """Carga la configuración guardada desde el proyecto de FreeCAD"""
+ try:
+ # Buscar objeto de configuración en el proyecto
+ config_obj = None
+ for obj in FreeCAD.ActiveDocument.Objects:
+ if obj.Name == "DXF_Export_Config":
+ config_obj = obj
+ break
+
+ if config_obj and hasattr(config_obj, "TableSettings"):
+ table_json = config_obj.TableSettings
+ if table_json:
+ table_data = json.loads(table_json)
+ self.load_table_data(table_data)
+ return
+
+ # Si no hay configuración guardada para este proyecto, cargar configuración por defecto
+ self.add_default_rows()
+ except Exception as e:
+ print("Error cargando configuración del proyecto:", e)
+ self.add_default_rows()
+
+ def add_default_rows(self):
+ """Añade filas por defecto cuando no hay configuración guardada"""
+ self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True)
+ self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "DASHED", "1", True)
+ self.add_row("Internal_Roads", QtGui.QColor(128, 128, 128), "CONTINUOUS", "1", True)
+ self.add_row("Frames", QtGui.QColor(0, 255, 0), "CONTINUOUS", "1", True)
+
+ def load_table_data(self, table_data):
+ """Carga los datos en la tabla desde una estructura de datos"""
+ for layer_data in table_data:
+ # Parsear color
+ color_parts = layer_data["color"].split(",")
+ if len(color_parts) == 3:
+ color = QtGui.QColor(int(color_parts[0]), int(color_parts[1]), int(color_parts[2]))
+ else:
+ color = QtGui.QColor(0, 0, 0) # Color por defecto si hay error
+
+ # Añadir fila
+ self.add_row(
+ layer_data["name"],
+ color,
+ layer_data["line_type"],
+ layer_data["thickness"],
+ layer_data["checked"]
+ )
+
+ def add_row(self, name, color, line_type, thickness, checked=True):
row = self.form.tableLayers.rowCount()
self.form.tableLayers.insertRow(row)
- self.form.tableLayers.setRowHeight(row, 20)
+ self.form.tableLayers.setRowHeight(row, 25)
- # Columna 0: Checkbox
+ # Checkbox
checkbox = QtWidgets.QCheckBox()
+ checkbox.setChecked(checked)
cell_widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(cell_widget)
layout.addWidget(checkbox)
@@ -464,28 +729,17 @@ class _PVPlantExportDXF(QtGui.QWidget):
layout.setContentsMargins(0, 0, 0, 0)
self.form.tableLayers.setCellWidget(row, 0, cell_widget)
- # Columna 1: Nombre (editable)
+ # Nombre de capa
item = QtWidgets.QTableWidgetItem(name)
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
self.form.tableLayers.setItem(row, 1, item)
- # Columna 2: Selector de color
+ # Selector de color
color_btn = QtWidgets.QPushButton()
- color_btn.setFixedSize(20, 20) # Tamaño del cuadrado
- color_btn.setStyleSheet(f"""
- QPushButton {{
- background-color: {color.name()};
- border: 1px solid #808080;
- border-radius: 3px;
- }}
- QPushButton:hover {{
- border: 2px solid #606060;
- }}
- """)
- color_btn.color = color # Almacenar el color como atributo
+ color_btn.setFixedSize(20, 20)
+ color_btn.setStyleSheet(f"background-color: {color.name()}; border: 1px solid #808080; border-radius: 3px;")
+ color_btn.color = color
color_btn.clicked.connect(lambda: self.change_color(color_btn))
-
- # Widget contenedor para centrar el botón
cell_widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(cell_widget)
layout.addWidget(color_btn)
@@ -493,15 +747,13 @@ class _PVPlantExportDXF(QtGui.QWidget):
layout.setContentsMargins(0, 0, 0, 0)
self.form.tableLayers.setCellWidget(row, 2, cell_widget)
- # Columna 3: Tipo de línea (combobox)
- line_combo = LineTypeComboBox()
- line_combo.addItems(["Continua", "Discontinua", "Punteada", "Mixta"])
- line_combo.setCurrentText(line_type)
- self.form.tableLayers.setCellWidget(row, 3, line_combo)
+ # Tipo de línea
+ item = QtWidgets.QTableWidgetItem(line_type)
+ self.form.tableLayers.setItem(row, 3, item)
- # Columna 4: Grosor de línea (combobox)
+ # Grosor de línea
thickness_combo = QtWidgets.QComboBox()
- thickness_combo.addItems(["1", "2", "3", "4"])
+ thickness_combo.addItems(["0.1", "0.2", "0.3", "0.5", "1.0", "2.0"])
thickness_combo.setCurrentText(thickness)
self.form.tableLayers.setCellWidget(row, 4, thickness_combo)
@@ -509,53 +761,61 @@ class _PVPlantExportDXF(QtGui.QWidget):
color = QtWidgets.QColorDialog.getColor(button.color, self, "Seleccionar color")
if color.isValid():
button.color = color
- button.setStyleSheet(f"background-color: {color.name()}")
+ button.setStyleSheet(f"background-color: {color.name()}; border: 1px solid #808080; border-radius: 3px;")
- def createLayers(self):
- ''' '''
- self.exporter.createLayer("Areas_Boundary", layerColor=(0, 125, 125), layerLineType="FENCELINE1")
- self.exporter.createLayer("Areas_Exclusion", layerColor=(255, 0, 0))
- self.exporter.createLayer("Areas_Offsets", layerColor=(128, 128, 255))
+ def createLayers(self, exporter):
+ for row in range(self.form.tableLayers.rowCount()):
+ if self.form.tableLayers.cellWidget(row, 0).findChild(QtWidgets.QCheckBox).isChecked():
+ layer_name = self.form.tableLayers.item(row, 1).text()
+ color = self.form.tableLayers.cellWidget(row, 2).findChild(QtWidgets.QPushButton).color
+ #line_type = self.form.tableLayers.cellWidget(row, 3).currentText()
+ line_type = self.form.tableLayers.item(row, 3).text()
- self.exporter.createLayer("Internal_Roads", layerColor=(128, 128, 128))
- self.exporter.createLayer("Internal_Roads_Axis", layerColor=(255, 255, 255), layerLineType="DASHEDX2")
+ exporter.createLayer(
+ layerName=layer_name,
+ layerColor=(color.red(), color.green(), color.blue()),
+ layerLineType=line_type.upper()
+ )
+ def writeArea(self, exporter):
+ exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "boundary")
- def writeArea(self):
- pol = self.exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary)
- if pol:
- pol.dxf.layer = "boundary"
+ areas_types = ["Boundaries", "Exclusions", "Offsets"]
+ for area_type in areas_types:
+ if hasattr(FreeCAD.ActiveDocument, area_type):
+ for area in FreeCAD.ActiveDocument.Boundaries.Group:
+ exporter.createPolyline(area, "Areas_Boundary")
- for area in FreeCAD.ActiveDocument.Boundaries.Group:
- pol = self.exporter.createPolyline(area)
+ '''for area in FreeCAD.ActiveDocument.Boundaries.Group:
+ pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Boundary"
for area in FreeCAD.ActiveDocument.Exclusions.Group:
- pol = self.exporter.createPolyline(area)
+ pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Exclusion"
for area in FreeCAD.ActiveDocument.Offsets.Group:
- pol = self.exporter.createPolyline(area)
- pol.dxf.layer = "Areas_Offsets"
+ pol = exporter.createPolyline(area)
+ pol.dxf.layer = "Areas_Offsets"'''
- def writeFrameSetups(self):
- import Part
- # 1. Profiles:
- profilelist = list()
+ def writeFrameSetups(self, exporter):
+ if not hasattr(FreeCAD.ActiveDocument, "Site"):
+ return
+
+ # 2. Postes
for ts in FreeCAD.ActiveDocument.Site.Frames:
for poletype in ts.PoleType:
- if not (poletype in profilelist):
- profilelist.append(poletype)
- block = self.exporter.createBlock(poletype.Label)
- w = poletype.Base.Shape.Wires[0]
- w.Placement.Base = FreeCAD.Vector(w.Placement.Base).sub(w.BoundBox.Center)
- block.add_lwpolyline(getWire(w))
+ block = exporter.createBlock(poletype.Label)
+ w = poletype.Base.Shape.Wires[0]
+ center = w.BoundBox.Center
+ w.Placement.Base = w.Placement.Base.sub(center)
+ block.add_lwpolyline(getWire(w))
- block.add_circle((0, 0), 0.2, dxfattribs={'color': 2})
- p = math.sin(math.radians(45)) * 0.2
+ block.add_circle((0, 0), 0.2, dxfattribs={'color': 2})
+ p = math.sin(math.radians(45)) * 0.2
- block.add_line((-p, -p), (p, p), dxfattribs={"layer": "MyLines"})
- block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"})
+ block.add_line((-p, -p), (p, p), dxfattribs={"layer": "MyLines"})
+ block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"})
# 2. Frames
for ts in FreeCAD.ActiveDocument.Site.Frames:
@@ -565,10 +825,10 @@ class _PVPlantExportDXF(QtGui.QWidget):
w = Part.makePolygon(pts)
w.Placement.Base = w.Placement.Base.sub(w.BoundBox.Center)
mblockname = "Trina_TSM-DEG21C-20-6XXWp Vertex"
- mblock = self.exporter.createBlock(mblockname)
+ mblock = exporter.createBlock(mblockname)
mblock.add_lwpolyline(getWire(w))
- rblock = self.exporter.createBlock(ts.Label)
+ rblock = exporter.createBlock(ts.Label)
w = max(ts.Shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda x: x.Placement.Base.z).Wires[0]
w.Placement.Base = w.Placement.Base.sub(w.BoundBox.Center)
rblock.add_lwpolyline(getWire(w))
@@ -588,34 +848,38 @@ class _PVPlantExportDXF(QtGui.QWidget):
'yscale': .0,
'rotation': 0})
- def writeFrames(self):
+ def writeFrames(self, exporter):
objects = findObjects('Tracker')
for frame in objects:
if hasattr(frame, "Setup"):
point = frame.Placement.Base * 0.001
point = point[:2]
- self.exporter.insertBlock(frame.Setup.Label, point=point, rotation=frame.AngleZ)
+ exporter.insertBlock(frame.Setup.Label, point=point, rotation=frame.AngleZ)
- def writeRoads(self):
+ def writeRoads(self, exporter):
objects = findObjects("Road")
- #rblock = self.exporter.createBlock("Internal_roads")
+ #rblock = exporter.createBlock("Internal_roads")
for road in objects:
- base = self.exporter.createPolyline(road.Base)
+ base = exporter.createPolyline(road.Base, "Internal_Roads")
base.dxf.const_width = road.Width.Value * 0.001
- base.dxf.layer = "Internal_Roads"
- axis = self.exporter.createPolyline(road.Base)
+ axis = exporter.createPolyline(road.Base, "Internal_Roads_Axis")
axis.dxf.const_width = .2
- axis.dxf.layer = "Internal_Roads_Axis"
- #my_lines = doc.layers.get('MyLines')
- def writeTrenches(self):
+ if FreeCAD.ActiveDocument.Transport:
+ for road in FreeCAD.ActiveDocument.Transport.Group:
+ base = exporter.createPolyline(road, "External_Roads")
+ base.dxf.const_width = road.Width
+
+ axis = exporter.createPolyline(road.Base, "External_Roads_Axis")
+ axis.dxf.const_width = .2
+
+ def writeTrenches(self, exporter):
objects = findObjects("Trench")
- # rblock = self.exporter.createBlock("Internal_roads")
+ # rblock = exporter.createBlock("Internal_roads")
for obj in objects:
- base = self.exporter.createPolyline(obj.Base)
+ base = exporter.createPolyline(obj.Base, "Trench")
base.dxf.const_width = obj.Width.Value * 0.001
- base.dxf.layer = "Trench"
def setup_layout4(self, doc):
layout2 = doc.layouts.new("scale 1-1")
@@ -652,23 +916,84 @@ class _PVPlantExportDXF(QtGui.QWidget):
layout2.add_line((center.x, y1), (center.x, y2)) # vertical center line
layout2.add_circle((0, 0), radius=5) # plot origin
- def onAceptClick(self):
- self.exporter = exportDXF(self.filename)
- if self.exporter:
- self.exporter.createFile()
- self.createLayers()
- self.writeArea()
- self.writeFrameSetups()
- self.writeFrames()
- self.writeRoads()
- self.writeTrenches()
- self.setup_layout4(self.exporter.doc)
+ def createPaperSpaces(self, exporter):
+ # Datos para el cajetín
+ title_block_data = {
+ 'width': 190,
+ 'height': 50,
+ 'fields': [
+ {'name': 'Proyecto', 'value': FreeCAD.ActiveDocument.Label},
+ {'name': 'Escala', 'value': '1:100'},
+ {'name': 'Fecha', 'value': QtCore.QDate.currentDate().toString("dd/MM/yyyy")},
+ {'name': 'Dibujado por', 'value': os.getlogin()}
+ ]
+ }
- self.exporter.save()
- print(self.filename)
+ # Datos para la tabla de información
+ table_data = {
+ 'Potencia pico': '50 MWp',
+ 'Número de trackers': str(len(findObjects('Tracker'))),
+ 'Superficie total': f"{FreeCAD.ActiveDocument.Site.Boundary.Shape.Area / 10000:.2f} ha"
+ }
+
+ # Datos para el viewport
+ viewport_data = {
+ 'center': (150, 100),
+ 'size': (250, 180),
+ 'view_center': (0, 0),
+ 'view_height': 200
+ }
+
+ # Crear diferentes layouts para diferentes tamaños de papel
+ exporter.createPaperSpaceLayout(
+ "Layout_A4",
+ (297, 210), # A4 landscape
+ title_block_data,
+ table_data,
+ viewport_data
+ )
+
+ exporter.createPaperSpaceLayout(
+ "Layout_A3",
+ (420, 297), # A3 landscape
+ title_block_data,
+ table_data,
+ viewport_data
+ )
+
+ def onAceptClick(self):
+ exporter = exportDXF(self.filename)
+ exporter.createFile()
+
+ # Crear capas configuradas
+ self.createLayers(exporter)
+
+ # Exportar objetos
+ self.writeArea(exporter)
+ self.writeFrameSetups(exporter)
+ self.writeFrames(exporter)
+ self.writeRoads(exporter)
+ self.writeTrenches(exporter)
+
+ # Crear espacios de papel
+ self.setup_layout4(exporter.doc)
+ self.createPaperSpaces(exporter)
+
+ # Guardar archivo
+ exporter.save()
+
+ # Mostrar mensaje de éxito
+ QtWidgets.QMessageBox.information(
+ self,
+ "Exportación completada",
+ f"Archivo DXF guardado en:\n{self.filename}"
+ )
self.close()
+ def onCancelClick(self):
+ # Cerrar el diálogo sin hacer nada
+ self.close()
class CommandExportDXF:
def GetResources(self):
diff --git a/Export/exportDXF.ui b/Export/exportDXF.ui
index d8638d3..4694a97 100644
--- a/Export/exportDXF.ui
+++ b/Export/exportDXF.ui
@@ -6,8 +6,8 @@
0
0
- 715
- 520
+ 462
+ 282
@@ -78,6 +78,11 @@
Lineweight
+ -
+
+
+
+
@@ -204,6 +209,21 @@
-
+
+ 6
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
diff --git a/Export/exportPVSyst.py b/Export/exportPVSyst.py
index 11b30eb..9368f24 100644
--- a/Export/exportPVSyst.py
+++ b/Export/exportPVSyst.py
@@ -524,6 +524,7 @@ def exportToPVC(path, exportTerrain = False):
# TODO: revisar
for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower()
+ isTracker = False
objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
tmp = []
diff --git a/Importer/importOSM.py b/Importer/importOSM.py
index 7a305ab..024716b 100644
--- a/Importer/importOSM.py
+++ b/Importer/importOSM.py
@@ -9,6 +9,7 @@ import urllib.request
import math
import utm
from collections import defaultdict
+import PVPlantImportGrid as ImportElevation
scale = 1000.0
@@ -42,8 +43,10 @@ class OSMImporter:
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transform_from_latlon(self, lat, lon):
- x, y, _, _ = utm.from_latlon(lat, lon)
- return FreeCAD.Vector(x, y, .0) * scale - self.Origin
+ 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"""
@@ -148,7 +151,6 @@ class OSMImporter:
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)
@@ -737,27 +739,163 @@ class OSMImporter:
def create_vegetation(self):
vegetation_layer = self.create_layer("Vegetation")
- # Árboles individuales
- for node_id, coords in self.nodes.items():
- # Verificar si es un árbol
- # (Necesitarías procesar los tags de los nodos, implementación simplificada)
- cylinder = Part.makeCylinder(0.5, 5.0, FreeCAD.Vector(coords[0], coords[1], 0))# * scale - self.Origin )
- tree = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree")
- vegetation_layer.addObject(tree)
- tree.Shape = cylinder
- tree.ViewObject.ShapeColor = self.feature_colors['vegetation']
+ # 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
- # Áreas verdes
+ 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():
- if 'natural' in data['tags'] or 'landuse' in data['tags']:
- nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
- if len(nodes) > 2:
- polygon_points = [n for n in nodes]
- polygon = Part.makePolygon(polygon_points)
- face = Part.Face(polygon)
- area = vegetation_layer.addObject("Part::Feature", "GreenArea")
- area.Shape = face
- area.ViewObject.ShapeColor = self.feature_colors['vegetation']
+ 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")
@@ -769,7 +907,8 @@ class OSMImporter:
polygon_points = [n for n in nodes]
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
- water = water_layer.addObject("Part::Feature", "WaterBody")
+ 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']
diff --git a/PVPlantGeoreferencing.py b/PVPlantGeoreferencing.py
index 5eb2b81..064670a 100644
--- a/PVPlantGeoreferencing.py
+++ b/PVPlantGeoreferencing.py
@@ -52,7 +52,9 @@ class MapWindow(QtGui.QWidget):
self.maxLat = None
self.minLon = None
self.maxLon = None
+ self.zoom = None
self.WinTitle = WinTitle
+ self.georeference_coordinates = {'lat': None, 'lon': None}
self.setupUi()
def setupUi(self):
@@ -152,6 +154,9 @@ class MapWindow(QtGui.QWidget):
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
RightLayout.addWidget(self.checkboxImportGis)
+ self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital")
+ RightLayout.addWidget(self.checkboxImportSatelitalImagen)
+
verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
RightLayout.addItem(verticalSpacer)
@@ -192,6 +197,7 @@ class MapWindow(QtGui.QWidget):
"var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));"
)
+
self.close()
@QtCore.Slot(float, float)
@@ -203,17 +209,22 @@ class MapWindow(QtGui.QWidget):
' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(x, y))
- @QtCore.Slot(float, float, float, float)
- def onMapZoom(self, minLat, minLon, maxLat, maxLon):
+ @QtCore.Slot(float, float, float, float, int)
+ def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
self.minLat = min([minLat, maxLat])
self.maxLat = max([minLat, maxLat])
self.minLon = min([minLon, maxLon])
self.maxLon = max([minLon, maxLon])
+ self.zoom = zoom
@QtCore.Slot(float, float)
def georeference(self, lat, lng):
import PVPlantSite
from geopy.geocoders import Nominatim
+
+ self.georeference_coordinates['lat'] = lat
+ self.georeference_coordinates['lon'] = lng
+
Site = PVPlantSite.get(create=True)
Site.Proxy.setLatLon(lat, lng)
@@ -278,7 +289,7 @@ class MapWindow(QtGui.QWidget):
pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False)
- #obj.Placement.Base = offset
+ #obj.Placement.Base = Site.Origin
obj.Label = name
Draft.autogroup(obj)
@@ -288,6 +299,170 @@ class MapWindow(QtGui.QWidget):
if self.checkboxImportGis.isChecked():
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
+ if self.checkboxImportSatelitalImagen.isChecked():
+ # Usar los límites reales del terreno (rectangular)
+ '''s_lat = self.minLat
+ s_lon = self.minLon
+ n_lat = self.maxLat
+ n_lon = self.maxLon
+
+ # Obtener puntos UTM para las esquinas
+ corners = ImportElevation.getElevationFromOE([
+ [s_lat, s_lon], # Esquina suroeste
+ [n_lat, s_lon], # Esquina sureste
+ [n_lat, n_lon], # Esquina noreste
+ [s_lat, n_lon] # Esquina noroeste
+ ])
+
+ if not corners or len(corners) < 4:
+ FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas\n")
+ return
+
+ # Descargar imagen satelital
+ from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
+ downloader = GoogleMapDownloader(
+ zoom= 18, #self.zoom,
+ layer='raw_satellite'
+ )
+ img = downloader.generateImage(
+ sw_lat=s_lat,
+ sw_lng=s_lon,
+ ne_lat=n_lat,
+ ne_lng=n_lon
+ )
+
+ # Guardar imagen en el directorio del documento
+ doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
+ if not doc_path:
+ doc_path = FreeCAD.ConfigGet("UserAppData")
+
+ filename = os.path.join(doc_path, "background.jpeg")
+ img.save(filename)
+
+ ancho, alto = img.size
+
+ # Crear objeto de imagen en FreeCAD
+ doc = FreeCAD.ActiveDocument
+ img_obj = doc.addObject('Image::ImagePlane', 'Background')
+ img_obj.ImageFile = filename
+ img_obj.Label = 'Background'
+
+ # Calcular dimensiones en metros usando las coordenadas UTM
+ # Extraer las coordenadas de las esquinas
+ sw = corners[0] # Suroeste
+ se = corners[1] # Sureste
+ ne = corners[2] # Noreste
+ nw = corners[3] # Noroeste
+
+ # Calcular ancho (promedio de los lados superior e inferior)
+ width_bottom = se.x - sw.x
+ width_top = ne.x - nw.x
+ width_m = (width_bottom + width_top) / 2
+
+ # Calcular alto (promedio de los lados izquierdo y derecho)
+ height_left = nw.y - sw.y
+ height_right = ne.y - se.y
+ height_m = (height_left + height_right) / 2
+
+ img_obj.XSize = width_m
+ img_obj.YSize = height_m
+
+ # Posicionar el centro de la imagen en (0,0,0)
+ img_obj.Placement.Base = FreeCAD.Vector(-width_m / 2, -height_m / 2, 0)'''
+
+ # Definir área rectangular
+ s_lat = self.minLat
+ s_lon = self.minLon
+ n_lat = self.maxLat
+ n_lon = self.maxLon
+
+ # Obtener puntos UTM para las esquinas y el punto de referencia
+ points = [
+ [s_lat, s_lon], # Suroeste
+ [n_lat, n_lon], # Noreste
+ [self.georeference_coordinates['lat'], self.georeference_coordinates['lon']] # Punto de referencia
+ ]
+ utm_points = ImportElevation.getElevationFromOE(points)
+
+ if not utm_points or len(utm_points) < 3:
+ FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas y referencia\n")
+ return
+
+ sw_utm, ne_utm, ref_utm = utm_points
+
+ # Descargar imagen satelital
+ from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
+ downloader = GoogleMapDownloader(
+ zoom=self.zoom,
+ layer='raw_satellite'
+ )
+ img = downloader.generateImage(
+ sw_lat=s_lat,
+ sw_lng=s_lon,
+ ne_lat=n_lat,
+ ne_lng=n_lon
+ )
+
+ # Guardar imagen
+ doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
+ if not doc_path:
+ doc_path = FreeCAD.ConfigGet("UserAppData")
+
+ filename = os.path.join(doc_path, "background.jpeg")
+ img.save(filename)
+
+ # Calcular dimensiones reales en metros
+ width_m = ne_utm.x - sw_utm.x # Ancho en metros (este-oeste)
+ height_m = ne_utm.y - sw_utm.y # Alto en metros (norte-sur)
+
+ # Calcular posición relativa del punto de referencia dentro de la imagen
+ rel_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5
+ rel_y = (ref_utm.y - sw_utm.y) / height_m if height_m != 0 else 0.5
+
+ # Crear objeto de imagen en FreeCAD
+ doc = FreeCAD.ActiveDocument
+ img_obj = doc.addObject('Image::ImagePlane', 'Background')
+ img_obj.ImageFile = filename
+ img_obj.Label = 'Background'
+
+ # Convertir dimensiones a milímetros (FreeCAD trabaja en mm)
+ img_obj.XSize = width_m * 1000
+ img_obj.YSize = height_m * 1000
+
+ # Posicionar para que el punto de referencia esté en (0,0,0)
+ # La esquina inferior izquierda debe estar en:
+ # x = -rel_x * ancho_total
+ # y = -rel_y * alto_total
+ img_obj.Placement.Base = FreeCAD.Vector(
+ -rel_x * width_m * 1000,
+ -rel_y * height_m * 1000,
+ 0
+ )
+
+ # Refrescar el documento
+ doc.recompute()
+
+ def calculate_texture_transform(self, mesh_obj, width_m, height_m):
+ """Calcula la transformación precisa para la textura"""
+ try:
+ # Obtener coordenadas reales de las esquinas
+ import utm
+ sw = utm.from_latlon(self.minLat, self.minLon)
+ ne = utm.from_latlon(self.maxLat, self.maxLon)
+
+ # Crear matriz de transformación
+ scale_x = (ne[0] - sw[0]) / width_m
+ scale_y = (ne[1] - sw[1]) / height_m
+
+ # Aplicar transformación (solo si se usa textura avanzada)
+ if hasattr(mesh_obj.ViewObject, "TextureMapping"):
+ mesh_obj.ViewObject.TextureMapping = "PLANE"
+ mesh_obj.ViewObject.TextureScale = (scale_x, scale_y)
+ mesh_obj.ViewObject.TextureOffset = (sw[0], sw[1])
+
+ except Exception as e:
+ FreeCAD.Console.PrintWarning(f"No se pudo calcular transformación: {str(e)}\n")
+
def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM
import PVPlantSite
diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py
index d7b68d4..3d3b4cd 100644
--- a/PVPlantImportGrid.py
+++ b/PVPlantImportGrid.py
@@ -125,9 +125,9 @@ def getElevationFromOE(coordinates):
points = []
for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1])
- points.append(FreeCAD.Vector(round(c[0] * 1000, 0),
- round(c[1] * 1000, 0),
- 0))
+ 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
@@ -136,14 +136,16 @@ def getElevationFromOE(coordinates):
results = r.json()
for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"])
- v = FreeCAD.Vector(round(c[0] * 1000, 0),
- round(c[1] * 1000, 0),
- round(point["elevation"] * 1000, 0))
+ v = FreeCAD.Vector(round(c[0], 0),
+ round(c[1], 0),
+ round(point["elevation"], 0)) * 1000
points.append(v)
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)
source += "&heights=sealevel"
@@ -153,11 +155,9 @@ def getSinglePointElevationFromBing(lat, lng):
response = requests.get(source)
ans = response.text
- # +# to do: error handling - wait and try again
s = json.loads(ans)
+ print(s)
res = s['resourceSets'][0]['resources'][0]['elevations']
-
- import utm
for elevation in res:
c = utm.from_latlon(lat, lng)
v = FreeCAD.Vector(
@@ -324,7 +324,6 @@ def getSinglePointElevationUtm(lat, lon):
print (v)
return v
-
def getElevationUTM(polygon, lat, lng, resolution = 10000):
import utm
@@ -448,47 +447,6 @@ def getElevation(lat, lon, b=50.35, le=11.17, size=40):
FreeCADGui.updateGui()
return FreeCAD.activeDocument().ActiveObject
-
-'''
-# original::
-def getElevation(lat, lon, b=50.35, le=11.17, size=40):
- tm.lat = lat
- tm.lon = lon
- baseheight = 0 #getheight(tm.lat, tm.lon)
- center = tm.fromGeographic(tm.lat, tm.lon)
-
- #https://maps.googleapis.com/maps/api/elevation/json?path=36.578581,-118.291994|36.23998,-116.83171&samples=3&key=YOUR_API_KEY
- #https://maps.googleapis.com/maps/api/elevation/json?locations=39.7391536,-104.9847034&key=YOUR_API_KEY
-
- source = "https://maps.googleapis.com/maps/api/elevation/json?path="
- source += str(b-size*0.001) + "," + str(le) + "|" + str(b+size*0.001) + "," + str(le)
- source += "&samples=" + str(100)
- source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
-
- response = urllib.request.urlopen(source)
- ans = response.read()
-
- # +# to do: error handling - wait and try again
- s = json.loads(ans)
- res = s['results']
-
- points = []
- for r in res:
- c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
- v = FreeCAD.Vector(
- round(c[0], 2),
- round(c[1], 2),
- round(r['elevation'] * 1000, 2) - baseheight
- )
- points.append(v)
-
- line = Draft.makeWire(points, closed=False, face=False, support=None)
- line.ViewObject.Visibility = False
- #FreeCAD.activeDocument().recompute()
- FreeCADGui.updateGui()
- return FreeCAD.activeDocument().ActiveObject
-'''
-
class _ImportPointsTaskPanel:
def __init__(self, obj = None):
diff --git a/PVPlantSite.py b/PVPlantSite.py
index 677ee56..dc177b4 100644
--- a/PVPlantSite.py
+++ b/PVPlantSite.py
@@ -578,22 +578,22 @@ class _PVPlantSite(ArchSite._Site):
obj.addProperty("App::PropertyLink",
"Boundary",
- "Site",
+ "PVPlant",
"Boundary of land")
obj.addProperty("App::PropertyLinkList",
"Frames",
- "Site",
+ "PVPlant",
"Frames templates")
obj.addProperty("App::PropertyEnumeration",
"UtmZone",
- "Base",
+ "PVPlant",
"UTM zone").UtmZone = zone_list
obj.addProperty("App::PropertyVector",
"Origin",
- "Base",
+ "PVPlant",
"Origin point.").Origin = (0, 0, 0)
def onDocumentRestored(self, obj):
@@ -771,10 +771,12 @@ class _PVPlantSite(ArchSite._Site):
import PVPlantImportGrid
x, y, zone_number, zone_letter = utm.from_latlon(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
- # self.obj.UtmZone = "Z"+str(zone_number)
- #z = PVPlantImportGrid.get_elevation(lat, lon)
- zz = PVPlantImportGrid.getSinglePointElevationFromBing(lat, lon)
- self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz.z)
+ zz = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
+ self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz[0].z)
+ #self.obj.OriginOffset = FreeCAD.Vector(x * 1000, y * 1000, 0) #??
+ self.obj.Latitude = lat
+ self.obj.Longitude = lon
+ self.obj.Elevation = zz[0].z
class _ViewProviderSite(ArchSite._ViewProviderSite):
diff --git a/Resources/webs/map.js b/Resources/webs/map.js
index 27c235b..0c39565 100644
--- a/Resources/webs/map.js
+++ b/Resources/webs/map.js
@@ -41,7 +41,7 @@ map.on('mousemove', function(e)
MyApp.onMapMove(e.latlng.lat, e.latlng.lng);
const bounds = map.getBounds();
- MyApp.onMapZoom(bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast());
+ MyApp.onMapZoom(bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast(), map.getZoom());
});
var DrawShapes;
diff --git a/lib/GoogleMapDownloader.py b/lib/GoogleMapDownloader.py
index e36075b..56eba05 100644
--- a/lib/GoogleMapDownloader.py
+++ b/lib/GoogleMapDownloader.py
@@ -7,7 +7,7 @@
# Find the associated blog post at: http://blog.eskriett.com/2013/07/19/downloading-google-maps/
import math
-# from PIL import Image
+from PIL import Image
import os
import urllib
@@ -15,7 +15,7 @@ import urllib
# alternativa a PIL: Image
# CV2
-class GoogleMapDownloader:
+class GoogleMapDownloader1:
"""
A class which generates high resolution google maps images given
a longitude, latitude and zoom level
diff --git a/lib/GoogleSatelitalImageDownload.py b/lib/GoogleSatelitalImageDownload.py
new file mode 100644
index 0000000..6fa2787
--- /dev/null
+++ b/lib/GoogleSatelitalImageDownload.py
@@ -0,0 +1,207 @@
+import math
+from PIL import Image
+import urllib.request
+from io import BytesIO
+import time
+
+
+class GoogleMapDownloader:
+ def __init__(self, zoom=12, layer='raw_satellite'):
+ self._zoom = zoom
+ self.layer_map = {
+ 'roadmap': 'm',
+ 'terrain': 'p',
+ 'satellite': 's',
+ 'hybrid': 'y',
+ 'raw_satellite': 's'
+ }
+ self._layer = self.layer_map.get(layer, 's')
+ self._style = 'style=feature:all|element:labels|visibility:off' if layer == 'raw_satellite' else ''
+
+ def latlng_to_tile(self, lat, lng):
+ """Convierte coordenadas a tiles X/Y con precisión decimal"""
+ tile_size = 256
+ numTiles = 1 << self._zoom
+
+ point_x = (tile_size / 2 + lng * tile_size / 360.0) * numTiles / tile_size
+ sin_y = math.sin(lat * (math.pi / 180.0))
+ point_y = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) *
+ -(tile_size / (2 * math.pi))) * numTiles / tile_size
+
+ return point_x, point_y
+
+ def generateImage(self, sw_lat, sw_lng, ne_lat, ne_lng):
+ """Genera la imagen para un área rectangular definida por coordenadas"""
+ # Convertir coordenadas a tiles con precisión decimal
+ sw_x, sw_y = self.latlng_to_tile(sw_lat, sw_lng)
+ ne_x, ne_y = self.latlng_to_tile(ne_lat, ne_lng)
+
+ # Asegurar que las coordenadas estén en el orden correcto
+ min_x = min(sw_x, ne_x)
+ max_x = max(sw_x, ne_x)
+ min_y = min(sw_y, ne_y)
+ max_y = max(sw_y, ne_y)
+
+ # Calcular los tiles mínimos y máximos necesarios
+ min_tile_x = math.floor(min_x)
+ max_tile_x = math.ceil(max_x)
+ min_tile_y = math.floor(min_y)
+ max_tile_y = math.ceil(max_y)
+
+ # Calcular dimensiones en tiles
+ tile_width = int(max_tile_x - min_tile_x) + 1
+ tile_height = int(max_tile_y - min_tile_y) + 1
+
+ # Crear imagen temporal para todos los tiles necesarios
+ full_img = Image.new('RGB', (tile_width * 256, tile_height * 256))
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
+ servers = ['mt0', 'mt1', 'mt2', 'mt3']
+
+ for x in range(min_tile_x, max_tile_x + 1):
+ for y in range(min_tile_y, max_tile_y + 1):
+ server = servers[(x + y) % len(servers)]
+ base_url = f"https://{server}.google.com/vt?lyrs={self._layer}&x={x}&y={y}&z={self._zoom}"
+ url = f"{base_url}&{self._style}" if self._style else base_url
+
+ try:
+ req = urllib.request.Request(url, headers=headers)
+ with urllib.request.urlopen(req) as response:
+ tile_data = response.read()
+
+ img = Image.open(BytesIO(tile_data))
+ pos_x = (x - min_tile_x) * 256
+ pos_y = (y - min_tile_y) * 256
+ full_img.paste(img, (pos_x, pos_y))
+ #print(f"✅ Tile ({x}, {y}) descargado")
+
+ except Exception as e:
+ #print(f"❌ Error en tile ({x},{y}): {str(e)}")
+ error_tile = Image.new('RGB', (256, 256), (255, 0, 0))
+ full_img.paste(error_tile, (pos_x, pos_y))
+
+ time.sleep(0.05)
+
+ # Calcular desplazamientos para recorte final
+ left_offset = int((min_x - min_tile_x) * 256)
+ right_offset = int((max_tile_x - max_x) * 256)
+ top_offset = int((min_y - min_tile_y) * 256)
+ bottom_offset = int((max_tile_y - max_y) * 256)
+
+ # Calcular coordenadas de recorte
+ left = left_offset
+ top = top_offset
+ right = full_img.width - right_offset
+ bottom = full_img.height - bottom_offset
+
+ # Asegurar que las coordenadas sean válidas
+ if right < left:
+ right = left + 1
+ if bottom < top:
+ bottom = top + 1
+ # Recortar la imagen al área exacta solicitada
+ result = full_img.crop((
+ left,
+ top,
+ right,
+ bottom
+ ))
+
+ return full_img
+
+
+class GoogleMapDownloader_1:
+ def __init__(self, zoom=12, layer='hybrid'):
+ """
+ Args:
+ zoom: Zoom level (0-23)
+ layer: Map type (roadmap, terrain, satellite, hybrid)
+ """
+ self._zoom = zoom
+ self.layer_map = {
+ 'roadmap': 'm',
+ 'terrain': 'p',
+ 'satellite': 's',
+ 'hybrid': 'y',
+ 'raw_satellite': 's' # Capa especial sin etiquetas
+ }
+ self._layer = self.layer_map.get(layer, 's')
+ self._style = 'style=feature:all|element:labels|visibility:off' if layer == 'raw_satellite' else ''
+
+ def latlng_to_tile(self, lat, lng):
+ """Convierte coordenadas a tiles X/Y"""
+ tile_size = 256
+ numTiles = 1 << self._zoom
+
+ # Cálculo para coordenada X
+ point_x = (tile_size / 2 + lng * tile_size / 360.0) * numTiles / tile_size
+
+ # Cálculo para coordenada Y
+ sin_y = math.sin(lat * (math.pi / 180.0))
+ point_y = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) *
+ -(tile_size / (2 * math.pi))) * numTiles / tile_size
+
+ return int(point_x), int(point_y)
+
+ def generateImage(self, sw_lat, sw_lng, ne_lat, ne_lng):
+ """
+ Genera la imagen para un área rectangular definida por:
+ - sw_lat, sw_lng: Esquina suroeste (latitud, longitud)
+ - ne_lat, ne_lng: Esquina noreste (latitud, longitud)
+ """
+ # Convertir coordenadas a tiles
+ sw_x, sw_y = self.latlng_to_tile(sw_lat, sw_lng)
+ ne_x, ne_y = self.latlng_to_tile(ne_lat, ne_lng)
+
+ # Determinar rango de tiles
+ min_x = min(sw_x, ne_x)
+ max_x = max(sw_x, ne_x)
+ min_y = min(sw_y, ne_y)
+ max_y = max(sw_y, ne_y)
+
+ # Calcular dimensiones en tiles
+ tile_width = max_x - min_x + 1
+ tile_height = max_y - min_y + 1
+
+ # Crear imagen final
+ result = Image.new('RGB', (256 * tile_width, 256 * tile_height))
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
+ servers = ['mt0', 'mt1', 'mt2', 'mt3']
+
+ print(f"Descargando {tile_width}x{tile_height} tiles ({tile_width * tile_height} total)")
+
+ for x in range(min_x, max_x + 1):
+ for y in range(min_y, max_y + 1):
+ # Seleccionar servidor rotatorio
+ server = servers[(x + y) % len(servers)]
+ # Construir URL con parámetro para quitar etiquetas si es necesario
+ url = f"https://{server}.google.com/vt?lyrs={self._layer}&x={x}&y={y}&z={self._zoom}"
+ if self._style:
+ url = f"{url}&{self._style}"
+
+ print("Descargando tile:", url)
+ try:
+ # Descargar tile
+ req = urllib.request.Request(url, headers=headers)
+ with urllib.request.urlopen(req) as response:
+ tile_data = response.read()
+
+ # Procesar en memoria
+ img = Image.open(BytesIO(tile_data))
+ pos_x = (x - min_x) * 256
+ pos_y = (y - min_y) * 256
+ result.paste(img, (pos_x, pos_y))
+
+ print(f"✅ Tile ({x}, {y}) descargado")
+
+ except Exception as e:
+ print(f"❌ Error en tile ({x},{y}): {str(e)}")
+ # Crear tile de error (rojo)
+ error_tile = Image.new('RGB', (256, 256), (255, 0, 0))
+ pos_x = (x - min_x) * 256
+ pos_y = (y - min_y) * 256
+ result.paste(error_tile, (pos_x, pos_y))
+
+ # Pausa para evitar bloqueos
+ time.sleep(0.05)
+
+ return result
\ No newline at end of file
diff --git a/package.xml b/package.xml
index b2cf71e..4adc7ac 100644
--- a/package.xml
+++ b/package.xml
@@ -2,8 +2,8 @@
PVPlant
FreeCAD Fotovoltaic Power Plant Toolkit
- 2025.02.22
- 2025.02.22
+ 2025.07.06
+ 2025.07.06
Javier Braña
LGPL-2.1-or-later
https://homehud.duckdns.org/javier/PVPlant