Commit 4331d023 authored by Rushikesh Padsala's avatar Rushikesh Padsala
Browse files

Replace App.js

parent 22e617f0
Pipeline #11782 passed with stage
in 7 seconds
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxYjBlYmYyNC1kNWRhLTRiNTUtOGNlYi02NGY1YWVhNjI2MjIiLCJpZCI6MTEwNzEsImlhdCI6MTYxOTcwNjI0M30.tlpaagcH93SjaHIn7eEVpanGSiH2yDylbGJZr2gsXnY';
//////////////////////////////////////////////////////////////////////////
// Loading Custom Terrain
//////////////////////////////////////////////////////////////////////////
var customterrain = new Cesium.CesiumTerrainProvider({
//url: 'https://w2.iaf-ex.hft-stuttgart.de/CesiumData/QuantizedMesh/DigiTwins4PEDs/Nordbahnhof/',
url: 'https://web3d.basemap.de/cesium/dgm5-mesh'
/* -----------------------------
Cesium setup & theme colors
----------------------------- */
Cesium.Ion.defaultAccessToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxYjBlYmYyNC1kNWRhLTRiNTUtOGNlYi02NGY1YWVhNjI2MjIiLCJpZCI6MTEwNzEsImlhdCI6MTYxOTcwNjI0M30.tlpaagcH93SjaHIn7eEVpanGSiH2yDylbGJZr2gsXnY';
const ACCENT_HEX = '#22c55e'; // Green accent used in UI
const ACCENT_GREEN = Cesium.Color.fromCssColorString(ACCENT_HEX);
const HOVER_GREEN_OPAQUE = ACCENT_GREEN.withAlpha(1.0); // Building hover color
const PICK_CYAN_OPAQUE = Cesium.Color.CYAN.withAlpha(1.0); // Building pick color
// Spinner (loading wheel) & PV legend DOM references
const spinnerEl = document.getElementById('spinner');
const legendBox = document.getElementById('legendBox');
// When the app boots, keep a flag so nested fetches don't hide the spinner mid-boot
let BOOTING = true;
/* --------------------------------------
Terrain / Cesium Viewer configuration
-------------------------------------- */
// Custom terrain service
const customterrain = new Cesium.CesiumTerrainProvider({
url: 'https://sgx.geodatenzentrum.de/gdz_basemapde_3d_gelaende/dgm5_4326_mesh'
});
var terrainProviderViewModels = [];
// Allow picking this terrain from the base layer picker
const terrainProviderViewModels = [];
terrainProviderViewModels.push(new Cesium.ProviderViewModel({
name: 'Custom Terrain',
iconUrl: Cesium.buildModuleUrl('https://w2.iaf-ex.hft-stuttgart.de/CesiumData/Images/TerrainProviders/Terrain.png'),
tooltip: 'Custom Terrain',
creationFunction: function () {
return customterrain;
}
name: 'Custom Terrain',
iconUrl: Cesium.buildModuleUrl('https://w2.iaf-ex.hft-stuttgart.de/CesiumData/Images/TerrainProviders/Terrain.png'),
tooltip: 'Custom Terrain',
creationFunction: function () { return customterrain; }
}));
//////////////////////////////////////////////////////////////////////////
// Creating the Viewer
//////////////////////////////////////////////////////////////////////////
var viewer = new Cesium.Viewer('cesiumContainer', {
scene3DOnly: true,
selectionIndicator: false,
timeline: false,
animation: false,
shadow: false,
imageryProvider: new Cesium.WebMapServiceImageryProvider({
url: 'https://sgx.geodatenzentrum.de/wms_basemapde?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities',
layers: "de_basemapde_web_raster_farbe",
enablePickFeatures: false,
parameters: {
transparent: true,
format: "image/png",
}
}),
baseLayerPicker: true,
terrainProviderViewModels: terrainProviderViewModels
// Create the Cesium viewer with basemap imagery & no timeline/animation
const viewer = new Cesium.Viewer('cesiumContainer', {
scene3DOnly: true,
selectionIndicator: false,
timeline: false,
animation: false,
shadow: false,
imageryProvider: new Cesium.WebMapServiceImageryProvider({
url: 'https://sgx.geodatenzentrum.de/wms_basemapde?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities',
layers: 'de_basemapde_web_raster_farbe',
enablePickFeatures: false,
parameters: { transparent: true, format: 'image/png' }
}),
baseLayerPicker: true,
terrainProviderViewModels: terrainProviderViewModels
});
// Clock
viewer.clock.shouldAnimate = false;
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601('2024-07-05T12:00:00Z');
viewer.clock.multiplier = 0; // pause the time
viewer.clock.multiplier = 0;
viewer.scene.globe.enableLighting = true;
viewer.scene.globe.depthTestAgainstTerrain = true;
//////////////////////////////////////////////////////////////////////////
// Set the camera to Nordbahnhof, Stuttgart, Germany with a slanted view
//////////////////////////////////////////////////////////////////////////
// Define the Nordbahnhof initial position and orientation
var nordbahnhofPosition = {
destination: Cesium.Cartesian3.fromDegrees(9.201653, 48.791294, 1047.54), // Nordbahnhof coordinates with height
orientation: {
heading: Cesium.Math.toRadians(313.31), // Heading: 19.98°
pitch: Cesium.Math.toRadians(-38.18), // Pitch: -28.11°
roll: Cesium.Math.toRadians(360) // Roll: 0°
}
/* -------------------------
Camera preset (Home view)
------------------------- */
const nordbahnhofPosition = {
//destination: Cesium.Cartesian3.fromDegrees(9.201548, 48.791157, 1077.10),
destination: Cesium.Cartesian3.fromDegrees(9.203916, 48.787847, 1069.24),
orientation: {
heading: Cesium.Math.toRadians(319.05),
pitch: Cesium.Math.toRadians(-30.25),
roll: Cesium.Math.toRadians(0)
}
};
// Ensure the camera moves to Nordbahnhof once the viewer is fully loaded
viewer.scene.camera.setView(nordbahnhofPosition);
// Override the home button behavior to fly to Nordbahnhof
viewer.homeButton.viewModel.command.beforeExecute.addEventListener(function (e) {
e.cancel = true; // Prevent default home behavior
viewer.scene.camera.flyTo(nordbahnhofPosition); // Fly to Nordbahnhof
e.cancel = true;
viewer.scene.camera.flyTo(nordbahnhofPosition);
});
//////////////////////////////////////////////////////////////////////////
// Log Camera Heading, Pitch, and Roll in Console
//////////////////////////////////////////////////////////////////////////
function logCameraPosition() {
var camera = viewer.camera;
/* ----------------
3D Tiles layers
---------------- */
// Basemap buildings layer (info disabled)
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: 'https://web3d.basemap.de/cesium/buildings-fly/root.json'
})
);
// Get the camera position in Cartographic coordinates (longitude, latitude, height)
var cartographic = Cesium.Cartographic.fromCartesian(camera.position);
// Project tileset (Nordbahnhof) – main layer to style by PV
const tilesetnord = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: 'https://w2.iaf-ex.hft-stuttgart.de/CesiumData/3DTiles/Buildings/BuildingSolid/DigiTwin4PEDs/Nordbahnhof/tileset.json'
})
);
// Convert the longitude and latitude from radians to degrees
var longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6); // 6 decimal places for precision
var latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
var height = cartographic.height.toFixed(2); // Height in meters
/* -------------------------------------------------------
ALWAYS hide the GML IDs (on basemap tileset)
------------------------------------------------------- */
const hiddenGmlIds = [
// hiding these IDs on the BASEMAP tiles
'DEBW_522100053we','DEBW_52210003ulx','DEBW_52210006LHY','DEBW_52210005gIu','DEBW_52210006LNk','DEBW_52210005zbD','DEBW_52210005O8W','DEBW_52210005Kxc','DEBW_52210004zZb','DEBW_52210005PEz','DEBW_52210004IRl','DEBW_52210004ddl','DEBW_5221KL0002V','DEBW_52210005hsz','DEBW_52210003wlS','DEBW_522100050sv','DEBW_52210004FnS','DEBW_52210003ulG','DEBW_52210003ryU','DEBW_5221KL0002i','DEBW_52210003uU7','DEBW_52210004LH0','DEBW_522100050IF','DEBW_52210003vqL','DEBW_52210004gZA','DEBW_522100053qO','DEBW_522100063PZ','DEBW_52210006Iup','DEBW_522100053z0','DEBW_52210004eqA','DEBW_52210005O0q','DEBW_52210004cou','DEBW_5221000604Q','DEBW_52210004KLX','DEBW_52210004Jdv','DEBW_522100050KK','DEBW_52210004G1X','DEBW_52210006LTL','DEBW_52210005437','DEBW_52210004IDK','DEBW_52210006JQT','DEBW_5221000cD5u','DEBW_522Hc00000C','DEBW_52210004yts','DEBW_52210005Lb3','DEBW_52210005fhl','DEBW_52210004ffc','DEBW_522100052gA','DEBW_5221000549l','DEBW_52210004iD6','DEBW_522100060N7','DEBW_52210006LGU','DEBW_52210005NyR','DEBW_52210006Gc3','DEBW_522100052qj','DEBW_5221KL0002w','DEBW_52210005PXk','DEBW_52210005fgO','DEBW_52210004Hvv','DEBW_52210006MbW','DEBW_52210005LV4','DEBW_52210006LFU','DEBW_52210005J6x','DEBW_52210005y5N','DEBW_522100050IN','DEBW_52210006K64','DEBW_52210005PVy','DEBW_52210004L6O','DEBW_52210005gJc','DEBW_52210003wdH','DEBW_5221KL0002l','DEBW_52210005MWw','DEBW_52210006LIF','DEBW_52210004gf4','DEBW_52210006IuB','DEBW_52210004yYG','DEBW_52210004gZp','DEBW_52210004Lfg','DEBW_52210005z4h','DEBW_52210004zc1','DEBW_52210005JuM','DEBW_5221000c0RS','DEBW_52210005fXo','DEBW_52210003sn8','DEBW_5221KL0002j','DEBW_522100060xG','DEBW_52210005k1O','DEBW_52210004emI','DEBW_52210006MpT','DEBW_522100052p6','DEBW_52210004eUw','DEBW_52210004yr0','DEBW_522100061MX','DEBW_52210004hlr','DEBW_522Xh00003r','DEBW_52210005euN','DEBW_52210004Lfv','DEBW_52210004fqi','DEBW_522100060k1','DEBW_5221KL0002h','DEBW_52210004IqN','DEBW_52210005PxG','DEBW_52210004ft9','DEBW_52210004HGq','DEBW_52210005Oal','DEBW_5221KL0002k','DEBW_52210004iSX','DEBW_52210005PJM','DEBW_52210006KCK','DEBW_52210004gRP','DEBW_5221000642S','DEBW_522100052OM','DEBW_52210005fW2','DEBW_5221000542S','DEBW_52210004ebt','DEBW_522100063Nh','DEBW_52210005O3B','DEBW_52210005gGA','DEBW_52210003wCB','DEBW_52210005LEX','DEBW_52210004eU8','DEBW_52210006NPu','DEBW_52210004fqX','DEBW_52210005gUI','DEBW_52210005LgX','DEBW_5221KL0002y','DEBW_52210003suT','DEBW_52210004LoD','DEBW_52210004gJD','DEBW_5221KL00034','DEBW_52210005geZ','DEBW_52210005Pir','DEBW_52210004ISg','DEBW_52210005kDu','DEBW_52210003y1J','DEBW_522100062FH','DEBW_52210004dL7','DEBW_52210004JcN','DEBW_52210005MdK','DEBW_52210004yij','DEBW_52210004gcl','DEBW_522100052iz','DEBW_52210006LwJ','DEBW_522100050cr','DEBW_52210003uMg','DEBW_52210006LBH','DEBW_52210004iXp','DEBW_52210004Li3','DEBW_522100060iV','DEBW_5221KL0002m','DEBW_52210006N8X','DEBW_52210005z6J','DEBW_52210004LfC','DEBW_52210005h9F','DEBW_52210004Lq8','DEBW_52210006LLL','DEBW_52210005MFs','DEBW_52210004dRb','DEBW_52210004Kwz','DEBW_522100060ek','DEBW_522100063Ju','DEBW_52210004dIo','DEBW_522100050of','DEBW_52210004LA9','DEBW_52210004I9P','DEBW_52210005MMb','DEBW_52210004gVz','DEBW_52210004HIV','DEBW_52210006Ldu','DEBW_52210003why','DEBW_52210004gd4','DEBW_52210005NF0','DEBW_52210004fBX','DEBW_52210006LOT','DEBW_52210003w3N','DEBW_52210005yib','DEBW_52210003vCA','DEBW_52210005L7q','DEBW_52210004Hcg','DEBW_52210004cpx','DEBW_52210005gNM','DEBW_52210006LBZ','DEBW_522100047AC','DEBW_5221000Bwee','DEBW_522100061yV','DEBW_522100051dE','DEBW_52210004dKI','DEBW_52210005kDi','DEBW_52210006LFe','DEBW_52210005Nq8','DEBW_52210004zpT','DEBW_5221000626z','DEBW_52210004L3T','DEBW_52210004fiz','DEBW_522100060uC','DEBW_52210003w8x','DEBW_52210004GFF','DEBW_522100060fJ','DEBW_52210003xs3','DEBW_52210004hyo','DEBW_52210004KO9','DEBW_52210006NHq','DEBW_522100051ti','DEBW_52210005ikT','DEBW_52210003y0j','DEBW_52210005hz1','DEBW_52210003w3q','DEBW_52210004gRL','DEBW_52210005O7K','DEBW_52210005Lhw','DEBW_522100051AL','DEBW_522100051wF','DEBW_52210006LHQ','DEBW_52210004Jc6','DEBW_52210004hkG','DEBW_52210004fN8','DEBW_522100054Pb','DEBW_522100063Kw','DEBW_52210003y5u','DEBW_52210004Lcb','DEBW_52210004fHT','DEBW_52210005yty','DEBW_52210005jOj','DEBW_52210005405','DEBW_52210004fIR','DEBW_52210005PDa','DEBW_52210006NFb','DEBW_52210005gb6','DEBW_52210005N6a','DEBW_52210005zY9','DEBW_52210003uF8','DEBW_52210004gUn','DEBW_52210006M3d','DEBW_52210004cmm','DEBW_52210005Pv1','DEBW_52210005iXk','DEBW_52210005fT0','DEBW_52210004fgQ','DEBW_52210005kZo','DEBW_52210004GRa','DEBW_52210006LoV','DEBW_52210003yDp','DEBW_52210003yD2','DEBW_522u500004c','DEBW_522100051xB','DEBW_52210004hoB','DEBW_52210005hHy','DEBW_52210005yyX','DEBW_52210003y56','DEBW_52210004LhX','DEBW_52210003xu6','DEBW_52210005KAK','DEBW_52210004ga4','DEBW_52210003wcg','DEBW_52210004zPg','DEBW_52210005K2F','DEBW_52210004cav','DEBW_52210005PJP','DEBW_52210004IIN','DEBW_52210003v5F','DEBW_52210003uIy','DEBW_52210005O0m','DEBW_52210005yK7','DEBW_52210004fVM','DEBW_522100054FE','DEBW_522100050Yc','DEBW_52210006HyA','DEBW_522u500004Q','DEBW_52210006Llm','DEBW_52210003vsq','DEBW_52214b0003K','DEBW_52210003ta1','DEBW_52210006N9P','DEBW_52210005khP','DEBW_52210006NJd','DEBW_52210006NFS','DEBW_52210004IHr','DEBW_52210005PM3','DEBW_522100061sD','DEBW_522100051DC','DEBW_52210005hHL','DEBW_522100051A7','DEBW_52210003wBi','DEBW_52210004GYQ','DEBW_522100064Bf','DEBW_52210005ilS','DEBW_52210004fmV','DEBW_522100062iU','DEBW_52210005eKj','DEBW_52210006Jy0','DEBW_522100052bs','DEBW_52210005O8I','DEBW_52210006Nt4','DEBW_522100060bS','DEBW_52210003wFg','DEBW_52210005O3N','DEBW_5221000528Z','DEBW_5221000543U','DEBW_5221000626Z','DEBW_522100063UQ','DEBW_52210003w0u','DEBW_522100063po','DEBW_522100053ul','DEBW_52210003vlS','DEBW_52210006NSo','DEBW_52210005k8O','DEBW_5221000514t','DEBW_52210004LVZ','DEBW_52210005N4U','DEBW_52210003y9v','DEBW_52210004fhQ','DEBW_52210006KWm','DEBW_52210006NP1','DEBW_52210003ubf','DEBW_522100061Lp','DEBW_52210006456','DEBW_52210006M21','DEBW_52210003uX2','DEBW_522100047Gk','DEBW_522100060oe','DEBW_52210005Pbk','DEBW_52210004FoN','DEBW_52210004zWG','DEBW_522100060aT','DEBW_522100063SY','DEBW_52210005MXr','DEBW_52210005h8J','DEBW_52210005eSD','DEBW_52210004fCv','DEBW_52210004LWA','DEBW_52210004Lac','DEBW_52210005hM5','DEBW_522100051H6','DEBW_52210004GLJ','DEBW_52210004Fu7','DEBW_52210006KYS','DEBW_52210005jno','DEBW_52210006IMR','DEBW_52210004Izg','DEBW_52210005hN6','DEBW_52210003uXD','DEBW_52210005h5h','DEBW_52210005fbP','DEBW_52210004L6M','DEBW_52210005yo6','DEBW_52210005igA','DEBW_52210003uQ6','DEBW_52210006NbS','DEBW_522100051GL','DEBW_5221000547y','DEBW_52210005MxA','DEBW_52210003rs0','DEBW_52210003xzp','DEBW_52210005KsH','DEBW_52210005PwT','DEBW_52210004ggT','DEBW_52210005yvi','DEBW_522100051I9','DEBW_522100051Bn','DEBW_52210003yBb','DEBW_5221000649X','DEBW_522100060AK','DEBW_52210003xxF','DEBW_52210003run','DEBW_52210004GZK','DEBW_52210003sVG','DEBW_52210006Lut','DEBW_52210005iFY','DEBW_52210004LV4','DEBW_52210005fZi','DEBW_52210003w5n','DEBW_52210006N4A','DEBW_52210003rtb','DEBW_52210005gZO','DEBW_52210005hLi','DEBW_522Pu00006B','DEBW_5222X10002n','DEBW_5222ez0002P','DEBW_5222X10002p','DEBW_5222ez0002L','DEBW_5221hW00021','DEBW_5221hW00022','DEBW_5221hW0001y','DEBW_5222ez0002N','DEBW_5222ez0002Y','DEBW_5221hW00023','DEBW_5222ez0002a','DEBW_5222ez0002W','DEBW_5222X10002o','DEBW_5221hW00020'
];
// Build "show: false" conditions for the above IDs (basemap tileset only)
function buildHiddenShowConditionsForBasemap() {
const conditions = hiddenGmlIds.map(gmlId => [`\${gml_id} === '${gmlId}'`, false]);
conditions.push(['true', true]);
return conditions;
}
// Get the heading, pitch, and roll in degrees
var heading = Cesium.Math.toDegrees(camera.heading).toFixed(2);
var pitch = Cesium.Math.toDegrees(camera.pitch).toFixed(2);
var roll = Cesium.Math.toDegrees(camera.roll).toFixed(2);
// Apply the hiding style on the basemap tileset (call after tileset ready and anytime layer changes)
function applyHiddenBasemapStyle() {
tileset.style = new Cesium.Cesium3DTileStyle({
color: { conditions: [['true', 'color("white")']] },
show: { conditions: buildHiddenShowConditionsForBasemap() }
});
}
applyHiddenBasemapStyle(); // ensure hidden on load
// Log the camera settings to the console
console.log(`Camera Settings:
Longitude: ${longitude}°, Latitude: ${latitude}°, Height: ${height}m
Heading: ${heading}°, Pitch: ${pitch}°, Roll: ${roll}°`);
// Reset Nord tileset color to white (when "Select/Reset" is chosen)
function setDefaultColor() {
tilesetnord.style = new Cesium.Cesium3DTileStyle({ color: 'color("white")' });
}
// Add event listener to track and log camera changes
viewer.scene.camera.changed.addEventListener(logCameraPosition);
/* ---------------------------------------
District polygon (GeoJSON) on terrain
--------------------------------------- */
// draw the district polygon with a semi-transparent green fill.
// keep its alpha (transparency) consistent if recolored later.
let districtPolygonEntity = null;
let districtReadyResolve;
const districtReadyPromise = new Promise(res => (districtReadyResolve = res));
Cesium.GeoJsonDataSource.load('./Polygons.geojson').then(function (dataSource) {
dataSource.entities.values.forEach(entity => {
if (entity.polygon) {
// Sample the terrain so the polygon clamps the ground
const positions = entity.polygon.hierarchy.getValue().positions;
const carto = positions.map(p => Cesium.Cartographic.fromCartesian(p));
Cesium.sampleTerrainMostDetailed(customterrain, carto).then(updated => {
const terrainPositions = updated.map(c =>
Cesium.Cartesian3.fromRadians(c.longitude, c.latitude, c.height)
);
const existingAlpha = 0.25; // Keep translucency
districtPolygonEntity = viewer.entities.add({
name: entity.properties.Name ? entity.properties.Name.getValue() : 'District Boundary',
polygon: {
hierarchy: new Cesium.PolygonHierarchy(terrainPositions),
material: Cesium.Color.fromCssColorString(ACCENT_HEX).withAlpha(existingAlpha),
perPositionHeight: false,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
classificationType: Cesium.ClassificationType.TERRAIN
},
properties: entity.properties
});
// Add a black outline polyline around the polygon
viewer.entities.add({
polyline: {
positions: [...terrainPositions, terrainPositions[0]],
width: 3.0,
material: Cesium.Color.BLACK,
clampToGround: true
}
});
//////////////////////////////////////////////////////////////////////////
// Load 3D Tileset
//////////////////////////////////////////////////////////////////////////
var tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
//url: 'https://w2.iaf-ex.hft-stuttgart.de/CesiumData/3DTiles/Buildings/BuildingSolid/DigiTwin4PEDs/Nordbahnhof/tileset.json'
url: 'https://web3d.basemap.de/cesium/buildings-fly/root.json'
})
);
var hiddenGmlIds = ['DEBW_522100053we', 'DEBW_52210003ulx', 'DEBW_52210006LHY', 'DEBW_52210005gIu', 'DEBW_52210006LNk', 'DEBW_52210005zbD', 'DEBW_52210005O8W', 'DEBW_52210005Kxc', 'DEBW_52210004zZb', 'DEBW_52210005PEz', 'DEBW_52210004IRl', 'DEBW_52210004ddl', 'DEBW_5221KL0002V', 'DEBW_52210005hsz', 'DEBW_52210003wlS', 'DEBW_522100050sv', 'DEBW_52210004FnS', 'DEBW_52210003ulG', 'DEBW_52210003ryU', 'DEBW_5221KL0002i', 'DEBW_52210003uU7', 'DEBW_52210004LH0', 'DEBW_522100050IF', 'DEBW_52210003vqL', 'DEBW_52210004gZA', 'DEBW_522100053qO', 'DEBW_522100063PZ', 'DEBW_52210006Iup', 'DEBW_522100053z0', 'DEBW_52210004eqA', 'DEBW_52210005O0q', 'DEBW_52210004cou', 'DEBW_5221000604Q', 'DEBW_52210004KLX', 'DEBW_52210004Jdv', 'DEBW_522100050KK', 'DEBW_52210004G1X', 'DEBW_52210006LTL', 'DEBW_52210005437', 'DEBW_52210004IDK', 'DEBW_52210006JQT', 'DEBW_5221000cD5u', 'DEBW_522Hc00000C', 'DEBW_52210004yts', 'DEBW_52210005Lb3', 'DEBW_52210005fhl', 'DEBW_52210004ffc', 'DEBW_522100052gA', 'DEBW_5221000549l', 'DEBW_52210004iD6', 'DEBW_522100060N7', 'DEBW_52210006LGU', 'DEBW_52210005NyR', 'DEBW_52210006Gc3', 'DEBW_522100052qj', 'DEBW_5221KL0002w', 'DEBW_52210005PXk', 'DEBW_52210005fgO', 'DEBW_52210004Hvv', 'DEBW_52210006MbW', 'DEBW_52210005LV4', 'DEBW_52210006LFU', 'DEBW_52210005J6x', 'DEBW_52210005y5N', 'DEBW_522100050IN', 'DEBW_52210006K64', 'DEBW_52210005PVy', 'DEBW_52210004L6O', 'DEBW_52210005gJc', 'DEBW_52210003wdH', 'DEBW_5221KL0002j', 'DEBW_52210005MWw', 'DEBW_52210006LIF', 'DEBW_52210004gf4', 'DEBW_52210006IuB', 'DEBW_52210004yYG', 'DEBW_52210004gZp', 'DEBW_52210004Lfg', 'DEBW_52210005z4h', 'DEBW_52210004zc1', 'DEBW_52210005JuM', 'DEBW_5221000c0RS', 'DEBW_52210005fXo', 'DEBW_52210003sn8', 'DEBW_5221KL0002l', 'DEBW_522100060xG', 'DEBW_52210005k1O', 'DEBW_52210004emI', 'DEBW_52210006MpT', 'DEBW_522100052p6', 'DEBW_52210004eUw', 'DEBW_52210004yr0', 'DEBW_522100061MX', 'DEBW_52210004hlr', 'DEBW_522Xh00003r', 'DEBW_52210005euN', 'DEBW_52210004Lfv', 'DEBW_52210004fqi', 'DEBW_522100060k1', 'DEBW_5221KL0002h', 'DEBW_52210004IqN', 'DEBW_52210005PxG', 'DEBW_52210004ft9', 'DEBW_52210004HGq', 'DEBW_52210005Oal', 'DEBW_5221KL0002k', 'DEBW_52210004iSX', 'DEBW_52210005PJM', 'DEBW_52210006KCK', 'DEBW_52210004gRP', 'DEBW_5221000642S', 'DEBW_522100052OM', 'DEBW_52210005fW2', 'DEBW_5221000542S', 'DEBW_52210004ebt', 'DEBW_522100063Nh', 'DEBW_52210005O3B', 'DEBW_52210005gGA', 'DEBW_52210003wCB', 'DEBW_52210005LEX', 'DEBW_52210004eU8', 'DEBW_52210006NPu', 'DEBW_52210004fqX', 'DEBW_52210005gUI', 'DEBW_52210005LgX', 'DEBW_5221KL0002y', 'DEBW_52210003suT', 'DEBW_52210004LoD', 'DEBW_52210004gJD', 'DEBW_5221KL00034', 'DEBW_52210005geZ', 'DEBW_52210005Pir', 'DEBW_52210004ISg', 'DEBW_52210005kDu', 'DEBW_52210003y1J', 'DEBW_522100062FH', 'DEBW_52210004dL7', 'DEBW_52210004JcN', 'DEBW_52210005MdK', 'DEBW_52210004yij', 'DEBW_52210004gcl', 'DEBW_522100052iz', 'DEBW_52210006LwJ', 'DEBW_522100050cr', 'DEBW_52210003uMg', 'DEBW_52210006LBH', 'DEBW_52210004iXp', 'DEBW_52210004Li3', 'DEBW_522100060iV', 'DEBW_5221KL0002m', 'DEBW_52210006N8X', 'DEBW_52210005z6J', 'DEBW_52210004LfC', 'DEBW_52210005h9F', 'DEBW_52210004Lq8', 'DEBW_52210006LLL', 'DEBW_52210005MFs', 'DEBW_52210004dRb', 'DEBW_52210004Kwz', 'DEBW_522100060ek', 'DEBW_522100063Ju', 'DEBW_52210004dIo', 'DEBW_522100050of', 'DEBW_52210004LA9', 'DEBW_52210004I9P', 'DEBW_52210005MMb', 'DEBW_52210004gVz', 'DEBW_52210004HIV', 'DEBW_52210006Ldu', 'DEBW_52210003why', 'DEBW_52210004gd4', 'DEBW_52210005NF0', 'DEBW_52210004fBX', 'DEBW_52210006LOT', 'DEBW_52210003w3N', 'DEBW_52210005yib', 'DEBW_52210003vCA', 'DEBW_52210005L7q', 'DEBW_52210004Hcg', 'DEBW_52210004cpx', 'DEBW_52210005gNM', 'DEBW_52210006LBZ', 'DEBW_522100047AC', 'DEBW_5221000Bwee', 'DEBW_522100061yV', 'DEBW_522100051dE', 'DEBW_52210004dKI', 'DEBW_52210005kDi', 'DEBW_52210006LFe', 'DEBW_52210005Nq8', 'DEBW_52210004zpT', 'DEBW_5221000626z', 'DEBW_52210004L3T', 'DEBW_52210004fiz', 'DEBW_522100060uC', 'DEBW_52210003w8x', 'DEBW_52210004GFF', 'DEBW_522100060fJ', 'DEBW_52210003xs3', 'DEBW_52210004hyo', 'DEBW_52210004KO9', 'DEBW_52210006NHq', 'DEBW_522100051ti', 'DEBW_52210005ikT', 'DEBW_52210003y0j', 'DEBW_52210005hz1', 'DEBW_52210003w3q', 'DEBW_52210004gRL', 'DEBW_52210005O7K', 'DEBW_52210005Lhw', 'DEBW_522100051AL', 'DEBW_522100051wF', 'DEBW_52210006LHQ', 'DEBW_52210004Jc6', 'DEBW_52210004hkG', 'DEBW_52210004fN8', 'DEBW_522100054Pb', 'DEBW_522100063Kw', 'DEBW_52210003y5u', 'DEBW_52210004Lcb', 'DEBW_52210004fHT', 'DEBW_52210005yty', 'DEBW_52210005jOj', 'DEBW_52210005405', 'DEBW_52210004fIR', 'DEBW_52210005PDa', 'DEBW_52210006NFb', 'DEBW_52210005gb6', 'DEBW_52210005N6a', 'DEBW_52210005zY9', 'DEBW_52210003uF8', 'DEBW_52210004gUn', 'DEBW_52210006M3d', 'DEBW_52210004cmm', 'DEBW_52210005Pv1', 'DEBW_52210005iXk', 'DEBW_52210005fT0', 'DEBW_52210004fgQ', 'DEBW_52210005kZo', 'DEBW_52210004GRa', 'DEBW_52210006LoV', 'DEBW_52210003yDp', 'DEBW_52210003yD2', 'DEBW_522u500004c', 'DEBW_522100051xB', 'DEBW_52210004hoB', 'DEBW_52210005hHy', 'DEBW_52210005yyX', 'DEBW_52210003y56', 'DEBW_52210004LhX', 'DEBW_52210003xu6', 'DEBW_52210005KAK', 'DEBW_52210004ga4', 'DEBW_52210003wcg', 'DEBW_52210004zPg', 'DEBW_52210005K2F', 'DEBW_52210004cav', 'DEBW_52210005PJP', 'DEBW_52210004IIN', 'DEBW_52210003v5F', 'DEBW_52210003uIy', 'DEBW_52210005O0m', 'DEBW_52210005yK7', 'DEBW_52210004fVM', 'DEBW_522100054FE', 'DEBW_522100050Yc', 'DEBW_52210006HyA', 'DEBW_522u500004Q', 'DEBW_52210006Llm', 'DEBW_52210003vsq', 'DEBW_52214b0003K', 'DEBW_52210003ta1', 'DEBW_52210006N9P', 'DEBW_52210005khP', 'DEBW_52210006NJd', 'DEBW_52210006NFS', 'DEBW_52210004IHr', 'DEBW_52210005PM3', 'DEBW_522100061sD', 'DEBW_522100051DC', 'DEBW_52210005hHL', 'DEBW_522100051A7', 'DEBW_52210003wBi', 'DEBW_52210004GYQ', 'DEBW_522100064Bf', 'DEBW_52210005ilS', 'DEBW_52210004fmV', 'DEBW_522100062iU', 'DEBW_52210005eKj', 'DEBW_52210006Jy0', 'DEBW_522100052bs', 'DEBW_52210005O8I', 'DEBW_52210006Nt4', 'DEBW_522100060bS', 'DEBW_52210003wFg', 'DEBW_52210005O3N', 'DEBW_5221000528Z', 'DEBW_5221000543U', 'DEBW_5221000626Z', 'DEBW_522100063UQ', 'DEBW_52210003w0u', 'DEBW_522100063po', 'DEBW_522100053ul', 'DEBW_52210003vlS', 'DEBW_52210006NSo', 'DEBW_52210005k8O', 'DEBW_5221000514t', 'DEBW_52210004LVZ', 'DEBW_52210005N4U', 'DEBW_52210003y9v', 'DEBW_52210004fhQ', 'DEBW_52210006KWm', 'DEBW_52210006NP1', 'DEBW_52210003ubf', 'DEBW_522100061Lp', 'DEBW_52210006456', 'DEBW_52210006M21', 'DEBW_52210003uX2', 'DEBW_522100047Gk', 'DEBW_522100060oe', 'DEBW_52210005Pbk', 'DEBW_52210004FoN', 'DEBW_52210004zWG', 'DEBW_522100060aT', 'DEBW_522100063SY', 'DEBW_52210005MXr', 'DEBW_52210005h8J', 'DEBW_52210005eSD', 'DEBW_52210004fCv', 'DEBW_52210004LWA', 'DEBW_52210004Lac', 'DEBW_52210005hM5', 'DEBW_522100051H6', 'DEBW_52210004GLJ', 'DEBW_52210004Fu7', 'DEBW_52210006KYS', 'DEBW_52210005jno', 'DEBW_52210006IMR', 'DEBW_52210004Izg', 'DEBW_52210005hN6', 'DEBW_52210003uXD', 'DEBW_52210005h5h', 'DEBW_52210005fbP', 'DEBW_52210004L6M', 'DEBW_52210005yo6', 'DEBW_52210005igA', 'DEBW_52210003uQ6', 'DEBW_52210006NbS', 'DEBW_522100051GL', 'DEBW_5221000547y', 'DEBW_52210005MxA', 'DEBW_52210003rs0', 'DEBW_52210003xzp', 'DEBW_52210005KsH', 'DEBW_52210005PwT', 'DEBW_52210004ggT', 'DEBW_52210005yvi', 'DEBW_522100051I9', 'DEBW_522100051Bn', 'DEBW_52210003yBb', 'DEBW_5221000649X', 'DEBW_522100060AK', 'DEBW_52210003xxF', 'DEBW_52210003run', 'DEBW_52210004GZK', 'DEBW_52210003sVG', 'DEBW_52210006Lut', 'DEBW_52210005iFY', 'DEBW_52210004LV4', 'DEBW_52210005fZi', 'DEBW_52210003w5n', 'DEBW_52210006N4A', 'DEBW_52210003rtb', 'DEBW_52210005gZO', 'DEBW_52210005hLi']; // Replace with actual gml_ids you want to hide
// Apply style to hide specific buildings
tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: hiddenGmlIds.map(gmlId => {
return [`\${gml_id} === '${gmlId}'`, 'color("rgba(255, 255, 255, 0.0)")'];
}).concat([['true', 'color("white")']])
},
show: {
conditions: hiddenGmlIds.map(gmlId => {
return [`\${gml_id} === '${gmlId}'`, false]; // Hide the building
}).concat([['true', true]]) // Show all others
if (districtReadyResolve) districtReadyResolve();
});
}
});
var tilesetnord = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: 'https://w2.iaf-ex.hft-stuttgart.de/CesiumData/3DTiles/Buildings/BuildingSolid/DigiTwin4PEDs/Nordbahnhof/tileset.json'
//url: 'https://web3d.basemap.de/cesium/buildings-fly/root.json'
})
);
});
// Ensure custom terrain is used
viewer.terrainProvider = customterrain;
}).catch(console.error);
// recolor the polygon, preserve alpha (transparency) level
function applyDistrictLightGreenPreserveAlpha() {
if (!districtPolygonEntity || !districtPolygonEntity.polygon || !districtPolygonEntity.polygon.material) return;
let alpha = 0.25;
try {
const currentColor = districtPolygonEntity.polygon.material.color.getValue(viewer.clock.currentTime);
if (currentColor && typeof currentColor.alpha === 'number') alpha = currentColor.alpha;
} catch (e) {}
districtPolygonEntity.polygon.material = Cesium.Color.fromCssColorString(ACCENT_HEX).withAlpha(alpha);
}
/* -------------------------
Helper XML parse methods
------------------------- */
// helpers to read text/series from WFS responses.
function parseText(elem, tagLocal) {
const node = elem.getElementsByTagName(tagLocal)[0] ||
elem.getElementsByTagNameNS('*', tagLocal)[0];
return node ? node.textContent : null;
}
function parseValuesListFromTimeDep(timeDepElem) {
const valuesList = timeDepElem.getElementsByTagNameNS('*', 'valuesList')[0];
if (!valuesList) return null;
const parts = valuesList.textContent.trim().split(/\s+/);
const nums = parts.map(x => parseFloat(x)).filter(v => isFinite(v));
return nums.length ? nums : null;
}
function findEnergyRecord(containerElem, descExact) {
const energies = containerElem.getElementsByTagNameNS('*', 'Energy');
for (let i = 0; i < energies.length; i++) {
const e = energies[i];
const desc = parseText(e, 'description');
if (!desc) continue;
if (desc.trim().toLowerCase() !== descExact.trim().toLowerCase()) continue;
const amountNode = e.getElementsByTagNameNS('*', 'amount')[0];
const timeDep = e.getElementsByTagNameNS('*', 'timeDependentAmount')[0];
if (amountNode) {
const val = parseFloat(amountNode.textContent);
const uom = amountNode.getAttribute('uom') || '';
if (isFinite(val)) return { amount: { value: val, uom } };
}
if (timeDep) {
const series = parseValuesListFromTimeDep(timeDep);
if (series) return { series };
}
}
return null;
}
function findEnergyAmountByDescription(containerElem, descExact) {
const rec = findEnergyRecord(containerElem, descExact);
return (rec && rec.amount) ? rec.amount : null;
}
//viewer.zoomTo(tileset);
/* ------------------------------------
WFS – Per-building base attributes
------------------------------------ */
// load attributes for buildings: year, type, and PV annual values used for InfoBox and PV styling.
async function fetchBuildingAttributes() {
if (!BOOTING) spinnerEl.style.display = 'block';
try {
const url = 'https://w2-8080.iaf-ex.hft-stuttgart.de/vcs-wfsT/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=bldg:Building';
console.log('[WFS] Fetching buildings:', url);
const resp = await fetch(url);
const text = await resp.text();
const xmlDoc = new DOMParser().parseFromString(text, 'application/xml');
return parseWFSBuildings(xmlDoc);
} catch (e) {
console.error('Error fetching building attributes:', e);
return null;
} finally {
if (!BOOTING) spinnerEl.style.display = 'none';
}
}
function parseWFSBuildings(xmlDoc) {
const buildings = {};
const bldgElems = xmlDoc.getElementsByTagNameNS('*', 'Building');
const DESC_PV_POTENTIAL = 'roof top pv potential - annual photovoltaic potential yield for a typical year'; // #9
const DESC_PV_EXISTING = 'existing roof top pv yield - annual photovoltaic yield for a typical year'; // #12
for (let i = 0; i < bldgElems.length; i++) {
const b = bldgElems[i];
const gmlId = b.getAttribute('gml:id');
if (!gmlId) continue;
const yearOfConstruction = parseText(b, 'yearOfConstruction');
const bdgType = parseText(b, 'bdgType');
const pvPotential = findEnergyAmountByDescription(b, DESC_PV_POTENTIAL);
const pvExisting = findEnergyAmountByDescription(b, DESC_PV_EXISTING);
buildings[gmlId] = {
gml_id: gmlId,
year_of_construction: yearOfConstruction,
building_type: bdgType,
annual_pv_potential_MWh: pvPotential ? pvPotential.value : null,
annual_pv_existing_MWh: pvExisting ? pvExisting.value : null
};
}
console.log(`[WFS] Stored ${Object.keys(buildings).length} buildings with attributes`);
return buildings;
}
let buildingAttributes = {};
async function fetchAndStoreBuildingAttributes() {
try {
const data = await fetchBuildingAttributes();
if (data) buildingAttributes = data;
} catch (e) { console.error('Failed to fetch building attributes', e); }
}
function setDefaultColor() {
tilesetnord.style = new Cesium.Cesium3DTileStyle({
color: 'color("white")'
});
/* ------------------------------------
WFS – District (UrbanFunctionArea)
------------------------------------ */
// load district-level arrays: base load, base PV, and prices, etc.
let districtAttributes = null;
async function fetchDistrictAttributes() {
if (!BOOTING) spinnerEl.style.display = 'block';
try {
const url = 'https://w2-8080.iaf-ex.hft-stuttgart.de/vcs-wfsT/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=nrg2:UrbanFunctionArea';
console.log('[WFS] Fetching district:', url);
const resp = await fetch(url);
const text = await resp.text();
const xmlDoc = new DOMParser().parseFromString(text, 'application/xml');
districtAttributes = parseWFSDistricts(xmlDoc);
console.log('[WFS] District parsed; records:', Object.keys(districtAttributes || {}).length);
} catch (e) {
console.error('Error fetching district attributes:', e);
districtAttributes = null;
} finally {
if (!BOOTING) spinnerEl.style.display = 'none';
}
}
function parseWFSDistricts(xmlDoc) {
const result = {};
const nodes = xmlDoc.getElementsByTagNameNS('*', 'UrbanFunctionArea');
const D16 = 'Nordbahnhof annual residential electricity consumption for base behavior';
const D17 = 'Nordbahnhof annual non-residential electricity consumption for base behavior';
const D18 = 'Nordbahnhof typical summer residential 24 hours load profile for base behavior';
const D19 = 'Nordbahnhof typical winter residential 24 hours load profile for base behavior';
const D20 = 'Nordbahnhof typical summer non-residential 24 hours load profile for base behavior';
const D21 = 'Nordbahnhof typical winter non-residential 24 hours load profile for base behavior';
const D24 = 'Nordbahnhof existing roof top pv yield - typical summer 24 hours photovoltaic yield';
const D25 = 'Nordbahnhof existing roof top pv yield - typical winter 24 hours photovoltaic yield';
const D26 = 'Nordbahnhof electricity cost - summer 24 hours cost profile';
const D27 = 'Nordbahnhof electricity cost - winter 24 hours cost profile';
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
const gmlId = n.getAttribute('gml:id') || 'N.A.';
function readStringAttribute(name) {
const attrs = n.getElementsByTagNameNS('*', 'stringAttribute');
for (let j = 0; j < attrs.length; j++) {
if (attrs[j].getAttribute('name') === name) {
const v = attrs[j].getElementsByTagNameNS('*', 'value')[0];
return v ? v.textContent : null;
}
}
return null;
}
const area = readStringAttribute('District_Area');
theAreaUnit = readStringAttribute('District_Area_Unit'); // keep as-is
const buildingsEst = readStringAttribute('District_Buildings_Estimated');
const householdsEst = readStringAttribute('District_Households_Estimated');
function getSeries(desc) {
const rec = findEnergyRecord(n, desc);
return rec && rec.series ? rec.series : null;
}
function getAmount(desc) {
const rec = findEnergyRecord(n, desc);
return rec && rec.amount ? rec.amount.value : null;
}
//////////////////////////////////////////////////////////////////////////
// Fetch building attributes from WFS service
//////////////////////////////////////////////////////////////////////////
async function fetchBuildingAttributes() {
const loadingIndicator = document.getElementById('spinner');
loadingIndicator.style.display = 'block';
result[gmlId] = {
gml_id: gmlId,
area, areaUnit: theAreaUnit, buildingsEstimated: buildingsEst, householdsEstimated: householdsEst,
annual_demand_res_base_MWh: getAmount(D16),
annual_demand_nonres_base_MWh: getAmount(D17),
summer_load_res_base_24h_kWh: getSeries(D18),
winter_load_res_base_24h_kWh: getSeries(D19),
summer_load_nonres_base_24h_kWh: getSeries(D20),
winter_load_nonres_base_24h_kWh: getSeries(D21),
summer_pv_existing_24h_kWh: getSeries(D24),
winter_pv_existing_24h_kWh: getSeries(D25),
cost_summer_24h_EurPerKWh: getSeries(D26),
cost_winter_24h_EurPerKWh: getSeries(D27)
};
}
return result;
}
function getFirstDistrictRecord() {
if (!districtAttributes) return null;
const keys = Object.keys(districtAttributes);
if (keys.length === 0) return null;
return districtAttributes[keys[0]];
}
try {
const response = await fetch('https://w2-8080.iaf-ex.hft-stuttgart.de/vcs-wfsT/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=bldg:Building');
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, 'application/xml');
return parseWFSResponse(xmlDoc);
} catch (error) {
console.error('Error fetching building attributes:', error);
return null;
} finally {
loadingIndicator.style.display = 'none';
}
/* ---------------------------------------------------------
Photovoltaic potential coloring (manual, cached style)
- Buildings without a match are made transparent.
- Legend is shown when styling is applied.
--------------------------------------------------------- */
const ORIGINAL_COLORS = [
'rgb(97, 185, 73)', 'rgb(164, 199, 17)', 'rgb(178, 213, 49)',
'rgb(209, 224, 35)', 'rgb(246, 236, 0)', 'rgb(254, 206, 2)',
'rgb(249, 167, 23)', 'rgb(245, 109, 31)', 'rgb(242, 46, 34)'
];
const REVERSED_COLORS = ORIGINAL_COLORS.slice().reverse();
function pvValueToReversedColor(value) {
if (!isFinite(value)) return 'color("rgba(255,255,255,0.0)")';
if (value <= 2) return `color("${REVERSED_COLORS[0]}")`;
if (value <= 5) return `color("${REVERSED_COLORS[1]}")`;
if (value <= 10) return `color("${REVERSED_COLORS[2]}")`;
if (value <= 20) return `color("${REVERSED_COLORS[3]}")`;
if (value <= 30) return `color("${REVERSED_COLORS[4]}")`;
if (value <= 50) return `color("${REVERSED_COLORS[5]}")`;
if (value <= 75) return `color("${REVERSED_COLORS[6]}")`;
if (value <= 100) return `color("${REVERSED_COLORS[7]}")`;
return `color("${REVERSED_COLORS[8]}")`;
}
function parseWFSResponse(xmlDoc) {
const buildings = {};
const buildingElements = xmlDoc.getElementsByTagName('bldg:Building');
for (let i = 0; i < buildingElements.length; i++) {
const buildingElement = buildingElements[i];
const gmlId = buildingElement.getAttribute('gml:id');
const energyDemands = buildingElement.getElementsByTagName('energy:EnergyDemand');
const energyEnduses = [];
const buildingFunction = buildingElement.getElementsByTagName('bldg:function')[0]?.textContent;
const yearOfConstruction = buildingElement.getElementsByTagName('bldg:yearOfConstruction')[0]?.textContent;
const measuredHeight = buildingElement.getElementsByTagName('bldg:measuredHeight')[0]?.textContent;
const measuredHeightUnit = buildingElement.getElementsByTagName('bldg:measuredHeight')[0]?.getAttribute('uom')?.replace('urn:adv:uom:', '');
const buildingType = buildingElement.getElementsByTagName('energy:buildingType')[0]?.textContent;
// Updated Floor Area extraction
const floorAreaElements = buildingElement.getElementsByTagName('energy:FloorArea');
let floorAreaValue = null;
let floorAreaUom = null;
if (floorAreaElements.length > 0) {
const valueElement = floorAreaElements[0].getElementsByTagName('energy:value')[0];
floorAreaValue = valueElement?.textContent;
floorAreaUom = valueElement?.getAttribute('uom')?.replace('urn:adv:uom:', '');
}
// Cache the style so switching back to "PV" from "Reset" feels instant (doesnt really work as expected)
let pvColorStyle = null;
async function applyPhotovoltaicPotentialColoring() {
if (pvColorStyle) {
tilesetnord.style = pvColorStyle;
if (legendBox) legendBox.style.display = 'block';
console.log('[PV-Style] Re-applied cached style instantly.');
return;
}
spinnerEl.style.display = 'block';
try {
await tilesetnord.readyPromise;
if (!buildingAttributes || Object.keys(buildingAttributes).length === 0) {
await fetchAndStoreBuildingAttributes();
}
// Updated Volume extraction
const volumeElements = buildingElement.getElementsByTagName('energy:VolumeType');
let volumeValue = null;
let volumeUom = null;
if (volumeElements.length > 0) {
const valueElement = volumeElements[0].getElementsByTagName('energy:value')[0];
volumeValue = valueElement?.textContent;
volumeUom = valueElement?.getAttribute('uom')?.replace('urn:adv:uom:', '');
}
const colorConds = [];
for (const id of Object.keys(buildingAttributes)) {
const b = buildingAttributes[id];
let val = null;
if (isFinite(b.annual_pv_potential_MWh)) val = b.annual_pv_potential_MWh;
else if (isFinite(b.annual_pv_existing_MWh)) val = b.annual_pv_existing_MWh;
for (let j = 0; j < energyDemands.length; j++) {
const energyDemand = energyDemands[j];
const regularTimeSeries = energyDemand.getElementsByTagName('energy:RegularTimeSeries')[0];
const thematicDescription = regularTimeSeries?.getElementsByTagName('energy:thematicDescription')[0]?.textContent;
const values = regularTimeSeries?.getElementsByTagName('energy:values')[0]?.textContent;
const uom = regularTimeSeries?.getElementsByTagName('energy:values')[0]?.getAttribute('uom');
const acquisitionMethod = regularTimeSeries?.getElementsByTagName('energy:acquisitionMethod')[0]?.textContent;
const source = regularTimeSeries?.getElementsByTagName('energy:source')[0]?.textContent;
const timeInterval = regularTimeSeries?.getElementsByTagName('energy:timeInterval')[0]?.textContent;
const timeIntervalUnit = regularTimeSeries?.getElementsByTagName('energy:timeInterval')[0]?.getAttribute('unit');
const endUse = energyDemand.getElementsByTagName('energy:endUse')[0]?.textContent;
energyEnduses.push({
end_use: endUse,
thematic_description: thematicDescription,
regular_timeseries_values: values,
regular_timeseries_values_uom: uom,
acquisition_method: acquisitionMethod,
acquisition_source: source,
time_interval: timeInterval,
time_interval_unit: timeIntervalUnit
});
}
const colorExpr = (val == null)
? 'color("rgba(255,255,255,0.0)")' // unmatched → transparent
: pvValueToReversedColor(val);
buildings[gmlId] = {
gml_id: gmlId,
building_function: buildingFunction,
year_of_construction: yearOfConstruction,
measured_height: measuredHeight,
measured_height_unit: measuredHeightUnit,
building_type: buildingType,
floorarea_value: floorAreaValue,
floorarea_value_uom: floorAreaUom,
volumetype_value: volumeValue,
volumetype_value_uom: volumeUom,
energy_enduses: energyEnduses
};
colorConds.push([`\${gml_id} === '${id}'`, colorExpr]);
}
// Default fallback → transparent
colorConds.push(['true', 'color("rgba(255,255,255,0.0)")']);
pvColorStyle = new Cesium.Cesium3DTileStyle({ color: { conditions: colorConds } });
tilesetnord.style = pvColorStyle;
console.log('[PV-Style] Built & applied style with', colorConds.length, 'conditions. Unmatched buildings transparent.');
if (legendBox) legendBox.style.display = 'block';
} catch (e) {
console.error('Failed to apply PV coloring:', e);
} finally {
spinnerEl.style.display = 'none';
}
}
return buildings;
/* ------------------------------------------
Hover & Select – building / district info
------------------------------------------ */
// highlight on hover and cyan on click. Show building or district InfoBox with key properties.
let hoveredFeature; let hoveredOriginalColor;
let selectedFeature; let selectedOriginalBaseColor;
function clearHovered() {
if (hoveredFeature && hoveredFeature !== selectedFeature && hoveredOriginalColor) {
try { hoveredFeature.color = hoveredOriginalColor; } catch (e) {}
}
hoveredFeature = undefined; hoveredOriginalColor = undefined;
}
function clearSelected() {
if (selectedFeature && selectedOriginalBaseColor) {
try { selectedFeature.color = selectedOriginalBaseColor; } catch (e) {}
}
selectedFeature = undefined; selectedOriginalBaseColor = undefined;
if (viewer.selectedEntity) viewer.selectedEntity = undefined;
}
// Declare buildingAttributes globally
var buildingAttributes = {};
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
const picked = viewer.scene.pick(movement.endPosition);
clearHovered();
if (Cesium.defined(picked) && picked instanceof Cesium.Cesium3DTileFeature) {
if (picked.tileset === tileset) return; // disable Info on basemap
if (picked.tileset !== tilesetnord) return;
if (picked === selectedFeature) return;
hoveredFeature = picked;
hoveredOriginalColor = Cesium.Color.clone(picked.color, new Cesium.Color());
picked.color = HOVER_GREEN_OPAQUE;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
viewer.screenSpaceEventHandler.setInputAction(async function (movement) {
const picked = viewer.scene.pick(movement.position);
if (!Cesium.defined(picked)) {
clearSelected(); return;
}
if (picked instanceof Cesium.Cesium3DTileFeature) {
if (picked.tileset === tileset) return; // basemap - no InfoBox
if (picked.tileset === tilesetnord) {
if (selectedFeature && selectedFeature !== picked) clearSelected();
selectedFeature = picked;
if (hoveredFeature === picked && hoveredOriginalColor) {
selectedOriginalBaseColor = Cesium.Color.clone(hoveredOriginalColor, new Cesium.Color());
} else {
selectedOriginalBaseColor = Cesium.Color.clone(picked.color, new Cesium.Color());
}
picked.color = PICK_CYAN_OPAQUE;
const gmlId = picked.getProperty('gml_id');
const b = buildingAttributes[gmlId];
const entity = new Cesium.Entity();
entity.name = 'Nordbahnhofviertel Building';
entity.description = `
<table class="cesium-infoBox-defaultTable"><tbody>
<tr><th>Building ID</th><td>${gmlId || 'N.A.'}</td></tr>
<tr><th>Year of Construction</th><td>${(b && b.year_of_construction) || 'N.A.'}</td></tr>
<tr><th>Building Type</th><td>${(b && b.building_type) || 'N.A.'}</td></tr>
</tbody></table>`;
viewer.selectedEntity = entity;
}
return;
}
if (picked.id instanceof Cesium.Entity && picked.id.polygon) {
clearSelected();
if (!districtAttributes) { await fetchDistrictAttributes(); }
const district = getFirstDistrictRecord();
const id = district ? district.gml_id : (picked.id.id || 'N.A.');
const area = (district && district.area) ? district.area : 'N.A.';
const unit = (district && district.areaUnit) ? district.areaUnit : '';
const bld = (district && district.buildingsEstimated) ? district.buildingsEstimated : 'N.A.';
const hh = (district && district.householdsEstimated) ? district.householdsEstimated : 'N.A.';
const entity = new Cesium.Entity();
entity.name = 'Nordbahnhofviertel';
entity.description = `
<table class="cesium-infoBox-defaultTable"><tbody>
<tr><th>ID</th><td>${id}</td></tr>
<tr><th>District Area</th><td>${area} ${unit}</td></tr>
<tr><th>Estimated Buildings</th><td>${bld}</td></tr>
<tr><th>Estimated Households</th><td>${hh}</td></tr>
</tbody></table>`;
viewer.selectedEntity = entity;
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
/* ---------------------------------------
Old InfoBox from previous application charts disabled (kept off)
--------------------------------------- */
(function disableOldCharts() {
const chartContainer = document.getElementById('chartContainer');
const checkboxContainer = document.getElementById('checkboxContainer');
if (chartContainer) chartContainer.style.display = 'none';
if (checkboxContainer) checkboxContainer.style.display = 'none';
const showTimeSeriesCheckbox = document.getElementById('showTimeSeriesCheckbox');
if (showTimeSeriesCheckbox) showTimeSeriesCheckbox.checked = false;
})();
/* --------------------------------------
Dropdown to color by PV potential
-------------------------------------- */
// Show spinner while switching to/from PV style (not working as expected).
// Re-apply hidden basemap style every time.
const colorByEndUseSelect = document.getElementById('colorByEndUse');
if (colorByEndUseSelect) {
colorByEndUseSelect.addEventListener('change', async function (e) {
const val = e.target.value;
spinnerEl.style.display = 'block';
applyHiddenBasemapStyle(); // keep basemap hidden list enforced
// Fetch and populate the building attributes globally
async function fetchAndStoreBuildingAttributes() {
try {
const data = await fetchBuildingAttributes();
if (data) {
buildingAttributes = data;
}
} catch (error) {
console.error("Failed to fetch building attributes", error);
if (!val) {
setDefaultColor();
if (legendBox) legendBox.style.display = 'none';
} else if (val === 'pvPotential') {
if (legendBox) legendBox.style.display = 'none';
await applyPhotovoltaicPotentialColoring();
applyDistrictLightGreenPreserveAlpha();
applyHiddenBasemapStyle();
}
} finally {
spinnerEl.style.display = 'none';
}
});
}
fetchAndStoreBuildingAttributes();
//////////////////////////////////////////////////////////////////////////
// Function to map regular_timeseries_values to a color gradient
//////////////////////////////////////////////////////////////////////////
function getColorFromTimeseriesValue(value) {
if (value <=25) {
return 'color("rgb(97, 185, 73)")'; // Low energy consumption
} else if (value <= 50) {
return 'color("rgb(164, 199, 17)")'; // Medium energy consumption
} else if (value <= 75) {
return 'color("rgb(178, 213, 49)")'; // Medium energy consumption
} else if (value <= 100) {
return 'color("rgb(209, 224, 35)")'; // Medium energy consumption
} else if (value <= 125) {
return 'color("rgb(246, 236, 0)")'; // Medium energy consumption
} else if (value <= 150) {
return 'color("rgb(254, 206, 2)")'; // Medium energy consumption
} else if (value <= 200) {
return 'color("rgb(249, 167, 23)")'; // Medium energy consumption
} else if (value <= 250) {
return 'color("rgb(245, 109, 31)")'; // Medium energy consumption
} else {
return 'color("rgb(242, 46, 34)")'; // High energy consumption
}
/* -------------------
Camera debug logger
------------------- */
function logCameraPosition() {
const camera = viewer.camera;
const cartographic = Cesium.Cartographic.fromCartesian(camera.position);
const longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6);
const latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
const height = cartographic.height.toFixed(2);
const heading = Cesium.Math.toDegrees(camera.heading).toFixed(2);
const pitch = Cesium.Math.toDegrees(camera.pitch).toFixed(2);
const roll = Cesium.Math.toDegrees(camera.roll).toFixed(2);
console.log(`Camera Settings:
Longitude: ${longitude}°, Latitude: ${latitude}°, Height: ${height}m
Heading: ${heading}°, Pitch: ${pitch}°, Roll: ${roll}°`);
}
viewer.scene.camera.changed.addEventListener(logCameraPosition);
viewer.scene.postProcessStages.fxaa.enabled = true;
viewer.scene.highDynamicRange = false;
/* ==========================================
UI Controls – Sliders, Submit, and Clear
========================================== */
// Helper to find elements by any of a set of IDs
function findEl(ids){ for(const id of ids){ const el=document.getElementById(id); if(el) return el; } return null; }
// Element ID sets (HTML already provided)
const SLIDER1_IDS = ['scenarioSliderA','scenarioA','behaviourChange','behaviorChange'];
const SLIDER1_VALUE_IDS = ['scenarioSliderAValue','scenarioAValue','behaviourChangeValue','behaviorChangeValue'];
const SLIDER2_IDS = ['scenarioSliderB','scenarioB','pvCoverage'];
const SLIDER2_VALUE_IDS = ['scenarioSliderBValue','scenarioBValue','pvCoverageValue'];
const SUBMIT_IDS = ['scenarioSubmitBtn'];
const CLEAR_IDS = ['scenarioClearBtn','scenarioClear','clearScenarioButton'];
function getS1(){ return findEl(SLIDER1_IDS); }
function getS1Span(){ return findEl(SLIDER1_VALUE_IDS); }
function getS2(){ return findEl(SLIDER2_IDS); }
function getS2Span(){ return findEl(SLIDER2_VALUE_IDS); }
function getSubmitBtn(){ return findEl(SUBMIT_IDS); }
function getClearBtn(){ return findEl(CLEAR_IDS); }
function setS1Value(v){ const s=getS1(), sp=getS1Span(); if(s) s.value=v; if(sp) sp.textContent=`${v}%`; }
function setS2Value(v){ const s=getS2(), sp=getS2Span(); if(s) s.value=v; if(sp) sp.textContent=`${v}%`; }
// Slider 2 has minimum 3% (idle position)
function clampSlider2Min(val){ val = Number(val)||0; return val < 3 ? 3 : val; }
/* ----------------------------
Data sources for the sliders
---------------------------- */
const ELECTRICAL_SORTED_URL =
'https://w2-8080.iaf-ex.hft-stuttgart.de/vcs-wfsT/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&STOREDQUERY_ID=Electrical_Consumption_Sorted_Desc';
const PV_SORTED_URL =
'https://w2-8080.iaf-ex.hft-stuttgart.de/vcs-wfsT/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&STOREDQUERY_ID=PV_Buildings_Sorted_Desc';
let electricalSortedList = null;
let pvSortedList = null;
// Fetch buildings sorted by electrical consumption (with base and shifted load profiles)
async function fetchElectricalConsumptionSortedDesc() {
const resp = await fetch(ELECTRICAL_SORTED_URL);
const text = await resp.text();
const xmlDoc = new DOMParser().parseFromString(text, 'application/xml');
const out = [];
const buildings = xmlDoc.getElementsByTagNameNS('*', 'Building');
for (let i = 0; i < buildings.length; i++) {
const b = buildings[i];
const gml_id = b.getAttribute('gml:id') || null;
const recSummerBase = findEnergyRecord(b, 'base user behavior - typical summer 24 hours load profile');
const summer_base_24h_kWh = recSummerBase && recSummerBase.series ? recSummerBase.series : null;
const recWinterBase = findEnergyRecord(b, 'base user behavior - typical winter 24 hours load profile');
const winter_base_24h_kWh = recWinterBase && recWinterBase.series ? recWinterBase.series : null;
const recSummerShift = findEnergyRecord(b, 'load shift behavior - typical summer 24 hours load profile');
const summer_shifted_24h_kWh = recSummerShift && recSummerShift.series ? recSummerShift.series : null;
const recWinterShift = findEnergyRecord(b, 'load shift behavior - typical winter 24 hours load profile');
const winter_shifted_24h_kWh = recWinterShift && recWinterShift.series ? recWinterShift.series : null;
out.push({ gml_id, summer_base_24h_kWh, winter_base_24h_kWh, summer_shifted_24h_kWh, winter_shifted_24h_kWh });
}
console.log('[Slider1] Loaded buildings:', out.length);
return out;
}
//////////////////////////////////////////////////////////////////////////
// Apply color based on the selected energy demand type
//////////////////////////////////////////////////////////////////////////
// Fetch buildings sorted by PV potential (with 24h PV arrays)
async function fetchPvBuildingsSortedDesc() {
const resp = await fetch(PV_SORTED_URL);
const text = await resp.text();
const xmlDoc = new DOMParser().parseFromString(text, 'application/xml');
const out = [];
const buildings = xmlDoc.getElementsByTagNameNS('*', 'Building');
for (let i = 0; i < buildings.length; i++) {
const b = buildings[i];
const gml_id = b.getAttribute('gml:id') || null;
const recSummerPv = findEnergyRecord(b, 'roof top pv potential - typical summer 24 hours photovoltaic potential yield');
const summer_pv_24h_kWh = recSummerPv && recSummerPv.series ? recSummerPv.series : null;
const recWinterPv = findEnergyRecord(b, 'roof top pv potential - typical winter 24 hours photovoltaic potential yield');
const winter_pv_24h_kWh = recWinterPv && recWinterPv.series ? recWinterPv.series : null;
out.push({ gml_id, summer_pv_24h_kWh, winter_pv_24h_kWh });
}
console.log('[Slider2] Loaded PV buildings:', out.length);
return out;
}
async function ensureElectricalDataLoaded() {
if (!electricalSortedList) electricalSortedList = await fetchElectricalConsumptionSortedDesc();
}
async function ensurePvDataLoaded() {
if (!pvSortedList) pvSortedList = await fetchPvBuildingsSortedDesc();
}
async function applyColorToBuildings(thematicDescriptionToSearch) {
const webServiceData = await fetchBuildingAttributes(); // Fetch building attributes
/* ---------------------------------
Selection state from both sliders
--------------------------------- */
// store selected building IDs for each slider. DO NOT log GML IDs during sliding; log them on Submit button click.
let selectedBySlider1Set = new Set();
let selectedBySlider2Set = new Set();
// Bind slider 1 (behavior change). Only updates the selection; no KPI changes yet.
function bindSlider1() {
const s = getS1();
if (!s) return false;
const onInput = async (e) => {
await ensureElectricalDataLoaded();
const total = electricalSortedList.length;
const pct = Math.max(0, Math.min(100, Number(e.target.value)||0));
const cntRaw = Math.round((pct/100) * total);
const count = pct>0 && cntRaw===0 ? 1 : cntRaw;
const ids = electricalSortedList.slice(0, count).map(b => b.gml_id).filter(Boolean);
selectedBySlider1Set = new Set(ids);
const span = getS1Span(); if (span) span.textContent = `${pct}%`;
console.log(`[Slider1] ${pct}% → selecting ${ids.length}/${total} buildings (IDs will be logged on Submit)`);
};
s.addEventListener('input', onInput);
s.addEventListener('change', onInput);
console.log('[Slider1] bound to #' + s.id);
return true;
}
if (!webServiceData) {
console.error('Failed to fetch or parse web service data.');
return;
// Bind slider 2 (PV coverage). Only updates the selection; no KPI changes yet.
function bindSlider2() {
const s = getS2();
if (!s) return false;
const onInput = async (e) => {
await ensurePvDataLoaded();
let raw = Number(e.target.value)||0;
const pct = clampSlider2Min(raw);
if (pct !== raw) e.target.value = pct;
const total = pvSortedList.length;
if (pct <= 3) {
selectedBySlider2Set.clear();
console.log(`[Slider2] ${pct}% (idle) → selecting 0/${total} buildings (cleared; IDs will be logged on Submit)`);
} else {
const cntRaw = Math.round((pct/100) * total);
const count = pct>0 && cntRaw===0 ? 1 : cntRaw;
const ids = pvSortedList.slice(0, count).map(b => b.gml_id).filter(Boolean);
selectedBySlider2Set = new Set(ids);
console.log(`[Slider2] ${pct}% → selecting ${ids.length}/${total} buildings (IDs will be logged on Submit)`);
}
const span = getS2Span(); if (span) span.textContent = `${pct}%`;
};
s.addEventListener('input', onInput);
s.addEventListener('change', onInput);
console.log('[Slider2] bound to #' + s.id);
return true;
}
const conditions = Object.keys(webServiceData).map((gml_id) => {
const buildingData = webServiceData[gml_id];
let color = 'color("rgba(255, 255, 255, 0.0)")'; // Default color
//////////////////////////////////////////////////////////////////////////
// Check for energy_enduses array and specific thematic_description
//////////////////////////////////////////////////////////////////////////
if (buildingData.energy_enduses && Array.isArray(buildingData.energy_enduses)) {
buildingData.energy_enduses.forEach((enduse) => {
if (enduse.thematic_description === thematicDescriptionToSearch) {
const timeseriesValue = enduse.regular_timeseries_values;
color = getColorFromTimeseriesValue(timeseriesValue);
}
});
}
return [`\${gml_id} === '${gml_id}'`, color];
});
/* ======================================
Right Panel – Cards 1, 2, and 3 setup
====================================== */
/* ---------------
Card 1 (Savings)
--------------- */
// Default: show N.A., hide arrow, show helper message
function setCostSavingIdle() {
const kpiHero = document.querySelector('#vizSavings .kpi-hero');
const trendEl = document.querySelector('#vizSavings .kpi-trend');
if (kpiHero) kpiHero.textContent = 'N.A.';
if (trendEl) trendEl.style.display = 'none';
const coinArea = document.querySelector('#vizSavings .viz-placeholder');
if (coinArea) {
coinArea.textContent =
'Please move either the behaviour change slider or the photovoltaic coverage slider or both.';
}
console.log('[KPI] Cost saving set to N.A. (idle, triangle hidden)');
}
conditions.push(['true', 'color("rgba(255, 255, 255, 0.1)")']); // Default color for unmatched buildings
/* --------------------------
Card 2 (Annual Coverage)
-------------------------- */
// Render a simple horizontal stacked bar (PV vs Grid). Tooltip displays energy in MWh with German decimal format.
let lastCoveragePct = { pv: 0, grid: 100 };
let lastCoverageEnergy = { pv: 0, grid: 0 }; // store kWh (convert in tooltip)
function ensureCoverageBarDom() {
const host = document.querySelector('#vizCoverage .viz-placeholder');
if (!host) return null;
const titleSpan = document.querySelector('#vizCoverage .viz-card-title .title-left span');
if (titleSpan) titleSpan.textContent = 'Annual Energy Coverage';
// Keep dotted border inside and prevent overflow
host.style.position = 'relative';
host.style.boxSizing = 'border-box';
host.style.padding = '34px 12px 14px 12px';
host.style.minHeight = '86px';
host.style.margin = '0';
host.style.width = '100%';
host.style.maxWidth = '100%';
host.style.borderRadius = '10px';
host.style.overflow = 'hidden';
let bar = host.querySelector('#pvCoverageBar');
if (!bar) {
// Wrapper around the two filled sections
const wrapper = document.createElement('div');
wrapper.id = 'pvCoverageBar';
wrapper.style.display = 'flex';
wrapper.style.width = '100%';
wrapper.style.height = '22px';
wrapper.style.borderRadius = '12px';
wrapper.style.overflow = 'hidden';
wrapper.style.marginTop = '6px';
wrapper.style.boxShadow = 'inset 0 0 0 1px rgba(0,0,0,0.1)';
wrapper.style.position = 'relative';
wrapper.style.cursor = 'default';
// Green PV fill
const pv = document.createElement('div');
pv.id = 'pvCoverageFill';
pv.style.height = '100%';
pv.style.background = ACCENT_HEX;
pv.style.display = 'flex';
pv.style.alignItems = 'center';
pv.style.justifyContent = 'center';
pv.style.fontSize = '12px';
pv.style.color = '#0b3a1b';
pv.style.fontWeight = '600';
// Red Grid fill
const grid = document.createElement('div');
grid.id = 'gridShareFill';
grid.style.height = '100%';
grid.style.background = '#ef4444';
grid.style.display = 'flex';
grid.style.alignItems = 'center';
grid.style.justifyContent = 'center';
grid.style.fontSize = '12px';
grid.style.color = '#3a0b0b';
grid.style.fontWeight = '600';
wrapper.appendChild(pv);
wrapper.appendChild(grid);
host.innerHTML = '';
host.appendChild(wrapper);
// Legend pinned to top-right
const legend = document.createElement('div');
legend.className = 'legend-overlay';
legend.style.position = 'absolute';
legend.style.top = '8px';
legend.style.right = '10px';
legend.style.display = 'flex';
legend.style.gap = '12px';
legend.style.pointerEvents = 'none';
legend.style.fontSize = '13px';
legend.style.lineHeight = '1';
legend.style.whiteSpace = 'nowrap';
legend.innerHTML = `
<span class="legend-item" style="display:inline-flex;align-items:center;gap:6px;"><span class="legend-dot pv" style="width:10px;height:10px;border-radius:50%;background:${ACCENT_HEX};display:inline-block"></span>Photovoltaics (Roof)</span>
<span class="legend-item" style="display:inline-flex;align-items:center;gap:6px;"><span class="legend-dot grid" style="width:10px;height:10px;border-radius:50%;background:#ef4444;display:inline-block"></span>Grid</span>
`;
host.appendChild(legend);
// Tooltip box
const tip = document.createElement('div');
tip.id = 'coverageTooltip';
tip.style.position = 'absolute';
tip.style.pointerEvents = 'none';
tip.style.background = 'rgba(20,20,20,0.92)';
tip.style.border = '1px solid rgba(255,255,255,0.12)';
tip.style.borderRadius = '6px';
tip.style.padding = '8px 10px';
tip.style.fontSize = '12px';
tip.style.color = '#fff';
tip.style.display = 'none';
tip.style.zIndex = '2';
host.appendChild(tip);
// DE formatter and kWh - MWh converter used only in tooltip
const deFmt = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 3, maximumFractionDigits: 3 });
function fmtMWh(vKWh){ const mwh = Number(vKWh || 0) / 1000; return `${deFmt.format(mwh)} MWh`; }
// Render tooltip text with latest energy values
function renderTip() {
tip.innerHTML =
`<b>Annual Energy Coverage</b><br/>
<span style="color:${ACCENT_HEX}">●</span> Photovoltaic (Roof): ${fmtMWh(lastCoverageEnergy.pv)}<br/>
<span style="color:#ef4444">●</span> Grid: ${fmtMWh(lastCoverageEnergy.grid)}`;
}
// Apply the style to the 3D tileset
tilesetnord.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: conditions
}
});
// After the buildings are colored, display the legend box
const legendBox = document.getElementById('legendBox');
legendBox.style.display = 'block'; // Show the legend after the buildings are colored
}
//////////////////////////////////////////////////////////////////////////
// Handle dropdown selection
//////////////////////////////////////////////////////////////////////////
// Show the legend when an option is selected
document.getElementById('colorByEndUse').addEventListener('change', function (event) {
const selectedValue = event.target.value;
if (selectedValue === 'specificDomesticHotWater') {
applyColorToBuildings('specific domestic hot water demand for a typical year'); // Color buildings and display legend
} else if (selectedValue === 'specificSpaceHeating') {
applyColorToBuildings('specific space heating demand for a typical year'); // Color buildings and display legend
} else if (selectedValue === 'specificSpaceCooling') {
applyColorToBuildings('specific space cooling demand for a typical year'); // Color buildings and display legend
} else {
setDefaultColor(); // Reset to default white if no specific option is selected
document.getElementById('legendBox').style.display = 'none'; // Hide the legend when no selection
// Keep tooltip within the dotted box
function moveTip(ev){
const rect = host.getBoundingClientRect();
const x = ev.clientX - rect.left + 12;
const y = ev.clientY - rect.top - 10;
tip.style.left = Math.min(rect.width - 220, Math.max(8, x)) + 'px';
tip.style.top = Math.min(rect.height - 90, Math.max(8, y)) + 'px';
}
});
wrapper.addEventListener('mouseenter', (ev)=>{ renderTip(); tip.style.display='block'; moveTip(ev); });
wrapper.addEventListener('mousemove', moveTip);
wrapper.addEventListener('mouseleave', ()=>{ tip.style.display='none'; });
}
return host.querySelector('#pvCoverageBar');
}
// Set the default color when the tileset is ready
tilesetnord.readyPromise.then(setDefaultColor).catch(function(error) {
console.error('Error loading tileset:', error);
});
// Update the stacked bar widths and store the raw energies for tooltip
function renderPvCoverageBar(pvCoveragePct, gridSharePct, pv_kWh, grid_kWh) {
const bar = ensureCoverageBarDom();
if (!bar) return;
const pvEl = bar.querySelector('#pvCoverageFill');
const gridEl = bar.querySelector('#gridShareFill');
const pvW = Math.max(0, Math.min(100, pvCoveragePct));
const grW = Math.max(0, Math.min(100, gridSharePct));
pvEl.style.width = pvW + '%';
gridEl.style.width = grW + '%';
pvEl.textContent = `${Math.round(pvW)}%`;
gridEl.textContent = `${Math.round(grW)}%`;
lastCoveragePct = { pv: pvW, grid: grW };
lastCoverageEnergy = {
pv: Number(pv_kWh || 0), // store in kWh (tooltip converts to MWh)
grid: Number(grid_kWh || 0)
};
}
//////////////////////////////////////////////////////////////////////////
// Function to format and filter energy end uses with collapsible sections
//////////////////////////////////////////////////////////////////////////
function filterEnergyEnduses(enduses) {
return enduses.filter(enduse =>
!enduse.thematic_description.includes("monthly space heating demand for a typical year") &&
!enduse.thematic_description.includes("monthly space cooling demand for a typical year")
);
/* -----------------------------
Card 3 (24h Load Line Chart)
----------------------------- */
// Keep the dotted border inside, ensure x-axis visible, "New load" (blue), and "Photovoltaic potential" (green).
function ensureDailyChartDom() {
const host = document.querySelector('#vizLoadProfile .viz-placeholder');
if (!host) { console.warn('[Chart] LoadProfile host not found'); return null; }
host.style.position = 'relative';
host.style.boxSizing = 'border-box';
host.style.padding = '12px 10px 22px 10px';
// host.style.minHeight = '420px';
host.style.margin = '0';
host.style.width = '100%';
host.style.maxWidth = '100%';
host.style.borderRadius = '10px';
host.style.overflow = 'hidden';
let chart = host.querySelector('#dailyLoadChart');
if (!chart) {
chart = document.createElement('div');
chart.id = 'dailyLoadChart';
chart.style.width = '100%';
chart.style.height = '320px';
host.innerHTML = '';
host.appendChild(chart);
}
return chart;
}
function formatEnergyEnduses(energyEnduses) {
let filteredEnduses = filterEnergyEnduses(energyEnduses); // Apply the filter
let groupedEnduses = {};
// Chart state: base series, new series, current season, and whether "New load" is visible
let dailyChart;
let baseSeries = null;
let newSeries = null;
let currentSeason = 'summer';
let newLoadActive = false;
// Build Highcharts series with proper styles
function makeElegantSeries({currentLoad, newLoad, pv}) {
const current = {
name: 'Current load',
type: 'spline',
data: currentLoad,
color: '#ef4444',
dashStyle: 'ShortDot',
lineWidth: 2,
zIndex: 1,
states: { hover: { lineWidth: 2.5 } }
};
const updated = {
name: 'New load',
type: 'spline',
data: newLoad,
color: '#3b82f6',
lineWidth: 3,
zIndex: 3,
shadow: true,
states: { hover: { lineWidth: 3.5 } },
visible: !!newLoadActive // visible only if slider 1 > 0 on Submit
};
const pvSeries = {
name: 'Photovoltaic potential',
type: 'spline',
data: pv,
color: '#22c55e',
lineWidth: 3,
zIndex: 2,
states: { hover: { lineWidth: 3.5 } }
};
return [updated, current, pvSeries];
}
// Group energy end uses by type using <energy:endUse> values
filteredEnduses.forEach(enduse => {
if (!groupedEnduses[enduse.end_use]) {
groupedEnduses[enduse.end_use] = [];
}
groupedEnduses[enduse.end_use].push(enduse);
});
// Generate HTML for energy end uses with collapsible details
let result = '<tr><th colspan="2">Energy End Uses</th></tr>';
for (let [enduse, entries] of Object.entries(groupedEnduses)) {
result += `<tr><td colspan="2">
<details>
<summary>${enduse || 'N/A'}</summary>
<table>`;
entries.forEach((entry, index) => {
result += `
<tr><th>Thematic Description (${index + 1})</th><td>${entry.thematic_description || 'N/A'}</td></tr>
<tr><th>Regular Timeseries Value (${index + 1})</th><td>${entry.regular_timeseries_values || 'N/A'} ${entry.regular_timeseries_values_uom || ''}</td></tr>
<tr><th>Time Interval (${index + 1})</th><td>${entry.time_interval || 'N/A'} ${entry.time_interval_unit || ''}</td></tr>
<tr><th>Acquisition Method (${index + 1})</th><td>${entry.acquisition_method || 'N/A'}</td></tr>
<tr><th>Acquisition Source (${index + 1})</th><td>${entry.acquisition_source || 'N/A'}</td></tr>
`;
// Render the chart for Summer or Winter with subtle animation
function renderDailyLineChartForSeason(season) {
const container = ensureDailyChartDom();
if (!container || !baseSeries) return;
currentSeason = season === 'winter' ? 'winter' : 'summer';
const categories = Array.from({length:24}, (_,i)=> String(i).padStart(2,'0') + ':00');
const baseLoad = currentSeason==='summer' ? baseSeries.loadSummer24 : baseSeries.loadWinter24;
const basePv = currentSeason==='summer' ? baseSeries.sPv : baseSeries.wPv;
const useNewLoad = newSeries ? (currentSeason==='summer' ? newSeries.newLoadSummer : newSeries.newLoadWinter) : baseLoad;
const useNewPv = (newSeries && ((currentSeason==='summer' && newSeries.newPvSummer) || (currentSeason==='winter' && newSeries.newPvWinter)))
? (currentSeason==='summer' ? newSeries.newPvSummer : newSeries.newPvWinter)
: basePv;
const series = makeElegantSeries({
currentLoad: baseLoad,
newLoad: useNewLoad,
pv: useNewPv
});
// Render the chart
dailyChart = Highcharts.chart(container.id, {
chart: {
type: 'spline',
reflow: true,
animation: { duration: 450 },
backgroundColor: 'transparent',
spacingTop: 8, spacingRight: 8, spacingBottom: 12, spacingLeft: 8,
marginTop: 10, marginRight: 10, marginBottom: 76, marginLeft: 60
},
title: { text: null },
xAxis: {
categories,
title: { text: 'Hour' },
tickInterval: 1,
lineColor: 'rgba(255,255,255,0.15)',
tickColor: 'rgba(255,255,255,0.2)',
gridLineWidth: 0,
labels: { style: { color: '#cbd5e1', fontSize: '10px' }, y: 18 }
},
yAxis: {
title: { text: 'kWh', style: { color: '#e5e7eb' } },
min: 0,
gridLineColor: 'rgba(255,255,255,0.08)',
labels: { style: { color: '#cbd5e1' } }
},
legend: {
align: 'right', verticalAlign: 'top', y: -2, x: -2, itemStyle: { color: '#e5e7eb' }
},
credits: { enabled:false },
plotOptions: {
series: { marker: { enabled: false }, animation: { duration: 450 } }
},
series,
tooltip: {
shared: true,
borderRadius: 6,
backgroundColor: 'rgba(20,20,20,0.92)',
style: { color: '#fff' },
formatter: function () {
const cats = this?.points?.[0]?.series?.xAxis?.categories || [];
let idx = (typeof this.x === 'number') ? this.x : this?.points?.[0]?.point?.x;
if (!Number.isFinite(idx)) idx = 0;
const hourLabel = (cats[idx] || (String(idx).padStart(2,'0') + ':00'));
let body = '';
(this.points || []).forEach(p => {
const val = (typeof p.y === 'number') ? p.y.toFixed(3) : p.y;
body += `<span style="color:${p.color}">●</span> ${p.series.name}: ${val} kWh<br/>`;
});
result += `</table></details></td></tr>`;
return `<b>Time: ${hourLabel}</b><br/>${body}`;
}
}
return result;
});
}
/* ---------------------------------------
Base coverage / arrays from district
--------------------------------------- */
// Combine residential + non-residential base load for Summer/Winter. Compute annual self-used PV and annual total load for default KPIs.
function computeBasePvCoverageFromDistrict(d) {
if (!d) return null;
const sRes = d.summer_load_res_base_24h_kWh || [];
const sNon = d.summer_load_nonres_base_24h_kWh || [];
const wRes = d.winter_load_res_base_24h_kWh || [];
const wNon = d.winter_load_nonres_base_24h_kWh || [];
const sPv = d.summer_pv_existing_24h_kWh || [];
const wPv = d.winter_pv_existing_24h_kWh || [];
const selfUsedSummer = Array.from({length:24}, (_,h)=> Math.min((sPv[h]||0),(sRes[h]||0)+(sNon[h]||0)));
const selfUsedWinter = Array.from({length:24}, (_,h)=> Math.min((wPv[h]||0),(wRes[h]||0)+(wNon[h]||0)));
const sum = (arr)=> arr.reduce((a,b)=> a + (Number(b)||0), 0);
const SUMMER_DAYS = 153, WINTER_DAYS = 212;
const annualSelfUsed_kWh = sum(selfUsedSummer)*SUMMER_DAYS + sum(selfUsedWinter)*WINTER_DAYS;
const loadSummer24 = Array.from({length:24}, (_,h)=> (sRes[h]||0)+(sNon[h]||0));
const loadWinter24 = Array.from({length:24}, (_,h)=> (wRes[h]||0)+(wNon[h]||0));
const annualLoad_kWh = sum(loadSummer24)*SUMMER_DAYS + sum(loadWinter24)*WINTER_DAYS;
const pvCoveragePct = annualLoad_kWh>0 ? 100*(annualSelfUsed_kWh/annualLoad_kWh) : 0;
const gridSharePct = 100 - pvCoveragePct;
return { pvCoveragePct, gridSharePct, loadSummer24, loadWinter24, sPv, wPv, annualSelfUsed_kWh, annualLoad_kWh };
}
//////////////////////////////////////////////////////////////////////////
// Function to format building attributes into HTML and include energy end uses
//////////////////////////////////////////////////////////////////////////
function formatBuildingAttributes(buildingData) {
const buildingInfo = `
<tr><th>Building Function</th><td>${buildingData.building_function || 'N/A'}</td></tr>
<tr><th>Year of Construction</th><td>${buildingData.year_of_construction || 'N/A'}</td></tr>
<tr><th>Measured Height</th><td>${buildingData.measured_height || 'N/A'} ${buildingData.measured_height_unit || ''}</td></tr>
<tr><th>Building Type</th><td>${buildingData.building_type || 'N/A'}</td></tr>
<tr><th>Floor Area</th><td>${buildingData.floorarea_value || 'N/A'} ${buildingData.floorarea_value_uom || ''}</td></tr>
<tr><th>Volume Type</th><td>${buildingData.volumetype_value || 'N/A'} ${buildingData.volumetype_value_uom || ''}</td></tr>
`;
/* ------------------------------
Season toggle click handlers
------------------------------ */
const EL_TOGGLE_SUMMER_IDS = ['seasonSummer'];
const EL_TOGGLE_WINTER_IDS = ['seasonWinter'];
function q$ids(ids){ return findEl(ids); }
function bindSeasonToggleHandlers() {
const summerEl = q$ids(EL_TOGGLE_SUMMER_IDS);
const winterEl = q$ids(EL_TOGGLE_WINTER_IDS);
if (!summerEl || !winterEl) {
console.warn('[Toggle] Season radios not found.');
return;
}
const summerLabel = document.querySelector('label[for="seasonSummer"]');
const winterLabel = document.querySelector('label[for="seasonWinter"]');
const toSummer = () => { currentSeason = 'summer'; summerEl.checked = true; renderDailyLineChartForSeason('summer'); };
const toWinter = () => { currentSeason = 'winter'; winterEl.checked = true; renderDailyLineChartForSeason('winter'); };
summerEl.addEventListener('change', () => { if (summerEl.checked) toSummer(); });
winterEl.addEventListener('change', () => { if (winterEl.checked) toWinter(); });
if (summerLabel) summerLabel.addEventListener('click', () => setTimeout(toSummer, 0));
if (winterLabel) winterLabel.addEventListener('click', () => setTimeout(toWinter, 0));
if (summerEl.checked) toSummer(); else if (winterEl.checked) toWinter();
}
const energyEnduses = buildingData.energy_enduses ? formatEnergyEnduses(buildingData.energy_enduses) : '';
/* -----------------------------
Render default right-panel UI
----------------------------- */
function renderDefaultKPIs() {
setCostSavingIdle();
// Compute default PV coverage and set Card 2
const district = getFirstDistrictRecord();
const base = computeBasePvCoverageFromDistrict(district);
if (!base) { renderPvCoverageBar(0, 100, 0, 0); return; }
const pv_kWh = base.annualSelfUsed_kWh;
const grid_kWh = Math.max(0, base.annualLoad_kWh - base.annualSelfUsed_kWh);
renderPvCoverageBar(base.pvCoveragePct, base.gridSharePct, pv_kWh, grid_kWh);
// Save default arrays for Card 3
baseSeries = {
loadSummer24: base.loadSummer24,
loadWinter24: base.loadWinter24,
sPv: base.sPv,
wPv: base.wPv,
annualSelfUsed_kWh: base.annualSelfUsed_kWh,
annualLoad_kWh: base.annualLoad_kWh
};
newSeries = null;
newLoadActive = false;
// Set season back to Summer for default view
currentSeason = 'summer';
const summerEl = q$ids(EL_TOGGLE_SUMMER_IDS);
if (summerEl) summerEl.checked = true;
const winterEl = q$ids(EL_TOGGLE_WINTER_IDS);
if (winterEl) winterEl.checked = false;
// Draw the default chart and ensure toggles work
renderDailyLineChartForSeason('summer');
bindSeasonToggleHandlers();
// Default logs for Card 2 and 3
console.log('[Defaults] Card2 Annual Coverage -> PV%:', +base.pvCoveragePct.toFixed(3), 'Grid%:', +base.gridSharePct.toFixed(3),
'AnnualSelfUsed_kWh:', +pv_kWh.toFixed(3), 'AnnualLoad_kWh:', +base.annualLoad_kWh.toFixed(3));
console.log('[Defaults] Card3 (Summer) Current load (kWh):', base.loadSummer24.map(v=>+Number(v||0).toFixed(3)));
console.log('[Defaults] Card3 (Winter) Current load (kWh):', base.loadWinter24.map(v=>+Number(v||0).toFixed(3)));
console.log('[Defaults] Card3 PV Summer (kWh):', base.sPv.map(v=>+Number(v||0).toFixed(3)));
console.log('[Defaults] Card3 PV Winter (kWh):', base.wPv.map(v=>+Number(v||0).toFixed(3)));
}
return buildingInfo + energyEnduses;
/* ===========================================================
SUBMIT: compute new arrays and update Cards 1–3 together
=========================================================== */
// Helper: add array B into A hour-by-hour
function add24(dest, src){ for(let h=0;h<24;h++){ dest[h]=(dest[h]||0)+(src[h]||0);} return dest; }
// A small helper to format arrays in logs with 3 decimals
function fmt24(arr){ return (arr||[]).map(v=> +Number(v||0).toFixed(3)); }
// Compute new load profiles and new PV arrays from slider selections
function computeNewProfiles() {
if (!baseSeries) return null;
const district = getFirstDistrictRecord();
if (!district) return null;
// Non-Residential base profiles come from district
const nonResSummer = district.summer_load_nonres_base_24h_kWh || new Array(24).fill(0);
const nonResWinter = district.winter_load_nonres_base_24h_kWh || new Array(24).fill(0);
// Slider selections (lists of GML IDs)
const selected1 = Array.from(selectedBySlider1Set);
const selected2 = Array.from(selectedBySlider2Set);
// Start building new load arrays
const loadSummer = new Array(24).fill(0);
const loadWinter = new Array(24).fill(0);
// Non-selected set = all - selected (for slider 1)
const all1 = new Set((electricalSortedList||[]).map(b => b.gml_id).filter(Boolean));
const nonSelected1 = Array.from(all1).filter(id => !selectedBySlider1Set.has(id));
// Sum shifted profiles for selected (slider 1)
for (const id of selected1) {
const b = (electricalSortedList||[]).find(x => x.gml_id===id); if (!b) continue;
add24(loadSummer, b.summer_shifted_24h_kWh||[]);
add24(loadWinter, b.winter_shifted_24h_kWh||[]);
}
// Sum base profiles for non-selected (slider 1)
for (const id of nonSelected1) {
const b = (electricalSortedList||[]).find(x => x.gml_id===id); if (!b) continue;
add24(loadSummer, b.summer_base_24h_kWh||[]);
add24(loadWinter, b.winter_base_24h_kWh||[]);
}
// Add non-residential from district
add24(loadSummer, nonResSummer);
add24(loadWinter, nonResWinter);
// Build new PV arrays if slider 2 selected anything, else keep "base PV"
let pvSummer = null, pvWinter = null;
if (selected2.length > 0) {
pvSummer = new Array(24).fill(0);
pvWinter = new Array(24).fill(0);
for (const id of selected2) {
const b = (pvSortedList||[]).find(x => x.gml_id===id); if (!b) continue;
add24(pvSummer, b.summer_pv_24h_kWh||[]);
add24(pvWinter, b.winter_pv_24h_kWh||[]);
}
// Add base PV from district
add24(pvSummer, baseSeries.sPv || []);
add24(pvWinter, baseSeries.wPv || []);
}
// Detailed logs for Card 3 new arrays
console.log('[Submit][Card3] NewLoad Summer (kWh):', fmt24(loadSummer));
console.log('[Submit][Card3] NewLoad Winter (kWh):', fmt24(loadWinter));
if (pvSummer) console.log('[Submit][Card3] NewPV Summer (kWh):', fmt24(pvSummer));
if (pvWinter) console.log('[Submit][Card3] NewPV Winter (kWh):', fmt24(pvWinter));
return {
newLoadSummer: loadSummer,
newLoadWinter: loadWinter,
newPvSummer: pvSummer,
newPvWinter: pvWinter
};
}
//////////////////////////////////////////////////////////////////////////
// Function to extract monthly demand values from building attributes
//////////////////////////////////////////////////////////////////////////
function extractMonthlyDemand(buildingData, thematicDescription) {
const enduses = buildingData.energy_enduses;
// Find the specific enduse matching the given thematic description
const matchingEnduse = enduses.find(enduse => enduse.thematic_description === thematicDescription);
/* ------------------------------------------------------
KPI calculations from "fresh" arrays (Cards 1 & 2)
------------------------------------------------------ */
function sum(arr){ return (arr||[]).reduce((a,b)=> a + (Number(b)||0), 0); }
function map24(a, fn){ return Array.from({length:24}, (_,h)=> fn(Number(a?.[h]||0), h)); }
function computeKpisFromNew(district, base, fresh) {
if (!district || !base || !fresh) return null;
const SUMMER_DAYS=153, WINTER_DAYS=212;
// Base arrays (for GridBase)
const baseLoadS = base.loadSummer24 || new Array(24).fill(0);
const baseLoadW = base.loadWinter24 || new Array(24).fill(0);
const basePvS = base.sPv || new Array(24).fill(0);
const basePvW = base.wPv || new Array(24).fill(0);
// New arrays (computed above). If new PV not supplied, keep base PV
const newLoadS = fresh.newLoadSummer || baseLoadS;
const newLoadW = fresh.newLoadWinter || baseLoadW;
const newPvS = fresh.newPvSummer || basePvS;
const newPvW = fresh.newPvWinter || basePvW;
// Prices (€/kWh) per hour for summer/winter
const priceS = district.cost_summer_24h_EurPerKWh || new Array(24).fill(0);
const priceW = district.cost_winter_24h_EurPerKWh || new Array(24).fill(0);
console.groupCollapsed('[Submit] Detailed KPI calculations');
console.log('Step 1 (Per-hour arrays)');
// Step 1 per-hour arrays
const selfUsedS = map24(new Array(24), (_,h)=> Math.min(newPvS[h], newLoadS[h]));
const selfUsedW = map24(new Array(24), (_,h)=> Math.min(newPvW[h], newLoadW[h]));
const pvExportS = map24(new Array(24), (_,h)=> Math.max(newPvS[h] - newLoadS[h], 0)); // discarded later
const pvExportW = map24(new Array(24), (_,h)=> Math.max(newPvW[h] - newLoadW[h], 0)); // discarded later
const gridNewS = map24(new Array(24), (_,h)=> Math.max(newLoadS[h] - newPvS[h], 0));
const gridNewW = map24(new Array(24), (_,h)=> Math.max(newLoadW[h] - newPvW[h], 0));
const gridBaseS = map24(new Array(24), (_,h)=> Math.max(baseLoadS[h] - basePvS[h], 0));
const gridBaseW = map24(new Array(24), (_,h)=> Math.max(baseLoadW[h] - basePvW[h], 0));
console.log('SelfUsed Summer [h]:', fmt24(selfUsedS));
console.log('SelfUsed Winter [h]:', fmt24(selfUsedW));
console.log('PV_Export Summer [h] (discarded):', fmt24(pvExportS));
console.log('PV_Export Winter [h] (discarded):', fmt24(pvExportW));
console.log('GridNew Summer [h]:', fmt24(gridNewS));
console.log('GridNew Winter [h]:', fmt24(gridNewW));
console.log('GridBase Summer [h]:', fmt24(gridBaseS));
console.log('GridBase Winter [h]:', fmt24(gridBaseW));
// Step 2 – daily sums (kWh/day)
console.log('Step 2 (Daily sums)');
const selfUsedDayS = sum(selfUsedS);
const selfUsedDayW = sum(selfUsedW);
const newLoadDayS = sum(newLoadS);
const newLoadDayW = sum(newLoadW);
const gridNewDayS = sum(gridNewS);
const gridNewDayW = sum(gridNewW);
const gridBaseDayS = sum(gridBaseS);
const gridBaseDayW = sum(gridBaseW);
console.log('SelfUsed Summer (kWh/day):', +selfUsedDayS.toFixed(3));
console.log('SelfUsed Winter (kWh/day):', +selfUsedDayW.toFixed(3));
console.log('NewLoad Summer (kWh/day):', +newLoadDayS.toFixed(3));
console.log('NewLoad Winter (kWh/day):', +newLoadDayW.toFixed(3));
console.log('GridNew Summer (kWh/day):', +gridNewDayS.toFixed(3));
console.log('GridNew Winter (kWh/day):', +gridNewDayW.toFixed(3));
console.log('GridBase Summer (kWh/day):', +gridBaseDayS.toFixed(3));
console.log('GridBase Winter (kWh/day):', +gridBaseDayW.toFixed(3));
// Step 3 – Annual totals (kWh/year)
console.log('Step 3 (Annual totals)');
const AnnualSelfUsed_kWh = selfUsedDayS*SUMMER_DAYS + selfUsedDayW*WINTER_DAYS;
const AnnualLoadProfile_kWh = newLoadDayS*SUMMER_DAYS + newLoadDayW*WINTER_DAYS;
const AnnualGridNew_kWh = gridNewDayS*SUMMER_DAYS + gridNewDayW*WINTER_DAYS;
const AnnualGridBase_kWh = gridBaseDayS*SUMMER_DAYS + gridBaseDayW*WINTER_DAYS;
console.log('AnnualSelfUsed_kWh:', +AnnualSelfUsed_kWh.toFixed(3));
console.log('AnnualLoadProfile_kWh:', +AnnualLoadProfile_kWh.toFixed(3));
console.log('AnnualGridNew_kWh:', +AnnualGridNew_kWh.toFixed(3));
console.log('AnnualGridBase_kWh:', +AnnualGridBase_kWh.toFixed(3));
// Step 4 – KPIs & costs
console.log('Step 4 (KPIs & Costs)');
const pvCoveragePct = AnnualLoadProfile_kWh>0 ? 100*(AnnualSelfUsed_kWh/AnnualLoadProfile_kWh) : 0;
const gridSharePct = 100 - pvCoveragePct;
// Costs per day using hourly prices, then scaled to year
const costBaseSummer = sum(map24(new Array(24), (_,h)=> gridBaseS[h] * (Number(priceS[h]||0))));
const costBaseWinter = sum(map24(new Array(24), (_,h)=> gridBaseW[h] * (Number(priceW[h]||0))));
const costNewSummer = sum(map24(new Array(24), (_,h)=> gridNewS[h] * (Number(priceS[h]||0))));
const costNewWinter = sum(map24(new Array(24), (_,h)=> gridNewW[h] * (Number(priceW[h]||0))));
const CostBase = costBaseSummer*SUMMER_DAYS + costBaseWinter*WINTER_DAYS;
const CostNew = costNewSummer*SUMMER_DAYS + costNewWinter*WINTER_DAYS;
const CostSavingsPct = CostBase>0 ? 100*((CostBase - CostNew)/CostBase) : 0;
console.log('PV_Coverage%:', +pvCoveragePct.toFixed(3), 'Grid_Share%:', +gridSharePct.toFixed(3));
console.log('CostBase (EUR):', +CostBase.toFixed(3), 'CostNew (EUR):', +CostNew.toFixed(3), 'CostSavings%:', +CostSavingsPct.toFixed(3));
console.groupEnd();
return {
pvCoveragePct, gridSharePct,
CostBase, CostNew, CostSavingsPct,
AnnualSelfUsed_kWh, AnnualLoadProfile_kWh
};
}
if (matchingEnduse && matchingEnduse.regular_timeseries_values) {
// Split the space-separated values into an array of numbers (monthly values)
return matchingEnduse.regular_timeseries_values.split(' ').map(Number);
} else {
// Return an array of 0's if no data is found for the description
return new Array(12).fill(0);
}
/* -------------------------------------------
Card 1 coin animation (tiers & transitions)
------------------------------------------- */
// Animate horizontally by tier (1..N) and each column. stacks coins vertically equal to its column index (tier).
function ensureCoinKeyframes() {
if (document.getElementById('coin-anim-style')) return;
const style = document.createElement('style');
style.id = 'coin-anim-style';
style.textContent = `
@keyframes coinPop { from { transform: translateY(12px) scale(0.8); opacity:0 } to { transform: translateY(0) scale(1); opacity:1 } }
`;
document.head.appendChild(style);
}
let coinAnimTimers = [];
function clearCoinTimers(){
coinAnimTimers.forEach(t => clearTimeout(t));
coinAnimTimers = [];
}
function buildCoinColumnHTML(count, colIndex){
let coins = '';
for (let i=0;i<count;i++){
const delay = i * 60;
coins += `<span style="display:block; line-height:1; font-size:28px; margin-top:${i===0?0:-6}px; animation: coinPop 300ms ease ${delay}ms both">🪙</span>`;
}
return `<div class="coin-col" style="display:flex;flex-direction:column;align-items:center;margin-left:${colIndex===0?0:14}px;">${coins}</div>`;
}
function animateCoinStack(targetCount, labelTier){
ensureCoinKeyframes();
const coinArea = document.querySelector('#vizSavings .viz-placeholder');
if (!coinArea) return;
coinArea.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;gap:10px;">
<div id="coinStackRow" style="display:flex;flex-direction:row;align-items:flex-end;justify-content:center;"></div>
<div id="coinTier" style="font-size:16px;color:#cbd5e1">Savings tier: ${labelTier}</div>
</div>
`;
const row = coinArea.querySelector('#coinStackRow');
clearCoinTimers();
for (let col=1; col<=targetCount; col++){
coinAnimTimers.push(setTimeout(()=>{
row.insertAdjacentHTML('beforeend', buildCoinColumnHTML(col, col-1));
}, (col-1)*280));
}
}
// Render Card 1 KPI text, arrow, and coins based on percentage
function renderCostSavings(costSavingsPct) {
ensureCoinKeyframes();
const kpiHero = document.querySelector('#vizSavings .kpi-hero');
const trendEl = document.querySelector('#vizSavings .kpi-trend');
const pctWhole = Math.round(costSavingsPct);
if (kpiHero) kpiHero.textContent = `${pctWhole}%`;
if (trendEl) {
trendEl.style.display = 'inline-block';
trendEl.classList.remove('kpi-trend--pending','kpi-trend--up','kpi-trend--down');
trendEl.classList.add(costSavingsPct >= 0 ? 'kpi-trend--up' : 'kpi-trend--down');
}
// Tiers: 0–10% = 1, 10–20% = 2, 20–30% = 3, 30–40% = 4, >40% = 5
let coins = 1;
if (costSavingsPct > 40) coins = 5;
else if (costSavingsPct >= 30) coins = 4;
else if (costSavingsPct >= 20) coins = 3;
else if (costSavingsPct >= 10) coins = 2;
else coins = 1;
let tier = 'Low';
if (coins === 3) tier = 'Medium';
else if (coins === 5) tier = 'High';
else if (coins === 2) tier = 'Low-Medium';
else if (coins === 4) tier = 'Medium-High';
animateCoinStack(coins, tier);
}
/* ------------------------------------------
Card 2 (bar) – simple wrapper for updates
------------------------------------------ */
function renderCoverageKpi(pvCoveragePct, gridSharePct, pv_kWh, grid_kWh) {
renderPvCoverageBar(pvCoveragePct, gridSharePct, pv_kWh, grid_kWh);
}
/* -----------------------------
Submit button – main handler
----------------------------- */
function bindSubmit() {
const btn = getSubmitBtn();
if (!btn) return false;
btn.addEventListener('click', async () => {
console.groupCollapsed('--- [Submit] Start compute & render ---');
await ensureElectricalDataLoaded();
await ensurePvDataLoaded();
// Log full selected ID lists for both sliders at submit time
const s1Val = Number(getS1()?.value || 0);
const s2Val = clampSlider2Min(Number(getS2()?.value || 0));
const ids1 = Array.from(selectedBySlider1Set);
const ids2 = Array.from(selectedBySlider2Set);
console.log('[Submit] Slider values -> S1:', s1Val, 'S2:', s2Val);
console.log('[Submit] Slider1 selected GML IDs (complete):', ids1);
console.log('[Submit] Slider2 selected GML IDs (complete):', ids2);
// 1) Compute new series for Card 3 (and log arrays)
newSeries = computeNewProfiles();
if (!newSeries) {
console.warn('[Submit] Could not compute new series (missing data).');
console.groupEnd();
return;
}
//////////////////////////////////////////////////////////////////////////
// Function to load graphs using Highcharts directly into the page
//////////////////////////////////////////////////////////////////////////
function loadGraph(heatingData, coolingData) {
const chartContainer = document.getElementById('chartContainer');
chartContainer.style.display = 'block'; // Show the chart container
const chart = Highcharts.chart('chart', {
accessibility: {
enabled: false // Disable accessibility to avoid the warning
},
title: {
text: 'Energy Time Series Data',
align: 'left'
},
yAxis: {
title: {
text: 'Monthly Energy Demand (kWh)'
},
softMin: 0, // Set the min for dragging
softMax: 400 // Set a soft max for dragging
},
xAxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
labels: {
style: {
fontSize: '12px'
}
}
},
legend: {
align: 'right', // Align legend to the right
verticalAlign: 'top', // Place legend at the top
layout: 'horizontal', // Keep legend items horizontal
floating: true, // Allow manual positioning
x: 0, // Fine-tune horizontal position
y: 20 // Align vertically with the title
},
chart: {
marginBottom: 80, // Add margin at the bottom for x-axis labels
},
plotOptions: {
series: {
stickyTracking: false, // Disable sticky tracking
dragDrop: {
draggableX: false, // We only want Y draggable
draggableY: false // Set to false by default (edit mode off)
},
point: {
events: {
drag: function (e) {
console.log(`Dragging point: x: ${e.newPoint.x}, y: ${e.newPoint.y}`);
},
drop: function () {
console.log(`Dropped point: ${this.y}`);
}
}
}
}
},
series: [{
name: 'Space Heating Demand',
data: heatingData,
type: 'line' // Define the chart type
}, {
name: 'Space Cooling Demand',
data: coolingData,
type: 'line'
}],
tooltip: {
formatter: function () {
return `<b>${this.series.name}</b><br>${this.x}: ${Highcharts.numberFormat(this.y, 0)} kWh`;
}
}
});
//////////////////////////////////////////////////////////////////////////
// Toggle Button for Edit Mode
//////////////////////////////////////////////////////////////////////////
const editToggleButton = document.getElementById('editToggle');
let editMode = false; // Set initial state (edit mode is off by default)
// Set initial button style for "Edit Off"
editToggleButton.style.backgroundColor = 'red';
editToggleButton.style.color = 'white';
// Event listener to toggle edit mode
editToggleButton.addEventListener('click', function () {
editMode = !editMode; // Toggle edit mode
if (editMode) {
// Enable draggable points and change cursor to grab
toggleDraggable(true);
editToggleButton.innerText = 'Edit On'; // Update button text
editToggleButton.style.backgroundColor = 'green'; // Change button color to green
} else {
// Disable draggable points and revert cursor
toggleDraggable(false);
editToggleButton.innerText = 'Edit Off'; // Update button text
editToggleButton.style.backgroundColor = 'red'; // Change button color to red
}
});
// Function to enable or disable draggable points and cursor change
function toggleDraggable(enable) {
chart.series.forEach(function (series) {
series.update({
dragDrop: {
draggableY: enable // Enable or disable dragging on Y-axis
},
cursor: enable ? 'grab' : 'default' // Set cursor to grab when draggable
});
});
// "New load" is only activated if slider 1 > 0
newLoadActive = s1Val > 0;
// 2) Compute KPIs for Card 1 and 2 (with detailed logs)
const district = getFirstDistrictRecord();
const kpis = computeKpisFromNew(district, baseSeries, newSeries);
if (kpis) {
// Card 2 – update bar (also store energies for tooltip)
const pv_kWh = kpis.AnnualSelfUsed_kWh;
const grid_kWh = Math.max(0, kpis.AnnualLoadProfile_kWh - kpis.AnnualSelfUsed_kWh);
renderCoverageKpi(kpis.pvCoveragePct, kpis.gridSharePct, pv_kWh, grid_kWh);
// Card 1 – update percentage/arrow/coins
renderCostSavings(kpis.CostSavingsPct);
// Extra summary log for KPI numbers
console.log('[Submit][Card2] PV%:', +kpis.pvCoveragePct.toFixed(3),
'Grid%:', +kpis.gridSharePct.toFixed(3),
'AnnualSelfUsed_kWh:', +pv_kWh.toFixed(3),
'AnnualLoad_kWh:', +kpis.AnnualLoadProfile_kWh.toFixed(3));
console.log('[Submit][Card1] CostSavings%:', +kpis.CostSavingsPct.toFixed(3),
'CostBase(EUR):', +kpis.CostBase.toFixed(3),
'CostNew(EUR):', +kpis.CostNew.toFixed(3));
}
}
//////////////////////////////////////////////////////////////////////////
// Function to hide the graph and checkbox containers
//////////////////////////////////////////////////////////////////////////
function hideGraphAndCheckbox() {
document.getElementById('chartContainer').style.display = 'none'; // Hide the chart container
document.getElementById('checkboxContainer').style.display = 'none'; // Hide the checkbox container
}
//////////////////////////////////////////////////////////////////////////
// Function to hide the graph container
//////////////////////////////////////////////////////////////////////////
function hideGraph() {
document.getElementById('chartContainer').style.display = 'none'; // Hide the chart container
}
//////////////////////////////////////////////////////////////////////////
// Function to handle building selection and dynamically update graph
//////////////////////////////////////////////////////////////////////////
function active3DTilePicker() {
var selected = {
feature: undefined,
originalColor: new Cesium.Color()
}; // Declare the selected object
var selectedEntity = new Cesium.Entity();
var clickHandler = viewer.screenSpaceEventHandler.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
// Select feature on click and display metadata (turns aqua on click)
viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement) {
// Reset the color of the previously selected feature back to its original color
if (Cesium.defined(selected.feature)) {
selected.feature.color = selected.originalColor; // Restore original color
selected.feature = undefined;
viewer.selectedEntity = undefined;
}
// 3) Re-render Card 3 for the currently selected season (with "New load" visibility applied)
renderDailyLineChartForSeason(currentSeason);
// Pick a new feature on click
var picked3DtileFeature = viewer.scene.pick(movement.position);
if (!Cesium.defined(picked3DtileFeature)) {
clickHandler(movement);
return;
}
console.groupEnd();
});
console.log('[Submit] bound to #' + btn.id);
return true;
}
// Set the selected feature to aqua and store its original color
selected.feature = picked3DtileFeature;
Cesium.Color.clone(picked3DtileFeature.color, selected.originalColor); // Store original color
picked3DtileFeature.color = Cesium.Color.WHEAT ; // Change selected building to aqua
// Match gml_id with building attributes from the fetched data
var gmlId = picked3DtileFeature.getProperty('gml_id');
var buildingData = buildingAttributes[gmlId];
if (buildingData) {
// Extract space heating and cooling data for this building
const heatingData = extractMonthlyDemand(buildingData, 'monthly space heating demand for a typical year');
const coolingData = extractMonthlyDemand(buildingData, 'monthyl space cooling demand for a typical year');
// Display gml ID, gml parent ID, and building attributes in the infoBox
var featureName = "Building Attributes";
selectedEntity.name = featureName;
var description =
'<table class="cesium-infoBox-defaultTable"><tbody>' +
'<tr><th>gml ID</th><td>' + picked3DtileFeature.getProperty('gml_id') + '</td></tr>';
description += formatBuildingAttributes(buildingData);
description += '</tbody></table>';
selectedEntity.description = description;
viewer.selectedEntity = selectedEntity;
// Update the chart with the new heating and cooling data
const checkbox = document.getElementById('showTimeSeriesCheckbox');
if (checkbox) {
checkbox.checked = false; // Reset checkbox on new selection
checkbox.style.display = 'block'; // Ensure checkbox is visible
checkbox._heatingData = heatingData; // Store heating data in checkbox
checkbox._coolingData = coolingData; // Store cooling data in checkbox
}
hideGraph(); // Hide the graph initially
// Attach event listener to checkbox to toggle graph display
if (checkbox && !checkbox._hasEventListener) {
checkbox.addEventListener('change', function () {
if (checkbox.checked) {
loadGraph(checkbox._heatingData, checkbox._coolingData); // Show graph with stored data
} else {
hideGraph(); // Hide the graph when unchecked
}
});
checkbox._hasEventListener = true; // Prevent duplicate listeners
}
} else {
console.error("No data found for gml_id: ", gmlId);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
//////////////////////////////////////////////////////////////////////////
// Attach viewer.selectedEntityChanged listener on initialization
//////////////////////////////////////////////////////////////////////////
viewer.selectedEntityChanged.addEventListener(function(entity) {
if (entity) {
// Delay to ensure InfoBox is fully rendered before positioning the checkbox and chart
setTimeout(() => {
const infoBox = document.querySelector('.cesium-infoBox');
const checkboxContainer = document.getElementById('checkboxContainer');
const chartContainer = document.getElementById('chartContainer');
if (infoBox && checkboxContainer && chartContainer) {
// Get dimensions of the InfoBox
const infoBoxRect = infoBox.getBoundingClientRect();
// Position checkbox below InfoBox
checkboxContainer.style.display = 'block';
checkboxContainer.style.top = infoBoxRect.bottom + 10 + 'px';
checkboxContainer.style.left = infoBoxRect.left + 'px';
// Calculate available space below the checkbox
const spaceBelowCheckbox = window.innerHeight - checkboxContainer.getBoundingClientRect().bottom - 20;
// Adjust the height of the chart container to fit within the available space
const chartHeight = Math.min(430, spaceBelowCheckbox);
// Set width and height of the chart container
chartContainer.style.width = infoBoxRect.width + 'px';
chartContainer.style.height = chartHeight + 'px';
// Position chart container below the checkbox
chartContainer.style.top = checkboxContainer.getBoundingClientRect().bottom + 10 + 'px';
chartContainer.style.left = checkboxContainer.getBoundingClientRect().left + 'px';
}
}, 500); // Delay to ensure InfoBox is fully rendered
} else {
hideGraphAndCheckbox(); // Hide the checkbox and graph if no entity is selected
}
});
/* ----------------------------
Clear button – reset to idle
---------------------------- */
function bindClear() {
const btn = getClearBtn();
if (!btn) return false;
btn.addEventListener('click', () => {
selectedBySlider1Set.clear();
selectedBySlider2Set.clear();
setS1Value(0);
setS2Value(3);
newLoadActive = false;
// Reset season to Summer on clear
const summerEl = q$ids(EL_TOGGLE_SUMMER_IDS);
const winterEl = q$ids(EL_TOGGLE_WINTER_IDS);
if (summerEl) summerEl.checked = true;
if (winterEl) winterEl.checked = false;
currentSeason = 'summer';
// Render defaults again
renderDefaultKPIs();
console.log('[Clear] Reset sliders (A=0%, B=3%), season=Summer, restored default KPIs.');
});
console.log('[Clear] bound to #' + btn.id);
return true;
}
//////////////////////////////////////////////////////////////////////////
// Initialize active picker for 3D tile interaction
//////////////////////////////////////////////////////////////////////////
active3DTilePicker();
/* ---------------------------------------
Scenario pill toggle – show defaults
--------------------------------------- */
const scenarioToggle = document.getElementById('scenarioToggle');
if (scenarioToggle) {
scenarioToggle.addEventListener('change', () => {
renderDefaultKPIs();
});
}
/* ----------------
Boot sequence
---------------- */
// Wait for DOM, load all data, then bind controls and draw defaults.
function domReady() {
return new Promise(resolve => {
if (document.readyState === 'complete' || document.readyState === 'interactive') return resolve();
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
});
}
(async function boot() {
try {
spinnerEl.style.display = 'block';
console.log('[Boot] Starting…');
await domReady();
console.log('[Boot] DOM ready');
await Promise.all([
tileset.readyPromise.catch(e => console.warn('[Boot] tileset ready:', e && e.message)),
tilesetnord.readyPromise.catch(e => console.warn('[Boot] tilesetnord ready:', e && e.message)),
districtReadyPromise.catch(e => console.warn('[Boot] district ready:', e && e.message)),
fetchAndStoreBuildingAttributes(),
fetchDistrictAttributes(),
ensureElectricalDataLoaded(),
ensurePvDataLoaded()
]);
// Enforce hidden basemap IDs after everything is ready
applyHiddenBasemapStyle();
// Default Nord color and district styling
setDefaultColor();
applyDistrictLightGreenPreserveAlpha();
// Bind UI events
bindSlider1();
bindSlider2();
bindSubmit();
bindClear();
// Initial KPI/Chart render
renderDefaultKPIs();
console.log('[Boot] App initialized.');
} catch (e) {
console.error('[Boot] Initialization error:', e);
} finally {
BOOTING = false;
spinnerEl.style.display = 'none';
}
})();
/* ===== END OF FILE ===== */
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment