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<HeatingInfo>

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<MapProps, MapState> {

    private cesium3dTilesetDictionary: Dictionary<Cesium.Cesium3DTileset> = {};
    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<MapProps>, prevState: Readonly<MapState>, 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<Cesium.Viewer> | 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 (<Viewer
            style={{width: "100%", height: "100%", cursor: this.isLoading() ? "progress" : "default"}}
            ref={this.handleCesiumViewerRef.bind(this)}
            timeline={false}
            animation={false}
        >
            {this.state.popupInfo ? <Popup info={this.state.popupInfo} handleClose={this.closePopup.bind(this)}/> : null}
            <div style={{
                position: "absolute",
                right: 50,
                bottom: 50,
                background: "#fffffff2",
                padding: 20,
                paddingBottom: 10,
                borderRadius: 10,
                display: this.legendGradients().length ? "unset" : "none"
            }}>
                <Typography>Legend</Typography>
                <Divider/>
                <div style={{marginTop: 10}}>
                    {this.legendGradients().map(g => g.getLegendComponent())}
                </div>
            </div>
            <Snackbar
                anchorOrigin={{
                    vertical: "bottom",
                    horizontal: "center",
                }}
                open={this.state.snackbar.shown}
                autoHideDuration={3000}
                onClose={this.handleSnackbarClose.bind(this)}
                message={this.state.snackbar.message}
            >
                <Alert severity={this.state.snackbar.severity} onClose={this.handleSnackbarClose.bind(this)}>
                    {this.state.snackbar.message}
                </Alert>
            </Snackbar>
            {this.state.tilesets.map(tileset => (
                <Cesium3DTileset
                    key={tileset.id}
                    url={tileset.link.href}
                    show={tileset.visible}
                    onReady={cesium3dTileset => 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 ? <GeoJsonDataSource
                key={`geo-json-${this.state.trafficInfo.visibilityMode}`}
                data={this.state.trafficInfo.trafficGeoJson}
                clampToGround={true}
                fill={Color.GRAY}
                stroke={Color.GRAY}
                strokeWidth={3}
                onLoad={(geoJsonDataSource) => {
                    this.handleTrafficGeoJsonDataSourceLoaded(geoJsonDataSource);
                }}
            /> : null}
            {this.state.trafficInfo.pmGeoJson && this.state.trafficInfo.visibilityMode === "pm" ?
                <GeoJsonDataSource
                    key={`geo-json-pm-${this.state.trafficInfo.visibilityMode}`}
                    data={this.state.trafficInfo.pmGeoJson}
                    clampToGround={true}
                    onLoad={(geoJsonDataSource) => {
                        this.handlePMGeoJsonDataSourceLoaded(geoJsonDataSource);
                    }}
                /> : null}
        </Viewer>);
    }
}
