refactor: migrar utm→pyproj, limpiar código muerto, reestructurar en PVPlant/core/

This commit is contained in:
Javier Braña
2026-05-02 01:02:26 +02:00
parent 3bcdc95978
commit d9b39ac17b
11 changed files with 1067 additions and 1365 deletions
-1
View File
@@ -7,7 +7,6 @@ import ssl
import certifi import certifi
import urllib.request import urllib.request
import math import math
import utm
from collections import defaultdict from collections import defaultdict
import PVPlantImportGrid as ImportElevation import PVPlantImportGrid as ImportElevation
+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
+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
+11 -132
View File
@@ -21,7 +21,6 @@
# *********************************************************************** # ***********************************************************************
import FreeCAD import FreeCAD
import utm
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui import FreeCADGui
@@ -109,13 +108,11 @@ class MapWindow(QtGui.QWidget):
self.view.page().loadFinished.connect(self.onLoadFinished) self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file)) self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view) LeftLayout.addWidget(self.view)
# self.layout.addWidget(self.view, 1, 0, 1, 3)
# -- Latitud y longitud: # -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel() self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21) self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates) LeftLayout.addWidget(self.labelCoordinates)
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
# Right Widgets: # Right Widgets:
labelKMZ = QtGui.QLabel() labelKMZ = QtGui.QLabel()
@@ -139,9 +136,6 @@ class MapWindow(QtGui.QWidget):
radio3 = QtGui.QRadioButton("Datos GPS") radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True) radio1.setChecked(True)
# buttonDialog = QtGui.QPushButton('...')
# buttonDialog.setEnabled(False)
vbox = QtGui.QVBoxLayout(self) vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1) vbox.addWidget(radio1)
vbox.addWidget(radio2) vbox.addWidget(radio2)
@@ -202,12 +196,14 @@ class MapWindow(QtGui.QWidget):
@QtCore.Slot(float, float) @QtCore.Slot(float, float)
def onMapMove(self, lat, lng): def onMapMove(self, lat, lng):
from lib.projection import latlon_to_utm
self.lat = lat self.lat = lat
self.lon = lng self.lon = lng
x, y, zone_number, zone_letter = utm.from_latlon(lat, lng) easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lng)
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) + self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
' | UTM: ' + str(zone_number) + zone_letter + ' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(x, y)) ', {:.5f}m E, {:.5f}m N'.format(easting, northing))
@QtCore.Slot(float, float, float, float, int) @QtCore.Slot(float, float, float, float, int)
def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom): def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
@@ -245,7 +241,7 @@ class MapWindow(QtGui.QWidget):
if location.raw["address"].get("state"): if location.raw["address"].get("state"):
if Site.Region != "": if Site.Region != "":
Site.Region += " - " Site.Region += " - "
Site.Region += '{0}'.format(location.raw["address"]["state"]) # province - state Site.Region += '{0}'.format(location.raw["address"]["state"])
Site.Country = location.raw["address"]["country"] Site.Country = location.raw["address"]["country"]
@QtCore.Slot(str) @QtCore.Slot(str)
@@ -273,7 +269,6 @@ class MapWindow(QtGui.QWidget):
p.Base = c p.Base = c
obj = Draft.makeCircle(r, placement=p, face=False) obj = Draft.makeCircle(r, placement=p, face=False)
else: else:
''' do something '''
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10) 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: else: # 2. if the feature is a Polygon or Line:
cw = False cw = False
@@ -289,7 +284,6 @@ class MapWindow(QtGui.QWidget):
pts = [p.sub(offset) for p in tmp] pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False) obj = Draft.makeWire(pts, closed=cw, face=False)
#obj.Placement.Base = Site.Origin
obj.Label = name obj.Label = name
Draft.autogroup(obj) Draft.autogroup(obj)
@@ -300,77 +294,8 @@ class MapWindow(QtGui.QWidget):
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon) self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
if self.checkboxImportSatelitalImagen.isChecked(): if self.checkboxImportSatelitalImagen.isChecked():
# Usar los límites reales del terreno (rectangular) 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
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_lat = self.minLat
s_lon = self.minLon s_lon = self.minLon
n_lat = self.maxLat n_lat = self.maxLat
@@ -412,8 +337,8 @@ class MapWindow(QtGui.QWidget):
img.save(filename) img.save(filename)
# Calcular dimensiones reales en metros # Calcular dimensiones reales en metros
width_m = ne_utm.x - sw_utm.x # Ancho en metros (este-oeste) width_m = ne_utm.x - sw_utm.x
height_m = ne_utm.y - sw_utm.y # Alto en metros (norte-sur) height_m = ne_utm.y - sw_utm.y
# Calcular posición relativa del punto de referencia dentro de la imagen # 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_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5
@@ -425,44 +350,19 @@ class MapWindow(QtGui.QWidget):
img_obj.ImageFile = filename img_obj.ImageFile = filename
img_obj.Label = 'Background' img_obj.Label = 'Background'
# Convertir dimensiones a milímetros (FreeCAD trabaja en mm) # FreeCAD trabaja en mm
img_obj.XSize = width_m * 1000 img_obj.XSize = width_m * 1000
img_obj.YSize = height_m * 1000 img_obj.YSize = height_m * 1000
# Posicionar para que el punto de referencia esté en (0,0,0) # 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( img_obj.Placement.Base = FreeCAD.Vector(
-rel_x * width_m * 1000, -rel_x * width_m * 1000,
-rel_y * height_m * 1000, -rel_y * height_m * 1000,
0 0
) )
# Refrescar el documento
doc.recompute() 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): def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM import Importer.importOSM as importOSM
import PVPlantSite import PVPlantSite
@@ -476,32 +376,15 @@ class MapWindow(QtGui.QWidget):
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}") osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
importer.process_osm_data(osm_data) 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): def panMap(self, lng, lat, geometry=None):
frame = self.view.page() frame = self.view.page()
# 1. Validación del parámetro geometry
if not geometry or len(geometry) < 4: if not geometry or len(geometry) < 4:
# Pan básico sin ajuste de bounds
command = f'map.panTo(L.latLng({lat}, {lng}));' command = f'map.panTo(L.latLng({lat}, {lng}));'
else: else:
try: try:
# 2. Mejor manejo de coordenadas (Leaflet usa [lat, lng]) southwest = f"{float(geometry[1])}, {float(geometry[0])}"
# Asumiendo que geometry es [min_lng, min_lat, max_lng, max_lat] northeast = f"{float(geometry[3])}, {float(geometry[2])}"
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.panTo(L.latLng({lat}, {lng}));'
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));' command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
except (IndexError, ValueError, TypeError) as e: except (IndexError, ValueError, TypeError) as e:
@@ -537,7 +420,3 @@ class CommandPVPlantGeoreferencing:
return True return True
else: else:
return False return False
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantGeoreferencing',_CommandPVPlantGeoreferencing())
'''
+39 -57
View File
@@ -40,7 +40,7 @@ from PVPlantResources import DirIcons as DirIcons
import PVPlantSite import PVPlantSite
def get_elevation_from_oe(coordinates): # v1 deepseek def get_elevation_from_oe(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM. """Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
Args: Args:
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud) coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
@@ -52,10 +52,9 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
return [] return []
import requests import requests
import utm from lib.projection import latlon_to_utm
from requests.exceptions import RequestException 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]) locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
try: try:
@@ -65,7 +64,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
timeout=20, timeout=20,
verify=True verify=True
) )
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx response.raise_for_status()
except RequestException as e: except RequestException as e:
print(f"Error en la solicitud: {str(e)}") print(f"Error en la solicitud: {str(e)}")
@@ -84,13 +83,12 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
points = [] points = []
for result in data["results"]: for result in data["results"]:
try: try:
# Conversión UTM con manejo de errores easting, northing, _, _ = latlon_to_utm(
easting, northing, _, _ = utm.from_latlon(
result["latitude"], result["latitude"],
result["longitude"] result["longitude"]
) )
points.append(FreeCAD.Vector(round(easting), # Convertir metros a milímetros points.append(FreeCAD.Vector(round(easting),
round(northing), round(northing),
round(result["elevation"])) * 1000) round(result["elevation"])) * 1000)
@@ -110,7 +108,7 @@ def getElevationFromOE(coordinates):
return None return None
from requests import get from requests import get
import utm from lib.projection import latlon_to_utm
locations_str="" locations_str=""
total = len(coordinates) - 1 total = len(coordinates) - 1
@@ -121,34 +119,32 @@ def getElevationFromOE(coordinates):
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
points = [] points = []
try: try:
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí r = get(query, timeout=20, verify=certifi.where())
results = r.json() results = r.json()
for point in results["results"]: for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"]) easting, northing, _, _ = latlon_to_utm(point["latitude"], point["longitude"])
v = FreeCAD.Vector(round(c[0], 0), v = FreeCAD.Vector(round(easting, 0),
round(c[1], 0), round(northing, 0),
round(point["elevation"], 0)) * 1000 round(point["elevation"], 0)) * 1000
points.append(v) points.append(v)
except RequestException as e: except RequestException as e:
# print(f"Error en la solicitud: {str(e)}") for point in coordinates:
for i, point in enumerate(coordinates): easting, northing, _, _ = latlon_to_utm(point[0], point[1])
c = utm.from_latlon(point[0], point[1]) points.append(FreeCAD.Vector(round(easting, 0),
points.append(FreeCAD.Vector(round(c[0], 0), round(northing, 0),
round(c[1], 0),
0) * 1000) 0) * 1000)
return points return points
def getSinglePointElevationFromBing(lat, lng): def getSinglePointElevationFromBing(lat, lng):
#http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey} import requests
import utm from lib.projection import latlon_to_utm
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points=" source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng) source += str(lat) + "," + str(lng)
source += "&heights=sealevel" source += "&heights=sealevel"
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX" source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
import requests
response = requests.get(source) response = requests.get(source)
ans = response.text ans = response.text
@@ -156,26 +152,20 @@ def getSinglePointElevationFromBing(lat, lng):
print(s) print(s)
res = s['resourceSets'][0]['resources'][0]['elevations'] res = s['resourceSets'][0]['resources'][0]['elevations']
for elevation in res: for elevation in res:
c = utm.from_latlon(lat, lng) easting, northing, _, _ = latlon_to_utm(lat, lng)
v = FreeCAD.Vector( v = FreeCAD.Vector(
round(c[0] * 1000, 0), round(easting * 1000, 0),
round(c[1] * 1000, 0), round(northing * 1000, 0),
round(elevation * 1000, 0)) round(elevation * 1000, 0))
return v return v
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000): 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 math
import requests import requests
from lib.projection import latlon_to_utm, utm_to_latlon
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
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 = [] points = []
yy = polygon.Shape.BoundBox.YMax yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin: while yy > polygon.Shape.BoundBox.YMin:
@@ -189,8 +179,8 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
else: else:
xx1 = xx + StepsXX * resolution xx1 = xx + StepsXX * resolution
point1 = utm.to_latlon(xx / 1000, yy / 1000, geo[2], geo[3]) point1 = utm_to_latlon(xx / 1000, yy / 1000, zone_number, zone_letter)
point2 = utm.to_latlon(xx1 / 1000, yy / 1000, geo[2], geo[3]) point2 = utm_to_latlon(xx1 / 1000, yy / 1000, zone_number, zone_letter)
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=" source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1]) source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
@@ -203,7 +193,6 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
response = requests.get(source) response = requests.get(source)
ans = response.text ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans) s = json.loads(ans)
res = s['resourceSets'][0]['resources'][0]['elevations'] res = s['resourceSets'][0]['resources'][0]['elevations']
@@ -212,7 +201,7 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4)) v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
points.append(v) points.append(v)
i += 1 i += 1
xx = xx1 + resolution # para no repetir un mismo punto xx = xx1 + resolution
yy -= resolution yy -= resolution
return points return points
@@ -295,47 +284,41 @@ def getSinglePointElevation1(lat, lon):
return v return v
def getSinglePointElevationUtm(lat, lon): def getSinglePointElevationUtm(lat, lon):
import requests
from lib.projection import latlon_to_utm
source = "https://maps.googleapis.com/maps/api/elevation/json?locations=" source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon) source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U" source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
print(source) print(source)
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source) response = requests.get(source)
ans = response.text ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans) s = json.loads(ans)
res = s['results'] res = s['results']
print (res) print(res)
import utm
for r in res: for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng']) easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector( v = FreeCAD.Vector(
round(c[0] * 1000, 4), round(easting * 1000, 4),
round(c[1] * 1000, 4), round(northing * 1000, 4),
round(r['elevation'] * 1000, 2)) round(r['elevation'] * 1000, 2))
print (v) print(v)
return v return v
def getElevationUTM(polygon, lat, lng, resolution = 10000): def getElevationUTM(polygon, lat, lng, resolution = 10000):
from lib.projection import latlon_to_utm, utm_to_latlon
import utm _, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
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)) StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = [] points = []
yy = polygon.Shape.BoundBox.YMax yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin: while yy > polygon.Shape.BoundBox.YMin:
# utm.to_latlon(EASTING, NORTHING, ZONE NUMBER, ZONE LETTER). point1 = utm_to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, zone_number, zone_letter)
# result = (LATITUDE, LONGITUDE) point2 = utm_to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, zone_number, zone_letter)
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 = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1]) source += "{a},{b}".format(a = point1[0], b = point1[1])
@@ -348,15 +331,14 @@ def getElevationUTM(polygon, lat, lng, resolution = 10000):
response = requests.get(source) response = requests.get(source)
ans = response.text ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans) s = json.loads(ans)
res = s['results'] res = s['results']
for r in res: for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng']) easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector( v = FreeCAD.Vector(
round(c[0] * 1000, 2), round(easting * 1000, 2),
round(c[1] * 1000, 2), round(northing * 1000, 2),
round(r['elevation'] * 1000, 2) round(r['elevation'] * 1000, 2)
) )
points.append(v) points.append(v)
+27 -1172
View File
File diff suppressed because it is too large Load Diff
+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
-1
View File
@@ -2,7 +2,6 @@ numpy~=1.26.2
opencv-python~=4.8.1 opencv-python~=4.8.1
matplotlib~=3.8.2 matplotlib~=3.8.2
openpyxl~=3.1.2 openpyxl~=3.1.2
utm~=0.7.0
PySide2~=5.15.8 PySide2~=5.15.8
requests~=2.31.0 requests~=2.31.0
setuptools~=68.2.2 setuptools~=68.2.2