diff --git a/PVPlant/core/georef.py b/PVPlant/core/georef.py new file mode 100644 index 0000000..6f63223 --- /dev/null +++ b/PVPlant/core/georef.py @@ -0,0 +1,422 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * This program is free software; you can redistribute it and/or modify* +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307* +# * USA * +# * * +# *********************************************************************** + +import FreeCAD + +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 \ No newline at end of file diff --git a/PVPlant/import_grid/__init__.py b/PVPlant/import_grid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PVPlant/import_grid/grid.py b/PVPlant/import_grid/grid.py new file mode 100644 index 0000000..b8b741d --- /dev/null +++ b/PVPlant/import_grid/grid.py @@ -0,0 +1,671 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * This program is free software; you can redistribute it and/or modify* +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307* +# * USA * +# * * +# *********************************************************************** + +import 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()) + diff --git a/PVPlant/placement/__init__.py b/PVPlant/placement/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PVPlant/placement/placement.py b/PVPlant/placement/placement.py new file mode 100644 index 0000000..f130245 --- /dev/null +++ b/PVPlant/placement/placement.py @@ -0,0 +1,2330 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * This program is free software; you can redistribute it and/or modify* +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307* +# * USA * +# * * +# *********************************************************************** + +import FreeCAD +import Part + +if FreeCAD.GuiUp: + import FreeCADGui, os + from PySide import QtCore, QtGui + from PySide.QtGui import QListWidgetItem + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt, txt): + return txt + + + def QT_TRANSLATE_NOOP(ctxt, txt): + return txt + # \endcond + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +import PVPlantResources +import PVPlantSite + +version = "0.1.0" + + +def selectionFilter(sel, objtype): + fil = [] + for obj in sel: + if hasattr(obj, "Proxy"): + if obj.Proxy.__class__ is objtype: + fil.append(obj) + return fil + + +class _PVPlantPlacementTaskPanel_old: + '''The editmode TaskPanel for Schedules''' + + def __init__(self, obj=None): + self.site = PVPlantSite.get() + self.Terrain = self.site.Terrain + self.FrameSetups = None + self.PVArea = None + self.Area = None + self.gap_col = .0 + self.gap_row = .0 + self.offsetX = .0 + self.offsetY = .0 + self.Dir = FreeCAD.Vector(0, -1, 0) # Norte a sur + + # self.form: + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) + self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) + + self.addFrames() + self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) + + self.form.buttonPVArea.clicked.connect(self.addPVArea) + self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) + self.update_inner_spacing() + + def addPVArea(self): + sel = FreeCADGui.Selection.getSelection() + if len(sel) > 0: + self.PVArea = sel[0] + self.form.editPVArea.setText(self.PVArea.Label) + + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) + + def update_inner_spacing(self): + self.form.editInnerSpacing.setText( + ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) + + def createFrameFromPoints(self, dataframe): + from Mechanical.Frame import PVPlantFrame + '''try: + MechanicalGroup = FreeCAD.ActiveDocument.Frames + except: + MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames') + MechanicalGroup.Label = "Frames" + FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) + + if self.form.cbSubfolders.isChecked: + name = "Frames-" + self.PVArea.Label + if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]: + MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0] + else: + group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) + group.Label = name + MechanicalGroup.addObject(group) + MechanicalGroup = group''' + + doc = FreeCAD.ActiveDocument + + # 1. Obtener o crear el grupo principal 'Frames' + main_group_name = "Frames" + main_group = doc.getObject(main_group_name) + if not main_group: + main_group = doc.addObject("App::DocumentObjectGroup", main_group_name) + main_group.Label = main_group_name + # Asumiendo que existe un grupo 'MechanicalGroup' + if hasattr(doc, 'MechanicalGroup'): + doc.MechanicalGroup.addObject(main_group) + + # 2. Manejar subgrupo si es necesario + group = main_group # Grupo donde se añadirán los marcos + if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis! + subgroup_name = f"Frames-{self.PVArea.Label}" + + # Buscar subgrupo existente + subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) + + if not subgroup: + subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) + subgroup.Label = subgroup_name + main_group.addObject(subgroup) + group = subgroup + + try: + placements = dataframe["placement"].tolist() + types = dataframe["type"].tolist() + frames = [] + for idx in range(len(placements)): + newrack = PVPlantFrame.makeTracker(setup=types[idx]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placements[idx] + group.addObject(newrack) + frames.append(newrack) + except: + placements = dataframe[0] + frames = [] + for idx in placements: + print(idx) + newrack = PVPlantFrame.makeTracker(setup=idx[0]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = idx[1] + groupq.addObject(newrack) + frames.append(newrack) + + if self.PVArea.Name.startswith("FrameArea"): + self.PVArea.Frames = frames + + def getProjected(self, shape): + """ returns projected edges from a shape and a direction """ + if shape.BoundBox.ZLength == 0: + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) + + def calculateWorkingArea(self): + self.Area = self.getProjected(self.PVArea.Shape) + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: + face = self.getProjected(obj.Shape.SubShapes[1]) + if face.isValid(): + prohibited_faces.append(face) + self.Area = self.Area.cut(prohibited_faces) + + def getAligments(self): + # TODO: revisar todo esto: ----------------------------------------------------------------- + sel = FreeCADGui.Selection.getSelectionEx()[0] + refh = None + refv = None + + if len(sel.SubObjects) == 0: + return + + elif len(sel.SubObjects) == 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + refh = refv = sel.SubObjects[0] + + elif len(sel.SubObjects) > 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: + refh = sel.SubObjects[0] + else: + refh = sel.SubObjects[1] + + if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: + refv = sel.SubObjects[0] + else: + refv = sel.SubObjects[1] + + steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col) + startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps) + steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row) + starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps) + # todo end ---------------------------------------------------------------------------------- + + return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ + np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) + + def adjustToTerrain_old(self, coordinates): + mode = 1 + terrain = self.Terrain.Mesh + + def placeRegion(df): # TODO: new + import MeshPart as mp + from scipy import stats + linregression = [] + for colnum in df.column.unique().tolist(): + dftmp = df[df["column"] == colnum] + for id in dftmp["ID"].tolist(): + data = df.loc[df['ID'] == id] + frametype = data["type"].tolist()[0] + # col = data["column"] + # row = data["row"] + base = data["placement"].tolist()[0] + + yl = frametype.Length.Value / 2 + ptop = FreeCAD.Vector(base) + ptop.y += yl + pbot = FreeCAD.Vector(base) + pbot.y -= yl + line = Part.LineSegment(ptop, pbot).toShape() + profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] + '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo. + tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) + profilepoints = [ver.Point for ver in tmp.Vertexes]''' + + xx = list() + yy = list() + zz = list() + for pts in profilepoints: + xx.append(pts.x) + yy.append(pts.y) + zz.append(pts.z) + slope, intercept, r, p, std_err = stats.linregress(yy, zz) + + # linregression.append(slope, intercept, r, p, std_err) + def myfunc(x): + return slope * x + intercept + + newzz = list(map(myfunc, [yy[0], yy[-1]])) + points3D = list() + points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) + points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) + linregression.append(points3D) + + # for ind in range(0, len(points3D) - 1): + pl = FreeCAD.Placement() + pl.Base = (points3D[0] + points3D[1]) / 2 + rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) + pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) + df.at[id - 1, "placement"] = pl + df["regression"] = linregression + + # 01. Grouping: + from scipy.ndimage import label as sclabel + import pandas as pd + tmp = [] + for c, col in enumerate(coordinates): + tmpcol = [] + for n, obj in enumerate(col): + if obj != 0: + tmpcol.append(1) + else: + tmpcol.append(0) + tmp.append(tmpcol) + + data = {"ID": [], + "region": [], + "type": [], + "column": [], + "row": [], + "placement": []} + + arr = np.array(tmp) + labeled_array, num_features = sclabel(arr) + id = 1 + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + unique, counts = np.unique(cols, return_counts=True) + result = np.column_stack((unique, counts)) + cnt = 0 + for val, count in result: + for c in range(count): + data["ID"].append(id) + data["region"].append(label) + data["type"].append(coordinates[val][rows[cnt]][0]) + data["column"].append(val) + data["row"].append(rows[cnt]) + data["placement"].append(coordinates[val][rows[cnt]][1]) + cnt += 1 + id += 1 + df = pd.DataFrame(data) + placeRegion(df) + return df + + def _setup_terrain_interpolator(self): + """Prepara interpolador del terreno para ajuste rápido""" + import numpy as np + from scipy.interpolate import LinearNDInterpolator + + mesh = self.Terrain.Mesh + points = np.array([p.Vector for p in mesh.Points]) + bbox = self.Area.BoundBox + + # Filtrar puntos dentro del área de trabajo + in_bbox = [ + p for p in points + if bbox.XMin <= p[0] <= bbox.XMax and + bbox.YMin <= p[1] <= bbox.YMax + ] + + if not in_bbox: + return None + + coords = np.array(in_bbox) + return LinearNDInterpolator(coords[:, :2], coords[:, 2]) + + def adjustToTerrain(self, coordinates): + from scipy.ndimage import label as sclabel + import pandas as pd + import numpy as np + from scipy import stats + import MeshPart + + # Crear matriz binaria + arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) + labeled_array, num_features = sclabel(arr) + + # Construir DataFrame optimizado + data = [] + terrain_interp = self._setup_terrain_interpolator() + + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for idx, (col, row) in enumerate(zip(cols, rows)): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + df = pd.DataFrame(data) + + # Ajustar al terreno + for idx, row in df.iterrows(): + pl = row['placement'] + yl = row['type'].Length.Value / 2 + + # Calcular puntos extremos + top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) + bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) + + # Usar interpolador si está disponible + if terrain_interp: + yy = np.linspace(bot_point.y, top_point.y, 10) + xx = np.full(10, pl.x) + zz = terrain_interp(xx, yy) + + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + else: + # Fallback a proyección directa + line = Part.LineSegment(bot_point, top_point).toShape() + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + + # Actualizar placement + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), + new_top - new_bot + ) + df.at[idx, 'placement'] = new_pl + + return df + + def isInside(self, frame, point): + if self.Area.isInside(point, 10, True): + frame.Placement.Base = point + cut = frame.cut([self.Area]) + if len(cut.Vertexes) == 0: + return True + return False + + def calculateAlignedArray(self): + import FreeCAD + pointsx, pointsy = self.getAligments() + + footprints = [] + for frame in self.FrameSetups: + xx = frame.Length.Value + yy = frame.Width.Value + xx_med = xx / 2 + yy_med = yy / 2 + rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0), + FreeCAD.Vector(xx_med, -yy_med, 0), + FreeCAD.Vector(xx_med, yy_med, 0), + FreeCAD.Vector(-xx_med, yy_med, 0), + FreeCAD.Vector(-xx_med, -yy_med, 0)]) + rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + footprints.append([frame, rec]) + ref = footprints.pop(0) + xx = ref[0].Length.Value + yy = ref[0].Width.Value + xx_med = xx / 2 + yy_med = yy / 2 + + # variables for corridors: + countcols = 0 + countrows = 0 + offsetcols = 0 # ?? + offsetrows = 0 # ?? + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) + + cols = [] + for x in pointsx: + col = [] + for y in pointsy: + found = False + point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0) + if self.isInside(ref[1], point): + col.append([ref[0], point]) + found = True + continue + else: + for footprint in footprints: + l = int((ref[0].Length - footprint[0].Length) / 2) + for i in range(2): + point1 = FreeCAD.Vector(point) + point1.y = point1.y + l + if self.isInside(footprint[1], point1): + col.append([footprint[0], point1]) + found = True + break + l = -l + if found: + break + if not found: + col.append(0) + cols.append(col) + + # if len(col) > 0: + # code for vertical corridors: + if self.form.groupCorridor.isChecked(): + if self.form.editColCount.value() > 0: + countcols += 1 + if countcols == self.form.editColCount.value(): + offsetcols += valcols + countcols = 0 + + return self.adjustToTerrain(cols) + + def calculateNonAlignedArray(self): + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0: + FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n") + return [] + + footprints = [] + for frame in self.FrameSetups: + l = frame.Length.Value + w = frame.Width.Value + l_med = l / 2 + w_med = w / 2 + rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0), + FreeCAD.Vector( l_med, -w_med, 0), + FreeCAD.Vector( l_med, w_med, 0), + FreeCAD.Vector(-l_med, w_med, 0), + FreeCAD.Vector(-l_med, -w_med, 0)]) + rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + footprints.append([frame, rec]) + + corridor = self.form.groupCorridor.isChecked() + corridor_offset = 0 + count = 0 + + cols = [] + for x in pointsx: + col=[] + x += corridor_offset + p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0) + line = Part.makePolygon([p1, p2]) + inter = self.Area.section([line]) + pts = [ver.Point for ver in inter.Vertexes] + pts = sorted(pts, key=lambda p: p.y, reverse=True) + for i in range(0, len(pts), 2): + top = pts[i] + bootom = pts[i + 1] + if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength: + y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2) + cp = footprints[-1][1].copy() + cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0) + inter = cp.cut([self.Area]) + vtx = [ver.Point for ver in inter.Vertexes] + mod = top.y + if len(vtx) != 0: + mod = min(vtx, key=lambda p: p.y).y + #y1 = cp.Placement.Base.y - mod + + tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy') + for opt in tmp[0]: + mod -= (footprints[opt][1].BoundBox.YLength / 2) + pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0) + cp = footprints[opt][1].copy() + if self.isInside(cp, pl): + col.append([footprints[opt][0], pl]) + mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500) + Part.show(cp) + + if corridor and len(col) > 0: + count += 1 + if count == self.form.editColCount.value(): + corridor_offset += 12000 + count = 0 + + cols.append(cols) + return self.adjustToTerrain(cols) + + def accept(self): + from datetime import datetime + starttime = datetime.now() + + params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") + auto_save_enabled = params.GetBool("AutoSaveEnabled") + params.SetBool("AutoSaveEnabled", False) + FreeCAD.ActiveDocument.RecomputesFrozen = True + + items = [ + FreeCAD.ActiveDocument.getObject(item.text()) + for i in range(self.form.listFrameSetups.count()) + if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked + ] + + unique_frames = {frame.Length.Value: frame for frame in items} + self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) + + self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value + self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value + self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + + FreeCAD.ActiveDocument.openTransaction("Create Placement") + # 1. Calculate working area: + self.calculateWorkingArea() + # 2. Calculate aligned array: + if self.form.cbAlignFrames.isChecked(): + dataframe = self.calculateAlignedArray() + else: + dataframe = self.calculateNonAlignedArray() + # 3. Adjust to terrain: + self.createFrameFromPoints(dataframe) + + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) + + + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.RecomputesFrozen = False + params.SetBool("AutoSaveEnabled", auto_save_enabled) + + total_time = datetime.now() - starttime + print(" -- Tiempo tardado:", total_time) + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + +class _PVPlantPlacementTaskPanel_new1: + '''The editmode TaskPanel for Schedules''' + + def __init__(self, obj=None): + self.site = PVPlantSite.get() + self.Terrain = self.site.Terrain + self.FrameSetups = None + self.PVArea = None + self.Area = None + self.gap_col = .0 + self.gap_row = .0 + self.offsetX = .0 + self.offsetY = .0 + self.Dir = FreeCAD.Vector(0, -1, 0) # Norte a sur + + # self.form: + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) + self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) + + self.addFrames() + self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) + + self.form.buttonPVArea.clicked.connect(self.addPVArea) + self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) + self.update_inner_spacing() + + def addPVArea(self): + sel = FreeCADGui.Selection.getSelection() + if len(sel) > 0: + self.PVArea = sel[0] + self.form.editPVArea.setText(self.PVArea.Label) + + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) + + def update_inner_spacing(self): + self.form.editInnerSpacing.setText( + ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) + + def createFrameFromPoints(self, dataframe): + from Mechanical.Frame import PVPlantFrame + doc = FreeCAD.ActiveDocument + + # 1. Obtener o crear el grupo principal 'Frames' + main_group_name = "Frames" + main_group = doc.getObject(main_group_name) + if not main_group: + main_group = doc.addObject("App::DocumentObjectGroup", main_group_name) + main_group.Label = main_group_name + # Asumiendo que existe un grupo 'MechanicalGroup' + if hasattr(doc, 'MechanicalGroup'): + doc.MechanicalGroup.addObject(main_group) + + # 2. Manejar subgrupo si es necesario + group = main_group # Grupo donde se añadirán los marcos + if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis! + subgroup_name = f"Frames-{self.PVArea.Label}" + + # Buscar subgrupo existente + subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) + + if not subgroup: + subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) + subgroup.Label = subgroup_name + main_group.addObject(subgroup) + group = subgroup + + try: + placements = dataframe["placement"].tolist() + types = dataframe["type"].tolist() + frames = [] + for idx in range(len(placements)): + newrack = PVPlantFrame.makeTracker(setup=types[idx]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placements[idx] + group.addObject(newrack) + frames.append(newrack) + except: + placements = dataframe[0] + frames = [] + for idx in placements: + print(idx) + newrack = PVPlantFrame.makeTracker(setup=idx[0]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = idx[1] + groupq.addObject(newrack) + frames.append(newrack) + + if self.PVArea.Name.startswith("FrameArea"): + self.PVArea.Frames = frames + + def getProjected(self, shape): + """ returns projected edges from a shape and a direction """ + if shape.BoundBox.ZLength == 0: + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) + + def calculateWorkingArea(self): + self.Area = self.getProjected(self.PVArea.Shape) + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: + face = self.getProjected(obj.Shape.SubShapes[1]) + if face.isValid(): + prohibited_faces.append(face) + self.Area = self.Area.cut(prohibited_faces) + + def getAligments(self): + # TODO: revisar todo esto: ----------------------------------------------------------------- + sel = FreeCADGui.Selection.getSelectionEx()[0] + refh = None + refv = None + + if len(sel.SubObjects) == 0: + return + + elif len(sel.SubObjects) == 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + refh = refv = sel.SubObjects[0] + + elif len(sel.SubObjects) > 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: + refh = sel.SubObjects[0] + else: + refh = sel.SubObjects[1] + + if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: + refv = sel.SubObjects[0] + else: + refv = sel.SubObjects[1] + + steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col) + startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps) + steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row) + starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps) + # todo end ---------------------------------------------------------------------------------- + + return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ + np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) + + def adjustToTerrain_old(self, coordinates): + mode = 1 + terrain = self.Terrain.Mesh + + def placeRegion(df): # TODO: new + import MeshPart as mp + from scipy import stats + linregression = [] + for colnum in df.column.unique().tolist(): + dftmp = df[df["column"] == colnum] + for id in dftmp["ID"].tolist(): + data = df.loc[df['ID'] == id] + frametype = data["type"].tolist()[0] + # col = data["column"] + # row = data["row"] + base = data["placement"].tolist()[0] + + yl = frametype.Length.Value / 2 + ptop = FreeCAD.Vector(base) + ptop.y += yl + pbot = FreeCAD.Vector(base) + pbot.y -= yl + line = Part.LineSegment(ptop, pbot).toShape() + profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] + '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo. + tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) + profilepoints = [ver.Point for ver in tmp.Vertexes]''' + + xx = list() + yy = list() + zz = list() + for pts in profilepoints: + xx.append(pts.x) + yy.append(pts.y) + zz.append(pts.z) + slope, intercept, r, p, std_err = stats.linregress(yy, zz) + + # linregression.append(slope, intercept, r, p, std_err) + def myfunc(x): + return slope * x + intercept + + newzz = list(map(myfunc, [yy[0], yy[-1]])) + points3D = list() + points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) + points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) + linregression.append(points3D) + + # for ind in range(0, len(points3D) - 1): + pl = FreeCAD.Placement() + pl.Base = (points3D[0] + points3D[1]) / 2 + rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) + pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) + df.at[id - 1, "placement"] = pl + df["regression"] = linregression + + # 01. Grouping: + from scipy.ndimage import label as sclabel + import pandas as pd + tmp = [] + for c, col in enumerate(coordinates): + tmpcol = [] + for n, obj in enumerate(col): + if obj != 0: + tmpcol.append(1) + else: + tmpcol.append(0) + tmp.append(tmpcol) + + data = {"ID": [], + "region": [], + "type": [], + "column": [], + "row": [], + "placement": []} + + arr = np.array(tmp) + labeled_array, num_features = sclabel(arr) + id = 1 + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + unique, counts = np.unique(cols, return_counts=True) + result = np.column_stack((unique, counts)) + cnt = 0 + for val, count in result: + for c in range(count): + data["ID"].append(id) + data["region"].append(label) + data["type"].append(coordinates[val][rows[cnt]][0]) + data["column"].append(val) + data["row"].append(rows[cnt]) + data["placement"].append(coordinates[val][rows[cnt]][1]) + cnt += 1 + id += 1 + df = pd.DataFrame(data) + placeRegion(df) + return df + + def _setup_terrain_interpolator(self): + """Prepara interpolador del terreno para ajuste rápido""" + import numpy as np + from scipy.interpolate import LinearNDInterpolator + + mesh = self.Terrain.Mesh + points = np.array([p.Vector for p in mesh.Points]) + bbox = self.Area.BoundBox + + # Filtrar puntos dentro del área de trabajo + in_bbox = [ + p for p in points + if bbox.XMin <= p[0] <= bbox.XMax and + bbox.YMin <= p[1] <= bbox.YMax + ] + + if not in_bbox: + return None + + coords = np.array(in_bbox) + return LinearNDInterpolator(coords[:, :2], coords[:, 2]) + + def adjustToTerrain(self, coordinates): + from scipy.ndimage import label as sclabel + import pandas as pd + import numpy as np + from scipy import stats + import MeshPart + + # Crear matriz binaria + arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) + labeled_array, num_features = sclabel(arr) + + # Construir DataFrame optimizado + data = [] + terrain_interp = self._setup_terrain_interpolator() + + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for idx, (col, row) in enumerate(zip(cols, rows)): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + df = pd.DataFrame(data) + + # Ajustar al terreno + for idx, row in df.iterrows(): + pl = row['placement'] + yl = row['type'].Length.Value / 2 + + # Calcular puntos extremos + top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) + bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) + + # Usar interpolador si está disponible + if terrain_interp: + yy = np.linspace(bot_point.y, top_point.y, 10) + xx = np.full(10, pl.x) + zz = terrain_interp(xx, yy) + + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + else: + # Fallback a proyección directa + line = Part.LineSegment(bot_point, top_point).toShape() + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + + # Actualizar placement + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), + new_top - new_bot + ) + df.at[idx, 'placement'] = new_pl + + return df + + def isInside(self, frame, point): + if self.Area.isInside(point, 10, True): + frame.Placement.Base = point + cut = frame.cut([self.Area]) + if len(cut.Vertexes) == 0: + return True + return False + + def calculateAlignedArray(self): + import FreeCAD + pointsx, pointsy = self.getAligments() + + footprints = [] + for frame in self.FrameSetups: + xx = frame.Length.Value + yy = frame.Width.Value + xx_med = xx / 2 + yy_med = yy / 2 + rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0), + FreeCAD.Vector(xx_med, -yy_med, 0), + FreeCAD.Vector(xx_med, yy_med, 0), + FreeCAD.Vector(-xx_med, yy_med, 0), + FreeCAD.Vector(-xx_med, -yy_med, 0)]) + rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + footprints.append([frame, rec]) + ref = footprints.pop(0) + xx = ref[0].Length.Value + yy = ref[0].Width.Value + xx_med = xx / 2 + yy_med = yy / 2 + + # variables for corridors: + countcols = 0 + countrows = 0 + offsetcols = 0 # ?? + offsetrows = 0 # ?? + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) + + cols = [] + for x in pointsx: + col = [] + for y in pointsy: + found = False + point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0) + if self.isInside(ref[1], point): + col.append([ref[0], point]) + found = True + continue + else: + for footprint in footprints: + l = int((ref[0].Length - footprint[0].Length) / 2) + for i in range(2): + point1 = FreeCAD.Vector(point) + point1.y = point1.y + l + if self.isInside(footprint[1], point1): + col.append([footprint[0], point1]) + found = True + break + l = -l + if found: + break + if not found: + col.append(0) + cols.append(col) + + # if len(col) > 0: + # code for vertical corridors: + if self.form.groupCorridor.isChecked(): + if self.form.editColCount.value() > 0: + countcols += 1 + if countcols == self.form.editColCount.value(): + offsetcols += valcols + countcols = 0 + + return self.adjustToTerrain(cols) + + def calculateNonAlignedArray(self): + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0: + FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n") + return [] + + footprints = [] + for frame in self.FrameSetups: + l = frame.Length.Value + w = frame.Width.Value + l_med = l / 2 + w_med = w / 2 + rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0), + FreeCAD.Vector( l_med, -w_med, 0), + FreeCAD.Vector( l_med, w_med, 0), + FreeCAD.Vector(-l_med, w_med, 0), + FreeCAD.Vector(-l_med, -w_med, 0)]) + rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + footprints.append([frame, rec]) + + corridor = self.form.groupCorridor.isChecked() + corridor_offset = 0 + count = 0 + + cols = [] + for x in pointsx: + col=[] + x += corridor_offset + p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0) + line = Part.makePolygon([p1, p2]) + inter = self.Area.section([line]) + pts = [ver.Point for ver in inter.Vertexes] + pts = sorted(pts, key=lambda p: p.y, reverse=True) + for i in range(0, len(pts), 2): + top = pts[i] + bootom = pts[i + 1] + if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength: + y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2) + cp = footprints[-1][1].copy() + cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0) + inter = cp.cut([self.Area]) + vtx = [ver.Point for ver in inter.Vertexes] + mod = top.y + if len(vtx) != 0: + mod = min(vtx, key=lambda p: p.y).y + #y1 = cp.Placement.Base.y - mod + + tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy') + for opt in tmp[0]: + mod -= (footprints[opt][1].BoundBox.YLength / 2) + pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0) + cp = footprints[opt][1].copy() + if self.isInside(cp, pl): + col.append([footprints[opt][0], pl]) + mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500) + Part.show(cp) + + if corridor and len(col) > 0: + count += 1 + if count == self.form.editColCount.value(): + corridor_offset += 12000 + count = 0 + + cols.append(col) + return self.adjustToTerrain(cols) + + def accept(self): + from datetime import datetime + starttime = datetime.now() + + params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") + auto_save_enabled = params.GetBool("AutoSaveEnabled") + params.SetBool("AutoSaveEnabled", False) + FreeCAD.ActiveDocument.RecomputesFrozen = True + + items = [ + FreeCAD.ActiveDocument.getObject(item.text()) + for i in range(self.form.listFrameSetups.count()) + if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked + ] + + unique_frames = {frame.Length.Value: frame for frame in items} + self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) + + self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value + self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value + self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + + FreeCAD.ActiveDocument.openTransaction("Create Placement") + # 1. Calculate working area: + self.calculateWorkingArea() + # 2. Calculate aligned array: + if self.form.cbAlignFrames.isChecked(): + dataframe = self.calculateAlignedArray() + else: + dataframe = self.calculateNonAlignedArray() + # 3. Adjust to terrain: + self.createFrameFromPoints(dataframe) + + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) + + + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.RecomputesFrozen = False + params.SetBool("AutoSaveEnabled", auto_save_enabled) + + total_time = datetime.now() - starttime + print(" -- Tiempo tardado:", total_time) + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + +import numpy as np +import pandas as pd +from scipy.ndimage import label as sclabel +from scipy import stats +from scipy.interpolate import LinearNDInterpolator +import Part +import FreeCAD +import FreeCADGui +from PySide2 import QtCore, QtGui +from PySide2.QtWidgets import QListWidgetItem +import os +import PVPlantResources + + +class _PVPlantPlacementTaskPanel: + '''The editmode TaskPanel for Schedules''' + + def __init__(self, obj=None): + self.site = PVPlantSite.get() + self.Terrain = self.site.Terrain + self.FrameSetups = None + self.PVArea = None + self.Area = None + self.gap_col = .0 + self.gap_row = .0 + self.offsetX = .0 + self.offsetY = .0 + self.Dir = FreeCAD.Vector(0, -1, 0) + self._terrain_interpolator = None + self._frame_footprints_cache = {} + + # UI setup + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) + self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) + + self.addFrames() + self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) + + self.form.buttonPVArea.clicked.connect(self.addPVArea) + self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) + self.update_inner_spacing() + + def addPVArea(self): + sel = FreeCADGui.Selection.getSelection() + if sel: + self.PVArea = sel[0] + self.form.editPVArea.setText(self.PVArea.Label) + + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) + + def update_inner_spacing(self): + self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m") + + def _get_or_create_frame_group(self): + """Optimized group creation and management""" + doc = FreeCAD.ActiveDocument + + # Get or create main group + main_group = doc.getObject("Frames") or doc.addObject("App::DocumentObjectGroup", "Frames") + if not main_group.Label == "Frames": + main_group.Label = "Frames" + + # Add to MechanicalGroup if exists + if not hasattr(doc, 'MechanicalGroup') and hasattr(doc, 'getObject') and doc.getObject('MechanicalGroup'): + doc.MechanicalGroup.addObject(main_group) + + # Handle subfolder + if self.form.cbSubfolders.isChecked() and self.PVArea: + subgroup_name = f"Frames-{self.PVArea.Label}" + subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) + if not subgroup: + subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) + subgroup.Label = subgroup_name + main_group.addObject(subgroup) + return subgroup + + return main_group + + def createFrameFromPoints(self, dataframe): + from Mechanical.Frame import PVPlantFrame + doc = FreeCAD.ActiveDocument + + group = self._get_or_create_frame_group() + + frames = [] + placements_key = "placement" if "placement" in dataframe.columns else 0 + + if placements_key == "placement": + placements = dataframe["placement"].tolist() + types = dataframe["type"].tolist() + + for idx, (placement, frame_type) in enumerate(zip(placements, types)): + newrack = PVPlantFrame.makeTracker(setup=frame_type) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placement + group.addObject(newrack) + frames.append(newrack) + + if self.PVArea and self.PVArea.Name.startswith("FrameArea"): + self.PVArea.Frames = frames + + def getProjected(self, shape): + """Optimized projection calculation""" + if shape.BoundBox.ZLength == 0: + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire and wire.isClosed() else Part.Face(wire) + + def calculateWorkingArea(self): + """Optimized working area calculation""" + self.Area = self.getProjected(self.PVArea.Shape) + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: + face = self.getProjected(obj.Shape.SubShapes[1]) + if face and face.isValid(): + prohibited_faces.append(face) + + if prohibited_faces: + self.Area = self.Area.cut(prohibited_faces) + + # Clear terrain interpolator cache when area changes + self._terrain_interpolator = None + + def _setup_terrain_interpolator(self): + """Cached terrain interpolator""" + if self._terrain_interpolator is not None: + return self._terrain_interpolator + + mesh = self.Terrain.Mesh + points = np.array([v.Vector for v in mesh.Points]) + bbox = self.Area.BoundBox + + # Filter points within working area efficiently + mask = ((points[:, 0] >= bbox.XMin) & (points[:, 0] <= bbox.XMax) & + (points[:, 1] >= bbox.YMin) & (points[:, 1] <= bbox.YMax)) + filtered_points = points[mask] + + if len(filtered_points) == 0: + self._terrain_interpolator = None + return None + + try: + self._terrain_interpolator = LinearNDInterpolator( + filtered_points[:, :2], filtered_points[:, 2] + ) + except: + self._terrain_interpolator = None + + return self._terrain_interpolator + + def _get_frame_footprint(self, frame): + """Cached footprint calculation""" + frame_key = (frame.Length.Value, frame.Width.Value) + if frame_key not in self._frame_footprints_cache: + l, w = frame.Length.Value, frame.Width.Value + l_med, w_med = l / 2, w / 2 + + footprint = Part.makePolygon([ + FreeCAD.Vector(-l_med, -w_med, 0), + FreeCAD.Vector(l_med, -w_med, 0), + FreeCAD.Vector(l_med, w_med, 0), + FreeCAD.Vector(-l_med, w_med, 0), + FreeCAD.Vector(-l_med, -w_med, 0) + ]) + footprint.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + + self._frame_footprints_cache[frame_key] = footprint + + return self._frame_footprints_cache[frame_key] + + def _calculate_terrain_adjustment_batch(self, points_data): + """Process terrain adjustments in batches for better performance""" + terrain_interp = self._setup_terrain_interpolator() + results = [] + + for frame_type, base_point in points_data: + yl = frame_type.Length.Value / 2 + top_point = FreeCAD.Vector(base_point.x, base_point.y + yl, 0) + bot_point = FreeCAD.Vector(base_point.x, base_point.y - yl, 0) + + if terrain_interp: + # Use interpolator for faster elevation calculation + yy = np.linspace(bot_point.y, top_point.y, 6) # Reduced points for speed + xx = np.full_like(yy, base_point.x) + try: + zz = terrain_interp(xx, yy) + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + except: + z_top = z_bot = 0 + else: + # Fallback to direct projection (slower) + line = Part.LineSegment(bot_point, top_point).toShape() + try: + import MeshPart + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + except: + z_top = z_bot = 0 + + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), new_top - new_bot + ) + + results.append((frame_type, new_pl)) + + return results + + def adjustToTerrain(self, coordinates): + """Unified terrain adjustment function for both aligned and non-aligned arrays""" + # Create binary array efficiently + arr = np.array([[int(obj != 0) for obj in col] for col in coordinates], dtype=np.uint8) + labeled_array, num_features = sclabel(arr) + + # Build DataFrame efficiently + data = [] + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for col, row in zip(cols, rows): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + if not data: + return pd.DataFrame(columns=['ID', 'region', 'type', 'column', 'row', 'placement']) + + df = pd.DataFrame(data) + + # Process terrain adjustments in batches + points_data = [(row['type'], row['placement']) for _, row in df.iterrows()] + adjusted_results = self._calculate_terrain_adjustment_batch(points_data) + + # Update placements in DataFrame + for idx, (frame_type, new_placement) in enumerate(adjusted_results): + df.at[idx, 'placement'] = new_placement + + return df + + def isInside(self, frame, point): + """Optimized inside check with early termination""" + if not self.Area.isInside(point, 1e-6, True): # Reduced tolerance for speed + return False + + frame_footprint = self._get_frame_footprint(frame) + frame_footprint.Placement.Base = point + + try: + cut = frame_footprint.cut([self.Area]) + return len(cut.Vertexes) == 0 + except: + return False + + def getAligments(self): + """Optimized alignment calculation""" + sel = FreeCADGui.Selection.getSelectionEx() + if not sel or not sel[0].SubObjects: + return np.array([]), np.array([]) + + sub_objects = sel[0].SubObjects + + if len(sub_objects) == 1: + refh = refv = sub_objects[0] + else: + # Choose references based on bounding box dimensions + refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength) + refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength) + + # Calculate ranges efficiently + startx = refv.BoundBox.XMin + self.offsetX - self.gap_col * int( + (refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col + ) + + starty = refh.BoundBox.YMin + self.offsetY + self.gap_row * int( + (refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row + ) + + x_range = np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.float64) + y_range = np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.float64) + + return x_range, y_range + + def calculateAlignedArray(self): + """Optimized aligned array calculation""" + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0 or len(pointsy) == 0: + return pd.DataFrame() + + # Precompute footprints once + footprints = [] + for frame in self.FrameSetups: + footprint = self._get_frame_footprint(frame) + footprints.append((frame, footprint)) + + ref_frame, ref_footprint = footprints[0] + ref_length = ref_frame.Length.Value + ref_width = ref_frame.Width.Value + + # Corridor variables + countcols = 0 + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - ref_width) + corridor_enabled = self.form.groupCorridor.isChecked() + + cols = [] + for x in pointsx: + col = [] + corridor_offset = 0 + + for y in pointsy: + point = FreeCAD.Vector(x + ref_width / 2 + corridor_offset, y - ref_length / 2, 0.0) + found = False + + # Check reference frame first (most common case) + if self.isInside(ref_frame, point): + col.append([ref_frame, point]) + found = True + else: + # Check alternative frames + for frame, footprint in footprints[1:]: + length_diff = (ref_frame.Length.Value - frame.Length.Value) / 2 + for offset in [length_diff, -length_diff]: + test_point = FreeCAD.Vector(point.x, point.y + offset, 0.0) + if self.isInside(frame, test_point): + col.append([frame, test_point]) + found = True + break + if found: + break + + if not found: + col.append(0) + + # Handle corridors + if corridor_enabled and col: + countcols += 1 + if countcols >= self.form.editColCount.value(): + corridor_offset += valcols + countcols = 0 + + cols.append(col) + + return self.adjustToTerrain(cols) + + def calculateNonAlignedArray(self): + """Optimized non-aligned array calculation""" + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0: + FreeCAD.Console.PrintWarning("No X alignments found.\n") + return pd.DataFrame() + + # Precompute footprints + footprints = [] + for frame in self.FrameSetups: + footprint = self._get_frame_footprint(frame) + footprints.append((frame, footprint)) + + corridor_enabled = self.form.groupCorridor.isChecked() + corridor_count = 0 + corridor_offset = 0 + + cols = [] + for x in pointsx: + col = [] + current_x = x + corridor_offset + + # Create vertical line for intersection + p1 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMin, 0.0) + line = Part.LineSegment(p1, p2).toShape() + + # Get intersections with area + try: + inter = self.Area.section(line) + pts = sorted([v.Point for v in inter.Vertexes], key=lambda p: p.y, reverse=True) + + for i in range(0, len(pts) - 1, 2): + top, bottom = pts[i], pts[i + 1] + available_height = top.y - bottom.y + + if available_height > footprints[-1][0].Width.Value: + # Use optimized placement algorithm + self._place_frames_in_segment(col, footprints, current_x, top, bottom) + + except Exception as e: + FreeCAD.Console.PrintWarning(f"Error in segment processing: {e}\n") + + # Handle corridor offset + if corridor_enabled and col: + corridor_count += 1 + if corridor_count >= self.form.editColCount.value(): + corridor_offset += 12000 # 12m corridor + corridor_count = 0 + + cols.append(col) + + return self.adjustToTerrain(cols) + + def _place_frames_in_segment(self, col, footprints, x, top, bottom): + """Optimized frame placement within a segment""" + current_y = top.y + frame_heights = [ftp[0].Width.Value for ftp in footprints] + min_frame_height = min(frame_heights) + + while current_y - bottom.y > min_frame_height: + placed = False + + for frame, footprint in footprints: + test_y = current_y - frame.Width.Value / 2 + test_point = FreeCAD.Vector(x, test_y, 0.0) + + if self.isInside(frame, test_point): + col.append([frame, test_point]) + current_y -= frame.Width.Value + placed = True + break + + if not placed: + break + + def accept(self): + from datetime import datetime + starttime = datetime.now() + + # Document optimization + params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") + auto_save_enabled = params.GetBool("AutoSaveEnabled") + params.SetBool("AutoSaveEnabled", False) + FreeCAD.ActiveDocument.RecomputesFrozen = True + + try: + # Get selected frames + items = [ + FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text()) + for i in range(self.form.listFrameSetups.count()) + if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked + ] + + # Remove duplicates efficiently + self.FrameSetups = list({frame.Length.Value: frame for frame in items}.values()) + self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True) + + # Parse parameters + self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value + self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value + self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + + FreeCAD.ActiveDocument.openTransaction("Create Placement") + + # Main processing + self.calculateWorkingArea() + + if self.form.cbAlignFrames.isChecked(): + dataframe = self.calculateAlignedArray() + else: + dataframe = self.calculateNonAlignedArray() + + if not dataframe.empty: + self.createFrameFromPoints(dataframe) + + # Group trackers + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row) + + FreeCAD.ActiveDocument.commitTransaction() + + finally: + # Restore document settings + FreeCAD.ActiveDocument.RecomputesFrozen = False + params.SetBool("AutoSaveEnabled", auto_save_enabled) + + total_time = datetime.now() - starttime + print(f" -- Total time: {total_time}") + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + +def optimized_cut(L_total, piezas, margen=0, metodo='auto'): + """ + Encuentra la combinación óptima de piezas para minimizar el desperdicio, + considerando un margen entre piezas. + + Args: + L_total (int): Longitud total del material. + piezas (list): Lista de longitudes de los patrones de corte. + margen (int): Espacio perdido entre piezas consecutivas. + metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática. + + Returns: + tuple: (piezas_seleccionadas, desperdicio) + """ + # Filtrar piezas inválidas + piezas = [p for p in piezas if 0 < p <= L_total] + if not piezas: + return [], L_total + + # Transformar longitudes y longitud total con margen + longitudes_aumentadas = [p + margen for p in piezas] + L_total_aumentado = L_total + margen + + # Selección automática de método + if metodo == 'auto': + if L_total_aumentado <= 10000 and len(piezas) <= 100: + metodo = 'dp' + else: + metodo = 'greedy' + + if metodo == 'dp': + n = len(piezas) + dp = [0] * (L_total_aumentado + 1) + parent = [-1] * (L_total_aumentado + 1) # Almacena índices de piezas usadas + + # Llenar la tabla dp y parent + for j in range(1, L_total_aumentado + 1): + for i in range(n): + p_aum = longitudes_aumentadas[i] + if p_aum <= j: + if dp[j] < dp[j - p_aum] + p_aum: + dp[j] = dp[j - p_aum] + p_aum + parent[j] = i # Guardar índice de la pieza + + # Reconstruir solución desde el final + current = L_total_aumentado + seleccion_indices = [] + while current > 0 and parent[current] != -1: + i = parent[current] + seleccion_indices.append(i) + current -= longitudes_aumentadas[i] + + # Calcular desperdicio real + k = len(seleccion_indices) + if k == 0: + desperdicio = L_total + else: + suma_original = sum(piezas[i] for i in seleccion_indices) + desperdicio = L_total - suma_original - margen * (k - 1) + + return seleccion_indices, desperdicio + + elif metodo == 'greedy': + # Crear lista con índices y longitudes aumentadas + lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))] + lista_con_indices.sort(key=lambda x: x[0], reverse=True) # Ordenar descendente + + seleccion_indices = [] + restante = L_total_aumentado + + # Seleccionar piezas vorazmente + for p_aum, i in lista_con_indices: + while restante >= p_aum: + seleccion_indices.append(i) + restante -= p_aum + + # Calcular desperdicio real + k = len(seleccion_indices) + if k == 0: + desperdicio = L_total + else: + suma_original = sum(piezas[i] for i in seleccion_indices) + desperdicio = L_total - suma_original - margen * (k - 1) + + return seleccion_indices, desperdicio + + +# Ejemplo de uso +'''if __name__ == "__main__": + L_total = 100 + piezas = [25, 35, 40, 20, 15, 30, 50] + margen = 5 + + print("Solución óptima con margen (programación dinámica):") + seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp') + print(f"Piezas usadas: {seleccion}") + print(f"Margen entre piezas: {margen} cm") + print(f"Material útil: {sum(seleccion)} cm") + print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm") + print(f"Desperdicio total: {desperd} cm") + + print("\nSolución aproximada con margen (algoritmo voraz):") + seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy') + print(f"Piezas usadas: {seleccion_g}") + print(f"Margen entre piezas: {margen} cm") + print(f"Material útil: {sum(seleccion_g)} cm") + print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm") + print(f"Desperdicio total: {desperd_g} cm")''' + + +# ---------------------------------------------------------------------------------------------------------------------- +# function AdjustToTerrain +# Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh +# +# Inputs: +# 1. frames: group of objest to adjust +# ---------------------------------------------------------------------------------------------------------------------- +class adjustToTerrainTaskPanel: + def __init__(self, obj=None): + self.obj = obj + self.form = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantPlacementAdjust.ui") + + def accept(self): + frames = [] + for obj in FreeCADGui.Selection.getSelection(): + if obj.Name.startswith("Tracker"): + frames.append(obj) + elif obj.Name.startswith("FrameArea"): + frames.extend(obj.Frames) + adjustToTerrain(frames, self.form.comboMethod.currentIndex() == 0) + self.close() + return True + + def reject(self): + self.close() + return False + + def close(self): + FreeCADGui.Control.closeDialog() + +import numpy as np +from scipy import stats + +def get_trend(points): + """Return the trend of a list of 3D points""" + x, y, z = zip(*[(point.x, point.y, point.z) for point in points]) + slope, intercept, _, _, _ = stats.linregress(y, z) + new_z = slope * np.array([y[0], y[-1]]) + intercept + return [FreeCAD.Vector(x[0], y[0], new_z[0]), FreeCAD.Vector(x[-1], y[-1], new_z[1])] + +def getTrend(points): # old + from scipy import stats + def getNewZ(x): + return slope * x + intercept + + xx = list() + yy = list() + zz = list() + for point in points: + xx.append(point.x) + yy.append(point.y) + zz.append(point.z) + slope, intercept, r, p, std_err = stats.linregress(yy, zz) + newzz = list(map(getNewZ, [yy[0], yy[-1]])) + return [FreeCAD.Vector(xx[0], yy[0], newzz[0]), + FreeCAD.Vector(xx[-1], yy[-1], newzz[1])] + + +def adjustToTerrain(frames, individual=True): + from datetime import datetime + starttime = datetime.now() + + import MeshPart as mp + + FreeCAD.ActiveDocument.openTransaction("Adjust to terrain") + terrain = PVPlantSite.get().Terrain.Mesh + + if individual: + for frame in frames: + length = frame.Setup.Length.Value / 2 + 5000 + p1 = FreeCAD.Vector(-length, 0, 0, ) + p2 = FreeCAD.Vector(length, 0, 0, ) + line = Part.LineSegment(p1, p2).toShape() + line.Placement = frame.Placement.copy() + line.Placement.Base.z = 0 + xyz = line.Placement.Rotation.toEulerAngles("XYZ") + line.Placement.Rotation.setEulerAngles("XYZ", 0, 0, xyz[2]) + pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1)) + pts = [] + for points in pro: + pts.extend(points) + points3D = get_trend(pts) + + pl = FreeCAD.Placement() + pl.Base = (points3D[0] + points3D[1]) / 2 + rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) + pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) + frame.Placement = pl + else: + import math + def getLineAngle(line): + # ángulo en grados = arctan(ángulo en porcentaje / 100%) + import math + p1 = FreeCAD.Vector(line.Vertexes[0].Point) + p2 = FreeCAD.Vector(line.Vertexes[1].Point) + hi = p2.z - p1.z + p1.z = 0 + p2.z = 0 + le = p2.sub(p1).Length + return math.degrees(math.atan2(hi, le)) + + cols = getCols(frames) + for col in cols: + for group in col: + # Ver 1 ----------------- + lines = [] + # 1. Generar las líneas de trabajo. + for frame in group: + # 1.1. Corregir los frames que estén fuera de tolerancia: + if frame.AngleY < FreeCAD.ActiveDocument.MaximumTiltNegative.Value: + frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltNegative.Value + if frame.AngleY > FreeCAD.ActiveDocument.MaximumTiltPositive.Value: + frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltPositive.Value + + # 1.2. Generar las líneas con las que se trabajarán: + l = frame.Setup.Length / 2 + pn = FreeCAD.Vector(-l, 0, 0) + ps = FreeCAD.Vector( l, 0, 0) + line = Part.LineSegment(pn, ps).toShape() + line.Placement = frame.Placement.copy() + lines.append([frame, line]) + + # 2. Poner los tracker en tolerancia: + cnt = len(lines) + if cnt > 1: + angleLine=[] + anglesTwoLines=[] + for frame in lines: + angleLine.append(frame[0].AngleY.Value) + for ind in range(cnt - 1): + frame1 = lines[ind] + frame2 = lines[ind + 1] + vec1 = frame1[1].Vertexes[1].Point.sub(frame1[1].Vertexes[0].Point) + vec2 = frame2[1].Vertexes[1].Point.sub(frame2[1].Vertexes[0].Point) + anglesTwoLines.append(math.degrees(vec2.getAngle(vec1))) + print(angleLine) + print(anglesTwoLines) + pass + + for ind, frame in enumerate(lines): + frame0 = None + frame1 = None + if ind > 0: + frame0 = lines[ind - 1] + if ind < (len(group) - 1): + frame1 = lines[ind + 1] + + if (frame0 is None) and (frame1 is None): # Caso 1: sólo 1 frame por fila + # no se hace nada. ya está con todos los parámetros dentro de tolerancia + pass + elif (frame0 is None) and not (frame1 is None): # Caso 2: frame es el primero y hay más frames + pass + elif not (frame0 is None) and (frame1 is None): # Caso 3: el frame es el último y hay más frames + pass + else: # Caso 4: el frame está en el médio de varios frames + pass + + continue + + # Ver 0 ----------------- + points = [] + # 1. Get lines/points to project on land + frame1 = group[0] # Norte + frame2 = group[-1] # Sur + # 1.1. Get the first and last points: + + # TODO: revisar esta parte: + p0 = FreeCAD.Vector(frame1.Shape.BoundBox.Center.x, frame1.Shape.BoundBox.YMax, 0.0) + pf = FreeCAD.Vector(frame2.Shape.BoundBox.Center.x, frame2.Shape.BoundBox.YMin, 0.0) + + vec = (pf - p0).normalize() + points.append(p0) + for ind in range(0, len(group) - 1): + frame1 = group[ind] + frame2 = group[ind + 1] + vec1 = FreeCAD.Vector(frame1.Placement.Base) + vec2 = FreeCAD.Vector(frame2.Placement.Base) + vec1.z = 0 + vec2.z = 0 + vec3 = vec2.sub(vec1) + c = vec3.Length / 2 + (frame1.Setup.Length.Value - frame2.Setup.Length.Value) / 4 + v = FreeCAD.Vector(vec) + v.Length = c + v = vec1.add(v) + v.z = 0 + points.append(v) + points.append(pf) + + # 2. Calculate trend: + points3D = [] + for ind in range(len(points) - 1): + line = Part.LineSegment(points[ind], points[ind + 1]).toShape() + pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1)) + pts = [] + for lp in pro: + pts.extend(lp) + points3D.extend(get_trend(pts)) + # Todo: aplicar aproximación de los vertices: + # prueba: + for i in range(0, len(points3D) - 2, 2): + # p0 = points3D[i] + p1 = points3D[i + 1] + p2 = points3D[i + 2] + # p3 = points3D[i + 3] + + l = p1.sub(p2).Length + if l > 250: + l = (l - 250) / 2 + if p1.z > p2.z: + p1.z -= l + p2.z += l + else: + p1.z += l + p2.z -= l + + # 3. Aplicar placement + for ind, frame in enumerate(group): + v1 = points3D[ind * 2] + v2 = points3D[ind * 2 + 1] + pl = frame.Placement.copy() + pl.Base.z = (v1.add(v2) / 2).z + rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), v1.sub(v2)) + pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) + frame.Placement = pl + + FreeCAD.ActiveDocument.commitTransaction() + total_time = datetime.now() - starttime + print(" -- Tiempo tardado en ajustar al terreno:", total_time) + FreeCAD.activeDocument().recompute() + + +def getRows(objs): + ''' ''' + def countFrames(columns): + cnt = 0 + for icol in columns: + cnt += len(icol) + return cnt + + if len(objs) == 0: + return None, None + + cols = getCols(list(objs)) + tmpcols = [] + for col in cols: + g = [] + for group in col: + g.extend(group) + tmpcols.append(g) + + rows = [] + while countFrames(tmpcols) > 0: + firstCol = max(tmpcols, key=lambda col: col[0].Placement.Base.y) + compFrame = max(firstCol, key=lambda x: x.Placement.Base.y) + ind = tmpcols.index(firstCol) + group = [compFrame,] + tmpcols[ind].remove(compFrame) + for i in range(ind - 1, 0, -1): + if len(tmpcols[i]) == 0: + break + frame = tmpcols[i][0] + framelen = frame.Setup.Length / 2 + compFramelen = compFrame.Setup.Length / 2 + l = max([framelen, compFramelen]) + if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l: + group.append(frame) + tmpcols[i].remove(frame) + compFrame = frame + else: + break + + for i in range(ind + 1, len(cols)): + if len(tmpcols[i]) == 0: + break + frame = tmpcols[i][0] + framelen = frame.Setup.Length / 2 + compFramelen = compFrame.Setup.Length / 2 + l = max([framelen, compFramelen]) + if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l: + group.append(frame) + tmpcols[i].remove(frame) + compFrame = frame + else: + break + + if len(group) > 0: + group = sorted(group, key=lambda x: x.Placement.Base.x) + rows.append(group) + + return rows, cols + + +def getCols(objs): + def getRound(num): + return round(num / 100, 0) + xx = set(getRound(obj.Placement.Base.x) for obj in objs) + xx = sorted(xx) + columns = [] + + for x in xx: + # 1. identificar los objetos de una columna + tmpcol = [] + for obj in objs: + if getRound(obj.Placement.Base.x) == x: + tmpcol.append(obj) + tmpcol = sorted(tmpcol, key=lambda obj: getRound(obj.Placement.Base.y), reverse=True) + for obj in tmpcol: + objs.remove(obj) + + # 2. dividir los objetos en grupos: + group = [] + col = [] + for i, f2 in enumerate(tmpcol): + if i > 0: + f1 = group[-1] + d = abs(f1.Placement.Base.y - f2.Placement.Base.y) - \ + (f1.Setup.Length.Value + f2.Setup.Length.Value) / 2 + if d > 1000: + col.append(group.copy()) + group.clear() + group.append(f2) + col.append(group) + columns.append(col) + return columns + + +# en el caso de que no sean perpendiculares a x: + +def getCols_old(sel, tolerance=4000, sort=True): + # TODO: get only frames from de selection + if not sel: + return + if len(sel) == 0: + return + + cols = [] + while len(sel) > 0: + obj = sel[0] + p = obj.Shape.BoundBox.Center + vec = obj.Shape.SubShapes[1].SubShapes[1].BoundBox.Center - \ + obj.Shape.SubShapes[1].SubShapes[0].BoundBox.Center + n = FreeCAD.Vector(vec.y, -vec.x, 0) + + # 1. Detectar los objetos que están en una misma columna + col = [] + newsel = [] + for obj1 in sel: + if obj1.Shape.BoundBox.isCutPlane(p, n): # todo: esto no es del todo correcto. buscar otra manera + col.append(obj1) + else: + newsel.append(obj1) + sel = newsel.copy() + col = sorted(col, key=lambda k: k.Placement.Base.y, reverse=True) # Orden Norte - Sur (Arriba a abajo) + + # 2. Detectar y separar los grupos dentro de una misma columna: + group = [] + newcol = [] + group.append(col[0]) + if len(col) > 1: + for ind in range(0, len(col) - 1): + vec1 = FreeCAD.Vector(col[ind].Placement.Base) + vec1.z = 0 + vec2 = FreeCAD.Vector(col[ind + 1].Placement.Base) + vec2.z = 0 + distance = abs((vec1 - vec2).Length) - (col[ind].Setup.Width.Value + col[ind + 1].Setup.Width.Value) / 2 + if distance > tolerance: + newcol.append(group.copy()) + group.clear() + group.append(col[ind + 1]) + newcol.append(group) + cols.append(newcol) + + if sort: + cols = sorted(cols, key=lambda k: k[0][0].Placement.Base.x, reverse=False) + + return cols + + +# ----------------------------------------------------------------------------------------------------------------------- +# Convert +# ----------------------------------------------------------------------------------------------------------------------- +class _PVPlantConvertTaskPanel: + '''The editmode TaskPanel for Conversions''' + + def __init__(self): + self.To = None + # self.form: + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacementConvert.ui")) + self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "Trace.svg"))) + self.form.buttonTo.clicked.connect(self.addTo) + + def addTo(self): + sel = FreeCADGui.Selection.getSelection() + if len(sel) > 0: + self.To = sel[0] + self.form.editTo.setText(self.To.Label) + + def accept(self): + sel = FreeCADGui.Selection.getSelection() + if sel == self.To: + return False + if len(sel) > 0 and self.To is not None: + FreeCAD.ActiveDocument.openTransaction("Convert to") + ConvertObjectsTo(sel, self.To) + return True + return False + + +def getHeadsAndSoil(frame=None): + if frame == None: + return None + import MeshPart as mp + + data = {"heads": [], + "soil": []} + poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes + for pole in poles: + vec = pole.BoundBox.Center + vec.z = pole.BoundBox.ZMax + data["heads"].append(vec) + + data["soil"].extend(mp.projectPointsOnMesh(data["heads"], + FreeCAD.ActiveDocument.Terrain.Mesh, + FreeCAD.Vector(0, 0, 1))) + return data + + +def moveFrameHead(obj, head=0, dist=0): + import math + print(dist) + dir = 1 if head == 0 else -1 + base = obj.Placement.Base + dist /= 2 + base.z += dist + angles = obj.Placement.Rotation.toEulerAngles("XYZ") + angley = math.degrees(math.asin(dist/(obj.Setup.Length.Value / 2))) * dir + print(angley) + rot = FreeCAD.Rotation(angles[2], angles[1] + angley, angles[0]) + obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0, 0, 0)) + obj.recompute() + +# --------------------------------------------------------------------------------------------------------------------- +# function ConvertObjectsTo +# +# --------------------------------------------------------------------------------------------------------------------- +def ConvertObjectsTo(sel, objTo): + if hasattr(objTo, "Proxy"): + isFrame = objTo.Proxy.__class__ is PVPlantRack._Tracker + # isFrame = issubclass(objTo.Proxy.__class__, PVPlantRack._Frame) + isFrame = True + + for obj in sel: + if isFrame: + if hasattr(obj, "Proxy"): + if obj.Proxy.__class__ is PVPlantRack._Tracker: + # if issubclass(obj.Proxy.__class__, PVPlantRack._Frame): # 1. Si los dos son Frames + cp = FreeCAD.ActiveDocument.copyObject(objTo, False) + cp.Placement = obj.Placement + cp.CloneOf = objTo + else: # 2. De un objeto no Frame a Frame + place = FreeCAD.Placement() # obj.Placement + place.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), + 90) # TODO: rotar conforme a lados más largos + bb = None + if obj.isDerivedFrom("Part::Feature"): + bb = obj.Shape.BoundBox + elif obj.isDerivedFrom("Mesh::Feature"): + bb = obj.Mesh.BoundBox + place.Base = bb.Center + cp = FreeCAD.ActiveDocument.copyObject(objTo, False) + cp.Placement = place + if isFrame: + cp.CloneOf = objTo + else: # 3. De un objeto a otro objeto (cualesquieran que sean) + place = FreeCAD.Placement() # obj.Placement + bb = None + if obj.isDerivedFrom("Part::Feature"): + bb = obj.Shape.BoundBox + elif obj.isDerivedFrom("Mesh::Feature"): + bb = obj.Mesh.BoundBox + place.Base = bb.Center + cp = FreeCAD.ActiveDocument.copyObject(objTo, False) + cp.Placement = place + if isFrame: + cp.CloneOf = objTo + FreeCAD.ActiveDocument.removeObject(obj.Name) + FreeCAD.activeDocument().recompute() + + +## Comandos: ----------------------------------------------------------------------------------------------------------- +class CommandPVPlantPlacement: + + def GetResources(self): + return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "way.svg")), + 'Accel': "P,P", + 'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"), + 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")} + + def Activated(self): + taskd = _PVPlantPlacementTaskPanel(None) + FreeCADGui.Control.showDialog(taskd) + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + + +class CommandAdjustToTerrain: + + def GetResources(self): + return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "adjust.svg")), + 'Accel': "P, A", + 'MenuText': QT_TRANSLATE_NOOP("Placement", "Adjust"), + 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Adjust object to terrain")} + + def Activated(self): + sel = FreeCADGui.Selection.getSelection() + if len(sel) > 0: + # adjustToTerrain(sel) + FreeCADGui.Control.showDialog(adjustToTerrainTaskPanel()) + else: + print("No selected object") + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + + +class CommandConvert: + def GetResources(self): + return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "convert.svg")), + 'Accel': "P, C", + 'MenuText': QT_TRANSLATE_NOOP("Placement", "Convert"), + 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Convertir un objeto en otro")} + + def IsActive(self): + return (not FreeCAD.ActiveDocument is None and + not FreeCAD.ActiveDocument.getObject("Site") is None and + not FreeCAD.ActiveDocument.getObject("Terrain") is None and + not FreeCAD.ActiveDocument.getObject("TrackerSetup") is None) + + def Activated(self): + taskd = _PVPlantConvertTaskPanel() + FreeCADGui.Control.showDialog(taskd) + + +'''if FreeCAD.GuiUp: + FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement()) + FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain()) + FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())''' diff --git a/PVPlantGeoreferencing.py b/PVPlantGeoreferencing.py index 6f63223..85ff41a 100644 --- a/PVPlantGeoreferencing.py +++ b/PVPlantGeoreferencing.py @@ -20,403 +20,10 @@ # * * # *********************************************************************** -import FreeCAD +""" +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) - - # -- 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 \ No newline at end of file +from PVPlant.core.georef import MapWindow, CommandPVPlantGeoreferencing \ No newline at end of file diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py index b8b741d..a07fad4 100644 --- a/PVPlantImportGrid.py +++ b/PVPlantImportGrid.py @@ -20,652 +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): - """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()) - +""" +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, +) \ No newline at end of file diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index f130245..19b68d6 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -20,2311 +20,14 @@ # * * # *********************************************************************** -import FreeCAD -import Part +""" +PVPlantPlacement - Wrapper de compatibilidad. -if FreeCAD.GuiUp: - import FreeCADGui, os - from PySide import QtCore, QtGui - from PySide.QtGui import QListWidgetItem - from PySide.QtCore import QT_TRANSLATE_NOOP -else: - # \cond - def translate(ctxt, txt): - return txt +Código movido a PVPlant/placement/placement.py. +""" - - def QT_TRANSLATE_NOOP(ctxt, txt): - return txt - # \endcond - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - def _fromUtf8(s): - return s - -import PVPlantResources -import PVPlantSite - -version = "0.1.0" - - -def selectionFilter(sel, objtype): - fil = [] - for obj in sel: - if hasattr(obj, "Proxy"): - if obj.Proxy.__class__ is objtype: - fil.append(obj) - return fil - - -class _PVPlantPlacementTaskPanel_old: - '''The editmode TaskPanel for Schedules''' - - def __init__(self, obj=None): - self.site = PVPlantSite.get() - self.Terrain = self.site.Terrain - self.FrameSetups = None - self.PVArea = None - self.Area = None - self.gap_col = .0 - self.gap_row = .0 - self.offsetX = .0 - self.offsetY = .0 - self.Dir = FreeCAD.Vector(0, -1, 0) # Norte a sur - - # self.form: - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) - self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) - - self.addFrames() - self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) - - self.form.buttonPVArea.clicked.connect(self.addPVArea) - self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) - self.update_inner_spacing() - - def addPVArea(self): - sel = FreeCADGui.Selection.getSelection() - if len(sel) > 0: - self.PVArea = sel[0] - self.form.editPVArea.setText(self.PVArea.Label) - - def addFrames(self): - for frame_setup in self.site.Frames: - list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) - list_item.setCheckState(QtCore.Qt.Checked) - - def update_inner_spacing(self): - self.form.editInnerSpacing.setText( - ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) - - def createFrameFromPoints(self, dataframe): - from Mechanical.Frame import PVPlantFrame - '''try: - MechanicalGroup = FreeCAD.ActiveDocument.Frames - except: - MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames') - MechanicalGroup.Label = "Frames" - FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) - - if self.form.cbSubfolders.isChecked: - name = "Frames-" + self.PVArea.Label - if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]: - MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0] - else: - group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) - group.Label = name - MechanicalGroup.addObject(group) - MechanicalGroup = group''' - - doc = FreeCAD.ActiveDocument - - # 1. Obtener o crear el grupo principal 'Frames' - main_group_name = "Frames" - main_group = doc.getObject(main_group_name) - if not main_group: - main_group = doc.addObject("App::DocumentObjectGroup", main_group_name) - main_group.Label = main_group_name - # Asumiendo que existe un grupo 'MechanicalGroup' - if hasattr(doc, 'MechanicalGroup'): - doc.MechanicalGroup.addObject(main_group) - - # 2. Manejar subgrupo si es necesario - group = main_group # Grupo donde se añadirán los marcos - if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis! - subgroup_name = f"Frames-{self.PVArea.Label}" - - # Buscar subgrupo existente - subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) - - if not subgroup: - subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) - subgroup.Label = subgroup_name - main_group.addObject(subgroup) - group = subgroup - - try: - placements = dataframe["placement"].tolist() - types = dataframe["type"].tolist() - frames = [] - for idx in range(len(placements)): - newrack = PVPlantFrame.makeTracker(setup=types[idx]) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = placements[idx] - group.addObject(newrack) - frames.append(newrack) - except: - placements = dataframe[0] - frames = [] - for idx in placements: - print(idx) - newrack = PVPlantFrame.makeTracker(setup=idx[0]) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = idx[1] - groupq.addObject(newrack) - frames.append(newrack) - - if self.PVArea.Name.startswith("FrameArea"): - self.PVArea.Frames = frames - - def getProjected(self, shape): - """ returns projected edges from a shape and a direction """ - if shape.BoundBox.ZLength == 0: - return Part.Face(Part.Wire(shape.Edges)) - - from Utils import PVPlantUtils as utils - wire = utils.simplifyWire(utils.getProjected(shape)) - return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) - - def calculateWorkingArea(self): - self.Area = self.getProjected(self.PVArea.Shape) - exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") - - if exclusion_areas: - prohibited_faces = [] - for obj in exclusion_areas: - face = self.getProjected(obj.Shape.SubShapes[1]) - if face.isValid(): - prohibited_faces.append(face) - self.Area = self.Area.cut(prohibited_faces) - - def getAligments(self): - # TODO: revisar todo esto: ----------------------------------------------------------------- - sel = FreeCADGui.Selection.getSelectionEx()[0] - refh = None - refv = None - - if len(sel.SubObjects) == 0: - return - - elif len(sel.SubObjects) == 1: - # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma - refh = refv = sel.SubObjects[0] - - elif len(sel.SubObjects) > 1: - # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma - if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: - refh = sel.SubObjects[0] - else: - refh = sel.SubObjects[1] - - if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: - refv = sel.SubObjects[0] - else: - refv = sel.SubObjects[1] - - steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col) - startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps) - steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row) - starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps) - # todo end ---------------------------------------------------------------------------------- - - return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ - np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) - - def adjustToTerrain_old(self, coordinates): - mode = 1 - terrain = self.Terrain.Mesh - - def placeRegion(df): # TODO: new - import MeshPart as mp - from scipy import stats - linregression = [] - for colnum in df.column.unique().tolist(): - dftmp = df[df["column"] == colnum] - for id in dftmp["ID"].tolist(): - data = df.loc[df['ID'] == id] - frametype = data["type"].tolist()[0] - # col = data["column"] - # row = data["row"] - base = data["placement"].tolist()[0] - - yl = frametype.Length.Value / 2 - ptop = FreeCAD.Vector(base) - ptop.y += yl - pbot = FreeCAD.Vector(base) - pbot.y -= yl - line = Part.LineSegment(ptop, pbot).toShape() - profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] - '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo. - tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) - profilepoints = [ver.Point for ver in tmp.Vertexes]''' - - xx = list() - yy = list() - zz = list() - for pts in profilepoints: - xx.append(pts.x) - yy.append(pts.y) - zz.append(pts.z) - slope, intercept, r, p, std_err = stats.linregress(yy, zz) - - # linregression.append(slope, intercept, r, p, std_err) - def myfunc(x): - return slope * x + intercept - - newzz = list(map(myfunc, [yy[0], yy[-1]])) - points3D = list() - points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) - points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) - linregression.append(points3D) - - # for ind in range(0, len(points3D) - 1): - pl = FreeCAD.Placement() - pl.Base = (points3D[0] + points3D[1]) / 2 - rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) - pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) - df.at[id - 1, "placement"] = pl - df["regression"] = linregression - - # 01. Grouping: - from scipy.ndimage import label as sclabel - import pandas as pd - tmp = [] - for c, col in enumerate(coordinates): - tmpcol = [] - for n, obj in enumerate(col): - if obj != 0: - tmpcol.append(1) - else: - tmpcol.append(0) - tmp.append(tmpcol) - - data = {"ID": [], - "region": [], - "type": [], - "column": [], - "row": [], - "placement": []} - - arr = np.array(tmp) - labeled_array, num_features = sclabel(arr) - id = 1 - for label in range(1, num_features + 1): - cols, rows = np.where(labeled_array == label) - unique, counts = np.unique(cols, return_counts=True) - result = np.column_stack((unique, counts)) - cnt = 0 - for val, count in result: - for c in range(count): - data["ID"].append(id) - data["region"].append(label) - data["type"].append(coordinates[val][rows[cnt]][0]) - data["column"].append(val) - data["row"].append(rows[cnt]) - data["placement"].append(coordinates[val][rows[cnt]][1]) - cnt += 1 - id += 1 - df = pd.DataFrame(data) - placeRegion(df) - return df - - def _setup_terrain_interpolator(self): - """Prepara interpolador del terreno para ajuste rápido""" - import numpy as np - from scipy.interpolate import LinearNDInterpolator - - mesh = self.Terrain.Mesh - points = np.array([p.Vector for p in mesh.Points]) - bbox = self.Area.BoundBox - - # Filtrar puntos dentro del área de trabajo - in_bbox = [ - p for p in points - if bbox.XMin <= p[0] <= bbox.XMax and - bbox.YMin <= p[1] <= bbox.YMax - ] - - if not in_bbox: - return None - - coords = np.array(in_bbox) - return LinearNDInterpolator(coords[:, :2], coords[:, 2]) - - def adjustToTerrain(self, coordinates): - from scipy.ndimage import label as sclabel - import pandas as pd - import numpy as np - from scipy import stats - import MeshPart - - # Crear matriz binaria - arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) - labeled_array, num_features = sclabel(arr) - - # Construir DataFrame optimizado - data = [] - terrain_interp = self._setup_terrain_interpolator() - - for label in range(1, num_features + 1): - cols, rows = np.where(labeled_array == label) - for idx, (col, row) in enumerate(zip(cols, rows)): - frame_type, placement = coordinates[col][row] - data.append({ - 'ID': len(data) + 1, - 'region': label, - 'type': frame_type, - 'column': col, - 'row': row, - 'placement': placement - }) - - df = pd.DataFrame(data) - - # Ajustar al terreno - for idx, row in df.iterrows(): - pl = row['placement'] - yl = row['type'].Length.Value / 2 - - # Calcular puntos extremos - top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) - bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) - - # Usar interpolador si está disponible - if terrain_interp: - yy = np.linspace(bot_point.y, top_point.y, 10) - xx = np.full(10, pl.x) - zz = terrain_interp(xx, yy) - - if not np.isnan(zz).all(): - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - else: - # Fallback a proyección directa - line = Part.LineSegment(bot_point, top_point).toShape() - projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] - if len(projected) >= 2: - yy = [p.y for p in projected] - zz = [p.z for p in projected] - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - - # Actualizar placement - new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) - new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) - - new_pl = FreeCAD.Placement() - new_pl.Base = (new_top + new_bot) / 2 - new_pl.Rotation = FreeCAD.Rotation( - FreeCAD.Vector(-1, 0, 0), - new_top - new_bot - ) - df.at[idx, 'placement'] = new_pl - - return df - - def isInside(self, frame, point): - if self.Area.isInside(point, 10, True): - frame.Placement.Base = point - cut = frame.cut([self.Area]) - if len(cut.Vertexes) == 0: - return True - return False - - def calculateAlignedArray(self): - import FreeCAD - pointsx, pointsy = self.getAligments() - - footprints = [] - for frame in self.FrameSetups: - xx = frame.Length.Value - yy = frame.Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, -yy_med, 0)]) - rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) - footprints.append([frame, rec]) - ref = footprints.pop(0) - xx = ref[0].Length.Value - yy = ref[0].Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - - # variables for corridors: - countcols = 0 - countrows = 0 - offsetcols = 0 # ?? - offsetrows = 0 # ?? - valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) - - cols = [] - for x in pointsx: - col = [] - for y in pointsy: - found = False - point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0) - if self.isInside(ref[1], point): - col.append([ref[0], point]) - found = True - continue - else: - for footprint in footprints: - l = int((ref[0].Length - footprint[0].Length) / 2) - for i in range(2): - point1 = FreeCAD.Vector(point) - point1.y = point1.y + l - if self.isInside(footprint[1], point1): - col.append([footprint[0], point1]) - found = True - break - l = -l - if found: - break - if not found: - col.append(0) - cols.append(col) - - # if len(col) > 0: - # code for vertical corridors: - if self.form.groupCorridor.isChecked(): - if self.form.editColCount.value() > 0: - countcols += 1 - if countcols == self.form.editColCount.value(): - offsetcols += valcols - countcols = 0 - - return self.adjustToTerrain(cols) - - def calculateNonAlignedArray(self): - pointsx, pointsy = self.getAligments() - if len(pointsx) == 0: - FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n") - return [] - - footprints = [] - for frame in self.FrameSetups: - l = frame.Length.Value - w = frame.Width.Value - l_med = l / 2 - w_med = w / 2 - rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0), - FreeCAD.Vector( l_med, -w_med, 0), - FreeCAD.Vector( l_med, w_med, 0), - FreeCAD.Vector(-l_med, w_med, 0), - FreeCAD.Vector(-l_med, -w_med, 0)]) - rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) - footprints.append([frame, rec]) - - corridor = self.form.groupCorridor.isChecked() - corridor_offset = 0 - count = 0 - - cols = [] - for x in pointsx: - col=[] - x += corridor_offset - p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0) - line = Part.makePolygon([p1, p2]) - inter = self.Area.section([line]) - pts = [ver.Point for ver in inter.Vertexes] - pts = sorted(pts, key=lambda p: p.y, reverse=True) - for i in range(0, len(pts), 2): - top = pts[i] - bootom = pts[i + 1] - if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength: - y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2) - cp = footprints[-1][1].copy() - cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0) - inter = cp.cut([self.Area]) - vtx = [ver.Point for ver in inter.Vertexes] - mod = top.y - if len(vtx) != 0: - mod = min(vtx, key=lambda p: p.y).y - #y1 = cp.Placement.Base.y - mod - - tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy') - for opt in tmp[0]: - mod -= (footprints[opt][1].BoundBox.YLength / 2) - pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0) - cp = footprints[opt][1].copy() - if self.isInside(cp, pl): - col.append([footprints[opt][0], pl]) - mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500) - Part.show(cp) - - if corridor and len(col) > 0: - count += 1 - if count == self.form.editColCount.value(): - corridor_offset += 12000 - count = 0 - - cols.append(cols) - return self.adjustToTerrain(cols) - - def accept(self): - from datetime import datetime - starttime = datetime.now() - - params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") - auto_save_enabled = params.GetBool("AutoSaveEnabled") - params.SetBool("AutoSaveEnabled", False) - FreeCAD.ActiveDocument.RecomputesFrozen = True - - items = [ - FreeCAD.ActiveDocument.getObject(item.text()) - for i in range(self.form.listFrameSetups.count()) - if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked - ] - - unique_frames = {frame.Length.Value: frame for frame in items} - self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) - - self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value - self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value - self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value - self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value - - FreeCAD.ActiveDocument.openTransaction("Create Placement") - # 1. Calculate working area: - self.calculateWorkingArea() - # 2. Calculate aligned array: - if self.form.cbAlignFrames.isChecked(): - dataframe = self.calculateAlignedArray() - else: - dataframe = self.calculateNonAlignedArray() - # 3. Adjust to terrain: - self.createFrameFromPoints(dataframe) - - import Electrical.group as egroup - import importlib - importlib.reload(egroup) - egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) - - - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.RecomputesFrozen = False - params.SetBool("AutoSaveEnabled", auto_save_enabled) - - total_time = datetime.now() - starttime - print(" -- Tiempo tardado:", total_time) - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - - -class _PVPlantPlacementTaskPanel_new1: - '''The editmode TaskPanel for Schedules''' - - def __init__(self, obj=None): - self.site = PVPlantSite.get() - self.Terrain = self.site.Terrain - self.FrameSetups = None - self.PVArea = None - self.Area = None - self.gap_col = .0 - self.gap_row = .0 - self.offsetX = .0 - self.offsetY = .0 - self.Dir = FreeCAD.Vector(0, -1, 0) # Norte a sur - - # self.form: - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) - self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) - - self.addFrames() - self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) - - self.form.buttonPVArea.clicked.connect(self.addPVArea) - self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) - self.update_inner_spacing() - - def addPVArea(self): - sel = FreeCADGui.Selection.getSelection() - if len(sel) > 0: - self.PVArea = sel[0] - self.form.editPVArea.setText(self.PVArea.Label) - - def addFrames(self): - for frame_setup in self.site.Frames: - list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) - list_item.setCheckState(QtCore.Qt.Checked) - - def update_inner_spacing(self): - self.form.editInnerSpacing.setText( - ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) - - def createFrameFromPoints(self, dataframe): - from Mechanical.Frame import PVPlantFrame - doc = FreeCAD.ActiveDocument - - # 1. Obtener o crear el grupo principal 'Frames' - main_group_name = "Frames" - main_group = doc.getObject(main_group_name) - if not main_group: - main_group = doc.addObject("App::DocumentObjectGroup", main_group_name) - main_group.Label = main_group_name - # Asumiendo que existe un grupo 'MechanicalGroup' - if hasattr(doc, 'MechanicalGroup'): - doc.MechanicalGroup.addObject(main_group) - - # 2. Manejar subgrupo si es necesario - group = main_group # Grupo donde se añadirán los marcos - if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis! - subgroup_name = f"Frames-{self.PVArea.Label}" - - # Buscar subgrupo existente - subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) - - if not subgroup: - subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) - subgroup.Label = subgroup_name - main_group.addObject(subgroup) - group = subgroup - - try: - placements = dataframe["placement"].tolist() - types = dataframe["type"].tolist() - frames = [] - for idx in range(len(placements)): - newrack = PVPlantFrame.makeTracker(setup=types[idx]) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = placements[idx] - group.addObject(newrack) - frames.append(newrack) - except: - placements = dataframe[0] - frames = [] - for idx in placements: - print(idx) - newrack = PVPlantFrame.makeTracker(setup=idx[0]) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = idx[1] - groupq.addObject(newrack) - frames.append(newrack) - - if self.PVArea.Name.startswith("FrameArea"): - self.PVArea.Frames = frames - - def getProjected(self, shape): - """ returns projected edges from a shape and a direction """ - if shape.BoundBox.ZLength == 0: - return Part.Face(Part.Wire(shape.Edges)) - - from Utils import PVPlantUtils as utils - wire = utils.simplifyWire(utils.getProjected(shape)) - return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) - - def calculateWorkingArea(self): - self.Area = self.getProjected(self.PVArea.Shape) - exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") - - if exclusion_areas: - prohibited_faces = [] - for obj in exclusion_areas: - face = self.getProjected(obj.Shape.SubShapes[1]) - if face.isValid(): - prohibited_faces.append(face) - self.Area = self.Area.cut(prohibited_faces) - - def getAligments(self): - # TODO: revisar todo esto: ----------------------------------------------------------------- - sel = FreeCADGui.Selection.getSelectionEx()[0] - refh = None - refv = None - - if len(sel.SubObjects) == 0: - return - - elif len(sel.SubObjects) == 1: - # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma - refh = refv = sel.SubObjects[0] - - elif len(sel.SubObjects) > 1: - # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma - if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: - refh = sel.SubObjects[0] - else: - refh = sel.SubObjects[1] - - if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: - refv = sel.SubObjects[0] - else: - refv = sel.SubObjects[1] - - steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col) - startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps) - steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row) - starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps) - # todo end ---------------------------------------------------------------------------------- - - return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ - np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) - - def adjustToTerrain_old(self, coordinates): - mode = 1 - terrain = self.Terrain.Mesh - - def placeRegion(df): # TODO: new - import MeshPart as mp - from scipy import stats - linregression = [] - for colnum in df.column.unique().tolist(): - dftmp = df[df["column"] == colnum] - for id in dftmp["ID"].tolist(): - data = df.loc[df['ID'] == id] - frametype = data["type"].tolist()[0] - # col = data["column"] - # row = data["row"] - base = data["placement"].tolist()[0] - - yl = frametype.Length.Value / 2 - ptop = FreeCAD.Vector(base) - ptop.y += yl - pbot = FreeCAD.Vector(base) - pbot.y -= yl - line = Part.LineSegment(ptop, pbot).toShape() - profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] - '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo. - tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) - profilepoints = [ver.Point for ver in tmp.Vertexes]''' - - xx = list() - yy = list() - zz = list() - for pts in profilepoints: - xx.append(pts.x) - yy.append(pts.y) - zz.append(pts.z) - slope, intercept, r, p, std_err = stats.linregress(yy, zz) - - # linregression.append(slope, intercept, r, p, std_err) - def myfunc(x): - return slope * x + intercept - - newzz = list(map(myfunc, [yy[0], yy[-1]])) - points3D = list() - points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) - points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) - linregression.append(points3D) - - # for ind in range(0, len(points3D) - 1): - pl = FreeCAD.Placement() - pl.Base = (points3D[0] + points3D[1]) / 2 - rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) - pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) - df.at[id - 1, "placement"] = pl - df["regression"] = linregression - - # 01. Grouping: - from scipy.ndimage import label as sclabel - import pandas as pd - tmp = [] - for c, col in enumerate(coordinates): - tmpcol = [] - for n, obj in enumerate(col): - if obj != 0: - tmpcol.append(1) - else: - tmpcol.append(0) - tmp.append(tmpcol) - - data = {"ID": [], - "region": [], - "type": [], - "column": [], - "row": [], - "placement": []} - - arr = np.array(tmp) - labeled_array, num_features = sclabel(arr) - id = 1 - for label in range(1, num_features + 1): - cols, rows = np.where(labeled_array == label) - unique, counts = np.unique(cols, return_counts=True) - result = np.column_stack((unique, counts)) - cnt = 0 - for val, count in result: - for c in range(count): - data["ID"].append(id) - data["region"].append(label) - data["type"].append(coordinates[val][rows[cnt]][0]) - data["column"].append(val) - data["row"].append(rows[cnt]) - data["placement"].append(coordinates[val][rows[cnt]][1]) - cnt += 1 - id += 1 - df = pd.DataFrame(data) - placeRegion(df) - return df - - def _setup_terrain_interpolator(self): - """Prepara interpolador del terreno para ajuste rápido""" - import numpy as np - from scipy.interpolate import LinearNDInterpolator - - mesh = self.Terrain.Mesh - points = np.array([p.Vector for p in mesh.Points]) - bbox = self.Area.BoundBox - - # Filtrar puntos dentro del área de trabajo - in_bbox = [ - p for p in points - if bbox.XMin <= p[0] <= bbox.XMax and - bbox.YMin <= p[1] <= bbox.YMax - ] - - if not in_bbox: - return None - - coords = np.array(in_bbox) - return LinearNDInterpolator(coords[:, :2], coords[:, 2]) - - def adjustToTerrain(self, coordinates): - from scipy.ndimage import label as sclabel - import pandas as pd - import numpy as np - from scipy import stats - import MeshPart - - # Crear matriz binaria - arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) - labeled_array, num_features = sclabel(arr) - - # Construir DataFrame optimizado - data = [] - terrain_interp = self._setup_terrain_interpolator() - - for label in range(1, num_features + 1): - cols, rows = np.where(labeled_array == label) - for idx, (col, row) in enumerate(zip(cols, rows)): - frame_type, placement = coordinates[col][row] - data.append({ - 'ID': len(data) + 1, - 'region': label, - 'type': frame_type, - 'column': col, - 'row': row, - 'placement': placement - }) - - df = pd.DataFrame(data) - - # Ajustar al terreno - for idx, row in df.iterrows(): - pl = row['placement'] - yl = row['type'].Length.Value / 2 - - # Calcular puntos extremos - top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) - bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) - - # Usar interpolador si está disponible - if terrain_interp: - yy = np.linspace(bot_point.y, top_point.y, 10) - xx = np.full(10, pl.x) - zz = terrain_interp(xx, yy) - - if not np.isnan(zz).all(): - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - else: - # Fallback a proyección directa - line = Part.LineSegment(bot_point, top_point).toShape() - projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] - if len(projected) >= 2: - yy = [p.y for p in projected] - zz = [p.z for p in projected] - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - - # Actualizar placement - new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) - new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) - - new_pl = FreeCAD.Placement() - new_pl.Base = (new_top + new_bot) / 2 - new_pl.Rotation = FreeCAD.Rotation( - FreeCAD.Vector(-1, 0, 0), - new_top - new_bot - ) - df.at[idx, 'placement'] = new_pl - - return df - - def isInside(self, frame, point): - if self.Area.isInside(point, 10, True): - frame.Placement.Base = point - cut = frame.cut([self.Area]) - if len(cut.Vertexes) == 0: - return True - return False - - def calculateAlignedArray(self): - import FreeCAD - pointsx, pointsy = self.getAligments() - - footprints = [] - for frame in self.FrameSetups: - xx = frame.Length.Value - yy = frame.Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, -yy_med, 0)]) - rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) - footprints.append([frame, rec]) - ref = footprints.pop(0) - xx = ref[0].Length.Value - yy = ref[0].Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - - # variables for corridors: - countcols = 0 - countrows = 0 - offsetcols = 0 # ?? - offsetrows = 0 # ?? - valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) - - cols = [] - for x in pointsx: - col = [] - for y in pointsy: - found = False - point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0) - if self.isInside(ref[1], point): - col.append([ref[0], point]) - found = True - continue - else: - for footprint in footprints: - l = int((ref[0].Length - footprint[0].Length) / 2) - for i in range(2): - point1 = FreeCAD.Vector(point) - point1.y = point1.y + l - if self.isInside(footprint[1], point1): - col.append([footprint[0], point1]) - found = True - break - l = -l - if found: - break - if not found: - col.append(0) - cols.append(col) - - # if len(col) > 0: - # code for vertical corridors: - if self.form.groupCorridor.isChecked(): - if self.form.editColCount.value() > 0: - countcols += 1 - if countcols == self.form.editColCount.value(): - offsetcols += valcols - countcols = 0 - - return self.adjustToTerrain(cols) - - def calculateNonAlignedArray(self): - pointsx, pointsy = self.getAligments() - if len(pointsx) == 0: - FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n") - return [] - - footprints = [] - for frame in self.FrameSetups: - l = frame.Length.Value - w = frame.Width.Value - l_med = l / 2 - w_med = w / 2 - rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0), - FreeCAD.Vector( l_med, -w_med, 0), - FreeCAD.Vector( l_med, w_med, 0), - FreeCAD.Vector(-l_med, w_med, 0), - FreeCAD.Vector(-l_med, -w_med, 0)]) - rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) - footprints.append([frame, rec]) - - corridor = self.form.groupCorridor.isChecked() - corridor_offset = 0 - count = 0 - - cols = [] - for x in pointsx: - col=[] - x += corridor_offset - p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0) - line = Part.makePolygon([p1, p2]) - inter = self.Area.section([line]) - pts = [ver.Point for ver in inter.Vertexes] - pts = sorted(pts, key=lambda p: p.y, reverse=True) - for i in range(0, len(pts), 2): - top = pts[i] - bootom = pts[i + 1] - if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength: - y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2) - cp = footprints[-1][1].copy() - cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0) - inter = cp.cut([self.Area]) - vtx = [ver.Point for ver in inter.Vertexes] - mod = top.y - if len(vtx) != 0: - mod = min(vtx, key=lambda p: p.y).y - #y1 = cp.Placement.Base.y - mod - - tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy') - for opt in tmp[0]: - mod -= (footprints[opt][1].BoundBox.YLength / 2) - pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0) - cp = footprints[opt][1].copy() - if self.isInside(cp, pl): - col.append([footprints[opt][0], pl]) - mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500) - Part.show(cp) - - if corridor and len(col) > 0: - count += 1 - if count == self.form.editColCount.value(): - corridor_offset += 12000 - count = 0 - - cols.append(col) - return self.adjustToTerrain(cols) - - def accept(self): - from datetime import datetime - starttime = datetime.now() - - params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") - auto_save_enabled = params.GetBool("AutoSaveEnabled") - params.SetBool("AutoSaveEnabled", False) - FreeCAD.ActiveDocument.RecomputesFrozen = True - - items = [ - FreeCAD.ActiveDocument.getObject(item.text()) - for i in range(self.form.listFrameSetups.count()) - if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked - ] - - unique_frames = {frame.Length.Value: frame for frame in items} - self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) - - self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value - self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value - self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value - self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value - - FreeCAD.ActiveDocument.openTransaction("Create Placement") - # 1. Calculate working area: - self.calculateWorkingArea() - # 2. Calculate aligned array: - if self.form.cbAlignFrames.isChecked(): - dataframe = self.calculateAlignedArray() - else: - dataframe = self.calculateNonAlignedArray() - # 3. Adjust to terrain: - self.createFrameFromPoints(dataframe) - - import Electrical.group as egroup - import importlib - importlib.reload(egroup) - egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) - - - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.RecomputesFrozen = False - params.SetBool("AutoSaveEnabled", auto_save_enabled) - - total_time = datetime.now() - starttime - print(" -- Tiempo tardado:", total_time) - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - - -import numpy as np -import pandas as pd -from scipy.ndimage import label as sclabel -from scipy import stats -from scipy.interpolate import LinearNDInterpolator -import Part -import FreeCAD -import FreeCADGui -from PySide2 import QtCore, QtGui -from PySide2.QtWidgets import QListWidgetItem -import os -import PVPlantResources - - -class _PVPlantPlacementTaskPanel: - '''The editmode TaskPanel for Schedules''' - - def __init__(self, obj=None): - self.site = PVPlantSite.get() - self.Terrain = self.site.Terrain - self.FrameSetups = None - self.PVArea = None - self.Area = None - self.gap_col = .0 - self.gap_row = .0 - self.offsetX = .0 - self.offsetY = .0 - self.Dir = FreeCAD.Vector(0, -1, 0) - self._terrain_interpolator = None - self._frame_footprints_cache = {} - - # UI setup - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) - self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) - - self.addFrames() - self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) - - self.form.buttonPVArea.clicked.connect(self.addPVArea) - self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) - self.update_inner_spacing() - - def addPVArea(self): - sel = FreeCADGui.Selection.getSelection() - if sel: - self.PVArea = sel[0] - self.form.editPVArea.setText(self.PVArea.Label) - - def addFrames(self): - for frame_setup in self.site.Frames: - list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) - list_item.setCheckState(QtCore.Qt.Checked) - - def update_inner_spacing(self): - self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m") - - def _get_or_create_frame_group(self): - """Optimized group creation and management""" - doc = FreeCAD.ActiveDocument - - # Get or create main group - main_group = doc.getObject("Frames") or doc.addObject("App::DocumentObjectGroup", "Frames") - if not main_group.Label == "Frames": - main_group.Label = "Frames" - - # Add to MechanicalGroup if exists - if not hasattr(doc, 'MechanicalGroup') and hasattr(doc, 'getObject') and doc.getObject('MechanicalGroup'): - doc.MechanicalGroup.addObject(main_group) - - # Handle subfolder - if self.form.cbSubfolders.isChecked() and self.PVArea: - subgroup_name = f"Frames-{self.PVArea.Label}" - subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) - if not subgroup: - subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) - subgroup.Label = subgroup_name - main_group.addObject(subgroup) - return subgroup - - return main_group - - def createFrameFromPoints(self, dataframe): - from Mechanical.Frame import PVPlantFrame - doc = FreeCAD.ActiveDocument - - group = self._get_or_create_frame_group() - - frames = [] - placements_key = "placement" if "placement" in dataframe.columns else 0 - - if placements_key == "placement": - placements = dataframe["placement"].tolist() - types = dataframe["type"].tolist() - - for idx, (placement, frame_type) in enumerate(zip(placements, types)): - newrack = PVPlantFrame.makeTracker(setup=frame_type) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = placement - group.addObject(newrack) - frames.append(newrack) - - if self.PVArea and self.PVArea.Name.startswith("FrameArea"): - self.PVArea.Frames = frames - - def getProjected(self, shape): - """Optimized projection calculation""" - if shape.BoundBox.ZLength == 0: - return Part.Face(Part.Wire(shape.Edges)) - - from Utils import PVPlantUtils as utils - wire = utils.simplifyWire(utils.getProjected(shape)) - return Part.Face(wire.removeSplitter()) if wire and wire.isClosed() else Part.Face(wire) - - def calculateWorkingArea(self): - """Optimized working area calculation""" - self.Area = self.getProjected(self.PVArea.Shape) - exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") - - if exclusion_areas: - prohibited_faces = [] - for obj in exclusion_areas: - face = self.getProjected(obj.Shape.SubShapes[1]) - if face and face.isValid(): - prohibited_faces.append(face) - - if prohibited_faces: - self.Area = self.Area.cut(prohibited_faces) - - # Clear terrain interpolator cache when area changes - self._terrain_interpolator = None - - def _setup_terrain_interpolator(self): - """Cached terrain interpolator""" - if self._terrain_interpolator is not None: - return self._terrain_interpolator - - mesh = self.Terrain.Mesh - points = np.array([v.Vector for v in mesh.Points]) - bbox = self.Area.BoundBox - - # Filter points within working area efficiently - mask = ((points[:, 0] >= bbox.XMin) & (points[:, 0] <= bbox.XMax) & - (points[:, 1] >= bbox.YMin) & (points[:, 1] <= bbox.YMax)) - filtered_points = points[mask] - - if len(filtered_points) == 0: - self._terrain_interpolator = None - return None - - try: - self._terrain_interpolator = LinearNDInterpolator( - filtered_points[:, :2], filtered_points[:, 2] - ) - except: - self._terrain_interpolator = None - - return self._terrain_interpolator - - def _get_frame_footprint(self, frame): - """Cached footprint calculation""" - frame_key = (frame.Length.Value, frame.Width.Value) - if frame_key not in self._frame_footprints_cache: - l, w = frame.Length.Value, frame.Width.Value - l_med, w_med = l / 2, w / 2 - - footprint = Part.makePolygon([ - FreeCAD.Vector(-l_med, -w_med, 0), - FreeCAD.Vector(l_med, -w_med, 0), - FreeCAD.Vector(l_med, w_med, 0), - FreeCAD.Vector(-l_med, w_med, 0), - FreeCAD.Vector(-l_med, -w_med, 0) - ]) - footprint.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) - - self._frame_footprints_cache[frame_key] = footprint - - return self._frame_footprints_cache[frame_key] - - def _calculate_terrain_adjustment_batch(self, points_data): - """Process terrain adjustments in batches for better performance""" - terrain_interp = self._setup_terrain_interpolator() - results = [] - - for frame_type, base_point in points_data: - yl = frame_type.Length.Value / 2 - top_point = FreeCAD.Vector(base_point.x, base_point.y + yl, 0) - bot_point = FreeCAD.Vector(base_point.x, base_point.y - yl, 0) - - if terrain_interp: - # Use interpolator for faster elevation calculation - yy = np.linspace(bot_point.y, top_point.y, 6) # Reduced points for speed - xx = np.full_like(yy, base_point.x) - try: - zz = terrain_interp(xx, yy) - if not np.isnan(zz).all(): - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - except: - z_top = z_bot = 0 - else: - # Fallback to direct projection (slower) - line = Part.LineSegment(bot_point, top_point).toShape() - try: - import MeshPart - projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] - if len(projected) >= 2: - yy = [p.y for p in projected] - zz = [p.z for p in projected] - slope, intercept, *_ = stats.linregress(yy, zz) - z_top = slope * top_point.y + intercept - z_bot = slope * bot_point.y + intercept - else: - z_top = z_bot = 0 - except: - z_top = z_bot = 0 - - new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) - new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) - - new_pl = FreeCAD.Placement() - new_pl.Base = (new_top + new_bot) / 2 - new_pl.Rotation = FreeCAD.Rotation( - FreeCAD.Vector(-1, 0, 0), new_top - new_bot - ) - - results.append((frame_type, new_pl)) - - return results - - def adjustToTerrain(self, coordinates): - """Unified terrain adjustment function for both aligned and non-aligned arrays""" - # Create binary array efficiently - arr = np.array([[int(obj != 0) for obj in col] for col in coordinates], dtype=np.uint8) - labeled_array, num_features = sclabel(arr) - - # Build DataFrame efficiently - data = [] - for label in range(1, num_features + 1): - cols, rows = np.where(labeled_array == label) - for col, row in zip(cols, rows): - frame_type, placement = coordinates[col][row] - data.append({ - 'ID': len(data) + 1, - 'region': label, - 'type': frame_type, - 'column': col, - 'row': row, - 'placement': placement - }) - - if not data: - return pd.DataFrame(columns=['ID', 'region', 'type', 'column', 'row', 'placement']) - - df = pd.DataFrame(data) - - # Process terrain adjustments in batches - points_data = [(row['type'], row['placement']) for _, row in df.iterrows()] - adjusted_results = self._calculate_terrain_adjustment_batch(points_data) - - # Update placements in DataFrame - for idx, (frame_type, new_placement) in enumerate(adjusted_results): - df.at[idx, 'placement'] = new_placement - - return df - - def isInside(self, frame, point): - """Optimized inside check with early termination""" - if not self.Area.isInside(point, 1e-6, True): # Reduced tolerance for speed - return False - - frame_footprint = self._get_frame_footprint(frame) - frame_footprint.Placement.Base = point - - try: - cut = frame_footprint.cut([self.Area]) - return len(cut.Vertexes) == 0 - except: - return False - - def getAligments(self): - """Optimized alignment calculation""" - sel = FreeCADGui.Selection.getSelectionEx() - if not sel or not sel[0].SubObjects: - return np.array([]), np.array([]) - - sub_objects = sel[0].SubObjects - - if len(sub_objects) == 1: - refh = refv = sub_objects[0] - else: - # Choose references based on bounding box dimensions - refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength) - refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength) - - # Calculate ranges efficiently - startx = refv.BoundBox.XMin + self.offsetX - self.gap_col * int( - (refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col - ) - - starty = refh.BoundBox.YMin + self.offsetY + self.gap_row * int( - (refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row - ) - - x_range = np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.float64) - y_range = np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.float64) - - return x_range, y_range - - def calculateAlignedArray(self): - """Optimized aligned array calculation""" - pointsx, pointsy = self.getAligments() - if len(pointsx) == 0 or len(pointsy) == 0: - return pd.DataFrame() - - # Precompute footprints once - footprints = [] - for frame in self.FrameSetups: - footprint = self._get_frame_footprint(frame) - footprints.append((frame, footprint)) - - ref_frame, ref_footprint = footprints[0] - ref_length = ref_frame.Length.Value - ref_width = ref_frame.Width.Value - - # Corridor variables - countcols = 0 - valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - ref_width) - corridor_enabled = self.form.groupCorridor.isChecked() - - cols = [] - for x in pointsx: - col = [] - corridor_offset = 0 - - for y in pointsy: - point = FreeCAD.Vector(x + ref_width / 2 + corridor_offset, y - ref_length / 2, 0.0) - found = False - - # Check reference frame first (most common case) - if self.isInside(ref_frame, point): - col.append([ref_frame, point]) - found = True - else: - # Check alternative frames - for frame, footprint in footprints[1:]: - length_diff = (ref_frame.Length.Value - frame.Length.Value) / 2 - for offset in [length_diff, -length_diff]: - test_point = FreeCAD.Vector(point.x, point.y + offset, 0.0) - if self.isInside(frame, test_point): - col.append([frame, test_point]) - found = True - break - if found: - break - - if not found: - col.append(0) - - # Handle corridors - if corridor_enabled and col: - countcols += 1 - if countcols >= self.form.editColCount.value(): - corridor_offset += valcols - countcols = 0 - - cols.append(col) - - return self.adjustToTerrain(cols) - - def calculateNonAlignedArray(self): - """Optimized non-aligned array calculation""" - pointsx, pointsy = self.getAligments() - if len(pointsx) == 0: - FreeCAD.Console.PrintWarning("No X alignments found.\n") - return pd.DataFrame() - - # Precompute footprints - footprints = [] - for frame in self.FrameSetups: - footprint = self._get_frame_footprint(frame) - footprints.append((frame, footprint)) - - corridor_enabled = self.form.groupCorridor.isChecked() - corridor_count = 0 - corridor_offset = 0 - - cols = [] - for x in pointsx: - col = [] - current_x = x + corridor_offset - - # Create vertical line for intersection - p1 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMin, 0.0) - line = Part.LineSegment(p1, p2).toShape() - - # Get intersections with area - try: - inter = self.Area.section(line) - pts = sorted([v.Point for v in inter.Vertexes], key=lambda p: p.y, reverse=True) - - for i in range(0, len(pts) - 1, 2): - top, bottom = pts[i], pts[i + 1] - available_height = top.y - bottom.y - - if available_height > footprints[-1][0].Width.Value: - # Use optimized placement algorithm - self._place_frames_in_segment(col, footprints, current_x, top, bottom) - - except Exception as e: - FreeCAD.Console.PrintWarning(f"Error in segment processing: {e}\n") - - # Handle corridor offset - if corridor_enabled and col: - corridor_count += 1 - if corridor_count >= self.form.editColCount.value(): - corridor_offset += 12000 # 12m corridor - corridor_count = 0 - - cols.append(col) - - return self.adjustToTerrain(cols) - - def _place_frames_in_segment(self, col, footprints, x, top, bottom): - """Optimized frame placement within a segment""" - current_y = top.y - frame_heights = [ftp[0].Width.Value for ftp in footprints] - min_frame_height = min(frame_heights) - - while current_y - bottom.y > min_frame_height: - placed = False - - for frame, footprint in footprints: - test_y = current_y - frame.Width.Value / 2 - test_point = FreeCAD.Vector(x, test_y, 0.0) - - if self.isInside(frame, test_point): - col.append([frame, test_point]) - current_y -= frame.Width.Value - placed = True - break - - if not placed: - break - - def accept(self): - from datetime import datetime - starttime = datetime.now() - - # Document optimization - params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") - auto_save_enabled = params.GetBool("AutoSaveEnabled") - params.SetBool("AutoSaveEnabled", False) - FreeCAD.ActiveDocument.RecomputesFrozen = True - - try: - # Get selected frames - items = [ - FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text()) - for i in range(self.form.listFrameSetups.count()) - if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked - ] - - # Remove duplicates efficiently - self.FrameSetups = list({frame.Length.Value: frame for frame in items}.values()) - self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True) - - # Parse parameters - self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value - self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value - self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value - self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value - - FreeCAD.ActiveDocument.openTransaction("Create Placement") - - # Main processing - self.calculateWorkingArea() - - if self.form.cbAlignFrames.isChecked(): - dataframe = self.calculateAlignedArray() - else: - dataframe = self.calculateNonAlignedArray() - - if not dataframe.empty: - self.createFrameFromPoints(dataframe) - - # Group trackers - import Electrical.group as egroup - import importlib - importlib.reload(egroup) - egroup.groupTrackersToTransformers(5000000, self.gap_row) - - FreeCAD.ActiveDocument.commitTransaction() - - finally: - # Restore document settings - FreeCAD.ActiveDocument.RecomputesFrozen = False - params.SetBool("AutoSaveEnabled", auto_save_enabled) - - total_time = datetime.now() - starttime - print(f" -- Total time: {total_time}") - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - -def optimized_cut(L_total, piezas, margen=0, metodo='auto'): - """ - Encuentra la combinación óptima de piezas para minimizar el desperdicio, - considerando un margen entre piezas. - - Args: - L_total (int): Longitud total del material. - piezas (list): Lista de longitudes de los patrones de corte. - margen (int): Espacio perdido entre piezas consecutivas. - metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática. - - Returns: - tuple: (piezas_seleccionadas, desperdicio) - """ - # Filtrar piezas inválidas - piezas = [p for p in piezas if 0 < p <= L_total] - if not piezas: - return [], L_total - - # Transformar longitudes y longitud total con margen - longitudes_aumentadas = [p + margen for p in piezas] - L_total_aumentado = L_total + margen - - # Selección automática de método - if metodo == 'auto': - if L_total_aumentado <= 10000 and len(piezas) <= 100: - metodo = 'dp' - else: - metodo = 'greedy' - - if metodo == 'dp': - n = len(piezas) - dp = [0] * (L_total_aumentado + 1) - parent = [-1] * (L_total_aumentado + 1) # Almacena índices de piezas usadas - - # Llenar la tabla dp y parent - for j in range(1, L_total_aumentado + 1): - for i in range(n): - p_aum = longitudes_aumentadas[i] - if p_aum <= j: - if dp[j] < dp[j - p_aum] + p_aum: - dp[j] = dp[j - p_aum] + p_aum - parent[j] = i # Guardar índice de la pieza - - # Reconstruir solución desde el final - current = L_total_aumentado - seleccion_indices = [] - while current > 0 and parent[current] != -1: - i = parent[current] - seleccion_indices.append(i) - current -= longitudes_aumentadas[i] - - # Calcular desperdicio real - k = len(seleccion_indices) - if k == 0: - desperdicio = L_total - else: - suma_original = sum(piezas[i] for i in seleccion_indices) - desperdicio = L_total - suma_original - margen * (k - 1) - - return seleccion_indices, desperdicio - - elif metodo == 'greedy': - # Crear lista con índices y longitudes aumentadas - lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))] - lista_con_indices.sort(key=lambda x: x[0], reverse=True) # Ordenar descendente - - seleccion_indices = [] - restante = L_total_aumentado - - # Seleccionar piezas vorazmente - for p_aum, i in lista_con_indices: - while restante >= p_aum: - seleccion_indices.append(i) - restante -= p_aum - - # Calcular desperdicio real - k = len(seleccion_indices) - if k == 0: - desperdicio = L_total - else: - suma_original = sum(piezas[i] for i in seleccion_indices) - desperdicio = L_total - suma_original - margen * (k - 1) - - return seleccion_indices, desperdicio - - -# Ejemplo de uso -'''if __name__ == "__main__": - L_total = 100 - piezas = [25, 35, 40, 20, 15, 30, 50] - margen = 5 - - print("Solución óptima con margen (programación dinámica):") - seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp') - print(f"Piezas usadas: {seleccion}") - print(f"Margen entre piezas: {margen} cm") - print(f"Material útil: {sum(seleccion)} cm") - print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm") - print(f"Desperdicio total: {desperd} cm") - - print("\nSolución aproximada con margen (algoritmo voraz):") - seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy') - print(f"Piezas usadas: {seleccion_g}") - print(f"Margen entre piezas: {margen} cm") - print(f"Material útil: {sum(seleccion_g)} cm") - print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm") - print(f"Desperdicio total: {desperd_g} cm")''' - - -# ---------------------------------------------------------------------------------------------------------------------- -# function AdjustToTerrain -# Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh -# -# Inputs: -# 1. frames: group of objest to adjust -# ---------------------------------------------------------------------------------------------------------------------- -class adjustToTerrainTaskPanel: - def __init__(self, obj=None): - self.obj = obj - self.form = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantPlacementAdjust.ui") - - def accept(self): - frames = [] - for obj in FreeCADGui.Selection.getSelection(): - if obj.Name.startswith("Tracker"): - frames.append(obj) - elif obj.Name.startswith("FrameArea"): - frames.extend(obj.Frames) - adjustToTerrain(frames, self.form.comboMethod.currentIndex() == 0) - self.close() - return True - - def reject(self): - self.close() - return False - - def close(self): - FreeCADGui.Control.closeDialog() - -import numpy as np -from scipy import stats - -def get_trend(points): - """Return the trend of a list of 3D points""" - x, y, z = zip(*[(point.x, point.y, point.z) for point in points]) - slope, intercept, _, _, _ = stats.linregress(y, z) - new_z = slope * np.array([y[0], y[-1]]) + intercept - return [FreeCAD.Vector(x[0], y[0], new_z[0]), FreeCAD.Vector(x[-1], y[-1], new_z[1])] - -def getTrend(points): # old - from scipy import stats - def getNewZ(x): - return slope * x + intercept - - xx = list() - yy = list() - zz = list() - for point in points: - xx.append(point.x) - yy.append(point.y) - zz.append(point.z) - slope, intercept, r, p, std_err = stats.linregress(yy, zz) - newzz = list(map(getNewZ, [yy[0], yy[-1]])) - return [FreeCAD.Vector(xx[0], yy[0], newzz[0]), - FreeCAD.Vector(xx[-1], yy[-1], newzz[1])] - - -def adjustToTerrain(frames, individual=True): - from datetime import datetime - starttime = datetime.now() - - import MeshPart as mp - - FreeCAD.ActiveDocument.openTransaction("Adjust to terrain") - terrain = PVPlantSite.get().Terrain.Mesh - - if individual: - for frame in frames: - length = frame.Setup.Length.Value / 2 + 5000 - p1 = FreeCAD.Vector(-length, 0, 0, ) - p2 = FreeCAD.Vector(length, 0, 0, ) - line = Part.LineSegment(p1, p2).toShape() - line.Placement = frame.Placement.copy() - line.Placement.Base.z = 0 - xyz = line.Placement.Rotation.toEulerAngles("XYZ") - line.Placement.Rotation.setEulerAngles("XYZ", 0, 0, xyz[2]) - pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1)) - pts = [] - for points in pro: - pts.extend(points) - points3D = get_trend(pts) - - pl = FreeCAD.Placement() - pl.Base = (points3D[0] + points3D[1]) / 2 - rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) - pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) - frame.Placement = pl - else: - import math - def getLineAngle(line): - # ángulo en grados = arctan(ángulo en porcentaje / 100%) - import math - p1 = FreeCAD.Vector(line.Vertexes[0].Point) - p2 = FreeCAD.Vector(line.Vertexes[1].Point) - hi = p2.z - p1.z - p1.z = 0 - p2.z = 0 - le = p2.sub(p1).Length - return math.degrees(math.atan2(hi, le)) - - cols = getCols(frames) - for col in cols: - for group in col: - # Ver 1 ----------------- - lines = [] - # 1. Generar las líneas de trabajo. - for frame in group: - # 1.1. Corregir los frames que estén fuera de tolerancia: - if frame.AngleY < FreeCAD.ActiveDocument.MaximumTiltNegative.Value: - frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltNegative.Value - if frame.AngleY > FreeCAD.ActiveDocument.MaximumTiltPositive.Value: - frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltPositive.Value - - # 1.2. Generar las líneas con las que se trabajarán: - l = frame.Setup.Length / 2 - pn = FreeCAD.Vector(-l, 0, 0) - ps = FreeCAD.Vector( l, 0, 0) - line = Part.LineSegment(pn, ps).toShape() - line.Placement = frame.Placement.copy() - lines.append([frame, line]) - - # 2. Poner los tracker en tolerancia: - cnt = len(lines) - if cnt > 1: - angleLine=[] - anglesTwoLines=[] - for frame in lines: - angleLine.append(frame[0].AngleY.Value) - for ind in range(cnt - 1): - frame1 = lines[ind] - frame2 = lines[ind + 1] - vec1 = frame1[1].Vertexes[1].Point.sub(frame1[1].Vertexes[0].Point) - vec2 = frame2[1].Vertexes[1].Point.sub(frame2[1].Vertexes[0].Point) - anglesTwoLines.append(math.degrees(vec2.getAngle(vec1))) - print(angleLine) - print(anglesTwoLines) - pass - - for ind, frame in enumerate(lines): - frame0 = None - frame1 = None - if ind > 0: - frame0 = lines[ind - 1] - if ind < (len(group) - 1): - frame1 = lines[ind + 1] - - if (frame0 is None) and (frame1 is None): # Caso 1: sólo 1 frame por fila - # no se hace nada. ya está con todos los parámetros dentro de tolerancia - pass - elif (frame0 is None) and not (frame1 is None): # Caso 2: frame es el primero y hay más frames - pass - elif not (frame0 is None) and (frame1 is None): # Caso 3: el frame es el último y hay más frames - pass - else: # Caso 4: el frame está en el médio de varios frames - pass - - continue - - # Ver 0 ----------------- - points = [] - # 1. Get lines/points to project on land - frame1 = group[0] # Norte - frame2 = group[-1] # Sur - # 1.1. Get the first and last points: - - # TODO: revisar esta parte: - p0 = FreeCAD.Vector(frame1.Shape.BoundBox.Center.x, frame1.Shape.BoundBox.YMax, 0.0) - pf = FreeCAD.Vector(frame2.Shape.BoundBox.Center.x, frame2.Shape.BoundBox.YMin, 0.0) - - vec = (pf - p0).normalize() - points.append(p0) - for ind in range(0, len(group) - 1): - frame1 = group[ind] - frame2 = group[ind + 1] - vec1 = FreeCAD.Vector(frame1.Placement.Base) - vec2 = FreeCAD.Vector(frame2.Placement.Base) - vec1.z = 0 - vec2.z = 0 - vec3 = vec2.sub(vec1) - c = vec3.Length / 2 + (frame1.Setup.Length.Value - frame2.Setup.Length.Value) / 4 - v = FreeCAD.Vector(vec) - v.Length = c - v = vec1.add(v) - v.z = 0 - points.append(v) - points.append(pf) - - # 2. Calculate trend: - points3D = [] - for ind in range(len(points) - 1): - line = Part.LineSegment(points[ind], points[ind + 1]).toShape() - pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1)) - pts = [] - for lp in pro: - pts.extend(lp) - points3D.extend(get_trend(pts)) - # Todo: aplicar aproximación de los vertices: - # prueba: - for i in range(0, len(points3D) - 2, 2): - # p0 = points3D[i] - p1 = points3D[i + 1] - p2 = points3D[i + 2] - # p3 = points3D[i + 3] - - l = p1.sub(p2).Length - if l > 250: - l = (l - 250) / 2 - if p1.z > p2.z: - p1.z -= l - p2.z += l - else: - p1.z += l - p2.z -= l - - # 3. Aplicar placement - for ind, frame in enumerate(group): - v1 = points3D[ind * 2] - v2 = points3D[ind * 2 + 1] - pl = frame.Placement.copy() - pl.Base.z = (v1.add(v2) / 2).z - rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), v1.sub(v2)) - pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) - frame.Placement = pl - - FreeCAD.ActiveDocument.commitTransaction() - total_time = datetime.now() - starttime - print(" -- Tiempo tardado en ajustar al terreno:", total_time) - FreeCAD.activeDocument().recompute() - - -def getRows(objs): - ''' ''' - def countFrames(columns): - cnt = 0 - for icol in columns: - cnt += len(icol) - return cnt - - if len(objs) == 0: - return None, None - - cols = getCols(list(objs)) - tmpcols = [] - for col in cols: - g = [] - for group in col: - g.extend(group) - tmpcols.append(g) - - rows = [] - while countFrames(tmpcols) > 0: - firstCol = max(tmpcols, key=lambda col: col[0].Placement.Base.y) - compFrame = max(firstCol, key=lambda x: x.Placement.Base.y) - ind = tmpcols.index(firstCol) - group = [compFrame,] - tmpcols[ind].remove(compFrame) - for i in range(ind - 1, 0, -1): - if len(tmpcols[i]) == 0: - break - frame = tmpcols[i][0] - framelen = frame.Setup.Length / 2 - compFramelen = compFrame.Setup.Length / 2 - l = max([framelen, compFramelen]) - if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l: - group.append(frame) - tmpcols[i].remove(frame) - compFrame = frame - else: - break - - for i in range(ind + 1, len(cols)): - if len(tmpcols[i]) == 0: - break - frame = tmpcols[i][0] - framelen = frame.Setup.Length / 2 - compFramelen = compFrame.Setup.Length / 2 - l = max([framelen, compFramelen]) - if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l: - group.append(frame) - tmpcols[i].remove(frame) - compFrame = frame - else: - break - - if len(group) > 0: - group = sorted(group, key=lambda x: x.Placement.Base.x) - rows.append(group) - - return rows, cols - - -def getCols(objs): - def getRound(num): - return round(num / 100, 0) - xx = set(getRound(obj.Placement.Base.x) for obj in objs) - xx = sorted(xx) - columns = [] - - for x in xx: - # 1. identificar los objetos de una columna - tmpcol = [] - for obj in objs: - if getRound(obj.Placement.Base.x) == x: - tmpcol.append(obj) - tmpcol = sorted(tmpcol, key=lambda obj: getRound(obj.Placement.Base.y), reverse=True) - for obj in tmpcol: - objs.remove(obj) - - # 2. dividir los objetos en grupos: - group = [] - col = [] - for i, f2 in enumerate(tmpcol): - if i > 0: - f1 = group[-1] - d = abs(f1.Placement.Base.y - f2.Placement.Base.y) - \ - (f1.Setup.Length.Value + f2.Setup.Length.Value) / 2 - if d > 1000: - col.append(group.copy()) - group.clear() - group.append(f2) - col.append(group) - columns.append(col) - return columns - - -# en el caso de que no sean perpendiculares a x: - -def getCols_old(sel, tolerance=4000, sort=True): - # TODO: get only frames from de selection - if not sel: - return - if len(sel) == 0: - return - - cols = [] - while len(sel) > 0: - obj = sel[0] - p = obj.Shape.BoundBox.Center - vec = obj.Shape.SubShapes[1].SubShapes[1].BoundBox.Center - \ - obj.Shape.SubShapes[1].SubShapes[0].BoundBox.Center - n = FreeCAD.Vector(vec.y, -vec.x, 0) - - # 1. Detectar los objetos que están en una misma columna - col = [] - newsel = [] - for obj1 in sel: - if obj1.Shape.BoundBox.isCutPlane(p, n): # todo: esto no es del todo correcto. buscar otra manera - col.append(obj1) - else: - newsel.append(obj1) - sel = newsel.copy() - col = sorted(col, key=lambda k: k.Placement.Base.y, reverse=True) # Orden Norte - Sur (Arriba a abajo) - - # 2. Detectar y separar los grupos dentro de una misma columna: - group = [] - newcol = [] - group.append(col[0]) - if len(col) > 1: - for ind in range(0, len(col) - 1): - vec1 = FreeCAD.Vector(col[ind].Placement.Base) - vec1.z = 0 - vec2 = FreeCAD.Vector(col[ind + 1].Placement.Base) - vec2.z = 0 - distance = abs((vec1 - vec2).Length) - (col[ind].Setup.Width.Value + col[ind + 1].Setup.Width.Value) / 2 - if distance > tolerance: - newcol.append(group.copy()) - group.clear() - group.append(col[ind + 1]) - newcol.append(group) - cols.append(newcol) - - if sort: - cols = sorted(cols, key=lambda k: k[0][0].Placement.Base.x, reverse=False) - - return cols - - -# ----------------------------------------------------------------------------------------------------------------------- -# Convert -# ----------------------------------------------------------------------------------------------------------------------- -class _PVPlantConvertTaskPanel: - '''The editmode TaskPanel for Conversions''' - - def __init__(self): - self.To = None - # self.form: - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacementConvert.ui")) - self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "Trace.svg"))) - self.form.buttonTo.clicked.connect(self.addTo) - - def addTo(self): - sel = FreeCADGui.Selection.getSelection() - if len(sel) > 0: - self.To = sel[0] - self.form.editTo.setText(self.To.Label) - - def accept(self): - sel = FreeCADGui.Selection.getSelection() - if sel == self.To: - return False - if len(sel) > 0 and self.To is not None: - FreeCAD.ActiveDocument.openTransaction("Convert to") - ConvertObjectsTo(sel, self.To) - return True - return False - - -def getHeadsAndSoil(frame=None): - if frame == None: - return None - import MeshPart as mp - - data = {"heads": [], - "soil": []} - poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes - for pole in poles: - vec = pole.BoundBox.Center - vec.z = pole.BoundBox.ZMax - data["heads"].append(vec) - - data["soil"].extend(mp.projectPointsOnMesh(data["heads"], - FreeCAD.ActiveDocument.Terrain.Mesh, - FreeCAD.Vector(0, 0, 1))) - return data - - -def moveFrameHead(obj, head=0, dist=0): - import math - print(dist) - dir = 1 if head == 0 else -1 - base = obj.Placement.Base - dist /= 2 - base.z += dist - angles = obj.Placement.Rotation.toEulerAngles("XYZ") - angley = math.degrees(math.asin(dist/(obj.Setup.Length.Value / 2))) * dir - print(angley) - rot = FreeCAD.Rotation(angles[2], angles[1] + angley, angles[0]) - obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0, 0, 0)) - obj.recompute() - -# --------------------------------------------------------------------------------------------------------------------- -# function ConvertObjectsTo -# -# --------------------------------------------------------------------------------------------------------------------- -def ConvertObjectsTo(sel, objTo): - if hasattr(objTo, "Proxy"): - isFrame = objTo.Proxy.__class__ is PVPlantRack._Tracker - # isFrame = issubclass(objTo.Proxy.__class__, PVPlantRack._Frame) - isFrame = True - - for obj in sel: - if isFrame: - if hasattr(obj, "Proxy"): - if obj.Proxy.__class__ is PVPlantRack._Tracker: - # if issubclass(obj.Proxy.__class__, PVPlantRack._Frame): # 1. Si los dos son Frames - cp = FreeCAD.ActiveDocument.copyObject(objTo, False) - cp.Placement = obj.Placement - cp.CloneOf = objTo - else: # 2. De un objeto no Frame a Frame - place = FreeCAD.Placement() # obj.Placement - place.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), - 90) # TODO: rotar conforme a lados más largos - bb = None - if obj.isDerivedFrom("Part::Feature"): - bb = obj.Shape.BoundBox - elif obj.isDerivedFrom("Mesh::Feature"): - bb = obj.Mesh.BoundBox - place.Base = bb.Center - cp = FreeCAD.ActiveDocument.copyObject(objTo, False) - cp.Placement = place - if isFrame: - cp.CloneOf = objTo - else: # 3. De un objeto a otro objeto (cualesquieran que sean) - place = FreeCAD.Placement() # obj.Placement - bb = None - if obj.isDerivedFrom("Part::Feature"): - bb = obj.Shape.BoundBox - elif obj.isDerivedFrom("Mesh::Feature"): - bb = obj.Mesh.BoundBox - place.Base = bb.Center - cp = FreeCAD.ActiveDocument.copyObject(objTo, False) - cp.Placement = place - if isFrame: - cp.CloneOf = objTo - FreeCAD.ActiveDocument.removeObject(obj.Name) - FreeCAD.activeDocument().recompute() - - -## Comandos: ----------------------------------------------------------------------------------------------------------- -class CommandPVPlantPlacement: - - def GetResources(self): - return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "way.svg")), - 'Accel': "P,P", - 'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"), - 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")} - - def Activated(self): - taskd = _PVPlantPlacementTaskPanel(None) - FreeCADGui.Control.showDialog(taskd) - - def IsActive(self): - if FreeCAD.ActiveDocument: - return True - else: - return False - - -class CommandAdjustToTerrain: - - def GetResources(self): - return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "adjust.svg")), - 'Accel': "P, A", - 'MenuText': QT_TRANSLATE_NOOP("Placement", "Adjust"), - 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Adjust object to terrain")} - - def Activated(self): - sel = FreeCADGui.Selection.getSelection() - if len(sel) > 0: - # adjustToTerrain(sel) - FreeCADGui.Control.showDialog(adjustToTerrainTaskPanel()) - else: - print("No selected object") - - def IsActive(self): - if FreeCAD.ActiveDocument: - return True - else: - return False - - -class CommandConvert: - def GetResources(self): - return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "convert.svg")), - 'Accel': "P, C", - 'MenuText': QT_TRANSLATE_NOOP("Placement", "Convert"), - 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Convertir un objeto en otro")} - - def IsActive(self): - return (not FreeCAD.ActiveDocument is None and - not FreeCAD.ActiveDocument.getObject("Site") is None and - not FreeCAD.ActiveDocument.getObject("Terrain") is None and - not FreeCAD.ActiveDocument.getObject("TrackerSetup") is None) - - def Activated(self): - taskd = _PVPlantConvertTaskPanel() - FreeCADGui.Control.showDialog(taskd) - - -'''if FreeCAD.GuiUp: - FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement()) - FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain()) - FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())''' +from PVPlant.placement.placement import ( + _PVPlantPlacementTaskPanel_old, + _PVPlantPlacementTaskPanel_new1, + selectionFilter, +) \ No newline at end of file