import React from "react"; import {Cesium3DTileset, CesiumComponentRef, CesiumMovementEvent, GeoJsonDataSource, Viewer} from "resium"; import Cesium, { Camera, Cesium3DTile, Cesium3DTileFeature, Cesium3DTileStyle, Color, ColorMaterialProperty, EllipseGraphics, HeadingPitchRange, JulianDate, MaterialProperty, Rectangle, ScreenSpaceEventHandler, ScreenSpaceEventType } from "cesium"; import {Link} from "ogc3dcontainerentitiesmodel"; import {AQIRange, Dictionary, getAQIFromPM, randomString} from "../common/Helpers"; import {TrafficCacheManager} from "../common/TrafficCacheManager"; import { trafficCo2ColorGradient, ColorGradient, heatingColorGradient, jamFactorColorGradient, pmSensorColorGradient, pmTrafficColorGradient, speedColorGradient, speedStrategyColorGradient, DEFAULT_BUILDINGS_COLOR_CSS, aqiSensorColorGradient } from "../common/ColorGradient"; import {FeatureCollection} from "geojson"; import {Color as AlertSeverity} from "@material-ui/lab/Alert"; import {Alert} from "@material-ui/lab"; import {Divider, Snackbar, Typography} from "@material-ui/core"; import {Popup, PopupDataInfo} from "./popups/Popup"; import {addHours, subHours} from "date-fns"; export type HeatingInfo = { spaceHeating: number; domesticWaterHeating: number; electricalAppliancesHeating: number; } export type HeatingInfoDictionary = Dictionary export type TilesetInfo = { id: string; link: Link; visible?: boolean; } export type TrafficVisibilityMode = "speed" | "co2" | "jam" | "pm"; export type DateFilter = { fromDate?: Date; toDate?: Date }; export type TrafficInfo = { visibilityMode?: TrafficVisibilityMode; colorSpeedByStrategy: boolean; colorPMByAqi: boolean; trafficGeoJson?: FeatureCollection; isTrafficGeoJsonFiltered: boolean; pmGeoJson?: FeatureCollection; isPMGeoJsonFiltered: boolean; dateFilter: DateFilter; } type MapState = { tilesets: TilesetInfo[]; trafficInfo: TrafficInfo; flownHome: boolean; loadingIds: string[]; heatingInfo?: HeatingInfoDictionary; snackbar: { message?: string; severity?: AlertSeverity; shown: boolean; }, popupInfo?: PopupDataInfo; } type MapProps = {} Camera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees(9.170776511568553, 48.780659153207395, 9.211500122636073, 48.803598100631824); Camera.DEFAULT_VIEW_FACTOR = 0.0005; Camera.DEFAULT_OFFSET = new HeadingPitchRange(0, -0.5, 0); const DEFAULT_BUILDINGS_CESIUM_COLOR = Color.fromCssColorString(DEFAULT_BUILDINGS_COLOR_CSS); export class Map extends React.Component { private cesium3dTilesetDictionary: Dictionary = {}; private cesiumViewer?: Cesium.Viewer; private trafficCacheManager = new TrafficCacheManager(); trafficJsonDataSource?: Cesium.GeoJsonDataSource; pmJsonDataSource?: Cesium.GeoJsonDataSource; constructor(props: any) { super(props); this.state = { tilesets: [], trafficInfo: { visibilityMode: undefined, colorSpeedByStrategy: false, colorPMByAqi: false, isTrafficGeoJsonFiltered: false, isPMGeoJsonFiltered: false, dateFilter: {}, }, flownHome: false, loadingIds: [], snackbar: { shown: false }, }; fetch("/assets/stockach_heating.csv") .then(data => data.text()) .then(csv => { const lines = csv.split("\n") .map(line => line.split(",").map(cell => cell.toString().trim())); const heatingInfo: HeatingInfoDictionary = {}; lines.slice(1).forEach( line => { const gmlId = line[0]; const parentGmlId = line[1]; const spaceHeating = +line[16] / 365; const domesticWaterHeating = +line[17] / 365; const electricalAppliancesHeating = +line[18] / 365; heatingInfo[gmlId] = heatingInfo[parentGmlId] = { spaceHeating, domesticWaterHeating, electricalAppliancesHeating }; } ); this.setState({ heatingInfo }); }).then(() => this.makeAllCesium3dStylesDirty()); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { if (prevState.trafficInfo.colorSpeedByStrategy !== this.state.trafficInfo.colorSpeedByStrategy) { this.colorTrafficLayer(); } if (prevState.trafficInfo.colorPMByAqi !== this.state.trafficInfo.colorPMByAqi) { this.colorPMLayer(); } } flyToTileset(tilesetId: string) { const cesium3DTileset = this.cesium3dTilesetDictionary[tilesetId]; if (this.cesiumViewer && cesium3DTileset) { this.cesiumViewer.flyTo(cesium3DTileset).then(() => {}); } } showTileset(tileset: TilesetInfo): void { tileset.visible = true; let existingTileset = this.state.tilesets.find(t => t.id === tileset.id); if (!existingTileset) { this.setState(state => ({ tilesets: [...state.tilesets, tileset] })); } else if (!existingTileset.visible) { existingTileset = {...existingTileset, visible: true}; const otherTilesets = this.state.tilesets.filter(t => t.id !== tileset.id); this.setState({ tilesets: [...otherTilesets, existingTileset] }); } } hideTileset(id: string): void { const tileset = this.state.tilesets.find(t => t.id === id); const otherTilesets = this.state.tilesets.filter(t => t.id !== id); if (tileset) { const visibleTileset = {...tileset, visible: false}; this.setState({ tilesets: [...otherTilesets, visibleTileset] }); } } showTrafficLayer(geoJson: FeatureCollection, visibilityMode: TrafficVisibilityMode, dateFilter: DateFilter, isFiltered: boolean) { this.setState(state => ( { trafficInfo: { ...state.trafficInfo, visibilityMode: visibilityMode, trafficGeoJson: geoJson, isTrafficGeoJsonFiltered: isFiltered, dateFilter: dateFilter, } } )); this.colorTrafficLayer(visibilityMode); this.makeAllCesium3dStylesDirty(); this.addCustomLoadingId("show-traffic-layer"); } showPMLayer(geoJson: FeatureCollection, visibilityMode: TrafficVisibilityMode, dateFilter: DateFilter, isFiltered: boolean) { this.setState(state => ( { trafficInfo: { ...state.trafficInfo, visibilityMode: visibilityMode, pmGeoJson: geoJson, isPMGeoJsonFiltered: isFiltered, dateFilter: dateFilter, } } )); this.colorTrafficLayer(visibilityMode); this.makeAllCesium3dStylesDirty(); this.addCustomLoadingId("show-pm-layer"); } hideTrafficLayer() { if (this.state.trafficInfo.visibilityMode) { this.setState(state => ( { trafficInfo: { ...state.trafficInfo, visibilityMode: undefined, trafficGeoJson: undefined, pmGeoJson: undefined, dateFilter: {}, } } )); this.makeAllCesium3dStylesDirty(); } } onColorByStrategyChanged(newValue: boolean) { this.setState(state => ({ trafficInfo: { ...state.trafficInfo, colorSpeedByStrategy: newValue } })); } onColorByAqiChanged(newValue: boolean) { this.setState(state => ({ trafficInfo: { ...state.trafficInfo, colorPMByAqi: newValue } })); } getVisibleItemsIds(): string[] { return this.state.tilesets.map(t => t.id); } handleCesiumViewerRef(ref: CesiumComponentRef | null) { if (ref?.cesiumElement && !this.cesiumViewer) { this.cesiumViewer = ref.cesiumElement; this.setMouseClickEvent(); // Set Timeline Clock const d = new Date(); d.setHours(10); d.setMinutes(0); this.cesiumViewer.clock.currentTime = JulianDate.fromDate(d); } } _pickedEntityData?: { entity?: any; material?: MaterialProperty; }; get pickedEntityData() {return this._pickedEntityData?.entity;} set pickedEntityData(newEntity) { const oldData = this._pickedEntityData; if (oldData?.entity === newEntity) { return; } if (oldData && oldData.entity && oldData.material) { if (oldData.entity.polyline) oldData.entity.polyline.material = oldData.material; else if (oldData.entity.ellipse) oldData.entity.ellipse.material = oldData.material; else if (oldData.entity.color) { oldData.entity.color = oldData.material; } } let material; if (newEntity) { if (newEntity.polyline) { material = newEntity.polyline.material; } else if (newEntity.ellipse) { material = newEntity.ellipse.material; } else if (newEntity.color) { material = (newEntity.color as Color).clone(); newEntity.color = Color.VIOLET; } } this.setPopupInfo(newEntity); this._pickedEntityData = { entity: newEntity, material: material }; this.colorTrafficLayer(); this.colorPMLayer(); } setPopupInfo(entity?: any) { if (entity?.polyline) { const dateFilter = this.state.trafficInfo.dateFilter; if (dateFilter.toDate) { dateFilter.fromDate = subHours(dateFilter.toDate, 24); } else if (dateFilter.fromDate) { dateFilter.toDate = addHours(dateFilter.fromDate, 24); } this.setState({ popupInfo: { trafficVisibilityMode: this.state.trafficInfo.visibilityMode, type: "traffic", trafficInfo: { id: entity.name, ...this.trafficCacheManager.trafficCache[entity.name] }, dateFilter: dateFilter, isFiltered: this.state.trafficInfo.isTrafficGeoJsonFiltered } }); } else if (entity?.ellipse) { const aqi: { aqiValue: number, aqiRange: AQIRange } = entity.properties.aqi._value; console.log(aqi); this.setState({ popupInfo: { trafficVisibilityMode: this.state.trafficInfo.visibilityMode, type: "pm", pmInfo: { id: entity.properties.id.toString(), description: entity.description.toString(), pm: +entity.properties.pm, aqiValue: aqi.aqiValue, aqiRange: aqi.aqiRange, longitude: +entity.properties.longitude, latitude: +entity.properties.latitude, }, dateFilter: this.state.trafficInfo.dateFilter, isFiltered: this.state.trafficInfo.isPMGeoJsonFiltered } }); } else if (entity?.color) { const feature = entity as Cesium3DTileFeature; this.setState({ popupInfo: { trafficVisibilityMode: this.state.trafficInfo.visibilityMode, type: "building", buildingInfo: { id: feature.getProperty("gml_id"), heatingInfo: this.getHeatingByCesium3DTileFeature(feature), properties: feature.getPropertyNames().reduce((prev, curr) => ({...prev, [curr]: feature.getProperty(curr)}), {}) }, dateFilter: this.state.trafficInfo.dateFilter, isFiltered: false, } }); } else { this.setState({ popupInfo: undefined }); } } private closePopup() { this.setState({popupInfo: undefined}); this.pickedEntityData = null; } setMouseClickEvent() { const handler = new ScreenSpaceEventHandler(this.cesiumViewer?.scene.canvas); const action = (movement: CesiumMovementEvent) => { if (!movement?.position) return; const feature = this.cesiumViewer?.scene.pick(movement.position); this.pickedEntityData = feature?.id || (feature?.getProperty ? feature : undefined); }; handler.setInputAction(action, ScreenSpaceEventType.LEFT_CLICK); this.cesiumViewer?.screenSpaceEventHandler.setInputAction(action, ScreenSpaceEventType.LEFT_CLICK); } isLoading() { return !!this.state.loadingIds.length; } handleTilesetReady(tilesetId: string, cesium3dTileset: Cesium.Cesium3DTileset) { this.cesium3dTilesetDictionary[tilesetId] = cesium3dTileset; if (!this.state.flownHome) { this.flyToTileset(tilesetId); this.setState({flownHome: true}); } this.setTilesetStyle(cesium3dTileset); const loadingId = this.addNewLoadingId(); cesium3dTileset.tileLoad.addEventListener((tile: Cesium3DTile) => { this.trafficCacheManager.setTileInfo(tile); }); const cleanLoadingIds = () => this.removeLoadingId(loadingId); cesium3dTileset.allTilesLoaded.addEventListener(() => { const mode = this.state.trafficInfo.visibilityMode; const filtered = this.state.trafficInfo.isTrafficGeoJsonFiltered; if (filtered && mode && ["speed", "jam"].includes(mode)) { this.trafficCacheManager.recalculateFactorsForDirtyFeatures() .then(() => cesium3dTileset.makeStyleDirty()) .then(cleanLoadingIds); } else { cleanLoadingIds(); } }); } evaluateShowAccordingToStreetDistance(feature: Cesium3DTileFeature): boolean { return this.trafficCacheManager.getIsCloseToStreet(feature) === true; // will not show if false or undefined (dirty) } evaluateColorAccordingToHeating(feature: Cesium3DTileFeature, result?: Color): Color { if (!this.state.heatingInfo) return Color.clone(DEFAULT_BUILDINGS_CESIUM_COLOR, result); let heating = undefined; const heatingInfo = this.getHeatingByCesium3DTileFeature(feature); if (heatingInfo) { heating = heatingInfo.spaceHeating + heatingInfo.domesticWaterHeating + heatingInfo.electricalAppliancesHeating; } return heatingColorGradient.getColorAtAsCesiumColor(heating); } getHeatingByCesium3DTileFeature(feature: Cesium3DTileFeature): HeatingInfo | undefined { if (!this.state.heatingInfo) return undefined; let features = [ "gml_id", "gml_parent_id", "gmlIdALKISLageBezeichnung_1", "gmlIdALKISLageBezeichnung_2", ].map(property => feature.getProperty(property)); const id = features.find(id => this.state.heatingInfo!.hasOwnProperty(id)); if (id) { return this.state.heatingInfo[id]; } return undefined; } setTilesetStyle(cesium3dTileset: Cesium.Cesium3DTileset) { let style = new Cesium3DTileStyle(); // @ts-ignore style.color = { evaluateColor: (feature, result) => { switch (this.state.trafficInfo.visibilityMode) { case "co2": return this.evaluateColorAccordingToHeating(feature, result); default: return DEFAULT_BUILDINGS_CESIUM_COLOR; } } }; // @ts-ignore style.show = { evaluate: (feature: Cesium3DTileFeature, _?: boolean): boolean => { if (!this.state.trafficInfo.isTrafficGeoJsonFiltered) return true; switch (this.state.trafficInfo.visibilityMode) { case "speed": case "jam": return this.evaluateShowAccordingToStreetDistance(feature); default: return true; } } }; cesium3dTileset.style = style; } async refreshTilesetsStyles() { const loadingId = this.addNewLoadingId(); return this.trafficCacheManager.makeAllFeaturesDirty() .then(() => this.trafficCacheManager.recalculateFactorsForDirtyFeatures()) .then(() => this.makeAllCesium3dStylesDirty()) .then(() => this.removeLoadingId(loadingId)); } makeAllCesium3dStylesDirty() { Object.values(this.cesium3dTilesetDictionary) .forEach(cesium3dTileset => cesium3dTileset.makeStyleDirty()); } updateTrafficCache() { this.trafficCacheManager.recalculateTrafficCache(this.trafficJsonDataSource?.entities.values); } colorTrafficLayer(trafficVisibilityMode?: TrafficVisibilityMode) { const loadingId = this.addNewLoadingId(); if (!trafficVisibilityMode) trafficVisibilityMode = this.state.trafficInfo.visibilityMode; this.trafficJsonDataSource?.entities.values.forEach(entity => { try { if (entity?.polyline) { const id = entity.name!; let color; if (this.pickedEntityData?.name === id) color = Color.VIOLET; else switch (trafficVisibilityMode) { case "speed": const speed = this.trafficCacheManager.trafficCache[id]?.speed; if (this.state.trafficInfo.colorSpeedByStrategy) { color = speedStrategyColorGradient.getColorAtAsCesiumColor(speed); } else { color = speedColorGradient.getColorAtAsCesiumColor(speed); } break; case "jam": const jamFactor = this.trafficCacheManager.trafficCache[id]?.jamFactor; color = jamFactorColorGradient.getColorAtAsCesiumColor(jamFactor); break; case "co2": const co2 = this.trafficCacheManager.trafficCache[id]?.co2Factor; color = trafficCo2ColorGradient.getColorAtAsCesiumColor(co2); break; case "pm": const pm = this.trafficCacheManager.trafficCache[id]?.pmFactor; color = pmTrafficColorGradient.getColorAtAsCesiumColor(pm); break; default: color = Color.VIOLET; } entity.polyline.material = new ColorMaterialProperty(color); } } catch (e) { console.error(e); } }); this.removeLoadingId(loadingId); } colorPMLayer(trafficVisibilityMode?: TrafficVisibilityMode) { if (!trafficVisibilityMode) { trafficVisibilityMode = this.state.trafficInfo.visibilityMode; } if (trafficVisibilityMode !== "pm") { return; } this.pmJsonDataSource?.entities.values.forEach(entity => { try { const pm = entity.properties!["pm"]; let color; if (this.pickedEntityData?.name === entity.name) color = Color.VIOLET.withAlpha(0.7); else { if (this.state.trafficInfo.colorPMByAqi) { const aqi = getAQIFromPM(pm); color = aqiSensorColorGradient.getColorAtAsCesiumColor(aqi.aqiValue).withAlpha(0.5); } else { color = pmSensorColorGradient.getColorAtAsCesiumColor(pm).withAlpha(0.5); } } entity.ellipse = new EllipseGraphics({ semiMinorAxis: 100, semiMajorAxis: 100, material: color }); entity.billboard = undefined; } catch (e) { console.error(e); } }); } private handleSnackbarClose() { this.setState(state => ({ snackbar: { ...state.snackbar, shown: false } })); } showSnackbar(message: string, severity: AlertSeverity = "info") { this.setState({ snackbar: { message, severity, shown: true } }); } private handleTrafficGeoJsonDataSourceLoaded(geoJsonDataSource: Cesium.GeoJsonDataSource) { const loadingId = this.addNewLoadingId(5); this.trafficJsonDataSource = geoJsonDataSource; this.updateTrafficCache(); this.colorTrafficLayer(); const cleanLoadingIdsAndShowSnackbar = () => { this.removeLoadingId(loadingId, 5); this.removeLoadingId("show-traffic-layer", 5); const featureCount = this.state.trafficInfo.trafficGeoJson?.features.length; if (featureCount) { this.showSnackbar(`${featureCount} traffic features loaded.`, "success"); } else { this.showSnackbar("Couldn't find any traffic features matching the query!", "info"); } }; const mode = this.state.trafficInfo.visibilityMode; const filtered = this.state.trafficInfo.isTrafficGeoJsonFiltered; if (filtered && mode && ["speed", "jam"].includes(mode)) this.refreshTilesetsStyles().then(cleanLoadingIdsAndShowSnackbar); else cleanLoadingIdsAndShowSnackbar(); } private handlePMGeoJsonDataSourceLoaded(geoJsonDataSource: Cesium.GeoJsonDataSource) { const loadingId = this.addNewLoadingId(5); this.pmJsonDataSource = geoJsonDataSource; this.colorPMLayer(); const cleanLoadingIdsAndShowSnackbar = () => { this.removeLoadingId(loadingId, 5); this.removeLoadingId("show-pm-layer", 5); const featureCount = this.state.trafficInfo.pmGeoJson?.features.length; if (featureCount) { this.showSnackbar(`${featureCount} PM features loaded.`, "success"); } else { this.showSnackbar("Couldn't find any PM features matching the query!", "info"); } }; cleanLoadingIdsAndShowSnackbar(); } addNewLoadingId(delay?: number): string { const loadingId = randomString(50); return this.addCustomLoadingId(loadingId, delay); } addCustomLoadingId(loadingId: string, delay?: number): string { const lambda = () => this.setState(state => ({ loadingIds: [...state.loadingIds, loadingId] })); if (delay) { setTimeout(lambda, delay); } else { lambda(); } return loadingId; } removeLoadingId(loadingId: string, delay?: number) { const lambda = () => this.setState(state => ({ loadingIds: state.loadingIds.filter(id => id !== loadingId) })); if (delay) { setTimeout(lambda, delay); } else { lambda(); } } private legendGradients(): ColorGradient[] { switch (this.state.trafficInfo.visibilityMode) { case "speed": return this.state.trafficInfo.colorSpeedByStrategy ? [speedStrategyColorGradient] : [speedColorGradient]; case "jam": return [jamFactorColorGradient]; case "co2": if (this.state.tilesets.some(t => t.visible)) return [trafficCo2ColorGradient, heatingColorGradient]; return [trafficCo2ColorGradient]; case "pm": if (this.state.trafficInfo.colorPMByAqi) return [pmTrafficColorGradient, aqiSensorColorGradient]; return [pmTrafficColorGradient, pmSensorColorGradient]; default: return []; } } render() { return ( {this.state.popupInfo ? : null}
Legend
{this.legendGradients().map(g => g.getLegendComponent())}
{this.state.snackbar.message} {this.state.tilesets.map(tileset => ( this.handleTilesetReady(tileset.id, cesium3dTileset)} onLoadProgress={(numberOfPendingRequests, numberOfTilesProcessing) => { const loadingId = "tileset-load-progress"; if (numberOfPendingRequests + numberOfTilesProcessing === 0) { this.removeLoadingId(loadingId); } else { this.addCustomLoadingId(loadingId); } }} />))} {this.state.trafficInfo.trafficGeoJson && this.state.trafficInfo.visibilityMode ? { this.handleTrafficGeoJsonDataSourceLoaded(geoJsonDataSource); }} /> : null} {this.state.trafficInfo.pmGeoJson && this.state.trafficInfo.visibilityMode === "pm" ? { this.handlePMGeoJsonDataSourceLoaded(geoJsonDataSource); }} /> : null}
); } }