Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57f85d0153 | |||
| d9b39ac17b | |||
| 3bcdc95978 | |||
| 4b7035e6be | |||
| 02758a6ee8 | |||
| 111df89033 |
+2
-1
@@ -971,7 +971,8 @@ class _PVPlantExportDXF(QtGui.QWidget):
|
||||
if FreeCAD.ActiveDocument.Transport:
|
||||
for road in FreeCAD.ActiveDocument.Transport.Group:
|
||||
base = exporter.createPolyline(road, "CIVIL External Roads")
|
||||
base.dxf.const_width = road.Width
|
||||
if hasattr(road, 'Width'):
|
||||
base.dxf.const_width = road.Width
|
||||
|
||||
axis = exporter.createPolyline(road, "CIVIL External Roads Axis")
|
||||
axis.dxf.const_width = .2
|
||||
|
||||
+149
-46
@@ -7,7 +7,6 @@ import ssl
|
||||
import certifi
|
||||
import urllib.request
|
||||
import math
|
||||
import utm
|
||||
from collections import defaultdict
|
||||
import PVPlantImportGrid as ImportElevation
|
||||
|
||||
@@ -43,53 +42,74 @@ class OSMImporter:
|
||||
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
def transform_from_latlon(self, coordinates):
|
||||
"""Transforma coordenadas lat/lon a coordenadas FreeCAD"""
|
||||
if not coordinates:
|
||||
return []
|
||||
|
||||
points = ImportElevation.getElevationFromOE(coordinates)
|
||||
pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points]
|
||||
return pts
|
||||
|
||||
def get_osm_data(self, bbox):
|
||||
query = f"""
|
||||
[out:xml][bbox:{bbox}];
|
||||
(
|
||||
way["building"];
|
||||
way["highway"];
|
||||
way["railway"];
|
||||
way["power"="line"];
|
||||
way["power"="substation"];
|
||||
way["natural"="water"];
|
||||
way["landuse"="forest"];
|
||||
node["natural"="tree"];
|
||||
);
|
||||
(._;>;);
|
||||
out body;
|
||||
"""
|
||||
req = urllib.request.Request(
|
||||
self.overpass_url,
|
||||
data=query.encode('utf-8'),
|
||||
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
|
||||
method='POST'
|
||||
)
|
||||
return urllib.request.urlopen(req, context=self.ssl_context, timeout=160).read()
|
||||
""" Obtiene datos de OpenStreetMap """
|
||||
# Modificar la consulta en get_osm_data para incluir más tipos de agua:
|
||||
query = f"""[out:xml][bbox:{bbox}];
|
||||
(
|
||||
way["building"];
|
||||
way["highway"];
|
||||
way["railway"];
|
||||
way["power"="line"];
|
||||
way["power"="substation"];
|
||||
way["natural"="water"];
|
||||
way["waterway"];
|
||||
way["waterway"="river"];
|
||||
way["waterway"="stream"];
|
||||
way["waterway"="canal"];
|
||||
way["landuse"="basin"];
|
||||
way["landuse"="reservoir"];
|
||||
node["natural"="tree"];
|
||||
way["landuse"="forest"];
|
||||
way["landuse"="farmland"];
|
||||
);
|
||||
(._;>;);
|
||||
out body;
|
||||
"""
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.overpass_url,
|
||||
data=query.encode('utf-8'),
|
||||
#headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
|
||||
method='POST'
|
||||
)
|
||||
|
||||
response = urllib.request.urlopen(req, context=self.ssl_context, timeout=160)
|
||||
return response.read()
|
||||
except Exception as e:
|
||||
print(f"Error obteniendo datos OSM: {str(e)}")
|
||||
return None
|
||||
|
||||
def create_layer(self, name):
|
||||
"""Crea o obtiene una capa en el documento"""
|
||||
if not FreeCAD.ActiveDocument.getObject(name):
|
||||
return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
|
||||
return FreeCAD.ActiveDocument.getObject(name)
|
||||
|
||||
def process_osm_data(self, osm_data):
|
||||
"""Procesa los datos XML de OSM"""
|
||||
if not osm_data:
|
||||
print("No hay datos OSM para procesar")
|
||||
return
|
||||
|
||||
root = ET.fromstring(osm_data)
|
||||
|
||||
# Primero, recolectar todos los nodos
|
||||
print(f"Procesando {len(root.findall('node'))} nodos...")
|
||||
|
||||
# Almacenar nodos transformados
|
||||
coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')]
|
||||
coordinates = self.transform_from_latlon(coordinates)
|
||||
for i, node in enumerate(root.findall('node')):
|
||||
self. nodes[node.attrib['id']] = coordinates[i]
|
||||
'''return
|
||||
for node in root.findall('node'):
|
||||
self.nodes[node.attrib['id']] = self.transform_from_latlon(
|
||||
float(node.attrib['lat']),
|
||||
float(node.attrib['lon'])
|
||||
)'''
|
||||
self.nodes[node.attrib['id']] = coordinates[i]
|
||||
|
||||
# Procesar ways
|
||||
for way in root.findall('way'):
|
||||
@@ -166,7 +186,7 @@ class OSMImporter:
|
||||
def create_buildings(self):
|
||||
building_layer = self.create_layer("Buildings")
|
||||
for way_id, data in self.ways_data.items():
|
||||
print(data)
|
||||
#print(data)
|
||||
if 'building' not in data['tags']:
|
||||
continue
|
||||
|
||||
@@ -226,11 +246,11 @@ class OSMImporter:
|
||||
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
|
||||
|
||||
if 'power' in tags:
|
||||
print("\n\n")
|
||||
print(tags)
|
||||
#print("\n\n")
|
||||
#print(tags)
|
||||
feature_type = tags['power']
|
||||
if feature_type == 'line':
|
||||
print("3.1. Create Power Lines")
|
||||
#print("3.1. Create Power Lines")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_power_line(
|
||||
nodes=nodes,
|
||||
@@ -239,7 +259,7 @@ class OSMImporter:
|
||||
)
|
||||
|
||||
elif feature_type == 'substation':
|
||||
print("3.1. Create substations")
|
||||
#print("3.1. Create substations")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_substation(
|
||||
way_id=way_id,
|
||||
@@ -249,7 +269,7 @@ class OSMImporter:
|
||||
)
|
||||
|
||||
elif feature_type == 'tower':
|
||||
print("3.1. Create power towers")
|
||||
#print("3.1. Create power towers")
|
||||
FreeCADGui.updateGui()
|
||||
self.create_power_tower(
|
||||
position=nodes[0] if nodes else None,
|
||||
@@ -562,13 +582,15 @@ class OSMImporter:
|
||||
if polygon_points[0] != polygon_points[-1]:
|
||||
polygon_points.append(polygon_points[0])
|
||||
|
||||
|
||||
# 3. Base del terreno
|
||||
base_height = 0.3
|
||||
try:
|
||||
base_shape = Part.makePolygon(polygon_points)
|
||||
base_face = Part.Face(base_shape)
|
||||
base_extrude = base_face.extrude(FreeCAD.Vector(0, 0, base_height))
|
||||
base_obj = layer.addObject("Part::Feature", f"{name}_Base")
|
||||
base_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Base")
|
||||
layer.addObject(base_obj)
|
||||
base_obj.Shape = base_extrude
|
||||
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
|
||||
except Exception as e:
|
||||
@@ -583,7 +605,8 @@ class OSMImporter:
|
||||
fence_shape = Part.makePolygon(fence_points)
|
||||
fence_face = Part.Face(fence_shape)
|
||||
fence_extrude = fence_face.extrude(FreeCAD.Vector(0, 0, 2.8))
|
||||
fence_obj = layer.addObject("Part::Feature", f"{name}_Fence")
|
||||
fence_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Fence")
|
||||
layer.addObject(fence_obj)
|
||||
fence_obj.Shape = fence_extrude
|
||||
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
|
||||
except Exception as e:
|
||||
@@ -599,14 +622,15 @@ class OSMImporter:
|
||||
building_shape = Part.makePolygon(building_points)
|
||||
building_face = Part.Face(building_shape)
|
||||
building_extrude = building_face.extrude(FreeCAD.Vector(0, 0, building_height))
|
||||
building_obj = layer.addObject("Part::Feature", f"{name}_Building")
|
||||
building_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Building")
|
||||
layer.addObject(building_obj)
|
||||
building_obj.Shape = building_extrude
|
||||
building_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Error edificio {way_id}: {str(e)}\n")
|
||||
|
||||
# 6. Transformadores
|
||||
try:
|
||||
'''try:
|
||||
num_transformers = int(tags.get('transformers', 1))
|
||||
for i in range(num_transformers):
|
||||
transformer_pos = self.calculate_equipment_position(
|
||||
@@ -618,11 +642,11 @@ class OSMImporter:
|
||||
transformer = self.create_transformer(
|
||||
position=transformer_pos,
|
||||
voltage=voltage,
|
||||
tech_type=tags.get('substation:type', 'outdoor')
|
||||
technology=tags.get('substation:type', 'outdoor')
|
||||
)
|
||||
layer.addObject(transformer)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")
|
||||
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")'''
|
||||
|
||||
# 7. Torre de seccionamiento para alta tensión
|
||||
if substation_type == 'transmission' and voltage >= 110000:
|
||||
@@ -637,7 +661,8 @@ class OSMImporter:
|
||||
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
|
||||
|
||||
# 8. Propiedades técnicas
|
||||
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
|
||||
substation_data = FreeCAD.ActiveDocument.addObject("App::FeaturePython", f"{name}_Data")
|
||||
layer.addObject(substation_data)
|
||||
props = {
|
||||
"Voltage": voltage,
|
||||
"Type": substation_type,
|
||||
@@ -651,7 +676,8 @@ class OSMImporter:
|
||||
else:
|
||||
substation_data.addProperty(
|
||||
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
|
||||
prop, "Technical").setValue(value)
|
||||
prop, "Technical")
|
||||
setattr(substation_data, prop, value)
|
||||
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
|
||||
@@ -900,9 +926,9 @@ class OSMImporter:
|
||||
if face.isInside(rand_point, 0.1, True):
|
||||
return rand_point
|
||||
|
||||
def create_water_bodies(self):
|
||||
def create_water_bodies_old(self):
|
||||
water_layer = self.create_layer("Water")
|
||||
|
||||
print(self.ways_data)
|
||||
for way_id, data in self.ways_data.items():
|
||||
if 'natural' in data['tags'] and data['tags']['natural'] == 'water':
|
||||
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
|
||||
@@ -915,3 +941,80 @@ class OSMImporter:
|
||||
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
|
||||
water.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
|
||||
def create_water_bodies(self):
|
||||
|
||||
water_layer = self.create_layer("Water")
|
||||
|
||||
for way_id, data in self.ways_data.items():
|
||||
tags = data['tags']
|
||||
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
|
||||
|
||||
if len(nodes) < 2:
|
||||
continue
|
||||
|
||||
# ===== 1) RÍOS / CANALES (líneas) =====
|
||||
name = self.get_osm_name(tags, tags["waterway"])
|
||||
if 'waterway' in tags:
|
||||
if len(nodes) < 2:
|
||||
continue
|
||||
|
||||
try:
|
||||
width = self.parse_width(tags, default=2.0)
|
||||
points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
|
||||
wire = Draft.make_wire(points, closed=False, face=False)
|
||||
wire.Label = f"{name} ({tags['waterway']})"
|
||||
|
||||
wire.ViewObject.LineWidth = max(1, int(width * 0.5))
|
||||
wire.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
|
||||
water_layer.addObject(wire)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creando waterway {way_id}: {e}")
|
||||
|
||||
continue # importante
|
||||
|
||||
# ===== 2) LAGOS / EMBALSES (polígonos) =====
|
||||
is_area_water = (
|
||||
tags.get('natural') == 'water' or
|
||||
tags.get('landuse') in ['reservoir', 'basin'] or
|
||||
tags.get('water') is not None
|
||||
)
|
||||
|
||||
if not is_area_water or len(nodes) < 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
polygon_points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
|
||||
|
||||
if polygon_points[0] != polygon_points[-1]:
|
||||
polygon_points.append(polygon_points[0])
|
||||
|
||||
polygon = Part.makePolygon(polygon_points)
|
||||
face = Part.Face(polygon)
|
||||
|
||||
water = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Water_{way_id}")
|
||||
water.Shape = face
|
||||
water.ViewObject.ShapeColor = self.feature_colors['water']
|
||||
water.Label = f"{name} ({tags['waterway']})"
|
||||
|
||||
water_layer.addObject(water)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creando área de agua {way_id}: {e}")
|
||||
|
||||
def get_osm_name(self, tags, fallback=""):
|
||||
for key in ["name", "name:es", "name:en", "alt_name", "ref"]:
|
||||
if key in tags and tags[key].strip():
|
||||
return tags[key]
|
||||
return fallback
|
||||
|
||||
def parse_width(self, tags, default=2.0):
|
||||
for key in ["width", "est_width"]:
|
||||
if key in tags:
|
||||
try:
|
||||
w = tags[key].replace("m", "").strip()
|
||||
return float(w)
|
||||
except:
|
||||
pass
|
||||
return default
|
||||
|
||||
+11
-4
@@ -39,9 +39,11 @@ class PVPlantWorkbench(Workbench):
|
||||
ToolTip = "Workbench for PV design"
|
||||
Icon = str(os.path.join(DirIcons, "icon.svg"))
|
||||
|
||||
def __init__(self):
|
||||
''' init '''
|
||||
|
||||
def Initialize(self):
|
||||
|
||||
#sys.path.append(r"C:\Users\javie\AppData\Roaming\FreeCAD\Mod")
|
||||
sys.path.append(os.path.join(FreeCAD.getUserAppDataDir(), 'Mod'))
|
||||
import PVPlantTools, reload
|
||||
|
||||
@@ -144,7 +146,10 @@ class PVPlantWorkbench(Workbench):
|
||||
from widgets import CountSelection
|
||||
|
||||
def Activated(self):
|
||||
"This function is executed when the workbench is activated"
|
||||
"""This function is executed when the workbench is activated"""
|
||||
|
||||
FreeCAD.Console.PrintLog("Road workbench activated.\n")
|
||||
|
||||
import SelectionObserver
|
||||
import FreeCADGui
|
||||
|
||||
@@ -153,7 +158,9 @@ class PVPlantWorkbench(Workbench):
|
||||
return
|
||||
|
||||
def Deactivated(self):
|
||||
"This function is executed when the workbench is deactivated"
|
||||
"""This function is executed when the workbench is deactivated"""
|
||||
|
||||
FreeCAD.Console.PrintLog("Road workbench deactivated.\n")
|
||||
|
||||
#FreeCADGui.Selection.removeObserver(self.observer)
|
||||
return
|
||||
@@ -201,4 +208,4 @@ class PVPlantWorkbench(Workbench):
|
||||
return "Gui::PythonWorkbench"
|
||||
|
||||
|
||||
Gui.addWorkbench(PVPlantWorkbench())
|
||||
FreeCADGui.addWorkbench(PVPlantWorkbench())
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# PVPlant - Paquete reestructurado
|
||||
#
|
||||
# Los imports legacy (from PVPlantSite import X, etc.) siguen funcionando.
|
||||
# Para nuevo código, usar: from PVPlant.core.site import _PVPlantSite
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import FreeCAD
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
import os
|
||||
else:
|
||||
# \cond
|
||||
def translate(ctxt,txt):
|
||||
return txt
|
||||
def QT_TRANSLATE_NOOP(ctxt,txt):
|
||||
return txt
|
||||
# \endcond
|
||||
|
||||
import PVPlantResources
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
from PVPlantResources import DirResources as DirResources
|
||||
|
||||
|
||||
class MapWindow(QtGui.QWidget):
|
||||
def __init__(self, WinTitle="MapWindow"):
|
||||
super(MapWindow, self).__init__()
|
||||
self.raise_()
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
self.minLat = None
|
||||
self.maxLat = None
|
||||
self.minLon = None
|
||||
self.maxLon = None
|
||||
self.zoom = None
|
||||
self.WinTitle = WinTitle
|
||||
self.georeference_coordinates = {'lat': None, 'lon': None}
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
from PySide2.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
|
||||
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
|
||||
|
||||
self.resize(1200, 800)
|
||||
self.setWindowTitle(self.WinTitle)
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
|
||||
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
|
||||
|
||||
self.layout = QtGui.QHBoxLayout(self)
|
||||
self.layout.setContentsMargins(4, 4, 4, 4)
|
||||
|
||||
LeftWidget = QtGui.QWidget(self)
|
||||
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
|
||||
LeftWidget.setLayout(LeftLayout)
|
||||
LeftLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
RightWidget = QtGui.QWidget(self)
|
||||
RightWidget.setFixedWidth(350)
|
||||
RightLayout = QtGui.QVBoxLayout(RightWidget)
|
||||
RightWidget.setLayout(RightLayout)
|
||||
RightLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.layout.addWidget(LeftWidget)
|
||||
self.layout.addWidget(RightWidget)
|
||||
|
||||
# Left Widgets:
|
||||
# -- Search Bar:
|
||||
self.valueSearch = QtGui.QLineEdit(self)
|
||||
self.valueSearch.setPlaceholderText("Search")
|
||||
self.valueSearch.returnPressed.connect(self.onSearch)
|
||||
|
||||
searchbutton = QtGui.QPushButton('Search')
|
||||
searchbutton.setFixedWidth(80)
|
||||
searchbutton.clicked.connect(self.onSearch)
|
||||
|
||||
SearchBarLayout = QtGui.QHBoxLayout(self)
|
||||
SearchBarLayout.addWidget(self.valueSearch)
|
||||
SearchBarLayout.addWidget(searchbutton)
|
||||
LeftLayout.addLayout(SearchBarLayout)
|
||||
|
||||
# -- Webbroser:
|
||||
self.view = QWebEngineView()
|
||||
self.channel = QWebChannel(self.view.page())
|
||||
self.view.page().setWebChannel(self.channel)
|
||||
self.channel.registerObject("MyApp", self)
|
||||
file = os.path.join(DirResources, "webs", "main.html")
|
||||
self.view.page().loadFinished.connect(self.onLoadFinished)
|
||||
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
|
||||
LeftLayout.addWidget(self.view)
|
||||
|
||||
# -- Latitud y longitud:
|
||||
self.labelCoordinates = QtGui.QLabel()
|
||||
self.labelCoordinates.setFixedHeight(21)
|
||||
LeftLayout.addWidget(self.labelCoordinates)
|
||||
|
||||
# Right Widgets:
|
||||
labelKMZ = QtGui.QLabel()
|
||||
labelKMZ.setText("Cargar un archivo KMZ/KML:")
|
||||
self.kmlButton = QtGui.QPushButton()
|
||||
self.kmlButton.setFixedSize(32, 32)
|
||||
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
|
||||
widget = QtGui.QWidget(self)
|
||||
layout = QtGui.QHBoxLayout(widget)
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(labelKMZ)
|
||||
layout.addWidget(self.kmlButton)
|
||||
RightLayout.addWidget(widget)
|
||||
|
||||
# -----------------------
|
||||
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
|
||||
self.groupbox.setCheckable(True)
|
||||
self.groupbox.setChecked(True)
|
||||
radio1 = QtGui.QRadioButton("Google Elevation")
|
||||
radio2 = QtGui.QRadioButton("Nube de Puntos")
|
||||
radio3 = QtGui.QRadioButton("Datos GPS")
|
||||
radio1.setChecked(True)
|
||||
|
||||
vbox = QtGui.QVBoxLayout(self)
|
||||
vbox.addWidget(radio1)
|
||||
vbox.addWidget(radio2)
|
||||
vbox.addWidget(radio3)
|
||||
|
||||
self.groupbox.setLayout(vbox)
|
||||
RightLayout.addWidget(self.groupbox)
|
||||
# ------------------------
|
||||
|
||||
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
|
||||
RightLayout.addWidget(self.checkboxImportGis)
|
||||
|
||||
self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital")
|
||||
RightLayout.addWidget(self.checkboxImportSatelitalImagen)
|
||||
|
||||
verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
RightLayout.addItem(verticalSpacer)
|
||||
|
||||
self.bAccept = QtGui.QPushButton('Accept')
|
||||
self.bAccept.clicked.connect(self.onAcceptClick)
|
||||
RightLayout.addWidget(self.bAccept)
|
||||
|
||||
# signals/slots
|
||||
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
|
||||
|
||||
|
||||
def onLoadFinished(self):
|
||||
file = os.path.join(DirResources, "webs", "map.js")
|
||||
frame = self.view.page()
|
||||
with open(file, 'r') as f:
|
||||
frame.runJavaScript(f.read())
|
||||
|
||||
def onSearch(self):
|
||||
if self.valueSearch.text() == "":
|
||||
return
|
||||
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
geolocator = Nominatim(user_agent="http")
|
||||
location = geolocator.geocode(self.valueSearch.text())
|
||||
self.valueSearch.setText(location.address)
|
||||
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
|
||||
|
||||
def onAcceptClick(self):
|
||||
frame = self.view.page()
|
||||
# 1. georeferenciar
|
||||
frame.runJavaScript(
|
||||
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
|
||||
)
|
||||
|
||||
# 2. importar todos los elementos dibujados:
|
||||
frame.runJavaScript(
|
||||
"var data = drawnItems.toGeoJSON();"
|
||||
"MyApp.shapes(JSON.stringify(data));"
|
||||
)
|
||||
|
||||
self.close()
|
||||
|
||||
@QtCore.Slot(float, float)
|
||||
def onMapMove(self, lat, lng):
|
||||
from lib.projection import latlon_to_utm
|
||||
|
||||
self.lat = lat
|
||||
self.lon = lng
|
||||
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lng)
|
||||
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
|
||||
' | UTM: ' + str(zone_number) + zone_letter +
|
||||
', {:.5f}m E, {:.5f}m N'.format(easting, northing))
|
||||
|
||||
@QtCore.Slot(float, float, float, float, int)
|
||||
def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
|
||||
self.minLat = min([minLat, maxLat])
|
||||
self.maxLat = max([minLat, maxLat])
|
||||
self.minLon = min([minLon, maxLon])
|
||||
self.maxLon = max([minLon, maxLon])
|
||||
self.zoom = zoom
|
||||
|
||||
@QtCore.Slot(float, float)
|
||||
def georeference(self, lat, lng):
|
||||
import PVPlantSite
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
self.georeference_coordinates['lat'] = lat
|
||||
self.georeference_coordinates['lon'] = lng
|
||||
|
||||
Site = PVPlantSite.get(create=True)
|
||||
Site.Proxy.setLatLon(lat, lng)
|
||||
|
||||
geolocator = Nominatim(user_agent="http")
|
||||
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
|
||||
if location:
|
||||
if location.raw["address"].get("road"):
|
||||
str = location.raw["address"]["road"]
|
||||
if location.raw["address"].get("house_number"):
|
||||
str += ' ({0})'.format(location.raw["address"]["house_number"])
|
||||
Site.Address = str
|
||||
if location.raw["address"].get("city"):
|
||||
Site.City = location.raw["address"]["city"]
|
||||
if location.raw["address"].get("postcode"):
|
||||
Site.PostalCode = location.raw["address"]["postcode"]
|
||||
if location.raw["address"].get("address"):
|
||||
Site.Region = '{0}'.format(location.raw["address"]["province"])
|
||||
if location.raw["address"].get("state"):
|
||||
if Site.Region != "":
|
||||
Site.Region += " - "
|
||||
Site.Region += '{0}'.format(location.raw["address"]["state"])
|
||||
Site.Country = location.raw["address"]["country"]
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def shapes(self, drawnItems):
|
||||
import geojson
|
||||
import PVPlantImportGrid as ImportElevation
|
||||
import Draft
|
||||
import PVPlantSite
|
||||
Site = PVPlantSite.get()
|
||||
|
||||
offset = FreeCAD.Vector(0, 0, 0)
|
||||
if not (self.lat is None or self.lon is None):
|
||||
offset = FreeCAD.Vector(Site.Origin)
|
||||
offset.z = 0
|
||||
|
||||
items = geojson.loads(drawnItems)
|
||||
for item in items['features']:
|
||||
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
|
||||
coord = item['geometry']['coordinates']
|
||||
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
|
||||
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
|
||||
if item['properties'].get('radius'):
|
||||
r = round(item['properties']['radius'] * 1000, 0)
|
||||
p = FreeCAD.Placement()
|
||||
p.Base = c
|
||||
obj = Draft.makeCircle(r, placement=p, face=False)
|
||||
else:
|
||||
obj = Draft.make_point(c * 1000, color=(0.5, 0.3, 0.6), point_size=10)
|
||||
else: # 2. if the feature is a Polygon or Line:
|
||||
cw = False
|
||||
name = "Línea"
|
||||
lp = item['geometry']['coordinates']
|
||||
if item['geometry']['type'] == "Polygon":
|
||||
cw = True
|
||||
name = "Area"
|
||||
lp = item['geometry']['coordinates'][0]
|
||||
|
||||
pts = [[cords[1], cords[0]] for cords in lp]
|
||||
tmp = ImportElevation.getElevationFromOE(pts)
|
||||
pts = [p.sub(offset) for p in tmp]
|
||||
|
||||
obj = Draft.makeWire(pts, closed=cw, face=False)
|
||||
obj.Label = name
|
||||
Draft.autogroup(obj)
|
||||
|
||||
if item['properties'].get('name'):
|
||||
obj.Label = item['properties']['name']
|
||||
|
||||
if self.checkboxImportGis.isChecked():
|
||||
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon)
|
||||
|
||||
if self.checkboxImportSatelitalImagen.isChecked():
|
||||
from lib.projection import latlon_to_utm
|
||||
|
||||
s_lat = self.minLat
|
||||
s_lon = self.minLon
|
||||
n_lat = self.maxLat
|
||||
n_lon = self.maxLon
|
||||
|
||||
# Obtener puntos UTM para las esquinas y el punto de referencia
|
||||
points = [
|
||||
[s_lat, s_lon], # Suroeste
|
||||
[n_lat, n_lon], # Noreste
|
||||
[self.georeference_coordinates['lat'], self.georeference_coordinates['lon']] # Punto de referencia
|
||||
]
|
||||
utm_points = ImportElevation.getElevationFromOE(points)
|
||||
|
||||
if not utm_points or len(utm_points) < 3:
|
||||
FreeCAD.Console.PrintError("Error obteniendo elevaciones para las esquinas y referencia\n")
|
||||
return
|
||||
|
||||
sw_utm, ne_utm, ref_utm = utm_points
|
||||
|
||||
# Descargar imagen satelital
|
||||
from lib.GoogleSatelitalImageDownload import GoogleMapDownloader
|
||||
downloader = GoogleMapDownloader(
|
||||
zoom=self.zoom,
|
||||
layer='raw_satellite'
|
||||
)
|
||||
img = downloader.generateImage(
|
||||
sw_lat=s_lat,
|
||||
sw_lng=s_lon,
|
||||
ne_lat=n_lat,
|
||||
ne_lng=n_lon
|
||||
)
|
||||
|
||||
# Guardar imagen
|
||||
doc_path = os.path.dirname(FreeCAD.ActiveDocument.FileName) if FreeCAD.ActiveDocument.FileName else ""
|
||||
if not doc_path:
|
||||
doc_path = FreeCAD.ConfigGet("UserAppData")
|
||||
|
||||
filename = os.path.join(doc_path, "background.jpeg")
|
||||
img.save(filename)
|
||||
|
||||
# Calcular dimensiones reales en metros
|
||||
width_m = ne_utm.x - sw_utm.x
|
||||
height_m = ne_utm.y - sw_utm.y
|
||||
|
||||
# Calcular posición relativa del punto de referencia dentro de la imagen
|
||||
rel_x = (ref_utm.x - sw_utm.x) / width_m if width_m != 0 else 0.5
|
||||
rel_y = (ref_utm.y - sw_utm.y) / height_m if height_m != 0 else 0.5
|
||||
|
||||
# Crear objeto de imagen en FreeCAD
|
||||
doc = FreeCAD.ActiveDocument
|
||||
img_obj = doc.addObject('Image::ImagePlane', 'Background')
|
||||
img_obj.ImageFile = filename
|
||||
img_obj.Label = 'Background'
|
||||
|
||||
# FreeCAD trabaja en mm
|
||||
img_obj.XSize = width_m * 1000
|
||||
img_obj.YSize = height_m * 1000
|
||||
|
||||
# Posicionar para que el punto de referencia esté en (0,0,0)
|
||||
img_obj.Placement.Base = FreeCAD.Vector(
|
||||
-rel_x * width_m * 1000,
|
||||
-rel_y * height_m * 1000,
|
||||
0
|
||||
)
|
||||
|
||||
doc.recompute()
|
||||
|
||||
def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
|
||||
import Importer.importOSM as importOSM
|
||||
import PVPlantSite
|
||||
site = PVPlantSite.get()
|
||||
|
||||
offset = FreeCAD.Vector(0, 0, 0)
|
||||
if not (self.lat is None or self.lon is None):
|
||||
offset = FreeCAD.Vector(site.Origin)
|
||||
offset.z = 0
|
||||
importer = importOSM.OSMImporter(offset)
|
||||
osm_data = importer.get_osm_data(f"{min_lat},{min_lon},{max_lat},{max_lon}")
|
||||
importer.process_osm_data(osm_data)
|
||||
|
||||
def panMap(self, lng, lat, geometry=None):
|
||||
frame = self.view.page()
|
||||
|
||||
if not geometry or len(geometry) < 4:
|
||||
command = f'map.panTo(L.latLng({lat}, {lng}));'
|
||||
else:
|
||||
try:
|
||||
southwest = f"{float(geometry[1])}, {float(geometry[0])}"
|
||||
northeast = f"{float(geometry[3])}, {float(geometry[2])}"
|
||||
command = f'map.panTo(L.latLng({lat}, {lng}));'
|
||||
command += f'map.fitBounds(L.latLngBounds([{southwest}], [{northeast}]));'
|
||||
except (IndexError, ValueError, TypeError) as e:
|
||||
print(f"Error en geometry: {str(e)}")
|
||||
command = f'map.panTo(L.latLng({lat}, {lng}));'
|
||||
frame.runJavaScript(command)
|
||||
|
||||
def importKML(self):
|
||||
file = QtGui.QFileDialog.getOpenFileName(None, "FileDialog", "", "Google Earth (*.kml *.kmz)")[0]
|
||||
|
||||
from lib.kml2geojson import kmz_convert
|
||||
layers = kmz_convert(file, "", )
|
||||
frame = self.view.page()
|
||||
for layer in layers:
|
||||
command = "var geoJsonLayer = L.geoJSON({0}); drawnItems.addLayer(geoJsonLayer); map.fitBounds(geoJsonLayer.getBounds());".format( layer)
|
||||
frame.runJavaScript(command)
|
||||
|
||||
|
||||
class CommandPVPlantGeoreferencing:
|
||||
|
||||
def GetResources(self):
|
||||
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
|
||||
'Accel': "G, R",
|
||||
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
|
||||
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
|
||||
|
||||
def Activated(self):
|
||||
self.form = MapWindow()
|
||||
self.form.show()
|
||||
|
||||
def IsActive(self):
|
||||
if FreeCAD.ActiveDocument:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -0,0 +1,208 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import FreeCAD, Draft, math, datetime
|
||||
import ArchSite
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from DraftTools import translate
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
from pivy import coin
|
||||
else:
|
||||
def translate(ctxt, txt):
|
||||
return txt
|
||||
def QT_TRANSLATE_NOOP(ctxt, txt):
|
||||
return txt
|
||||
|
||||
import os
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
|
||||
zone_list = ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7", "Z8", "Z9", "Z10", "Z11", "Z12",
|
||||
"Z13", "Z14", "Z15", "Z16", "Z17", "Z18", "Z19", "Z20", "Z21", "Z22", "Z23", "Z24",
|
||||
"Z25", "Z26", "Z27", "Z28", "Z29", "Z30", "Z31", "Z32", "Z33", "Z34", "Z35", "Z36",
|
||||
"Z37", "Z38", "Z39", "Z40", "Z41", "Z42", "Z43", "Z44", "Z45", "Z46", "Z47", "Z48",
|
||||
"Z49", "Z50", "Z51", "Z52", "Z53", "Z54", "Z55", "Z56", "Z57", "Z58", "Z59", "Z60"]
|
||||
|
||||
|
||||
def get(origin=FreeCAD.Vector(0, 0, 0), create=False):
|
||||
obj = FreeCAD.ActiveDocument.getObject('Site')
|
||||
if obj:
|
||||
if obj.Origin == FreeCAD.Vector(0, 0, 0):
|
||||
obj.Origin = origin
|
||||
return obj
|
||||
if not obj and create:
|
||||
obj = makePVPlantSite()
|
||||
return obj
|
||||
|
||||
|
||||
def PartToWire(part):
|
||||
import Part, Draft
|
||||
PointList = []
|
||||
edges = Part.__sortEdges__(part.Shape.Edges)
|
||||
for edge in edges:
|
||||
PointList.append(edge.Vertexes[0].Point)
|
||||
PointList.append(edges[-1].Vertexes[-1].Point)
|
||||
Draft.makeWire(PointList, closed=True, face=None, support=None)
|
||||
|
||||
|
||||
def projectWireOnMesh(Boundary, Mesh):
|
||||
import Draft
|
||||
import MeshPart as mp
|
||||
plist = mp.projectShapeOnMesh(Boundary.Shape, Mesh, FreeCAD.Vector(0, 0, 1))
|
||||
PointList = []
|
||||
for pl in plist:
|
||||
PointList += pl
|
||||
Draft.makeWire(PointList, closed=True, face=None, support=None)
|
||||
FreeCAD.activeDocument().recompute()
|
||||
|
||||
|
||||
def makePVPlantSite():
|
||||
def createGroup(father, groupname, type=None):
|
||||
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", groupname)
|
||||
group.Label = groupname
|
||||
father.addObject(group)
|
||||
return group
|
||||
|
||||
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Site")
|
||||
_PVPlantSite(obj)
|
||||
if FreeCAD.GuiUp:
|
||||
_ViewProviderSite(obj.ViewObject)
|
||||
|
||||
group = createGroup(obj, "CivilGroup")
|
||||
group1 = createGroup(group, "Areas")
|
||||
createGroup(group1, "Boundaries")
|
||||
createGroup(group1, "CadastralPlots")
|
||||
createGroup(group1, "Exclusions")
|
||||
createGroup(group1, "FrameZones")
|
||||
createGroup(group1, "Offsets")
|
||||
createGroup(group1, "Plots")
|
||||
createGroup(group, "Drains")
|
||||
createGroup(group, "Earthworks")
|
||||
createGroup(group, "Fences")
|
||||
createGroup(group, "Foundations")
|
||||
createGroup(group, "Pads")
|
||||
createGroup(group, "Points")
|
||||
createGroup(group, "Roads")
|
||||
createGroup(group, "Trenches")
|
||||
|
||||
group = createGroup(obj, "ElectricalGroup")
|
||||
createGroup(group, "StringInverters")
|
||||
createGroup(group, "CentralInverter")
|
||||
group1 = createGroup(group, "AC")
|
||||
createGroup(group1, "CableAC")
|
||||
group1 = createGroup(group, "DC")
|
||||
createGroup(group1, "CableDC")
|
||||
createGroup(group1, "StringsSetup")
|
||||
createGroup(group1, "Strings")
|
||||
createGroup(group1, "StringsBoxes")
|
||||
|
||||
group = createGroup(obj, "MechanicalGroup")
|
||||
createGroup(group, "FramesSetups")
|
||||
createGroup(group, "Frames")
|
||||
|
||||
group = createGroup(obj, "Environment")
|
||||
createGroup(group, "Vegetation")
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class _PVPlantSite(ArchSite._Site):
|
||||
"The Site object"
|
||||
|
||||
def __init__(self, obj):
|
||||
ArchSite._Site.__init__(self, obj)
|
||||
self.obj = obj
|
||||
self.Type = "Site"
|
||||
obj.Proxy = self
|
||||
obj.IfcType = "Site"
|
||||
obj.setEditorMode("IfcType", 1)
|
||||
|
||||
def setProperties(self, obj):
|
||||
ArchSite._Site.setProperties(self, obj)
|
||||
obj.addProperty("App::PropertyLink", "Boundary", "PVPlant", "Boundary of land")
|
||||
obj.addProperty("App::PropertyLinkList", "Frames", "PVPlant", "Frames templates")
|
||||
obj.addProperty("App::PropertyEnumeration", "UtmZone", "PVPlant", "UTM zone").UtmZone = zone_list
|
||||
obj.addProperty("App::PropertyVector", "Origin", "PVPlant", "Origin point.").Origin = (0, 0, 0)
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
self.obj = obj
|
||||
self.Type = "Site"
|
||||
obj.Proxy = self
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
ArchSite._Site.onChanged(self, obj, prop)
|
||||
if (prop == "Terrain") or (prop == "Boundary"):
|
||||
if obj.Terrain and obj.Boundary:
|
||||
print("Calcular 3D boundary")
|
||||
if prop == "UtmZone":
|
||||
node = self.get_geoorigin()
|
||||
zone = obj.getPropertyByName("UtmZone")
|
||||
geo_system = ["UTM", zone, "FLAT"]
|
||||
node.geoSystem.setValues(geo_system)
|
||||
if prop == "Origin":
|
||||
node = self.get_geoorigin()
|
||||
origin = obj.getPropertyByName("Origin")
|
||||
node.geoCoords.setValue(origin.x, origin.y, 0)
|
||||
obj.Placement.Base = obj.getPropertyByName(prop)
|
||||
|
||||
def execute(self, obj):
|
||||
ArchSite._Site.execute(self, obj)
|
||||
|
||||
def computeAreas(self, obj):
|
||||
ArchSite._Site.computeAreas(self, obj)
|
||||
|
||||
def __getstate__(self):
|
||||
node = self.get_geoorigin()
|
||||
system = node.geoSystem.getValues()
|
||||
x, y, z = node.geoCoords.getValue().getValue()
|
||||
return system, [x, y, z]
|
||||
|
||||
def __setstate__(self, state):
|
||||
if state:
|
||||
system = state[0]
|
||||
origin = state[1]
|
||||
node = self.get_geoorigin()
|
||||
node.geoSystem.setValues(system)
|
||||
node.geoCoords.setValue(origin[0], origin[1], 0)
|
||||
|
||||
def get_geoorigin(self):
|
||||
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
|
||||
node = sg.getChild(0)
|
||||
if not isinstance(node, coin.SoGeoOrigin):
|
||||
node = coin.SoGeoOrigin()
|
||||
sg.insertChild(node, 0)
|
||||
return node
|
||||
|
||||
def setLatLon(self, lat, lon):
|
||||
from lib.projection import latlon_to_utm
|
||||
import PVPlantImportGrid
|
||||
easting, northing, zone_number, zone_letter = latlon_to_utm(lat, lon)
|
||||
self.obj.UtmZone = zone_list[zone_number - 1]
|
||||
point = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
|
||||
self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z)
|
||||
self.obj.Latitude = lat
|
||||
self.obj.Longitude = lon
|
||||
self.obj.Elevation = point[0].z
|
||||
|
||||
|
||||
from PVPlant.core.view_provider import ViewProviderSite as _ViewProviderSite
|
||||
@@ -0,0 +1,353 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import FreeCAD, math, datetime
|
||||
from pivy import coin
|
||||
|
||||
|
||||
def makeSolarDiagram(longitude, latitude, scale=1, complete=False, tz=None):
|
||||
"""makeSolarDiagram(longitude,latitude,[scale,complete,tz]):
|
||||
returns a solar diagram as a pivy node. If complete is
|
||||
True, the 12 months are drawn. Tz is the timezone related to
|
||||
UTC (ex: -3 = UTC-3)"""
|
||||
|
||||
oldversion = False
|
||||
ladybug = False
|
||||
try:
|
||||
import ladybug
|
||||
from ladybug import location
|
||||
from ladybug import sunpath
|
||||
except:
|
||||
ladybug = False
|
||||
try:
|
||||
import pysolar
|
||||
except:
|
||||
try:
|
||||
import Pysolar as pysolar
|
||||
except:
|
||||
FreeCAD.Console.PrintError("The pysolar module was not found. Unable to generate solar diagrams\n")
|
||||
return None
|
||||
else:
|
||||
oldversion = True
|
||||
if tz:
|
||||
tz = datetime.timezone(datetime.timedelta(hours=-3))
|
||||
else:
|
||||
tz = datetime.timezone.utc
|
||||
else:
|
||||
loc = ladybug.location.Location(latitude=latitude, longitude=longitude, time_zone=tz)
|
||||
sunpath = ladybug.sunpath.Sunpath.from_location(loc)
|
||||
|
||||
if not scale:
|
||||
return None
|
||||
|
||||
circles = []
|
||||
sunpaths = []
|
||||
hourpaths = []
|
||||
circlepos = []
|
||||
hourpos = []
|
||||
|
||||
import Part
|
||||
for i in range(1, 9):
|
||||
circles.append(Part.makeCircle(scale * (i / 8.0)))
|
||||
for ad in range(0, 360, 15):
|
||||
a = math.radians(ad)
|
||||
p1 = FreeCAD.Vector(math.cos(a) * scale, math.sin(a) * scale, 0)
|
||||
p2 = FreeCAD.Vector(math.cos(a) * scale * 0.125, math.sin(a) * scale * 0.125, 0)
|
||||
p3 = FreeCAD.Vector(math.cos(a) * scale * 1.08, math.sin(a) * scale * 1.08, 0)
|
||||
circles.append(Part.LineSegment(p1, p2).toShape())
|
||||
circlepos.append((ad, p3))
|
||||
|
||||
year = datetime.datetime.now().year
|
||||
hpts = [[] for i in range(24)]
|
||||
m = [(6, 21), (7, 21), (8, 21), (9, 21), (10, 21), (11, 21), (12, 21)]
|
||||
if complete:
|
||||
m.extend([(1, 21), (2, 21), (3, 21), (4, 21), (5, 21)])
|
||||
for i, d in enumerate(m):
|
||||
pts = []
|
||||
for h in range(24):
|
||||
if ladybug:
|
||||
sun = sunpath.calculate_sun(month=d[0], day=d[1], hour=h)
|
||||
alt = math.radians(sun.altitude)
|
||||
az = 90 + sun.azimuth
|
||||
elif oldversion:
|
||||
dt = datetime.datetime(year, d[0], d[1], h)
|
||||
alt = math.radians(pysolar.solar.GetAltitudeFast(latitude, longitude, dt))
|
||||
az = pysolar.solar.GetAzimuth(latitude, longitude, dt)
|
||||
az = -90 + az
|
||||
else:
|
||||
dt = datetime.datetime(year, d[0], d[1], h, tzinfo=tz)
|
||||
alt = math.radians(pysolar.solar.get_altitude_fast(latitude, longitude, dt))
|
||||
az = pysolar.solar.get_azimuth(latitude, longitude, dt)
|
||||
az = 90 + az
|
||||
if az < 0:
|
||||
az = 360 + az
|
||||
az = math.radians(az)
|
||||
zc = math.sin(alt) * scale
|
||||
ic = math.cos(alt) * scale
|
||||
xc = math.cos(az) * ic
|
||||
yc = math.sin(az) * ic
|
||||
p = FreeCAD.Vector(xc, yc, zc)
|
||||
pts.append(p)
|
||||
hpts[h].append(p)
|
||||
if i in [0, 6]:
|
||||
ep = FreeCAD.Vector(p)
|
||||
ep.multiply(1.08)
|
||||
if ep.z >= 0:
|
||||
if not oldversion:
|
||||
h = 24 - h
|
||||
if h == 12:
|
||||
if i == 0:
|
||||
h = "SUMMER"
|
||||
else:
|
||||
h = "WINTER"
|
||||
if latitude < 0:
|
||||
if h == "SUMMER":
|
||||
h = "WINTER"
|
||||
else:
|
||||
h = "SUMMER"
|
||||
hourpos.append((h, ep))
|
||||
if i < 7:
|
||||
sunpaths.append(Part.makePolygon(pts))
|
||||
|
||||
for h in hpts:
|
||||
if complete:
|
||||
h.append(h[0])
|
||||
hourpaths.append(Part.makePolygon(h))
|
||||
|
||||
sz = 2.1 * scale
|
||||
cube = Part.makeBox(sz, sz, sz)
|
||||
cube.translate(FreeCAD.Vector(-sz / 2, -sz / 2, -sz))
|
||||
sunpaths = [sp.cut(cube) for sp in sunpaths]
|
||||
hourpaths = [hp.cut(cube) for hp in hourpaths]
|
||||
|
||||
ts = 0.005 * scale
|
||||
mastersep = coin.SoSeparator()
|
||||
circlesep = coin.SoSeparator()
|
||||
numsep = coin.SoSeparator()
|
||||
pathsep = coin.SoSeparator()
|
||||
hoursep = coin.SoSeparator()
|
||||
hournumsep = coin.SoSeparator()
|
||||
mastersep.addChild(circlesep)
|
||||
mastersep.addChild(numsep)
|
||||
mastersep.addChild(pathsep)
|
||||
mastersep.addChild(hoursep)
|
||||
for item in circles:
|
||||
circlesep.addChild(toNode(item))
|
||||
for item in sunpaths:
|
||||
for w in item.Edges:
|
||||
pathsep.addChild(toNode(w))
|
||||
for item in hourpaths:
|
||||
for w in item.Edges:
|
||||
hoursep.addChild(toNode(w))
|
||||
for p in circlepos:
|
||||
text = coin.SoText2()
|
||||
s = p[0] - 90
|
||||
s = -s
|
||||
if s > 360:
|
||||
s = s - 360
|
||||
if s < 0:
|
||||
s = 360 + s
|
||||
if s == 0:
|
||||
s = "N"
|
||||
elif s == 90:
|
||||
s = "E"
|
||||
elif s == 180:
|
||||
s = "S"
|
||||
elif s == 270:
|
||||
s = "W"
|
||||
else:
|
||||
s = str(s)
|
||||
text.string = s
|
||||
text.justification = coin.SoText2.CENTER
|
||||
coords = coin.SoTransform()
|
||||
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
|
||||
coords.scaleFactor.setValue([ts, ts, ts])
|
||||
item = coin.SoSeparator()
|
||||
item.addChild(coords)
|
||||
item.addChild(text)
|
||||
numsep.addChild(item)
|
||||
for p in hourpos:
|
||||
text = coin.SoText2()
|
||||
s = str(p[0])
|
||||
text.string = s
|
||||
text.justification = coin.SoText2.CENTER
|
||||
coords = coin.SoTransform()
|
||||
coords.translation.setValue([p[1].x, p[1].y, p[1].z])
|
||||
coords.scaleFactor.setValue([ts, ts, ts])
|
||||
item = coin.SoSeparator()
|
||||
item.addChild(coords)
|
||||
item.addChild(text)
|
||||
numsep.addChild(item)
|
||||
return mastersep
|
||||
|
||||
|
||||
def makeWindRose(epwfile, scale=1, sectors=24):
|
||||
try:
|
||||
import ladybug
|
||||
from ladybug import epw
|
||||
except:
|
||||
FreeCAD.Console.PrintError("The ladybug module was not found. Unable to generate solar diagrams\n")
|
||||
return None
|
||||
if not epwfile:
|
||||
FreeCAD.Console.PrintWarning("No EPW file, unable to generate wind rose.\n")
|
||||
return None
|
||||
epw_data = ladybug.epw.EPW(epwfile)
|
||||
baseangle = 360 / sectors
|
||||
sectorangles = [i * baseangle for i in range(sectors)]
|
||||
basebissect = baseangle / 2
|
||||
angles = [basebissect]
|
||||
for i in range(1, sectors):
|
||||
angles.append(angles[-1] + baseangle)
|
||||
windsbysector = [0 for i in range(sectors)]
|
||||
for hour in epw_data.wind_direction:
|
||||
sector = min(angles, key=lambda x: abs(x - hour))
|
||||
sectorindex = angles.index(sector)
|
||||
windsbysector[sectorindex] = windsbysector[sectorindex] + 1
|
||||
maxwind = max(windsbysector)
|
||||
windsbysector = [wind / maxwind for wind in windsbysector]
|
||||
vectors = []
|
||||
dividers = []
|
||||
for i in range(sectors):
|
||||
angle = math.radians(90 + angles[i])
|
||||
x = math.cos(angle) * windsbysector[i] * scale
|
||||
y = math.sin(angle) * windsbysector[i] * scale
|
||||
vectors.append(FreeCAD.Vector(x, y, 0))
|
||||
secangle = math.radians(90 + sectorangles[i])
|
||||
x = math.cos(secangle) * scale
|
||||
y = math.sin(secangle) * scale
|
||||
dividers.append(FreeCAD.Vector(x, y, 0))
|
||||
vectors.append(vectors[0])
|
||||
|
||||
import Part
|
||||
masternode = coin.SoSeparator()
|
||||
for r in (0.25, 0.5, 0.75, 1.0):
|
||||
c = Part.makeCircle(r * scale)
|
||||
masternode.addChild(toNode(c))
|
||||
for divider in dividers:
|
||||
l = Part.makeLine(FreeCAD.Vector(), divider)
|
||||
masternode.addChild(toNode(l))
|
||||
ds = coin.SoDrawStyle()
|
||||
ds.lineWidth = 2.0
|
||||
masternode.addChild(ds)
|
||||
d = Part.makePolygon(vectors)
|
||||
masternode.addChild(toNode(d))
|
||||
return masternode
|
||||
|
||||
|
||||
# Values in mm
|
||||
COMPASS_POINTER_LENGTH = 1000
|
||||
COMPASS_POINTER_WIDTH = 100
|
||||
|
||||
|
||||
class Compass(object):
|
||||
def __init__(self):
|
||||
self.rootNode = self.setupCoin()
|
||||
|
||||
def show(self):
|
||||
self.compassswitch.whichChild = coin.SO_SWITCH_ALL
|
||||
|
||||
def hide(self):
|
||||
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
|
||||
|
||||
def rotate(self, angleInDegrees):
|
||||
self.transform.rotation.setValue(
|
||||
coin.SbVec3f(0, 0, 1), math.radians(angleInDegrees))
|
||||
|
||||
def locate(self, x, y, z):
|
||||
self.transform.translation.setValue(x, y, z)
|
||||
|
||||
def scale(self, area):
|
||||
s = round(max(math.sqrt(area.getValueAs("m^2").Value) / 10, 1))
|
||||
self.transform.scaleFactor.setValue(coin.SbVec3f(s, s, 1))
|
||||
|
||||
def setupCoin(self):
|
||||
compasssep = coin.SoSeparator()
|
||||
self.transform = coin.SoTransform()
|
||||
|
||||
darkNorthMaterial = coin.SoMaterial()
|
||||
darkNorthMaterial.diffuseColor.set1Value(0, 0.5, 0, 0)
|
||||
lightNorthMaterial = coin.SoMaterial()
|
||||
lightNorthMaterial.diffuseColor.set1Value(0, 0.9, 0, 0)
|
||||
darkGreyMaterial = coin.SoMaterial()
|
||||
darkGreyMaterial.diffuseColor.set1Value(0, 0.9, 0.9, 0.9)
|
||||
lightGreyMaterial = coin.SoMaterial()
|
||||
lightGreyMaterial.diffuseColor.set1Value(0, 0.5, 0.5, 0.5)
|
||||
|
||||
coords = self.buildCoordinates()
|
||||
lightColorFaceset = coin.SoIndexedFaceSet()
|
||||
lightColorCoordinateIndex = [4, 5, 6, -1, 8, 9, 10, -1, 12, 13, 14, -1]
|
||||
lightColorFaceset.coordIndex.setValues(0, len(lightColorCoordinateIndex), lightColorCoordinateIndex)
|
||||
darkColorFaceset = coin.SoIndexedFaceSet()
|
||||
darkColorCoordinateIndex = [6, 7, 4, -1, 10, 11, 8, -1, 14, 15, 12, -1]
|
||||
darkColorFaceset.coordIndex.setValues(0, len(darkColorCoordinateIndex), darkColorCoordinateIndex)
|
||||
lightNorthFaceset = coin.SoIndexedFaceSet()
|
||||
lightNorthCoordinateIndex = [2, 3, 0, -1]
|
||||
lightNorthFaceset.coordIndex.setValues(0, len(lightNorthCoordinateIndex), lightNorthCoordinateIndex)
|
||||
darkNorthFaceset = coin.SoIndexedFaceSet()
|
||||
darkNorthCoordinateIndex = [0, 1, 2, -1]
|
||||
darkNorthFaceset.coordIndex.setValues(0, len(darkNorthCoordinateIndex), darkNorthCoordinateIndex)
|
||||
|
||||
self.compassswitch = coin.SoSwitch()
|
||||
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
|
||||
self.compassswitch.addChild(compasssep)
|
||||
|
||||
lightGreySeparator = coin.SoSeparator()
|
||||
lightGreySeparator.addChild(lightGreyMaterial)
|
||||
lightGreySeparator.addChild(lightColorFaceset)
|
||||
darkGreySeparator = coin.SoSeparator()
|
||||
darkGreySeparator.addChild(darkGreyMaterial)
|
||||
darkGreySeparator.addChild(darkColorFaceset)
|
||||
lightNorthSeparator = coin.SoSeparator()
|
||||
lightNorthSeparator.addChild(lightNorthMaterial)
|
||||
lightNorthSeparator.addChild(lightNorthFaceset)
|
||||
darkNorthSeparator = coin.SoSeparator()
|
||||
darkNorthSeparator.addChild(darkNorthMaterial)
|
||||
darkNorthSeparator.addChild(darkNorthFaceset)
|
||||
|
||||
compasssep.addChild(coords)
|
||||
compasssep.addChild(self.transform)
|
||||
compasssep.addChild(lightGreySeparator)
|
||||
compasssep.addChild(darkGreySeparator)
|
||||
compasssep.addChild(lightNorthSeparator)
|
||||
compasssep.addChild(darkNorthSeparator)
|
||||
|
||||
return self.compassswitch
|
||||
|
||||
def buildCoordinates(self):
|
||||
coords = coin.SoCoordinate3()
|
||||
coords.point.set1Value(0, 0, 0, 0)
|
||||
coords.point.set1Value(1, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(2, 0, COMPASS_POINTER_LENGTH, 0)
|
||||
coords.point.set1Value(3, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(4, 0, 0, 0)
|
||||
coords.point.set1Value(5, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(6, COMPASS_POINTER_LENGTH, 0, 0)
|
||||
coords.point.set1Value(7, COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(8, 0, 0, 0)
|
||||
coords.point.set1Value(9, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(10, 0, -COMPASS_POINTER_LENGTH, 0)
|
||||
coords.point.set1Value(11, COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(12, 0, 0, 0)
|
||||
coords.point.set1Value(13, -COMPASS_POINTER_WIDTH, COMPASS_POINTER_WIDTH, 0)
|
||||
coords.point.set1Value(14, -COMPASS_POINTER_LENGTH, 0, 0)
|
||||
coords.point.set1Value(15, -COMPASS_POINTER_WIDTH, -COMPASS_POINTER_WIDTH, 0)
|
||||
return coords
|
||||
@@ -0,0 +1,283 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import FreeCAD, math
|
||||
from pivy import coin
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from DraftTools import translate
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
from PVPlant.core.solar_compass import makeSolarDiagram, makeWindRose, Compass
|
||||
|
||||
|
||||
class ViewProviderSite(object):
|
||||
"""View Provider for the Site object. Handles solar diagram, wind rose, compass and true north."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
|
||||
self.setProperties(vobj)
|
||||
|
||||
def setProperties(self, vobj):
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
pl = vobj.PropertiesList
|
||||
if not "WindRose" in pl:
|
||||
vobj.addProperty("App::PropertyBool", "WindRose", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Show wind rose diagram or not. Uses solar diagram scale. Needs Ladybug module"))
|
||||
if not "SolarDiagram" in pl:
|
||||
vobj.addProperty("App::PropertyBool", "SolarDiagram", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Show solar diagram or not"))
|
||||
if not "SolarDiagramScale" in pl:
|
||||
vobj.addProperty("App::PropertyFloat", "SolarDiagramScale", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The scale of the solar diagram"))
|
||||
vobj.SolarDiagramScale = 1
|
||||
if not "SolarDiagramPosition" in pl:
|
||||
vobj.addProperty("App::PropertyVector", "SolarDiagramPosition", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The position of the solar diagram"))
|
||||
if not "SolarDiagramColor" in pl:
|
||||
vobj.addProperty("App::PropertyColor", "SolarDiagramColor", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The color of the solar diagram"))
|
||||
vobj.SolarDiagramColor = (0.16, 0.16, 0.25)
|
||||
if not "Orientation" in pl:
|
||||
vobj.addProperty("App::PropertyEnumeration", "Orientation", "Site",
|
||||
QT_TRANSLATE_NOOP("App::Property", "When set to 'True North' the whole geometry will be rotated to match the true north of this site"))
|
||||
vobj.Orientation = ["Project North", "True North"]
|
||||
vobj.Orientation = "Project North"
|
||||
if not "Compass" in pl:
|
||||
vobj.addProperty("App::PropertyBool", "Compass", "Compass",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Show compass or not"))
|
||||
if not "CompassRotation" in pl:
|
||||
vobj.addProperty("App::PropertyAngle", "CompassRotation", "Compass",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The rotation of the Compass relative to the Site"))
|
||||
if not "CompassPosition" in pl:
|
||||
vobj.addProperty("App::PropertyVector", "CompassPosition", "Compass",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The position of the Compass relative to the Site placement"))
|
||||
if not "UpdateDeclination" in pl:
|
||||
vobj.addProperty("App::PropertyBool", "UpdateDeclination", "Compass",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Update the Declination value based on the compass rotation"))
|
||||
|
||||
def onDocumentRestored(self, vobj):
|
||||
self.setProperties(vobj)
|
||||
|
||||
def getIcon(self):
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
return str(os.path.join(DirIcons, "solar-panel.svg"))
|
||||
|
||||
def claimChildren(self):
|
||||
objs = []
|
||||
if hasattr(self, "Object"):
|
||||
objs = self.Object.Group + [self.Object.Terrain]
|
||||
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
|
||||
if hasattr(self.Object, "Additions") and prefs.GetBool("swallowAdditions", True):
|
||||
objs.extend(self.Object.Additions)
|
||||
if hasattr(self.Object, "Subtractions") and prefs.GetBool("swallowSubtractions", True):
|
||||
objs.extend(self.Object.Subtractions)
|
||||
return objs
|
||||
|
||||
def setEdit(self, vobj, mode):
|
||||
if (mode == 0) and hasattr(self, "Object"):
|
||||
import ArchComponent
|
||||
taskd = ArchComponent.ComponentTaskPanel()
|
||||
taskd.obj = self.Object
|
||||
taskd.update()
|
||||
FreeCADGui.Control.showDialog(taskd)
|
||||
return True
|
||||
return False
|
||||
|
||||
def unsetEdit(self, vobj, mode):
|
||||
FreeCADGui.Control.closeDialog()
|
||||
return False
|
||||
|
||||
def attach(self, vobj):
|
||||
self.Object = vobj.Object
|
||||
basesep = coin.SoSeparator()
|
||||
vobj.Annotation.addChild(basesep)
|
||||
self.color = coin.SoBaseColor()
|
||||
self.coords = coin.SoTransform()
|
||||
basesep.addChild(self.coords)
|
||||
basesep.addChild(self.color)
|
||||
self.diagramsep = coin.SoSeparator()
|
||||
self.diagramswitch = coin.SoSwitch()
|
||||
self.diagramswitch.whichChild = -1
|
||||
self.diagramswitch.addChild(self.diagramsep)
|
||||
basesep.addChild(self.diagramswitch)
|
||||
self.windrosesep = coin.SoSeparator()
|
||||
self.windroseswitch = coin.SoSwitch()
|
||||
self.windroseswitch.whichChild = -1
|
||||
self.windroseswitch.addChild(self.windrosesep)
|
||||
basesep.addChild(self.windroseswitch)
|
||||
self.compass = Compass()
|
||||
self.updateCompassVisibility(vobj)
|
||||
self.updateCompassScale(vobj)
|
||||
self.rotateCompass(vobj)
|
||||
vobj.Annotation.addChild(self.compass.rootNode)
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
if prop in ["Longitude", "Latitude"]:
|
||||
self.onChanged(obj.ViewObject, "SolarDiagram")
|
||||
elif prop == "Declination":
|
||||
self.onChanged(obj.ViewObject, "SolarDiagramPosition")
|
||||
self.updateTrueNorthRotation()
|
||||
elif prop == "Terrain":
|
||||
self.updateCompassLocation(obj.ViewObject)
|
||||
elif prop == "Placement":
|
||||
self.updateCompassLocation(obj.ViewObject)
|
||||
self.updateDeclination(obj.ViewObject)
|
||||
elif prop == "ProjectedArea":
|
||||
self.updateCompassScale(obj.ViewObject)
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
if prop == "SolarDiagramPosition":
|
||||
if hasattr(vobj, "SolarDiagramPosition"):
|
||||
p = vobj.SolarDiagramPosition
|
||||
self.coords.translation.setValue([p.x, p.y, p.z])
|
||||
if hasattr(vobj.Object, "Declination"):
|
||||
self.coords.rotation.setValue(coin.SbVec3f((0, 0, 1)), math.radians(vobj.Object.Declination.Value))
|
||||
elif prop == "SolarDiagramColor":
|
||||
if hasattr(vobj, "SolarDiagramColor"):
|
||||
l = vobj.SolarDiagramColor
|
||||
self.color.rgb.setValue([l[0], l[1], l[2]])
|
||||
elif "SolarDiagram" in prop:
|
||||
if hasattr(self, "diagramnode"):
|
||||
self.diagramsep.removeChild(self.diagramnode)
|
||||
del self.diagramnode
|
||||
if hasattr(vobj, "SolarDiagram") and hasattr(vobj, "SolarDiagramScale"):
|
||||
if vobj.SolarDiagram:
|
||||
tz = 0
|
||||
if hasattr(vobj.Object, "TimeZone"):
|
||||
tz = vobj.Object.TimeZone
|
||||
self.diagramnode = makeSolarDiagram(vobj.Object.Longitude, vobj.Object.Latitude,
|
||||
vobj.SolarDiagramScale, tz=tz)
|
||||
if self.diagramnode:
|
||||
self.diagramsep.addChild(self.diagramnode)
|
||||
self.diagramswitch.whichChild = 0
|
||||
else:
|
||||
del self.diagramnode
|
||||
else:
|
||||
self.diagramswitch.whichChild = -1
|
||||
elif prop == "WindRose":
|
||||
if hasattr(self, "windrosenode"):
|
||||
del self.windrosenode
|
||||
if hasattr(vobj, "WindRose"):
|
||||
if vobj.WindRose:
|
||||
if hasattr(vobj.Object, "EPWFile") and vobj.Object.EPWFile:
|
||||
try:
|
||||
import ladybug
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self.windrosenode = makeWindRose(vobj.Object.EPWFile, vobj.SolarDiagramScale)
|
||||
if self.windrosenode:
|
||||
self.windrosesep.addChild(self.windrosenode)
|
||||
self.windroseswitch.whichChild = 0
|
||||
else:
|
||||
del self.windrosenode
|
||||
else:
|
||||
self.windroseswitch.whichChild = -1
|
||||
elif prop == 'Visibility':
|
||||
if vobj.Visibility:
|
||||
self.updateCompassVisibility(self.Object)
|
||||
else:
|
||||
self.compass.hide()
|
||||
elif prop == 'Orientation':
|
||||
if vobj.Orientation == 'True North':
|
||||
self.addTrueNorthRotation()
|
||||
else:
|
||||
self.removeTrueNorthRotation()
|
||||
elif prop == "UpdateDeclination":
|
||||
self.updateDeclination(vobj)
|
||||
elif prop == "Compass":
|
||||
self.updateCompassVisibility(vobj)
|
||||
elif prop == "CompassRotation":
|
||||
self.updateDeclination(vobj)
|
||||
self.rotateCompass(vobj)
|
||||
elif prop == "CompassPosition":
|
||||
self.updateCompassLocation(vobj)
|
||||
|
||||
def updateDeclination(self, vobj):
|
||||
if not hasattr(vobj, 'UpdateDeclination') or not vobj.UpdateDeclination:
|
||||
return
|
||||
compassRotation = vobj.CompassRotation.Value
|
||||
siteRotation = math.degrees(vobj.Object.Placement.Rotation.Angle)
|
||||
vobj.Object.Declination = compassRotation + siteRotation
|
||||
|
||||
def addTrueNorthRotation(self):
|
||||
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
|
||||
return
|
||||
self.trueNorthRotation = coin.SoTransform()
|
||||
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
|
||||
sg.insertChild(self.trueNorthRotation, 0)
|
||||
self.updateTrueNorthRotation()
|
||||
|
||||
def removeTrueNorthRotation(self):
|
||||
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
|
||||
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
|
||||
sg.removeChild(self.trueNorthRotation)
|
||||
self.trueNorthRotation = None
|
||||
|
||||
def updateTrueNorthRotation(self):
|
||||
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
|
||||
angle = self.Object.Declination.Value
|
||||
self.trueNorthRotation.rotation.setValue(coin.SbVec3f(0, 0, 1), math.radians(-angle))
|
||||
|
||||
def updateCompassVisibility(self, vobj):
|
||||
if not hasattr(self, 'compass'):
|
||||
return
|
||||
show = hasattr(vobj, 'Compass') and vobj.Compass
|
||||
if show:
|
||||
self.compass.show()
|
||||
else:
|
||||
self.compass.hide()
|
||||
|
||||
def rotateCompass(self, vobj):
|
||||
if not hasattr(self, 'compass'):
|
||||
return
|
||||
if hasattr(vobj, 'CompassRotation'):
|
||||
self.compass.rotate(vobj.CompassRotation.Value)
|
||||
|
||||
def updateCompassLocation(self, vobj):
|
||||
if not hasattr(self, 'compass'):
|
||||
return
|
||||
if not vobj.Object.Shape:
|
||||
return
|
||||
boundBox = vobj.Object.Shape.BoundBox
|
||||
pos = vobj.Object.Placement.Base
|
||||
x = 0
|
||||
y = 0
|
||||
if hasattr(vobj, "CompassPosition"):
|
||||
x = vobj.CompassPosition.x
|
||||
y = vobj.CompassPosition.y
|
||||
z = boundBox.ZMax = pos.z
|
||||
self.compass.locate(x, y, z + 1000)
|
||||
|
||||
def updateCompassScale(self, vobj):
|
||||
if not hasattr(self, 'compass'):
|
||||
return
|
||||
self.compass.scale(vobj.Object.ProjectedArea)
|
||||
|
||||
def __getstate__(self):
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
return None
|
||||
@@ -0,0 +1,671 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
import Draft
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
import os
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
import PVPlantSite
|
||||
|
||||
|
||||
def get_elevation_from_oe(coordinates):
|
||||
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
|
||||
Args:
|
||||
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
|
||||
Returns:
|
||||
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
|
||||
o lista vacía en caso de error.
|
||||
"""
|
||||
if not coordinates:
|
||||
return []
|
||||
|
||||
import requests
|
||||
from lib.projection import latlon_to_utm
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
url="https://api.open-elevation.com/api/v1/lookup",
|
||||
params={'locations': locations},
|
||||
timeout=20,
|
||||
verify=True
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
except RequestException as e:
|
||||
print(f"Error en la solicitud: {str(e)}")
|
||||
return []
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
print("Respuesta JSON inválida")
|
||||
return []
|
||||
|
||||
if "results" not in data or len(data["results"]) != len(coordinates):
|
||||
print("Formato de respuesta inesperado")
|
||||
return []
|
||||
|
||||
points = []
|
||||
for result in data["results"]:
|
||||
try:
|
||||
easting, northing, _, _ = latlon_to_utm(
|
||||
result["latitude"],
|
||||
result["longitude"]
|
||||
)
|
||||
|
||||
points.append(FreeCAD.Vector(round(easting),
|
||||
round(northing),
|
||||
round(result["elevation"])) * 1000)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error procesando coordenadas: {str(e)}")
|
||||
continue
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def getElevationFromOE(coordinates):
|
||||
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
|
||||
|
||||
import certifi
|
||||
from requests.exceptions import RequestException
|
||||
if len(coordinates) == 0:
|
||||
return None
|
||||
|
||||
from requests import get
|
||||
from lib.projection import latlon_to_utm
|
||||
|
||||
locations_str=""
|
||||
total = len(coordinates) - 1
|
||||
for i, point in enumerate(coordinates):
|
||||
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
|
||||
if i != total:
|
||||
locations_str += '|'
|
||||
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
|
||||
points = []
|
||||
try:
|
||||
r = get(query, timeout=20, verify=certifi.where())
|
||||
results = r.json()
|
||||
for point in results["results"]:
|
||||
easting, northing, _, _ = latlon_to_utm(point["latitude"], point["longitude"])
|
||||
v = FreeCAD.Vector(round(easting, 0),
|
||||
round(northing, 0),
|
||||
round(point["elevation"], 0)) * 1000
|
||||
points.append(v)
|
||||
except RequestException as e:
|
||||
for point in coordinates:
|
||||
easting, northing, _, _ = latlon_to_utm(point[0], point[1])
|
||||
points.append(FreeCAD.Vector(round(easting, 0),
|
||||
round(northing, 0),
|
||||
0) * 1000)
|
||||
|
||||
return points
|
||||
|
||||
def getSinglePointElevationFromBing(lat, lng):
|
||||
import requests
|
||||
from lib.projection import latlon_to_utm
|
||||
|
||||
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
|
||||
source += str(lat) + "," + str(lng)
|
||||
source += "&heights=sealevel"
|
||||
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
|
||||
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
s = json.loads(ans)
|
||||
print(s)
|
||||
res = s['resourceSets'][0]['resources'][0]['elevations']
|
||||
for elevation in res:
|
||||
easting, northing, _, _ = latlon_to_utm(lat, lng)
|
||||
v = FreeCAD.Vector(
|
||||
round(easting * 1000, 0),
|
||||
round(northing * 1000, 0),
|
||||
round(elevation * 1000, 0))
|
||||
return v
|
||||
|
||||
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
|
||||
import math
|
||||
import requests
|
||||
from lib.projection import latlon_to_utm, utm_to_latlon
|
||||
|
||||
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
|
||||
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
xx = polygon.Shape.BoundBox.XMin
|
||||
while xx < polygon.Shape.BoundBox.XMax:
|
||||
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
|
||||
|
||||
if StepsXX > 1000:
|
||||
StepsXX = 1000
|
||||
xx1 = xx + 1000 * resolution
|
||||
else:
|
||||
xx1 = xx + StepsXX * resolution
|
||||
|
||||
point1 = utm_to_latlon(xx / 1000, yy / 1000, zone_number, zone_letter)
|
||||
point2 = utm_to_latlon(xx1 / 1000, yy / 1000, zone_number, zone_letter)
|
||||
|
||||
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
|
||||
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
|
||||
source += ","
|
||||
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
|
||||
source += "&heights=sealevel"
|
||||
source += "&samples={steps}".format(steps=StepsXX)
|
||||
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
|
||||
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['resourceSets'][0]['resources'][0]['elevations']
|
||||
|
||||
i = 0
|
||||
for elevation in res:
|
||||
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
|
||||
points.append(v)
|
||||
i += 1
|
||||
xx = xx1 + resolution
|
||||
yy -= resolution
|
||||
|
||||
return points
|
||||
|
||||
def getSinglePointElevation(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
#print (source)
|
||||
|
||||
#response = request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
from geopy.distance import geodesic
|
||||
for r in res:
|
||||
|
||||
reference = (0.0, 0.0)
|
||||
v = FreeCAD.Vector(
|
||||
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
|
||||
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
def _getSinglePointElevation(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
#print (source)
|
||||
|
||||
#response = request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
import pymap3d as pm
|
||||
for r in res:
|
||||
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
|
||||
round(r['location']['lat'], 2),
|
||||
0)
|
||||
v = FreeCAD.Vector(x,y,z)
|
||||
|
||||
return v
|
||||
|
||||
def getSinglePointElevation1(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
#response = urllib.request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
for r in res:
|
||||
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0], 4),
|
||||
round(c[1], 4),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
return v
|
||||
|
||||
def getSinglePointElevationUtm(lat, lon):
|
||||
import requests
|
||||
from lib.projection import latlon_to_utm
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
print(source)
|
||||
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
print(res)
|
||||
|
||||
for r in res:
|
||||
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(easting * 1000, 4),
|
||||
round(northing * 1000, 4),
|
||||
round(r['elevation'] * 1000, 2))
|
||||
print(v)
|
||||
return v
|
||||
|
||||
def getElevationUTM(polygon, lat, lng, resolution = 10000):
|
||||
from lib.projection import latlon_to_utm, utm_to_latlon
|
||||
|
||||
_, _, zone_number, zone_letter = latlon_to_utm(lat, lng)
|
||||
|
||||
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
point1 = utm_to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, zone_number, zone_letter)
|
||||
point2 = utm_to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, zone_number, zone_letter)
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
|
||||
source += "{a},{b}".format(a = point1[0], b = point1[1])
|
||||
source += "|"
|
||||
source += "{a},{b}".format(a = point2[0], b = point2[1])
|
||||
source += "&samples={a}".format(a = StepsXX)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
for r in res:
|
||||
easting, northing, _, _ = latlon_to_utm(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(easting * 1000, 2),
|
||||
round(northing * 1000, 2),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
points.append(v)
|
||||
yy -= (resolution*1000)
|
||||
|
||||
FreeCAD.activeDocument().recompute()
|
||||
return points
|
||||
|
||||
def getElevation1(polygon,resolution=10):
|
||||
|
||||
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
|
||||
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
|
||||
source += "{a},{b}".format(a = point1[0], b = point1[1])
|
||||
source += "|"
|
||||
source += "{a},{b}".format(a = point2[0], b = point2[1])
|
||||
source += "&samples={a}".format(a = StepsXX)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
try:
|
||||
#response = urllib.request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
except:
|
||||
continue
|
||||
|
||||
#points = []
|
||||
for r in res:
|
||||
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0], 2),
|
||||
round(c[1], 2),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
points.append(v)
|
||||
|
||||
FreeCAD.activeDocument().recompute()
|
||||
yy -= (resolution*1000)
|
||||
|
||||
return points
|
||||
|
||||
## download the heights from google:
|
||||
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
|
||||
#https://maps.googleapis.com/maps/api/elevation/json?path=36.578581,-118.291994|36.23998,-116.83171&samples=3&key=YOUR_API_KEY
|
||||
#https://maps.googleapis.com/maps/api/elevation/json?locations=39.7391536,-104.9847034&key=YOUR_API_KEY
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
|
||||
source += str(b-size*0.001) + "," + str(le) + "|" + str(b+size*0.001) + "," + str(le)
|
||||
source += "&samples=" + str(100)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
response = urllib.request.urlopen(source)
|
||||
ans = response.read()
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
from geopy.distance import geodesic
|
||||
points = []
|
||||
for r in res:
|
||||
reference = (0.0, 0.0)
|
||||
v = FreeCAD.Vector(
|
||||
round(geodesic(reference, (0.0, r['location']['lat'])).m, 2),
|
||||
round(geodesic(reference, (r['location']['lng'], 0.0)).m, 2),
|
||||
round(r['elevation'] * 1000, 2) - baseheight
|
||||
)
|
||||
points.append(v)
|
||||
|
||||
line = Draft.makeWire(points, closed=False, face=False, support=None)
|
||||
line.ViewObject.Visibility = False
|
||||
#FreeCAD.activeDocument().recompute()
|
||||
FreeCADGui.updateGui()
|
||||
return FreeCAD.activeDocument().ActiveObject
|
||||
|
||||
class _ImportPointsTaskPanel:
|
||||
|
||||
def __init__(self, obj = None):
|
||||
self.obj = None
|
||||
self.Boundary = None
|
||||
self.select = 0
|
||||
self.filename = ""
|
||||
|
||||
# form:
|
||||
self.form1 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantImportGrid.ui")
|
||||
self.form1.radio1.toggled.connect(lambda: self.mainToggle(self.form1.radio1))
|
||||
self.form1.radio2.toggled.connect(lambda: self.mainToggle(self.form1.radio2))
|
||||
self.form1.radio1.setChecked(True) # << --------------Poner al final para que no dispare antes de crear los componentes a los que va a llamar
|
||||
#self.form.buttonAdd.clicked.connect(self.add)
|
||||
self.form1.buttonDEM.clicked.connect(self.openFileDEM)
|
||||
|
||||
self.form2 = FreeCADGui.PySideUic.loadUi(os.path.dirname(__file__) + "/PVPlantCreateTerrainMesh.ui")
|
||||
#self.form2.buttonAdd.clicked.connect(self.add)
|
||||
self.form2.buttonBoundary.clicked.connect(self.addBoundary)
|
||||
|
||||
|
||||
#self.form = [self.form1, self.form2]
|
||||
self.form = self.form1
|
||||
|
||||
''' future:
|
||||
def retranslateUi(self, dialog):
|
||||
self.form1.setWindowTitle("Configuracion del Rack")
|
||||
self.labelModule.setText(QtGui.QApplication.translate("PVPlant", "Modulo:", None))
|
||||
self.labelModuleLength.setText(QtGui.QApplication.translate("PVPlant", "Longitud:", None))
|
||||
self.labelModuleWidth.setText(QtGui.QApplication.translate("PVPlant", "Ancho:", None))
|
||||
self.labelModuleHeight.setText(QtGui.QApplication.translate("PVPlant", "Alto:", None))
|
||||
self.labelModuleFrame.setText(QtGui.QApplication.translate("PVPlant", "Ancho del marco:", None))
|
||||
self.labelModuleColor.setText(QtGui.QApplication.translate("PVPlant", "Color del modulo:", None))
|
||||
self.labelModules.setText(QtGui.QApplication.translate("Arch", "Colocacion de los Modulos", None))
|
||||
self.labelModuleOrientation.setText(QtGui.QApplication.translate("Arch", "Orientacion del modulo:", None))
|
||||
self.labelModuleGapX.setText(QtGui.QApplication.translate("Arch", "Separacion Horizontal (mm):", None))
|
||||
self.labelModuleGapY.setText(QtGui.QApplication.translate("Arch", "Separacion Vertical (mm):", None))
|
||||
self.labelModuleRows.setText(QtGui.QApplication.translate("Arch", "Filas de modulos:", None))
|
||||
self.labelModuleCols.setText(QtGui.QApplication.translate("Arch", "Columnas de modulos:", None))
|
||||
self.labelRack.setText(QtGui.QApplication.translate("Arch", "Configuracion de la estructura", None))
|
||||
self.labelRackType.setText(QtGui.QApplication.translate("Arch", "Tipo de estructura:", None))
|
||||
self.labelLevel.setText(QtGui.QApplication.translate("Arch", "Nivel:", None))
|
||||
self.labelOffset.setText(QtGui.QApplication.translate("Arch", "Offset", None))
|
||||
'''
|
||||
|
||||
def add(self):
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if len(sel) > 0:
|
||||
self.obj = sel[0]
|
||||
self.lineEdit1.setText(self.obj.Label)
|
||||
|
||||
def addBoundary(self):
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if len(sel) > 0:
|
||||
self.Boundary = sel[0]
|
||||
self.form2.editBoundary.setText(self.Boundary.Label)
|
||||
|
||||
def openFileDEM(self):
|
||||
filters = "Esri ASC (*.asc);;CSV (*.csv);;All files (*.*)"
|
||||
filename = QtGui.QFileDialog.getOpenFileName(None,
|
||||
"Open DEM,",
|
||||
"",
|
||||
filters)
|
||||
self.filename = filename[0]
|
||||
self.form1.editDEM.setText(filename[0])
|
||||
|
||||
def mainToggle(self, radiobox):
|
||||
if radiobox is self.form1.radio1:
|
||||
self.select = 0
|
||||
self.form1.gbLocalFile.setVisible(True)
|
||||
elif radiobox is self.form1.radio2:
|
||||
self.select = 1
|
||||
self.form1.gbLocalFile.setVisible(True)
|
||||
|
||||
def accept(self):
|
||||
from datetime import datetime
|
||||
starttime = datetime.now()
|
||||
|
||||
site = PVPlantSite.get()
|
||||
|
||||
try:
|
||||
PointGroups = FreeCAD.ActiveDocument.Point_Groups
|
||||
except:
|
||||
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
|
||||
PointGroups.Label = "Point Groups"
|
||||
|
||||
PointGroup = FreeCAD.ActiveDocument.addObject('Points::Feature', "Point_Group")
|
||||
PointGroup.Label = "Land_Grid_Points"
|
||||
FreeCAD.ActiveDocument.Point_Groups.addObject(PointGroup)
|
||||
PointObject = PointGroup.Points.copy()
|
||||
|
||||
if self.select == 0: # Google or bing or ...
|
||||
#for item in self.obj:
|
||||
#if self.groupbox.isChecked:break
|
||||
resol = FreeCAD.Units.Quantity(self.valueResolution.text()).Value
|
||||
Site = FreeCAD.ActiveDocument.Site
|
||||
pts = getGridElevationFromBing(self.obj, Site.Latitude, Site.Longitude, resol)
|
||||
PointObject.addPoints(pts)
|
||||
PointGroup.Points = PointObject
|
||||
|
||||
else:
|
||||
if self.filename == "":
|
||||
return
|
||||
|
||||
import Utils.importDEM as openDEM
|
||||
if self.select == 1: # DEM.
|
||||
import numpy as np
|
||||
root, extension = os.path.splitext(self.filename)
|
||||
if extension.lower() == ".asc":
|
||||
x, y, datavals, cellsize, nodata_value = openDEM.openEsri(self.filename)
|
||||
|
||||
if self.Boundary:
|
||||
inc_x = self.Boundary.Shape.BoundBox.XLength * 0.05
|
||||
inc_y = self.Boundary.Shape.BoundBox.YLength * 0.05
|
||||
|
||||
min_x = 0
|
||||
max_x = 0
|
||||
|
||||
comp = (self.Boundary.Shape.BoundBox.XMin - inc_x) / 1000
|
||||
for i in range(nx):
|
||||
if x[i] > comp:
|
||||
min_x = i - 1
|
||||
break
|
||||
comp = (self.Boundary.Shape.BoundBox.XMax + inc_x) / 1000
|
||||
for i in range(min_x, nx):
|
||||
if x[i] > comp:
|
||||
max_x = i
|
||||
break
|
||||
|
||||
min_y = 0
|
||||
max_y = 0
|
||||
|
||||
comp = (self.Boundary.Shape.BoundBox.YMax + inc_y) / 1000
|
||||
for i in range(ny):
|
||||
if y[i] < comp:
|
||||
max_y = i
|
||||
break
|
||||
comp = (self.Boundary.Shape.BoundBox.YMin - inc_y) / 1000
|
||||
for i in range(max_y, ny):
|
||||
if y[i] < comp:
|
||||
min_y = i
|
||||
break
|
||||
|
||||
x = x[min_x:max_x]
|
||||
y = y[max_y:min_y]
|
||||
datavals = datavals[max_y:min_y, min_x:max_x]
|
||||
|
||||
pts = []
|
||||
if True: # faster but more memory 46s - 4,25 gb
|
||||
x, y = np.meshgrid(x, y)
|
||||
xx = x.flatten()
|
||||
yy = y.flatten()
|
||||
zz = datavals.flatten()
|
||||
x[:] = 0
|
||||
y[:] = 0
|
||||
datavals[:] = 0
|
||||
|
||||
pts = []
|
||||
for i in range(0, len(xx)):
|
||||
pts.append(FreeCAD.Vector(xx[i], yy[i], zz[i]) * 1000)
|
||||
|
||||
xx[:] = 0
|
||||
yy[:] = 0
|
||||
zz[:] = 0
|
||||
|
||||
else: # 51s 3,2 gb
|
||||
createmesh = True
|
||||
if createmesh:
|
||||
import Part, Draft
|
||||
|
||||
lines=[]
|
||||
for j in range(len(y)):
|
||||
edges = []
|
||||
for i in range(0, len(x) - 1):
|
||||
ed = Part.makeLine(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000,
|
||||
FreeCAD.Vector(x[i + 1], y[j], datavals[j][i + 1]) * 1000)
|
||||
edges.append(ed)
|
||||
|
||||
#bspline = Draft.makeBSpline(pts)
|
||||
#bspline.ViewObject.hide()
|
||||
line = Part.Wire(edges)
|
||||
lines.append(line)
|
||||
|
||||
'''
|
||||
for i in range(0, len(bsplines), 100):
|
||||
p = Part.makeLoft(bsplines[i:i + 100], False, False, False)
|
||||
Part.show(p)
|
||||
'''
|
||||
p = Part.makeLoft(lines, False, True, False)
|
||||
p = Part.Solid(p)
|
||||
Part.show(p)
|
||||
|
||||
else:
|
||||
pts = []
|
||||
for j in range(ny):
|
||||
for i in range(nx):
|
||||
pts.append(FreeCAD.Vector(x[i], y[j], datavals[j][i]) * 1000)
|
||||
|
||||
elif extension.lower() == ".csv" or extension.lower() == ".txt": # x, y, z from gps
|
||||
pts = openDEM.interpolatePoints(openDEM.openCSV(self.filename))
|
||||
|
||||
PointObject.addPoints(pts)
|
||||
PointGroup.Points = PointObject
|
||||
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
FreeCADGui.Control.closeDialog()
|
||||
print("tiempo: ", datetime.now() - starttime)
|
||||
|
||||
def reject(self):
|
||||
FreeCADGui.Control.closeDialog()
|
||||
|
||||
## Comandos -----------------------------------------------------------------------------------------------------------
|
||||
class CommandImportPoints:
|
||||
|
||||
def GetResources(self):
|
||||
return {'Pixmap': str(os.path.join(DirIcons, "cloud.svg")),
|
||||
'MenuText': QT_TRANSLATE_NOOP("PVPlant", "Importer Grid"),
|
||||
'Accel': "B, U",
|
||||
'ToolTip': QT_TRANSLATE_NOOP("PVPlant", "Creates a cloud of points.")}
|
||||
|
||||
def IsActive(self):
|
||||
return not FreeCAD.ActiveDocument is None
|
||||
|
||||
def Activated(self):
|
||||
self.TaskPanel = _ImportPointsTaskPanel()
|
||||
FreeCADGui.Control.showDialog(self.TaskPanel)
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
class CommandPointsGroup:
|
||||
|
||||
def GetCommands(self):
|
||||
return tuple(['ImportPoints'
|
||||
])
|
||||
def GetResources(self):
|
||||
return { 'MenuText': QT_TRANSLATE_NOOP("",'Cloud of Points'),
|
||||
'ToolTip': QT_TRANSLATE_NOOP("",'Cloud of Points')
|
||||
}
|
||||
def IsActive(self):
|
||||
return not FreeCAD.ActiveDocument is None
|
||||
|
||||
FreeCADGui.addCommand('ImportPoints', CommandImportPoints())
|
||||
FreeCADGui.addCommand('PointsGroup', CommandPointsGroup())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+5
-519
@@ -20,524 +20,10 @@
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import utm
|
||||
"""
|
||||
PVPlantGeoreferencing - Wrapper de compatibilidad.
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
Código movido a PVPlant/core/georef.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
else:
|
||||
# \cond
|
||||
def translate(ctxt,txt):
|
||||
return txt
|
||||
def QT_TRANSLATE_NOOP(ctxt,txt):
|
||||
return txt
|
||||
# \endcond
|
||||
|
||||
import PVPlantResources
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
from PVPlantResources import DirResources as DirResources
|
||||
|
||||
|
||||
class MapWindow(QtGui.QWidget):
|
||||
def __init__(self, WinTitle="MapWindow"):
|
||||
super(MapWindow, self).__init__()
|
||||
self.raise_()
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
self.minLat = None
|
||||
self.maxLat = None
|
||||
self.minLon = None
|
||||
self.maxLon = None
|
||||
self.zoom = None
|
||||
self.WinTitle = WinTitle
|
||||
self.georeference_coordinates = {'lat': None, 'lon': None}
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
from PySide2.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
|
||||
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
|
||||
|
||||
self.resize(1200, 800)
|
||||
self.setWindowTitle(self.WinTitle)
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(DirIcons, "Location.svg")))
|
||||
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
|
||||
|
||||
self.layout = QtGui.QHBoxLayout(self)
|
||||
self.layout.setContentsMargins(4, 4, 4, 4)
|
||||
|
||||
LeftWidget = QtGui.QWidget(self)
|
||||
LeftLayout = QtGui.QVBoxLayout(LeftWidget)
|
||||
LeftWidget.setLayout(LeftLayout)
|
||||
LeftLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
RightWidget = QtGui.QWidget(self)
|
||||
RightWidget.setFixedWidth(350)
|
||||
RightLayout = QtGui.QVBoxLayout(RightWidget)
|
||||
RightWidget.setLayout(RightLayout)
|
||||
RightLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.layout.addWidget(LeftWidget)
|
||||
self.layout.addWidget(RightWidget)
|
||||
|
||||
# Left Widgets:
|
||||
# -- Search Bar:
|
||||
self.valueSearch = QtGui.QLineEdit(self)
|
||||
self.valueSearch.setPlaceholderText("Search")
|
||||
self.valueSearch.returnPressed.connect(self.onSearch)
|
||||
|
||||
searchbutton = QtGui.QPushButton('Search')
|
||||
searchbutton.setFixedWidth(80)
|
||||
searchbutton.clicked.connect(self.onSearch)
|
||||
|
||||
SearchBarLayout = QtGui.QHBoxLayout(self)
|
||||
SearchBarLayout.addWidget(self.valueSearch)
|
||||
SearchBarLayout.addWidget(searchbutton)
|
||||
LeftLayout.addLayout(SearchBarLayout)
|
||||
|
||||
# -- Webbroser:
|
||||
self.view = QWebEngineView()
|
||||
self.channel = QWebChannel(self.view.page())
|
||||
self.view.page().setWebChannel(self.channel)
|
||||
self.channel.registerObject("MyApp", self)
|
||||
file = os.path.join(DirResources, "webs", "main.html")
|
||||
self.view.page().loadFinished.connect(self.onLoadFinished)
|
||||
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
|
||||
LeftLayout.addWidget(self.view)
|
||||
# self.layout.addWidget(self.view, 1, 0, 1, 3)
|
||||
|
||||
# -- Latitud y longitud:
|
||||
self.labelCoordinates = QtGui.QLabel()
|
||||
self.labelCoordinates.setFixedHeight(21)
|
||||
LeftLayout.addWidget(self.labelCoordinates)
|
||||
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
|
||||
|
||||
# Right Widgets:
|
||||
labelKMZ = QtGui.QLabel()
|
||||
labelKMZ.setText("Cargar un archivo KMZ/KML:")
|
||||
self.kmlButton = QtGui.QPushButton()
|
||||
self.kmlButton.setFixedSize(32, 32)
|
||||
self.kmlButton.setIcon(QtGui.QIcon(os.path.join(DirIcons, "googleearth.svg")))
|
||||
widget = QtGui.QWidget(self)
|
||||
layout = QtGui.QHBoxLayout(widget)
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(labelKMZ)
|
||||
layout.addWidget(self.kmlButton)
|
||||
RightLayout.addWidget(widget)
|
||||
|
||||
# -----------------------
|
||||
self.groupbox = QtGui.QGroupBox("Importar datos desde:")
|
||||
self.groupbox.setCheckable(True)
|
||||
self.groupbox.setChecked(True)
|
||||
radio1 = QtGui.QRadioButton("Google Elevation")
|
||||
radio2 = QtGui.QRadioButton("Nube de Puntos")
|
||||
radio3 = QtGui.QRadioButton("Datos GPS")
|
||||
radio1.setChecked(True)
|
||||
|
||||
# buttonDialog = QtGui.QPushButton('...')
|
||||
# buttonDialog.setEnabled(False)
|
||||
|
||||
vbox = QtGui.QVBoxLayout(self)
|
||||
vbox.addWidget(radio1)
|
||||
vbox.addWidget(radio2)
|
||||
vbox.addWidget(radio3)
|
||||
|
||||
self.groupbox.setLayout(vbox)
|
||||
RightLayout.addWidget(self.groupbox)
|
||||
# ------------------------
|
||||
|
||||
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
|
||||
RightLayout.addWidget(self.checkboxImportGis)
|
||||
|
||||
self.checkboxImportSatelitalImagen = QtGui.QCheckBox("Importar Imagen Satelital")
|
||||
RightLayout.addWidget(self.checkboxImportSatelitalImagen)
|
||||
|
||||
verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
RightLayout.addItem(verticalSpacer)
|
||||
|
||||
self.bAccept = QtGui.QPushButton('Accept')
|
||||
self.bAccept.clicked.connect(self.onAcceptClick)
|
||||
RightLayout.addWidget(self.bAccept)
|
||||
|
||||
# signals/slots
|
||||
QtCore.QObject.connect(self.kmlButton, QtCore.SIGNAL("clicked()"), self.importKML)
|
||||
|
||||
|
||||
def onLoadFinished(self):
|
||||
file = os.path.join(DirResources, "webs", "map.js")
|
||||
frame = self.view.page()
|
||||
with open(file, 'r') as f:
|
||||
frame.runJavaScript(f.read())
|
||||
|
||||
def onSearch(self):
|
||||
if self.valueSearch.text() == "":
|
||||
return
|
||||
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
geolocator = Nominatim(user_agent="http")
|
||||
location = geolocator.geocode(self.valueSearch.text())
|
||||
self.valueSearch.setText(location.address)
|
||||
self.panMap(location.longitude, location.latitude, location.raw['boundingbox'])
|
||||
|
||||
def onAcceptClick(self):
|
||||
frame = self.view.page()
|
||||
# 1. georeferenciar
|
||||
frame.runJavaScript(
|
||||
"MyApp.georeference(drawnItems.getBounds().getCenter().lat, drawnItems.getBounds().getCenter().lng);"
|
||||
)
|
||||
|
||||
# 2. importar todos los elementos dibujados:
|
||||
frame.runJavaScript(
|
||||
"var data = drawnItems.toGeoJSON();"
|
||||
"MyApp.shapes(JSON.stringify(data));"
|
||||
)
|
||||
|
||||
self.close()
|
||||
|
||||
@QtCore.Slot(float, float)
|
||||
def onMapMove(self, lat, lng):
|
||||
self.lat = lat
|
||||
self.lon = lng
|
||||
x, y, zone_number, zone_letter = utm.from_latlon(lat, lng)
|
||||
self.labelCoordinates.setText('Longitud: {:.5f}, Latitud: {:.5f}'.format(lng, lat) +
|
||||
' | UTM: ' + str(zone_number) + zone_letter +
|
||||
', {:.5f}m E, {:.5f}m N'.format(x, y))
|
||||
|
||||
@QtCore.Slot(float, float, float, float, int)
|
||||
def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
|
||||
self.minLat = min([minLat, maxLat])
|
||||
self.maxLat = max([minLat, maxLat])
|
||||
self.minLon = min([minLon, maxLon])
|
||||
self.maxLon = max([minLon, maxLon])
|
||||
self.zoom = zoom
|
||||
|
||||
@QtCore.Slot(float, float)
|
||||
def georeference(self, lat, lng):
|
||||
import PVPlantSite
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
self.georeference_coordinates['lat'] = lat
|
||||
self.georeference_coordinates['lon'] = lng
|
||||
|
||||
Site = PVPlantSite.get(create=True)
|
||||
Site.Proxy.setLatLon(lat, lng)
|
||||
|
||||
geolocator = Nominatim(user_agent="http")
|
||||
location = geolocator.reverse('{:.5f}, {:.5f}'.format(lat, lng))
|
||||
if location:
|
||||
if location.raw["address"].get("road"):
|
||||
str = location.raw["address"]["road"]
|
||||
if location.raw["address"].get("house_number"):
|
||||
str += ' ({0})'.format(location.raw["address"]["house_number"])
|
||||
Site.Address = str
|
||||
if location.raw["address"].get("city"):
|
||||
Site.City = location.raw["address"]["city"]
|
||||
if location.raw["address"].get("postcode"):
|
||||
Site.PostalCode = location.raw["address"]["postcode"]
|
||||
if location.raw["address"].get("address"):
|
||||
Site.Region = '{0}'.format(location.raw["address"]["province"])
|
||||
if location.raw["address"].get("state"):
|
||||
if Site.Region != "":
|
||||
Site.Region += " - "
|
||||
Site.Region += '{0}'.format(location.raw["address"]["state"]) # province - state
|
||||
Site.Country = location.raw["address"]["country"]
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def shapes(self, drawnItems):
|
||||
import geojson
|
||||
import PVPlantImportGrid as ImportElevation
|
||||
import Draft
|
||||
import PVPlantSite
|
||||
Site = PVPlantSite.get()
|
||||
|
||||
offset = FreeCAD.Vector(0, 0, 0)
|
||||
if not (self.lat is None or self.lon is None):
|
||||
offset = FreeCAD.Vector(Site.Origin)
|
||||
offset.z = 0
|
||||
|
||||
items = geojson.loads(drawnItems)
|
||||
for item in items['features']:
|
||||
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
|
||||
coord = item['geometry']['coordinates']
|
||||
point = ImportElevation.getElevationFromOE([[coord[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())
|
||||
'''
|
||||
from PVPlant.core.georef import MapWindow, CommandPVPlantGeoreferencing
|
||||
+21
-671
@@ -20,674 +20,24 @@
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
import Draft
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtGui
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
import os
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
import PVPlantSite
|
||||
|
||||
|
||||
def get_elevation_from_oe(coordinates): # v1 deepseek
|
||||
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
|
||||
Args:
|
||||
coordinates (list): Lista de tuplas con coordenadas (latitud, longitud)
|
||||
Returns:
|
||||
list: Lista de vectores FreeCAD con coordenadas UTM y elevación (en milímetros)
|
||||
o lista vacía en caso de error.
|
||||
"""
|
||||
if not coordinates:
|
||||
return []
|
||||
|
||||
import requests
|
||||
import utm
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Construcción más eficiente de parámetros
|
||||
locations = "|".join([f"{lat:.6f},{lon:.6f}" for lat, lon in coordinates])
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
url="https://api.open-elevation.com/api/v1/lookup",
|
||||
params={'locations': locations},
|
||||
timeout=20,
|
||||
verify=True
|
||||
)
|
||||
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx
|
||||
|
||||
except RequestException as e:
|
||||
print(f"Error en la solicitud: {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:
|
||||
# Conversión UTM con manejo de errores
|
||||
easting, northing, _, _ = utm.from_latlon(
|
||||
result["latitude"],
|
||||
result["longitude"]
|
||||
)
|
||||
|
||||
points.append(FreeCAD.Vector(round(easting), # Convertir metros a milímetros
|
||||
round(northing),
|
||||
round(result["elevation"])) * 1000)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error procesando coordenadas: {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
|
||||
import utm
|
||||
|
||||
locations_str=""
|
||||
total = len(coordinates) - 1
|
||||
for i, point in enumerate(coordinates):
|
||||
locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
|
||||
if i != total:
|
||||
locations_str += '|'
|
||||
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
|
||||
try:
|
||||
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
|
||||
except RequestException as e:
|
||||
print(f"Error en la solicitud: {str(e)}")
|
||||
points = []
|
||||
for i, point in enumerate(coordinates):
|
||||
c = utm.from_latlon(point[0], point[1])
|
||||
points.append(FreeCAD.Vector(round(c[0], 0),
|
||||
round(c[1], 0),
|
||||
0) * 1000)
|
||||
return points
|
||||
|
||||
# Only get the json response in case of 200 or 201
|
||||
points = []
|
||||
if r.status_code == 200 or r.status_code == 201:
|
||||
results = r.json()
|
||||
for point in results["results"]:
|
||||
c = utm.from_latlon(point["latitude"], point["longitude"])
|
||||
v = FreeCAD.Vector(round(c[0], 0),
|
||||
round(c[1], 0),
|
||||
round(point["elevation"], 0)) * 1000
|
||||
points.append(v)
|
||||
return points
|
||||
|
||||
def getSinglePointElevationFromBing(lat, lng):
|
||||
#http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey}
|
||||
import utm
|
||||
|
||||
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
|
||||
source += str(lat) + "," + str(lng)
|
||||
source += "&heights=sealevel"
|
||||
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
|
||||
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
s = json.loads(ans)
|
||||
print(s)
|
||||
res = s['resourceSets'][0]['resources'][0]['elevations']
|
||||
for elevation in res:
|
||||
c = utm.from_latlon(lat, lng)
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0] * 1000, 0),
|
||||
round(c[1] * 1000, 0),
|
||||
round(elevation * 1000, 0))
|
||||
return v
|
||||
|
||||
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
|
||||
#http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=35.89431,-110.72522,35.89393,-110.72578,35.89374,-110.72606,35.89337,-110.72662
|
||||
# &heights=ellipsoid&samples=10&key={BingMapsAPIKey}
|
||||
import utm
|
||||
import math
|
||||
import requests
|
||||
|
||||
|
||||
geo = utm.from_latlon(lat, lng)
|
||||
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
|
||||
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
|
||||
|
||||
#StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
xx = polygon.Shape.BoundBox.XMin
|
||||
while xx < polygon.Shape.BoundBox.XMax:
|
||||
StepsXX = int(math.ceil((polygon.Shape.BoundBox.XMax - xx) / resolution))
|
||||
|
||||
if StepsXX > 1000:
|
||||
StepsXX = 1000
|
||||
xx1 = xx + 1000 * resolution
|
||||
else:
|
||||
xx1 = xx + StepsXX * resolution
|
||||
|
||||
point1 = utm.to_latlon(xx / 1000, yy / 1000, geo[2], geo[3])
|
||||
point2 = utm.to_latlon(xx1 / 1000, yy / 1000, geo[2], geo[3])
|
||||
|
||||
source = "http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points="
|
||||
source += "{lat1},{lng1}".format(lat1=point1[0], lng1=point1[1])
|
||||
source += ","
|
||||
source += "{lat2},{lng2}".format(lat2=point2[0], lng2=point2[1])
|
||||
source += "&heights=sealevel"
|
||||
source += "&samples={steps}".format(steps=StepsXX)
|
||||
source += "&key=AmsPZA-zRt2iuIdQgvXZIxme2gWcgLaz7igOUy7VPB8OKjjEd373eCnj1KFv2CqX"
|
||||
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['resourceSets'][0]['resources'][0]['elevations']
|
||||
|
||||
i = 0
|
||||
for elevation in res:
|
||||
v = FreeCAD.Vector(xx + resolution * i, yy, round(elevation * 1000, 4))
|
||||
points.append(v)
|
||||
i += 1
|
||||
xx = xx1 + resolution # para no repetir un mismo punto
|
||||
yy -= resolution
|
||||
|
||||
return points
|
||||
|
||||
def getSinglePointElevation(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
#print (source)
|
||||
|
||||
#response = request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
from geopy.distance import geodesic
|
||||
for r in res:
|
||||
|
||||
reference = (0.0, 0.0)
|
||||
v = FreeCAD.Vector(
|
||||
round(geodesic(reference, (0.0, r['location']['lng'])).m, 2),
|
||||
round(geodesic(reference, (r['location']['lat'], 0.0)).m, 2),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
def _getSinglePointElevation(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
#print (source)
|
||||
|
||||
#response = request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
import pymap3d as pm
|
||||
for r in res:
|
||||
x, y, z = pm.geodetic2ecef(round(r['location']['lng'], 2),
|
||||
round(r['location']['lat'], 2),
|
||||
0)
|
||||
v = FreeCAD.Vector(x,y,z)
|
||||
|
||||
return v
|
||||
|
||||
def getSinglePointElevation1(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
#response = urllib.request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
for r in res:
|
||||
c = tm.fromGeographic(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0], 4),
|
||||
round(c[1], 4),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
return v
|
||||
|
||||
def getSinglePointElevationUtm(lat, lon):
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?locations="
|
||||
source += str(lat) + "," + str(lon)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
print(source)
|
||||
|
||||
#response = urllib.request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
print (res)
|
||||
|
||||
import utm
|
||||
for r in res:
|
||||
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0] * 1000, 4),
|
||||
round(c[1] * 1000, 4),
|
||||
round(r['elevation'] * 1000, 2))
|
||||
print (v)
|
||||
return v
|
||||
|
||||
def getElevationUTM(polygon, lat, lng, resolution = 10000):
|
||||
|
||||
import utm
|
||||
geo = utm.from_latlon(lat, lng)
|
||||
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
|
||||
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
|
||||
|
||||
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution*1000))
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
# utm.to_latlon(EASTING, NORTHING, ZONE NUMBER, ZONE LETTER).
|
||||
# result = (LATITUDE, LONGITUDE)
|
||||
point1 = utm.to_latlon(polygon.Shape.BoundBox.XMin / 1000, yy / 1000, geo[2], geo[3])
|
||||
point2 = utm.to_latlon(polygon.Shape.BoundBox.XMax / 1000, yy / 1000, geo[2], geo[3])
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
|
||||
source += "{a},{b}".format(a = point1[0], b = point1[1])
|
||||
source += "|"
|
||||
source += "{a},{b}".format(a = point2[0], b = point2[1])
|
||||
source += "&samples={a}".format(a = StepsXX)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
|
||||
for r in res:
|
||||
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
|
||||
v = FreeCAD.Vector(
|
||||
round(c[0] * 1000, 2),
|
||||
round(c[1] * 1000, 2),
|
||||
round(r['elevation'] * 1000, 2)
|
||||
)
|
||||
points.append(v)
|
||||
yy -= (resolution*1000)
|
||||
|
||||
FreeCAD.activeDocument().recompute()
|
||||
return points
|
||||
|
||||
def getElevation1(polygon,resolution=10):
|
||||
|
||||
StepsXX = int((polygon.Shape.BoundBox.XMax - polygon.Shape.BoundBox.XMin) / (resolution * 1000))
|
||||
points = []
|
||||
yy = polygon.Shape.BoundBox.YMax
|
||||
while yy > polygon.Shape.BoundBox.YMin:
|
||||
point1 = tm.toGeographic(polygon.Shape.BoundBox.XMin, yy)
|
||||
point2 = tm.toGeographic(polygon.Shape.BoundBox.XMax, yy)
|
||||
|
||||
source = "https://maps.googleapis.com/maps/api/elevation/json?path="
|
||||
source += "{a},{b}".format(a = point1[0], b = point1[1])
|
||||
source += "|"
|
||||
source += "{a},{b}".format(a = point2[0], b = point2[1])
|
||||
source += "&samples={a}".format(a = StepsXX)
|
||||
source += "&key=AIzaSyB07X6lowYJ-iqyPmaFJvr-6zp1J63db8U"
|
||||
|
||||
try:
|
||||
#response = urllib.request.urlopen(source)
|
||||
#ans = response.read()
|
||||
import requests
|
||||
response = requests.get(source)
|
||||
ans = response.text
|
||||
|
||||
# +# to do: error handling - wait and try again
|
||||
|
||||
s = json.loads(ans)
|
||||
res = s['results']
|
||||
except:
|
||||
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,
|
||||
)
|
||||
+9
-2306
File diff suppressed because it is too large
Load Diff
+27
-1172
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -652,6 +652,9 @@ if FreeCAD.GuiUp:
|
||||
from Civil.Fence import PVPlantFence
|
||||
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
|
||||
|
||||
import docgenerator
|
||||
FreeCADGui.addCommand('GenerateDocuments', docgenerator.generateDocuments())
|
||||
|
||||
projectlist = [ # "Reload",
|
||||
"PVPlantSite",
|
||||
"ProjectSetup",
|
||||
@@ -687,4 +690,6 @@ pv_mechanical = [
|
||||
]
|
||||
|
||||
objectlist = ['PVPlantTree',
|
||||
'PVPlantFenceGroup',]
|
||||
'PVPlantFenceGroup',
|
||||
'GenerateDocuments',
|
||||
]
|
||||
@@ -4,14 +4,17 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
|
||||
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw.css">
|
||||
<script src="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw-src.js"></script>
|
||||
<!-- 1. Core Leaflet library -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/togeojson@0.16.0"></script>
|
||||
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
|
||||
<!-- 2. Leaflet.draw Plugin (MUST be loaded AFTER Leaflet) -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
|
||||
<!-- 3. Other plugins -->
|
||||
<script src="https://unpkg.com/togeojson@0.16.0"></script>
|
||||
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
|
||||
|
||||
<!--script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script-->
|
||||
<script type="text/javascript" src="./qwebchannel.js"></script>
|
||||
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
# Script para FreeCAD - Procesador de Documentos Word con Carátula
|
||||
import os
|
||||
import glob
|
||||
from PySide2 import QtWidgets, QtCore
|
||||
from PySide2.QtWidgets import (QFileDialog, QMessageBox, QProgressDialog,
|
||||
QApplication, QVBoxLayout, QWidget, QPushButton,
|
||||
QLabel, QTextEdit)
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
import PVPlantResources
|
||||
from PVPlantResources import DirIcons as DirIcons
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
DOCX_AVAILABLE = True
|
||||
except ImportError:
|
||||
DOCX_AVAILABLE = False
|
||||
FreeCAD.Console.PrintError("Error: python-docx no está instalado. Instala con: pip install python-docx\n")
|
||||
|
||||
|
||||
class DocumentProcessor(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(DocumentProcessor, self).__init__(parent)
|
||||
self.caratula_path = ""
|
||||
self.carpeta_path = ""
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Procesador de Documentos Word")
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(500)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Título
|
||||
title = QLabel("<h2>Procesador de Documentos Word</h2>")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Información
|
||||
info_text = QLabel(
|
||||
"Este script buscará recursivamente todos los archivos .docx en una carpeta,\n"
|
||||
"insertará una carátula y aplicará formato estándar a todos los documentos."
|
||||
)
|
||||
info_text.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(info_text)
|
||||
|
||||
# Botón seleccionar carátula
|
||||
self.btn_caratula = QPushButton("1. Seleccionar Carátula")
|
||||
self.btn_caratula.clicked.connect(self.seleccionar_caratula)
|
||||
layout.addWidget(self.btn_caratula)
|
||||
|
||||
self.label_caratula = QLabel("No se ha seleccionado carátula")
|
||||
self.label_caratula.setWordWrap(True)
|
||||
layout.addWidget(self.label_caratula)
|
||||
|
||||
# Botón seleccionar carpeta
|
||||
self.btn_carpeta = QPushButton("2. Seleccionar Carpeta de Documentos")
|
||||
self.btn_carpeta.clicked.connect(self.seleccionar_carpeta)
|
||||
layout.addWidget(self.btn_carpeta)
|
||||
|
||||
self.label_carpeta = QLabel("No se ha seleccionado carpeta")
|
||||
self.label_carpeta.setWordWrap(True)
|
||||
layout.addWidget(self.label_carpeta)
|
||||
|
||||
# Botón procesar
|
||||
self.btn_procesar = QPushButton("3. Procesar Documentos")
|
||||
self.btn_procesar.clicked.connect(self.procesar_documentos)
|
||||
self.btn_procesar.setEnabled(False)
|
||||
layout.addWidget(self.btn_procesar)
|
||||
|
||||
# Área de log
|
||||
self.log_area = QTextEdit()
|
||||
self.log_area.setReadOnly(True)
|
||||
layout.addWidget(self.log_area)
|
||||
|
||||
# Botón cerrar
|
||||
self.btn_cerrar = QPushButton("Cerrar")
|
||||
self.btn_cerrar.clicked.connect(self.close)
|
||||
layout.addWidget(self.btn_cerrar)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def log(self, mensaje):
|
||||
"""Agrega un mensaje al área de log"""
|
||||
self.log_area.append(mensaje)
|
||||
QApplication.processEvents() # Para actualizar la UI
|
||||
|
||||
def seleccionar_caratula(self):
|
||||
"""Abre un diálogo para seleccionar el archivo de carátula"""
|
||||
archivo, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Seleccionar archivo de carátula",
|
||||
"",
|
||||
"Word documents (*.docx);;All files (*.*)"
|
||||
)
|
||||
|
||||
if archivo and os.path.exists(archivo):
|
||||
self.caratula_path = archivo
|
||||
self.label_caratula.setText(f"Carátula: {os.path.basename(archivo)}")
|
||||
self.verificar_estado()
|
||||
self.log(f"✓ Carátula seleccionada: {archivo}")
|
||||
|
||||
def seleccionar_carpeta(self):
|
||||
"""Abre un diálogo para seleccionar la carpeta de documentos"""
|
||||
carpeta = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Seleccionar carpeta con documentos"
|
||||
)
|
||||
|
||||
if carpeta:
|
||||
self.carpeta_path = carpeta
|
||||
self.label_carpeta.setText(f"Carpeta: {carpeta}")
|
||||
self.verificar_estado()
|
||||
self.log(f"✓ Carpeta seleccionada: {carpeta}")
|
||||
|
||||
def verificar_estado(self):
|
||||
"""Habilita el botón procesar si ambos paths están seleccionados"""
|
||||
if self.caratula_path and self.carpeta_path:
|
||||
self.btn_procesar.setEnabled(True)
|
||||
|
||||
def buscar_docx_recursivamente(self, carpeta):
|
||||
"""Busca recursivamente todos los archivos .docx en una carpeta"""
|
||||
archivos_docx = []
|
||||
patron = os.path.join(carpeta, "**", "*.docx")
|
||||
|
||||
for archivo in glob.glob(patron, recursive=True):
|
||||
archivos_docx.append(archivo)
|
||||
|
||||
return archivos_docx
|
||||
|
||||
def aplicar_formato_estandar(self, doc):
|
||||
"""Aplica formato estándar al documento"""
|
||||
try:
|
||||
# Configurar estilos por defecto
|
||||
style = doc.styles['Normal']
|
||||
font = style.font
|
||||
font.name = 'Arial'
|
||||
font.size = Pt(11)
|
||||
font.color.rgb = RGBColor(0, 0, 0) # Negro
|
||||
|
||||
# Configurar encabezados
|
||||
try:
|
||||
heading_style = doc.styles['Heading 1']
|
||||
heading_font = heading_style.font
|
||||
heading_font.name = 'Arial'
|
||||
heading_font.size = Pt(14)
|
||||
heading_font.bold = True
|
||||
heading_font.color.rgb = RGBColor(0, 51, 102) # Azul oscuro
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ⚠ Advertencia en formato: {str(e)}")
|
||||
|
||||
def aplicar_formato_avanzado(self, doc):
|
||||
"""Aplica formato más avanzado y personalizado"""
|
||||
try:
|
||||
# Configurar márgenes
|
||||
sections = doc.sections
|
||||
for section in sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
|
||||
# Configurar estilos de párrafo
|
||||
for paragraph in doc.paragraphs:
|
||||
paragraph.paragraph_format.space_after = Pt(6)
|
||||
paragraph.paragraph_format.space_before = Pt(0)
|
||||
paragraph.paragraph_format.line_spacing = 1.15
|
||||
|
||||
# Alinear párrafos justificados
|
||||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
|
||||
# Aplicar fuente específica a cada run
|
||||
for run in paragraph.runs:
|
||||
run.font.name = 'Arial'
|
||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Arial')
|
||||
run.font.size = Pt(11)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ⚠ Advertencia en formato avanzado: {str(e)}")
|
||||
|
||||
def insertar_caratula_y_formatear(self, archivo_docx, archivo_caratula):
|
||||
"""Inserta la carátula y aplica formato al documento"""
|
||||
try:
|
||||
# Abrir el documento de carátula
|
||||
doc_caratula = Document(archivo_caratula)
|
||||
|
||||
# Abrir el documento destino
|
||||
doc_destino = Document(archivo_docx)
|
||||
|
||||
# Crear un nuevo documento que contendrá la carátula + contenido original
|
||||
nuevo_doc = Document()
|
||||
|
||||
# Copiar todo el contenido de la carátula
|
||||
for elemento in doc_caratula.element.body:
|
||||
print(elemento)
|
||||
nuevo_doc.element.body.append(elemento)
|
||||
|
||||
# Agregar un salto de página después de la carátula
|
||||
nuevo_doc.add_page_break()
|
||||
|
||||
# Copiar todo el contenido del documento original
|
||||
for elemento in doc_destino.element.body:
|
||||
nuevo_doc.element.body.append(elemento)
|
||||
|
||||
# Aplicar formatos
|
||||
self.aplicar_formato_estandar(nuevo_doc)
|
||||
self.aplicar_formato_avanzado(nuevo_doc)
|
||||
|
||||
# Guardar el documento (sobrescribir el original)
|
||||
nombre_base = os.path.splitext(os.path.basename(archivo_docx))[0]
|
||||
extension = os.path.splitext(archivo_docx)[1]
|
||||
name = f"{nombre_base}{extension}"
|
||||
nuevo_docx = os.path.join(self.output_carpeta, name)
|
||||
nuevo_doc.save(nuevo_docx)
|
||||
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def procesar_documentos(self):
|
||||
"""Función principal que orquesta todo el proceso"""
|
||||
if not DOCX_AVAILABLE:
|
||||
QMessageBox.critical(self, "Error",
|
||||
"La biblioteca python-docx no está disponible.\n\n"
|
||||
"Instala con: pip install python-docx")
|
||||
return
|
||||
|
||||
# Verificar paths
|
||||
if not os.path.exists(self.caratula_path):
|
||||
QMessageBox.warning(self, "Error", "El archivo de carátula no existe.")
|
||||
return
|
||||
|
||||
if not os.path.exists(self.carpeta_path):
|
||||
QMessageBox.warning(self, "Error", "La carpeta de documentos no existe.")
|
||||
return
|
||||
|
||||
self.log("\n=== INICIANDO PROCESAMIENTO ===")
|
||||
self.log(f"Carátula: {self.caratula_path}")
|
||||
self.log(f"Carpeta: {self.carpeta_path}")
|
||||
|
||||
directorio_padre = os.path.dirname(self.carpeta_path)
|
||||
self.output_carpeta = os.path.join(directorio_padre, "03.Outputs")
|
||||
os.makedirs(self.output_carpeta, exist_ok=True)
|
||||
|
||||
# Buscar archivos .docx
|
||||
self.log("Buscando archivos .docx...")
|
||||
archivos_docx = self.buscar_docx_recursivamente(self.carpeta_path)
|
||||
|
||||
if not archivos_docx:
|
||||
self.log("No se encontraron archivos .docx en la carpeta seleccionada.")
|
||||
QMessageBox.information(self, "Información",
|
||||
"No se encontraron archivos .docx en la carpeta seleccionada.")
|
||||
return
|
||||
|
||||
self.log(f"Se encontraron {len(archivos_docx)} archivos .docx")
|
||||
|
||||
# Crear diálogo de progreso
|
||||
progress = QProgressDialog("Procesando documentos...", "Cancelar", 0, len(archivos_docx), self)
|
||||
progress.setWindowTitle("Procesando")
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.show()
|
||||
|
||||
# Procesar cada archivo
|
||||
exitosos = 0
|
||||
fallidos = 0
|
||||
errores_detallados = []
|
||||
|
||||
for i, archivo_docx in enumerate(archivos_docx):
|
||||
if progress.wasCanceled():
|
||||
self.log("Proceso cancelado por el usuario.")
|
||||
break
|
||||
|
||||
progress.setValue(i)
|
||||
progress.setLabelText(f"Procesando {i + 1}/{len(archivos_docx)}: {os.path.basename(archivo_docx)}")
|
||||
QApplication.processEvents()
|
||||
|
||||
self.log(f"Procesando: {os.path.basename(archivo_docx)}")
|
||||
|
||||
success, error_msg = self.insertar_caratula_y_formatear(archivo_docx, self.caratula_path)
|
||||
|
||||
if success:
|
||||
self.log(f" ✓ Completado")
|
||||
exitosos += 1
|
||||
else:
|
||||
self.log(f" ✗ Error: {error_msg}")
|
||||
fallidos += 1
|
||||
errores_detallados.append(f"{os.path.basename(archivo_docx)}: {error_msg}")
|
||||
|
||||
progress.setValue(len(archivos_docx))
|
||||
|
||||
# Mostrar resumen
|
||||
self.log("\n=== RESUMEN ===")
|
||||
self.log(f"Documentos procesados exitosamente: {exitosos}")
|
||||
self.log(f"Documentos con errores: {fallidos}")
|
||||
self.log(f"Total procesados: {exitosos + fallidos}")
|
||||
|
||||
# Mostrar mensaje final
|
||||
mensaje = (f"Procesamiento completado:\n"
|
||||
f"✓ Exitosos: {exitosos}\n"
|
||||
f"✗ Fallidos: {fallidos}\n"
|
||||
f"Total: {len(archivos_docx)}")
|
||||
|
||||
if fallidos > 0:
|
||||
mensaje += f"\n\nErrores encontrados:\n" + "\n".join(
|
||||
errores_detallados[:5]) # Mostrar solo primeros 5 errores
|
||||
if len(errores_detallados) > 5:
|
||||
mensaje += f"\n... y {len(errores_detallados) - 5} más"
|
||||
|
||||
QMessageBox.information(self, "Proceso Completado", mensaje)
|
||||
|
||||
|
||||
# Función para ejecutar desde FreeCAD
|
||||
def run_document_processor():
|
||||
"""Función principal para ejecutar el procesador desde FreeCAD"""
|
||||
# Verificar si python-docx está disponible
|
||||
if not DOCX_AVAILABLE:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setText("Biblioteca python-docx no encontrada")
|
||||
msg.setInformativeText(
|
||||
"Para usar este script necesitas instalar python-docx:\n\n"
|
||||
"1. Abre la consola de FreeCAD\n"
|
||||
"2. Ejecuta: import subprocess, sys\n"
|
||||
"3. Ejecuta: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-docx'])\n\n"
|
||||
"O instala desde una terminal externa con: pip install python-docx"
|
||||
)
|
||||
msg.setWindowTitle("Dependencia faltante")
|
||||
msg.exec_()
|
||||
return
|
||||
|
||||
# Crear y mostrar la interfaz
|
||||
dialog = DocumentProcessor(FreeCADGui.getMainWindow())
|
||||
dialog.exec_()
|
||||
|
||||
class generateDocuments:
|
||||
def GetResources(self):
|
||||
return {'Pixmap': str(os.path.join(DirIcons, "house.svg")),
|
||||
'MenuText': "DocumentGenerator",
|
||||
'Accel': "D, G",
|
||||
'ToolTip': "Creates a Building object from setup dialog."}
|
||||
|
||||
def IsActive(self):
|
||||
return not FreeCAD.ActiveDocument is None
|
||||
|
||||
|
||||
def Activated(self):
|
||||
run_document_processor()
|
||||
@@ -0,0 +1,139 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2026 Javier Brana <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify*
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
|
||||
# * USA *
|
||||
# * *
|
||||
# ***********************************************************************
|
||||
|
||||
"""
|
||||
Proyecciones y transformaciones geodésicas unificadas para PVPlant.
|
||||
|
||||
Reemplaza el uso disperso de la librería 'utm' con pyproj (PROJ),
|
||||
soporte multi-zona UTM y transformaciones entre datums.
|
||||
|
||||
Uso básico:
|
||||
from lib.projection import latlon_to_utm, utm_to_latlon, get_utm_zone
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
from pyproj import CRS, Transformer
|
||||
from pyproj.aoi import AreaOfInterest
|
||||
from pyproj.database import query_utm_crs_info
|
||||
|
||||
# WGS84 – sistema de coordenadas geográfico de referencia
|
||||
_WGS84 = CRS.from_epsg(4326)
|
||||
|
||||
# Cache de transformadores UTM por zona (lazy)
|
||||
_utm_transformers = {}
|
||||
|
||||
|
||||
def _get_utm_transformer(lat, lon):
|
||||
"""Obtiene (o crea en caché) un transformador UTM para la zona de las coordenadas dadas.
|
||||
Returns:
|
||||
tuple: (transformer, zone_number, zone_letter)
|
||||
"""
|
||||
# Determinar la zona UTM a partir de lat/lon
|
||||
zone_number = int((lon + 180) / 6) + 1
|
||||
|
||||
if lat >= 0:
|
||||
zone_letter = 'N'
|
||||
epsg = 32600 + zone_number
|
||||
else:
|
||||
zone_letter = 'S'
|
||||
epsg = 32700 + zone_number
|
||||
|
||||
cache_key = (zone_number, zone_letter)
|
||||
if cache_key not in _utm_transformers:
|
||||
utm_crs = CRS.from_epsg(epsg)
|
||||
_utm_transformers[cache_key] = Transformer.from_crs(
|
||||
_WGS84, utm_crs, always_xy=True
|
||||
)
|
||||
|
||||
return _utm_transformers[cache_key], zone_number, zone_letter
|
||||
|
||||
|
||||
def latlon_to_utm(lat, lon):
|
||||
"""Convierte coordenadas geográficas (WGS84) a UTM (este, norte, zona, letra).
|
||||
|
||||
Args:
|
||||
lat (float): Latitud en grados.
|
||||
lon (float): Longitud en grados.
|
||||
|
||||
Returns:
|
||||
tuple: (easting, northing, zone_number, zone_letter)
|
||||
easting/northing en metros.
|
||||
"""
|
||||
transformer, zone_number, zone_letter = _get_utm_transformer(lat, lon)
|
||||
easting, northing = transformer.transform(lon, lat)
|
||||
return easting, northing, zone_number, zone_letter
|
||||
|
||||
|
||||
def utm_to_latlon(easting, northing, zone_number, zone_letter='N'):
|
||||
"""Convierte coordenadas UTM a geográficas (WGS84).
|
||||
|
||||
Args:
|
||||
easting (float): Coordenada E en metros.
|
||||
northing (float): Coordenada N en metros.
|
||||
zone_number (int): Número de zona UTM (1-60).
|
||||
zone_letter (str): Letra de zona ('N' o 'S').
|
||||
|
||||
Returns:
|
||||
tuple: (latitude, longitude) en grados.
|
||||
"""
|
||||
if zone_letter.upper() not in ('N', 'S'):
|
||||
zone_letter = 'N'
|
||||
|
||||
epsg = 32600 + zone_number if zone_letter.upper() == 'N' else 32700 + zone_number
|
||||
utm_crs = CRS.from_epsg(epsg)
|
||||
transformer = Transformer.from_crs(utm_crs, _WGS84, always_xy=True)
|
||||
lon, lat = transformer.transform(easting, northing)
|
||||
return lat, lon
|
||||
|
||||
|
||||
def get_utm_zone(lat, lon):
|
||||
"""Obtiene la zona UTM para unas coordenadas dadas.
|
||||
|
||||
Args:
|
||||
lat (float): Latitud en grados.
|
||||
lon (float): Longitud en grados.
|
||||
|
||||
Returns:
|
||||
tuple: (zone_number, zone_letter)
|
||||
"""
|
||||
_, _, zone_number, zone_letter = latlon_to_utm(lat, lon)
|
||||
return zone_number, zone_letter
|
||||
|
||||
|
||||
def latlon_to_utm_vector(lat, lon, elevation=0.0):
|
||||
"""Convierte lat/lon/elevación a un FreeCAD.Vector en UTM (mm).
|
||||
|
||||
Args:
|
||||
lat (float): Latitud en grados.
|
||||
lon (float): Longitud en grados.
|
||||
elevation (float): Elevación en metros (default 0).
|
||||
|
||||
Returns:
|
||||
FreeCAD.Vector: Coordenadas UTM en milímetros.
|
||||
"""
|
||||
transformer, _, _ = _get_utm_transformer(lat, lon)
|
||||
easting, northing = transformer.transform(lon, lat)
|
||||
return FreeCAD.Vector(
|
||||
round(easting, 0),
|
||||
round(northing, 0),
|
||||
round(elevation, 0)
|
||||
) * 1000
|
||||
+20
-6
@@ -2,13 +2,27 @@
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>PVPlant</name>
|
||||
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
|
||||
<version>2025.11.20</version>
|
||||
<date>2025.11.20</date>
|
||||
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
|
||||
<version>2026.02.12</version>
|
||||
<date>2026.02.15</date>
|
||||
|
||||
<maintainer email="javier.branagutierrez@gmail.com">
|
||||
Javier Braña
|
||||
</maintainer>
|
||||
|
||||
<license file="LICENSE">LGPL-2.1-or-later</license>
|
||||
<url type="repository" branch="developed">https://homehud.duckdns.org/javier/PVPlant</url>
|
||||
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
|
||||
<url type="readme">https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md</url>
|
||||
|
||||
<url type="repository" branch="main">
|
||||
https://homehud.duckdns.org/javier/PVPlant
|
||||
</url>
|
||||
|
||||
<url type="bugtracker">
|
||||
https://homehud.duckdns.org/javier/PVPlant/issues
|
||||
</url>
|
||||
|
||||
<url type="readme">
|
||||
https://homehud.duckdns.org/javier/PVPlant/raw/branch/main/README.md
|
||||
</url>
|
||||
|
||||
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>
|
||||
|
||||
<content>
|
||||
|
||||
@@ -55,6 +55,10 @@ class _CommandReload:
|
||||
import hydro.hydrological as hydro
|
||||
import Importer.importOSM as iOSM
|
||||
|
||||
import docgenerator
|
||||
|
||||
importlib.reload(docgenerator)
|
||||
|
||||
importlib.reload(ProjectSetup)
|
||||
importlib.reload(PVPlantPlacement)
|
||||
importlib.reload(PVPlantImportGrid)
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ numpy~=1.26.2
|
||||
opencv-python~=4.8.1
|
||||
matplotlib~=3.8.2
|
||||
openpyxl~=3.1.2
|
||||
utm~=0.7.0
|
||||
PySide2~=5.15.8
|
||||
requests~=2.31.0
|
||||
setuptools~=68.2.2
|
||||
@@ -17,4 +16,4 @@ certifi~=2023.11.17
|
||||
SciPy~=1.11.4
|
||||
pycollada~=0.7.2
|
||||
shapely
|
||||
rtree
|
||||
rtree
|
||||
|
||||
Reference in New Issue
Block a user