# /********************************************************************** # * * # * 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 utm 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) # self.layout.addWidget(self.view, 1, 0, 1, 3) # -- Latitud y longitud: self.labelCoordinates = QtGui.QLabel() self.labelCoordinates.setFixedHeight(21) LeftLayout.addWidget(self.labelCoordinates) # self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3) # Right Widgets: labelKMZ = QtGui.QLabel() labelKMZ.setText("Cargar un archivo KMZ/KML:") self.kmlButton = QtGui.QPushButton() self.kmlButton.setFixedSize(32, 32) self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg"))) widget = QtGui.QWidget(self) layout = QtGui.QHBoxLayout(widget) widget.setLayout(layout) layout.addWidget(labelKMZ) layout.addWidget(self.kmlButton) RightLayout.addWidget(widget) # ----------------------- self.groupbox = QtGui.QGroupBox("Importar datos desde:") self.groupbox.setCheckable(True) self.groupbox.setChecked(True) radio1 = QtGui.QRadioButton("Google Elevation") radio2 = QtGui.QRadioButton("Nube de Puntos") radio3 = QtGui.QRadioButton("Datos GPS") radio1.setChecked(True) # buttonDialog = QtGui.QPushButton('...') # buttonDialog.setEnabled(False) vbox = QtGui.QVBoxLayout(self) vbox.addWidget(radio1) vbox.addWidget(radio2) vbox.addWidget(radio3) self.groupbox.setLayout(vbox) RightLayout.addWidget(self.groupbox) # ------------------------ self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS") RightLayout.addWidget(self.checkboxImportGis) self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital") RightLayout.addWidget(self.checkboxImportSatelitalImagen) verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) RightLayout.addItem(verticalSpacer) self.bAccept = QtGui.QPushButton('Accept') self.bAccept.clicked.connect(self.onAcceptClick) RightLayout.addWidget(self.bAccept) # signals/slots QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML) def onLoadFinished(self): file = os.path.join(DirResources, "webs", "map.js") frame = self.view.page() with open(file, 'r') as f: frame.runJavaScript(f.read()) def onSearch(self): if self.valueSearch.text() == "": return from geopy.geocoders import Nominatim geolocator = Nominatim(user_agent="http") location = geolocator.geocode(self.valueSearch.text()) self.valueSearch.setText(location.address) self.panMap(location.longitude, location.latitude, location.raw['boundingbox']) def onAcceptClick(self): frame = self.view.page() # 1. georeferenciar frame.runJavaScript( "MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);" ) # 2. importar todos los elementos dibujados: frame.runJavaScript( "var data = drawnItems.toGeoJSON();" "MyApp.shapes(JSON.stringify(data));" ) self.close() @QtCore.Slot(float, float) def onMapMove(self, lat, lng): self.lat = lat self.lon = lng x, y, zone_number, zone_letter = utm.from_latlon(lat, lng) self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) + ' | UTM: ' + str(zone_number) + zone_letter + ', {:.5f}m E, {:.5f}m N'.format(x, y)) @QtCore.Slot(float, float, float, float, int) def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom): self.minLat = min([minLat, maxLat]) self.maxLat = max([minLat, maxLat]) self.minLon = min([minLon, maxLon]) self.maxLon = max([minLon, maxLon]) self.zoom = zoom @QtCore.Slot(float, float) def georeference(self, lat, lng): import PVPlantSite from geopy.geocoders import Nominatim self.georeference_coordinates['lat'] = lat self.georeference_coordinates['lon'] = lng Site = PVPlantSite.get(create=True) Site.Proxy.setLatLon(lat, lng) geolocator = Nominatim(user_agent="http") location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng)) if location: if location.raw["address"].get("road"): str = location.raw["address"]["road"] if location.raw["address"].get("house_number"): str += ' ({0})'.format(location.raw["address"]["house_number"]) Site.Address = str if location.raw["address"].get("city"): Site.City = location.raw["address"]["city"] if location.raw["address"].get("postcode"): Site.PostalCode = location.raw["address"]["postcode"] if location.raw["address"].get("address"): Site.Region = '{0}'.format(location.raw["address"]["province"]) if location.raw["address"].get("state"): if Site.Region != "": Site.Region += " - " Site.Region += '{0}'.format(location.raw["address"]["state"]) # province - state Site.Country = location.raw["address"]["country"] @QtCore.Slot(str) def shapes(self, drawnItems): import geojson import PVPlantImportGrid as ImportElevation import Draft import PVPlantSite Site = PVPlantSite.get() offset = FreeCAD.Vector(0, 0, 0) if not (self.lat is None or self.lon is None): offset = FreeCAD.Vector(Site.Origin) offset.z = 0 items = geojson.loads(drawnItems) for item in items['features']: if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle: coord = item['geometry']['coordinates'] point = ImportElevation.getElevationFromOE([[coord[0], coord[1]],]) c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset) if item['properties'].get('radius'): r = round(item['properties']['radius'] * 1000, 0) p = FreeCAD.Placement() p.Base = c obj = Draft.makeCircle(r, placement=p, face=False) else: ''' do something ''' obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10) else: # 2. if the feature is a Polygon or Line: cw = False name = "Línea" lp = item['geometry']['coordinates'] if item['geometry']['type'] == "Polygon": cw = True name = "Area" lp = item['geometry']['coordinates'][0] pts = [[cords[1], cords[0]] for cords in lp] tmp = ImportElevation.getElevationFromOE(pts) pts = [p.sub(offset) for p in tmp] obj = Draft.makeWire(pts, closed=cw, face=False) #obj.Placement.Base = Site.Origin obj.Label = name Draft.autogroup(obj) if item['properties'].get('name'): obj.Label = item['properties']['name'] if self.checkboxImportGis.isChecked(): self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon) if self.checkboxImportSatelitalImagen.isChecked(): # Usar los límites reales del terreno (rectangular) '''s_lat = self.minLat s_lon = self.minLon n_lat = self.maxLat n_lon = self.maxLon # Obtener puntos UTM para las esquinas corners = ImportElevation.getElevationFromOE([ [s_lat, s_lon], # Esquina suroeste [n_lat, s_lon], # Esquina sureste [n_lat, n_lon], # Esquina noreste [s_lat, n_lon] # Esquina noroeste ]) if not corners or len(corners) < 4: FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas\n") return # Descargar imagen satelital from lib.GoogleSatelitalImageDownload import GoogleMapDownloader downloader = GoogleMapDownloader( zoom= 18, #self.zoom, layer='raw_satellite' ) img = downloader.generateImage( sw_lat=s_lat, sw_lng=s_lon, ne_lat=n_lat, ne_lng=n_lon ) # Guardar imagen en el directorio del documento doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else "" if not doc_path: doc_path = FreeCAD.ConfigGet("UserAppData") filename = os.path.join(doc_path, "background.jpeg") img.save(filename) ancho, alto = img.size # Crear objeto de imagen en FreeCAD doc = FreeCAD.ActiveDocument img_obj = doc.addObject('Image::ImagePlane', 'Background') img_obj.ImageFile = filename img_obj.Label = 'Background' # Calcular dimensiones en metros usando las coordenadas UTM # Extraer las coordenadas de las esquinas sw = corners[0] # Suroeste se = corners[1] # Sureste ne = corners[2] # Noreste nw = corners[3] # Noroeste # Calcular ancho (promedio de los lados superior e inferior) width_bottom = se.x - sw.x width_top = ne.x - nw.x width_m = (width_bottom + width_top) / 2 # Calcular alto (promedio de los lados izquierdo y derecho) height_left = nw.y - sw.y height_right = ne.y - se.y height_m = (height_left + height_right) / 2 img_obj.XSize = width_m img_obj.YSize = height_m # Posicionar el centro de la imagen en (0,0,0) img_obj.Placement.Base = FreeCAD.Vector(-width_m / 2, -height_m / 2, 0)''' # Definir área rectangular s_lat = self.minLat s_lon = self.minLon n_lat = self.maxLat n_lon = self.maxLon # Obtener puntos UTM para las esquinas y el punto de referencia points = [ [s_lat, s_lon], # Suroeste [n_lat, n_lon], # Noreste [self.georeference_coordinates['lat'], self.georeference_coordinates['lon']] # Punto de referencia ] utm_points = ImportElevation.getElevationFromOE(points) if not utm_points or len(utm_points) < 3: FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas y referencia\n") return sw_utm, ne_utm, ref_utm = utm_points # Descargar imagen satelital from lib.GoogleSatelitalImageDownload import GoogleMapDownloader downloader = GoogleMapDownloader( zoom=self.zoom, layer='raw_satellite' ) img = downloader.generateImage( sw_lat=s_lat, sw_lng=s_lon, ne_lat=n_lat, ne_lng=n_lon ) # Guardar imagen doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else "" if not doc_path: doc_path = FreeCAD.ConfigGet("UserAppData") filename = os.path.join(doc_path, "background.jpeg") img.save(filename) # Calcular dimensiones reales en metros width_m = ne_utm.x - sw_utm.x # Ancho en metros (este-oeste) height_m = ne_utm.y - sw_utm.y # Alto en metros (norte-sur) # Calcular posición relativa del punto de referencia dentro de la imagen rel_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5 rel_y = (ref_utm.y - sw_utm.y) / height_m if height_m != 0 else 0.5 # Crear objeto de imagen en FreeCAD doc = FreeCAD.ActiveDocument img_obj = doc.addObject('Image::ImagePlane', 'Background') img_obj.ImageFile = filename img_obj.Label = 'Background' # Convertir dimensiones a milímetros (FreeCAD trabaja en mm) img_obj.XSize = width_m * 1000 img_obj.YSize = height_m * 1000 # Posicionar para que el punto de referencia esté en (0,0,0) # La esquina inferior izquierda debe estar en: # x = -rel_x * ancho_total # y = -rel_y * alto_total img_obj.Placement.Base = FreeCAD.Vector( -rel_x * width_m * 1000, -rel_y * height_m * 1000, 0 ) # Refrescar el documento doc.recompute() def calculate_texture_transform(self, mesh_obj, width_m, height_m): """Calcula la transformación precisa para la textura""" try: # Obtener coordenadas reales de las esquinas import utm sw = utm.from_latlon(self.minLat, self.minLon) ne = utm.from_latlon(self.maxLat, self.maxLon) # Crear matriz de transformación scale_x = (ne[0] - sw[0]) / width_m scale_y = (ne[1] - sw[1]) / height_m # Aplicar transformación (solo si se usa textura avanzada) if hasattr(mesh_obj.ViewObject, "TextureMapping"): mesh_obj.ViewObject.TextureMapping = "PLANE" mesh_obj.ViewObject.TextureScale = (scale_x, scale_y) mesh_obj.ViewObject.TextureOffset = (sw[0], sw[1]) except Exception as e: FreeCAD.Console.PrintWarning(f"No se pudo calcular transformación: {str(e)}\n") def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon): import Importer.importOSM as importOSM import PVPlantSite site = PVPlantSite.get() offset = FreeCAD.Vector(0, 0, 0) if not (self.lat is None or self.lon is None): offset = FreeCAD.Vector(site.Origin) offset.z = 0 importer = importOSM.OSMImporter(offset) osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}") importer.process_osm_data(osm_data) '''FreeCAD.activeDocument().recompute() FreeCADGui.updateGui() FreeCADGui.SendMsgToActiveView("ViewFit")''' def panMap_old(self, lng, lat, geometry=""): frame = self.view.page() bbox = "[{0}, {1}], [{2}, {3}]".format(float(geometry[0]), float(geometry[2]), float(geometry[1]), float(geometry[3])) command = 'map.panTo(L.latLng({lt}, {lg}));'.format(lt=lat, lg=lng) command += 'map.fitBounds([{box}]);'.format(box=bbox) frame.runJavaScript(command) # deepseek def panMap(self, lng, lat, geometry=None): frame = self.view.page() # 1. Validación del parámetro geometry if not geometry or len(geometry) < 4: # Pan básico sin ajuste de bounds command = f'map.panTo(L.latLng({lat}, {lng}));' else: try: # 2. Mejor manejo de coordenadas (Leaflet usa [lat, lng]) # Asumiendo que geometry es [min_lng, min_lat, max_lng, max_lat] southwest = f"{float(geometry[1])}, {float(geometry[0])}" # min_lat, min_lng northeast = f"{float(geometry[3])}, {float(geometry[2])}" # max_lat, max_lng command = f'map.panTo(L.latLng({lat}, {lng}));' command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));' except (IndexError, ValueError, TypeError) as e: print(f"Error en geometry: {str(e)}") command = f'map.panTo(L.latLng({lat}, {lng}));' frame.runJavaScript(command) def importKML(self): file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0] from lib.kml2geojson import kmz_convert layers = kmz_convert(file, "", ) frame = self.view.page() for layer in layers: command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer) frame.runJavaScript(command) class CommandPVPlantGeoreferencing: def GetResources(self): return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")), 'Accel': "G, R", 'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"), 'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")} def Activated(self): self.form = MapWindow() self.form.show() def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False '''if FreeCAD.GuiUp: FreeCADGui.addCommand('PVPlantGeoreferencing',_CommandPVPlantGeoreferencing()) '''