2 Commits

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

Los archivos originales ahora son wrappers de compatibilidad.
Preparado para revisión exhaustiva de PVPlantPlacement.
2026-05-02 01:54:28 +02:00
Javier Braña d9b39ac17b refactor: migrar utm→pyproj, limpiar código muerto, reestructurar en PVPlant/core/ 2026-05-02 01:02:26 +02:00
21 changed files with 4578 additions and 4821 deletions
-1
View File
@@ -7,7 +7,6 @@ import ssl
import certifi
import urllib.request
import math
import utm
from collections import defaultdict
import PVPlantImportGrid as ImportElevation
+5
View File
@@ -0,0 +1,5 @@
# PVPlant - Paquete reestructurado
#
# Los imports legacy (from PVPlantSite import X, etc.) siguen funcionando.
# Para nuevo código, usar: from PVPlant.core.site import _PVPlantSite
View File
+422
View File
@@ -0,0 +1,422 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import os
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirResources as DirResources
class MapWindow(QtGui.QWidget):
def __init__(self, WinTitle="MapWindow"):
super(MapWindow, self).__init__()
self.raise_()
self.lat = None
self.lon = None
self.minLat = None
self.maxLat = None
self.minLon = None
self.maxLon = None
self.zoom = None
self.WinTitle = WinTitle
self.georeference_coordinates = {'lat': None, 'lon': None}
self.setupUi()
def setupUi(self):
from PySide2.QtWebEngineWidgets import QWebEngineView
from PySide2.QtWebChannel import QWebChannel
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
self.resize(1200, 800)
self.setWindowTitle(self.WinTitle)
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.layout = QtGui.QHBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
LeftWidget = QtGui.QWidget(self)
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
LeftWidget.setLayout(LeftLayout)
LeftLayout.setContentsMargins(0, 0, 0, 0)
RightWidget = QtGui.QWidget(self)
RightWidget.setFixedWidth(350)
RightLayout = QtGui.QVBoxLayout(RightWidget)
RightWidget.setLayout(RightLayout)
RightLayout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(LeftWidget)
self.layout.addWidget(RightWidget)
# Left Widgets:
# -- Search Bar:
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Search")
self.valueSearch.returnPressed.connect(self.onSearch)
searchbutton = QtGui.QPushButton('Search')
searchbutton.setFixedWidth(80)
searchbutton.clicked.connect(self.onSearch)
SearchBarLayout = QtGui.QHBoxLayout(self)
SearchBarLayout.addWidget(self.valueSearch)
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
# -- Webbroser:
self.view = QWebEngineView()
self.channel = QWebChannel(self.view.page())
self.view.page().setWebChannel(self.channel)
self.channel.registerObject("MyApp", self)
file = os.path.join(DirResources, "webs", "main.html")
self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view)
# -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates)
# Right Widgets:
labelKMZ = QtGui.QLabel()
labelKMZ.setText("Cargar un archivo KMZ/KML:")
self.kmlButton = QtGui.QPushButton()
self.kmlButton.setFixedSize(32, 32)
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
widget = QtGui.QWidget(self)
layout = QtGui.QHBoxLayout(widget)
widget.setLayout(layout)
layout.addWidget(labelKMZ)
layout.addWidget(self.kmlButton)
RightLayout.addWidget(widget)
# -----------------------
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
self.groupbox.setCheckable(True)
self.groupbox.setChecked(True)
radio1 = QtGui.QRadioButton("Google Elevation")
radio2 = QtGui.QRadioButton("Nube de Puntos")
radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True)
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1)
vbox.addWidget(radio2)
vbox.addWidget(radio3)
self.groupbox.setLayout(vbox)
RightLayout.addWidget(self.groupbox)
# ------------------------
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
RightLayout.addWidget(self.checkboxImportGis)
self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital")
RightLayout.addWidget(self.checkboxImportSatelitalImagen)
verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
RightLayout.addItem(verticalSpacer)
self.bAccept = QtGui.QPushButton('Accept')
self.bAccept.clicked.connect(self.onAcceptClick)
RightLayout.addWidget(self.bAccept)
# signals/slots
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
def onLoadFinished(self):
file = os.path.join(DirResources, "webs", "map.js")
frame = self.view.page()
with open(file, 'r') as f:
frame.runJavaScript(f.read())
def onSearch(self):
if self.valueSearch.text() == "":
return
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="http")
location = geolocator.geocode(self.valueSearch.text())
self.valueSearch.setText(location.address)
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
def onAcceptClick(self):
frame = self.view.page()
# 1. georeferenciar
frame.runJavaScript(
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
)
# 2. importar todos los elementos dibujados:
frame.runJavaScript(
"var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));"
)
self.close()
@QtCore.Slot(float, float)
def onMapMove(self, lat, lng):
from lib.projection import latlon_to_utm
self.lat = lat
self.lon = lng
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lng)
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(easting, northing))
@QtCore.Slot(float, float, float, float, int)
def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
self.minLat = min([minLat, maxLat])
self.maxLat = max([minLat, maxLat])
self.minLon = min([minLon, maxLon])
self.maxLon = max([minLon, maxLon])
self.zoom = zoom
@QtCore.Slot(float, float)
def georeference(self, lat, lng):
import PVPlantSite
from geopy.geocoders import Nominatim
self.georeference_coordinates['lat'] = lat
self.georeference_coordinates['lon'] = lng
Site = PVPlantSite.get(create=True)
Site.Proxy.setLatLon(lat, lng)
geolocator = Nominatim(user_agent="http")
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
if location:
if location.raw["address"].get("road"):
str = location.raw["address"]["road"]
if location.raw["address"].get("house_number"):
str += ' ({0})'.format(location.raw["address"]["house_number"])
Site.Address = str
if location.raw["address"].get("city"):
Site.City = location.raw["address"]["city"]
if location.raw["address"].get("postcode"):
Site.PostalCode = location.raw["address"]["postcode"]
if location.raw["address"].get("address"):
Site.Region = '{0}'.format(location.raw["address"]["province"])
if location.raw["address"].get("state"):
if Site.Region != "":
Site.Region += " - "
Site.Region += '{0}'.format(location.raw["address"]["state"])
Site.Country = location.raw["address"]["country"]
@QtCore.Slot(str)
def shapes(self, drawnItems):
import geojson
import PVPlantImportGrid as ImportElevation
import Draft
import PVPlantSite
Site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(Site.Origin)
offset.z = 0
items = geojson.loads(drawnItems)
for item in items['features']:
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
coord = item['geometry']['coordinates']
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
if item['properties'].get('radius'):
r = round(item['properties']['radius'] * 1000, 0)
p = FreeCAD.Placement()
p.Base = c
obj = Draft.makeCircle(r, placement=p, face=False)
else:
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10)
else: # 2. if the feature is a Polygon or Line:
cw = False
name = "Línea"
lp = item['geometry']['coordinates']
if item['geometry']['type'] == "Polygon":
cw = True
name = "Area"
lp = item['geometry']['coordinates'][0]
pts = [[cords[1], cords[0]] for cords in lp]
tmp = ImportElevation.getElevationFromOE(pts)
pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False)
obj.Label = name
Draft.autogroup(obj)
if item['properties'].get('name'):
obj.Label = item['properties']['name']
if self.checkboxImportGis.isChecked():
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
if self.checkboxImportSatelitalImagen.isChecked():
from lib.projection import latlon_to_utm
s_lat = self.minLat
s_lon = self.minLon
n_lat = self.maxLat
n_lon = self.maxLon
# Obtener puntos UTM para las esquinas y el punto de referencia
points = [
[s_lat, s_lon], # Suroeste
[n_lat, n_lon], # Noreste
[self.georeference_coordinates['lat'], self.georeference_coordinates['lon']] # Punto de referencia
]
utm_points = ImportElevation.getElevationFromOE(points)
if not utm_points or len(utm_points) < 3:
FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas y referencia\n")
return
sw_utm, ne_utm, ref_utm = utm_points
# Descargar imagen satelital
from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
downloader = GoogleMapDownloader(
zoom=self.zoom,
layer='raw_satellite'
)
img = downloader.generateImage(
sw_lat=s_lat,
sw_lng=s_lon,
ne_lat=n_lat,
ne_lng=n_lon
)
# Guardar imagen
doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
if not doc_path:
doc_path = FreeCAD.ConfigGet("UserAppData")
filename = os.path.join(doc_path, "background.jpeg")
img.save(filename)
# Calcular dimensiones reales en metros
width_m = ne_utm.x - sw_utm.x
height_m = ne_utm.y - sw_utm.y
# Calcular posición relativa del punto de referencia dentro de la imagen
rel_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5
rel_y = (ref_utm.y - sw_utm.y) / height_m if height_m != 0 else 0.5
# Crear objeto de imagen en FreeCAD
doc = FreeCAD.ActiveDocument
img_obj = doc.addObject('Image::ImagePlane', 'Background')
img_obj.ImageFile = filename
img_obj.Label = 'Background'
# FreeCAD trabaja en mm
img_obj.XSize = width_m * 1000
img_obj.YSize = height_m * 1000
# Posicionar para que el punto de referencia esté en (0,0,0)
img_obj.Placement.Base = FreeCAD.Vector(
-rel_x * width_m * 1000,
-rel_y * height_m * 1000,
0
)
doc.recompute()
def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM
import PVPlantSite
site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(site.Origin)
offset.z = 0
importer = importOSM.OSMImporter(offset)
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
importer.process_osm_data(osm_data)
def panMap(self, lng, lat, geometry=None):
frame = self.view.page()
if not geometry or len(geometry) < 4:
command = f'map.panTo(L.latLng({lat}, {lng}));'
else:
try:
southwest = f"{float(geometry[1])}, {float(geometry[0])}"
northeast = f"{float(geometry[3])}, {float(geometry[2])}"
command = f'map.panTo(L.latLng({lat}, {lng}));'
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
except (IndexError, ValueError, TypeError) as e:
print(f"Error en geometry: {str(e)}")
command = f'map.panTo(L.latLng({lat}, {lng}));'
frame.runJavaScript(command)
def importKML(self):
file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0]
from lib.kml2geojson import kmz_convert
layers = kmz_convert(file, "", )
frame = self.view.page()
for layer in layers:
command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer)
frame.runJavaScript(command)
class CommandPVPlantGeoreferencing:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
def Activated(self):
self.form = MapWindow()
self.form.show()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
+208
View File
@@ -0,0 +1,208 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, Draft, math, datetime
import ArchSite
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
from pivy import coin
else:
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
import os
from PVPlantResources import DirIcons as DirIcons
zone_list = ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7", "Z8", "Z9", "Z10", "Z11", "Z12",
"Z13", "Z14", "Z15", "Z16", "Z17", "Z18", "Z19", "Z20", "Z21", "Z22", "Z23", "Z24",
"Z25", "Z26", "Z27", "Z28", "Z29", "Z30", "Z31", "Z32", "Z33", "Z34", "Z35", "Z36",
"Z37", "Z38", "Z39", "Z40", "Z41", "Z42", "Z43", "Z44", "Z45", "Z46", "Z47", "Z48",
"Z49", "Z50", "Z51", "Z52", "Z53", "Z54", "Z55", "Z56", "Z57", "Z58", "Z59", "Z60"]
def get(origin=FreeCAD.Vector(0, 0, 0), create=False):
obj = FreeCAD.ActiveDocument.getObject('Site')
if obj:
if obj.Origin == FreeCAD.Vector(0, 0, 0):
obj.Origin = origin
return obj
if not obj and create:
obj = makePVPlantSite()
return obj
def PartToWire(part):
import Part, Draft
PointList = []
edges = Part.__sortEdges__(part.Shape.Edges)
for edge in edges:
PointList.append(edge.Vertexes[0].Point)
PointList.append(edges[-1].Vertexes[-1].Point)
Draft.makeWire(PointList, closed=True, face=None, support=None)
def projectWireOnMesh(Boundary, Mesh):
import Draft
import MeshPart as mp
plist = mp.projectShapeOnMesh(Boundary.Shape, Mesh, FreeCAD.Vector(0, 0, 1))
PointList = []
for pl in plist:
PointList += pl
Draft.makeWire(PointList, closed=True, face=None, support=None)
FreeCAD.activeDocument().recompute()
def makePVPlantSite():
def createGroup(father, groupname, type=None):
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", groupname)
group.Label = groupname
father.addObject(group)
return group
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Site")
_PVPlantSite(obj)
if FreeCAD.GuiUp:
_ViewProviderSite(obj.ViewObject)
group = createGroup(obj, "CivilGroup")
group1 = createGroup(group, "Areas")
createGroup(group1, "Boundaries")
createGroup(group1, "CadastralPlots")
createGroup(group1, "Exclusions")
createGroup(group1, "FrameZones")
createGroup(group1, "Offsets")
createGroup(group1, "Plots")
createGroup(group, "Drains")
createGroup(group, "Earthworks")
createGroup(group, "Fences")
createGroup(group, "Foundations")
createGroup(group, "Pads")
createGroup(group, "Points")
createGroup(group, "Roads")
createGroup(group, "Trenches")
group = createGroup(obj, "ElectricalGroup")
createGroup(group, "StringInverters")
createGroup(group, "CentralInverter")
group1 = createGroup(group, "AC")
createGroup(group1, "CableAC")
group1 = createGroup(group, "DC")
createGroup(group1, "CableDC")
createGroup(group1, "StringsSetup")
createGroup(group1, "Strings")
createGroup(group1, "StringsBoxes")
group = createGroup(obj, "MechanicalGroup")
createGroup(group, "FramesSetups")
createGroup(group, "Frames")
group = createGroup(obj, "Environment")
createGroup(group, "Vegetation")
return obj
class _PVPlantSite(ArchSite._Site):
"The Site object"
def __init__(self, obj):
ArchSite._Site.__init__(self, obj)
self.obj = obj
self.Type = "Site"
obj.Proxy = self
obj.IfcType = "Site"
obj.setEditorMode("IfcType", 1)
def setProperties(self, obj):
ArchSite._Site.setProperties(self, obj)
obj.addProperty("App::PropertyLink", "Boundary", "PVPlant", "Boundary of land")
obj.addProperty("App::PropertyLinkList", "Frames", "PVPlant", "Frames templates")
obj.addProperty("App::PropertyEnumeration", "UtmZone", "PVPlant", "UTM zone").UtmZone = zone_list
obj.addProperty("App::PropertyVector", "Origin", "PVPlant", "Origin point.").Origin = (0, 0, 0)
def onDocumentRestored(self, obj):
self.obj = obj
self.Type = "Site"
obj.Proxy = self
def onChanged(self, obj, prop):
ArchSite._Site.onChanged(self, obj, prop)
if (prop == "Terrain") or (prop == "Boundary"):
if obj.Terrain and obj.Boundary:
print("Calcular 3D boundary")
if prop == "UtmZone":
node = self.get_geoorigin()
zone = obj.getPropertyByName("UtmZone")
geo_system = ["UTM", zone, "FLAT"]
node.geoSystem.setValues(geo_system)
if prop == "Origin":
node = self.get_geoorigin()
origin = obj.getPropertyByName("Origin")
node.geoCoords.setValue(origin.x, origin.y, 0)
obj.Placement.Base = obj.getPropertyByName(prop)
def execute(self, obj):
ArchSite._Site.execute(self, obj)
def computeAreas(self, obj):
ArchSite._Site.computeAreas(self, obj)
def __getstate__(self):
node = self.get_geoorigin()
system = node.geoSystem.getValues()
x, y, z = node.geoCoords.getValue().getValue()
return system, [x, y, z]
def __setstate__(self, state):
if state:
system = state[0]
origin = state[1]
node = self.get_geoorigin()
node.geoSystem.setValues(system)
node.geoCoords.setValue(origin[0], origin[1], 0)
def get_geoorigin(self):
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
node = sg.getChild(0)
if not isinstance(node, coin.SoGeoOrigin):
node = coin.SoGeoOrigin()
sg.insertChild(node, 0)
return node
def setLatLon(self, lat, lon):
from lib.projection import latlon_to_utm
import PVPlantImportGrid
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
point = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z)
self.obj.Latitude = lat
self.obj.Longitude = lon
self.obj.Elevation = point[0].z
from PVPlant.core.view_provider import ViewProviderSite as _ViewProviderSite
+353
View File
@@ -0,0 +1,353 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, math, datetime
from pivy import coin
def makeSolarDiagram(longitude, latitude, scale=1, complete=False, tz=None):
"""makeSolarDiagram(longitude,latitude,[scale,complete,tz]):
returns a solar diagram as a pivy node. If complete is
True, the 12 months are drawn. Tz is the timezone related to
UTC (ex: -3 = UTC-3)"""
oldversion = False
ladybug = False
try:
import ladybug
from ladybug import location
from ladybug import sunpath
except:
ladybug = False
try:
import pysolar
except:
try:
import Pysolar as pysolar
except:
FreeCAD.Console.PrintError("The pysolar module was not found. Unable to generate solar diagrams\n")
return None
else:
oldversion = True
if tz:
tz = datetime.timezone(datetime.timedelta(hours=-3))
else:
tz = datetime.timezone.utc
else:
loc = ladybug.location.Location(latitude=latitude, longitude=longitude, time_zone=tz)
sunpath = ladybug.sunpath.Sunpath.from_location(loc)
if not scale:
return None
circles = []
sunpaths = []
hourpaths = []
circlepos = []
hourpos = []
import Part
for i in range(1, 9):
circles.append(Part.makeCircle(scale * (i / 8.0)))
for ad in range(0, 360, 15):
a = math.radians(ad)
p1 = FreeCAD.Vector(math.cos(a) * scale, math.sin(a) * scale, 0)
p2 = FreeCAD.Vector(math.cos(a) * scale * 0.125, math.sin(a) * scale * 0.125, 0)
p3 = FreeCAD.Vector(math.cos(a) * scale * 1.08, math.sin(a) * scale * 1.08, 0)
circles.append(Part.LineSegment(p1, p2).toShape())
circlepos.append((ad, p3))
year = datetime.datetime.now().year
hpts = [[] for i in range(24)]
m = [(6, 21), (7, 21), (8, 21), (9, 21), (10, 21), (11, 21), (12, 21)]
if complete:
m.extend([(1, 21), (2, 21), (3, 21), (4, 21), (5, 21)])
for i, d in enumerate(m):
pts = []
for h in range(24):
if ladybug:
sun = sunpath.calculate_sun(month=d[0], day=d[1], hour=h)
alt = math.radians(sun.altitude)
az = 90 + sun.azimuth
elif oldversion:
dt = datetime.datetime(year, d[0], d[1], h)
alt = math.radians(pysolar.solar.GetAltitudeFast(latitude, longitude, dt))
az = pysolar.solar.GetAzimuth(latitude, longitude, dt)
az = -90 + az
else:
dt = datetime.datetime(year, d[0], d[1], h, tzinfo=tz)
alt = math.radians(pysolar.solar.get_altitude_fast(latitude, longitude, dt))
az = pysolar.solar.get_azimuth(latitude, longitude, dt)
az = 90 + az
if az < 0:
az = 360 + az
az = math.radians(az)
zc = math.sin(alt) * scale
ic = math.cos(alt) * scale
xc = math.cos(az) * ic
yc = math.sin(az) * ic
p = FreeCAD.Vector(xc, yc, zc)
pts.append(p)
hpts[h].append(p)
if i in [0, 6]:
ep = FreeCAD.Vector(p)
ep.multiply(1.08)
if ep.z >= 0:
if not oldversion:
h = 24 - h
if h == 12:
if i == 0:
h = "SUMMER"
else:
h = "WINTER"
if latitude < 0:
if h == "SUMMER":
h = "WINTER"
else:
h = "SUMMER"
hourpos.append((h, ep))
if i < 7:
sunpaths.append(Part.makePolygon(pts))
for h in hpts:
if complete:
h.append(h[0])
hourpaths.append(Part.makePolygon(h))
sz = 2.1 * scale
cube = Part.makeBox(sz, sz, sz)
cube.translate(FreeCAD.Vector(-sz / 2, -sz / 2, -sz))
sunpaths = [sp.cut(cube) for sp in sunpaths]
hourpaths = [hp.cut(cube) for hp in hourpaths]
ts = 0.005 * scale
mastersep = coin.SoSeparator()
circlesep = coin.SoSeparator()
numsep = coin.SoSeparator()
pathsep = coin.SoSeparator()
hoursep = coin.SoSeparator()
hournumsep = coin.SoSeparator()
mastersep.addChild(circlesep)
mastersep.addChild(numsep)
mastersep.addChild(pathsep)
mastersep.addChild(hoursep)
for item in circles:
circlesep.addChild(toNode(item))
for item in sunpaths:
for w in item.Edges:
pathsep.addChild(toNode(w))
for item in hourpaths:
for w in item.Edges:
hoursep.addChild(toNode(w))
for p in circlepos:
text = coin.SoText2()
s = p[0] - 90
s = -s
if s > 360:
s = s - 360
if s < 0:
s = 360 + s
if s == 0:
s = "N"
elif s == 90:
s = "E"
elif s == 180:
s = "S"
elif s == 270:
s = "W"
else:
s = str(s)
text.string = s
text.justification = coin.SoText2.CENTER
coords = coin.SoTransform()
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
coords.scaleFactor.setValue([ts, ts, ts])
item = coin.SoSeparator()
item.addChild(coords)
item.addChild(text)
numsep.addChild(item)
for p in hourpos:
text = coin.SoText2()
s = str(p[0])
text.string = s
text.justification = coin.SoText2.CENTER
coords = coin.SoTransform()
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
coords.scaleFactor.setValue([ts, ts, ts])
item = coin.SoSeparator()
item.addChild(coords)
item.addChild(text)
numsep.addChild(item)
return mastersep
def makeWindRose(epwfile, scale=1, sectors=24):
try:
import ladybug
from ladybug import epw
except:
FreeCAD.Console.PrintError("The ladybug module was not found. Unable to generate solar diagrams\n")
return None
if not epwfile:
FreeCAD.Console.PrintWarning("No EPW file, unable to generate wind rose.\n")
return None
epw_data = ladybug.epw.EPW(epwfile)
baseangle = 360 / sectors
sectorangles = [i * baseangle for i in range(sectors)]
basebissect = baseangle / 2
angles = [basebissect]
for i in range(1, sectors):
angles.append(angles[-1] + baseangle)
windsbysector = [0 for i in range(sectors)]
for hour in epw_data.wind_direction:
sector = min(angles, key=lambda x: abs(x - hour))
sectorindex = angles.index(sector)
windsbysector[sectorindex] = windsbysector[sectorindex] + 1
maxwind = max(windsbysector)
windsbysector = [wind / maxwind for wind in windsbysector]
vectors = []
dividers = []
for i in range(sectors):
angle = math.radians(90 + angles[i])
x = math.cos(angle) * windsbysector[i] * scale
y = math.sin(angle) * windsbysector[i] * scale
vectors.append(FreeCAD.Vector(x, y, 0))
secangle = math.radians(90 + sectorangles[i])
x = math.cos(secangle) * scale
y = math.sin(secangle) * scale
dividers.append(FreeCAD.Vector(x, y, 0))
vectors.append(vectors[0])
import Part
masternode = coin.SoSeparator()
for r in (0.25, 0.5, 0.75, 1.0):
c = Part.makeCircle(r * scale)
masternode.addChild(toNode(c))
for divider in dividers:
l = Part.makeLine(FreeCAD.Vector(), divider)
masternode.addChild(toNode(l))
ds = coin.SoDrawStyle()
ds.lineWidth = 2.0
masternode.addChild(ds)
d = Part.makePolygon(vectors)
masternode.addChild(toNode(d))
return masternode
# Values in mm
COMPASS_POINTER_LENGTH = 1000
COMPASS_POINTER_WIDTH = 100
class Compass(object):
def __init__(self):
self.rootNode = self.setupCoin()
def show(self):
self.compassswitch.whichChild = coin.SO_SWITCH_ALL
def hide(self):
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
def rotate(self, angleInDegrees):
self.transform.rotation.setValue(
coin.SbVec3f(0, 0, 1), math.radians(angleInDegrees))
def locate(self, x, y, z):
self.transform.translation.setValue(x, y, z)
def scale(self, area):
s = round(max(math.sqrt(area.getValueAs("m^2").Value) / 10, 1))
self.transform.scaleFactor.setValue(coin.SbVec3f(s, s, 1))
def setupCoin(self):
compasssep = coin.SoSeparator()
self.transform = coin.SoTransform()
darkNorthMaterial = coin.SoMaterial()
darkNorthMaterial.diffuseColor.set1Value(0, 0.5, 0, 0)
lightNorthMaterial = coin.SoMaterial()
lightNorthMaterial.diffuseColor.set1Value(0, 0.9, 0, 0)
darkGreyMaterial = coin.SoMaterial()
darkGreyMaterial.diffuseColor.set1Value(0, 0.9, 0.9, 0.9)
lightGreyMaterial = coin.SoMaterial()
lightGreyMaterial.diffuseColor.set1Value(0, 0.5, 0.5, 0.5)
coords = self.buildCoordinates()
lightColorFaceset = coin.SoIndexedFaceSet()
lightColorCoordinateIndex = [4, 5, 6, -1, 8, 9, 10, -1, 12, 13, 14, -1]
lightColorFaceset.coordIndex.setValues(0, len(lightColorCoordinateIndex), lightColorCoordinateIndex)
darkColorFaceset = coin.SoIndexedFaceSet()
darkColorCoordinateIndex = [6, 7, 4, -1, 10, 11, 8, -1, 14, 15, 12, -1]
darkColorFaceset.coordIndex.setValues(0, len(darkColorCoordinateIndex), darkColorCoordinateIndex)
lightNorthFaceset = coin.SoIndexedFaceSet()
lightNorthCoordinateIndex = [2, 3, 0, -1]
lightNorthFaceset.coordIndex.setValues(0, len(lightNorthCoordinateIndex), lightNorthCoordinateIndex)
darkNorthFaceset = coin.SoIndexedFaceSet()
darkNorthCoordinateIndex = [0, 1, 2, -1]
darkNorthFaceset.coordIndex.setValues(0, len(darkNorthCoordinateIndex), darkNorthCoordinateIndex)
self.compassswitch = coin.SoSwitch()
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
self.compassswitch.addChild(compasssep)
lightGreySeparator = coin.SoSeparator()
lightGreySeparator.addChild(lightGreyMaterial)
lightGreySeparator.addChild(lightColorFaceset)
darkGreySeparator = coin.SoSeparator()
darkGreySeparator.addChild(darkGreyMaterial)
darkGreySeparator.addChild(darkColorFaceset)
lightNorthSeparator = coin.SoSeparator()
lightNorthSeparator.addChild(lightNorthMaterial)
lightNorthSeparator.addChild(lightNorthFaceset)
darkNorthSeparator = coin.SoSeparator()
darkNorthSeparator.addChild(darkNorthMaterial)
darkNorthSeparator.addChild(darkNorthFaceset)
compasssep.addChild(coords)
compasssep.addChild(self.transform)
compasssep.addChild(lightGreySeparator)
compasssep.addChild(darkGreySeparator)
compasssep.addChild(lightNorthSeparator)
compasssep.addChild(darkNorthSeparator)
return self.compassswitch
def buildCoordinates(self):
coords = coin.SoCoordinate3()
coords.point.set1Value(0, 0, 0, 0)
coords.point.set1Value(1, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(2, 0, COMPASS_POINTER_LENGTH, 0)
coords.point.set1Value(3, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(4, 0, 0, 0)
coords.point.set1Value(5, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(6, COMPASS_POINTER_LENGTH, 0, 0)
coords.point.set1Value(7, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(8, 0, 0, 0)
coords.point.set1Value(9, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(10, 0, -COMPASS_POINTER_LENGTH, 0)
coords.point.set1Value(11, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(12, 0, 0, 0)
coords.point.set1Value(13, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
coords.point.set1Value(14, -COMPASS_POINTER_LENGTH, 0, 0)
coords.point.set1Value(15, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
return coords
+283
View File
@@ -0,0 +1,283 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD, math
from pivy import coin
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
from PVPlant.core.solar_compass import makeSolarDiagram, makeWindRose, Compass
class ViewProviderSite(object):
"""View Provider for the Site object. Handles solar diagram, wind rose, compass and true north."""
def __init__(self, vobj):
vobj.Proxy = self
vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
self.setProperties(vobj)
def setProperties(self, vobj):
from PVPlantResources import DirIcons as DirIcons
pl = vobj.PropertiesList
if not "WindRose" in pl:
vobj.addProperty("App::PropertyBool", "WindRose", "Site",
QT_TRANSLATE_NOOP("App::Property", "Show wind rose diagram or not. Uses solar diagram scale. Needs Ladybug module"))
if not "SolarDiagram" in pl:
vobj.addProperty("App::PropertyBool", "SolarDiagram", "Site",
QT_TRANSLATE_NOOP("App::Property", "Show solar diagram or not"))
if not "SolarDiagramScale" in pl:
vobj.addProperty("App::PropertyFloat", "SolarDiagramScale", "Site",
QT_TRANSLATE_NOOP("App::Property", "The scale of the solar diagram"))
vobj.SolarDiagramScale = 1
if not "SolarDiagramPosition" in pl:
vobj.addProperty("App::PropertyVector", "SolarDiagramPosition", "Site",
QT_TRANSLATE_NOOP("App::Property", "The position of the solar diagram"))
if not "SolarDiagramColor" in pl:
vobj.addProperty("App::PropertyColor", "SolarDiagramColor", "Site",
QT_TRANSLATE_NOOP("App::Property", "The color of the solar diagram"))
vobj.SolarDiagramColor = (0.16, 0.16, 0.25)
if not "Orientation" in pl:
vobj.addProperty("App::PropertyEnumeration", "Orientation", "Site",
QT_TRANSLATE_NOOP("App::Property", "When set to 'True North' the whole geometry will be rotated to match the true north of this site"))
vobj.Orientation = ["Project North", "True North"]
vobj.Orientation = "Project North"
if not "Compass" in pl:
vobj.addProperty("App::PropertyBool", "Compass", "Compass",
QT_TRANSLATE_NOOP("App::Property", "Show compass or not"))
if not "CompassRotation" in pl:
vobj.addProperty("App::PropertyAngle", "CompassRotation", "Compass",
QT_TRANSLATE_NOOP("App::Property", "The rotation of the Compass relative to the Site"))
if not "CompassPosition" in pl:
vobj.addProperty("App::PropertyVector", "CompassPosition", "Compass",
QT_TRANSLATE_NOOP("App::Property", "The position of the Compass relative to the Site placement"))
if not "UpdateDeclination" in pl:
vobj.addProperty("App::PropertyBool", "UpdateDeclination", "Compass",
QT_TRANSLATE_NOOP("App::Property", "Update the Declination value based on the compass rotation"))
def onDocumentRestored(self, vobj):
self.setProperties(vobj)
def getIcon(self):
from PVPlantResources import DirIcons as DirIcons
return str(os.path.join(DirIcons, "solar-panel.svg"))
def claimChildren(self):
objs = []
if hasattr(self, "Object"):
objs = self.Object.Group + [self.Object.Terrain]
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
if hasattr(self.Object, "Additions") and prefs.GetBool("swallowAdditions", True):
objs.extend(self.Object.Additions)
if hasattr(self.Object, "Subtractions") and prefs.GetBool("swallowSubtractions", True):
objs.extend(self.Object.Subtractions)
return objs
def setEdit(self, vobj, mode):
if (mode == 0) and hasattr(self, "Object"):
import ArchComponent
taskd = ArchComponent.ComponentTaskPanel()
taskd.obj = self.Object
taskd.update()
FreeCADGui.Control.showDialog(taskd)
return True
return False
def unsetEdit(self, vobj, mode):
FreeCADGui.Control.closeDialog()
return False
def attach(self, vobj):
self.Object = vobj.Object
basesep = coin.SoSeparator()
vobj.Annotation.addChild(basesep)
self.color = coin.SoBaseColor()
self.coords = coin.SoTransform()
basesep.addChild(self.coords)
basesep.addChild(self.color)
self.diagramsep = coin.SoSeparator()
self.diagramswitch = coin.SoSwitch()
self.diagramswitch.whichChild = -1
self.diagramswitch.addChild(self.diagramsep)
basesep.addChild(self.diagramswitch)
self.windrosesep = coin.SoSeparator()
self.windroseswitch = coin.SoSwitch()
self.windroseswitch.whichChild = -1
self.windroseswitch.addChild(self.windrosesep)
basesep.addChild(self.windroseswitch)
self.compass = Compass()
self.updateCompassVisibility(vobj)
self.updateCompassScale(vobj)
self.rotateCompass(vobj)
vobj.Annotation.addChild(self.compass.rootNode)
def updateData(self, obj, prop):
if prop in ["Longitude", "Latitude"]:
self.onChanged(obj.ViewObject, "SolarDiagram")
elif prop == "Declination":
self.onChanged(obj.ViewObject, "SolarDiagramPosition")
self.updateTrueNorthRotation()
elif prop == "Terrain":
self.updateCompassLocation(obj.ViewObject)
elif prop == "Placement":
self.updateCompassLocation(obj.ViewObject)
self.updateDeclination(obj.ViewObject)
elif prop == "ProjectedArea":
self.updateCompassScale(obj.ViewObject)
def onChanged(self, vobj, prop):
if prop == "SolarDiagramPosition":
if hasattr(vobj, "SolarDiagramPosition"):
p = vobj.SolarDiagramPosition
self.coords.translation.setValue([p.x, p.y, p.z])
if hasattr(vobj.Object, "Declination"):
self.coords.rotation.setValue(coin.SbVec3f((0, 0, 1)), math.radians(vobj.Object.Declination.Value))
elif prop == "SolarDiagramColor":
if hasattr(vobj, "SolarDiagramColor"):
l = vobj.SolarDiagramColor
self.color.rgb.setValue([l[0], l[1], l[2]])
elif "SolarDiagram" in prop:
if hasattr(self, "diagramnode"):
self.diagramsep.removeChild(self.diagramnode)
del self.diagramnode
if hasattr(vobj, "SolarDiagram") and hasattr(vobj, "SolarDiagramScale"):
if vobj.SolarDiagram:
tz = 0
if hasattr(vobj.Object, "TimeZone"):
tz = vobj.Object.TimeZone
self.diagramnode = makeSolarDiagram(vobj.Object.Longitude, vobj.Object.Latitude,
vobj.SolarDiagramScale, tz=tz)
if self.diagramnode:
self.diagramsep.addChild(self.diagramnode)
self.diagramswitch.whichChild = 0
else:
del self.diagramnode
else:
self.diagramswitch.whichChild = -1
elif prop == "WindRose":
if hasattr(self, "windrosenode"):
del self.windrosenode
if hasattr(vobj, "WindRose"):
if vobj.WindRose:
if hasattr(vobj.Object, "EPWFile") and vobj.Object.EPWFile:
try:
import ladybug
except:
pass
else:
self.windrosenode = makeWindRose(vobj.Object.EPWFile, vobj.SolarDiagramScale)
if self.windrosenode:
self.windrosesep.addChild(self.windrosenode)
self.windroseswitch.whichChild = 0
else:
del self.windrosenode
else:
self.windroseswitch.whichChild = -1
elif prop == 'Visibility':
if vobj.Visibility:
self.updateCompassVisibility(self.Object)
else:
self.compass.hide()
elif prop == 'Orientation':
if vobj.Orientation == 'True North':
self.addTrueNorthRotation()
else:
self.removeTrueNorthRotation()
elif prop == "UpdateDeclination":
self.updateDeclination(vobj)
elif prop == "Compass":
self.updateCompassVisibility(vobj)
elif prop == "CompassRotation":
self.updateDeclination(vobj)
self.rotateCompass(vobj)
elif prop == "CompassPosition":
self.updateCompassLocation(vobj)
def updateDeclination(self, vobj):
if not hasattr(vobj, 'UpdateDeclination') or not vobj.UpdateDeclination:
return
compassRotation = vobj.CompassRotation.Value
siteRotation = math.degrees(vobj.Object.Placement.Rotation.Angle)
vobj.Object.Declination = compassRotation + siteRotation
def addTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
return
self.trueNorthRotation = coin.SoTransform()
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
sg.insertChild(self.trueNorthRotation, 0)
self.updateTrueNorthRotation()
def removeTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
sg.removeChild(self.trueNorthRotation)
self.trueNorthRotation = None
def updateTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
angle = self.Object.Declination.Value
self.trueNorthRotation.rotation.setValue(coin.SbVec3f(0, 0, 1), math.radians(-angle))
def updateCompassVisibility(self, vobj):
if not hasattr(self, 'compass'):
return
show = hasattr(vobj, 'Compass') and vobj.Compass
if show:
self.compass.show()
else:
self.compass.hide()
def rotateCompass(self, vobj):
if not hasattr(self, 'compass'):
return
if hasattr(vobj, 'CompassRotation'):
self.compass.rotate(vobj.CompassRotation.Value)
def updateCompassLocation(self, vobj):
if not hasattr(self, 'compass'):
return
if not vobj.Object.Shape:
return
boundBox = vobj.Object.Shape.BoundBox
pos = vobj.Object.Placement.Base
x = 0
y = 0
if hasattr(vobj, "CompassPosition"):
x = vobj.CompassPosition.x
y = vobj.CompassPosition.y
z = boundBox.ZMax = pos.z
self.compass.locate(x, y, z + 1000)
def updateCompassScale(self, vobj):
if not hasattr(self, 'compass'):
return
self.compass.scale(vobj.Object.ProjectedArea)
def __getstate__(self):
return None
def __setstate__(self, state):
return None
View File
+671
View File
@@ -0,0 +1,671 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import json
import urllib.request
import Draft
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import os
from PVPlantResources import DirIcons as DirIcons
import PVPlantSite
def get_elevation_from_oe(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
Args:
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
Returns:
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
o lista vacía en caso de error.
"""
if not coordinates:
return []
import requests
from lib.projection import latlon_to_utm
from requests.exceptions import RequestException
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
try:
response = requests.get(
url="https://api.open-elevation.com/api/v1/lookup",
params={'locations': locations},
timeout=20,
verify=True
)
response.raise_for_status()
except RequestException as e:
print(f"Error en la solicitud: {str(e)}")
return []
try:
data = response.json()
except ValueError:
print("Respuesta JSON inválida")
return []
if "results" not in data or len(data["results"]) != len(coordinates):
print("Formato de respuesta inesperado")
return []
points = []
for result in data["results"]:
try:
easting, northing, _, _ = latlon_to_utm(
result["latitude"],
result["longitude"]
)
points.append(FreeCAD.Vector(round(easting),
round(northing),
round(result["elevation"])) * 1000)
except Exception as e:
print(f"Error procesando coordenadas: {str(e)}")
continue
return points
def getElevationFromOE(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
import certifi
from requests.exceptions import RequestException
if len(coordinates) == 0:
return None
from requests import get
from lib.projection import latlon_to_utm
locations_str=""
total = len(coordinates) - 1
for i, point in enumerate(coordinates):
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
if i != total:
locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
points = []
try:
r = get(query, timeout=20, verify=certifi.where())
results = r.json()
for point in results["results"]:
easting, northing, _, _ = latlon_to_utm(point["latitude"], point["longitude"])
v = FreeCAD.Vector(round(easting, 0),
round(northing, 0),
round(point["elevation"], 0)) * 1000
points.append(v)
except RequestException as e:
for point in coordinates:
easting, northing, _, _ = latlon_to_utm(point[0], point[1])
points.append(FreeCAD.Vector(round(easting, 0),
round(northing, 0),
0) * 1000)
return points
def getSinglePointElevationFromBing(lat, lng):
import requests
from lib.projection import latlon_to_utm
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng)
source += "&heights=sealevel"
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
s = json.loads(ans)
print(s)
res = s['resourceSets'][0]['resources'][0]['elevations']
for elevation in res:
easting, northing, _, _ = latlon_to_utm(lat, lng)
v = FreeCAD.Vector(
round(easting * 1000, 0),
round(northing * 1000, 0),
round(elevation * 1000, 0))
return v
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
import math
import requests
from lib.projection import latlon_to_utm, utm_to_latlon
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
xx = polygon.Shape.BoundBox.XMin
while xx < polygon.Shape.BoundBox.XMax:
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
if StepsXX > 1000:
StepsXX = 1000
xx1 = xx + 1000 * resolution
else:
xx1 = xx + StepsXX * resolution
point1 = utm_to_latlon(xx / 1000, yy / 1000, zone_number, zone_letter)
point2 = utm_to_latlon(xx1 / 1000, yy / 1000, zone_number, zone_letter)
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
source += ","
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
source += "&heights=sealevel"
source += "&samples={steps}".format(steps=StepsXX)
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['resourceSets'][0]['resources'][0]['elevations']
i = 0
for elevation in res:
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
points.append(v)
i += 1
xx = xx1 + resolution
yy -= resolution
return points
def getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2)
)
return v
def _getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
import pymap3d as pm
for r in res:
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
round(r['location']['lat'], 2),
0)
v = FreeCAD.Vector(x,y,z)
return v
def getSinglePointElevation1(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 4),
round(c[1], 4),
round(r['elevation'] * 1000, 2)
)
return v
def getSinglePointElevationUtm(lat, lon):
import requests
from lib.projection import latlon_to_utm
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
print(source)
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['results']
print(res)
for r in res:
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(easting * 1000, 4),
round(northing * 1000, 4),
round(r['elevation'] * 1000, 2))
print(v)
return v
def getElevationUTM(polygon, lat, lng, resolution = 10000):
from lib.projection import latlon_to_utm, utm_to_latlon
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = utm_to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, zone_number, zone_letter)
point2 = utm_to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, zone_number, zone_letter)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
import requests
response = requests.get(source)
ans = response.text
s = json.loads(ans)
res = s['results']
for r in res:
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(easting * 1000, 2),
round(northing * 1000, 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
yy -= (resolution*1000)
FreeCAD.activeDocument().recompute()
return points
def getElevation1(polygon,resolution=10):
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
try:
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
except:
continue
#points = []
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 2),
round(c[1], 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
FreeCAD.activeDocument().recompute()
yy -= (resolution*1000)
return points
## download the heights from google:
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
#https://maps.googleapis.com/maps/api/elevation/json?path=36.578581,-118.291994|36.23998,-116.83171&samples=3&key=YOUR_API_KEY
#https://maps.googleapis.com/maps/api/elevation/json?locations=39.7391536,-104.9847034&key=YOUR_API_KEY
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += str(b-size*0.001) + "," + str(le) + "|" + str(b+size*0.001) + "," + str(le)
source += "&samples=" + str(100)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
response = urllib.request.urlopen(source)
ans = response.read()
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
points = []
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lat'])).m, 2),
round(geodesic(reference, (r['location']['lng'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2) - baseheight
)
points.append(v)
line = Draft.makeWire(points, closed=False, face=False, support=None)
line.ViewObject.Visibility = False
#FreeCAD.activeDocument().recompute()
FreeCADGui.updateGui()
return FreeCAD.activeDocument().ActiveObject
class _ImportPointsTaskPanel:
def __init__(self, obj = None):
self.obj = None
self.Boundary = None
self.select = 0
self.filename = ""
# form:
self.form1 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantImportGrid.ui")
self.form1.radio1.toggled.connect(lambda: self.mainToggle(self.form1.radio1))
self.form1.radio2.toggled.connect(lambda: self.mainToggle(self.form1.radio2))
self.form1.radio1.setChecked(True) # << --------------Poner al final para que no dispare antes de crear los componentes a los que va a llamar
#self.form.buttonAdd.clicked.connect(self.add)
self.form1.buttonDEM.clicked.connect(self.openFileDEM)
self.form2 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantCreateTerrainMesh.ui")
#self.form2.buttonAdd.clicked.connect(self.add)
self.form2.buttonBoundary.clicked.connect(self.addBoundary)
#self.form = [self.form1, self.form2]
self.form = self.form1
''' future:
def retranslateUi(self, dialog):
self.form1.setWindowTitle("Configuracion del Rack")
self.labelModule.setText(QtGui.QApplication.translate("PVPlant", "Modulo:", None))
self.labelModuleLength.setText(QtGui.QApplication.translate("PVPlant", "Longitud:", None))
self.labelModuleWidth.setText(QtGui.QApplication.translate("PVPlant", "Ancho:", None))
self.labelModuleHeight.setText(QtGui.QApplication.translate("PVPlant", "Alto:", None))
self.labelModuleFrame.setText(QtGui.QApplication.translate("PVPlant", "Ancho del marco:", None))
self.labelModuleColor.setText(QtGui.QApplication.translate("PVPlant", "Color del modulo:", None))
self.labelModules.setText(QtGui.QApplication.translate("Arch", "Colocacion de los Modulos", None))
self.labelModuleOrientation.setText(QtGui.QApplication.translate("Arch", "Orientacion del modulo:", None))
self.labelModuleGapX.setText(QtGui.QApplication.translate("Arch", "Separacion Horizontal (mm):", None))
self.labelModuleGapY.setText(QtGui.QApplication.translate("Arch", "Separacion Vertical (mm):", None))
self.labelModuleRows.setText(QtGui.QApplication.translate("Arch", "Filas de modulos:", None))
self.labelModuleCols.setText(QtGui.QApplication.translate("Arch", "Columnas de modulos:", None))
self.labelRack.setText(QtGui.QApplication.translate("Arch", "Configuracion de la estructura", None))
self.labelRackType.setText(QtGui.QApplication.translate("Arch", "Tipo de estructura:", None))
self.labelLevel.setText(QtGui.QApplication.translate("Arch", "Nivel:", None))
self.labelOffset.setText(QtGui.QApplication.translate("Arch", "Offset", None))
'''
def add(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.obj = sel[0]
self.lineEdit1.setText(self.obj.Label)
def addBoundary(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Boundary = sel[0]
self.form2.editBoundary.setText(self.Boundary.Label)
def openFileDEM(self):
filters = "Esri ASC (*.asc);;CSV (*.csv);;All files (*.*)"
filename = QtGui.QFileDialog.getOpenFileName(None,
"Open DEM,",
"",
filters)
self.filename = filename[0]
self.form1.editDEM.setText(filename[0])
def mainToggle(self, radiobox):
if radiobox is self.form1.radio1:
self.select = 0
self.form1.gbLocalFile.setVisible(True)
elif radiobox is self.form1.radio2:
self.select = 1
self.form1.gbLocalFile.setVisible(True)
def accept(self):
from datetime import datetime
starttime = datetime.now()
site = PVPlantSite.get()
try:
PointGroups = FreeCAD.ActiveDocument.Point_Groups
except:
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
PointGroups.Label = "Point Groups"
PointGroup = FreeCAD.ActiveDocument.addObject('Points::Feature', "Point_Group")
PointGroup.Label = "Land_Grid_Points"
FreeCAD.ActiveDocument.Point_Groups.addObject(PointGroup)
PointObject = PointGroup.Points.copy()
if self.select == 0: # Google or bing or ...
#for item in self.obj:
#if self.groupbox.isChecked:break
resol = FreeCAD.Units.Quantity(self.valueResolution.text()).Value
Site = FreeCAD.ActiveDocument.Site
pts = getGridElevationFromBing(self.obj, Site.Latitude, Site.Longitude, resol)
PointObject.addPoints(pts)
PointGroup.Points = PointObject
else:
if self.filename == "":
return
import Utils.importDEM as openDEM
if self.select == 1: # DEM.
import numpy as np
root, extension = os.path.splitext(self.filename)
if extension.lower() == ".asc":
x, y, datavals, cellsize, nodata_value = openDEM.openEsri(self.filename)
if self.Boundary:
inc_x = self.Boundary.Shape.BoundBox.XLength * 0.05
inc_y = self.Boundary.Shape.BoundBox.YLength * 0.05
min_x = 0
max_x = 0
comp = (self.Boundary.Shape.BoundBox.XMin - inc_x) / 1000
for i in range(nx):
if x[i] > comp:
min_x = i - 1
break
comp = (self.Boundary.Shape.BoundBox.XMax + inc_x) / 1000
for i in range(min_x, nx):
if x[i] > comp:
max_x = i
break
min_y = 0
max_y = 0
comp = (self.Boundary.Shape.BoundBox.YMax + inc_y) / 1000
for i in range(ny):
if y[i] < comp:
max_y = i
break
comp = (self.Boundary.Shape.BoundBox.YMin - inc_y) / 1000
for i in range(max_y, ny):
if y[i] < comp:
min_y = i
break
x = x[min_x:max_x]
y = y[max_y:min_y]
datavals = datavals[max_y:min_y, min_x:max_x]
pts = []
if True: # faster but more memory 46s - 4,25 gb
x, y = np.meshgrid(x, y)
xx = x.flatten()
yy = y.flatten()
zz = datavals.flatten()
x[:] = 0
y[:] = 0
datavals[:] = 0
pts = []
for i in range(0, len(xx)):
pts.append(FreeCAD.Vector(xx[i], yy[i], zz[i]) * 1000)
xx[:] = 0
yy[:] = 0
zz[:] = 0
else: # 51s 3,2 gb
createmesh = True
if createmesh:
import Part, Draft
lines=[]
for j in range(len(y)):
edges = []
for i in range(0, len(x) - 1):
ed = Part.makeLine(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000,
FreeCAD.Vector(x[i + 1], y[j], datavals[j][i + 1]) * 1000)
edges.append(ed)
#bspline = Draft.makeBSpline(pts)
#bspline.ViewObject.hide()
line = Part.Wire(edges)
lines.append(line)
'''
for i in range(0, len(bsplines), 100):
p = Part.makeLoft(bsplines[i:i + 100], False, False, False)
Part.show(p)
'''
p = Part.makeLoft(lines, False, True, False)
p = Part.Solid(p)
Part.show(p)
else:
pts = []
for j in range(ny):
for i in range(nx):
pts.append(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000)
elif extension.lower() == ".csv" or extension.lower() == ".txt": # x, y, z from gps
pts = openDEM.interpolatePoints(openDEM.openCSV(self.filename))
PointObject.addPoints(pts)
PointGroup.Points = PointObject
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Control.closeDialog()
print("tiempo: ", datetime.now() - starttime)
def reject(self):
FreeCADGui.Control.closeDialog()
## Comandos -----------------------------------------------------------------------------------------------------------
class CommandImportPoints:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "cloud.svg")),
'MenuText': QT_TRANSLATE_NOOP("PVPlant", "Importer Grid"),
'Accel': "B, U",
'ToolTip': QT_TRANSLATE_NOOP("PVPlant", "Creates a cloud of points.")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.TaskPanel = _ImportPointsTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
if FreeCAD.GuiUp:
class CommandPointsGroup:
def GetCommands(self):
return tuple(['ImportPoints'
])
def GetResources(self):
return { 'MenuText': QT_TRANSLATE_NOOP("",'Cloud of Points'),
'ToolTip': QT_TRANSLATE_NOOP("",'Cloud of Points')
}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
FreeCADGui.addCommand('ImportPoints', CommandImportPoints())
FreeCADGui.addCommand('PointsGroup', CommandPointsGroup())
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -760,7 +760,7 @@ class EarthWorksTaskPanel:
if len(section) > 0:
try:
boundary.add(Part.makePolygon(section))
except Part.OCCError:
except:
pass
Part.show(boundary)'''
#mesh.smooth("Laplace", 3)
+5 -519
View File
@@ -20,524 +20,10 @@
# * *
# ***********************************************************************
import FreeCAD
import utm
"""
PVPlantGeoreferencing - Wrapper de compatibilidad.
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
Código movido a PVPlant/core/georef.py.
"""
import os
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirResources as DirResources
class MapWindow(QtGui.QWidget):
def __init__(self, WinTitle="MapWindow"):
super(MapWindow, self).__init__()
self.raise_()
self.lat = None
self.lon = None
self.minLat = None
self.maxLat = None
self.minLon = None
self.maxLon = None
self.zoom = None
self.WinTitle = WinTitle
self.georeference_coordinates = {'lat': None, 'lon': None}
self.setupUi()
def setupUi(self):
from PySide2.QtWebEngineWidgets import QWebEngineView
from PySide2.QtWebChannel import QWebChannel
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
self.resize(1200, 800)
self.setWindowTitle(self.WinTitle)
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.layout = QtGui.QHBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
LeftWidget = QtGui.QWidget(self)
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
LeftWidget.setLayout(LeftLayout)
LeftLayout.setContentsMargins(0, 0, 0, 0)
RightWidget = QtGui.QWidget(self)
RightWidget.setFixedWidth(350)
RightLayout = QtGui.QVBoxLayout(RightWidget)
RightWidget.setLayout(RightLayout)
RightLayout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(LeftWidget)
self.layout.addWidget(RightWidget)
# Left Widgets:
# -- Search Bar:
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Search")
self.valueSearch.returnPressed.connect(self.onSearch)
searchbutton = QtGui.QPushButton('Search')
searchbutton.setFixedWidth(80)
searchbutton.clicked.connect(self.onSearch)
SearchBarLayout = QtGui.QHBoxLayout(self)
SearchBarLayout.addWidget(self.valueSearch)
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
# -- Webbroser:
self.view = QWebEngineView()
self.channel = QWebChannel(self.view.page())
self.view.page().setWebChannel(self.channel)
self.channel.registerObject("MyApp", self)
file = os.path.join(DirResources, "webs", "main.html")
self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view)
# self.layout.addWidget(self.view, 1, 0, 1, 3)
# -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates)
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
# Right Widgets:
labelKMZ = QtGui.QLabel()
labelKMZ.setText("Cargar un archivo KMZ/KML:")
self.kmlButton = QtGui.QPushButton()
self.kmlButton.setFixedSize(32, 32)
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
widget = QtGui.QWidget(self)
layout = QtGui.QHBoxLayout(widget)
widget.setLayout(layout)
layout.addWidget(labelKMZ)
layout.addWidget(self.kmlButton)
RightLayout.addWidget(widget)
# -----------------------
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
self.groupbox.setCheckable(True)
self.groupbox.setChecked(True)
radio1 = QtGui.QRadioButton("Google Elevation")
radio2 = QtGui.QRadioButton("Nube de Puntos")
radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True)
# buttonDialog = QtGui.QPushButton('...')
# buttonDialog.setEnabled(False)
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1)
vbox.addWidget(radio2)
vbox.addWidget(radio3)
self.groupbox.setLayout(vbox)
RightLayout.addWidget(self.groupbox)
# ------------------------
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
RightLayout.addWidget(self.checkboxImportGis)
self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital")
RightLayout.addWidget(self.checkboxImportSatelitalImagen)
verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
RightLayout.addItem(verticalSpacer)
self.bAccept = QtGui.QPushButton('Accept')
self.bAccept.clicked.connect(self.onAcceptClick)
RightLayout.addWidget(self.bAccept)
# signals/slots
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
def onLoadFinished(self):
file = os.path.join(DirResources, "webs", "map.js")
frame = self.view.page()
with open(file, 'r') as f:
frame.runJavaScript(f.read())
def onSearch(self):
if self.valueSearch.text() == "":
return
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="http")
location = geolocator.geocode(self.valueSearch.text())
self.valueSearch.setText(location.address)
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
def onAcceptClick(self):
frame = self.view.page()
# 1. georeferenciar
frame.runJavaScript(
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
)
# 2. importar todos los elementos dibujados:
frame.runJavaScript(
"var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));"
)
self.close()
@QtCore.Slot(float, float)
def onMapMove(self, lat, lng):
self.lat = lat
self.lon = lng
x, y, zone_number, zone_letter = utm.from_latlon(lat, lng)
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(x, y))
@QtCore.Slot(float, float, float, float, int)
def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
self.minLat = min([minLat, maxLat])
self.maxLat = max([minLat, maxLat])
self.minLon = min([minLon, maxLon])
self.maxLon = max([minLon, maxLon])
self.zoom = zoom
@QtCore.Slot(float, float)
def georeference(self, lat, lng):
import PVPlantSite
from geopy.geocoders import Nominatim
self.georeference_coordinates['lat'] = lat
self.georeference_coordinates['lon'] = lng
Site = PVPlantSite.get(create=True)
Site.Proxy.setLatLon(lat, lng)
geolocator = Nominatim(user_agent="http")
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
if location:
if location.raw["address"].get("road"):
str = location.raw["address"]["road"]
if location.raw["address"].get("house_number"):
str += ' ({0})'.format(location.raw["address"]["house_number"])
Site.Address = str
if location.raw["address"].get("city"):
Site.City = location.raw["address"]["city"]
if location.raw["address"].get("postcode"):
Site.PostalCode = location.raw["address"]["postcode"]
if location.raw["address"].get("address"):
Site.Region = '{0}'.format(location.raw["address"]["province"])
if location.raw["address"].get("state"):
if Site.Region != "":
Site.Region += " - "
Site.Region += '{0}'.format(location.raw["address"]["state"]) # province - state
Site.Country = location.raw["address"]["country"]
@QtCore.Slot(str)
def shapes(self, drawnItems):
import geojson
import PVPlantImportGrid as ImportElevation
import Draft
import PVPlantSite
Site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(Site.Origin)
offset.z = 0
items = geojson.loads(drawnItems)
for item in items['features']:
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
coord = item['geometry']['coordinates']
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
if item['properties'].get('radius'):
r = round(item['properties']['radius'] * 1000, 0)
p = FreeCAD.Placement()
p.Base = c
obj = Draft.makeCircle(r, placement=p, face=False)
else:
''' do something '''
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10)
else: # 2. if the feature is a Polygon or Line:
cw = False
name = "Línea"
lp = item['geometry']['coordinates']
if item['geometry']['type'] == "Polygon":
cw = True
name = "Area"
lp = item['geometry']['coordinates'][0]
pts = [[cords[1], cords[0]] for cords in lp]
tmp = ImportElevation.getElevationFromOE(pts)
pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False)
#obj.Placement.Base = Site.Origin
obj.Label = name
Draft.autogroup(obj)
if item['properties'].get('name'):
obj.Label = item['properties']['name']
if self.checkboxImportGis.isChecked():
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
if self.checkboxImportSatelitalImagen.isChecked():
# Usar los límites reales del terreno (rectangular)
'''s_lat = self.minLat
s_lon = self.minLon
n_lat = self.maxLat
n_lon = self.maxLon
# Obtener puntos UTM para las esquinas
corners = ImportElevation.getElevationFromOE([
[s_lat, s_lon], # Esquina suroeste
[n_lat, s_lon], # Esquina sureste
[n_lat, n_lon], # Esquina noreste
[s_lat, n_lon] # Esquina noroeste
])
if not corners or len(corners) < 4:
FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas\n")
return
# Descargar imagen satelital
from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
downloader = GoogleMapDownloader(
zoom= 18, #self.zoom,
layer='raw_satellite'
)
img = downloader.generateImage(
sw_lat=s_lat,
sw_lng=s_lon,
ne_lat=n_lat,
ne_lng=n_lon
)
# Guardar imagen en el directorio del documento
doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
if not doc_path:
doc_path = FreeCAD.ConfigGet("UserAppData")
filename = os.path.join(doc_path, "background.jpeg")
img.save(filename)
ancho, alto = img.size
# Crear objeto de imagen en FreeCAD
doc = FreeCAD.ActiveDocument
img_obj = doc.addObject('Image::ImagePlane', 'Background')
img_obj.ImageFile = filename
img_obj.Label = 'Background'
# Calcular dimensiones en metros usando las coordenadas UTM
# Extraer las coordenadas de las esquinas
sw = corners[0] # Suroeste
se = corners[1] # Sureste
ne = corners[2] # Noreste
nw = corners[3] # Noroeste
# Calcular ancho (promedio de los lados superior e inferior)
width_bottom = se.x - sw.x
width_top = ne.x - nw.x
width_m = (width_bottom + width_top) / 2
# Calcular alto (promedio de los lados izquierdo y derecho)
height_left = nw.y - sw.y
height_right = ne.y - se.y
height_m = (height_left + height_right) / 2
img_obj.XSize = width_m
img_obj.YSize = height_m
# Posicionar el centro de la imagen en (0,0,0)
img_obj.Placement.Base = FreeCAD.Vector(-width_m / 2, -height_m / 2, 0)'''
# Definir área rectangular
s_lat = self.minLat
s_lon = self.minLon
n_lat = self.maxLat
n_lon = self.maxLon
# Obtener puntos UTM para las esquinas y el punto de referencia
points = [
[s_lat, s_lon], # Suroeste
[n_lat, n_lon], # Noreste
[self.georeference_coordinates['lat'], self.georeference_coordinates['lon']] # Punto de referencia
]
utm_points = ImportElevation.getElevationFromOE(points)
if not utm_points or len(utm_points) < 3:
FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas y referencia\n")
return
sw_utm, ne_utm, ref_utm = utm_points
# Descargar imagen satelital
from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
downloader = GoogleMapDownloader(
zoom=self.zoom,
layer='raw_satellite'
)
img = downloader.generateImage(
sw_lat=s_lat,
sw_lng=s_lon,
ne_lat=n_lat,
ne_lng=n_lon
)
# Guardar imagen
doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
if not doc_path:
doc_path = FreeCAD.ConfigGet("UserAppData")
filename = os.path.join(doc_path, "background.jpeg")
img.save(filename)
# Calcular dimensiones reales en metros
width_m = ne_utm.x - sw_utm.x # Ancho en metros (este-oeste)
height_m = ne_utm.y - sw_utm.y # Alto en metros (norte-sur)
# Calcular posición relativa del punto de referencia dentro de la imagen
rel_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5
rel_y = (ref_utm.y - sw_utm.y) / height_m if height_m != 0 else 0.5
# Crear objeto de imagen en FreeCAD
doc = FreeCAD.ActiveDocument
img_obj = doc.addObject('Image::ImagePlane', 'Background')
img_obj.ImageFile = filename
img_obj.Label = 'Background'
# Convertir dimensiones a milímetros (FreeCAD trabaja en mm)
img_obj.XSize = width_m * 1000
img_obj.YSize = height_m * 1000
# Posicionar para que el punto de referencia esté en (0,0,0)
# La esquina inferior izquierda debe estar en:
# x = -rel_x * ancho_total
# y = -rel_y * alto_total
img_obj.Placement.Base = FreeCAD.Vector(
-rel_x * width_m * 1000,
-rel_y * height_m * 1000,
0
)
# Refrescar el documento
doc.recompute()
def calculate_texture_transform(self, mesh_obj, width_m, height_m):
"""Calcula la transformación precisa para la textura"""
try:
# Obtener coordenadas reales de las esquinas
import utm
sw = utm.from_latlon(self.minLat, self.minLon)
ne = utm.from_latlon(self.maxLat, self.maxLon)
# Crear matriz de transformación
scale_x = (ne[0] - sw[0]) / width_m
scale_y = (ne[1] - sw[1]) / height_m
# Aplicar transformación (solo si se usa textura avanzada)
if hasattr(mesh_obj.ViewObject, "TextureMapping"):
mesh_obj.ViewObject.TextureMapping = "PLANE"
mesh_obj.ViewObject.TextureScale = (scale_x, scale_y)
mesh_obj.ViewObject.TextureOffset = (sw[0], sw[1])
except Exception as e:
FreeCAD.Console.PrintWarning(f"No se pudo calcular transformación: {str(e)}\n")
def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM
import PVPlantSite
site = PVPlantSite.get()
offset = FreeCAD.Vector(0, 0, 0)
if not (self.lat is None or self.lon is None):
offset = FreeCAD.Vector(site.Origin)
offset.z = 0
importer = importOSM.OSMImporter(offset)
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
importer.process_osm_data(osm_data)
'''FreeCAD.activeDocument().recompute()
FreeCADGui.updateGui()
FreeCADGui.SendMsgToActiveView("ViewFit")'''
def panMap_old(self, lng, lat, geometry=""):
frame = self.view.page()
bbox = "[{0}, {1}], [{2}, {3}]".format(float(geometry[0]), float(geometry[2]),
float(geometry[1]), float(geometry[3]))
command = 'map.panTo(L.latLng({lt}, {lg}));'.format(lt=lat, lg=lng)
command += 'map.fitBounds([{box}]);'.format(box=bbox)
frame.runJavaScript(command)
# deepseek
def panMap(self, lng, lat, geometry=None):
frame = self.view.page()
# 1. Validación del parámetro geometry
if not geometry or len(geometry) < 4:
# Pan básico sin ajuste de bounds
command = f'map.panTo(L.latLng({lat}, {lng}));'
else:
try:
# 2. Mejor manejo de coordenadas (Leaflet usa [lat, lng])
# Asumiendo que geometry es [min_lng, min_lat, max_lng, max_lat]
southwest = f"{float(geometry[1])}, {float(geometry[0])}" # min_lat, min_lng
northeast = f"{float(geometry[3])}, {float(geometry[2])}" # max_lat, max_lng
command = f'map.panTo(L.latLng({lat}, {lng}));'
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
except (IndexError, ValueError, TypeError) as e:
print(f"Error en geometry: {str(e)}")
command = f'map.panTo(L.latLng({lat}, {lng}));'
frame.runJavaScript(command)
def importKML(self):
file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0]
from lib.kml2geojson import kmz_convert
layers = kmz_convert(file, "", )
frame = self.view.page()
for layer in layers:
command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer)
frame.runJavaScript(command)
class CommandPVPlantGeoreferencing:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
def Activated(self):
self.form = MapWindow()
self.form.show()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantGeoreferencing',_CommandPVPlantGeoreferencing())
'''
from PVPlant.core.georef import MapWindow, CommandPVPlantGeoreferencing
+21 -667
View File
@@ -20,670 +20,24 @@
# * *
# ***********************************************************************
import json
import urllib.request
import Draft
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import os
from PVPlantResources import DirIcons as DirIcons
import PVPlantSite
def get_elevation_from_oe(coordinates): # v1 deepseek
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
Args:
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
Returns:
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
o lista vacía en caso de error.
"""
if not coordinates:
return []
import requests
import utm
from requests.exceptions import RequestException
# Construcción más eficiente de parámetros
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
try:
response = requests.get(
url="https://api.open-elevation.com/api/v1/lookup",
params={'locations': locations},
timeout=20,
verify=True
)
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx
except RequestException as e:
print(f"Error en la solicitud: {e}")
return []
try:
data = response.json()
except ValueError:
print("Respuesta JSON inválida")
return []
if "results" not in data or len(data["results"]) != len(coordinates):
print("Formato de respuesta inesperado")
return []
points = []
for result in data["results"]:
try:
# Conversión UTM con manejo de errores
easting, northing, _, _ = utm.from_latlon(
result["latitude"],
result["longitude"]
)
points.append(FreeCAD.Vector(round(easting), # Convertir metros a milímetros
round(northing),
round(result["elevation"])) * 1000)
except Exception as e:
print(f"Error procesando coordenadas: {e}")
continue
return points
def getElevationFromOE(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
import certifi
from requests.exceptions import RequestException
if len(coordinates) == 0:
return None
from requests import get
import utm
locations_str=""
total = len(coordinates) - 1
for i, point in enumerate(coordinates):
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
if i != total:
locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
points = []
try:
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
results = r.json()
for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"])
v = FreeCAD.Vector(round(c[0], 0),
round(c[1], 0),
round(point["elevation"], 0)) * 1000
points.append(v)
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),
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
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng)
source += "&heights=sealevel"
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
import requests
response = requests.get(source)
ans = response.text
s = json.loads(ans)
print(s)
res = s['resourceSets'][0]['resources'][0]['elevations']
for elevation in res:
c = utm.from_latlon(lat, lng)
v = FreeCAD.Vector(
round(c[0] * 1000, 0),
round(c[1] * 1000, 0),
round(elevation * 1000, 0))
return v
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
#http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=35.89431,-110.72522,35.89393,-110.72578,35.89374,-110.72606,35.89337,-110.72662
# &heights=ellipsoid&samples=10&key={BingMapsAPIKey}
import utm
import math
import requests
geo = utm.from_latlon(lat, lng)
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
#StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
xx = polygon.Shape.BoundBox.XMin
while xx < polygon.Shape.BoundBox.XMax:
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
if StepsXX > 1000:
StepsXX = 1000
xx1 = xx + 1000 * resolution
else:
xx1 = xx + StepsXX * resolution
point1 = utm.to_latlon(xx / 1000, yy / 1000, geo[2], geo[3])
point2 = utm.to_latlon(xx1 / 1000, yy / 1000, geo[2], geo[3])
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
source += ","
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
source += "&heights=sealevel"
source += "&samples={steps}".format(steps=StepsXX)
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['resourceSets'][0]['resources'][0]['elevations']
i = 0
for elevation in res:
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
points.append(v)
i += 1
xx = xx1 + resolution # para no repetir un mismo punto
yy -= resolution
return points
def getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2)
)
return v
def _getSinglePointElevation(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#print (source)
#response = request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
import pymap3d as pm
for r in res:
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
round(r['location']['lat'], 2),
0)
v = FreeCAD.Vector(x,y,z)
return v
def getSinglePointElevation1(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 4),
round(c[1], 4),
round(r['elevation'] * 1000, 2)
)
return v
def getSinglePointElevationUtm(lat, lon):
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
source += str(lat) + "," + str(lon)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
print(source)
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
print (res)
import utm
for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0] * 1000, 4),
round(c[1] * 1000, 4),
round(r['elevation'] * 1000, 2))
print (v)
return v
def getElevationUTM(polygon, lat, lng, resolution = 10000):
import utm
geo = utm.from_latlon(lat, lng)
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
# utm.to_latlon(EASTING, NORTHING, ZONE NUMBER, ZONE LETTER).
# result = (LATITUDE, LONGITUDE)
point1 = utm.to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, geo[2], geo[3])
point2 = utm.to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, geo[2], geo[3])
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0] * 1000, 2),
round(c[1] * 1000, 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
yy -= (resolution*1000)
FreeCAD.activeDocument().recompute()
return points
def getElevation1(polygon,resolution=10):
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
points = []
yy = polygon.Shape.BoundBox.YMax
while yy > polygon.Shape.BoundBox.YMin:
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += "{a},{b}".format(a = point1[0], b = point1[1])
source += "|"
source += "{a},{b}".format(a = point2[0], b = point2[1])
source += "&samples={a}".format(a = StepsXX)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
try:
#response = urllib.request.urlopen(source)
#ans = response.read()
import requests
response = requests.get(source)
ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
except (json.JSONDecodeError, KeyError):
continue
#points = []
for r in res:
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
round(c[0], 2),
round(c[1], 2),
round(r['elevation'] * 1000, 2)
)
points.append(v)
FreeCAD.activeDocument().recompute()
yy -= (resolution*1000)
return points
## download the heights from google:
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
#https://maps.googleapis.com/maps/api/elevation/json?path=36.578581,-118.291994|36.23998,-116.83171&samples=3&key=YOUR_API_KEY
#https://maps.googleapis.com/maps/api/elevation/json?locations=39.7391536,-104.9847034&key=YOUR_API_KEY
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
source += str(b-size*0.001) + "," + str(le) + "|" + str(b+size*0.001) + "," + str(le)
source += "&samples=" + str(100)
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
response = urllib.request.urlopen(source)
ans = response.read()
# +# to do: error handling - wait and try again
s = json.loads(ans)
res = s['results']
from geopy.distance import geodesic
points = []
for r in res:
reference = (0.0, 0.0)
v = FreeCAD.Vector(
round(geodesic(reference, (0.0, r['location']['lat'])).m, 2),
round(geodesic(reference, (r['location']['lng'], 0.0)).m, 2),
round(r['elevation'] * 1000, 2) - baseheight
)
points.append(v)
line = Draft.makeWire(points, closed=False, face=False, support=None)
line.ViewObject.Visibility = False
#FreeCAD.activeDocument().recompute()
FreeCADGui.updateGui()
return FreeCAD.activeDocument().ActiveObject
class _ImportPointsTaskPanel:
def __init__(self, obj = None):
self.obj = None
self.Boundary = None
self.select = 0
self.filename = ""
# form:
self.form1 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantImportGrid.ui")
self.form1.radio1.toggled.connect(lambda: self.mainToggle(self.form1.radio1))
self.form1.radio2.toggled.connect(lambda: self.mainToggle(self.form1.radio2))
self.form1.radio1.setChecked(True) # << --------------Poner al final para que no dispare antes de crear los componentes a los que va a llamar
#self.form.buttonAdd.clicked.connect(self.add)
self.form1.buttonDEM.clicked.connect(self.openFileDEM)
self.form2 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantCreateTerrainMesh.ui")
#self.form2.buttonAdd.clicked.connect(self.add)
self.form2.buttonBoundary.clicked.connect(self.addBoundary)
#self.form = [self.form1, self.form2]
self.form = self.form1
''' future:
def retranslateUi(self, dialog):
self.form1.setWindowTitle("Configuracion del Rack")
self.labelModule.setText(QtGui.QApplication.translate("PVPlant", "Modulo:", None))
self.labelModuleLength.setText(QtGui.QApplication.translate("PVPlant", "Longitud:", None))
self.labelModuleWidth.setText(QtGui.QApplication.translate("PVPlant", "Ancho:", None))
self.labelModuleHeight.setText(QtGui.QApplication.translate("PVPlant", "Alto:", None))
self.labelModuleFrame.setText(QtGui.QApplication.translate("PVPlant", "Ancho del marco:", None))
self.labelModuleColor.setText(QtGui.QApplication.translate("PVPlant", "Color del modulo:", None))
self.labelModules.setText(QtGui.QApplication.translate("Arch", "Colocacion de los Modulos", None))
self.labelModuleOrientation.setText(QtGui.QApplication.translate("Arch", "Orientacion del modulo:", None))
self.labelModuleGapX.setText(QtGui.QApplication.translate("Arch", "Separacion Horizontal (mm):", None))
self.labelModuleGapY.setText(QtGui.QApplication.translate("Arch", "Separacion Vertical (mm):", None))
self.labelModuleRows.setText(QtGui.QApplication.translate("Arch", "Filas de modulos:", None))
self.labelModuleCols.setText(QtGui.QApplication.translate("Arch", "Columnas de modulos:", None))
self.labelRack.setText(QtGui.QApplication.translate("Arch", "Configuracion de la estructura", None))
self.labelRackType.setText(QtGui.QApplication.translate("Arch", "Tipo de estructura:", None))
self.labelLevel.setText(QtGui.QApplication.translate("Arch", "Nivel:", None))
self.labelOffset.setText(QtGui.QApplication.translate("Arch", "Offset", None))
'''
def add(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.obj = sel[0]
self.lineEdit1.setText(self.obj.Label)
def addBoundary(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Boundary = sel[0]
self.form2.editBoundary.setText(self.Boundary.Label)
def openFileDEM(self):
filters = "Esri ASC (*.asc);;CSV (*.csv);;All files (*.*)"
filename = QtGui.QFileDialog.getOpenFileName(None,
"Open DEM,",
"",
filters)
self.filename = filename[0]
self.form1.editDEM.setText(filename[0])
def mainToggle(self, radiobox):
if radiobox is self.form1.radio1:
self.select = 0
self.form1.gbLocalFile.setVisible(True)
elif radiobox is self.form1.radio2:
self.select = 1
self.form1.gbLocalFile.setVisible(True)
def accept(self):
from datetime import datetime
starttime = datetime.now()
site = PVPlantSite.get()
try:
PointGroups = FreeCAD.ActiveDocument.Point_Groups
except AttributeError:
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
PointGroups.Label = "Point Groups"
PointGroup = FreeCAD.ActiveDocument.addObject('Points::Feature', "Point_Group")
PointGroup.Label = "Land_Grid_Points"
FreeCAD.ActiveDocument.Point_Groups.addObject(PointGroup)
PointObject = PointGroup.Points.copy()
if self.select == 0: # Google or bing or ...
#for item in self.obj:
#if self.groupbox.isChecked:break
resol = FreeCAD.Units.Quantity(self.valueResolution.text()).Value
Site = FreeCAD.ActiveDocument.Site
pts = getGridElevationFromBing(self.obj, Site.Latitude, Site.Longitude, resol)
PointObject.addPoints(pts)
PointGroup.Points = PointObject
else:
if self.filename == "":
return
import Utils.importDEM as openDEM
if self.select == 1: # DEM.
import numpy as np
root, extension = os.path.splitext(self.filename)
if extension.lower() == ".asc":
x, y, datavals, cellsize, nodata_value = openDEM.openEsri(self.filename)
if self.Boundary:
inc_x = self.Boundary.Shape.BoundBox.XLength * 0.05
inc_y = self.Boundary.Shape.BoundBox.YLength * 0.05
min_x = 0
max_x = 0
comp = (self.Boundary.Shape.BoundBox.XMin - inc_x) / 1000
for i in range(nx):
if x[i] > comp:
min_x = i - 1
break
comp = (self.Boundary.Shape.BoundBox.XMax + inc_x) / 1000
for i in range(min_x, nx):
if x[i] > comp:
max_x = i
break
min_y = 0
max_y = 0
comp = (self.Boundary.Shape.BoundBox.YMax + inc_y) / 1000
for i in range(ny):
if y[i] < comp:
max_y = i
break
comp = (self.Boundary.Shape.BoundBox.YMin - inc_y) / 1000
for i in range(max_y, ny):
if y[i] < comp:
min_y = i
break
x = x[min_x:max_x]
y = y[max_y:min_y]
datavals = datavals[max_y:min_y, min_x:max_x]
pts = []
if True: # faster but more memory 46s - 4,25 gb
x, y = np.meshgrid(x, y)
xx = x.flatten()
yy = y.flatten()
zz = datavals.flatten()
x[:] = 0
y[:] = 0
datavals[:] = 0
pts = []
for i in range(0, len(xx)):
pts.append(FreeCAD.Vector(xx[i], yy[i], zz[i]) * 1000)
xx[:] = 0
yy[:] = 0
zz[:] = 0
else: # 51s 3,2 gb
createmesh = True
if createmesh:
import Part, Draft
lines=[]
for j in range(len(y)):
edges = []
for i in range(0, len(x) - 1):
ed = Part.makeLine(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000,
FreeCAD.Vector(x[i + 1], y[j], datavals[j][i + 1]) * 1000)
edges.append(ed)
#bspline = Draft.makeBSpline(pts)
#bspline.ViewObject.hide()
line = Part.Wire(edges)
lines.append(line)
'''
for i in range(0, len(bsplines), 100):
p = Part.makeLoft(bsplines[i:i + 100], False, False, False)
Part.show(p)
'''
p = Part.makeLoft(lines, False, True, False)
p = Part.Solid(p)
Part.show(p)
else:
pts = []
for j in range(ny):
for i in range(nx):
pts.append(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000)
elif extension.lower() == ".csv" or extension.lower() == ".txt": # x, y, z from gps
pts = openDEM.interpolatePoints(openDEM.openCSV(self.filename))
PointObject.addPoints(pts)
PointGroup.Points = PointObject
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Control.closeDialog()
print("tiempo: ", datetime.now() - starttime)
def reject(self):
FreeCADGui.Control.closeDialog()
## Comandos -----------------------------------------------------------------------------------------------------------
class CommandImportPoints:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "cloud.svg")),
'MenuText': QT_TRANSLATE_NOOP("PVPlant", "Importer Grid"),
'Accel': "B, U",
'ToolTip': QT_TRANSLATE_NOOP("PVPlant", "Creates a cloud of points.")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.TaskPanel = _ImportPointsTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
if FreeCAD.GuiUp:
class CommandPointsGroup:
def GetCommands(self):
return tuple(['ImportPoints'
])
def GetResources(self):
return { 'MenuText': QT_TRANSLATE_NOOP("",'Cloud of Points'),
'ToolTip': QT_TRANSLATE_NOOP("",'Cloud of Points')
}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
FreeCADGui.addCommand('ImportPoints', CommandImportPoints())
FreeCADGui.addCommand('PointsGroup', CommandPointsGroup())
"""
PVPlantImportGrid - Wrapper de compatibilidad.
Código movido a PVPlant/import_grid/grid.py.
"""
from PVPlant.import_grid.grid import (
get_elevation_from_oe,
getElevationFromOE,
getSinglePointElevationFromBing,
getGridElevationFromBing,
getSinglePointElevation,
_getSinglePointElevation,
getSinglePointElevation1,
getSinglePointElevationUtm,
getElevationUTM,
getElevation1,
getElevation,
_ImportPointsTaskPanel,
CommandImportPoints,
)
+9 -2306
View File
File diff suppressed because it is too large Load Diff
+27 -1166
View File
File diff suppressed because it is too large Load Diff
+72 -148
View File
@@ -129,14 +129,8 @@ class Terrain(ArchComponent.Component):
# obj.IfcType = "Fence"
# obj.MoveWithHost = False
try:
self.site = PVPlantSite.get()
except Exception:
self.site = None
if self.site:
self.site.Terrain = obj
else:
FreeCAD.Console.PrintWarning('Terrain: No se encontró Site, algunas funciones DEM requerirán Site.\n')
self.site = PVPlantSite.get()
self.site.Terrain = obj
obj.ViewObject.ShapeColor = (0.0000, 0.6667, 0.4980)
obj.ViewObject.LineColor = (0.0000, 0.6000, 0.4392)
@@ -198,12 +192,6 @@ class Terrain(ArchComponent.Component):
if prop == "InitialMesh":
obj.mesh = obj.InitialMesh.copy()
# Forzar actualización visual
obj.publishProperty("Mesh")
if prop == "mesh":
# La propiedad mesh cambió → forzar recompute para que updateData se dispare
pass
if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime
@@ -249,7 +237,7 @@ class Terrain(ArchComponent.Component):
del templist
# create xy coordinates
offset = self.site.Origin if self.site else FreeCAD.Vector(0, 0, 0)
offset = self.site.Origin
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
datavals = datavals * 1000 # Ajuste de altura
@@ -281,95 +269,35 @@ class Terrain(ArchComponent.Component):
stepx = math.ceil(nx / stepsize)
stepy = math.ceil(ny / stepsize)
# Malla completa primero como numpy y filtramos todo de una
from datetime import datetime
t_start = datetime.now()
# Crear grid completo de coordenadas
XX, YY = np.meshgrid(x, y)
ZZ = datavals.copy()
# Enmascarar nodata
mask_valida = ZZ != nodata_value
# Enmascarar cutting boundary si existe
if obj.CuttingBoundary:
from FreeCAD import Base
shape = obj.CuttingBoundary.Shape
mask_boundary = np.zeros_like(ZZ, dtype=bool)
# Sampling: revisar solo puntos estratégicos para boundary grande
stride = max(1, min(nx, ny) // 200)
for i in range(0, ny, stride):
for j in range(0, nx, stride):
if mask_valida[i, j]:
if shape.isInside(FreeCAD.Vector(x[j], y[i], 0), 0, True):
mask_boundary[i, j] = True
mask_valida = mask_valida & mask_boundary
# Extraer puntos válidos como lista plana
pts_validos = np.column_stack([
XX[mask_valida].ravel(),
YY[mask_valida].ravel(),
ZZ[mask_valida].ravel()
])
del XX, YY, ZZ, mask_valida
# Triangulación completa de una vez (no por parches)
mesh = Mesh.Mesh()
if len(pts_validos) > 3:
# Si hay muchos puntos, triangulamos por parches para evitar OOM
patch_size = 50000
n_patches = max(1, math.ceil(len(pts_validos) / patch_size))
for p in range(n_patches):
patch = pts_validos[p * patch_size:(p + 1) * patch_size].tolist()
if len(patch) > 3:
for indx in range(stepx):
inix = indx * stepsize - 1
finx = min([stepsize * (indx + 1), len(x)-1])
for indy in range(stepy):
iniy = indy * stepsize - 1
finy = min([stepsize * (indy + 1), len(y) - 1])
pts = []
for i in range(inix, finx):
for j in range(iniy, finy):
if datavals[j][i] != nodata_value:
if obj.CuttingBoundary:
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
pts.append([x[i], y[j], datavals[j][i]])
else:
pts.append([x[i], y[j], datavals[j][i]])
if len(pts) > 3:
try:
triangulated = Triangulation.Triangulate(patch)
triangulated = Triangulation.Triangulate(pts)
mesh.addMesh(triangulated)
except TypeError as e:
print(f"Patch {p}: error al procesar {len(patch)} puntos: {str(e)}")
except Exception as e:
print(f"Patch {p}: error inesperado: {str(e)}")
print(f'Terraín DEM: {len(pts_validos)} pts válidos, {n_patches} parches, {datetime.now()-t_start}')
del pts_validos
except TypeError:
print(f"Error al procesar {len(pts)} puntos: {str(e)}")
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
# Limpiar objetos mesh huérfanos previos si existen
for o in FreeCAD.ActiveDocument.Objects:
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
FreeCAD.ActiveDocument.removeObject(o.Name)
mesh_obj = Mesh.show(mesh)
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
Mesh.show(mesh)
elif suffix in ['.xyz']:
pts_array = open_xyz_mmap(obj.DEM)
if pts_array is not None and len(pts_array) > 3:
import MeshTools.Triangulation as Triangulation
import Mesh
if obj.CuttingBoundary:
mask = []
for pt in pts_array:
mask.append(obj.CuttingBoundary.Shape.isInside(
FreeCAD.Vector(pt[0], pt[1], 0), 0, True))
pts_array = pts_array[mask]
if len(pts_array) > 3:
from datetime import datetime
t0 = datetime.now()
pts_list = pts_array.tolist()
mesh = Triangulation.Triangulate(pts_list)
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
# Limpiar objetos mesh huérfanos previos
for o in FreeCAD.ActiveDocument.Objects:
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
FreeCAD.ActiveDocument.removeObject(o.Name)
mesh_obj = Mesh.show(mesh)
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
print(f'XYZ import: {len(pts_array)} puntos en {datetime.now()-t0}')
data = open_xyz_mmap(obj.DEM)
@@ -401,11 +329,6 @@ class Terrain(ArchComponent.Component):
if obj.DEM:
obj.DEM = None
obj.mesh = mesh
# Forzar actualización visual llamando a publishProperty
try:
obj.publishProperty("Mesh")
except:
pass
def execute(self, obj):
''''''
@@ -624,47 +547,47 @@ class ViewProviderTerrain:
offset.factor = -2.0
# Boundary features.
self.boundary_color = coin.SoBaseColor()
'''self.boundary_color = coin.SoBaseColor()
self.boundary_coords = coin.SoGeoCoordinate()
self.boundary_lines = coin.SoLineSet()
self.boundary_style = coin.SoDrawStyle()
self.boundary_style.style = coin.SoDrawStyle.LINES
self.boundary_style.style = coin.SoDrawStyle.LINES'''
# Boundary root.
boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
'''boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
boundaries.style = 'EMISSIVE_DIFFUSE'
boundaries.addChild(self.boundary_color)
boundaries.addChild(self.boundary_style)
boundaries.addChild(self.boundary_coords)
boundaries.addChild(self.boundary_lines)
boundaries.addChild(self.boundary_lines)'''
# Major Contour features.
self.major_color = coin.SoBaseColor()
'''self.major_color = coin.SoBaseColor()
self.major_coords = coin.SoGeoCoordinate()
self.major_lines = coin.SoLineSet()
self.major_style = coin.SoDrawStyle()
self.major_style.style = coin.SoDrawStyle.LINES
self.major_style.style = coin.SoDrawStyle.LINES'''
# Major Contour root.
major_contours = coin.SoSeparator()
'''major_contours = coin.SoSeparator()
major_contours.addChild(self.major_color)
major_contours.addChild(self.major_style)
major_contours.addChild(self.major_coords)
major_contours.addChild(self.major_lines)
major_contours.addChild(self.major_lines)'''
# Minor Contour features.
self.minor_color = coin.SoBaseColor()
'''self.minor_color = coin.SoBaseColor()
self.minor_coords = coin.SoGeoCoordinate()
self.minor_lines = coin.SoLineSet()
self.minor_style = coin.SoDrawStyle()
self.minor_style.style = coin.SoDrawStyle.LINES
self.minor_style.style = coin.SoDrawStyle.LINES'''
# Minor Contour root.
minor_contours = coin.SoSeparator()
'''minor_contours = coin.SoSeparator()
minor_contours.addChild(self.minor_color)
minor_contours.addChild(self.minor_style)
minor_contours.addChild(self.minor_coords)
minor_contours.addChild(self.minor_lines)
minor_contours.addChild(self.minor_lines)'''
# Highlight for selection.
highlight = coin.SoType.fromName('SoFCSelection').createInstance()
@@ -673,7 +596,7 @@ class ViewProviderTerrain:
highlight.addChild(mat_binding)
highlight.addChild(self.geo_coords)
highlight.addChild(self.triangles)
highlight.addChild(boundaries)
#highlight.addChild(boundaries)
# Face root.
face = coin.SoSeparator()
@@ -686,19 +609,19 @@ class ViewProviderTerrain:
edge.addChild(self.edge_style)
edge.addChild(highlight)
# Surface root - con contour lines visibles.
# Surface root.
surface_root = coin.SoSeparator()
surface_root.addChild(face)
surface_root.addChild(offset)
surface_root.addChild(edge)
surface_root.addChild(major_contours)
surface_root.addChild(minor_contours)
#surface_root.addChild(major_contours)
#surface_root.addChild(minor_contours)
vobj.addDisplayMode(surface_root, "Surface")
# Boundary root.
boundary_root = coin.SoSeparator()
boundary_root.addChild(boundaries)
vobj.addDisplayMode(boundary_root, "Boundary")
#boundary_root = coin.SoSeparator()
#boundary_root.addChild(boundaries)
#vobj.addDisplayMode(boundary_root, "Boundary")
# Elevation/Shaded root.
'''shaded_root = coin.SoSeparator()
@@ -725,50 +648,52 @@ class ViewProviderTerrain:
self.onChanged(vobj, "ShapeColor")
self.onChanged(vobj, "LineColor")
self.onChanged(vobj, "LineWidth")
self.onChanged(vobj, "BoundaryColor")
self.onChanged(vobj, "BoundaryWidth")
self.onChanged(vobj, "BoundaryPattern")
self.onChanged(vobj, "PatternScale")
self.onChanged(vobj, "MajorColor")
self.onChanged(vobj, "MajorWidth")
self.onChanged(vobj, "MinorColor")
self.onChanged(vobj, "MinorWidth")
#self.onChanged(vobj, "BoundaryColor")
#self.onChanged(vobj, "BoundaryWidth")
#self.onChanged(vobj, "BoundaryPattern")
#self.onChanged(vobj, "PatternScale")
#self.onChanged(vobj, "MajorColor")
#self.onChanged(vobj, "MajorWidth")
#self.onChanged(vobj, "MinorColor")
#self.onChanged(vobj, "MinorWidth")
def updateData(self, obj, prop):
''' Update Object visuals when a data property changed. '''
# Set geosystem.
try:
utm_zone = FreeCAD.ActiveDocument.Site.UtmZone
except:
utm_zone = "30"
geo_system = ["UTM", utm_zone, "FLAT"]
geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
self.geo_coords.geoSystem.setValues(geo_system)
'''
self.boundary_coords.geoSystem.setValues(geo_system)
self.major_coords.geoSystem.setValues(geo_system)
self.minor_coords.geoSystem.setValues(geo_system)
'''
if prop == "mesh" or prop == "Mesh":
if prop == "Mesh":
if obj.mesh:
mesh = obj.mesh
try:
vertices = [tuple(v) for v in mesh.Topology[0]]
faces = []
for face in mesh.Topology[1]:
faces.extend(face)
faces.append(-1)
print("Mostrar mesh")
# Asignar a los nodos de visualización
self.geo_coords.point.values = vertices
self.triangles.coordIndex.values = faces
except Exception as e:
FreeCAD.Console.PrintError(f"Error actualizando mesh visual: {e}\n")
mesh = obj.mesh
vertices = [tuple(v) for v in mesh.Topology[0]]
faces = []
for face in mesh.Topology[1]:
faces.extend(face)
faces.append(-1)
# Asignar a los nodos de visualización
self.geo_coords.point.values = vertices # <-- ¡Clave!
self.triangles.coordIndex.values = faces # <-- ¡Clave!
def getDisplayModes(self, vobj):
''' Return a list of display modes. '''
return ["Surface", "Boundary", "Flat Lines", "Wireframe"]
modes = ["Surface", "Boundary"]
return modes
def getDefaultDisplayMode(self):
'''
Return the name of the default display mode.
'''
return "Surface"
def claimChildren(self):
@@ -811,4 +736,3 @@ class ViewProviderTerrain:
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Terrain', _CommandTerrain())'''
+30 -10
View File
@@ -450,18 +450,35 @@ class ContourTaskPanel():
starttime = datetime.now()
if self.land is None:
FreeCAD.Console.PrintWarning("No hay objetos para procesar\n")
print("No hay objetos para procesar")
return False
else:
minor = FreeCAD.Units.Quantity(self.inputMinorContourMargin.currentText()).Value
mayor = FreeCAD.Units.Quantity(self.inputMayorContourMargin.currentText()).Value
makeContours(
self.land, minor, mayor,
self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(),
self.inputMayorContourThickness.value()
)
i = 2
if i == 0:
makeContours(self.land, minor, mayor, self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(), self.inputMayorContourThickness.value())
elif i == 1:
import multiprocessing
p = multiprocessing.Process(target=makeContours,
args=(self.land, minor, mayor,
self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(),
self.inputMayorContourThickness.value(), ))
p.start()
p.join()
else:
import threading
hilo = threading.Thread(target = makeContours,
args = (self.land, minor, mayor,
self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(),
self.inputMayorContourThickness.value()))
hilo.daemon = True
hilo.start()
total_time = datetime.now() - starttime
print(" -- Tiempo tardado:", total_time)
@@ -552,7 +569,7 @@ class SlopeTaskPanel(_generalTaskPanel):
land.ViewObject.DiffuseColor = colorlist
# TODO: check this code:
elif hasattr(land, 'Mesh') and land.isDerivedFrom("Mesh::Feature"):
elif obj.isDerivedFrom("Mesh::Feature"):
fMesh = Mest2FemMesh(land)
import math
setColors = []
@@ -585,7 +602,10 @@ class SlopeTaskPanel(_generalTaskPanel):
print("Everything OK (", datetime.now() - starttime, ")")
def accept(self):
self.getPointSlope(self.ranges)
# self.getPointSlope()
import threading
hilo = threading.Thread(target=self.getPointSlope(self.ranges))
hilo.start()
return True
# Orientation Analisys: ---------------------------------------------------------------------------------
@@ -789,4 +809,4 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('SlopeAnalisys', _CommandSlopeAnalisys())
FreeCADGui.addCommand('HeightAnalisys', _CommandHeightAnalisys())
FreeCADGui.addCommand('OrientationAnalisys', _CommandOrientationAnalisys())
FreeCADGui.addCommand('TerrainAnalisys', CommandTerrainAnalisysGroup())'''
FreeCADGui.addCommand('TerrainAnalisys', CommandTerrainAnalisysGroup())'''
+1 -1
View File
@@ -159,7 +159,7 @@ def calculate_incenter(facet):
"""Calcula el incentro usando la función nativa de FreeCAD"""
try:
return facet.InCircle[0] # (x, y, z)
except (IndexError, AttributeError):
except:
return None
+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 -2
View File
@@ -2,7 +2,6 @@ numpy~=1.26.2
opencv-python~=4.8.1
matplotlib~=3.8.2
openpyxl~=3.1.2
utm~=0.7.0
PySide2~=5.15.8
requests~=2.31.0
setuptools~=68.2.2
@@ -17,4 +16,4 @@ certifi~=2023.11.17
SciPy~=1.11.4
pycollada~=0.7.2
shapely
rtree
rtree