From 5a642a4119b44c5de0fb10a83c0d5bc27908c41a Mon Sep 17 00:00:00 2001 From: Javi Date: Sun, 6 Jul 2025 01:12:08 +0200 Subject: [PATCH] update --- Export/exportDXF.py | 1021 ++++++++++++++++++--------- Export/exportDXF.ui | 24 +- Export/exportPVSyst.py | 1 + Importer/importOSM.py | 185 ++++- PVPlantGeoreferencing.py | 181 ++++- PVPlantImportGrid.py | 60 +- PVPlantSite.py | 18 +- Resources/webs/map.js | 2 +- lib/GoogleMapDownloader.py | 4 +- lib/GoogleSatelitalImageDownload.py | 207 ++++++ package.xml | 4 +- 11 files changed, 1267 insertions(+), 440 deletions(-) create mode 100644 lib/GoogleSatelitalImageDownload.py 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