354 lines
13 KiB
Python
354 lines
13 KiB
Python
"""
|
|
ExportsFactory export a city into several formats
|
|
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
|
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
|
"""
|
|
import xmltodict
|
|
import uuid
|
|
import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
class EnergyAde:
|
|
def __init__(self, city, path):
|
|
self._city = city
|
|
self._path = path
|
|
self._surface_members = None
|
|
self._export()
|
|
|
|
|
|
def _export(self):
|
|
energy_ade = {
|
|
'core:CityModel': {
|
|
'@xmlns:brid':'http://www.opengis.net/citygml/bridge/2.0',
|
|
'@xmlns:tran':'http://www.opengis.net/citygml/transportation/2.0',
|
|
'@xmlns:frn':'http://www.opengis.net/citygml/cityfurniture/2.0',
|
|
'@xmlns:wtr':'http://www.opengis.net/citygml/waterbody/2.0',
|
|
'@xmlns:sch':'http://www.ascc.net/xml/schematron',
|
|
'@xmlns:veg':'http://www.opengis.net/citygml/vegetation/2.0',
|
|
'@xmlns:xlink':'http://www.w3.org/1999/xlink',
|
|
'@xmlns:tun':'http://www.opengis.net/citygml/tunnel/2.0',
|
|
'@xmlns:tex':'http://www.opengis.net/citygml/texturedsurface/2.0',
|
|
'@xmlns:gml':'http://www.opengis.net/gml',
|
|
'@xmlns:genobj':'http://www.opengis.net/citygml/generics/2.0',
|
|
'@xmlns:dem':'http://www.opengis.net/citygml/relief/2.0',
|
|
'@xmlns:app':'http://www.opengis.net/citygml/appearance/2.0',
|
|
'@xmlns:luse':'http://www.opengis.net/citygml/landuse/2.0',
|
|
'@xmlns:xAL':'urn:oasis:names:tc:ciq:xsdschema:xAL:2.0',
|
|
'@xmlns:xsi':'http://www.w3.org/2001/XMLSchema-instance',
|
|
'@xmlns:smil20lang':'http://www.w3.org/2001/SMIL20/Language',
|
|
'@xmlns:pbase':'http://www.opengis.net/citygml/profiles/base/2.0',
|
|
'@xmlns:smil20': 'http://www.w3.org/2001/SMIL20/',
|
|
'@xmlns:bldg':'http://www.opengis.net/citygml/building/2.0',
|
|
'@xmlns:energy': "http://www.sig3d.org/citygml/2.0/energy/1.0",
|
|
'@xmlns:core':'http://www.opengis.net/citygml/2.0',
|
|
'@xmlns:grp':'http://www.opengis.net/citygml/cityobjectgroup/2.0',
|
|
'gml:boundedBy': {
|
|
'gml:Envelope': {
|
|
'@srsName': self._city.srs_name,
|
|
'@srsDimension': 3,
|
|
'gml:lowerCorner': ' '.join([str(e) for e in self._city.lower_corner]),
|
|
'gml:upperCorner': ' '.join([str(e) for e in self._city.upper_corner])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
buildings = []
|
|
for building in self._city.buildings:
|
|
building_dic = {
|
|
'bldg:Building': {
|
|
'@gml:id': building.name,
|
|
'gml:description': f'Building {building.name} at {self._city.name}',
|
|
'gml:name': f'{building.name}',
|
|
'core:creationDate': datetime.datetime.now().strftime('%Y-%m-%d')
|
|
}
|
|
}
|
|
building_dic = EnergyAde._measures(building, building_dic)
|
|
building_dic = self._building_geometry(building, building_dic, self._city)
|
|
building_dic['bldg:Building']['energy:volume'] = {
|
|
'energy:VolumeType': {
|
|
'energy:type':'grossVolume',
|
|
'energy:value': {
|
|
'@uom': 'm3',
|
|
'energy:value': building.volume
|
|
}
|
|
}
|
|
}
|
|
building_dic['bldg:Building']['energy:referencePoint'] = {
|
|
'gml:Point': {
|
|
'@srsName': self._city.srs_name,
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:Pos': f'{" ".join(map(str,building.centroid))}'
|
|
}
|
|
}
|
|
building_dic['bldg:Building']['energy:thermalZone'] = self._thermal_zones(building,self._city)
|
|
buildings.append(building_dic)
|
|
|
|
energy_ade['core:CityModel']['core:cityObjectMember'] = buildings
|
|
print(energy_ade)
|
|
file_name = self._city.name + '_ade.gml'
|
|
file_path = Path(self._path / file_name).resolve()
|
|
with open(file_path, 'w' ) as file:
|
|
file.write(xmltodict.unparse(energy_ade,pretty=True, short_empty_elements=True))
|
|
|
|
@staticmethod
|
|
def _measures(building, building_dic):
|
|
measures = []
|
|
measure = EnergyAde._measure(building.heating, 'year', 'Energy demand heating', 'INSEL')
|
|
if measure is not None:
|
|
measures.append(measure)
|
|
measure = EnergyAde._measure(building.cooling, 'year', 'Energy demand cooling', 'INSEL')
|
|
if measure is not None:
|
|
measures.append(measure)
|
|
if len(measures) != 0:
|
|
building_dic['bldg:Building']['genobj:measureAttribute'] = measures
|
|
|
|
demands = []
|
|
for key in building.heating:
|
|
if key != 'year':
|
|
demand = EnergyAde._demand(building.heating, key, 'Heating energy', 'INSEL')
|
|
demands.append(demand)
|
|
|
|
for key in building.cooling:
|
|
if key != 'year':
|
|
demand = EnergyAde._demand(building.cooling, key, 'Cooling energy', 'INSEL')
|
|
demands.append(demand)
|
|
if len(demands) != 0:
|
|
building_dic['bldg:Building']['energy:demands'] = demands
|
|
|
|
return building_dic
|
|
|
|
@staticmethod
|
|
def _measure(measure_dict, key_value, name, source):
|
|
measure = None
|
|
if key_value in measure_dict:
|
|
measure = {
|
|
'@name': name,
|
|
'genobj:value': {
|
|
'@uom': 'kWh',
|
|
'#text': ' '.join([str(e/1000) for e in measure_dict[key_value][source]])
|
|
}
|
|
}
|
|
return measure
|
|
|
|
@staticmethod
|
|
def _demand(measure_dict, key_value, description, source):
|
|
demand = {
|
|
'energy:EnergyDemand': {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'energy:energyAmount': {
|
|
'energy:RegularTimeSeries': {
|
|
'energy:variableProperties': {
|
|
'energy:TimeValuesProperties': {
|
|
'energy:acquisitionMethod': 'simulation',
|
|
'energy:source': source,
|
|
'energy:thematicDescription': description,
|
|
},
|
|
'energy:timeInterval': {
|
|
'@unit': key_value,
|
|
'#text': '1',
|
|
},
|
|
'energy:values': {
|
|
'@uom': 'kWh',
|
|
'#text': ' '.join([str(float(e) / 1000) for e in measure_dict[key_value][source]])
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'energy:endUse': 'spaceHeating'
|
|
}
|
|
}
|
|
return demand
|
|
|
|
|
|
def _building_geometry(self, building, building_dic, city):
|
|
|
|
building_dic['bldg:Building']['bldg:function'] = building.function
|
|
building_dic['bldg:Building']['bldg:usage'] = ', '.join([u.usage for u in building.usage_zones])
|
|
building_dic['bldg:Building']['bldg:yearOfConstruction'] = building.year_of_construction
|
|
building_dic['bldg:Building']['bldg:roofType'] = building.roof_type
|
|
building_dic['bldg:Building']['bldg:measuredHeight'] = {
|
|
'@uom': 'm',
|
|
'#text': f'{building.max_height}'
|
|
}
|
|
building_dic['bldg:Building']['bldg:storeysAboveGround'] = building.storeys_above_ground
|
|
if building.lod == 1:
|
|
building_dic = self._lod1(building, building_dic, city)
|
|
elif building.lod == 2:
|
|
building_dic = self._lod2(building, building_dic, city)
|
|
else:
|
|
raise NotImplementedError('Only lod 1 and 2 can be exported')
|
|
return building_dic
|
|
|
|
|
|
def _lod1(self, building, building_dic, city):
|
|
raise NotImplementedError('Only lod 1 and 2 can be exported')
|
|
|
|
def _lod2(self, building, building_dic, city):
|
|
self._surface_members = []
|
|
boundaries = [{
|
|
'gml:Envelope': {
|
|
'@srsName': city.srs_name,
|
|
'@srsDimension': 3,
|
|
'gml:lowerCorner': ' '.join([str(e) for e in city.lower_corner]),
|
|
'gml:upperCorner': ' '.join([str(e) for e in city.upper_corner])
|
|
}}]
|
|
for surface in building.surfaces:
|
|
surface_member = { '@xlink:href': f'#PolyId{surface.name}'}
|
|
self._surface_members.append(surface_member)
|
|
if surface.type == 'Wall':
|
|
surface_type = 'bldg:WallSurface'
|
|
elif surface.type == 'Ground':
|
|
surface_type = 'bldg:GroundSurface'
|
|
else:
|
|
surface_type = 'bldg:RoofSurface'
|
|
|
|
surface_dic = {
|
|
surface_type: {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:name': f'{surface.name} ({surface.type})',
|
|
'gml:boundedBy': {
|
|
'gml:Envelope': {
|
|
'@srsName': city.srs_name,
|
|
'gml:lowerCorner': f'{surface.min_x} {surface.min_y} {surface.min_z}',
|
|
'gml:upperCorner': f'{surface.max_x} {surface.max_y} {surface.max_z}'
|
|
}
|
|
},
|
|
'bldg:lod2MultiSurface': {
|
|
'gml:MultiSurface': {
|
|
'@srsName': city.srs_name,
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'surfaceMember': {
|
|
'gml:Polygon': {
|
|
'@srsName': city.srs_name,
|
|
'@gml:id': f'PolyId{surface.name}',
|
|
'gml:exterior': {
|
|
'gml:LinearRing': {
|
|
'@gml:id': f'PolyId{surface.name}_0',
|
|
'gml:posList': {
|
|
'@srsDimension': '3',
|
|
'@count': len(surface.points) + 1,
|
|
'#text': f'{" ".join(map(str, surface.points_list))} {" ".join(map(str, surface.points[0]))}'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
boundaries.append(surface_dic)
|
|
building_dic['bldg:Building']['bldg:lod2Solid'] = {
|
|
'gml:Solid': {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:exterior': {
|
|
'gml:CompositeSurface': {
|
|
'@srsName': city.srs_name,
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:surfaceMember': self._surface_members
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
building_dic['bldg:Building']['gml:boundedBy'] = boundaries
|
|
return building_dic
|
|
|
|
def _thermal_zones(self, building, city):
|
|
thermal_zones = []
|
|
for index, thermal_zone in enumerate(building.thermal_zones):
|
|
usage_zones = []
|
|
for usage_zone in thermal_zone.usage_zones:
|
|
usage_zones.append({'@xlink:href': f'#GML_{usage_zone.id}'})
|
|
|
|
thermal_zone_dic = {
|
|
'energy:ThermalZone': {
|
|
'@gml:id': f'GML_{thermal_zone.id}',
|
|
'gml:name': f'Thermal zone {index} in {building.name} building',
|
|
'energy:contains': [],
|
|
'energy:floorArea': {
|
|
'energy:FloorArea' : {
|
|
'energy:type': 'grossFloorArea',
|
|
'energy:value': {
|
|
'@uom': 'm2',
|
|
'#text': f'{thermal_zone.floor_area}'
|
|
}
|
|
}
|
|
},
|
|
'energy:volume': {
|
|
'energy:VolumeType': {
|
|
'energy:type': 'grossVolume',
|
|
'energy:value': {
|
|
'@uom': 'm3',
|
|
#todo: for now we have just one thermal zone, therefore is the building volume, this need to be changed
|
|
'#text': f'{building.volume}'
|
|
}
|
|
}
|
|
},
|
|
'energy:isCooled': f'{thermal_zone.is_cooled}',
|
|
'energy:isHeated': f'{thermal_zone.is_heated}',
|
|
'energy:volumeGeometry': {
|
|
'gml:Solid': {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:exterior': {
|
|
'gml:CompositeSurface': {
|
|
'@srsName': f'{city.srs_name}',
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:surfaceMember': self._surface_members
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'energy:boundedBy': self._thermal_boundaries(city, thermal_zone)
|
|
}
|
|
}
|
|
thermal_zone_dic['energy:ThermalZone']['energy:contains'] = usage_zones
|
|
thermal_zones.append(thermal_zone_dic)
|
|
return thermal_zones
|
|
|
|
def _thermal_boundaries(self, city, thermal_zone):
|
|
thermal_boundaries = []
|
|
for thermal_boundary in thermal_zone.bounded:
|
|
thermal_boundary_dic = {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:name': f'{thermal_boundary.construction_name}',
|
|
'energy:thermalBoundaryType': thermal_boundary.type,
|
|
'energy:azumuth': {
|
|
'@uom': 'rad',
|
|
'#text': f'{thermal_boundary.azimuth}'
|
|
},
|
|
'energy:inclination': {
|
|
'@uom': 'rad',
|
|
'#text': f'{thermal_boundary.inclination}'
|
|
},
|
|
'energy:area': {
|
|
'@uom': 'm2',
|
|
'#text': f'{thermal_boundary.area}'
|
|
},
|
|
'energy:surfaceGeometry': {
|
|
'gml:MultiSurface': {
|
|
'@srsName': city.srs_name,
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:surfaceMember': {
|
|
'gml:Polygon': {
|
|
'@srsName': city.srs_name,
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:exterior': {
|
|
'gml:LinearRing': {
|
|
'@gml:id': f'GML_{uuid.uuid4()}',
|
|
'gml:posList': {
|
|
'@srsDimension': '3',
|
|
'@count': len(thermal_boundary.surface.points) + 1,
|
|
'#text': f'{" ".join(map(str, thermal_boundary.surface.points_list))} '
|
|
f'{" ".join(map(str, thermal_boundary.surface.points[0]))}'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
thermal_boundaries.append(thermal_boundary_dic)
|
|
return thermal_boundaries |