From d9b39ac17ba86ce36d181d00ae5c812285610776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Sat, 2 May 2026 01:02:26 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20migrar=20utm=E2=86=92pyproj,=20limp?= =?UTF-8?q?iar=20c=C3=B3digo=20muerto,=20reestructurar=20en=20PVPlant/core?= =?UTF-8?q?/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Importer/importOSM.py | 1 - PVPlant/__init__.py | 5 + PVPlant/core/__init__.py | 0 PVPlant/core/site.py | 208 ++++++ PVPlant/core/solar_compass.py | 353 ++++++++++ PVPlant/core/view_provider.py | 283 ++++++++ PVPlantGeoreferencing.py | 145 +--- PVPlantImportGrid.py | 96 ++- PVPlantSite.py | 1199 +-------------------------------- lib/projection.py | 139 ++++ requirements.txt | 3 +- 11 files changed, 1067 insertions(+), 1365 deletions(-) create mode 100644 PVPlant/__init__.py create mode 100644 PVPlant/core/__init__.py create mode 100644 PVPlant/core/site.py create mode 100644 PVPlant/core/solar_compass.py create mode 100644 PVPlant/core/view_provider.py create mode 100644 lib/projection.py diff --git a/Importer/importOSM.py b/Importer/importOSM.py index bfaf682..5bba5e9 100644 --- a/Importer/importOSM.py +++ b/Importer/importOSM.py @@ -7,7 +7,6 @@ import ssl import certifi import urllib.request import math -import utm from collections import defaultdict import PVPlantImportGrid as ImportElevation diff --git a/PVPlant/__init__.py b/PVPlant/__init__.py new file mode 100644 index 0000000..0af4ecd --- /dev/null +++ b/PVPlant/__init__.py @@ -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 + diff --git a/PVPlant/core/__init__.py b/PVPlant/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PVPlant/core/site.py b/PVPlant/core/site.py new file mode 100644 index 0000000..b639465 --- /dev/null +++ b/PVPlant/core/site.py @@ -0,0 +1,208 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * 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 \ No newline at end of file diff --git a/PVPlant/core/solar_compass.py b/PVPlant/core/solar_compass.py new file mode 100644 index 0000000..21d1d5a --- /dev/null +++ b/PVPlant/core/solar_compass.py @@ -0,0 +1,353 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * 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 \ No newline at end of file diff --git a/PVPlant/core/view_provider.py b/PVPlant/core/view_provider.py new file mode 100644 index 0000000..d6310aa --- /dev/null +++ b/PVPlant/core/view_provider.py @@ -0,0 +1,283 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * 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 \ No newline at end of file diff --git a/PVPlantGeoreferencing.py b/PVPlantGeoreferencing.py index 7885931..6f63223 100644 --- a/PVPlantGeoreferencing.py +++ b/PVPlantGeoreferencing.py @@ -21,7 +21,6 @@ # *********************************************************************** import FreeCAD -import utm if FreeCAD.GuiUp: import FreeCADGui @@ -109,13 +108,11 @@ class MapWindow(QtGui.QWidget): 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() @@ -139,9 +136,6 @@ class MapWindow(QtGui.QWidget): radio3 = QtGui.QRadioButton("Datos GPS") radio1.setChecked(True) - # buttonDialog = QtGui.QPushButton('...') - # buttonDialog.setEnabled(False) - vbox = QtGui.QVBoxLayout(self) vbox.addWidget(radio1) vbox.addWidget(radio2) @@ -202,12 +196,14 @@ class MapWindow(QtGui.QWidget): @QtCore.Slot(float, float) def onMapMove(self, lat, lng): + from lib.projection import latlon_to_utm + self.lat = lat 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) + ' | 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) def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom): @@ -245,7 +241,7 @@ class MapWindow(QtGui.QWidget): if location.raw["address"].get("state"): if 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"] @QtCore.Slot(str) @@ -273,7 +269,6 @@ class MapWindow(QtGui.QWidget): 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 @@ -289,7 +284,6 @@ class MapWindow(QtGui.QWidget): 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) @@ -300,77 +294,8 @@ class MapWindow(QtGui.QWidget): 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 + from lib.projection import latlon_to_utm - # 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 @@ -412,8 +337,8 @@ class MapWindow(QtGui.QWidget): 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) + 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 @@ -425,44 +350,19 @@ class MapWindow(QtGui.QWidget): img_obj.ImageFile = filename 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.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 @@ -476,32 +376,15 @@ class MapWindow(QtGui.QWidget): 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 + 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: @@ -536,8 +419,4 @@ class CommandPVPlantGeoreferencing: if FreeCAD.ActiveDocument: return True else: - return False - -'''if FreeCAD.GuiUp: - FreeCADGui.addCommand('PVPlantGeoreferencing',_CommandPVPlantGeoreferencing()) -''' + return False \ No newline at end of file diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py index fe6970b..b8b741d 100644 --- a/PVPlantImportGrid.py +++ b/PVPlantImportGrid.py @@ -40,7 +40,7 @@ from PVPlantResources import DirIcons as DirIcons 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. Args: coordinates (list): Lista de tuplas con coordenadas (latitud, longitud) @@ -52,10 +52,9 @@ def get_elevation_from_oe(coordinates): # v1 deepseek return [] import requests - import utm + from lib.projection import latlon_to_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: @@ -65,7 +64,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek timeout=20, verify=True ) - response.raise_for_status() # Lanza excepción para códigos 4xx/5xx + response.raise_for_status() except RequestException as e: print(f"Error en la solicitud: {str(e)}") @@ -84,13 +83,12 @@ def get_elevation_from_oe(coordinates): # v1 deepseek points = [] for result in data["results"]: try: - # Conversión UTM con manejo de errores - easting, northing, _, _ = utm.from_latlon( + easting, northing, _, _ = latlon_to_utm( result["latitude"], result["longitude"] ) - points.append(FreeCAD.Vector(round(easting), # Convertir metros a milímetros + points.append(FreeCAD.Vector(round(easting), round(northing), round(result["elevation"])) * 1000) @@ -110,7 +108,7 @@ def getElevationFromOE(coordinates): return None from requests import get - import utm + from lib.projection import latlon_to_utm locations_str="" total = len(coordinates) - 1 @@ -121,34 +119,32 @@ def getElevationFromOE(coordinates): query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str points = [] try: - r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí + r = get(query, timeout=20, verify=certifi.where()) 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), + 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: - # print(f"Error en la solicitud: {str(e)}") - for i, point in enumerate(coordinates): - c = utm.from_latlon(point[0], point[1]) - points.append(FreeCAD.Vector(round(c[0], 0), - round(c[1], 0), + 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): - #http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey} - import utm + 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" - import requests response = requests.get(source) ans = response.text @@ -156,26 +152,20 @@ def getSinglePointElevationFromBing(lat, lng): print(s) res = s['resourceSets'][0]['resources'][0]['elevations'] for elevation in res: - c = utm.from_latlon(lat, lng) + easting, northing, _, _ = latlon_to_utm(lat, lng) v = FreeCAD.Vector( - round(c[0] * 1000, 0), - round(c[1] * 1000, 0), + round(easting * 1000, 0), + round(northing * 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 + 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 = [] yy = polygon.Shape.BoundBox.YMax while yy > polygon.Shape.BoundBox.YMin: @@ -189,8 +179,8 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000): 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]) + 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]) @@ -203,7 +193,6 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000): 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'] @@ -212,7 +201,7 @@ def getGridElevationFromBing(polygon, lat, lng, resolution = 1000): 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 + xx = xx1 + resolution yy -= resolution return points @@ -295,47 +284,41 @@ def getSinglePointElevation1(lat, lon): 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 = 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) + print(res) - import utm 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( - round(c[0] * 1000, 4), - round(c[1] * 1000, 4), + round(easting * 1000, 4), + round(northing * 1000, 4), round(r['elevation'] * 1000, 2)) - print (v) + print(v) return v def getElevationUTM(polygon, lat, lng, resolution = 10000): + from lib.projection import latlon_to_utm, utm_to_latlon - import utm - geo = utm.from_latlon(lat, lng) - # result = (679434.3578335291, 4294023.585627955, 30, 'S') - # EASTING, NORTHING, ZONE NUMBER, ZONE LETTER + _, _, 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: - # 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]) + 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]) @@ -348,15 +331,14 @@ def getElevationUTM(polygon, lat, lng, resolution = 10000): 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']) + easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng']) v = FreeCAD.Vector( - round(c[0] * 1000, 2), - round(c[1] * 1000, 2), + round(easting * 1000, 2), + round(northing * 1000, 2), round(r['elevation'] * 1000, 2) ) points.append(v) diff --git a/PVPlantSite.py b/PVPlantSite.py index 242b666..b95e832 100644 --- a/PVPlantSite.py +++ b/PVPlantSite.py @@ -20,1175 +20,30 @@ # * * # *********************************************************************** -import FreeCAD, Draft, ArchCommands, 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: - # \cond - def translate(ctxt, txt): - return txt - - - def QT_TRANSLATE_NOOP(ctxt, txt): - return txt - # \endcond - -import os -from PVPlantResources import DirIcons as DirIcons - -__title__ = "FreeCAD Site" -__author__ = "" -__url__ = "http://www.freecadweb.org" - -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): - """ Find the existing Site object """ - # Return an existing instance of the same name, if found. - 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 - - use = True - if use: - 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() - - else: ## posible código: - import MeshPart - - CopyMesh = Mesh.copy() - Base = CopyMesh.Placement.Base - CopyMesh.Placement.move(Base.negative()) - - CopyShape = Boundary.Shape.copy() - CopyShape.Placement.move(Base.negative()) - - Vec = CopyShape.Edge1.Vertexes[0].Point - CopyShape.Edge1.Vertexes[1].Point - Vec.x, Vec.y = -(Vec.y), Vec.x - - Section = CopyMesh.crossSections([(CopyShape.Edge1.Vertexes[0].Point, Vec)], 0.000001) - print(Section) - - for i in Section[0]: - Pwire = Draft.makeWire(i) - # Pwire.Placement.move(Base) - ##SectionGroup.addObject(Pwire) - - 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 - - -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: - # TODO - remove pysolar dependency - # FreeCAD.Console.PrintWarning("Ladybug module not found, using pysolar instead. Warning, this will be deprecated in the future\n") - 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) - - from pivy import coin - - if not scale: - return None - - circles = [] - sunpaths = [] - hourpaths = [] - circlepos = [] - hourpos = [] - - # build the base circle + number positions - 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)) - - # build the sun curves at solstices and equinoxe - 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 # pysolar's zero is south, ours is X direction - 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 # pysolar's zero is north, ours is X direction - 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 # not sure why this is needed now... But it is. - 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)) - - # cut underground lines - 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] - - # build nodes - ts = 0.005 * scale # text 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): - """makeWindRose(site,sectors): - returns a wind rose diagram as a pivy node""" - - 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)] # the divider angles between each sector - basebissect = baseangle / 2 - angles = [basebissect] # build a list of central direction for each sector - for i in range(1, sectors): - angles.append(angles[-1] + baseangle) - windsbysector = [0 for i in range(sectors)] # prepare a holder for values for each sector - for hour in epw_data.wind_direction: - sector = min(angles, key=lambda x: abs(x - hour)) # find the closest sector angle - sectorindex = angles.index(sector) - windsbysector[sectorindex] = windsbysector[sectorindex] + 1 - maxwind = max(windsbysector) - windsbysector = [wind / maxwind for wind in windsbysector] # normalize - vectors = [] # create 3D 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]) - - # build coin node - import Part - from pivy import coin - 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): - from pivy import coin - self.compassswitch.whichChild = coin.SO_SWITCH_ALL - - def hide(self): - from pivy import coin - self.compassswitch.whichChild = coin.SO_SWITCH_NONE - - def rotate(self, angleInDegrees): - from pivy import coin - self.transform.rotation.setValue( - coin.SbVec3f(0, 0, 1), math.radians(angleInDegrees)) - - def locate(self, x, y, z): - from pivy import coin - self.transform.translation.setValue(x, y, z) - - def scale(self, area): - from pivy import coin - - scale = round(max(math.sqrt(area.getValueAs("m^2").Value) / 10, 1)) - - self.transform.scaleFactor.setValue(coin.SbVec3f(scale, scale, 1)) - - def setupCoin(self): - from pivy import coin - - compasssep = coin.SoSeparator() - - self.transform = coin.SoTransform() - - darkNorthMaterial = coin.SoMaterial() - darkNorthMaterial.diffuseColor.set1Value( - 0, 0.5, 0, 0) # north dark color - - lightNorthMaterial = coin.SoMaterial() - lightNorthMaterial.diffuseColor.set1Value( - 0, 0.9, 0, 0) # north light color - - darkGreyMaterial = coin.SoMaterial() - darkGreyMaterial.diffuseColor.set1Value(0, 0.9, 0.9, 0.9) # dark color - - lightGreyMaterial = coin.SoMaterial() - lightGreyMaterial.diffuseColor.set1Value(0, 0.5, 0.5, 0.5) # light color - - coords = self.buildCoordinates() - - # coordIndex = [0, 1, 2, -1, 2, 3, 0, -1] - - 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): - from pivy import coin - - coords = coin.SoCoordinate3() - - # North Arrow - 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) - - # East Arrow - 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) - - # South Arrow - 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) - - # West Arrow - 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 - - -class _PVPlantSite(ArchSite._Site): - "The Site object" - - def __init__(self, obj): - ArchSite._Site.__init__(self, obj) - - self.obj = obj - # self.setProperties(obj) - self.Type = "Site" - obj.Proxy = self - obj.IfcType = "Site" - obj.setEditorMode("IfcType", 1) - - def setProperties(self, obj): - # Definicion de Propiedades: - 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): - """Method run when the document is restored. Re-adds the properties.""" - 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: - # TODO: Definir los objetos que se pueden proyectar - # if obj.Boundary.TypeId == 'Part::Part2DObject': - # projectWireOnMesh(obj.Boundary, obj.Terrain.Mesh) - 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): - - return - - if not obj.isDerivedFrom("Part::Feature"): # old-style Site - return - - pl = obj.Placement - shape = None - if obj.Terrain: - if obj.Terrain.isDerivedFrom("Part::Feature"): - if obj.Terrain.Shape: - if not obj.Terrain.Shape.isNull(): - shape = obj.Terrain.Shape.copy() - if shape: - shells = [] - for sub in obj.Subtractions: - if sub.isDerivedFrom("Part::Feature"): - if sub.Shape: - if sub.Shape.Solids: - for sol in sub.Shape.Solids: - rest = shape.cut(sol) - shells.append(sol.Shells[0].common(shape.extrude(obj.ExtrusionVector))) - shape = rest - for sub in obj.Additions: - if sub.isDerivedFrom("Part::Feature"): - if sub.Shape: - if sub.Shape.Solids: - for sol in sub.Shape.Solids: - rest = shape.cut(sol) - shells.append(sol.Shells[0].cut(shape.extrude(obj.ExtrusionVector))) - shape = rest - if not shape.isNull(): - if shape.isValid(): - for shell in shells: - shape = shape.fuse(shell) - if obj.RemoveSplitter: - shape = shape.removeSplitter() - obj.Shape = shape - if not pl.isNull(): - obj.Placement = pl - self.computeAreas(obj) - - def computeAreas(self, obj): - ArchSite._Site.computeAreas(self, obj) - return - - if not obj.Shape: - return - - if obj.Shape.isNull(): - return - if not obj.Shape.isValid(): - return - if not obj.Shape.Faces: - return - if not hasattr(obj, "Perimeter"): # check we have a latest version site - return - if not obj.Terrain: - return - # compute area - fset = [] - for f in obj.Shape.Faces: - if f.normalAt(0, 0).getAngle(FreeCAD.Vector(0, 0, 1)) < 1.5707: - fset.append(f) - if fset: - import Drawing, Part - pset = [] - for f in fset: - try: - pf = Part.Face(Part.Wire(Drawing.project(f, FreeCAD.Vector(0, 0, 1))[0].Edges)) - except Part.OCCError: - # error in computing the area. Better set it to zero than show a wrong value - if obj.ProjectedArea.Value != 0: - print("Error computing areas for ", obj.Label) - obj.ProjectedArea = 0 - else: - pset.append(pf) - if pset: - self.flatarea = pset.pop() - for f in pset: - self.flatarea = self.flatarea.fuse(f) - self.flatarea = self.flatarea.removeSplitter() - if obj.ProjectedArea.Value != self.flatarea.Area: - obj.ProjectedArea = self.flatarea.Area - # compute perimeter - lut = {} - for e in obj.Shape.Edges: - lut.setdefault(e.hashCode(), []).append(e) - l = 0 - for e in lut.values(): - if len(e) == 1: # keep only border edges - l += e[0].Length - if l: - if obj.Perimeter.Value != l: - obj.Perimeter = l - # compute volumes - if obj.Terrain.Shape.Solids: - shapesolid = obj.Terrain.Shape.copy() - else: - shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector) - addvol = 0 - subvol = 0 - for sub in obj.Subtractions: - subvol += sub.Shape.common(shapesolid).Volume - for sub in obj.Additions: - addvol += sub.Shape.cut(shapesolid).Volume - if obj.SubtractionVolume.Value != subvol: - obj.SubtractionVolume = subvol - if obj.AdditionVolume.Value != addvol: - obj.AdditionVolume = addvol - - - def __getstate__(self): - """ - Save variables to file. - """ - node = self.get_geoorigin() - system = node.geoSystem.getValues() - x, y, z = node.geoCoords.getValue().getValue() - return system, [x, y, z] - - def __setstate__(self, state): - """ - Get variables from file. - """ - 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): - import utm - import PVPlantImportGrid - x, y, zone_number, zone_letter = utm.from_latlon(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 - - -class _ViewProviderSite(ArchSite._ViewProviderSite): - """A View Provider for the Site object. - - Parameters - ---------- - vobj: - The view provider to turn into a site view provider. - """ - - def __init__(self, vobj): - ArchSite._ViewProviderSite.__init__(self, vobj) - vobj.Proxy = self - #vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) - #self.setProperties(vobj) - - def getIcon(self): - """ - Return the path to the appropriate icon. - """ - return str(os.path.join(DirIcons, "icon.svg")) - - def claimChildren(self): - """Define which objects will appear as children in the tree view. - - Set objects within the site group, and the terrain object as children. - - If the Arch preference swallowSubtractions is true, set the additions - and subtractions to the terrain as children. - - Returns - ------- - list of s: - The objects claimed as children. - """ - - objs = [] - if hasattr(self, "Object"): - objs = self.Object.Group + \ - [self.Object.Terrain, self.Object.Boundary, self.Object.Frames] - if hasattr(self.Object, "Frames"): - objs.extend(self.Object.Frames) - - return objs - - -''' -class _ViewProviderSite: - """A View Provider for the Site object. - - Parameters - ---------- - vobj: - The view provider to turn into a site view provider. - """ - - def __init__(self,vobj): - vobj.Proxy = self - vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self) - self.setProperties(vobj) - - def setProperties(self,vobj): - """Give the site view provider its site view provider specific properties. - - These include solar diagram and compass data, dealing the orientation - of the site, and its orientation to the sun. - - You can learn more about properties here: https://wiki.freecadweb.org/property - """ - - 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): - """Method run when the document is restored. Re-add the Arch component properties.""" - self.setProperties(vobj) - - def getIcon(self): - """Return the path to the appropriate icon.""" - - return str(os.path.join(DirIcons, "solar-panel.svg")) - - def claimChildren(self): - """Define which objects will appear as children in the tree view. - - Set objects within the site group, and the terrain object as children. - - If the Arch preference swallowSubtractions is true, set the additions - and subtractions to the terrain as children. - - Returns - ------- - list of s: - The objects claimed as children. - """ - - 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): - """Method called when the document requests the object to enter edit mode. - - Edit mode is entered when a user double clicks on an object in the tree - view, or when they use the menu option [Edit -> Toggle Edit Mode]. - - Just display the standard Arch component task panel. - - Parameters - ---------- - mode: int or str - The edit mode the document has requested. Set to 0 when requested via - a double click or [Edit -> Toggle Edit Mode]. - - Returns - ------- - bool - If edit mode was entered. - """ - - 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): - """Method called when the document requests the object exit edit mode. - - Close the Arch component edit task panel. - - Returns - ------- - False - """ - - FreeCADGui.Control.closeDialog() - return False - - def attach(self,vobj): - """Add display modes' data to the coin scenegraph. - - Add each display mode as a coin node, whose parent is this view - provider. - - Each display mode's node includes the data needed to display the object - in that mode. This might include colors of faces, or the draw style of - lines. This data is stored as additional coin nodes which are children - of the display mode node. - - Doe not add display modes, but do add the solar diagram and compass to - the scenegraph. - """ - - self.Object = vobj.Object - from pivy import coin - 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): - """Method called when the host object has a property changed. - - If the Longitude or Latitude has changed, set the SolarDiagram to - update. - - If Terrain or Placement has changed, move the compass to follow it. - - Parameters - ---------- - obj: - The host object that has changed. - prop: string - The name of the property that has changed. - """ - - 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"): - from pivy import coin - 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): - """Update the declination of the compass - - Update the declination by adding together how the site has been rotated - within the document, and the rotation of the site compass. - """ - - if not hasattr(vobj, 'UpdateDeclination') or not vobj.UpdateDeclination: - return - compassRotation = vobj.CompassRotation.Value - siteRotation = math.degrees(vobj.Object.Placement.Rotation.Angle) # This assumes Rotation.axis = (0,0,1) - vobj.Object.Declination = compassRotation + siteRotation - - def addTrueNorthRotation(self): - - if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None: - return - from pivy import coin - 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: - from pivy import coin - 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 - -''' - -'''class _CommandPVPlantSite: - "the Arch Site command definition" - - def GetResources(self): - return {'Pixmap': str(os.path.join(DirIcons, "icon.svg")), - 'MenuText': QT_TRANSLATE_NOOP("Arch_Site", "Site"), - 'Accel': "S, I", - 'ToolTip': QT_TRANSLATE_NOOP("Arch_Site", "Creates a site object including selected objects.")} - - def IsActive(self): - return ((not (FreeCAD.ActiveDocument is None)) and - (FreeCAD.ActiveDocument.getObject("Site") is None)) - - def Activated(self): - makePVPlantSite() - return - -if FreeCAD.GuiUp: - FreeCADGui.addCommand('PVPlantSite', _CommandPVPlantSite())''' +""" +PVPlantSite - Wrapper de compatibilidad. + +Todo el código fuente se ha movido a PVPlant/core/ para mejor organización. +Este archivo mantiene imports legacy para que los módulos existentes sigan funcionando. +Para nuevo código, importar directamente desde PVPlant.core. +""" + +from PVPlant.core.site import ( + _PVPlantSite, + _ViewProviderSite, + makePVPlantSite, + get, + PartToWire, + projectWireOnMesh, + zone_list, +) + +from PVPlant.core.solar_compass import ( + makeSolarDiagram, + makeWindRose, + Compass, +) + +from PVPlant.core.view_provider import ( + ViewProviderSite, +) \ No newline at end of file diff --git a/lib/projection.py b/lib/projection.py new file mode 100644 index 0000000..e704cff --- /dev/null +++ b/lib/projection.py @@ -0,0 +1,139 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2026 Javier Brana * +# * * +# * 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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b5ac3db..ee1d2f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +rtree