Files
PVPlant/PVPlantSite.py
2025-03-28 19:40:11 +06:00

1193 lines
43 KiB
Python

# /**********************************************************************
# * *
# * 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, ArchCommands, 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:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
import os
from PVPlantResources import DirIcons as DirIcons
__title__ = "FreeCAD Site"
__author__ = ""
__url__ = "http://www.freecadweb.org"
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):
""" Find the existing Site object """
# Return an existing instance of the same name, if found.
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
use = True
if use:
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()
else: ## posible código:
import MeshPart
CopyMesh = Mesh.copy()
Base = CopyMesh.Placement.Base
CopyMesh.Placement.move(Base.negative())
CopyShape = Boundary.Shape.copy()
CopyShape.Placement.move(Base.negative())
Vec = CopyShape.Edge1.Vertexes[0].Point - CopyShape.Edge1.Vertexes[1].Point
Vec.x, Vec.y = -(Vec.y), Vec.x
Section = CopyMesh.crossSections([(CopyShape.Edge1.Vertexes[0].Point, Vec)], 0.000001)
print(Section)
for i in Section[0]:
Pwire = Draft.makeWire(i)
# Pwire.Placement.move(Base)
##SectionGroup.addObject(Pwire)
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
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:
# TODO - remove pysolar dependency
# FreeCAD.Console.PrintWarning("Ladybug module not found, using pysolar instead. Warning, this will be deprecated in the future\n")
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)
from pivy import coin
if not scale:
return None
circles = []
sunpaths = []
hourpaths = []
circlepos = []
hourpos = []
# build the base circle + number positions
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))
# build the sun curves at solstices and equinoxe
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 # pysolar's zero is south, ours is X direction
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 # pysolar's zero is north, ours is X direction
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 # not sure why this is needed now... But it is.
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))
# cut underground lines
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]
# build nodes
ts = 0.005 * scale # text 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):
"""makeWindRose(site,sectors):
returns a wind rose diagram as a pivy node"""
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)] # the divider angles between each sector
basebissect = baseangle / 2
angles = [basebissect] # build a list of central direction for each sector
for i in range(1, sectors):
angles.append(angles[-1] + baseangle)
windsbysector = [0 for i in range(sectors)] # prepare a holder for values for each sector
for hour in epw_data.wind_direction:
sector = min(angles, key=lambda x: abs(x - hour)) # find the closest sector angle
sectorindex = angles.index(sector)
windsbysector[sectorindex] = windsbysector[sectorindex] + 1
maxwind = max(windsbysector)
windsbysector = [wind / maxwind for wind in windsbysector] # normalize
vectors = [] # create 3D 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])
# build coin node
import Part
from pivy import coin
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):
from pivy import coin
self.compassswitch.whichChild = coin.SO_SWITCH_ALL
def hide(self):
from pivy import coin
self.compassswitch.whichChild = coin.SO_SWITCH_NONE
def rotate(self, angleInDegrees):
from pivy import coin
self.transform.rotation.setValue(
coin.SbVec3f(0, 0, 1), math.radians(angleInDegrees))
def locate(self, x, y, z):
from pivy import coin
self.transform.translation.setValue(x, y, z)
def scale(self, area):
from pivy import coin
scale = round(max(math.sqrt(area.getValueAs("m^2").Value) / 10, 1))
self.transform.scaleFactor.setValue(coin.SbVec3f(scale, scale, 1))
def setupCoin(self):
from pivy import coin
compasssep = coin.SoSeparator()
self.transform = coin.SoTransform()
darkNorthMaterial = coin.SoMaterial()
darkNorthMaterial.diffuseColor.set1Value(
0, 0.5, 0, 0) # north dark color
lightNorthMaterial = coin.SoMaterial()
lightNorthMaterial.diffuseColor.set1Value(
0, 0.9, 0, 0) # north light color
darkGreyMaterial = coin.SoMaterial()
darkGreyMaterial.diffuseColor.set1Value(0, 0.9, 0.9, 0.9) # dark color
lightGreyMaterial = coin.SoMaterial()
lightGreyMaterial.diffuseColor.set1Value(0, 0.5, 0.5, 0.5) # light color
coords = self.buildCoordinates()
# coordIndex = [0, 1, 2, -1, 2, 3, 0, -1]
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):
from pivy import coin
coords = coin.SoCoordinate3()
# North Arrow
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)
# East Arrow
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)
# South Arrow
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)
# West Arrow
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
class _PVPlantSite(ArchSite._Site):
"The Site object"
def __init__(self, obj):
ArchSite._Site.__init__(self, obj)
self.obj = obj
# self.setProperties(obj)
self.Type = "Site"
obj.Proxy = self
obj.IfcType = "Site"
obj.setEditorMode("IfcType", 1)
def setProperties(self, obj):
# Definicion de Propiedades:
ArchSite._Site.setProperties(self, obj)
obj.addProperty("App::PropertyLink",
"Boundary",
"Site",
"Boundary of land")
obj.addProperty("App::PropertyLinkList",
"Frames",
"Site",
"Frames templates")
obj.addProperty("App::PropertyEnumeration",
"UtmZone",
"Base",
"UTM zone").UtmZone = zone_list
obj.addProperty("App::PropertyVector",
"Origin",
"Base",
"Origin point.").Origin = (0, 0, 0)
def onDocumentRestored(self, obj):
"""Method run when the document is restored. Re-adds the properties."""
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:
# TODO: Definir los objetos que se pueden proyectar
# if obj.Boundary.TypeId == 'Part::Part2DObject':
# projectWireOnMesh(obj.Boundary, obj.Terrain.Mesh)
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):
return
if not obj.isDerivedFrom("Part::Feature"): # old-style Site
return
pl = obj.Placement
shape = None
if obj.Terrain:
if obj.Terrain.isDerivedFrom("Part::Feature"):
if obj.Terrain.Shape:
if not obj.Terrain.Shape.isNull():
shape = obj.Terrain.Shape.copy()
if shape:
shells = []
for sub in obj.Subtractions:
if sub.isDerivedFrom("Part::Feature"):
if sub.Shape:
if sub.Shape.Solids:
for sol in sub.Shape.Solids:
rest = shape.cut(sol)
shells.append(sol.Shells[0].common(shape.extrude(obj.ExtrusionVector)))
shape = rest
for sub in obj.Additions:
if sub.isDerivedFrom("Part::Feature"):
if sub.Shape:
if sub.Shape.Solids:
for sol in sub.Shape.Solids:
rest = shape.cut(sol)
shells.append(sol.Shells[0].cut(shape.extrude(obj.ExtrusionVector)))
shape = rest
if not shape.isNull():
if shape.isValid():
for shell in shells:
shape = shape.fuse(shell)
if obj.RemoveSplitter:
shape = shape.removeSplitter()
obj.Shape = shape
if not pl.isNull():
obj.Placement = pl
self.computeAreas(obj)
def computeAreas(self, obj):
ArchSite._Site.computeAreas(self, obj)
return
if not obj.Shape:
return
if obj.Shape.isNull():
return
if not obj.Shape.isValid():
return
if not obj.Shape.Faces:
return
if not hasattr(obj, "Perimeter"): # check we have a latest version site
return
if not obj.Terrain:
return
# compute area
fset = []
for f in obj.Shape.Faces:
if f.normalAt(0, 0).getAngle(FreeCAD.Vector(0, 0, 1)) < 1.5707:
fset.append(f)
if fset:
import Drawing, Part
pset = []
for f in fset:
try:
pf = Part.Face(Part.Wire(Drawing.project(f, FreeCAD.Vector(0, 0, 1))[0].Edges))
except Part.OCCError:
# error in computing the area. Better set it to zero than show a wrong value
if obj.ProjectedArea.Value != 0:
print("Error computing areas for ", obj.Label)
obj.ProjectedArea = 0
else:
pset.append(pf)
if pset:
self.flatarea = pset.pop()
for f in pset:
self.flatarea = self.flatarea.fuse(f)
self.flatarea = self.flatarea.removeSplitter()
if obj.ProjectedArea.Value != self.flatarea.Area:
obj.ProjectedArea = self.flatarea.Area
# compute perimeter
lut = {}
for e in obj.Shape.Edges:
lut.setdefault(e.hashCode(), []).append(e)
l = 0
for e in lut.values():
if len(e) == 1: # keep only border edges
l += e[0].Length
if l:
if obj.Perimeter.Value != l:
obj.Perimeter = l
# compute volumes
if obj.Terrain.Shape.Solids:
shapesolid = obj.Terrain.Shape.copy()
else:
shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector)
addvol = 0
subvol = 0
for sub in obj.Subtractions:
subvol += sub.Shape.common(shapesolid).Volume
for sub in obj.Additions:
addvol += sub.Shape.cut(shapesolid).Volume
if obj.SubtractionVolume.Value != subvol:
obj.SubtractionVolume = subvol
if obj.AdditionVolume.Value != addvol:
obj.AdditionVolume = addvol
def __getstate__(self):
"""
Save variables to file.
"""
node = self.get_geoorigin()
system = node.geoSystem.getValues()
x, y, z = node.geoCoords.getValue().getValue()
return system, [x, y, z]
def __setstate__(self, state):
"""
Get variables from file.
"""
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):
import utm
import PVPlantImportGrid
x, y, zone_number, zone_letter = utm.from_latlon(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
# self.obj.UtmZone = "Z"+str(zone_number)
#z = PVPlantImportGrid.get_elevation(lat, lon)
zz = PVPlantImportGrid.getSinglePointElevationFromBing(lat, lon)
self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz.z)
class _ViewProviderSite(ArchSite._ViewProviderSite):
"""A View Provider for the Site object.
Parameters
----------
vobj: <Gui.ViewProviderDocumentObject>
The view provider to turn into a site view provider.
"""
def __init__(self, vobj):
ArchSite._ViewProviderSite.__init__(self, vobj)
vobj.Proxy = self
#vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
#self.setProperties(vobj)
def getIcon(self):
"""
Return the path to the appropriate icon.
"""
return str(os.path.join(DirIcons, "icon.svg"))
def claimChildren(self):
"""Define which objects will appear as children in the tree view.
Set objects within the site group, and the terrain object as children.
If the Arch preference swallowSubtractions is true, set the additions
and subtractions to the terrain as children.
Returns
-------
list of <App::DocumentObject>s:
The objects claimed as children.
"""
objs = []
if hasattr(self, "Object"):
objs = self.Object.Group + \
[self.Object.Terrain, self.Object.Boundary, self.Object.Frames]
if hasattr(self.Object, "Frames"):
objs.extend(self.Object.Frames)
return objs
'''
class _ViewProviderSite:
"""A View Provider for the Site object.
Parameters
----------
vobj: <Gui.ViewProviderDocumentObject>
The view provider to turn into a site view provider.
"""
def __init__(self,vobj):
vobj.Proxy = self
vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
self.setProperties(vobj)
def setProperties(self,vobj):
"""Give the site view provider its site view provider specific properties.
These include solar diagram and compass data, dealing the orientation
of the site, and its orientation to the sun.
You can learn more about properties here: https://wiki.freecadweb.org/property
"""
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):
"""Method run when the document is restored. Re-add the Arch component properties."""
self.setProperties(vobj)
def getIcon(self):
"""Return the path to the appropriate icon."""
return str(os.path.join(DirIcons, "solar-panel.svg"))
def claimChildren(self):
"""Define which objects will appear as children in the tree view.
Set objects within the site group, and the terrain object as children.
If the Arch preference swallowSubtractions is true, set the additions
and subtractions to the terrain as children.
Returns
-------
list of <App::DocumentObject>s:
The objects claimed as children.
"""
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):
"""Method called when the document requests the object to enter edit mode.
Edit mode is entered when a user double clicks on an object in the tree
view, or when they use the menu option [Edit -> Toggle Edit Mode].
Just display the standard Arch component task panel.
Parameters
----------
mode: int or str
The edit mode the document has requested. Set to 0 when requested via
a double click or [Edit -> Toggle Edit Mode].
Returns
-------
bool
If edit mode was entered.
"""
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):
"""Method called when the document requests the object exit edit mode.
Close the Arch component edit task panel.
Returns
-------
False
"""
FreeCADGui.Control.closeDialog()
return False
def attach(self,vobj):
"""Add display modes' data to the coin scenegraph.
Add each display mode as a coin node, whose parent is this view
provider.
Each display mode's node includes the data needed to display the object
in that mode. This might include colors of faces, or the draw style of
lines. This data is stored as additional coin nodes which are children
of the display mode node.
Doe not add display modes, but do add the solar diagram and compass to
the scenegraph.
"""
self.Object = vobj.Object
from pivy import coin
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):
"""Method called when the host object has a property changed.
If the Longitude or Latitude has changed, set the SolarDiagram to
update.
If Terrain or Placement has changed, move the compass to follow it.
Parameters
----------
obj: <App::FeaturePython>
The host object that has changed.
prop: string
The name of the property that has changed.
"""
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"):
from pivy import coin
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):
"""Update the declination of the compass
Update the declination by adding together how the site has been rotated
within the document, and the rotation of the site compass.
"""
if not hasattr(vobj, 'UpdateDeclination') or not vobj.UpdateDeclination:
return
compassRotation = vobj.CompassRotation.Value
siteRotation = math.degrees(vobj.Object.Placement.Rotation.Angle) # This assumes Rotation.axis = (0,0,1)
vobj.Object.Declination = compassRotation + siteRotation
def addTrueNorthRotation(self):
if hasattr(self, 'trueNorthRotation') and self.trueNorthRotation is not None:
return
from pivy import coin
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:
from pivy import coin
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
'''
'''class _CommandPVPlantSite:
"the Arch Site command definition"
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "icon.svg")),
'MenuText': QT_TRANSLATE_NOOP("Arch_Site", "Site"),
'Accel': "S, I",
'ToolTip': QT_TRANSLATE_NOOP("Arch_Site", "Creates a site object including selected objects.")}
def IsActive(self):
return ((not (FreeCAD.ActiveDocument is None)) and
(FreeCAD.ActiveDocument.getObject("Site") is None))
def Activated(self):
makePVPlantSite()
return
if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantSite', _CommandPVPlantSite())'''