This commit is contained in:
2025-07-06 01:12:08 +02:00
parent 74bf60101c
commit 5a642a4119
11 changed files with 1267 additions and 440 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>715</width> <width>462</width>
<height>520</height> <height>282</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -78,6 +78,11 @@
<string>Lineweight</string> <string>Lineweight</string>
</property> </property>
</column> </column>
<item row="2" column="1">
<property name="text">
<string/>
</property>
</item>
</widget> </widget>
</item> </item>
</layout> </layout>
@@ -204,6 +209,21 @@
<item> <item>
<widget class="QWidget" name="widget" native="true"> <widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QPushButton" name="buttonAcept"> <widget class="QPushButton" name="buttonAcept">
<property name="text"> <property name="text">

View File

@@ -524,6 +524,7 @@ def exportToPVC(path, exportTerrain = False):
# TODO: revisar # TODO: revisar
for typ in frameType: for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower() isTracker = "tracker" in typ.Proxy.Type.lower()
isTracker = False
objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker") objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
tmp = [] tmp = []

View File

@@ -9,6 +9,7 @@ import urllib.request
import math import math
import utm import utm
from collections import defaultdict from collections import defaultdict
import PVPlantImportGrid as ImportElevation
scale = 1000.0 scale = 1000.0
@@ -42,8 +43,10 @@ class OSMImporter:
self.ssl_context = ssl.create_default_context(cafile=certifi.where()) self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transform_from_latlon(self, lat, lon): def transform_from_latlon(self, lat, lon):
x, y, _, _ = utm.from_latlon(lat, lon) point = ImportElevation.getElevationFromOE([[lat, lon], ])
return FreeCAD.Vector(x, y, .0) * scale - self.Origin return FreeCAD.Vector(point[0].x, point[0].y, point[0].z) * scale - self.Origin
'''x, y, _, _ = utm.from_latlon(lat, lon)
return FreeCAD.Vector(x, y, .0) * scale - self.Origin'''
def get_osm_data(self, bbox): def get_osm_data(self, bbox):
query = f""" query = f"""
@@ -148,7 +151,6 @@ class OSMImporter:
polyline.addProperty("App::PropertyFloat", "Width", "Metadata", "Ancho de la vía").Width = width polyline.addProperty("App::PropertyFloat", "Width", "Metadata", "Ancho de la vía").Width = width
layer.addObject(polyline) layer.addObject(polyline)
def create_railway(self, nodes, layer, name=""): def create_railway(self, nodes, layer, name=""):
points = [n for n in nodes] points = [n for n in nodes]
rail_line = Draft.make_wire(points, closed=False, face=False) rail_line = Draft.make_wire(points, closed=False, face=False)
@@ -737,27 +739,163 @@ class OSMImporter:
def create_vegetation(self): def create_vegetation(self):
vegetation_layer = self.create_layer("Vegetation") vegetation_layer = self.create_layer("Vegetation")
# Árboles individuales # Procesar nodos de vegetación individual
for node_id, coords in self.nodes.items(): for way_id, tags in self.ways_data.items():
# Verificar si es un árbol coords = self.nodes.get(way_id)
# (Necesitarías procesar los tags de los nodos, implementación simplificada) if not coords:
cylinder = Part.makeCylinder(0.5, 5.0, FreeCAD.Vector(coords[0], coords[1], 0))# * scale - self.Origin ) continue
tree = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree")
vegetation_layer.addObject(tree)
tree.Shape = cylinder
tree.ViewObject.ShapeColor = self.feature_colors['vegetation']
# Áreas verdes pos = FreeCAD.Vector(*coords)
if tags.get('natural') == 'tree':
self.create_tree(pos, tags, vegetation_layer)
elif tags.get('natural') == 'shrub':
self.create_shrub(pos, tags, vegetation_layer)
"""elif tags.get('natural') == 'tree_stump':
self.create_tree_stump(pos, vegetation_layer)"""
# Procesar áreas vegetales
for way_id, data in self.ways_data.items(): for way_id, data in self.ways_data.items():
if 'natural' in data['tags'] or 'landuse' in data['tags']: tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) > 2:
polygon_points = [n for n in nodes] if not nodes or len(nodes) < 3:
polygon = Part.makePolygon(polygon_points) continue
face = Part.Face(polygon)
area = vegetation_layer.addObject("Part::Feature", "GreenArea") if tags.get('natural') == 'wood' or tags.get('landuse') == 'forest':
area.Shape = face self.create_forest(nodes, tags, vegetation_layer)
area.ViewObject.ShapeColor = self.feature_colors['vegetation'] elif tags.get('natural') == 'grassland':
self.create_grassland(nodes, vegetation_layer)
elif tags.get('natural') == 'heath':
self.create_heathland(nodes, vegetation_layer)
elif tags.get('natural') == 'scrub':
self.create_scrub_area(nodes, vegetation_layer)
def create_tree(self, position, tags, layer):
"""Crea un árbol individual con propiedades basadas en etiquetas OSM"""
height = float(tags.get('height', 10.0))
trunk_radius = float(tags.get('circumference', 1.0)) / (2 * math.pi)
canopy_radius = float(tags.get('diameter_crown', 4.0)) / 2
# Crear tronco
trunk = Part.makeCylinder(trunk_radius, height, position)
# Crear copa (forma cónica)
canopy_center = position + FreeCAD.Vector(0, 0, height)
canopy = Part.makeCone(canopy_radius, canopy_radius * 0.7, canopy_radius * 1.5, canopy_center)
tree = trunk.fuse(canopy)
tree_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree")
layer.addObject(tree_obj)
tree_obj.Shape = tree
tree_obj.ViewObject.ShapeColor = (0.3, 0.6, 0.2) # Verde bosque
# Añadir metadatos
for prop in ['genus', 'species', 'leaf_type', 'height']:
if prop in tags:
tree_obj.addProperty("App::PropertyString", prop.capitalize(), "Botany",
"Botanical property").__setattr__(prop.capitalize(), tags[prop])
def create_forest(self, nodes, tags, layer):
"""Crea un área boscosa con densidad variable"""
polygon_points = [FreeCAD.Vector(*n) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
# Crear base del bosque
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
forest_base = FreeCAD.ActiveDocument.addObject("Part::Feature", "Forest_Base")
layer.addObject(forest_base)
forest_base.Shape = face
forest_base.ViewObject.ShapeColor = (0.15, 0.4, 0.1) # Verde oscuro
# Generar árboles aleatorios dentro del polígono
density = float(tags.get('density', 0.5)) # Árboles por m²
area = face.Area
num_trees = int(area * density)
for _ in range(num_trees):
rand_point = self.random_point_in_polygon(polygon_points)
self.create_tree(rand_point, {}, layer)
def create_grassland(self, nodes, layer):
"""Crea un área de pastizales"""
polygon_points = [FreeCAD.Vector(*n) 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)
grassland = FreeCAD.ActiveDocument.addObject("Part::Feature", "Grassland")
layer.addObject(grassland)
grassland.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))
grassland.ViewObject.ShapeColor = (0.5, 0.7, 0.3) # Verde pasto
def create_heathland(self, nodes, layer):
"""Crea un área de brezales con vegetación baja"""
polygon_points = [FreeCAD.Vector(*n) 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)
heath = FreeCAD.ActiveDocument.addObject("Part::Feature", "Heathland")
layer.addObject(heath)
heath.Shape = face
heath.ViewObject.ShapeColor = (0.6, 0.5, 0.4) # Color terroso
# Añadir arbustos dispersos
for _ in range(int(face.Area * 0.1)): # 1 arbusto cada 10m²
rand_point = self.random_point_in_polygon(polygon_points)
self.create_shrub(rand_point, {}, layer)
def create_shrub(self, position, tags, layer):
"""Crea un arbusto individual"""
height = float(tags.get('height', 1.5))
radius = float(tags.get('diameter_crown', 1.0)) / 2
# Crear forma de arbusto (cono invertido)
base_center = position + FreeCAD.Vector(0, 0, height / 2)
shrub = Part.makeCone(radius, radius * 1.5, height, base_center)
shrub_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Shrub")
layer.addObject(shrub_obj)
shrub_obj.Shape = shrub
shrub_obj.ViewObject.ShapeColor = (0.4, 0.5, 0.3) # Verde arbusto
# Añadir metadatos si existen
if 'genus' in tags:
shrub_obj.addProperty("App::PropertyString", "Genus", "Botany", "Plant genus").Genus = tags['genus']
def create_tree_stump(self, position, layer):
"""Crea un tocón de árbol"""
height = 0.4
radius = 0.5
stump = Part.makeCylinder(radius, height, position)
stump_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree_Stump")
layer.addObject(stump_obj)
stump_obj.Shape = stump
stump_obj.ViewObject.ShapeColor = (0.3, 0.2, 0.1) # Marrón madera
def random_point_in_polygon(self, polygon_points):
"""Genera un punto aleatorio dentro de un polígono"""
min_x = min(p.x for p in polygon_points)
max_x = max(p.x for p in polygon_points)
min_y = min(p.y for p in polygon_points)
max_y = max(p.y for p in polygon_points)
while True:
rand_x = random.uniform(min_x, max_x)
rand_y = random.uniform(min_y, max_y)
rand_point = FreeCAD.Vector(rand_x, rand_y, 0)
# Verificar si el punto está dentro del polígono
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
if face.isInside(rand_point, 0.1, True):
return rand_point
def create_water_bodies(self): def create_water_bodies(self):
water_layer = self.create_layer("Water") water_layer = self.create_layer("Water")
@@ -769,7 +907,8 @@ class OSMImporter:
polygon_points = [n for n in nodes] polygon_points = [n for n in nodes]
polygon = Part.makePolygon(polygon_points) polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon) face = Part.Face(polygon)
water = water_layer.addObject("Part::Feature", "WaterBody") water = FreeCAD.ActiveDocument.addObject("Part::Feature", "WaterBody")
water_layer.addObject(water)
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin ) water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
water.ViewObject.ShapeColor = self.feature_colors['water'] water.ViewObject.ShapeColor = self.feature_colors['water']

View File

@@ -52,7 +52,9 @@ class MapWindow(QtGui.QWidget):
self.maxLat = None self.maxLat = None
self.minLon = None self.minLon = None
self.maxLon = None self.maxLon = None
self.zoom = None
self.WinTitle = WinTitle self.WinTitle = WinTitle
self.georeference_coordinates = {'lat': None, 'lon': None}
self.setupUi() self.setupUi()
def setupUi(self): def setupUi(self):
@@ -152,6 +154,9 @@ class MapWindow(QtGui.QWidget):
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS") self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
RightLayout.addWidget(self.checkboxImportGis) 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) verticalSpacer = QtGui.QSpacerItem(20, 48, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
RightLayout.addItem(verticalSpacer) RightLayout.addItem(verticalSpacer)
@@ -192,6 +197,7 @@ class MapWindow(QtGui.QWidget):
"var data = drawnItems.toGeoJSON();" "var data = drawnItems.toGeoJSON();"
"MyApp.shapes(JSON.stringify(data));" "MyApp.shapes(JSON.stringify(data));"
) )
self.close() self.close()
@QtCore.Slot(float, float) @QtCore.Slot(float, float)
@@ -203,17 +209,22 @@ class MapWindow(QtGui.QWidget):
' | UTM: ' + str(zone_number) + zone_letter + ' | UTM: ' + str(zone_number) + zone_letter +
', {:.5f}m E, {:.5f}m N'.format(x, y)) ', {:.5f}m E, {:.5f}m N'.format(x, y))
@QtCore.Slot(float, float, float, float) @QtCore.Slot(float, float, float, float, int)
def onMapZoom(self, minLat, minLon, maxLat, maxLon): def onMapZoom(self, minLat, minLon, maxLat, maxLon, zoom):
self.minLat = min([minLat, maxLat]) self.minLat = min([minLat, maxLat])
self.maxLat = max([minLat, maxLat]) self.maxLat = max([minLat, maxLat])
self.minLon = min([minLon, maxLon]) self.minLon = min([minLon, maxLon])
self.maxLon = max([minLon, maxLon]) self.maxLon = max([minLon, maxLon])
self.zoom = zoom
@QtCore.Slot(float, float) @QtCore.Slot(float, float)
def georeference(self, lat, lng): def georeference(self, lat, lng):
import PVPlantSite import PVPlantSite
from geopy.geocoders import Nominatim from geopy.geocoders import Nominatim
self.georeference_coordinates['lat'] = lat
self.georeference_coordinates['lon'] = lng
Site = PVPlantSite.get(create=True) Site = PVPlantSite.get(create=True)
Site.Proxy.setLatLon(lat, lng) Site.Proxy.setLatLon(lat, lng)
@@ -278,7 +289,7 @@ class MapWindow(QtGui.QWidget):
pts = [p.sub(offset) for p in tmp] pts = [p.sub(offset) for p in tmp]
obj = Draft.makeWire(pts, closed=cw, face=False) obj = Draft.makeWire(pts, closed=cw, face=False)
#obj.Placement.Base = offset #obj.Placement.Base = Site.Origin
obj.Label = name obj.Label = name
Draft.autogroup(obj) Draft.autogroup(obj)
@@ -288,6 +299,170 @@ class MapWindow(QtGui.QWidget):
if self.checkboxImportGis.isChecked(): if self.checkboxImportGis.isChecked():
self.getDataFromOSM(self.minLat, self.minLon, self.maxLat, self.maxLon) 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): def getDataFromOSM(self, min_lat, min_lon, max_lat, max_lon):
import Importer.importOSM as importOSM import Importer.importOSM as importOSM
import PVPlantSite import PVPlantSite

View File

@@ -125,9 +125,9 @@ def getElevationFromOE(coordinates):
points = [] points = []
for i, point in enumerate(coordinates): for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1]) c = utm.from_latlon(point[0], point[1])
points.append(FreeCAD.Vector(round(c[0] * 1000, 0), points.append(FreeCAD.Vector(round(c[0], 0),
round(c[1] * 1000, 0), round(c[1], 0),
0)) 0) * 1000)
return points return points
# Only get the json response in case of 200 or 201 # Only get the json response in case of 200 or 201
@@ -136,14 +136,16 @@ def getElevationFromOE(coordinates):
results = r.json() results = r.json()
for point in results["results"]: for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"]) c = utm.from_latlon(point["latitude"], point["longitude"])
v = FreeCAD.Vector(round(c[0] * 1000, 0), v = FreeCAD.Vector(round(c[0], 0),
round(c[1] * 1000, 0), round(c[1], 0),
round(point["elevation"] * 1000, 0)) round(point["elevation"], 0)) * 1000
points.append(v) points.append(v)
return points return points
def getSinglePointElevationFromBing(lat, lng): def getSinglePointElevationFromBing(lat, lng):
#http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey} #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 = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng) source += str(lat) + "," + str(lng)
source += "&heights=sealevel" source += "&heights=sealevel"
@@ -153,11 +155,9 @@ def getSinglePointElevationFromBing(lat, lng):
response = requests.get(source) response = requests.get(source)
ans = response.text ans = response.text
# +# to do: error handling - wait and try again
s = json.loads(ans) s = json.loads(ans)
print(s)
res = s['resourceSets'][0]['resources'][0]['elevations'] res = s['resourceSets'][0]['resources'][0]['elevations']
import utm
for elevation in res: for elevation in res:
c = utm.from_latlon(lat, lng) c = utm.from_latlon(lat, lng)
v = FreeCAD.Vector( v = FreeCAD.Vector(
@@ -324,7 +324,6 @@ def getSinglePointElevationUtm(lat, lon):
print (v) print (v)
return v return v
def getElevationUTM(polygon, lat, lng, resolution = 10000): def getElevationUTM(polygon, lat, lng, resolution = 10000):
import utm import utm
@@ -448,47 +447,6 @@ def getElevation(lat, lon, b=50.35, le=11.17, size=40):
FreeCADGui.updateGui() FreeCADGui.updateGui()
return FreeCAD.activeDocument().ActiveObject return FreeCAD.activeDocument().ActiveObject
'''
# original::
def getElevation(lat, lon, b=50.35, le=11.17, size=40):
tm.lat = lat
tm.lon = lon
baseheight = 0 #getheight(tm.lat, tm.lon)
center = tm.fromGeographic(tm.lat, tm.lon)
#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']
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) - 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: class _ImportPointsTaskPanel:
def __init__(self, obj = None): def __init__(self, obj = None):

View File

@@ -578,22 +578,22 @@ class _PVPlantSite(ArchSite._Site):
obj.addProperty("App::PropertyLink", obj.addProperty("App::PropertyLink",
"Boundary", "Boundary",
"Site", "PVPlant",
"Boundary of land") "Boundary of land")
obj.addProperty("App::PropertyLinkList", obj.addProperty("App::PropertyLinkList",
"Frames", "Frames",
"Site", "PVPlant",
"Frames templates") "Frames templates")
obj.addProperty("App::PropertyEnumeration", obj.addProperty("App::PropertyEnumeration",
"UtmZone", "UtmZone",
"Base", "PVPlant",
"UTM zone").UtmZone = zone_list "UTM zone").UtmZone = zone_list
obj.addProperty("App::PropertyVector", obj.addProperty("App::PropertyVector",
"Origin", "Origin",
"Base", "PVPlant",
"Origin point.").Origin = (0, 0, 0) "Origin point.").Origin = (0, 0, 0)
def onDocumentRestored(self, obj): def onDocumentRestored(self, obj):
@@ -771,10 +771,12 @@ class _PVPlantSite(ArchSite._Site):
import PVPlantImportGrid import PVPlantImportGrid
x, y, zone_number, zone_letter = utm.from_latlon(lat, lon) x, y, zone_number, zone_letter = utm.from_latlon(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1] self.obj.UtmZone = zone_list[zone_number - 1]
# self.obj.UtmZone = "Z"+str(zone_number) zz = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
#z = PVPlantImportGrid.get_elevation(lat, lon) self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz[0].z)
zz = PVPlantImportGrid.getSinglePointElevationFromBing(lat, lon) #self.obj.OriginOffset = FreeCAD.Vector(x * 1000, y * 1000, 0) #??
self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz.z) self.obj.Latitude = lat
self.obj.Longitude = lon
self.obj.Elevation = zz[0].z
class _ViewProviderSite(ArchSite._ViewProviderSite): class _ViewProviderSite(ArchSite._ViewProviderSite):

View File

@@ -41,7 +41,7 @@ map.on('mousemove', function(e)
MyApp.onMapMove(e.latlng.lat, e.latlng.lng); MyApp.onMapMove(e.latlng.lat, e.latlng.lng);
const bounds = map.getBounds(); const bounds = map.getBounds();
MyApp.onMapZoom(bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast()); MyApp.onMapZoom(bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast(), map.getZoom());
}); });
var DrawShapes; var DrawShapes;

View File

@@ -7,7 +7,7 @@
# Find the associated blog post at: http://blog.eskriett.com/2013/07/19/downloading-google-maps/ # Find the associated blog post at: http://blog.eskriett.com/2013/07/19/downloading-google-maps/
import math import math
# from PIL import Image from PIL import Image
import os import os
import urllib import urllib
@@ -15,7 +15,7 @@ import urllib
# alternativa a PIL: Image # alternativa a PIL: Image
# CV2 # CV2
class GoogleMapDownloader: class GoogleMapDownloader1:
""" """
A class which generates high resolution google maps images given A class which generates high resolution google maps images given
a longitude, latitude and zoom level a longitude, latitude and zoom level

View File

@@ -0,0 +1,207 @@
import math
from PIL import Image
import urllib.request
from io import BytesIO
import time
class GoogleMapDownloader:
def __init__(self, zoom=12, layer='raw_satellite'):
self._zoom = zoom
self.layer_map = {
'roadmap': 'm',
'terrain': 'p',
'satellite': 's',
'hybrid': 'y',
'raw_satellite': 's'
}
self._layer = self.layer_map.get(layer, 's')
self._style = 'style=feature:all|element:labels|visibility:off' if layer == 'raw_satellite' else ''
def latlng_to_tile(self, lat, lng):
"""Convierte coordenadas a tiles X/Y con precisión decimal"""
tile_size = 256
numTiles = 1 << self._zoom
point_x = (tile_size / 2 + lng * tile_size / 360.0) * numTiles / tile_size
sin_y = math.sin(lat * (math.pi / 180.0))
point_y = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) *
-(tile_size / (2 * math.pi))) * numTiles / tile_size
return point_x, point_y
def generateImage(self, sw_lat, sw_lng, ne_lat, ne_lng):
"""Genera la imagen para un área rectangular definida por coordenadas"""
# Convertir coordenadas a tiles con precisión decimal
sw_x, sw_y = self.latlng_to_tile(sw_lat, sw_lng)
ne_x, ne_y = self.latlng_to_tile(ne_lat, ne_lng)
# Asegurar que las coordenadas estén en el orden correcto
min_x = min(sw_x, ne_x)
max_x = max(sw_x, ne_x)
min_y = min(sw_y, ne_y)
max_y = max(sw_y, ne_y)
# Calcular los tiles mínimos y máximos necesarios
min_tile_x = math.floor(min_x)
max_tile_x = math.ceil(max_x)
min_tile_y = math.floor(min_y)
max_tile_y = math.ceil(max_y)
# Calcular dimensiones en tiles
tile_width = int(max_tile_x - min_tile_x) + 1
tile_height = int(max_tile_y - min_tile_y) + 1
# Crear imagen temporal para todos los tiles necesarios
full_img = Image.new('RGB', (tile_width * 256, tile_height * 256))
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
servers = ['mt0', 'mt1', 'mt2', 'mt3']
for x in range(min_tile_x, max_tile_x + 1):
for y in range(min_tile_y, max_tile_y + 1):
server = servers[(x + y) % len(servers)]
base_url = f"https://{server}.google.com/vt?lyrs={self._layer}&x={x}&y={y}&z={self._zoom}"
url = f"{base_url}&{self._style}" if self._style else base_url
try:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as response:
tile_data = response.read()
img = Image.open(BytesIO(tile_data))
pos_x = (x - min_tile_x) * 256
pos_y = (y - min_tile_y) * 256
full_img.paste(img, (pos_x, pos_y))
#print(f"✅ Tile ({x}, {y}) descargado")
except Exception as e:
#print(f"❌ Error en tile ({x},{y}): {str(e)}")
error_tile = Image.new('RGB', (256, 256), (255, 0, 0))
full_img.paste(error_tile, (pos_x, pos_y))
time.sleep(0.05)
# Calcular desplazamientos para recorte final
left_offset = int((min_x - min_tile_x) * 256)
right_offset = int((max_tile_x - max_x) * 256)
top_offset = int((min_y - min_tile_y) * 256)
bottom_offset = int((max_tile_y - max_y) * 256)
# Calcular coordenadas de recorte
left = left_offset
top = top_offset
right = full_img.width - right_offset
bottom = full_img.height - bottom_offset
# Asegurar que las coordenadas sean válidas
if right < left:
right = left + 1
if bottom < top:
bottom = top + 1
# Recortar la imagen al área exacta solicitada
result = full_img.crop((
left,
top,
right,
bottom
))
return full_img
class GoogleMapDownloader_1:
def __init__(self, zoom=12, layer='hybrid'):
"""
Args:
zoom: Zoom level (0-23)
layer: Map type (roadmap, terrain, satellite, hybrid)
"""
self._zoom = zoom
self.layer_map = {
'roadmap': 'm',
'terrain': 'p',
'satellite': 's',
'hybrid': 'y',
'raw_satellite': 's' # Capa especial sin etiquetas
}
self._layer = self.layer_map.get(layer, 's')
self._style = 'style=feature:all|element:labels|visibility:off' if layer == 'raw_satellite' else ''
def latlng_to_tile(self, lat, lng):
"""Convierte coordenadas a tiles X/Y"""
tile_size = 256
numTiles = 1 << self._zoom
# Cálculo para coordenada X
point_x = (tile_size / 2 + lng * tile_size / 360.0) * numTiles / tile_size
# Cálculo para coordenada Y
sin_y = math.sin(lat * (math.pi / 180.0))
point_y = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) *
-(tile_size / (2 * math.pi))) * numTiles / tile_size
return int(point_x), int(point_y)
def generateImage(self, sw_lat, sw_lng, ne_lat, ne_lng):
"""
Genera la imagen para un área rectangular definida por:
- sw_lat, sw_lng: Esquina suroeste (latitud, longitud)
- ne_lat, ne_lng: Esquina noreste (latitud, longitud)
"""
# Convertir coordenadas a tiles
sw_x, sw_y = self.latlng_to_tile(sw_lat, sw_lng)
ne_x, ne_y = self.latlng_to_tile(ne_lat, ne_lng)
# Determinar rango de tiles
min_x = min(sw_x, ne_x)
max_x = max(sw_x, ne_x)
min_y = min(sw_y, ne_y)
max_y = max(sw_y, ne_y)
# Calcular dimensiones en tiles
tile_width = max_x - min_x + 1
tile_height = max_y - min_y + 1
# Crear imagen final
result = Image.new('RGB', (256 * tile_width, 256 * tile_height))
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
servers = ['mt0', 'mt1', 'mt2', 'mt3']
print(f"Descargando {tile_width}x{tile_height} tiles ({tile_width * tile_height} total)")
for x in range(min_x, max_x + 1):
for y in range(min_y, max_y + 1):
# Seleccionar servidor rotatorio
server = servers[(x + y) % len(servers)]
# Construir URL con parámetro para quitar etiquetas si es necesario
url = f"https://{server}.google.com/vt?lyrs={self._layer}&x={x}&y={y}&z={self._zoom}"
if self._style:
url = f"{url}&{self._style}"
print("Descargando tile:", url)
try:
# Descargar tile
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as response:
tile_data = response.read()
# Procesar en memoria
img = Image.open(BytesIO(tile_data))
pos_x = (x - min_x) * 256
pos_y = (y - min_y) * 256
result.paste(img, (pos_x, pos_y))
print(f"✅ Tile ({x}, {y}) descargado")
except Exception as e:
print(f"❌ Error en tile ({x},{y}): {str(e)}")
# Crear tile de error (rojo)
error_tile = Image.new('RGB', (256, 256), (255, 0, 0))
pos_x = (x - min_x) * 256
pos_y = (y - min_y) * 256
result.paste(error_tile, (pos_x, pos_y))
# Pausa para evitar bloqueos
time.sleep(0.05)
return result

View File

@@ -2,8 +2,8 @@
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata"> <package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>PVPlant</name> <name>PVPlant</name>
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description> <description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
<version>2025.02.22</version> <version>2025.07.06</version>
<date>2025.02.22</date> <date>2025.07.06</date>
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer> <maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license> <license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url> <url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url>