10 Commits

Author SHA1 Message Date
Javier Braña 57f85d0153 refactor: separar PVPlantGeoreferencing, PVPlantImportGrid y PVPlantPlacement en submodulos
- PVPlantGeoreferencing → PVPlant/core/georef.py
- PVPlantImportGrid → PVPlant/import_grid/grid.py
- PVPlantPlacement → PVPlant/placement/placement.py

Los archivos originales ahora son wrappers de compatibilidad.
Preparado para revisión exhaustiva de PVPlantPlacement.
2026-05-02 01:54:28 +02:00
Javier Braña d9b39ac17b refactor: migrar utm→pyproj, limpiar código muerto, reestructurar en PVPlant/core/ 2026-05-02 01:02:26 +02:00
javier 3bcdc95978 Actualizar package.xml 2026-04-30 00:51:53 +02:00
javier 4b7035e6be Corregir URL: homehud -> homehub en package.xml 2026-04-30 00:43:30 +02:00
javier 02758a6ee8 Actualizar package.xml 2026-03-24 22:10:39 +01:00
javier 111df89033 updates 2026-02-15 20:23:52 +01:00
javier 4476afc1a2 updates 2025-11-20 11:20:18 +01:00
javier d61260fdd3 updates 2025-11-20 00:57:15 +01:00
javier 049898c939 updates 2025-08-17 13:34:09 +04:00
javier 3a188cc47d new code 2025-08-17 13:33:17 +04:00
32 changed files with 6043 additions and 4136 deletions
+85 -282
View File
@@ -22,6 +22,7 @@
import FreeCAD
import ArchComponent
import Part
import os
import zipfile
import re
@@ -48,17 +49,18 @@ import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects")
vector = ["Y", "YN", "Z", "ZN", "D"]
def makePCS():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "StringInverter")
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PowerConversionSystem")
PowerConverter(obj)
ViewProviderStringInverter(obj.ViewObject)
ViewProviderPowerConverter(obj.ViewObject)
try:
folder = FreeCAD.ActiveDocument.StringInverters
folder = FreeCAD.ActiveDocument.PowerConversionSystemGroup
except:
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'StringInverters')
folder.Label = "StringInverters"
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'PowerConversionSystemGroup')
folder.Label = "PowerConversionSystemGroup"
folder.addObject(obj)
return obj
@@ -67,9 +69,6 @@ class PowerConverter(ArchComponent.Component):
def __init__(self, obj):
''' Initialize the Area object '''
ArchComponent.Component.__init__(self, obj)
self.oldMPPTs = 0
self.Type = None
self.obj = None
self.setProperties(obj)
@@ -77,36 +76,69 @@ class PowerConverter(ArchComponent.Component):
def setProperties(self, obj):
pl = obj.PropertiesList
if not "File" in pl:
obj.addProperty("App::PropertyFile",
"File",
"Inverter",
"The base file this component is built upon")
if not ("MPPTs" in pl):
obj.addProperty("App::PropertyQuantity",
"MPPTs",
"Inverter",
"Points that define the area"
).MPPTs = 0
if not ("Generator" in pl):
# Transformer properties
if not "Technology" in pl:
obj.addProperty("App::PropertyEnumeration",
"Generator",
"Inverter",
"Points that define the area"
).Generator = ["Generic", "Library"]
obj.Generator = "Generic"
"Technology",
"Transformer",
"Number of phases and type of transformer"
).Technology = ["Single Phase Transformer", "Three Phase Transformer"]
if not ("Type" in pl):
obj.addProperty("App::PropertyString",
"Type",
"Base",
"Points that define the area"
).Type = "PowerConverter"
obj.setEditorMode("Type", 1)
if not "PowerPrimary" in pl:
obj.addProperty("App::PropertyPower",
"PowerPrimary",
"Transformer",
"The base file this component is built upon").PowerPrimary = 6000000000
self.Type = obj.Type
if not "PowerSecundary1" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary1",
"Transformer",
"The base file this component is built upon").PowerSecundary1 = 3000000000
if not "PowerSecundary2" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary2",
"Transformer",
"The base file this component is built upon").PowerSecundary2 = 3000000000
if not "VoltagePrimary" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltagePrimary",
"Transformer",
"The base file this component is built upon").VoltagePrimary = 33000000000
if not "VoltageSecundary1" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary1",
"Transformer",
"The base file this component is built upon").VoltageSecundary1 = 11000000000
if not "VoltageSecundary2" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary2",
"Transformer",
"The base file this component is built upon").VoltageSecundary2 = 11000000000
if not "VectorPrimary" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorPrimary",
"Transformer",
"The base file this component is built upon").VectorPrimary = vector
if not "VectorSecundary1" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary1",
"Transformer",
"The base file this component is built upon").VectorSecundary1 = vector
if not "VectorSecundary2" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary2",
"Transformer",
"The base file this component is built upon").VectorSecundary2 = vector
self.Type = "PowerConverter"
obj.Proxy = self
def onDocumentRestored(self, obj):
@@ -114,263 +146,34 @@ class PowerConverter(ArchComponent.Component):
self.setProperties(obj)
def onBeforeChange(self, obj, prop):
if prop == "MPPTs":
self.oldMPPTs = int(obj.MPPTs)
''' '''
# This method is called before a property is changed.
# It can be used to validate the property value or to update other properties.
# If the property is not valid, you can raise an exception.
# If you want to prevent the change, you can return False.
# Otherwise, return True to allow the change.
return True
def onChanged(self, obj, prop):
''' '''
if prop == "Generator":
if obj.Generator == "Generic":
obj.setEditorMode("MPPTs", 0)
else:
obj.setEditorMode("MPPTs", 1)
if prop == "MPPTs":
''' '''
if self.oldMPPTs > obj.MPPTs:
''' borrar sobrantes '''
obj.removeProperty()
elif self.oldMPPTs < obj.MPPTs:
''' crear los faltantes '''
for i in range(self.oldMPPTs, int(obj.MPPTs)):
''' '''
print(i)
else:
pass
if (prop == "File") and obj.File:
''' '''
def execute(self, obj):
''' '''
# obj.Shape: compound
# |- body: compound
# |-- inverter: solid
# |-- door: solid
# |-- holder: solid
# |- connectors: compound
# |-- DC: compound
# |--- MPPT 1..x: compound
# |---- positive: compound
# |----- connector 1..y: ??
# |---- negative 1..y: compound
# |----- connector 1..y: ??
# |-- AC: compound
# |--- R,S,T,: ??
# |-- Communication
# |- transformer: solid
# |- primary switchgear: compound
# |- secundary 1 switchgear: compound
# |- secundary 2 switchgear: compound
pl = obj.Placement
filename = self.getFile(obj)
if filename:
parts = self.getPartsList(obj)
if parts:
zdoc = zipfile.ZipFile(filename)
if zdoc:
f = zdoc.open(parts[list(parts.keys())[-1]][1])
shapedata = f.read()
f.close()
shapedata = shapedata.decode("utf8")
shape = self.cleanShape(shapedata, obj, parts[list(parts.keys())[-1]][2])
obj.Shape = shape
if not pl.isIdentity():
obj.Placement = pl
obj.MPPTs = len(shape.SubShapes[1].SubShapes[0].SubShapes)
obj.Shape = Part.makeBox(6058, 2438, 2591) # Placeholder for the shape
obj.Placement = pl
def cleanShape(self, shapedata, obj, materials):
"cleans the imported shape"
import Part
shape = Part.Shape()
shape.importBrepFromString(shapedata)
'''if obj.FuseArch and materials:
# separate lone edges
shapes = []
for edge in shape.Edges:
found = False
for solid in shape.Solids:
for soledge in solid.Edges:
if edge.hashCode() == soledge.hashCode():
found = True
break
if found:
break
if found:
break
else:
shapes.append(edge)
print("solids:",len(shape.Solids),"mattable:",materials)
for key,solindexes in materials.items():
if key == "Undefined":
# do not join objects with no defined material
for solindex in [int(i) for i in solindexes.split(",")]:
shapes.append(shape.Solids[solindex])
else:
fusion = None
for solindex in [int(i) for i in solindexes.split(",")]:
if not fusion:
fusion = shape.Solids[solindex]
else:
fusion = fusion.fuse(shape.Solids[solindex])
if fusion:
shapes.append(fusion)
shape = Part.makeCompound(shapes)
try:
shape = shape.removeSplitter()
except Exception:
print(obj.Label,": error removing splitter")'''
return shape
def getFile(self, obj, filename=None):
"gets a valid file, if possible"
if not filename:
filename = obj.File
if not filename:
return None
if not filename.lower().endswith(".fcstd"):
return None
if not os.path.exists(filename):
# search for the file in the current directory if not found
basename = os.path.basename(filename)
currentdir = os.path.dirname(obj.Document.FileName)
altfile = os.path.join(currentdir,basename)
if altfile == obj.Document.FileName:
return None
elif os.path.exists(altfile):
return altfile
else:
# search for subpaths in current folder
altfile = None
subdirs = self.splitall(os.path.dirname(filename))
for i in range(len(subdirs)):
subpath = [currentdir]+subdirs[-i:]+[basename]
altfile = os.path.join(*subpath)
if os.path.exists(altfile):
return altfile
return None
return filename
def getPartsList(self, obj, filename=None):
"returns a list of Part-based objects in a FCStd file"
parts = {}
materials = {}
filename = self.getFile(obj,filename)
if not filename:
return parts
zdoc = zipfile.ZipFile(filename)
with zdoc.open("Document.xml") as docf:
name = None
label = None
part = None
materials = {}
writemode = False
for line in docf:
line = line.decode("utf8")
if "<Object name=" in line:
n = re.findall('name=\"(.*?)\"',line)
if n:
name = n[0]
elif "<Property name=\"Label\"" in line:
writemode = True
elif writemode and "<String value=" in line:
n = re.findall('value=\"(.*?)\"',line)
if n:
label = n[0]
writemode = False
elif "<Property name=\"Shape\" type=\"Part::PropertyPartShape\"" in line:
writemode = True
elif writemode and "<Part file=" in line:
n = re.findall('file=\"(.*?)\"',line)
if n:
part = n[0]
writemode = False
elif "<Property name=\"MaterialsTable\" type=\"App::PropertyMap\"" in line:
writemode = True
elif writemode and "<Item key=" in line:
n = re.findall('key=\"(.*?)\"',line)
v = re.findall('value=\"(.*?)\"',line)
if n and v:
materials[n[0]] = v[0]
elif writemode and "</Map>" in line:
writemode = False
elif "</Object>" in line:
if name and label and part:
parts[name] = [label,part,materials]
name = None
label = None
part = None
materials = {}
writemode = False
return parts
def getColors(self,obj):
"returns the DiffuseColor of the referenced object"
filename = self.getFile(obj)
if not filename:
return None
part = obj.Part
if not obj.Part:
return None
zdoc = zipfile.ZipFile(filename)
if not "GuiDocument.xml" in zdoc.namelist():
return None
colorfile = None
with zdoc.open("GuiDocument.xml") as docf:
writemode1 = False
writemode2 = False
for line in docf:
line = line.decode("utf8")
if ("<ViewProvider name=" in line) and (part in line):
writemode1 = True
elif writemode1 and ("<Property name=\"DiffuseColor\"" in line):
writemode1 = False
writemode2 = True
elif writemode2 and ("<ColorList file=" in line):
n = re.findall('file=\"(.*?)\"',line)
if n:
colorfile = n[0]
break
if not colorfile:
return None
if not colorfile in zdoc.namelist():
return None
colors = []
cf = zdoc.open(colorfile)
buf = cf.read()
cf.close()
for i in range(1,int(len(buf)/4)):
colors.append((buf[i*4+3]/255.0,buf[i*4+2]/255.0,buf[i*4+1]/255.0,buf[i*4]/255.0))
if colors:
return colors
return None
def splitall(self,path):
"splits a path between its components"
allparts = []
while 1:
parts = os.path.split(path)
if parts[0] == path: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == path: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
path = parts[0]
allparts.insert(0, parts[1])
return allparts
class ViewProviderStringInverter(ArchComponent.ViewProviderComponent):
class ViewProviderPowerConverter(ArchComponent.ViewProviderComponent):
def __init__(self, vobj):
ArchComponent.ViewProviderComponent.__init__(self, vobj)
@@ -381,12 +184,12 @@ class CommandPowerConverter:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg")),
'Accel': "E, I",
'MenuText': "String Inverter",
'ToolTip': "String Placement",}
'Accel': "E, P",
'MenuText': "Power Converter",
'ToolTip': "Power Converter",}
def Activated(self):
sinverter = makeStringInverter()
sinverter = makePCS()
def IsActive(self):
active = not (FreeCAD.ActiveDocument is None)
+27 -22
View File
@@ -141,29 +141,31 @@ def groupTrackersToTransformers(transformer_power, max_distance):
for i, group in enumerate(transformer_groups):
# Crear la esfera que representará el CT
ct_sphere = FreeCAD.ActiveDocument.addObject("Part::Sphere", f"CT_{i + 1}")
ct_sphere.Radius = 5000 # 2m de radio
ct_sphere.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)
ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
ct_shape.Length = 6058
ct_shape.Width = 2438
ct_shape.Height = 2591
ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)
# Añadir propiedades personalizadas
ct_sphere.addProperty("App::PropertyLinkList", "Trackers", "CT",
ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
"Lista de trackers asociados a este CT")
ct_sphere.addProperty("App::PropertyFloat", "TotalPower", "CT",
ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
"Potencia total del grupo (W)")
ct_sphere.addProperty("App::PropertyFloat", "NominalPower", "CT",
ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
"Potencia nominal del transformador (W)")
ct_sphere.addProperty("App::PropertyFloat", "Utilization", "CT",
ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
"Porcentaje de utilización (Total/Nominal)")
# Establecer valores de las propiedades
ct_sphere.Trackers = group['trackers']
ct_sphere.TotalPower = group['total_power'].Value
ct_sphere.NominalPower = transformer_power
ct_sphere.Utilization = (group['total_power'].Value / transformer_power) * 100
ct_shape.Trackers = group['trackers']
ct_shape.TotalPower = group['total_power'].Value
ct_shape.NominalPower = transformer_power
ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100
# Configurar visualización
# Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
utilization = ct_sphere.Utilization
utilization = ct_shape.Utilization
if utilization <= 100:
color = (0.0, 1.0, 0.0) # Verde
elif utilization <= 110:
@@ -171,18 +173,19 @@ def groupTrackersToTransformers(transformer_power, max_distance):
else:
color = (1.0, 0.0, 0.0) # Rojo
ct_sphere.ViewObject.ShapeColor = color
ct_sphere.ViewObject.Transparency = 40 # 40% de transparencia
ct_shape.ViewObject.ShapeColor = color
ct_shape.ViewObject.Transparency = 40 # 40% de transparencia
# Añadir etiqueta con información
ct_sphere.ViewObject.DisplayMode = "Shaded"
ct_sphere.Label = f"CT {i + 1} ({ct_sphere.TotalPower / 1000:.1f}kW/{ct_sphere.NominalPower / 1000:.1f}kW)"
ct_shape.ViewObject.DisplayMode = "Shaded"
ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"
# Añadir al grupo principal
transformer_group.addObject(ct_sphere)
transformer_group.addObject(ct_shape)
FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")
onSelectGatePoint()
#onSelectGatePoint()
import FreeCAD, FreeCADGui, Part
@@ -195,7 +198,7 @@ class InternalPathCreator:
self.gate_point = gate_point
self.strategy = strategy
self.path_width = path_width
self.ct_spheres = []
self.ct_shapes = []
self.ct_positions = []
def get_transformers(self):
@@ -204,13 +207,13 @@ class InternalPathCreator:
FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
return False
self.ct_spheres = transformers_group.Group
if not self.ct_spheres:
self.ct_shapes = transformers_group.Group
if not self.ct_shapes:
FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
return False
# Obtener las posiciones de los CTs
for sphere in self.ct_spheres:
for sphere in self.ct_shapes:
base = sphere.Placement.Base
self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
return True
@@ -263,6 +266,8 @@ class InternalPathCreator:
y_proj = slope * x_proj + intercept
return FreeCAD.Vector(x_proj, y_proj, 0)
# return slope * x + intercept --> desde placement
projected_points = [project_point(p) for p in all_points]
# Calcular distancias a lo largo de la línea
+10 -3
View File
@@ -76,8 +76,10 @@ def getWire(wire, nospline=False, width=.0):
import DraftGeomUtils
import math
offset = FreeCAD.ActiveDocument.Site.Origin
def fmt(vec, b=0.0):
return (vec.x * 0.001, vec.y * 0.001, width, width, b)
return ((vec.x + offset.x) * 0.001, (vec.y + offset.y) * 0.001, width, width, b)
points = []
edges = Part.__sortEdges__(wire.Edges)
@@ -626,6 +628,7 @@ layers = [
("Available area Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Exclusion", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Offset", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Name", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Cadastral Plot", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Cadastral Plot Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
@@ -646,7 +649,7 @@ layers = [
("CIVIL External Roads Text", QtGui.QColor(255, 255, 192), "Continuous", "1", True),
("CIVIL Internal Roads", QtGui.QColor(153, 95, 76), "Continuous", "1", True),
("CIVIL Internal Roads Axis", QtGui.QColor(192, 192, 192), "Dashed", "1", True),
("CIVIL External Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True),
("CIVIL Internal Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True),
("Contour Line Legend text", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
@@ -893,6 +896,9 @@ class _PVPlantExportDXF(QtGui.QWidget):
angle=0,
layer=area_type[1]
)
for obj in FreeCADGui.Selection.getSelection():
tmp = exporter.createPolyline(obj, areas_types[0][1])
def writeFrameSetups(self, exporter):
if not hasattr(FreeCAD.ActiveDocument, "Site"):
@@ -965,7 +971,8 @@ class _PVPlantExportDXF(QtGui.QWidget):
if FreeCAD.ActiveDocument.Transport:
for road in FreeCAD.ActiveDocument.Transport.Group:
base = exporter.createPolyline(road, "CIVIL External Roads")
base.dxf.const_width = road.Width
if hasattr(road, 'Width'):
base.dxf.const_width = road.Width
axis = exporter.createPolyline(road, "CIVIL External Roads Axis")
axis.dxf.const_width = .2
+323 -27
View File
@@ -29,6 +29,15 @@ import Part
import numpy
import os
from xml.etree.ElementTree import Element, SubElement
import xml.etree.ElementTree as ElementTree
import datetime
from xml.dom import minidom
from numpy.matrixlib.defmatrix import matrix
from Utils import PVPlantUtils as utils
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore
@@ -63,6 +72,11 @@ def check_collada():
FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n")
return COLLADA_AVAILABLE
# Asegurar que el texto es Unicode válido
def safe_text(text):
if isinstance(text, bytes):
return text.decode('utf-8', errors='replace')
return text
# from ARCH:
def triangulate(shape):
@@ -249,7 +263,306 @@ def export(exportList, filename, tessellation=1, colors=None):
FreeCAD.Console.PrintMessage(translate("Arch", "file %s successfully created.") % filename)
def exportToPVC(path, exportTerrain = False):
def exportToPVC(path, exportTerrain=False):
filename = f"{path}.pvc"
# 1. Validación inicial de objetos esenciales
site = None
for obj in FreeCAD.ActiveDocument.Objects:
if obj.Name.startswith("Site") and hasattr(obj, 'Terrain'):
site = obj
break
if not site:
FreeCAD.Console.PrintError("No se encontró objeto 'Site' válido\n")
return False
# 2. Configuración de metadatos y autor
generated_on = str(datetime.datetime.now())
try:
author = FreeCAD.ActiveDocument.CreatedBy
except (AttributeError, UnicodeEncodeError):
author = "Unknown"
author = author.replace("<", "").replace(">", "")
ver = FreeCAD.Version()
appli = f"PVPlant for FreeCAD {ver[0]}.{ver[1]} build{ver[2]}"
# 3. Creación estructura XML base
root = Element('COLLADA')
root.set('xmlns', 'http://www.collada.org/2005/11/COLLADASchema')
root.set('version', '1.4.1')
# 4. Sección <asset>
asset = SubElement(root, 'asset')
contrib = SubElement(asset, 'contributor')
SubElement(contrib, 'author').text = safe_text(author)
SubElement(contrib, 'authoring_tool').text = safe_text(appli)
SubElement(asset, 'created').text = generated_on
SubElement(asset, 'modified').text = generated_on
SubElement(asset, 'title').text = safe_text(FreeCAD.ActiveDocument.Name)
unit = SubElement(asset, 'unit')
unit.set('name', 'meter')
unit.set('meter', '1')
# 5. Materiales y efectos
materials = ["Frames", "Tree_trunk", "Tree_crown", "Topography_mesh"]
# Library materials
lib_materials = SubElement(root, 'library_materials')
for i, name in enumerate(materials):
mat = SubElement(lib_materials, 'material')
mat.set('id', f'Material{i}')
mat.set('name', name)
SubElement(mat, 'instance_effect').set('url', f'#Material{i}-fx')
# Library effects
lib_effects = SubElement(root, 'library_effects')
for i, _ in enumerate(materials):
effect = SubElement(lib_effects, 'effect')
effect.set('id', f'Material{i}-fx')
effect.set('name', f'Material{i}')
profile = SubElement(effect, 'profile_COMMON')
technique = SubElement(profile, 'technique')
technique.set('sid', 'standard')
lambert = SubElement(technique, 'lambert')
# Componentes del material
color = SubElement(SubElement(lambert, 'emission'), 'color')
color.set('sid', 'emission')
color.text = '0.000000 0.000000 0.000000 1.000000'
color = SubElement(SubElement(lambert, 'ambient'), 'color')
color.set('sid', 'ambient')
color.text = '0.200000 0.200000 0.200000 1.000000'
color = SubElement(SubElement(lambert, 'diffuse'), 'color')
color.set('sid', 'diffuse')
color.text = '0.250000 0.500000 0.000000 1.000000'
transparent = SubElement(lambert, 'transparent')
transparent.set('opaque', 'RGB_ZERO')
color = SubElement(transparent, 'color')
color.set('sid', 'transparent')
color.text = '0.000000 0.000000 0.000000 1.000000'
value = SubElement(SubElement(lambert, 'transparency'), 'float')
value.set('sid', 'transparency')
value.text = '0.000000'
# 6. Geometrías
lib_geometries = SubElement(root, 'library_geometries')
# 7. Escena visual
lib_visual = SubElement(root, 'library_visual_scenes')
visual_scene = SubElement(lib_visual, 'visual_scene')
visual_scene.set('id', 'Scene') # cambiar a visual_scene_0
visual_scene.set('name', 'Scene') # cambiar a Default visual scene
scene_node = SubElement(visual_scene, 'node')
scene_node.set('id', 'node_0_id')
scene_node.set('name', 'node_0_name')
scene_node.set('sid', 'node_0_sid')
scene_matrix = SubElement(scene_node, 'matrix')
scene_matrix.set('sid', 'matrix_0')
scene_matrix.text = '1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 -1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000'
root_node = SubElement(scene_node, 'node')
root_node.set('id', 'node_1_id')
root_node.set('name', 'node_1_name')
root_node.set('sid', 'node_1_sid')
# 8. Función para procesar geometrías
def create_geometry(name, vindex, findex, material_id, objind=0, frame_data=None, isTracker = False, axis_line=None):
"""Crea elementos COLLADA para una geometría"""
# Source (vertices)
source_mesh = SubElement(geom, 'mesh')
source = SubElement(source_mesh, 'source')
source.set('id', f'{name}-mesh_source')
float_array = SubElement(source, 'float_array')
float_array.set('id', f'{name}-float_array')
float_array.set('count', str(len(vindex)))
float_array.text = ' '.join(f'{v:.6f}' for v in vindex)
technique = SubElement(source, 'technique_common')
accessor = SubElement(technique, 'accessor')
accessor.set('count', str(len(vindex)))
accessor.set('source', f'#{name}-float_array')
accessor.set('stride', '3')
for ax in ['X', 'Y', 'Z']:
param = SubElement(accessor, 'param')
param.set('name', ax)
param.set('type', 'float')
# Vertices
vertices = SubElement(source_mesh, 'vertices')
vertices.set('id', f'{name}-vertices_source')
vertices = SubElement(vertices, 'input')
vertices.set('semantic', 'POSITION')
vertices.set('source', f'#{name}-mesh_source')
# Triangles
triangles = SubElement(source_mesh, 'triangles')
triangles.set('count', '0')
triangles.set('material', f'Material{material_id}')
triangles_input = SubElement(triangles, 'input')
triangles_input.set('offset', '0')
triangles_input.set('semantic', 'VERTEX')
triangles_input.set('source', f'#{name}-vertices_source')
p = SubElement(triangles, 'p')
p.text = ' '.join(map(str, findex))
# Parámetros especiales para estructuras
frame_params = SubElement(source_mesh, 'tracker_parameters')
if frame_data:
for key, val in frame_data.items():
elem = SubElement(frame_params, key)
elem.text = str(val)
if isTracker:
axis_parameter = SubElement(frame_params, 'axis_vertices')
if axis_line:
for idx, vert in enumerate(axis_line):
array = SubElement(axis_parameter, 'float_array')
array.set('id', f'{name}-axis_float_array{idx}')
array.set('count', '3')
array.text = ' '.join(f'{v:.6f}' for v in vert)
# 9. Procesar estructuras (frames/trackers)
center = FreeCAD.Vector()
if site.Terrain:
center = site.Terrain.Mesh.BoundBox.Center
objind = 0
for frame_type in site.Frames:
is_tracker = "tracker" in frame_type.Proxy.Type.lower()
modules = frame_type.Shape.SubShapes[0].SubShapes[0]
pts = []
for i in range(4):
pts.append(modules.BoundBox.getPoint(i))
new_shape = Part.Face(Part.makePolygon(pts))
mesh = Mesh.Mesh(triangulate(new_shape))
axis = Part.makeLine(FreeCAD.Vector(modules.BoundBox.XMin, 0, modules.BoundBox.ZMax),
FreeCAD.Vector(modules.BoundBox.XMax, 0, modules.BoundBox.ZMax))
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Setup") and obj.Setup == frame_type:
# Procesar geometría
mesh.Placement = obj.getGlobalPlacement()
axis.Placement = obj.getGlobalPlacement()
# Transformar vértices
vindex = []
for point in mesh.Points:
adjusted = (point.Vector - center) * scale
vindex.extend([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Índices de caras
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
# AXIS
# TODO: revisar si es así:
vaxis = []
for vert in axis.Vertexes:
adjusted = (vert.Point - center) * scale
vaxis.append([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Crear geometría COLLADA
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', f'Frame_{objind}')
# Parámetros específicos de estructura
frame_data = {
'module_width': obj.Setup.ModuleWidth.Value,
'module_height': obj.Setup.ModuleHeight.Value,
'module_x_spacing': obj.Setup.ModuleColGap.Value,
'module_y_spacing': obj.Setup.ModuleRowGap.Value,
'module_name': 'Generic'
}
if is_tracker:
frame_data.update({
'tracker_type': 'single_axis_trackers',
'min_phi': obj.Setup.MinPhi.Value,
'max_phi': obj.Setup.MaxPhi.Value,
'min_theta': 0,
'max_theta': 0
})
create_geometry(
name=f'Frame_{objind}',
vindex=vindex,
findex=findex,
material_id=0,
objind=objind,
frame_data=frame_data,
isTracker = is_tracker,
axis_line=vaxis
)
# Instancia en escena
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', f'#Frame_{objind}')
bind_material = SubElement(instance, 'bind_material')
technique_common = SubElement(bind_material, 'technique_common')
instance_material = SubElement(technique_common, 'instance_material')
instance_material.set('symbol', 'Material0')
instance_material.set('target', '#Material0')
objind += 1
# 10. Procesar terreno si está habilitado
if exportTerrain and site.Terrain:
mesh = site.Terrain.Mesh
vindex = []
for point in mesh.Points:
point = point.Vector
vindex.extend([
-point.x * SCALE,
point.z * SCALE,
point.y * SCALE
])
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', 'Terrain')
create_geometry('Terrain', vindex, findex, material_id=3)
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', '#Terrain')
# 11. Escena principal
scene = SubElement(root, 'scene')
SubElement(scene, 'instance_visual_scene').set('url', '#Scene')
# 12. Exportar a archivo
xml_str = minidom.parseString(
ElementTree.tostring(root, encoding='utf-8')
).toprettyxml(indent=" ")
with open(filename, 'w', encoding='utf-8') as f:
f.write(xml_str)
FreeCAD.Console.PrintMessage(f"Archivo PVC generado: {filename}\n")
return True
def exportToPVC_old(path, exportTerrain = False):
filename = f"{path}.pvc"
from xml.etree.ElementTree import Element, SubElement
@@ -291,17 +604,18 @@ def exportToPVC(path, exportTerrain = False):
# xml: 1. Asset:
asset = SubElement(root, 'asset')
asset_contributor = SubElement(asset, 'contributor')
asset_contributor_autor = SubElement(asset_contributor, 'autor')
#asset_contributor_autor.text = author
asset_contributor_autor = SubElement(asset_contributor, 'author')
asset_contributor_autor.text = author
asset_contributor_authoring_tool = SubElement(asset_contributor, 'authoring_tool')
#asset_contributor_authoring_tool.text = appli
asset_contributor_authoring_tool.text = appli
asset_contributor_comments = SubElement(asset_contributor, 'comments')
asset_keywords = SubElement(asset, 'keywords')
asset_revision = SubElement(asset, 'revision')
asset_subject = SubElement(asset, 'subject')
asset_tittle = SubElement(asset, 'title')
#asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_unit = SubElement(asset, 'unit')
asset_unit.set('meter', '0.001')
asset_unit.set('name', 'millimeter')
@@ -359,7 +673,6 @@ def exportToPVC(path, exportTerrain = False):
# xml: 4. library_geometries:
library_geometries = SubElement(root, 'library_geometries')
def add_geometry(objtype, vindex, findex, objind = 0, centers = None):
isFrame = False
if objtype == 0:
geometryName = 'Frame'
@@ -505,36 +818,20 @@ def exportToPVC(path, exportTerrain = False):
end_time.text = '1.000000'
# xml: 6. scene:
scene = SubElement(root, 'scene')
'''scene = SubElement(root, 'scene')
instance = SubElement(scene, 'instance_visual_scene')
instance.set('url', '#')
full_list_of_objects = FreeCAD.ActiveDocument.Objects
instance.set('url', '#')'''
# CASO 1 - FRAMES:
frameType = site.Frames
frame_setup = {"type": [],
"footprint": []}
for obj in frameType:
frame_setup["type"] = obj
frame_setup["footprint"] = ""
objind = 0
# TODO: revisar
for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower()
isTracker = False
objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
tmp = []
for obj in objectlist:
if obj.Name.startswith("TrackerSetup"):
continue
else:
tmp.append(obj)
objectlist = tmp.copy()
#isTracker = False
objectlist = utils.findObjects("Tracker")
for obj in objectlist:
if obj.Setup == typ:
findex = numpy.array([])
@@ -580,7 +877,6 @@ def exportToPVC(path, exportTerrain = False):
v = Topology[0][i]
vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale,
(v.y - center.y) * scale)
# 2. face indices
findex = numpy.empty(len(Topology[1]) * 3, numpy.int64)
for i in range(len(Topology[1])):
+149 -46
View File
@@ -7,7 +7,6 @@ import ssl
import certifi
import urllib.request
import math
import utm
from collections import defaultdict
import PVPlantImportGrid as ImportElevation
@@ -43,53 +42,74 @@ class OSMImporter:
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transform_from_latlon(self, coordinates):
"""Transforma coordenadas lat/lon a coordenadas FreeCAD"""
if not coordinates:
return []
points = ImportElevation.getElevationFromOE(coordinates)
pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points]
return pts
def get_osm_data(self, bbox):
query = f"""
[out:xml][bbox:{bbox}];
(
way["building"];
way["highway"];
way["railway"];
way["power"="line"];
way["power"="substation"];
way["natural"="water"];
way["landuse"="forest"];
node["natural"="tree"];
);
(._;>;);
out body;
"""
req = urllib.request.Request(
self.overpass_url,
data=query.encode('utf-8'),
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
method='POST'
)
return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read()
""" Obtiene datos de OpenStreetMap """
# Modificar la consulta en get_osm_data para incluir más tipos de agua:
query = f"""[out:xml][bbox:{bbox}];
(
way["building"];
way["highway"];
way["railway"];
way["power"="line"];
way["power"="substation"];
way["natural"="water"];
way["waterway"];
way["waterway"="river"];
way["waterway"="stream"];
way["waterway"="canal"];
way["landuse"="basin"];
way["landuse"="reservoir"];
node["natural"="tree"];
way["landuse"="forest"];
way["landuse"="farmland"];
);
(._;>;);
out body;
"""
try:
req = urllib.request.Request(
self.overpass_url,
data=query.encode('utf-8'),
#headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
method='POST'
)
response = urllib.request.urlopen(req, context=self.ssl_context, timeout=160)
return response.read()
except Exception as e:
print(f"Error obteniendo datos OSM: {str(e)}")
return None
def create_layer(self, name):
"""Crea o obtiene una capa en el documento"""
if not FreeCAD.ActiveDocument.getObject(name):
return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
return FreeCAD.ActiveDocument.getObject(name)
def process_osm_data(self, osm_data):
"""Procesa los datos XML de OSM"""
if not osm_data:
print("No hay datos OSM para procesar")
return
root = ET.fromstring(osm_data)
# Primero, recolectar todos los nodos
print(f"Procesando {len(root.findall('node'))} nodos...")
# Almacenar nodos transformados
coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')]
coordinates = self.transform_from_latlon(coordinates)
for i, node in enumerate(root.findall('node')):
self. nodes[node.attrib['id']] = coordinates[i]
'''return
for node in root.findall('node'):
self.nodes[node.attrib['id']] = self.transform_from_latlon(
float(node.attrib['lat']),
float(node.attrib['lon'])
)'''
self.nodes[node.attrib['id']] = coordinates[i]
# Procesar ways
for way in root.findall('way'):
@@ -166,7 +186,7 @@ class OSMImporter:
def create_buildings(self):
building_layer = self.create_layer("Buildings")
for way_id, data in self.ways_data.items():
print(data)
#print(data)
if 'building' not in data['tags']:
continue
@@ -226,11 +246,11 @@ class OSMImporter:
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if 'power' in tags:
print("\n\n")
print(tags)
#print("\n\n")
#print(tags)
feature_type = tags['power']
if feature_type == 'line':
print("3.1. Create Power Lines")
#print("3.1. Create Power Lines")
FreeCADGui.updateGui()
self.create_power_line(
nodes=nodes,
@@ -239,7 +259,7 @@ class OSMImporter:
)
elif feature_type == 'substation':
print("3.1. Create substations")
#print("3.1. Create substations")
FreeCADGui.updateGui()
self.create_substation(
way_id=way_id,
@@ -249,7 +269,7 @@ class OSMImporter:
)
elif feature_type == 'tower':
print("3.1. Create power towers")
#print("3.1. Create power towers")
FreeCADGui.updateGui()
self.create_power_tower(
position=nodes[0] if nodes else None,
@@ -562,13 +582,15 @@ class OSMImporter:
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
# 3. Base del terreno
base_height = 0.3
try:
base_shape = Part.makePolygon(polygon_points)
base_face = Part.Face(base_shape)
base_extrude = base_face.extrude(FreeCAD.Vector(0, 0, base_height))
base_obj = layer.addObject("Part::Feature", f"{name}_Base")
base_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Base")
layer.addObject(base_obj)
base_obj.Shape = base_extrude
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
except Exception as e:
@@ -583,7 +605,8 @@ class OSMImporter:
fence_shape = Part.makePolygon(fence_points)
fence_face = Part.Face(fence_shape)
fence_extrude = fence_face.extrude(FreeCAD.Vector(0, 0, 2.8))
fence_obj = layer.addObject("Part::Feature", f"{name}_Fence")
fence_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Fence")
layer.addObject(fence_obj)
fence_obj.Shape = fence_extrude
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
except Exception as e:
@@ -599,14 +622,15 @@ class OSMImporter:
building_shape = Part.makePolygon(building_points)
building_face = Part.Face(building_shape)
building_extrude = building_face.extrude(FreeCAD.Vector(0, 0, building_height))
building_obj = layer.addObject("Part::Feature", f"{name}_Building")
building_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Building")
layer.addObject(building_obj)
building_obj.Shape = building_extrude
building_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error edificio {way_id}: {str(e)}\n")
# 6. Transformadores
try:
'''try:
num_transformers = int(tags.get('transformers', 1))
for i in range(num_transformers):
transformer_pos = self.calculate_equipment_position(
@@ -618,11 +642,11 @@ class OSMImporter:
transformer = self.create_transformer(
position=transformer_pos,
voltage=voltage,
tech_type=tags.get('substation:type', 'outdoor')
technology=tags.get('substation:type', 'outdoor')
)
layer.addObject(transformer)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")'''
# 7. Torre de seccionamiento para alta tensión
if substation_type == 'transmission' and voltage >= 110000:
@@ -637,7 +661,8 @@ class OSMImporter:
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
# 8. Propiedades técnicas
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
substation_data = FreeCAD.ActiveDocument.addObject("App::FeaturePython", f"{name}_Data")
layer.addObject(substation_data)
props = {
"Voltage": voltage,
"Type": substation_type,
@@ -651,7 +676,8 @@ class OSMImporter:
else:
substation_data.addProperty(
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
prop, "Technical").setValue(value)
prop, "Technical")
setattr(substation_data, prop, value)
except Exception as e:
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
@@ -900,9 +926,9 @@ class OSMImporter:
if face.isInside(rand_point, 0.1, True):
return rand_point
def create_water_bodies(self):
def create_water_bodies_old(self):
water_layer = self.create_layer("Water")
print(self.ways_data)
for way_id, data in self.ways_data.items():
if 'natural' in data['tags'] and data['tags']['natural'] == 'water':
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
@@ -915,3 +941,80 @@ class OSMImporter:
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
water.ViewObject.ShapeColor = self.feature_colors['water']
def create_water_bodies(self):
water_layer = self.create_layer("Water")
for way_id, data in self.ways_data.items():
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) < 2:
continue
# ===== 1) RÍOS / CANALES (líneas) =====
name = self.get_osm_name(tags, tags["waterway"])
if 'waterway' in tags:
if len(nodes) < 2:
continue
try:
width = self.parse_width(tags, default=2.0)
points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
wire = Draft.make_wire(points, closed=False, face=False)
wire.Label = f"{name} ({tags['waterway']})"
wire.ViewObject.LineWidth = max(1, int(width * 0.5))
wire.ViewObject.ShapeColor = self.feature_colors['water']
water_layer.addObject(wire)
except Exception as e:
print(f"Error creando waterway {way_id}: {e}")
continue # importante
# ===== 2) LAGOS / EMBALSES (polígonos) =====
is_area_water = (
tags.get('natural') == 'water' or
tags.get('landuse') in ['reservoir', 'basin'] or
tags.get('water') is not None
)
if not is_area_water or len(nodes) < 3:
continue
try:
polygon_points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
water = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Water_{way_id}")
water.Shape = face
water.ViewObject.ShapeColor = self.feature_colors['water']
water.Label = f"{name} ({tags['waterway']})"
water_layer.addObject(water)
except Exception as e:
print(f"Error creando área de agua {way_id}: {e}")
def get_osm_name(self, tags, fallback=""):
for key in ["name", "name:es", "name:en", "alt_name", "ref"]:
if key in tags and tags[key].strip():
return tags[key]
return fallback
def parse_width(self, tags, default=2.0):
for key in ["width", "est_width"]:
if key in tags:
try:
w = tags[key].replace("m", "").strip()
return float(w)
except:
pass
return default
+14 -4
View File
@@ -39,9 +39,11 @@ class PVPlantWorkbench(Workbench):
ToolTip = "Workbench for PV design"
Icon = str(os.path.join(DirIcons, "icon.svg"))
def __init__(self):
''' init '''
def Initialize(self):
#sys.path.append(r"C:\Users\javie\AppData\Roaming\FreeCAD\Mod")
sys.path.append(os.path.join(FreeCAD.getUserAppDataDir(), 'Mod'))
import PVPlantTools, reload
@@ -59,6 +61,7 @@ class PVPlantWorkbench(Workbench):
"PVPlantBuilding",
"PVPlantFenceGroup",
]'''
from Electrical.PowerConverter import PowerConverter
self.electricalList = ["PVPlantStringBox",
"PVPlantCable",
"PVPlanElectricalLine",
@@ -66,6 +69,8 @@ class PVPlantWorkbench(Workbench):
"Stringing",
"Separator",
"StringInverter",
"Separator",
"PowerConverter"
]
self.roads = ["PVPlantRoad",
@@ -141,7 +146,10 @@ class PVPlantWorkbench(Workbench):
from widgets import CountSelection
def Activated(self):
"This function is executed when the workbench is activated"
"""This function is executed when the workbench is activated"""
FreeCAD.Console.PrintLog("Road workbench activated.\n")
import SelectionObserver
import FreeCADGui
@@ -150,7 +158,9 @@ class PVPlantWorkbench(Workbench):
return
def Deactivated(self):
"This function is executed when the workbench is deactivated"
"""This function is executed when the workbench is deactivated"""
FreeCAD.Console.PrintLog("Road workbench deactivated.\n")
#FreeCADGui.Selection.removeObserver(self.observer)
return
@@ -198,4 +208,4 @@ class PVPlantWorkbench(Workbench):
return "Gui::PythonWorkbench"
Gui.addWorkbench(PVPlantWorkbench())
FreeCADGui.addWorkbench(PVPlantWorkbench())
+49 -28
View File
@@ -540,6 +540,7 @@ def makeTrackerSetup(name="TrackerSetup"):
pass
return obj
def getarray(array, numberofpoles):
if len(array) == 0:
newarray = [0] * numberofpoles
@@ -568,6 +569,7 @@ def getarray(array, numberofpoles):
newarray = [array[0]] * numberofpoles
return newarray
class TrackerSetup(FrameSetup):
"A 1 Axis Tracker Obcject"
@@ -589,7 +591,7 @@ class TrackerSetup(FrameSetup):
obj.addProperty("App::PropertyDistance",
"MotorGap",
"ModuleArray",
QT_TRANSLATE_NOOP("App::Property", "Thse height of this object")
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).MotorGap = 550
if not "UseGroupsOfModules" in pl:
@@ -880,6 +882,9 @@ class TrackerSetup(FrameSetup):
def CalculatePosts(self, obj, totalh, totalw):
# Temp: utilizar el uso de versiones:
if len(obj.PoleType) == 0:
return None, None
ver = 1
if ver == 0:
# versión 0:
@@ -906,8 +911,8 @@ class TrackerSetup(FrameSetup):
elif ver == 1:
# versión 1:
linetmp = Part.LineSegment(FreeCAD.Vector(0), FreeCAD.Vector(0, 10, 0)).toShape()
compoundPoles = Part.makeCompound([])
compoundAxis = Part.makeCompound([])
compound_poles = Part.makeCompound([])
compound_axis = Part.makeCompound([])
offsetX = - totalw / 2
arrayDistance = obj.DistancePole
@@ -915,15 +920,16 @@ class TrackerSetup(FrameSetup):
arrayPost = obj.PoleSequence
for x in range(int(obj.NumberPole.Value)):
postCopy = obj.PoleType[arrayPost[x]].Shape.copy()
post_copy = obj.PoleType[arrayPost[x]].Shape.copy()
offsetX += arrayDistance[x]
postCopy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(postCopy.BoundBox.ZLength - arrayAerial[x]))
compoundPoles.add(postCopy)
post_copy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(post_copy.BoundBox.ZLength - arrayAerial[x]))
compound_poles.add(post_copy)
axis = linetmp.copy()
axis.Placement.Base = FreeCAD.Vector(offsetX, 0, arrayAerial[x])
compoundAxis.add(axis)
return compoundPoles, compoundAxis
compound_axis.add(axis)
return compound_poles, compound_axis
def execute(self, obj):
# obj.Shape: compound
@@ -1029,14 +1035,14 @@ class Tracker(ArchComponent.Component):
"AngleY",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0
).AngleY = 0
if not ("AngleZ" in pl):
obj.addProperty("App::PropertyAngle",
"AngleZ",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0
).AngleZ = 0
self.Type = "Tracker"
#obj.Type = self.Type
@@ -1056,12 +1062,15 @@ class Tracker(ArchComponent.Component):
if prop.startswith("Angle"):
base = obj.Placement.Base
angles = obj.Placement.Rotation.toEulerAngles("XYZ")
# Actualizar rotación según el ángulo modificado
if prop == "AngleX":
rot = FreeCAD.Rotation(angles[2], angles[1], obj.AngleX.Value)
elif prop == "AngleY":
rot = FreeCAD.Rotation(angles[2], obj.AngleY.Value, angles[0])
elif prop == "AngleZ":
rot = FreeCAD.Rotation(obj.AngleZ.Value, angles[1], angles[0])
obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0,0,0))
if hasattr(FreeCAD.ActiveDocument, "FramesChecking"):
@@ -1083,28 +1092,38 @@ class Tracker(ArchComponent.Component):
# |-- PoleAxes: Edge
if obj.Setup is None:
print("Warning: No Setup defined for tracker")
return
pl = obj.Placement
shape = obj.Setup.Shape.copy()
try:
pl = obj.Placement
shape = obj.Setup.Shape.copy()
p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
axis = p1 - p2
modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
# Rotar módulos
p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
axis = p1 - p2
modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
angle = obj.Placement.Rotation.toEuler()[1]
newpoles = Part.makeCompound([])
for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
pole = shape.SubShapes[1].SubShapes[0].SubShapes[i]
axis = shape.SubShapes[1].SubShapes[1].SubShapes[i]
base = axis.Vertexes[0].Point
axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point
newpoles.add(pole.rotate(base, axis, -angle))
poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
# Rotar postes
angle = obj.Placement.Rotation.toEuler()[1]
newpoles = Part.makeCompound([])
for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
pole = shape.SubShapes[1].SubShapes[0].SubShapes[i]
axis = shape.SubShapes[1].SubShapes[1].SubShapes[i]
base = axis.Vertexes[0].Point
axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point
newpoles.add(pole.rotate(base, axis, -angle))
poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
obj.Shape = Part.makeCompound([modules, poles])
obj.Placement = pl
obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ")
# Crear forma final
obj.Shape = Part.makeCompound([modules, poles])
obj.Placement = pl
# Sincronizar propiedades de ángulo
obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ")
except Exception as e:
print(f"Error in Tracker execution: {str(e)}")
class ViewProviderTracker(ArchComponent.ViewProviderComponent):
@@ -1271,6 +1290,7 @@ class CommandFixedRack:
#FreeCADGui.Control.showDialog(self.TaskPanel)
return
class CommandTrackerSetup:
"the Arch Building command definition"
@@ -1292,6 +1312,7 @@ class CommandTrackerSetup:
FreeCADGui.Control.showDialog(self.TaskPanel)
return
class CommandTracker:
"the Arch Building command definition"
+5
View File
@@ -0,0 +1,5 @@
# PVPlant - Paquete reestructurado
#
# Los imports legacy (from PVPlantSite import X, etc.) siguen funcionando.
# Para nuevo código, usar: from PVPlant.core.site import _PVPlantSite
View File
+422
View File
@@ -0,0 +1,422 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import os
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirResources as DirResources
class MapWindow(QtGui.QWidget):
def __init__(self, WinTitle="MapWindow"):
super(MapWindow, self).__init__()
self.raise_()
self.lat = None
self.lon = None
self.minLat = None
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):
from PySide2.QtWebEngineWidgets import QWebEngineView
from PySide2.QtWebChannel import QWebChannel
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
self.resize(1200, 800)
self.setWindowTitle(self.WinTitle)
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.layout = QtGui.QHBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
LeftWidget = QtGui.QWidget(self)
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
LeftWidget.setLayout(LeftLayout)
LeftLayout.setContentsMargins(0, 0, 0, 0)
RightWidget = QtGui.QWidget(self)
RightWidget.setFixedWidth(350)
RightLayout = QtGui.QVBoxLayout(RightWidget)
RightWidget.setLayout(RightLayout)
RightLayout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(LeftWidget)
self.layout.addWidget(RightWidget)
# Left Widgets:
# -- Search Bar:
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Search")
self.valueSearch.returnPressed.connect(self.onSearch)
searchbutton = QtGui.QPushButton('Search')
searchbutton.setFixedWidth(80)
searchbutton.clicked.connect(self.onSearch)
SearchBarLayout = QtGui.QHBoxLayout(self)
SearchBarLayout.addWidget(self.valueSearch)
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
# -- Webbroser:
self.view = QWebEngineView()
self.channel = QWebChannel(self.view.page())
self.view.page().setWebChannel(self.channel)
self.channel.registerObject("MyApp", self)
file = os.path.join(DirResources, "webs", "main.html")
self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view)
# -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates)
# Right Widgets:
labelKMZ = QtGui.QLabel()
labelKMZ.setText("Cargar un archivo KMZ/KML:")
self.kmlButton = QtGui.QPushButton()
self.kmlButton.setFixedSize(32, 32)
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
widget = QtGui.QWidget(self)
layout = QtGui.QHBoxLayout(widget)
widget.setLayout(layout)
layout.addWidget(labelKMZ)
layout.addWidget(self.kmlButton)
RightLayout.addWidget(widget)
# -----------------------
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
self.groupbox.setCheckable(True)
self.groupbox.setChecked(True)
radio1 = QtGui.QRadioButton("Google Elevation")
radio2 = QtGui.QRadioButton("Nube de Puntos")
radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True)
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1)
vbox.addWidget(radio2)
vbox.addWidget(radio3)
self.groupbox.setLayout(vbox)
RightLayout.addWidget(self.groupbox)
# ------------------------
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)
self.bAccept = QtGui.QPushButton('Accept')
self.bAccept.clicked.connect(self.onAcceptClick)
RightLayout.addWidget(self.bAccept)
# signals/slots
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
def onLoadFinished(self):
file = os.path.join(DirResources, "webs", "map.js")
frame = self.view.page()
with open(file, 'r') as f:
frame.runJavaScript(f.read())
def onSearch(self):
if self.valueSearch.text() == "":
return
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="http")
location = geolocator.geocode(self.valueSearch.text())
self.valueSearch.setText(location.address)
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
def onAcceptClick(self):
frame = self.view.page()
# 1. georeferenciar
frame.runJavaScript(
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
)
# 2. importar todos los elementos dibujados:
frame.runJavaScript(
"var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));"
)
self.close()
@QtCore.Slot(float, float)
def onMapMove(self, lat, lng):
from lib.projection import latlon_to_utm
self.lat = lat
self.lon = lng
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lng)
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(easting, northing))
@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)
geolocator = Nominatim(user_agent="http")
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
if location:
if location.raw["address"].get("road"):
str = location.raw["address"]["road"]
if location.raw["address"].get("house_number"):
str += ' ({0})'.format(location.raw["address"]["house_number"])
Site.Address = str
if location.raw["address"].get("city"):
Site.City = location.raw["address"]["city"]
if location.raw["address"].get("postcode"):
Site.PostalCode = location.raw["address"]["postcode"]
if location.raw["address"].get("address"):
Site.Region = '{0}'.format(location.raw["address"]["province"])
if location.raw["address"].get("state"):
if Site.Region != "":
Site.Region += " - "
Site.Region += '{0}'.format(location.raw["address"]["state"])
Site.Country = location.raw["address"]["country"]
@QtCore.Slot(str)
def shapes(self, drawnItems):
import geojson
import PVPlantImportGrid as ImportElevation
import Draft
import PVPlantSite
Site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(Site.Origin)
offset.z = 0
items = geojson.loads(drawnItems)
for item in items['features']:
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
coord = item['geometry']['coordinates']
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
if item['properties'].get('radius'):
r = round(item['properties']['radius'] * 1000, 0)
p = FreeCAD.Placement()
p.Base = c
obj = Draft.makeCircle(r, placement=p, face=False)
else:
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10)
else: # 2. if the feature is a Polygon or Line:
cw = False
name = "Línea"
lp = item['geometry']['coordinates']
if item['geometry']['type'] == "Polygon":
cw = True
name = "Area"
lp = item['geometry']['coordinates'][0]
pts = [[cords[1], cords[0]] for cords in lp]
tmp = ImportElevation.getElevationFromOE(pts)
pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False)
obj.Label = name
Draft.autogroup(obj)
if item['properties'].get('name'):
obj.Label = item['properties']['name']
if self.checkboxImportGis.isChecked():
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
if self.checkboxImportSatelitalImagen.isChecked():
from lib.projection import latlon_to_utm
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
height_m = ne_utm.y - sw_utm.y
# 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'
# 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)
img_obj.Placement.Base = FreeCAD.Vector(
-rel_x * width_m * 1000,
-rel_y * height_m * 1000,
0
)
doc.recompute()
def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM
import PVPlantSite
site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(site.Origin)
offset.z = 0
importer = importOSM.OSMImporter(offset)
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
importer.process_osm_data(osm_data)
def panMap(self, lng, lat, geometry=None):
frame = self.view.page()
if not geometry or len(geometry) < 4:
command = f'map.panTo(L.latLng({lat}, {lng}));'
else:
try:
southwest = f"{float(geometry[1])}, {float(geometry[0])}"
northeast = f"{float(geometry[3])}, {float(geometry[2])}"
command = f'map.panTo(L.latLng({lat}, {lng}));'
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
except (IndexError, ValueError, TypeError) as e:
print(f"Error en geometry: {str(e)}")
command = f'map.panTo(L.latLng({lat}, {lng}));'
frame.runJavaScript(command)
def importKML(self):
file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0]
from lib.kml2geojson import kmz_convert
layers = kmz_convert(file, "", )
frame = self.view.page()
for layer in layers:
command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer)
frame.runJavaScript(command)
class CommandPVPlantGeoreferencing:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
def Activated(self):
self.form = MapWindow()
self.form.show()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
+208
View File
@@ -0,0 +1,208 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, Draft, math, datetime
import ArchSite
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
from pivy import coin
else:
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
import os
from PVPlantResources import DirIcons as DirIcons
zone_list = ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7", "Z8", "Z9", "Z10", "Z11", "Z12",
"Z13", "Z14", "Z15", "Z16", "Z17", "Z18", "Z19", "Z20", "Z21", "Z22", "Z23", "Z24",
"Z25", "Z26", "Z27", "Z28", "Z29", "Z30", "Z31", "Z32", "Z33", "Z34", "Z35", "Z36",
"Z37", "Z38", "Z39", "Z40", "Z41", "Z42", "Z43", "Z44", "Z45", "Z46", "Z47", "Z48",
"Z49", "Z50", "Z51", "Z52", "Z53", "Z54", "Z55", "Z56", "Z57", "Z58", "Z59", "Z60"]
def get(origin=FreeCAD.Vector(0, 0, 0), create=False):
obj = FreeCAD.ActiveDocument.getObject('Site')
if obj:
if obj.Origin == FreeCAD.Vector(0, 0, 0):
obj.Origin = origin
return obj
if not obj and create:
obj = makePVPlantSite()
return obj
def PartToWire(part):
import Part, Draft
PointList = []
edges = Part.__sortEdges__(part.Shape.Edges)
for edge in edges:
PointList.append(edge.Vertexes[0].Point)
PointList.append(edges[-1].Vertexes[-1].Point)
Draft.makeWire(PointList, closed=True, face=None, support=None)
def projectWireOnMesh(Boundary, Mesh):
import Draft
import MeshPart as mp
plist = mp.projectShapeOnMesh(Boundary.Shape, Mesh, FreeCAD.Vector(0, 0, 1))
PointList = []
for pl in plist:
PointList += pl
Draft.makeWire(PointList, closed=True, face=None, support=None)
FreeCAD.activeDocument().recompute()
def makePVPlantSite():
def createGroup(father, groupname, type=None):
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", groupname)
group.Label = groupname
father.addObject(group)
return group
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Site")
_PVPlantSite(obj)
if FreeCAD.GuiUp:
_ViewProviderSite(obj.ViewObject)
group = createGroup(obj, "CivilGroup")
group1 = createGroup(group, "Areas")
createGroup(group1, "Boundaries")
createGroup(group1, "CadastralPlots")
createGroup(group1, "Exclusions")
createGroup(group1, "FrameZones")
createGroup(group1, "Offsets")
createGroup(group1, "Plots")
createGroup(group, "Drains")
createGroup(group, "Earthworks")
createGroup(group, "Fences")
createGroup(group, "Foundations")
createGroup(group, "Pads")
createGroup(group, "Points")
createGroup(group, "Roads")
createGroup(group, "Trenches")
group = createGroup(obj, "ElectricalGroup")
createGroup(group, "StringInverters")
createGroup(group, "CentralInverter")
group1 = createGroup(group, "AC")
createGroup(group1, "CableAC")
group1 = createGroup(group, "DC")
createGroup(group1, "CableDC")
createGroup(group1, "StringsSetup")
createGroup(group1, "Strings")
createGroup(group1, "StringsBoxes")
group = createGroup(obj, "MechanicalGroup")
createGroup(group, "FramesSetups")
createGroup(group, "Frames")
group = createGroup(obj, "Environment")
createGroup(group, "Vegetation")
return obj
class _PVPlantSite(ArchSite._Site):
"The Site object"
def __init__(self, obj):
ArchSite._Site.__init__(self, obj)
self.obj = obj
self.Type = "Site"
obj.Proxy = self
obj.IfcType = "Site"
obj.setEditorMode("IfcType", 1)
def setProperties(self, obj):
ArchSite._Site.setProperties(self, obj)
obj.addProperty("App::PropertyLink", "Boundary", "PVPlant", "Boundary of land")
obj.addProperty("App::PropertyLinkList", "Frames", "PVPlant", "Frames templates")
obj.addProperty("App::PropertyEnumeration", "UtmZone", "PVPlant", "UTM zone").UtmZone = zone_list
obj.addProperty("App::PropertyVector", "Origin", "PVPlant", "Origin point.").Origin = (0, 0, 0)
def onDocumentRestored(self, obj):
self.obj = obj
self.Type = "Site"
obj.Proxy = self
def onChanged(self, obj, prop):
ArchSite._Site.onChanged(self, obj, prop)
if (prop == "Terrain") or (prop == "Boundary"):
if obj.Terrain and obj.Boundary:
print("Calcular 3D boundary")
if prop == "UtmZone":
node = self.get_geoorigin()
zone = obj.getPropertyByName("UtmZone")
geo_system = ["UTM", zone, "FLAT"]
node.geoSystem.setValues(geo_system)
if prop == "Origin":
node = self.get_geoorigin()
origin = obj.getPropertyByName("Origin")
node.geoCoords.setValue(origin.x, origin.y, 0)
obj.Placement.Base = obj.getPropertyByName(prop)
def execute(self, obj):
ArchSite._Site.execute(self, obj)
def computeAreas(self, obj):
ArchSite._Site.computeAreas(self, obj)
def __getstate__(self):
node = self.get_geoorigin()
system = node.geoSystem.getValues()
x, y, z = node.geoCoords.getValue().getValue()
return system, [x, y, z]
def __setstate__(self, state):
if state:
system = state[0]
origin = state[1]
node = self.get_geoorigin()
node.geoSystem.setValues(system)
node.geoCoords.setValue(origin[0], origin[1], 0)
def get_geoorigin(self):
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
node = sg.getChild(0)
if not isinstance(node, coin.SoGeoOrigin):
node = coin.SoGeoOrigin()
sg.insertChild(node, 0)
return node
def setLatLon(self, lat, lon):
from lib.projection import latlon_to_utm
import PVPlantImportGrid
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
point = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z)
self.obj.Latitude = lat
self.obj.Longitude = lon
self.obj.Elevation = point[0].z
from PVPlant.core.view_provider import ViewProviderSite as _ViewProviderSite
+353
View File
@@ -0,0 +1,353 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, math, datetime
from pivy import coin
def makeSolarDiagram(longitude, latitude, scale=1, complete=False, tz=None):
"""makeSolarDiagram(longitude,latitude,[scale,complete,tz]):
returns a solar diagram as a pivy node. If complete is
True, the 12 months are drawn. Tz is the timezone related to
UTC (ex: -3 = UTC-3)"""
oldversion = False
ladybug = False
try:
import ladybug
from ladybug import location
from ladybug import sunpath
except:
ladybug = False
try:
import pysolar
except:
try:
import Pysolar as pysolar
except:
FreeCAD.Console.PrintError("The pysolar module was not found. Unable to generate solar diagrams\n")
return None
else:
oldversion = True
if tz:
tz = datetime.timezone(datetime.timedelta(hours=-3))
else:
tz = datetime.timezone.utc
else:
loc = ladybug.location.Location(latitude=latitude, longitude=longitude, time_zone=tz)
sunpath = ladybug.sunpath.Sunpath.from_location(loc)
if not scale:
return None
circles = []
sunpaths = []
hourpaths = []
circlepos = []
hourpos = []
import Part
for i in range(1, 9):
circles.append(Part.makeCircle(scale * (i / 8.0)))
for ad in range(0, 360, 15):
a = math.radians(ad)
p1 = FreeCAD.Vector(math.cos(a) * scale, math.sin(a) * scale, 0)
p2 = FreeCAD.Vector(math.cos(a) * scale * 0.125, math.sin(a) * scale * 0.125, 0)
p3 = FreeCAD.Vector(math.cos(a) * scale * 1.08, math.sin(a) * scale * 1.08, 0)
circles.append(Part.LineSegment(p1, p2).toShape())
circlepos.append((ad, p3))
year = datetime.datetime.now().year
hpts = [[] for i in range(24)]
m = [(6, 21), (7, 21), (8, 21), (9, 21), (10, 21), (11, 21), (12, 21)]
if complete:
m.extend([(1, 21), (2, 21), (3, 21), (4, 21), (5, 21)])
for i, d in enumerate(m):
pts = []
for h in range(24):
if ladybug:
sun = sunpath.calculate_sun(month=d[0], day=d[1], hour=h)
alt = math.radians(sun.altitude)
az = 90 + sun.azimuth
elif oldversion:
dt = datetime.datetime(year, d[0], d[1], h)
alt = math.radians(pysolar.solar.GetAltitudeFast(latitude, longitude, dt))
az = pysolar.solar.GetAzimuth(latitude, longitude, dt)
az = -90 + az
else:
dt = datetime.datetime(year, d[0], d[1], h, tzinfo=tz)
alt = math.radians(pysolar.solar.get_altitude_fast(latitude, longitude, dt))
az = pysolar.solar.get_azimuth(latitude, longitude, dt)
az = 90 + az
if az < 0:
az = 360 + az
az = math.radians(az)
zc = math.sin(alt) * scale
ic = math.cos(alt) * scale
xc = math.cos(az) * ic
yc = math.sin(az) * ic
p = FreeCAD.Vector(xc, yc, zc)
pts.append(p)
hpts[h].append(p)
if i in [0, 6]:
ep = FreeCAD.Vector(p)
ep.multiply(1.08)
if ep.z >= 0:
if not oldversion:
h = 24 - h
if h == 12:
if i == 0:
h = "SUMMER"
else:
h = "WINTER"
if latitude < 0:
if h == "SUMMER":
h = "WINTER"
else:
h = "SUMMER"
hourpos.append((h, ep))
if i < 7:
sunpaths.append(Part.makePolygon(pts))
for h in hpts:
if complete:
h.append(h[0])
hourpaths.append(Part.makePolygon(h))
sz = 2.1 * scale
cube = Part.makeBox(sz, sz, sz)
cube.translate(FreeCAD.Vector(-sz / 2, -sz / 2, -sz))
sunpaths = [sp.cut(cube) for sp in sunpaths]
hourpaths = [hp.cut(cube) for hp in hourpaths]
ts = 0.005 * scale
mastersep = coin.SoSeparator()
circlesep = coin.SoSeparator()
numsep = coin.SoSeparator()
pathsep = coin.SoSeparator()
hoursep = coin.SoSeparator()
hournumsep = coin.SoSeparator()
mastersep.addChild(circlesep)
mastersep.addChild(numsep)
mastersep.addChild(pathsep)
mastersep.addChild(hoursep)
for item in circles:
circlesep.addChild(toNode(item))
for item in sunpaths:
for w in item.Edges:
pathsep.addChild(toNode(w))
for item in hourpaths:
for w in item.Edges:
hoursep.addChild(toNode(w))
for p in circlepos:
text = coin.SoText2()
s = p[0] - 90
s = -s
if s > 360:
s = s - 360
if s < 0:
s = 360 + s
if s == 0:
s = "N"
elif s == 90:
s = "E"
elif s == 180:
s = "S"
elif s == 270:
s = "W"
else:
s = str(s)
text.string = s
text.justification = coin.SoText2.CENTER
coords = coin.SoTransform()
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
coords.scaleFactor.setValue([ts, ts, ts])
item = coin.SoSeparator()
item.addChild(coords)
item.addChild(text)
numsep.addChild(item)
for p in hourpos:
text = coin.SoText2()
s = str(p[0])
text.string = s
text.justification = coin.SoText2.CENTER
coords = coin.SoTransform()
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
coords.scaleFactor.setValue([ts, ts, ts])
item = coin.SoSeparator()
item.addChild(coords)
item.addChild(text)
numsep.addChild(item)
return mastersep
def makeWindRose(epwfile, scale=1, sectors=24):
try:
import ladybug
from ladybug import epw
except:
FreeCAD.Console.PrintError("The ladybug module was not found. Unable to generate solar diagrams\n")
return None
if not epwfile:
FreeCAD.Console.PrintWarning("No EPW file, unable to generate wind rose.\n")
return None
epw_data = ladybug.epw.EPW(epwfile)
baseangle = 360 / sectors
sectorangles = [i * baseangle for i in range(sectors)]
basebissect = baseangle / 2
angles = [basebissect]
for i in range(1, sectors):
angles.append(angles[-1] + baseangle)
windsbysector = [0 for i in range(sectors)]
for hour in epw_data.wind_direction:
sector = min(angles, key=lambda x: abs(x - hour))
sectorindex = angles.index(sector)
windsbysector[sectorindex] = windsbysector[sectorindex] + 1
maxwind = max(windsbysector)
windsbysector = [wind / maxwind for wind in windsbysector]
vectors = []
dividers = []
for i in range(sectors):
angle = math.radians(90 + angles[i])
x = math.cos(angle) * windsbysector[i] * scale
y = math.sin(angle) * windsbysector[i] * scale
vectors.append(FreeCAD.Vector(x, y, 0))
secangle = math.radians(90 + sectorangles[i])
x = math.cos(secangle) * scale
y = math.sin(secangle) * scale
dividers.append(FreeCAD.Vector(x, y, 0))
vectors.append(vectors[0])
import Part
masternode = coin.SoSeparator()
for r in (0.25, 0.5, 0.75, 1.0):
c = Part.makeCircle(r * scale)
masternode.addChild(toNode(c))
for divider in dividers:
l = Part.makeLine(FreeCAD.Vector(), divider)
masternode.addChild(toNode(l))
ds = coin.SoDrawStyle()
ds.lineWidth = 2.0
masternode.addChild(ds)
d = Part.makePolygon(vectors)
masternode.addChild(toNode(d))
return masternode
# Values in mm
COMPASS_POINTER_LENGTH = 1000
COMPASS_POINTER_WIDTH = 100
class Compass(object):
def __init__(self):
self.rootNode = self.setupCoin()
def show(self):
self.compassswitch.whichChild = coin.SO_SWITCH_ALL
def hide(self):
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
def rotate(self, angleInDegrees):
self.transform.rotation.setValue(
coin.SbVec3f(0, 0, 1), math.radians(angleInDegrees))
def locate(self, x, y, z):
self.transform.translation.setValue(x, y, z)
def scale(self, area):
s = round(max(math.sqrt(area.getValueAs("m^2").Value) / 10, 1))
self.transform.scaleFactor.setValue(coin.SbVec3f(s, s, 1))
def setupCoin(self):
compasssep = coin.SoSeparator()
self.transform = coin.SoTransform()
darkNorthMaterial = coin.SoMaterial()
darkNorthMaterial.diffuseColor.set1Value(0, 0.5, 0, 0)
lightNorthMaterial = coin.SoMaterial()
lightNorthMaterial.diffuseColor.set1Value(0, 0.9, 0, 0)
darkGreyMaterial = coin.SoMaterial()
darkGreyMaterial.diffuseColor.set1Value(0, 0.9, 0.9, 0.9)
lightGreyMaterial = coin.SoMaterial()
lightGreyMaterial.diffuseColor.set1Value(0, 0.5, 0.5, 0.5)
coords = self.buildCoordinates()
lightColorFaceset = coin.SoIndexedFaceSet()
lightColorCoordinateIndex = [4, 5, 6, -1, 8, 9, 10, -1, 12, 13, 14, -1]
lightColorFaceset.coordIndex.setValues(0, len(lightColorCoordinateIndex), lightColorCoordinateIndex)
darkColorFaceset = coin.SoIndexedFaceSet()
darkColorCoordinateIndex = [6, 7, 4, -1, 10, 11, 8, -1, 14, 15, 12, -1]
darkColorFaceset.coordIndex.setValues(0, len(darkColorCoordinateIndex), darkColorCoordinateIndex)
lightNorthFaceset = coin.SoIndexedFaceSet()
lightNorthCoordinateIndex = [2, 3, 0, -1]
lightNorthFaceset.coordIndex.setValues(0, len(lightNorthCoordinateIndex), lightNorthCoordinateIndex)
darkNorthFaceset = coin.SoIndexedFaceSet()
darkNorthCoordinateIndex = [0, 1, 2, -1]
darkNorthFaceset.coordIndex.setValues(0, len(darkNorthCoordinateIndex), darkNorthCoordinateIndex)
self.compassswitch = coin.SoSwitch()
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
self.compassswitch.addChild(compasssep)
lightGreySeparator = coin.SoSeparator()
lightGreySeparator.addChild(lightGreyMaterial)
lightGreySeparator.addChild(lightColorFaceset)
darkGreySeparator = coin.SoSeparator()
darkGreySeparator.addChild(darkGreyMaterial)
darkGreySeparator.addChild(darkColorFaceset)
lightNorthSeparator = coin.SoSeparator()
lightNorthSeparator.addChild(lightNorthMaterial)
lightNorthSeparator.addChild(lightNorthFaceset)
darkNorthSeparator = coin.SoSeparator()
darkNorthSeparator.addChild(darkNorthMaterial)
darkNorthSeparator.addChild(darkNorthFaceset)
compasssep.addChild(coords)
compasssep.addChild(self.transform)
compasssep.addChild(lightGreySeparator)
compasssep.addChild(darkGreySeparator)
compasssep.addChild(lightNorthSeparator)
compasssep.addChild(darkNorthSeparator)
return self.compassswitch
def buildCoordinates(self):
coords = coin.SoCoordinate3()
coords.point.set1Value(0, 0, 0, 0)
coords.point.set1Value(1, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(2, 0, COMPASS_POINTER_LENGTH, 0)
coords.point.set1Value(3, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(4, 0, 0, 0)
coords.point.set1Value(5, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(6, COMPASS_POINTER_LENGTH, 0, 0)
coords.point.set1Value(7, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(8, 0, 0, 0)
coords.point.set1Value(9, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(10, 0, -COMPASS_POINTER_LENGTH, 0)
coords.point.set1Value(11, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(12, 0, 0, 0)
coords.point.set1Value(13, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(14, -COMPASS_POINTER_LENGTH, 0, 0)
coords.point.set1Value(15, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
return coords
+283
View File
@@ -0,0 +1,283 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, math
from pivy import coin
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
from PVPlant.core.solar_compass import makeSolarDiagram, makeWindRose, Compass
class ViewProviderSite(object):
"""View Provider for the Site object. Handles solar diagram, wind rose, compass and true north."""
def __init__(self, vobj):
vobj.Proxy = self
vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
self.setProperties(vobj)
def setProperties(self, vobj):
from PVPlantResources import DirIcons as DirIcons
pl = vobj.PropertiesList
if not "WindRose" in pl:
vobj.addProperty("App::PropertyBool", "WindRose", "Site",
QT_TRANSLATE_NOOP("App::Property", "Show wind rose diagram or not. Uses solar diagram scale. Needs Ladybug module"))
if not "SolarDiagram" in pl:
vobj.addProperty("App::PropertyBool", "SolarDiagram", "Site",
QT_TRANSLATE_NOOP("App::Property", "Show solar diagram or not"))
if not "SolarDiagramScale" in pl:
vobj.addProperty("App::PropertyFloat", "SolarDiagramScale", "Site",
QT_TRANSLATE_NOOP("App::Property", "The scale of the solar diagram"))
vobj.SolarDiagramScale = 1
if not "SolarDiagramPosition" in pl:
vobj.addProperty("App::PropertyVector", "SolarDiagramPosition", "Site",
QT_TRANSLATE_NOOP("App::Property", "The position of the solar diagram"))
if not "SolarDiagramColor" in pl:
vobj.addProperty("App::PropertyColor", "SolarDiagramColor", "Site",
QT_TRANSLATE_NOOP("App::Property", "The color of the solar diagram"))
vobj.SolarDiagramColor = (0.16, 0.16, 0.25)
if not "Orientation" in pl:
vobj.addProperty("App::PropertyEnumeration", "Orientation", "Site",
QT_TRANSLATE_NOOP("App::Property", "When set to 'True North' the whole geometry will be rotated to match the true north of this site"))
vobj.Orientation = ["Project North", "True North"]
vobj.Orientation = "Project North"
if not "Compass" in pl:
vobj.addProperty("App::PropertyBool", "Compass", "Compass",
QT_TRANSLATE_NOOP("App::Property", "Show compass or not"))
if not "CompassRotation" in pl:
vobj.addProperty("App::PropertyAngle", "CompassRotation", "Compass",
QT_TRANSLATE_NOOP("App::Property", "The rotation of the Compass relative to the Site"))
if not "CompassPosition" in pl:
vobj.addProperty("App::PropertyVector", "CompassPosition", "Compass",
QT_TRANSLATE_NOOP("App::Property", "The position of the Compass relative to the Site placement"))
if not "UpdateDeclination" in pl:
vobj.addProperty("App::PropertyBool", "UpdateDeclination", "Compass",
QT_TRANSLATE_NOOP("App::Property", "Update the Declination value based on the compass rotation"))
def onDocumentRestored(self, vobj):
self.setProperties(vobj)
def getIcon(self):
from PVPlantResources import DirIcons as DirIcons
return str(os.path.join(DirIcons, "solar-panel.svg"))
def claimChildren(self):
objs = []
if hasattr(self, "Object"):
objs = self.Object.Group + [self.Object.Terrain]
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
if hasattr(self.Object, "Additions") and prefs.GetBool("swallowAdditions", True):
objs.extend(self.Object.Additions)
if hasattr(self.Object, "Subtractions") and prefs.GetBool("swallowSubtractions", True):
objs.extend(self.Object.Subtractions)
return objs
def setEdit(self, vobj, mode):
if (mode == 0) and hasattr(self, "Object"):
import ArchComponent
taskd = ArchComponent.ComponentTaskPanel()
taskd.obj = self.Object
taskd.update()
FreeCADGui.Control.showDialog(taskd)
return True
return False
def unsetEdit(self, vobj, mode):
FreeCADGui.Control.closeDialog()
return False
def attach(self, vobj):
self.Object = vobj.Object
basesep = coin.SoSeparator()
vobj.Annotation.addChild(basesep)
self.color = coin.SoBaseColor()
self.coords = coin.SoTransform()
basesep.addChild(self.coords)
basesep.addChild(self.color)
self.diagramsep = coin.SoSeparator()
self.diagramswitch = coin.SoSwitch()
self.diagramswitch.whichChild = -1
self.diagramswitch.addChild(self.diagramsep)
basesep.addChild(self.diagramswitch)
self.windrosesep = coin.SoSeparator()
self.windroseswitch = coin.SoSwitch()
self.windroseswitch.whichChild = -1
self.windroseswitch.addChild(self.windrosesep)
basesep.addChild(self.windroseswitch)
self.compass = Compass()
self.updateCompassVisibility(vobj)
self.updateCompassScale(vobj)
self.rotateCompass(vobj)
vobj.Annotation.addChild(self.compass.rootNode)
def updateData(self, obj, prop):
if prop in ["Longitude", "Latitude"]:
self.onChanged(obj.ViewObject, "SolarDiagram")
elif prop == "Declination":
self.onChanged(obj.ViewObject, "SolarDiagramPosition")
self.updateTrueNorthRotation()
elif prop == "Terrain":
self.updateCompassLocation(obj.ViewObject)
elif prop == "Placement":
self.updateCompassLocation(obj.ViewObject)
self.updateDeclination(obj.ViewObject)
elif prop == "ProjectedArea":
self.updateCompassScale(obj.ViewObject)
def onChanged(self, vobj, prop):
if prop == "SolarDiagramPosition":
if hasattr(vobj, "SolarDiagramPosition"):
p = vobj.SolarDiagramPosition
self.coords.translation.setValue([p.x, p.y, p.z])
if hasattr(vobj.Object, "Declination"):
self.coords.rotation.setValue(coin.SbVec3f((0, 0, 1)), math.radians(vobj.Object.Declination.Value))
elif prop == "SolarDiagramColor":
if hasattr(vobj, "SolarDiagramColor"):
l = vobj.SolarDiagramColor
self.color.rgb.setValue([l[0], l[1], l[2]])
elif "SolarDiagram" in prop:
if hasattr(self, "diagramnode"):
self.diagramsep.removeChild(self.diagramnode)
del self.diagramnode
if hasattr(vobj, "SolarDiagram") and hasattr(vobj, "SolarDiagramScale"):
if vobj.SolarDiagram:
tz = 0
if hasattr(vobj.Object, "TimeZone"):
tz = vobj.Object.TimeZone
self.diagramnode = makeSolarDiagram(vobj.Object.Longitude, vobj.Object.Latitude,
vobj.SolarDiagramScale, tz=tz)
if self.diagramnode:
self.diagramsep.addChild(self.diagramnode)
self.diagramswitch.whichChild = 0
else:
del self.diagramnode
else:
self.diagramswitch.whichChild = -1
elif prop == "WindRose":
if hasattr(self, "windrosenode"):
del self.windrosenode
if hasattr(vobj, "WindRose"):
if vobj.WindRose:
if hasattr(vobj.Object, "EPWFile") and vobj.Object.EPWFile:
try:
import ladybug
except:
pass
else:
self.windrosenode = makeWindRose(vobj.Object.EPWFile, vobj.SolarDiagramScale)
if self.windrosenode:
self.windrosesep.addChild(self.windrosenode)
self.windroseswitch.whichChild = 0
else:
del self.windrosenode
else:
self.windroseswitch.whichChild = -1
elif prop == 'Visibility':
if vobj.Visibility:
self.updateCompassVisibility(self.Object)
else:
self.compass.hide()
elif prop == 'Orientation':
if vobj.Orientation == 'True North':
self.addTrueNorthRotation()
else:
self.removeTrueNorthRotation()
elif prop == "UpdateDeclination":
self.updateDeclination(vobj)
elif prop == "Compass":
self.updateCompassVisibility(vobj)
elif prop == "CompassRotation":
self.updateDeclination(vobj)
self.rotateCompass(vobj)
elif prop == "CompassPosition":
self.updateCompassLocation(vobj)
def updateDeclination(self, vobj):
if not hasattr(vobj, 'UpdateDeclination') or not vobj.UpdateDeclination:
return
compassRotation = vobj.CompassRotation.Value
siteRotation = math.degrees(vobj.Object.Placement.Rotation.Angle)
vobj.Object.Declination = compassRotation + siteRotation
def addTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
return
self.trueNorthRotation = coin.SoTransform()
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
sg.insertChild(self.trueNorthRotation, 0)
self.updateTrueNorthRotation()
def removeTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
sg.removeChild(self.trueNorthRotation)
self.trueNorthRotation = None
def updateTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
angle = self.Object.Declination.Value
self.trueNorthRotation.rotation.setValue(coin.SbVec3f(0, 0, 1), math.radians(-angle))
def updateCompassVisibility(self, vobj):
if not hasattr(self, 'compass'):
return
show = hasattr(vobj, 'Compass') and vobj.Compass
if show:
self.compass.show()
else:
self.compass.hide()
def rotateCompass(self, vobj):
if not hasattr(self, 'compass'):
return
if hasattr(vobj, 'CompassRotation'):
self.compass.rotate(vobj.CompassRotation.Value)
def updateCompassLocation(self, vobj):
if not hasattr(self, 'compass'):
return
if not vobj.Object.Shape:
return
boundBox = vobj.Object.Shape.BoundBox
pos = vobj.Object.Placement.Base
x = 0
y = 0
if hasattr(vobj, "CompassPosition"):
x = vobj.CompassPosition.x
y = vobj.CompassPosition.y
z = boundBox.ZMax = pos.z
self.compass.locate(x, y, z + 1000)
def updateCompassScale(self, vobj):
if not hasattr(self, 'compass'):
return
self.compass.scale(vobj.Object.ProjectedArea)
def __getstate__(self):
return None
def __setstate__(self, state):
return None
View File
+671
View File
@@ -0,0 +1,671 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import json
import urllib.request
import Draft
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import os
from PVPlantResources import DirIcons as DirIcons
import PVPlantSite
def get_elevation_from_oe(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
Args:
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
Returns:
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
o lista vacía en caso de error.
"""
if not coordinates:
return []
import requests
from lib.projection import latlon_to_utm
from requests.exceptions import RequestException
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
try:
response = requests.get(
url="https://api.open-elevation.com/api/v1/lookup",
params={'locations': locations},
timeout=20,
verify=True
)
response.raise_for_status()
except RequestException as e:
print(f"Error en la solicitud: {str(e)}")
return []
try:
data = response.json()
except ValueError:
print("Respuesta JSON inválida")
return []
if "results" not in data or len(data["results"]) != len(coordinates):
print("Formato de respuesta inesperado")
return []
points = []
for result in data["results"]:
try:
easting, northing, _, _ = latlon_to_utm(
result["latitude"],
result["longitude"]
)
points.append(FreeCAD.Vector(round(easting),
round(northing),
round(result["elevation"])) * 1000)
except Exception as e:
print(f"Error procesando coordenadas: {str(e)}")
continue
return points
def getElevationFromOE(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
import certifi
from requests.exceptions import RequestException
if len(coordinates) == 0:
return None
from requests import get
from lib.projection import latlon_to_utm
locations_str=""
total = len(coordinates) - 1
for i, point in enumerate(coordinates):
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
if i != total:
locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
points = []
try:
r = get(query, timeout=20, verify=certifi.where())
results = r.json()
for point in results["results"]:
easting, northing, _, _ = latlon_to_utm(point["latitude"], point["longitude"])
v = FreeCAD.Vector(round(easting, 0),
round(northing, 0),
round(point["elevation"], 0)) * 1000
points.append(v)
except RequestException as e:
for point in coordinates:
easting, northing, _, _ = latlon_to_utm(point[0], point[1])
points.append(FreeCAD.Vector(round(easting, 0),
round(northing, 0),
0) * 1000)
return points
def getSinglePointElevationFromBing(lat, lng):
import requests
from lib.projection import latlon_to_utm
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng)
source += "&heights=sealevel"
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
s = json.loads(ans)
print(s)
res = s['resourceSets'][0]['resources'][0]['elevations']
for elevation in res:
easting, northing, _, _ = latlon_to_utm(lat, lng)
v = FreeCAD.Vector(
round(easting * 1000, 0),
round(northing * 1000, 0),
round(elevation * 1000, 0))
return v
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
import math
import requests
from lib.projection import latlon_to_utm, utm_to_latlon
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
xx = polygon.Shape.BoundBox.XMin
while xx < polygon.Shape.BoundBox.XMax:
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
if StepsXX > 1000:
StepsXX = 1000
xx1 = xx + 1000 * resolution
else:
xx1 = xx + StepsXX * resolution
point1 = utm_to_latlon(xx / 1000, yy / 1000, zone_number, zone_letter)
point2 = utm_to_latlon(xx1 / 1000, yy / 1000, zone_number, zone_letter)
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
source += ","
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
source += "&heights=sealevel"
source += "&samples={steps}".format(steps=StepsXX)
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['resourceSets'][0]['resources'][0]['elevations']
i = 0
for elevation in res:
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
points.append(v)
i += 1
xx = xx1 + resolution
yy -= resolution
return points
def getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2)
)
return v
def _getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
import pymap3d as pm
for r in res:
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
round(r['location']['lat'], 2),
0)
v = FreeCAD.Vector(x,y,z)
return v
def getSinglePointElevation1(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 4),
round(c[1], 4),
round(r['elevation'] * 1000, 2)
)
return v
def getSinglePointElevationUtm(lat, lon):
import requests
from lib.projection import latlon_to_utm
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
print(source)
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['results']
print(res)
for r in res:
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(easting * 1000, 4),
round(northing * 1000, 4),
round(r['elevation'] * 1000, 2))
print(v)
return v
def getElevationUTM(polygon, lat, lng, resolution = 10000):
from lib.projection import latlon_to_utm, utm_to_latlon
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = utm_to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, zone_number, zone_letter)
point2 = utm_to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, zone_number, zone_letter)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
import requests
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['results']
for r in res:
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(easting * 1000, 2),
round(northing * 1000, 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
yy -= (resolution*1000)
FreeCAD.activeDocument().recompute()
return points
def getElevation1(polygon,resolution=10):
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
try:
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
except:
continue
#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)
)
points.append(v)
FreeCAD.activeDocument().recompute()
yy -= (resolution*1000)
return points
## download the heights from google:
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
#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']
from geopy.distance import geodesic
points = []
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lat'])).m, 2),
round(geodesic(reference, (r['location']['lng'], 0.0)).m, 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):
self.obj = None
self.Boundary = None
self.select = 0
self.filename = ""
# form:
self.form1 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantImportGrid.ui")
self.form1.radio1.toggled.connect(lambda: self.mainToggle(self.form1.radio1))
self.form1.radio2.toggled.connect(lambda: self.mainToggle(self.form1.radio2))
self.form1.radio1.setChecked(True) # << --------------Poner al final para que no dispare antes de crear los componentes a los que va a llamar
#self.form.buttonAdd.clicked.connect(self.add)
self.form1.buttonDEM.clicked.connect(self.openFileDEM)
self.form2 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantCreateTerrainMesh.ui")
#self.form2.buttonAdd.clicked.connect(self.add)
self.form2.buttonBoundary.clicked.connect(self.addBoundary)
#self.form = [self.form1, self.form2]
self.form = self.form1
''' future:
def retranslateUi(self, dialog):
self.form1.setWindowTitle("Configuracion del Rack")
self.labelModule.setText(QtGui.QApplication.translate("PVPlant", "Modulo:", None))
self.labelModuleLength.setText(QtGui.QApplication.translate("PVPlant", "Longitud:", None))
self.labelModuleWidth.setText(QtGui.QApplication.translate("PVPlant", "Ancho:", None))
self.labelModuleHeight.setText(QtGui.QApplication.translate("PVPlant", "Alto:", None))
self.labelModuleFrame.setText(QtGui.QApplication.translate("PVPlant", "Ancho del marco:", None))
self.labelModuleColor.setText(QtGui.QApplication.translate("PVPlant", "Color del modulo:", None))
self.labelModules.setText(QtGui.QApplication.translate("Arch", "Colocacion de los Modulos", None))
self.labelModuleOrientation.setText(QtGui.QApplication.translate("Arch", "Orientacion del modulo:", None))
self.labelModuleGapX.setText(QtGui.QApplication.translate("Arch", "Separacion Horizontal (mm):", None))
self.labelModuleGapY.setText(QtGui.QApplication.translate("Arch", "Separacion Vertical (mm):", None))
self.labelModuleRows.setText(QtGui.QApplication.translate("Arch", "Filas de modulos:", None))
self.labelModuleCols.setText(QtGui.QApplication.translate("Arch", "Columnas de modulos:", None))
self.labelRack.setText(QtGui.QApplication.translate("Arch", "Configuracion de la estructura", None))
self.labelRackType.setText(QtGui.QApplication.translate("Arch", "Tipo de estructura:", None))
self.labelLevel.setText(QtGui.QApplication.translate("Arch", "Nivel:", None))
self.labelOffset.setText(QtGui.QApplication.translate("Arch", "Offset", None))
'''
def add(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.obj = sel[0]
self.lineEdit1.setText(self.obj.Label)
def addBoundary(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Boundary = sel[0]
self.form2.editBoundary.setText(self.Boundary.Label)
def openFileDEM(self):
filters = "Esri ASC (*.asc);;CSV (*.csv);;All files (*.*)"
filename = QtGui.QFileDialog.getOpenFileName(None,
"Open DEM,",
"",
filters)
self.filename = filename[0]
self.form1.editDEM.setText(filename[0])
def mainToggle(self, radiobox):
if radiobox is self.form1.radio1:
self.select = 0
self.form1.gbLocalFile.setVisible(True)
elif radiobox is self.form1.radio2:
self.select = 1
self.form1.gbLocalFile.setVisible(True)
def accept(self):
from datetime import datetime
starttime = datetime.now()
site = PVPlantSite.get()
try:
PointGroups = FreeCAD.ActiveDocument.Point_Groups
except:
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
PointGroups.Label = "Point Groups"
PointGroup = FreeCAD.ActiveDocument.addObject('Points::Feature', "Point_Group")
PointGroup.Label = "Land_Grid_Points"
FreeCAD.ActiveDocument.Point_Groups.addObject(PointGroup)
PointObject = PointGroup.Points.copy()
if self.select == 0: # Google or bing or ...
#for item in self.obj:
#if self.groupbox.isChecked:break
resol = FreeCAD.Units.Quantity(self.valueResolution.text()).Value
Site = FreeCAD.ActiveDocument.Site
pts = getGridElevationFromBing(self.obj, Site.Latitude, Site.Longitude, resol)
PointObject.addPoints(pts)
PointGroup.Points = PointObject
else:
if self.filename == "":
return
import Utils.importDEM as openDEM
if self.select == 1: # DEM.
import numpy as np
root, extension = os.path.splitext(self.filename)
if extension.lower() == ".asc":
x, y, datavals, cellsize, nodata_value = openDEM.openEsri(self.filename)
if self.Boundary:
inc_x = self.Boundary.Shape.BoundBox.XLength * 0.05
inc_y = self.Boundary.Shape.BoundBox.YLength * 0.05
min_x = 0
max_x = 0
comp = (self.Boundary.Shape.BoundBox.XMin - inc_x) / 1000
for i in range(nx):
if x[i] > comp:
min_x = i - 1
break
comp = (self.Boundary.Shape.BoundBox.XMax + inc_x) / 1000
for i in range(min_x, nx):
if x[i] > comp:
max_x = i
break
min_y = 0
max_y = 0
comp = (self.Boundary.Shape.BoundBox.YMax + inc_y) / 1000
for i in range(ny):
if y[i] < comp:
max_y = i
break
comp = (self.Boundary.Shape.BoundBox.YMin - inc_y) / 1000
for i in range(max_y, ny):
if y[i] < comp:
min_y = i
break
x = x[min_x:max_x]
y = y[max_y:min_y]
datavals = datavals[max_y:min_y, min_x:max_x]
pts = []
if True: # faster but more memory 46s - 4,25 gb
x, y = np.meshgrid(x, y)
xx = x.flatten()
yy = y.flatten()
zz = datavals.flatten()
x[:] = 0
y[:] = 0
datavals[:] = 0
pts = []
for i in range(0, len(xx)):
pts.append(FreeCAD.Vector(xx[i], yy[i], zz[i]) * 1000)
xx[:] = 0
yy[:] = 0
zz[:] = 0
else: # 51s 3,2 gb
createmesh = True
if createmesh:
import Part, Draft
lines=[]
for j in range(len(y)):
edges = []
for i in range(0, len(x) - 1):
ed = Part.makeLine(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000,
FreeCAD.Vector(x[i + 1], y[j], datavals[j][i + 1]) * 1000)
edges.append(ed)
#bspline = Draft.makeBSpline(pts)
#bspline.ViewObject.hide()
line = Part.Wire(edges)
lines.append(line)
'''
for i in range(0, len(bsplines), 100):
p = Part.makeLoft(bsplines[i:i + 100], False, False, False)
Part.show(p)
'''
p = Part.makeLoft(lines, False, True, False)
p = Part.Solid(p)
Part.show(p)
else:
pts = []
for j in range(ny):
for i in range(nx):
pts.append(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000)
elif extension.lower() == ".csv" or extension.lower() == ".txt": # x, y, z from gps
pts = openDEM.interpolatePoints(openDEM.openCSV(self.filename))
PointObject.addPoints(pts)
PointGroup.Points = PointObject
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Control.closeDialog()
print("tiempo: ", datetime.now() - starttime)
def reject(self):
FreeCADGui.Control.closeDialog()
## Comandos -----------------------------------------------------------------------------------------------------------
class CommandImportPoints:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "cloud.svg")),
'MenuText': QT_TRANSLATE_NOOP("PVPlant", "Importer Grid"),
'Accel': "B, U",
'ToolTip': QT_TRANSLATE_NOOP("PVPlant", "Creates a cloud of points.")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.TaskPanel = _ImportPointsTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
if FreeCAD.GuiUp:
class CommandPointsGroup:
def GetCommands(self):
return tuple(['ImportPoints'
])
def GetResources(self):
return { 'MenuText': QT_TRANSLATE_NOOP("",'Cloud of Points'),
'ToolTip': QT_TRANSLATE_NOOP("",'Cloud of Points')
}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
FreeCADGui.addCommand('ImportPoints', CommandImportPoints())
FreeCADGui.addCommand('PointsGroup', CommandPointsGroup())
View File
File diff suppressed because it is too large Load Diff
+5 -519
View File
@@ -20,524 +20,10 @@
# * *
# ***********************************************************************
import FreeCAD
import utm
"""
PVPlantGeoreferencing - Wrapper de compatibilidad.
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
Código movido a PVPlant/core/georef.py.
"""
import os
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirResources as DirResources
class MapWindow(QtGui.QWidget):
def __init__(self, WinTitle="MapWindow"):
super(MapWindow, self).__init__()
self.raise_()
self.lat = None
self.lon = None
self.minLat = None
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):
from PySide2.QtWebEngineWidgets import QWebEngineView
from PySide2.QtWebChannel import QWebChannel
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
self.resize(1200, 800)
self.setWindowTitle(self.WinTitle)
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.layout = QtGui.QHBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
LeftWidget = QtGui.QWidget(self)
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
LeftWidget.setLayout(LeftLayout)
LeftLayout.setContentsMargins(0, 0, 0, 0)
RightWidget = QtGui.QWidget(self)
RightWidget.setFixedWidth(350)
RightLayout = QtGui.QVBoxLayout(RightWidget)
RightWidget.setLayout(RightLayout)
RightLayout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(LeftWidget)
self.layout.addWidget(RightWidget)
# Left Widgets:
# -- Search Bar:
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Search")
self.valueSearch.returnPressed.connect(self.onSearch)
searchbutton = QtGui.QPushButton('Search')
searchbutton.setFixedWidth(80)
searchbutton.clicked.connect(self.onSearch)
SearchBarLayout = QtGui.QHBoxLayout(self)
SearchBarLayout.addWidget(self.valueSearch)
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
# -- Webbroser:
self.view = QWebEngineView()
self.channel = QWebChannel(self.view.page())
self.view.page().setWebChannel(self.channel)
self.channel.registerObject("MyApp", self)
file = os.path.join(DirResources, "webs", "main.html")
self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view)
# self.layout.addWidget(self.view, 1, 0, 1, 3)
# -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates)
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
# Right Widgets:
labelKMZ = QtGui.QLabel()
labelKMZ.setText("Cargar un archivo KMZ/KML:")
self.kmlButton = QtGui.QPushButton()
self.kmlButton.setFixedSize(32, 32)
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
widget = QtGui.QWidget(self)
layout = QtGui.QHBoxLayout(widget)
widget.setLayout(layout)
layout.addWidget(labelKMZ)
layout.addWidget(self.kmlButton)
RightLayout.addWidget(widget)
# -----------------------
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
self.groupbox.setCheckable(True)
self.groupbox.setChecked(True)
radio1 = QtGui.QRadioButton("Google Elevation")
radio2 = QtGui.QRadioButton("Nube de Puntos")
radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True)
# buttonDialog = QtGui.QPushButton('...')
# buttonDialog.setEnabled(False)
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1)
vbox.addWidget(radio2)
vbox.addWidget(radio3)
self.groupbox.setLayout(vbox)
RightLayout.addWidget(self.groupbox)
# ------------------------
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)
self.bAccept = QtGui.QPushButton('Accept')
self.bAccept.clicked.connect(self.onAcceptClick)
RightLayout.addWidget(self.bAccept)
# signals/slots
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
def onLoadFinished(self):
file = os.path.join(DirResources, "webs", "map.js")
frame = self.view.page()
with open(file, 'r') as f:
frame.runJavaScript(f.read())
def onSearch(self):
if self.valueSearch.text() == "":
return
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="http")
location = geolocator.geocode(self.valueSearch.text())
self.valueSearch.setText(location.address)
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
def onAcceptClick(self):
frame = self.view.page()
# 1. georeferenciar
frame.runJavaScript(
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
)
# 2. importar todos los elementos dibujados:
frame.runJavaScript(
"var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));"
)
self.close()
@QtCore.Slot(float, float)
def onMapMove(self, lat, lng):
self.lat = lat
self.lon = lng
x, y, zone_number, zone_letter = utm.from_latlon(lat, lng)
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(x, y))
@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)
geolocator = Nominatim(user_agent="http")
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
if location:
if location.raw["address"].get("road"):
str = location.raw["address"]["road"]
if location.raw["address"].get("house_number"):
str += ' ({0})'.format(location.raw["address"]["house_number"])
Site.Address = str
if location.raw["address"].get("city"):
Site.City = location.raw["address"]["city"]
if location.raw["address"].get("postcode"):
Site.PostalCode = location.raw["address"]["postcode"]
if location.raw["address"].get("address"):
Site.Region = '{0}'.format(location.raw["address"]["province"])
if location.raw["address"].get("state"):
if Site.Region != "":
Site.Region += " - "
Site.Region += '{0}'.format(location.raw["address"]["state"]) # province - state
Site.Country = location.raw["address"]["country"]
@QtCore.Slot(str)
def shapes(self, drawnItems):
import geojson
import PVPlantImportGrid as ImportElevation
import Draft
import PVPlantSite
Site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(Site.Origin)
offset.z = 0
items = geojson.loads(drawnItems)
for item in items['features']:
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
coord = item['geometry']['coordinates']
point = ImportElevation.getElevationFromOE([[coord[0], coord[1]],])
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
if item['properties'].get('radius'):
r = round(item['properties']['radius'] * 1000, 0)
p = FreeCAD.Placement()
p.Base = c
obj = Draft.makeCircle(r, placement=p, face=False)
else:
''' do something '''
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10)
else: # 2. if the feature is a Polygon or Line:
cw = False
name = "Línea"
lp = item['geometry']['coordinates']
if item['geometry']['type'] == "Polygon":
cw = True
name = "Area"
lp = item['geometry']['coordinates'][0]
pts = [[cords[1], cords[0]] for cords in lp]
tmp = ImportElevation.getElevationFromOE(pts)
pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False)
#obj.Placement.Base = Site.Origin
obj.Label = name
Draft.autogroup(obj)
if item['properties'].get('name'):
obj.Label = item['properties']['name']
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
site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(site.Origin)
offset.z = 0
importer = importOSM.OSMImporter(offset)
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
importer.process_osm_data(osm_data)
'''FreeCAD.activeDocument().recompute()
FreeCADGui.updateGui()
FreeCADGui.SendMsgToActiveView("ViewFit")'''
def panMap_old(self, lng, lat, geometry=""):
frame = self.view.page()
bbox = "[{0}, {1}], [{2}, {3}]".format(float(geometry[0]), float(geometry[2]),
float(geometry[1]), float(geometry[3]))
command = 'map.panTo(L.latLng({lt}, {lg}));'.format(lt=lat, lg=lng)
command += 'map.fitBounds([{box}]);'.format(box=bbox)
frame.runJavaScript(command)
# deepseek
def panMap(self, lng, lat, geometry=None):
frame = self.view.page()
# 1. Validación del parámetro geometry
if not geometry or len(geometry) < 4:
# Pan básico sin ajuste de bounds
command = f'map.panTo(L.latLng({lat}, {lng}));'
else:
try:
# 2. Mejor manejo de coordenadas (Leaflet usa [lat, lng])
# Asumiendo que geometry es [min_lng, min_lat, max_lng, max_lat]
southwest = f"{float(geometry[1])}, {float(geometry[0])}" # min_lat, min_lng
northeast = f"{float(geometry[3])}, {float(geometry[2])}" # max_lat, max_lng
command = f'map.panTo(L.latLng({lat}, {lng}));'
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
except (IndexError, ValueError, TypeError) as e:
print(f"Error en geometry: {str(e)}")
command = f'map.panTo(L.latLng({lat}, {lng}));'
frame.runJavaScript(command)
def importKML(self):
file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0]
from lib.kml2geojson import kmz_convert
layers = kmz_convert(file, "", )
frame = self.view.page()
for layer in layers:
command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer)
frame.runJavaScript(command)
class CommandPVPlantGeoreferencing:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
def Activated(self):
self.form = MapWindow()
self.form.show()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantGeoreferencing',_CommandPVPlantGeoreferencing())
'''
from PVPlant.core.georef import MapWindow, CommandPVPlantGeoreferencing
+21 -671
View File
@@ -20,674 +20,24 @@
# * *
# ***********************************************************************
import json
import urllib.request
import Draft
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import os
from PVPlantResources import DirIcons as DirIcons
import PVPlantSite
def get_elevation_from_oe(coordinates): # v1 deepseek
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
Args:
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
Returns:
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
o lista vacía en caso de error.
"""
if not coordinates:
return []
import requests
import utm
from requests.exceptions import RequestException
# Construcción más eficiente de parámetros
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
try:
response = requests.get(
url="https://api.open-elevation.com/api/v1/lookup",
params={'locations': locations},
timeout=20,
verify=True
)
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx
except RequestException as e:
print(f"Error en la solicitud: {str(e)}")
return []
try:
data = response.json()
except ValueError:
print("Respuesta JSON inválida")
return []
if "results" not in data or len(data["results"]) != len(coordinates):
print("Formato de respuesta inesperado")
return []
points = []
for result in data["results"]:
try:
# Conversión UTM con manejo de errores
easting, northing, _, _ = utm.from_latlon(
result["latitude"],
result["longitude"]
)
points.append(FreeCAD.Vector(round(easting), # Convertir metros a milímetros
round(northing),
round(result["elevation"])) * 1000)
except Exception as e:
print(f"Error procesando coordenadas: {str(e)}")
continue
return points
def getElevationFromOE(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
import certifi
from requests.exceptions import RequestException
if len(coordinates) == 0:
return None
from requests import get
import utm
locations_str=""
total = len(coordinates) - 1
for i, point in enumerate(coordinates):
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
if i != total:
locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
try:
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
except RequestException as e:
print(f"Error en la solicitud: {str(e)}")
points = []
for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1])
points.append(FreeCAD.Vector(round(c[0], 0),
round(c[1], 0),
0) * 1000)
return points
# Only get the json response in case of 200 or 201
points = []
if r.status_code == 200 or r.status_code == 201:
results = r.json()
for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"])
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"
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
import requests
response = requests.get(source)
ans = response.text
s = json.loads(ans)
print(s)
res = s['resourceSets'][0]['resources'][0]['elevations']
for elevation in res:
c = utm.from_latlon(lat, lng)
v = FreeCAD.Vector(
round(c[0] * 1000, 0),
round(c[1] * 1000, 0),
round(elevation * 1000, 0))
return v
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
#http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=35.89431,-110.72522,35.89393,-110.72578,35.89374,-110.72606,35.89337,-110.72662
# &heights=ellipsoid&samples=10&key={BingMapsAPIKey}
import utm
import math
import requests
geo = utm.from_latlon(lat, lng)
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
#StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
xx = polygon.Shape.BoundBox.XMin
while xx < polygon.Shape.BoundBox.XMax:
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
if StepsXX > 1000:
StepsXX = 1000
xx1 = xx + 1000 * resolution
else:
xx1 = xx + StepsXX * resolution
point1 = utm.to_latlon(xx / 1000, yy / 1000, geo[2], geo[3])
point2 = utm.to_latlon(xx1 / 1000, yy / 1000, geo[2], geo[3])
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
source += ","
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
source += "&heights=sealevel"
source += "&samples={steps}".format(steps=StepsXX)
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['resourceSets'][0]['resources'][0]['elevations']
i = 0
for elevation in res:
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
points.append(v)
i += 1
xx = xx1 + resolution # para no repetir un mismo punto
yy -= resolution
return points
def getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2)
)
return v
def _getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
import pymap3d as pm
for r in res:
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
round(r['location']['lat'], 2),
0)
v = FreeCAD.Vector(x,y,z)
return v
def getSinglePointElevation1(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 4),
round(c[1], 4),
round(r['elevation'] * 1000, 2)
)
return v
def getSinglePointElevationUtm(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
print(source)
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
print (res)
import utm
for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0] * 1000, 4),
round(c[1] * 1000, 4),
round(r['elevation'] * 1000, 2))
print (v)
return v
def getElevationUTM(polygon, lat, lng, resolution = 10000):
import utm
geo = utm.from_latlon(lat, lng)
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
# utm.to_latlon(EASTING, NORTHING, ZONE NUMBER, ZONE LETTER).
# result = (LATITUDE, LONGITUDE)
point1 = utm.to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, geo[2], geo[3])
point2 = utm.to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, geo[2], geo[3])
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0] * 1000, 2),
round(c[1] * 1000, 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
yy -= (resolution*1000)
FreeCAD.activeDocument().recompute()
return points
def getElevation1(polygon,resolution=10):
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
try:
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
except:
continue
#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)
)
points.append(v)
FreeCAD.activeDocument().recompute()
yy -= (resolution*1000)
return points
## download the heights from google:
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
#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']
from geopy.distance import geodesic
points = []
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lat'])).m, 2),
round(geodesic(reference, (r['location']['lng'], 0.0)).m, 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):
self.obj = None
self.Boundary = None
self.select = 0
self.filename = ""
# form:
self.form1 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantImportGrid.ui")
self.form1.radio1.toggled.connect(lambda: self.mainToggle(self.form1.radio1))
self.form1.radio2.toggled.connect(lambda: self.mainToggle(self.form1.radio2))
self.form1.radio1.setChecked(True) # << --------------Poner al final para que no dispare antes de crear los componentes a los que va a llamar
#self.form.buttonAdd.clicked.connect(self.add)
self.form1.buttonDEM.clicked.connect(self.openFileDEM)
self.form2 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantCreateTerrainMesh.ui")
#self.form2.buttonAdd.clicked.connect(self.add)
self.form2.buttonBoundary.clicked.connect(self.addBoundary)
#self.form = [self.form1, self.form2]
self.form = self.form1
''' future:
def retranslateUi(self, dialog):
self.form1.setWindowTitle("Configuracion del Rack")
self.labelModule.setText(QtGui.QApplication.translate("PVPlant", "Modulo:", None))
self.labelModuleLength.setText(QtGui.QApplication.translate("PVPlant", "Longitud:", None))
self.labelModuleWidth.setText(QtGui.QApplication.translate("PVPlant", "Ancho:", None))
self.labelModuleHeight.setText(QtGui.QApplication.translate("PVPlant", "Alto:", None))
self.labelModuleFrame.setText(QtGui.QApplication.translate("PVPlant", "Ancho del marco:", None))
self.labelModuleColor.setText(QtGui.QApplication.translate("PVPlant", "Color del modulo:", None))
self.labelModules.setText(QtGui.QApplication.translate("Arch", "Colocacion de los Modulos", None))
self.labelModuleOrientation.setText(QtGui.QApplication.translate("Arch", "Orientacion del modulo:", None))
self.labelModuleGapX.setText(QtGui.QApplication.translate("Arch", "Separacion Horizontal (mm):", None))
self.labelModuleGapY.setText(QtGui.QApplication.translate("Arch", "Separacion Vertical (mm):", None))
self.labelModuleRows.setText(QtGui.QApplication.translate("Arch", "Filas de modulos:", None))
self.labelModuleCols.setText(QtGui.QApplication.translate("Arch", "Columnas de modulos:", None))
self.labelRack.setText(QtGui.QApplication.translate("Arch", "Configuracion de la estructura", None))
self.labelRackType.setText(QtGui.QApplication.translate("Arch", "Tipo de estructura:", None))
self.labelLevel.setText(QtGui.QApplication.translate("Arch", "Nivel:", None))
self.labelOffset.setText(QtGui.QApplication.translate("Arch", "Offset", None))
'''
def add(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.obj = sel[0]
self.lineEdit1.setText(self.obj.Label)
def addBoundary(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Boundary = sel[0]
self.form2.editBoundary.setText(self.Boundary.Label)
def openFileDEM(self):
filters = "Esri ASC (*.asc);;CSV (*.csv);;All files (*.*)"
filename = QtGui.QFileDialog.getOpenFileName(None,
"Open DEM,",
"",
filters)
self.filename = filename[0]
self.form1.editDEM.setText(filename[0])
def mainToggle(self, radiobox):
if radiobox is self.form1.radio1:
self.select = 0
self.form1.gbLocalFile.setVisible(True)
elif radiobox is self.form1.radio2:
self.select = 1
self.form1.gbLocalFile.setVisible(True)
def accept(self):
from datetime import datetime
starttime = datetime.now()
site = PVPlantSite.get()
try:
PointGroups = FreeCAD.ActiveDocument.Point_Groups
except:
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
PointGroups.Label = "Point Groups"
PointGroup = FreeCAD.ActiveDocument.addObject('Points::Feature', "Point_Group")
PointGroup.Label = "Land_Grid_Points"
FreeCAD.ActiveDocument.Point_Groups.addObject(PointGroup)
PointObject = PointGroup.Points.copy()
if self.select == 0: # Google or bing or ...
#for item in self.obj:
#if self.groupbox.isChecked:break
resol = FreeCAD.Units.Quantity(self.valueResolution.text()).Value
Site = FreeCAD.ActiveDocument.Site
pts = getGridElevationFromBing(self.obj, Site.Latitude, Site.Longitude, resol)
PointObject.addPoints(pts)
PointGroup.Points = PointObject
else:
if self.filename == "":
return
import Utils.importDEM as openDEM
if self.select == 1: # DEM.
import numpy as np
root, extension = os.path.splitext(self.filename)
if extension.lower() == ".asc":
x, y, datavals, cellsize, nodata_value = openDEM.openEsri(self.filename)
if self.Boundary:
inc_x = self.Boundary.Shape.BoundBox.XLength * 0.05
inc_y = self.Boundary.Shape.BoundBox.YLength * 0.05
min_x = 0
max_x = 0
comp = (self.Boundary.Shape.BoundBox.XMin - inc_x) / 1000
for i in range(nx):
if x[i] > comp:
min_x = i - 1
break
comp = (self.Boundary.Shape.BoundBox.XMax + inc_x) / 1000
for i in range(min_x, nx):
if x[i] > comp:
max_x = i
break
min_y = 0
max_y = 0
comp = (self.Boundary.Shape.BoundBox.YMax + inc_y) / 1000
for i in range(ny):
if y[i] < comp:
max_y = i
break
comp = (self.Boundary.Shape.BoundBox.YMin - inc_y) / 1000
for i in range(max_y, ny):
if y[i] < comp:
min_y = i
break
x = x[min_x:max_x]
y = y[max_y:min_y]
datavals = datavals[max_y:min_y, min_x:max_x]
pts = []
if True: # faster but more memory 46s - 4,25 gb
x, y = np.meshgrid(x, y)
xx = x.flatten()
yy = y.flatten()
zz = datavals.flatten()
x[:] = 0
y[:] = 0
datavals[:] = 0
pts = []
for i in range(0, len(xx)):
pts.append(FreeCAD.Vector(xx[i], yy[i], zz[i]) * 1000)
xx[:] = 0
yy[:] = 0
zz[:] = 0
else: # 51s 3,2 gb
createmesh = True
if createmesh:
import Part, Draft
lines=[]
for j in range(len(y)):
edges = []
for i in range(0, len(x) - 1):
ed = Part.makeLine(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000,
FreeCAD.Vector(x[i + 1], y[j], datavals[j][i + 1]) * 1000)
edges.append(ed)
#bspline = Draft.makeBSpline(pts)
#bspline.ViewObject.hide()
line = Part.Wire(edges)
lines.append(line)
'''
for i in range(0, len(bsplines), 100):
p = Part.makeLoft(bsplines[i:i + 100], False, False, False)
Part.show(p)
'''
p = Part.makeLoft(lines, False, True, False)
p = Part.Solid(p)
Part.show(p)
else:
pts = []
for j in range(ny):
for i in range(nx):
pts.append(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000)
elif extension.lower() == ".csv" or extension.lower() == ".txt": # x, y, z from gps
pts = openDEM.interpolatePoints(openDEM.openCSV(self.filename))
PointObject.addPoints(pts)
PointGroup.Points = PointObject
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Control.closeDialog()
print("tiempo: ", datetime.now() - starttime)
def reject(self):
FreeCADGui.Control.closeDialog()
## Comandos -----------------------------------------------------------------------------------------------------------
class CommandImportPoints:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "cloud.svg")),
'MenuText': QT_TRANSLATE_NOOP("PVPlant", "Importer Grid"),
'Accel': "B, U",
'ToolTip': QT_TRANSLATE_NOOP("PVPlant", "Creates a cloud of points.")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.TaskPanel = _ImportPointsTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
if FreeCAD.GuiUp:
class CommandPointsGroup:
def GetCommands(self):
return tuple(['ImportPoints'
])
def GetResources(self):
return { 'MenuText': QT_TRANSLATE_NOOP("",'Cloud of Points'),
'ToolTip': QT_TRANSLATE_NOOP("",'Cloud of Points')
}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
FreeCADGui.addCommand('ImportPoints', CommandImportPoints())
FreeCADGui.addCommand('PointsGroup', CommandPointsGroup())
"""
PVPlantImportGrid - Wrapper de compatibilidad.
Código movido a PVPlant/import_grid/grid.py.
"""
from PVPlant.import_grid.grid import (
get_elevation_from_oe,
getElevationFromOE,
getSinglePointElevationFromBing,
getGridElevationFromBing,
getSinglePointElevation,
_getSinglePointElevation,
getSinglePointElevation1,
getSinglePointElevationUtm,
getElevationUTM,
getElevation1,
getElevation,
_ImportPointsTaskPanel,
CommandImportPoints,
)
+9 -1116
View File
File diff suppressed because it is too large Load Diff
+27 -1172
View File
File diff suppressed because it is too large Load Diff
+129 -85
View File
@@ -73,6 +73,42 @@ line_patterns = {
"Dot (.5x) ...............................": 0x5555,
"Dot (2x) . . . . . . . . . . .": 0x8888}
def open_xyz_mmap(archivo_path):
"""
Usa memory-mapping para archivos muy grandes (máxima velocidad)
"""
# Primera pasada: contar líneas válidas
total_puntos = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
float(partes[0]);
float(partes[1]);
float(partes[2])
total_puntos += 1
except:
continue
# Segunda pasada: cargar datos
puntos = np.empty((total_puntos, 3))
idx = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
x, y, z = float(partes[0]), float(partes[1]), float(partes[2])
puntos[idx] = [x, y, z]
idx += 1
except:
continue
return puntos
def makeTerrain(name="Terrain"):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain")
obj.Label = name
@@ -81,7 +117,6 @@ def makeTerrain(name="Terrain"):
FreeCAD.ActiveDocument.recompute()
return obj
class Terrain(ArchComponent.Component):
"A Shadow Terrain Obcject"
@@ -161,101 +196,110 @@ class Terrain(ArchComponent.Component):
if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime
if obj.DEM and obj.CuttingBoundary:
'''
Parámetro Descripción Requisitos
NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
NROWS: Cantidad de filas de celdas Entero mayor que 0.
XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y.
YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x.
CELLSIZE: Tamaño de celda Mayor que 0.
NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999
'''
grid_space = 1
file = open(obj.DEM, "r")
templist = [line.split() for line in file.readlines()]
file.close()
del file
from pathlib import Path
suffix = Path(obj.DEM).suffix
if suffix == '.asc':
'''
ASC format:
Parámetro Descripción Requisitos
NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
NROWS: Cantidad de filas de celdas Entero mayor que 0.
XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y.
YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x.
CELLSIZE: Tamaño de celda Mayor que 0.
NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999
'''
grid_space = 1
file = open(obj.DEM, "r")
templist = [line.split() for line in file.readlines()]
file.close()
del file
# Read meta data:
meta = templist[0:6]
nx = int(meta[0][1]) # NCOLS
ny = int(meta[1][1]) # NROWS
xllref = meta[2][0] # XLLCENTER / XLLCORNER
xllvalue = round(float(meta[2][1]), 3)
yllref = meta[3][0] # YLLCENTER / XLLCORNER
yllvalue = round(float(meta[3][1]), 3)
cellsize = round(float(meta[4][1]), 3) # CELLSIZE
nodata_value = float(meta[5][1]) # NODATA_VALUE
# Read meta data:
meta = templist[0:6]
nx = int(meta[0][1]) # NCOLS
ny = int(meta[1][1]) # NROWS
xllref = meta[2][0] # XLLCENTER / XLLCORNER
xllvalue = round(float(meta[2][1]), 3)
yllref = meta[3][0] # YLLCENTER / XLLCORNER
yllvalue = round(float(meta[3][1]), 3)
cellsize = round(float(meta[4][1]), 3) # CELLSIZE
nodata_value = float(meta[5][1]) # NODATA_VALUE
# set coarse_factor
coarse_factor = max(round(grid_space / cellsize), 1)
# set coarse_factor
coarse_factor = max(round(grid_space / cellsize), 1)
# Get z values
templist = templist[6:(6 + ny)]
templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)]
datavals = np.array(templist).astype(float)
del templist
# Get z values
templist = templist[6:(6 + ny)]
templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)]
datavals = np.array(templist).astype(float)
del templist
# create xy coordinates
offset = self.site.Origin
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
datavals = datavals * 1000 # Ajuste de altura
# create xy coordinates
offset = self.site.Origin
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
datavals = datavals * 1000 # Ajuste de altura
# remove points out of area
# 1. coarse:
if obj.CuttingBoundary:
inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0
inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0
tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x),
x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
x_max = np.ndarray.max(tmp)
x_min = np.ndarray.min(tmp)
# remove points out of area
# 1. coarse:
if obj.CuttingBoundary:
inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0
inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0
tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x),
x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
x_max = np.ndarray.max(tmp)
x_min = np.ndarray.min(tmp)
tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y),
y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0]
y_max = np.ndarray.max(tmp)
y_min = np.ndarray.min(tmp)
del tmp
tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y),
y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0]
y_max = np.ndarray.max(tmp)
y_min = np.ndarray.min(tmp)
del tmp
x = x[x_min:x_max+1]
y = y[y_min:y_max+1]
datavals = datavals[y_min:y_max+1, x_min:x_max+1]
x = x[x_min:x_max+1]
y = y[y_min:y_max+1]
datavals = datavals[y_min:y_max+1, x_min:x_max+1]
# Create mesh - surface:
import MeshTools.Triangulation as Triangulation
import Mesh
stepsize = 75
stepx = math.ceil(nx / stepsize)
stepy = math.ceil(ny / stepsize)
# Create mesh - surface:
import MeshTools.Triangulation as Triangulation
import Mesh
stepsize = 75
stepx = math.ceil(nx / stepsize)
stepy = math.ceil(ny / stepsize)
mesh = Mesh.Mesh()
for indx in range(stepx):
inix = indx * stepsize - 1
finx = min([stepsize * (indx + 1), len(x)-1])
for indy in range(stepy):
iniy = indy * stepsize - 1
finy = min([stepsize * (indy + 1), len(y) - 1])
pts = []
for i in range(inix, finx):
for j in range(iniy, finy):
if datavals[j][i] != nodata_value:
if obj.CuttingBoundary:
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
mesh = Mesh.Mesh()
for indx in range(stepx):
inix = indx * stepsize - 1
finx = min([stepsize * (indx + 1), len(x)-1])
for indy in range(stepy):
iniy = indy * stepsize - 1
finy = min([stepsize * (indy + 1), len(y) - 1])
pts = []
for i in range(inix, finx):
for j in range(iniy, finy):
if datavals[j][i] != nodata_value:
if obj.CuttingBoundary:
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
pts.append([x[i], y[j], datavals[j][i]])
else:
pts.append([x[i], y[j], datavals[j][i]])
else:
pts.append([x[i], y[j], datavals[j][i]])
if len(pts) > 3:
try:
triangulated = Triangulation.Triangulate(pts)
mesh.addMesh(triangulated)
except TypeError:
print(f"Error al procesar {len(pts)} puntos: {str(e)}")
if len(pts) > 3:
try:
triangulated = Triangulation.Triangulate(pts)
mesh.addMesh(triangulated)
except TypeError:
print(f"Error al procesar {len(pts)} puntos: {str(e)}")
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
Mesh.show(mesh)
elif suffix in ['.xyz']:
data = open_xyz_mmap(obj.DEM)
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
Mesh.show(mesh)
if prop == "PointsGroup" or prop == "CuttingBoundary":
if obj.PointsGroup and obj.CuttingBoundary:
+6 -25
View File
@@ -54,30 +54,6 @@ class CommandPVPlantSite:
return
'''class CommandPVPlantGeoreferencing:
@staticmethod
def GetResources():
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
@staticmethod
def IsActive():
if FreeCAD.ActiveDocument:
return True
else:
return False
@staticmethod
def Activated():
import PVPlantGeoreferencing
taskd = PVPlantGeoreferencing.MapWindow()
#taskd.setParent(FreeCADGui.getMainWindow())
#taskd.setWindowFlags(QtCore.Qt.Window)
taskd.show()#exec_()'''
class CommandProjectSetup:
@staticmethod
def GetResources():
@@ -676,6 +652,9 @@ if FreeCAD.GuiUp:
from Civil.Fence import PVPlantFence
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
import docgenerator
FreeCADGui.addCommand('GenerateDocuments', docgenerator.generateDocuments())
projectlist = [ # "Reload",
"PVPlantSite",
"ProjectSetup",
@@ -711,4 +690,6 @@ pv_mechanical = [
]
objectlist = ['PVPlantTree',
'PVPlantFenceGroup',]
'PVPlantFenceGroup',
'GenerateDocuments',
]
+377 -119
View File
@@ -26,6 +26,9 @@ import PVPlantSite
import Utils.PVPlantUtils as utils
import MeshPart as mp
import pivy
from pivy import coin
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
@@ -361,12 +364,12 @@ class OffsetArea(_Area):
wire = utils.getProjected(base, vec)
wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
sections = mp.projectShapeOnMesh(wire, land, vec)
print(" javi ", sections)
pts = []
for section in sections:
pts.extend(section)
# Crear forma solo si hay resultados
if sections:
if len(pts)>0:
obj.Shape = Part.makePolygon(pts)
else:
obj.Shape = Part.Shape() # Forma vacía si falla
@@ -412,35 +415,9 @@ class ProhibitedArea(OffsetArea):
self.Type = obj.Type = "ProhibitedArea"
obj.Proxy = self
'''# Propiedades de color
if not hasattr(obj, "OriginalColor"):
obj.addProperty("App::PropertyColor",
"OriginalColor",
"Display",
"Color for original wire")
obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
if not hasattr(obj, "OffsetColor"):
obj.addProperty("App::PropertyColor",
"OffsetColor",
"Display",
"Color for offset wire")
obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
# Propiedades de grosor
if not hasattr(obj, "OriginalWidth"):
obj.addProperty("App::PropertyFloat",
"OriginalWidth",
"Display",
"Line width for original wire")
obj.OriginalWidth = 4.0
if not hasattr(obj, "OffsetWidth"):
obj.addProperty("App::PropertyFloat",
"OffsetWidth",
"Display",
"Line width for offset wire")
obj.OffsetWidth = 4.0'''
def onDocumentRestored(self, obj):
"""Method run when the document is restored."""
self.setProperties(obj)
def execute(self, obj):
# Comprobar dependencias
@@ -482,121 +459,402 @@ class ProhibitedArea(OffsetArea):
obj.Shape = Part.Shape()
# Actualizar colores en la vista
if FreeCAD.GuiUp and obj.ViewObject:
obj.ViewObject.Proxy.updateVisual()
"""if FreeCAD.GuiUp and obj.ViewObject:
obj.ViewObject.Proxy.updateVisual()"""
class ViewProviderForbiddenArea(_ViewProviderArea):
class ViewProviderForbiddenArea_old:
def __init__(self, vobj):
super().__init__(vobj)
# Valores por defecto
self.original_color = (1.0, 0.0, 0.0) # Rojo
self.offset_color = (1.0, 0.5, 0.0) # Naranja
self.original_width = 4.0
self.offset_width = 4.0
self.line_widths = [] # Almacenará los grosores por arista
vobj.Proxy = self
self.setProperties(vobj)
vobj.LineColor = (1.0, 0.0, 0.0)
vobj.LineWidth = 4
vobj.PointColor = (1.0, 0.0, 0.0)
vobj.PointSize = 4
def setProperties(self, vobj):
# Propiedades de color
if not hasattr(vobj, "OriginalColor"):
vobj.addProperty("App::PropertyColor",
"OriginalColor",
"ObjectStyle",
"Color for original wire")
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
def getIcon(self):
''' Return object treeview icon. '''
return str(os.path.join(DirIcons, "area_forbidden.svg"))
if not hasattr(vobj, "OffsetColor"):
vobj.addProperty("App::PropertyColor",
"OffsetColor",
"ObjectStyle",
"Color for offset wire")
vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo
def claimChildren(self):
""" Provides object grouping """
children = []
if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.ViewObject.Object.Base)
return children
# Propiedades de grosor
if not hasattr(vobj, "OriginalWidth"):
vobj.addProperty("App::PropertyFloat",
"OriginalWidth",
"ObjectStyle",
"Line width for original wire")
vobj.OriginalWidth = 4.0
if not hasattr(vobj, "OffsetWidth"):
vobj.addProperty("App::PropertyFloat",
"OffsetWidth",
"ObjectStyle",
"Line width for offset wire")
vobj.OffsetWidth = 4.0
# Deshabilitar el color por defecto
vobj.setPropertyStatus("LineColor", "Hidden")
vobj.setPropertyStatus("PointColor", "Hidden")
vobj.setPropertyStatus("ShapeAppearance", "Hidden")
def attach(self, vobj):
super().attach(vobj)
# Inicializar visualización
self.updateVisual()
self.ViewObject = vobj
self.Object = vobj.Object
def updateVisual(self):
"""Actualiza colores y grosores de línea"""
if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object:
return
# Crear la estructura de escena Coin3D
self.root = coin.SoGroup()
obj = self.ViewObject.Object
# Switch para habilitar/deshabilitar la selección
self.switch = coin.SoSwitch()
self.switch.whichChild = coin.SO_SWITCH_ALL
# Obtener propiedades de color y grosor
try:
self.original_color = obj.OriginalColor
self.offset_color = obj.OffsetColor
self.original_width = obj.OriginalWidth
self.offset_width = obj.OffsetWidth
except:
pass
# Separador para el wire original
self.original_sep = coin.SoSeparator()
self.original_color = coin.SoBaseColor()
self.original_coords = coin.SoCoordinate3()
self.original_line_set = coin.SoLineSet()
self.original_draw_style = coin.SoDrawStyle()
# Actualizar colores si hay forma
if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull():
if len(obj.Shape.SubShapes) >= 2:
# Asignar colores
colors = []
colors.append(self.original_color) # Primer wire (original)
colors.append(self.offset_color) # Segundo wire (offset)
self.ViewObject.DiffuseColor = colors
# Separador para el wire offset
self.offset_sep = coin.SoSeparator()
self.offset_color = coin.SoBaseColor()
self.offset_coords = coin.SoCoordinate3()
self.offset_line_set = coin.SoLineSet()
self.offset_draw_style = coin.SoDrawStyle()
# Preparar grosores por arista
#self.prepareLineWidths()
# Construir la jerarquía de escena
self.original_sep.addChild(self.original_color)
self.original_sep.addChild(self.original_draw_style)
self.original_sep.addChild(self.original_coords)
self.original_sep.addChild(self.original_line_set)
# Asignar grosores usando LineWidthArray
'''if self.line_widths:
self.ViewObject.LineWidthArray = self.line_widths'''
self.offset_sep.addChild(self.offset_color)
self.offset_sep.addChild(self.offset_draw_style)
self.offset_sep.addChild(self.offset_coords)
self.offset_sep.addChild(self.offset_line_set)
# Establecer grosor global como respaldo
#self.ViewObject.LineWidth = max(self.original_width, self.offset_width)
self.switch.addChild(self.original_sep)
self.switch.addChild(self.offset_sep)
self.root.addChild(self.switch)
def prepareLineWidths(self):
"""Prepara la lista de grosores para cada arista"""
self.line_widths = []
obj = self.ViewObject.Object
vobj.addDisplayMode(self.root, "Wireframe")
if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull():
# Contar aristas en cada subforma
for i, subshape in enumerate(obj.Shape.SubShapes):
edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1
# Inicializar estilos de dibujo
self.original_draw_style.style = coin.SoDrawStyle.LINES
self.offset_draw_style.style = coin.SoDrawStyle.LINES
# Determinar grosor según tipo de wire
width = self.original_width if i == 0 else self.offset_width
# Asignar el mismo grosor a todas las aristas de este wire
self.line_widths.extend([width] * edge_count)
def onChanged(self, vobj, prop):
"""Maneja cambios en propiedades de visualización"""
if prop in ["LineColor", "PointColor", "ShapeColor", "LineWidth"]:
# Actualizar visualización inicial
if hasattr(self.Object, 'Shape'):
self.updateData(self.Object, "Shape")
self.updateVisual()
def updateData(self, obj, prop):
"""Actualiza cuando cambian los datos del objeto"""
if prop == "Shape":
if prop == "Shape" and obj.Shape and not obj.Shape.isNull():
self.updateGeometry()
def updateGeometry(self):
"""Actualiza la geometría en la escena 3D"""
if not hasattr(self, 'Object') or not self.Object.Shape or self.Object.Shape.isNull():
return
# Limpiar coordenadas existentes
self.original_coords.point.deleteValues(0)
self.offset_coords.point.deleteValues(0)
# Obtener los sub-shapes
subshapes = []
if hasattr(self.Object.Shape, 'SubShapes') and self.Object.Shape.SubShapes:
subshapes = self.Object.Shape.SubShapes
elif hasattr(self.Object.Shape, 'ChildShapes') and self.Object.Shape.ChildShapes:
subshapes = self.Object.Shape.ChildShapes
# Procesar wire original (primer sub-shape)
if len(subshapes) > 0:
self.processShape(subshapes[0], self.original_coords, self.original_line_set)
# Procesar wire offset (segundo sub-shape)
if len(subshapes) > 1:
self.processShape(subshapes[1], self.offset_coords, self.offset_line_set)
# Actualizar colores y grosores
self.updateVisual()
def processShape(self, shape, coords_node, lineset_node):
"""Procesa una forma y la añade al nodo de coordenadas"""
if not shape or shape.isNull():
return
points = []
line_indices = []
current_index = 0
# Obtener todos los edges de la forma
edges = []
if hasattr(shape, 'Edges'):
edges = shape.Edges
elif hasattr(shape, 'ChildShapes'):
for child in shape.ChildShapes:
if hasattr(child, 'Edges'):
edges.extend(child.Edges)
for edge in edges:
try:
# Discretizar la curva para obtener puntos
vertices = edge.discretize(Number=50)
for i, vertex in enumerate(vertices):
points.append([vertex.x, vertex.y, vertex.z])
line_indices.append(current_index)
current_index += 1
# Añadir -1 para indicar fin de línea
line_indices.append(-1)
except Exception as e:
print(f"Error processing edge: {e}")
continue
# Configurar coordenadas y líneas
if points:
coords_node.point.setValues(0, len(points), points)
lineset_node.numVertices.deleteValues(0)
lineset_node.numVertices.setValues(0, len(line_indices), line_indices)
def updateVisual(self):
"""Actualiza colores y grosores según las propiedades"""
if not hasattr(self, 'ViewObject') or not self.ViewObject:
return
vobj = self.ViewObject
try:
# Configurar wire original
if hasattr(vobj, "OriginalColor"):
original_color = vobj.OriginalColor
self.original_color.rgb.setValue(original_color[0], original_color[1], original_color[2])
if hasattr(vobj, "OriginalWidth"):
self.original_draw_style.lineWidth = vobj.OriginalWidth
# Configurar wire offset
if hasattr(vobj, "OffsetColor"):
offset_color = vobj.OffsetColor
self.offset_color.rgb.setValue(offset_color[0], offset_color[1], offset_color[2])
if hasattr(vobj, "OffsetWidth"):
self.offset_draw_style.lineWidth = vobj.OffsetWidth
except Exception as e:
print(f"Error updating visual: {e}")
def onChanged(self, vobj, prop):
"""Maneja cambios en propiedades"""
if prop in ["OriginalColor", "OffsetColor", "OriginalWidth", "OffsetWidth"]:
self.updateVisual()
'''def __getstate__(self):
return {
"original_color": self.original_color,
"offset_color": self.offset_color,
"original_width": self.original_width,
"offset_width": self.offset_width
}
def getDisplayModes(self, obj):
return ["Wireframe"]
def getDefaultDisplayMode(self):
return "Wireframe"
def setDisplayMode(self, mode):
return mode
def claimChildren(self):
"""Proporciona agrupamiento de objetos"""
children = []
if hasattr(self, 'Object') and self.Object and hasattr(self.Object, "Base"):
children.append(self.Object.Base)
return children
def getIcon(self):
'''Return object treeview icon'''
return str(os.path.join(DirIcons, "area_forbidden.svg"))
def onDocumentRestored(self, vobj):
"""Método ejecutado cuando el documento es restaurado"""
self.ViewObject = vobj
self.Object = vobj.Object
self.setProperties(vobj)
self.attach(vobj)
def __getstate__(self):
return None
def __setstate__(self, state):
if "original_color" in state:
self.original_color = state["original_color"]
if "offset_color" in state:
self.offset_color = state["offset_color"]
if "original_width" in state:
self.original_width = state.get("original_width", 4.0)
if "offset_width" in state:
self.offset_width = state.get("offset_width", 4.0)'''
return None
class ViewProviderForbiddenArea:
def __init__(self, vobj):
vobj.Proxy = self
self.ViewObject = vobj
# Inicializar propiedades PRIMERO
self.setProperties(vobj)
# Configurar colores iniciales
self.updateColors(vobj)
def setProperties(self, vobj):
if not hasattr(vobj, "OriginalColor"):
vobj.addProperty("App::PropertyColor",
"OriginalColor",
"Display",
"Color for original wire")
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
if not hasattr(vobj, "OffsetColor"):
vobj.addProperty("App::PropertyColor",
"OffsetColor",
"Display",
"Color for offset wire")
vobj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
def updateColors(self, vobj):
"""Actualiza los colores desde las propiedades"""
try:
if hasattr(vobj, "OriginalColor"):
self.original_color.rgb.setValue(*vobj.OriginalColor)
else:
self.original_color.rgb.setValue(1.0, 0.0, 0.0)
if hasattr(vobj, "OffsetColor"):
self.offset_color.rgb.setValue(*vobj.OffsetColor)
else:
self.offset_color.rgb.setValue(1.0, 0.5, 0.0)
except Exception as e:
print(f"Error en updateColors: {e}")
def onDocumentRestored(self, vobj):
self.setProperties(vobj)
# No llamar a __init__ de nuevo, solo actualizar propiedades
self.updateColors(vobj)
def getIcon(self):
return str(os.path.join(DirIcons, "area_forbidden.svg"))
def attach(self, vobj):
self.ViewObject = vobj
# Inicializar nodos Coin3D
self.root = coin.SoGroup()
self.original_coords = coin.SoCoordinate3()
self.offset_coords = coin.SoCoordinate3()
self.original_color = coin.SoBaseColor()
self.offset_color = coin.SoBaseColor()
self.original_lineset = coin.SoLineSet()
self.offset_lineset = coin.SoLineSet()
# Añadir un nodo de dibujo para establecer el estilo de línea
self.draw_style = coin.SoDrawStyle()
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = 3.0
# Construir la escena
self.root.addChild(self.draw_style)
# Grupo para el polígono original
original_group = coin.SoGroup()
original_group.addChild(self.original_color)
original_group.addChild(self.original_coords)
original_group.addChild(self.original_lineset)
# Grupo para el polígono offset
offset_group = coin.SoGroup()
offset_group.addChild(self.offset_color)
offset_group.addChild(self.offset_coords)
offset_group.addChild(self.offset_lineset)
self.root.addChild(original_group)
self.root.addChild(offset_group)
vobj.addDisplayMode(self.root, "Standard")
# Asegurar que la visibilidad esté activada
vobj.Visibility = True
def updateData(self, obj, prop):
if prop == "Shape":
self.updateVisual(obj)
def updateVisual(self, obj):
"""Actualiza la representación visual basada en la forma del objeto"""
if not hasattr(obj, 'Shape') or not obj.Shape or obj.Shape.isNull():
return
try:
# Obtener todos los bordes de la forma compuesta
all_edges = obj.Shape.Edges
# Separar bordes por polígono (asumimos que el primer polígono es el original)
# Esto es una simplificación - podrías necesitar una lógica más sofisticada
if len(all_edges) >= 2:
# Polígono original - primer conjunto de bordes
original_edges = [all_edges[0]]
original_points = []
for edge in original_edges:
for vertex in edge.Vertexes:
original_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
# Polígono offset - segundo conjunto de bordes
offset_edges = [all_edges[1]]
offset_points = []
for edge in offset_edges:
for vertex in edge.Vertexes:
offset_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
# Asignar puntos a los nodos Coordinate3
if original_points:
self.original_coords.point.setValues(0, len(original_points), original_points)
self.original_lineset.numVertices.setValue(len(original_points))
if offset_points:
self.offset_coords.point.setValues(0, len(offset_points), offset_points)
self.offset_lineset.numVertices.setValue(len(offset_points))
# Actualizar colores
if hasattr(obj, 'ViewObject') and obj.ViewObject:
self.updateColors(obj.ViewObject)
except Exception as e:
print(f"Error en updateVisual: {e}")
def onChanged(self, vobj, prop):
if prop in ["OriginalColor", "OffsetColor"]:
self.updateColors(vobj)
elif prop == "Visibility" and vobj.Visibility:
# Cuando la visibilidad cambia a True, actualizar visual
self.updateVisual(vobj.Object)
def getDisplayModes(self, obj):
return ["Standard"]
def getDefaultDisplayMode(self):
return "Standard"
def setDisplayMode(self, mode):
return mode
def claimChildren(self):
children = []
if hasattr(self, 'ViewObject') and self.ViewObject and hasattr(self.ViewObject.Object, 'Base'):
children.append(self.ViewObject.Object.Base)
return children
def dumps(self):
return None
def loads(self, state):
return None
''' PV Area: '''
def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")
+10 -7
View File
@@ -4,14 +4,17 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw.css">
<script src="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw-src.js"></script>
<!-- 1. Core Leaflet library -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/togeojson@0.16.0"></script>
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
<!-- 2. Leaflet.draw Plugin (MUST be loaded AFTER Leaflet) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<!-- 3. Other plugins -->
<script src="https://unpkg.com/togeojson@0.16.0"></script>
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
<!--script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script-->
<script type="text/javascript" src="./qwebchannel.js"></script>
+8 -2
View File
@@ -224,14 +224,20 @@ def getProjected(shape, direction=FreeCAD.Vector(0, 0, 1)): # Based on Draft / s
return ow
def findObjects(classtype):
'''def findObjects(classtype):
objects = FreeCAD.ActiveDocument.Objects
objlist = list()
for object in objects:
if hasattr(object, "Proxy"):
if object.Proxy.Type == classtype:
objlist.append(object)
return objlist
return objlist'''
def findObjects(classtype):
return [obj for obj in FreeCAD.ActiveDocument.Objects
if hasattr(obj, "Proxy")
and hasattr(obj.Proxy, "Type")
and obj.Proxy.Type == classtype]
def getClosePoints(sh1, angle):
'''
+357
View File
@@ -0,0 +1,357 @@
# Script para FreeCAD - Procesador de Documentos Word con Carátula
import os
import glob
from PySide2 import QtWidgets, QtCore
from PySide2.QtWidgets import (QFileDialog, QMessageBox, QProgressDialog,
QApplication, QVBoxLayout, QWidget, QPushButton,
QLabel, QTextEdit)
import FreeCAD
import FreeCADGui
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
try:
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
FreeCAD.Console.PrintError("Error: python-docx no está instalado. Instala con: pip install python-docx\n")
class DocumentProcessor(QtWidgets.QDialog):
def __init__(self, parent=None):
super(DocumentProcessor, self).__init__(parent)
self.caratula_path = ""
self.carpeta_path = ""
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Procesador de Documentos Word")
self.setMinimumWidth(600)
self.setMinimumHeight(500)
layout = QVBoxLayout()
# Título
title = QLabel("<h2>Procesador de Documentos Word</h2>")
title.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(title)
# Información
info_text = QLabel(
"Este script buscará recursivamente todos los archivos .docx en una carpeta,\n"
"insertará una carátula y aplicará formato estándar a todos los documentos."
)
info_text.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(info_text)
# Botón seleccionar carátula
self.btn_caratula = QPushButton("1. Seleccionar Carátula")
self.btn_caratula.clicked.connect(self.seleccionar_caratula)
layout.addWidget(self.btn_caratula)
self.label_caratula = QLabel("No se ha seleccionado carátula")
self.label_caratula.setWordWrap(True)
layout.addWidget(self.label_caratula)
# Botón seleccionar carpeta
self.btn_carpeta = QPushButton("2. Seleccionar Carpeta de Documentos")
self.btn_carpeta.clicked.connect(self.seleccionar_carpeta)
layout.addWidget(self.btn_carpeta)
self.label_carpeta = QLabel("No se ha seleccionado carpeta")
self.label_carpeta.setWordWrap(True)
layout.addWidget(self.label_carpeta)
# Botón procesar
self.btn_procesar = QPushButton("3. Procesar Documentos")
self.btn_procesar.clicked.connect(self.procesar_documentos)
self.btn_procesar.setEnabled(False)
layout.addWidget(self.btn_procesar)
# Área de log
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
layout.addWidget(self.log_area)
# Botón cerrar
self.btn_cerrar = QPushButton("Cerrar")
self.btn_cerrar.clicked.connect(self.close)
layout.addWidget(self.btn_cerrar)
self.setLayout(layout)
def log(self, mensaje):
"""Agrega un mensaje al área de log"""
self.log_area.append(mensaje)
QApplication.processEvents() # Para actualizar la UI
def seleccionar_caratula(self):
"""Abre un diálogo para seleccionar el archivo de carátula"""
archivo, _ = QFileDialog.getOpenFileName(
self,
"Seleccionar archivo de carátula",
"",
"Word documents (*.docx);;All files (*.*)"
)
if archivo and os.path.exists(archivo):
self.caratula_path = archivo
self.label_caratula.setText(f"Carátula: {os.path.basename(archivo)}")
self.verificar_estado()
self.log(f"✓ Carátula seleccionada: {archivo}")
def seleccionar_carpeta(self):
"""Abre un diálogo para seleccionar la carpeta de documentos"""
carpeta = QFileDialog.getExistingDirectory(
self,
"Seleccionar carpeta con documentos"
)
if carpeta:
self.carpeta_path = carpeta
self.label_carpeta.setText(f"Carpeta: {carpeta}")
self.verificar_estado()
self.log(f"✓ Carpeta seleccionada: {carpeta}")
def verificar_estado(self):
"""Habilita el botón procesar si ambos paths están seleccionados"""
if self.caratula_path and self.carpeta_path:
self.btn_procesar.setEnabled(True)
def buscar_docx_recursivamente(self, carpeta):
"""Busca recursivamente todos los archivos .docx en una carpeta"""
archivos_docx = []
patron = os.path.join(carpeta, "**", "*.docx")
for archivo in glob.glob(patron, recursive=True):
archivos_docx.append(archivo)
return archivos_docx
def aplicar_formato_estandar(self, doc):
"""Aplica formato estándar al documento"""
try:
# Configurar estilos por defecto
style = doc.styles['Normal']
font = style.font
font.name = 'Arial'
font.size = Pt(11)
font.color.rgb = RGBColor(0, 0, 0) # Negro
# Configurar encabezados
try:
heading_style = doc.styles['Heading 1']
heading_font = heading_style.font
heading_font.name = 'Arial'
heading_font.size = Pt(14)
heading_font.bold = True
heading_font.color.rgb = RGBColor(0, 51, 102) # Azul oscuro
except:
pass
except Exception as e:
self.log(f" ⚠ Advertencia en formato: {str(e)}")
def aplicar_formato_avanzado(self, doc):
"""Aplica formato más avanzado y personalizado"""
try:
# Configurar márgenes
sections = doc.sections
for section in sections:
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
# Configurar estilos de párrafo
for paragraph in doc.paragraphs:
paragraph.paragraph_format.space_after = Pt(6)
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.line_spacing = 1.15
# Alinear párrafos justificados
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
# Aplicar fuente específica a cada run
for run in paragraph.runs:
run.font.name = 'Arial'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Arial')
run.font.size = Pt(11)
except Exception as e:
self.log(f" ⚠ Advertencia en formato avanzado: {str(e)}")
def insertar_caratula_y_formatear(self, archivo_docx, archivo_caratula):
"""Inserta la carátula y aplica formato al documento"""
try:
# Abrir el documento de carátula
doc_caratula = Document(archivo_caratula)
# Abrir el documento destino
doc_destino = Document(archivo_docx)
# Crear un nuevo documento que contendrá la carátula + contenido original
nuevo_doc = Document()
# Copiar todo el contenido de la carátula
for elemento in doc_caratula.element.body:
print(elemento)
nuevo_doc.element.body.append(elemento)
# Agregar un salto de página después de la carátula
nuevo_doc.add_page_break()
# Copiar todo el contenido del documento original
for elemento in doc_destino.element.body:
nuevo_doc.element.body.append(elemento)
# Aplicar formatos
self.aplicar_formato_estandar(nuevo_doc)
self.aplicar_formato_avanzado(nuevo_doc)
# Guardar el documento (sobrescribir el original)
nombre_base = os.path.splitext(os.path.basename(archivo_docx))[0]
extension = os.path.splitext(archivo_docx)[1]
name = f"{nombre_base}{extension}"
nuevo_docx = os.path.join(self.output_carpeta, name)
nuevo_doc.save(nuevo_docx)
return True, ""
except Exception as e:
return False, str(e)
def procesar_documentos(self):
"""Función principal que orquesta todo el proceso"""
if not DOCX_AVAILABLE:
QMessageBox.critical(self, "Error",
"La biblioteca python-docx no está disponible.\n\n"
"Instala con: pip install python-docx")
return
# Verificar paths
if not os.path.exists(self.caratula_path):
QMessageBox.warning(self, "Error", "El archivo de carátula no existe.")
return
if not os.path.exists(self.carpeta_path):
QMessageBox.warning(self, "Error", "La carpeta de documentos no existe.")
return
self.log("\n=== INICIANDO PROCESAMIENTO ===")
self.log(f"Carátula: {self.caratula_path}")
self.log(f"Carpeta: {self.carpeta_path}")
directorio_padre = os.path.dirname(self.carpeta_path)
self.output_carpeta = os.path.join(directorio_padre, "03.Outputs")
os.makedirs(self.output_carpeta, exist_ok=True)
# Buscar archivos .docx
self.log("Buscando archivos .docx...")
archivos_docx = self.buscar_docx_recursivamente(self.carpeta_path)
if not archivos_docx:
self.log("No se encontraron archivos .docx en la carpeta seleccionada.")
QMessageBox.information(self, "Información",
"No se encontraron archivos .docx en la carpeta seleccionada.")
return
self.log(f"Se encontraron {len(archivos_docx)} archivos .docx")
# Crear diálogo de progreso
progress = QProgressDialog("Procesando documentos...", "Cancelar", 0, len(archivos_docx), self)
progress.setWindowTitle("Procesando")
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.show()
# Procesar cada archivo
exitosos = 0
fallidos = 0
errores_detallados = []
for i, archivo_docx in enumerate(archivos_docx):
if progress.wasCanceled():
self.log("Proceso cancelado por el usuario.")
break
progress.setValue(i)
progress.setLabelText(f"Procesando {i + 1}/{len(archivos_docx)}: {os.path.basename(archivo_docx)}")
QApplication.processEvents()
self.log(f"Procesando: {os.path.basename(archivo_docx)}")
success, error_msg = self.insertar_caratula_y_formatear(archivo_docx, self.caratula_path)
if success:
self.log(f" ✓ Completado")
exitosos += 1
else:
self.log(f" ✗ Error: {error_msg}")
fallidos += 1
errores_detallados.append(f"{os.path.basename(archivo_docx)}: {error_msg}")
progress.setValue(len(archivos_docx))
# Mostrar resumen
self.log("\n=== RESUMEN ===")
self.log(f"Documentos procesados exitosamente: {exitosos}")
self.log(f"Documentos con errores: {fallidos}")
self.log(f"Total procesados: {exitosos + fallidos}")
# Mostrar mensaje final
mensaje = (f"Procesamiento completado:\n"
f"✓ Exitosos: {exitosos}\n"
f"✗ Fallidos: {fallidos}\n"
f"Total: {len(archivos_docx)}")
if fallidos > 0:
mensaje += f"\n\nErrores encontrados:\n" + "\n".join(
errores_detallados[:5]) # Mostrar solo primeros 5 errores
if len(errores_detallados) > 5:
mensaje += f"\n... y {len(errores_detallados) - 5} más"
QMessageBox.information(self, "Proceso Completado", mensaje)
# Función para ejecutar desde FreeCAD
def run_document_processor():
"""Función principal para ejecutar el procesador desde FreeCAD"""
# Verificar si python-docx está disponible
if not DOCX_AVAILABLE:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Biblioteca python-docx no encontrada")
msg.setInformativeText(
"Para usar este script necesitas instalar python-docx:\n\n"
"1. Abre la consola de FreeCAD\n"
"2. Ejecuta: import subprocess, sys\n"
"3. Ejecuta: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-docx'])\n\n"
"O instala desde una terminal externa con: pip install python-docx"
)
msg.setWindowTitle("Dependencia faltante")
msg.exec_()
return
# Crear y mostrar la interfaz
dialog = DocumentProcessor(FreeCADGui.getMainWindow())
dialog.exec_()
class generateDocuments:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "house.svg")),
'MenuText': "DocumentGenerator",
'Accel': "D, G",
'ToolTip': "Creates a Building object from setup dialog."}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
run_document_processor()
+139
View File
@@ -0,0 +1,139 @@
# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Brana <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
"""
Proyecciones y transformaciones geodésicas unificadas para PVPlant.
Reemplaza el uso disperso de la librería 'utm' con pyproj (PROJ),
soporte multi-zona UTM y transformaciones entre datums.
Uso básico:
from lib.projection import latlon_to_utm, utm_to_latlon, get_utm_zone
"""
import FreeCAD
from pyproj import CRS, Transformer
from pyproj.aoi import AreaOfInterest
from pyproj.database import query_utm_crs_info
# WGS84 sistema de coordenadas geográfico de referencia
_WGS84 = CRS.from_epsg(4326)
# Cache de transformadores UTM por zona (lazy)
_utm_transformers = {}
def _get_utm_transformer(lat, lon):
"""Obtiene (o crea en caché) un transformador UTM para la zona de las coordenadas dadas.
Returns:
tuple: (transformer, zone_number, zone_letter)
"""
# Determinar la zona UTM a partir de lat/lon
zone_number = int((lon + 180) / 6) + 1
if lat >= 0:
zone_letter = 'N'
epsg = 32600 + zone_number
else:
zone_letter = 'S'
epsg = 32700 + zone_number
cache_key = (zone_number, zone_letter)
if cache_key not in _utm_transformers:
utm_crs = CRS.from_epsg(epsg)
_utm_transformers[cache_key] = Transformer.from_crs(
_WGS84, utm_crs, always_xy=True
)
return _utm_transformers[cache_key], zone_number, zone_letter
def latlon_to_utm(lat, lon):
"""Convierte coordenadas geográficas (WGS84) a UTM (este, norte, zona, letra).
Args:
lat (float): Latitud en grados.
lon (float): Longitud en grados.
Returns:
tuple: (easting, northing, zone_number, zone_letter)
easting/northing en metros.
"""
transformer, zone_number, zone_letter = _get_utm_transformer(lat, lon)
easting, northing = transformer.transform(lon, lat)
return easting, northing, zone_number, zone_letter
def utm_to_latlon(easting, northing, zone_number, zone_letter='N'):
"""Convierte coordenadas UTM a geográficas (WGS84).
Args:
easting (float): Coordenada E en metros.
northing (float): Coordenada N en metros.
zone_number (int): Número de zona UTM (1-60).
zone_letter (str): Letra de zona ('N' o 'S').
Returns:
tuple: (latitude, longitude) en grados.
"""
if zone_letter.upper() not in ('N', 'S'):
zone_letter = 'N'
epsg = 32600 + zone_number if zone_letter.upper() == 'N' else 32700 + zone_number
utm_crs = CRS.from_epsg(epsg)
transformer = Transformer.from_crs(utm_crs, _WGS84, always_xy=True)
lon, lat = transformer.transform(easting, northing)
return lat, lon
def get_utm_zone(lat, lon):
"""Obtiene la zona UTM para unas coordenadas dadas.
Args:
lat (float): Latitud en grados.
lon (float): Longitud en grados.
Returns:
tuple: (zone_number, zone_letter)
"""
_, _, zone_number, zone_letter = latlon_to_utm(lat, lon)
return zone_number, zone_letter
def latlon_to_utm_vector(lat, lon, elevation=0.0):
"""Convierte lat/lon/elevación a un FreeCAD.Vector en UTM (mm).
Args:
lat (float): Latitud en grados.
lon (float): Longitud en grados.
elevation (float): Elevación en metros (default 0).
Returns:
FreeCAD.Vector: Coordenadas UTM en milímetros.
"""
transformer, _, _ = _get_utm_transformer(lat, lon)
easting, northing = transformer.transform(lon, lat)
return FreeCAD.Vector(
round(easting, 0),
round(northing, 0),
round(elevation, 0)
) * 1000
+21 -6
View File
@@ -2,17 +2,32 @@
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>PVPlant</name>
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
<version>2025.07.06</version>
<date>2025.07.06</date>
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
<version>2026.02.12</version>
<date>2026.02.15</date>
<maintainer email="javier.branagutierrez@gmail.com">
Javier Braña
</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url>
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
<url type="repository" branch="main">
https://homehud.duckdns.org/javier/PVPlant
</url>
<url type="bugtracker">
https://homehud.duckdns.org/javier/PVPlant/issues
</url>
<url type="readme">
https://homehud.duckdns.org/javier/PVPlant/raw/branch/main/README.md
</url>
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>
<content>
<workbench>
<classname>RoadWorkbench</classname>
<classname>PVPlantWorkbench</classname>
<subdirectory>./</subdirectory>
</workbench>
</content>
+4
View File
@@ -55,6 +55,10 @@ class _CommandReload:
import hydro.hydrological as hydro
import Importer.importOSM as iOSM
import docgenerator
importlib.reload(docgenerator)
importlib.reload(ProjectSetup)
importlib.reload(PVPlantPlacement)
importlib.reload(PVPlantImportGrid)
+1 -2
View File
@@ -2,7 +2,6 @@ numpy~=1.26.2
opencv-python~=4.8.1
matplotlib~=3.8.2
openpyxl~=3.1.2
utm~=0.7.0
PySide2~=5.15.8
requests~=2.31.0
setuptools~=68.2.2
@@ -17,4 +16,4 @@ certifi~=2023.11.17
SciPy~=1.11.4
pycollada~=0.7.2
shapely
rtree
rtree