packageDocumentation module import "./SolarTimeline.scss" ; import classnames from "classnames" ; import * as React from "react" ; import { Slider , Tooltip } from "@itwin/itwinui-react" ; import { ColorByName , ColorDef , HSVColor } from "@itwin/core-common" ; import { RelativePosition , TimeDisplay } from "@itwin/appui-abstract" ; import { BodyText , CommonProps , Popup } from "@itwin/core-react" ; import { adjustDateToTimezone , DatePicker , TimeField , TimeSpec , UiComponents } from "@itwin/components-react" ; import { HueSlider } from "../color/HueSlider" ; import { SaturationPicker } from "../color/SaturationPicker" ; import { ColorSwatch } from "../color/Swatch" ; import { SolarDataProvider } from "./interfaces" ; import { PlayButton } from "./PlayerButton" ; import { SpeedTimeline } from "./SpeedTimeline" ; import { CustomThumb , getPercentageOfRectangle , RailMarkers , useFocusedThumb } from "./Scrubber" ; import { UiIModelComponents } from "../UiIModelComponents" ; const millisecPerMinute = 1000 * 60 ; const millisecPerHour = millisecPerMinute * 60 ; const defaultPlaybackDuration = 40 * 1000 ; const addZero = ( i ) => { return ( i < 10 ) ? `0${ i }` : i ; } ; interface TimelineProps extends CommonProps { dayStartMs : number ; sunRiseOffsetMs : number ; sunSetOffsetMs : number ; currentTimeOffsetMs : number ; isPlaying : boolean ; formatTick ? : ( millisec : number ) => string ; formatTime : ( millisec : number ) => string ; onChange ? : ( values : ReadonlyArray < number > ) => void ; onUpdate ? : ( values : ReadonlyArray < number > ) => void ; } function Timeline ( props ) { const sliderRef = React . useRef < HTMLDivElement > ( null ) ; const [ sliderContainer , setSliderContainer ] = React . useState < HTMLDivElement > ( ) ; const [ pointerPercent , setPointerPercent ] = React . useState ( 0 ) ; React . useLayoutEffect ( ( ) => { if ( sliderRef . current ) { const container = sliderRef . current . querySelector ( ".iui-slider-container" ) ; if ( container && sliderContainer !== container ) { setSliderContainer ( container as HTMLDivElement ) ; } } } , [ sliderContainer ] ) ; const thumbProps = ( ) => { return { className : "components-timeline-thumb" , children : < CustomThumb /> } ; } ; const tooltipProps = React . useCallback ( ( ) => { return { visible : false } ; } , [ ] ) ; const [ showRailTooltip , setShowRailTooltip ] = React . useState ( false ) ; const handlePointerEnter = React . useCallback ( ( ) => { setShowRailTooltip ( true ) ; } , [ ] ) ; const handlePointerLeave = React . useCallback ( ( ) => { setShowRailTooltip ( false ) ; } , [ ] ) ; const handlePointerMove = React . useCallback ( ( event ) => { sliderContainer && setPointerPercent ( getPercentageOfRectangle ( sliderContainer . getBoundingClientRect ( ) , event . clientX ) ) ; } , [ sliderContainer ] ) ; const { formatTick , formatTime , onChange , onUpdate , dayStartMs , sunSetOffsetMs , sunRiseOffsetMs , currentTimeOffsetMs , isPlaying } = props ; const thumbHasFocus = useFocusedThumb ( sliderContainer ) ; const tickLabel = React . useMemo ( ( ) => { const showTip = isPlaying || showRailTooltip || thumbHasFocus ; const totalDuration = sunSetOffsetMs - sunRiseOffsetMs ; const percent = ( isPlaying || thumbHasFocus ) ? ( currentTimeOffsetMs - sunRiseOffsetMs ) / totalDuration : pointerPercent ; const tooltipText = formatTime ( ( isPlaying || thumbHasFocus ) ? ( dayStartMs + currentTimeOffsetMs ) : ( dayStartMs + ( sunRiseOffsetMs + ( pointerPercent * totalDuration ) ) ) ) ; return ( < RailMarkers showToolTip = { showTip } percent = { percent } tooltipText = { tooltipText } / > ) ; } , [ isPlaying , showRailTooltip , thumbHasFocus , sunSetOffsetMs , sunRiseOffsetMs , currentTimeOffsetMs , pointerPercent , formatTime , dayStartMs ] ) ; const className = classnames ( "solar-slider" , props . className , formatTick && "showticks" ) ; const sunRiseFormat = formatTime ( dayStartMs + sunRiseOffsetMs ) ; const sunSetFormat = formatTime ( dayStartMs + sunSetOffsetMs ) ; return ( < div className = { className } > < Tooltip content = { sunRiseFormat } > < span className = "sunrise" > & #x2600 ; < /span> < /Tooltip> < Slider ref = { sliderRef } className = { className } step = { millisecPerMinute } min = { sunRiseOffsetMs } max = { sunSetOffsetMs } minLabel = "" maxLabel = "" onUpdate = { onUpdate } onChange = { onChange } values = { [ currentTimeOffsetMs ] } tooltipProps = { tooltipProps } thumbProps = { thumbProps } tickLabels = { tickLabel } railContainerProps = { { onPointerEnter : handlePointerEnter , onPointerMove : handlePointerMove , onPointerLeave : handlePointerLeave } } /> < Tooltip content = { sunSetFormat } > < span className = "sunset" > & #x263D ; < /span> < /Tooltip> < /div>); } interface SolarTimelineComponentProps { dataProvider : SolarDataProvider ; onPlayPause ? : ( playing : boolean ) => void ; duration ? : number ; speed ? : number ; } interface SolarTimelineComponentState { isPlaying : boolean ; isDateOpened : boolean ; isSettingsOpened : boolean ; dayStartMs : number ; sunRiseOffsetMs : number ; sunSetOffsetMs : number ; sunDeltaMs : number ; currentTimeOffsetMs : number ; speed : number ; loop : boolean ; shadowColor : ColorDef ; duration : number ; adjustedDuration : number ; } alpha export class SolarTimeline extends React . PureComponent < SolarTimelineComponentProps , SolarTimelineComponentState > { private _datePicker : HTMLElement | null = null ; private _settings : HTMLElement | null = null ; private _requestFrame = 0 ; private _unmounted = false ; private _timeLastCycle = 0 ; private _totalPlayTime = 0 ; private _settingsPopupTitle = UiIModelComponents . translate ( "solarsettings.shadowcolor" ) ; private _playLabel = UiIModelComponents . translate ( "solartimeline.play" ) ; private _settingLabel = UiIModelComponents . translate ( "solartimeline.settings" ) ; private _loopLabel = UiIModelComponents . translate ( "timeline.repeat" ) ; private _speedLabel = UiIModelComponents . translate ( "solartimeline.speed" ) ; private _dateTimeLabel = UiIModelComponents . translate ( "solartimeline.dateTime" ) ; private _months = [ UiComponents . translate ( "month.short.january" ) , UiComponents . translate ( "month.short.february" ) , UiComponents . translate ( "month.short.march" ) , UiComponents . translate ( "month.short.april" ) , UiComponents . translate ( "month.short.may" ) , UiComponents . translate ( "month.short.june" ) , UiComponents . translate ( "month.short.july" ) , UiComponents . translate ( "month.short.august" ) , UiComponents . translate ( "month.short.september" ) , UiComponents . translate ( "month.short.october" ) , UiComponents . translate ( "month.short.november" ) , UiComponents . translate ( "month.short.december" ) , ] ; private _timeLabel = UiComponents . translate ( "datepicker.time" ) ; private _amLabel = UiComponents . translate ( "time.am" ) ; private _pmLabel = UiComponents . translate ( "time.pm" ) ; private readonly _presetColors = [ ColorDef . create ( ColorByName . grey ) , ColorDef . create ( ColorByName . lightGrey ) , ColorDef . create ( ColorByName . darkGrey ) , ColorDef . create ( ColorByName . lightBlue ) , ColorDef . create ( ColorByName . lightGreen ) , ColorDef . create ( ColorByName . darkGreen ) , ColorDef . create ( ColorByName . tan ) , ColorDef . create ( ColorByName . darkBrown ) , ] ; constructor ( props ) { super ( props ) ; const dayStartMs = this . props . dataProvider . dayStartMs ; const sunRiseOffsetMs = this . props . dataProvider . sunrise . getTime ( ) - dayStartMs ; const sunSetOffsetMs = this . props . dataProvider . sunset . getTime ( ) - dayStartMs ; const sunDeltaMs = sunSetOffsetMs - sunRiseOffsetMs ; const sunOffsetMs = this . props . dataProvider . timeOfDay . getTime ( ) - dayStartMs ; const currentTimeOffsetMs = this . ensureRange ( sunOffsetMs , sunRiseOffsetMs , sunSetOffsetMs ) ; const shadowColor = this . props . dataProvider . shadowColor ; const duration = this . props . duration ? this . props . duration : defaultPlaybackDuration ; const speed = this . props . speed ? this . props . speed : 2 ; const adjustedDuration = duration / speed ; this . setPlaybackTimeBySunTime ( currentTimeOffsetMs , sunRiseOffsetMs , sunDeltaMs , adjustedDuration ) ; this . state = { isDateOpened : false , isSettingsOpened : false , isPlaying : false , dayStartMs , sunRiseOffsetMs , sunSetOffsetMs , sunDeltaMs , currentTimeOffsetMs , speed , loop : false , shadowColor , duration , adjustedDuration } ; } public override componentWillUnmount ( ) { window . cancelAnimationFrame ( this . _requestFrame ) ; this . _unmounted = true ; } private _updateAnimation = ( _timestamp ) => { if ( ! this . state . isPlaying || this . _unmounted ) { window . cancelAnimationFrame ( this . _requestFrame ) ; return ; } const currentTime = new Date ( ) . getTime ( ) ; this . _totalPlayTime += ( currentTime - this . _timeLastCycle ) ; this . _timeLastCycle = currentTime ; let percentComplete = this . _totalPlayTime / this . state . adjustedDuration ; if ( percentComplete > 1 ) percentComplete = 1 ; let newPlayingState = true ; let nextSunOffset = Math . floor ( this . state . sunRiseOffsetMs + ( percentComplete * this . state . sunDeltaMs ) ) ; if ( percentComplete > 0.99 ) { if ( ! this . state . loop ) { newPlayingState = false ; nextSunOffset = this . state . sunSetOffsetMs ; window . cancelAnimationFrame ( this . _requestFrame ) ; } else { nextSunOffset = this . state . sunRiseOffsetMs ; this . _totalPlayTime = 0 ; } } if ( this . props . dataProvider . onTimeChanged ) { const currentSunTime = new Date ( this . state . dayStartMs + nextSunOffset ) ; this . props . dataProvider . onTimeChanged ( currentSunTime ) ; } this . setState ( { isPlaying : newPlayingState , currentTimeOffsetMs : nextSunOffset } , ( ) => { if ( newPlayingState ) this . _requestFrame = window . requestAnimationFrame ( this . _updateAnimation ) ; } ) ; } ; private _play ( sunTimeMs ) { this . _timeLastCycle = new Date ( ) . getTime ( ) ; this . setState ( { isPlaying : true , currentTimeOffsetMs : sunTimeMs } , ( ) => { this . _requestFrame = window . requestAnimationFrame ( this . _updateAnimation ) ; if ( this . props . onPlayPause ) this . props . onPlayPause ( true ) ; } ) ; } private _onPause = ( ) => { if ( ! this . state . isPlaying ) return ; const currentTime = new Date ( ) . getTime ( ) ; this . _totalPlayTime += ( currentTime - this . _timeLastCycle ) ; window . cancelAnimationFrame ( this . _requestFrame ) ; this . setState ( { isPlaying : false } ) ; if ( this . props . onPlayPause ) this . props . onPlayPause ( false ) ; } ; private _onPlay = ( ) => { if ( this . state . isPlaying ) return ; if ( this . state . currentTimeOffsetMs >= this . state . sunSetOffsetMs || this . state . currentTimeOffsetMs <= this . state . sunRiseOffsetMs ) { this . _totalPlayTime = 0 ; this . _play ( this . state . sunRiseOffsetMs ) ; } else { this . _play ( this . state . currentTimeOffsetMs ) ; } } ; private setPlaybackTimeBySunTime ( sunOffsetMs , sunRiseOffsetMs , sunDeltaMs , adjustedDuration ? ) { this . _totalPlayTime = ( ( sunOffsetMs - sunRiseOffsetMs ) / ( sunDeltaMs ) ) * ( ( adjustedDuration ) ? adjustedDuration : this . state . adjustedDuration ) ; } /** note the day passed in is in the time of the current user not in project time because the date picker works in
     * local time  */ private _onDayClick = ( day ) => { const selectedDate = new Date ( day . getTime ( ) + this . state . currentTimeOffsetMs ) ; this . props . dataProvider . setDateAndTime ( selectedDate ) ; const dayStartMs = this . props . dataProvider . dayStartMs ; const sunRiseOffsetMs = this . props . dataProvider . sunrise . getTime ( ) - dayStartMs ; const sunSetOffsetMs = this . props . dataProvider . sunset . getTime ( ) - dayStartMs ; const sunDeltaMs = sunSetOffsetMs - sunRiseOffsetMs ; const sunOffsetMs = this . ensureRange ( this . state . currentTimeOffsetMs , sunRiseOffsetMs , sunSetOffsetMs ) ; this . setPlaybackTimeBySunTime ( sunOffsetMs , sunRiseOffsetMs , sunDeltaMs ) ; /** call dataProvider to update display style */ if ( this . props . dataProvider . onTimeChanged ) this . props . dataProvider . onTimeChanged ( this . props . dataProvider . timeOfDay ) ; this . setState ( { dayStartMs , sunRiseOffsetMs , sunSetOffsetMs , currentTimeOffsetMs : sunOffsetMs , sunDeltaMs , isDateOpened : false } , ( ) => { this . _timeLastCycle = new Date ( ) . getTime ( ) ; } ) ; } ; private _onTimeChanged = ( time ) => { const dayStartMs = this . props . dataProvider . dayStartMs ; const sunTime = ( time . hours * millisecPerHour ) + ( time . minutes * millisecPerMinute ) ; const dateWithNewTime = new Date ( dayStartMs + sunTime ) ; this . props . dataProvider . setDateAndTime ( dateWithNewTime , true ) ; if ( this . props . dataProvider . onTimeChanged ) this . props . dataProvider . onTimeChanged ( dateWithNewTime ) ; const currentTimeOffsetMs = this . ensureRange ( sunTime , this . state . sunRiseOffsetMs , this . state . sunSetOffsetMs ) ; this . setPlaybackTimeBySunTime ( currentTimeOffsetMs , this . state . sunRiseOffsetMs , this . state . sunDeltaMs ) ; this . _timeLastCycle = new Date ( ) . getTime ( ) ; this . setState ( { currentTimeOffsetMs } ) ; } ; private _onCloseDayPicker = ( ) => { this . setState ( { isDateOpened : false } ) ; } ; private _onOpenDayPicker = ( ) => { this . setState ( ( prevState ) => ( { isDateOpened : ! prevState . isDateOpened } ) ) ; } ; private _onCloseSettingsPopup = ( ) => { this . setState ( { isSettingsOpened : false } ) ; } ; private _onOpenSettingsPopup = ( ) => { this . setState ( ( prevState ) => ( { isSettingsOpened : ! prevState . isSettingsOpened } ) ) ; } ; private ensureRange ( value , min , max ) { return Math . max ( Math . min ( value , max ) , min ) ; } private processSunTimeChange ( sunTime ) { if ( sunTime === this . state . currentTimeOffsetMs ) return ; const currentTimeOffsetMs = this . ensureRange ( sunTime , this . state . sunRiseOffsetMs , this . state . sunSetOffsetMs ) ; if ( this . props . dataProvider . onTimeChanged ) { const currentSunTime = new Date ( this . state . dayStartMs + currentTimeOffsetMs ) ; this . props . dataProvider . onTimeChanged ( currentSunTime ) ; } this . setState ( { currentTimeOffsetMs } , ( ) => { const currentTime = new Date ( ) . getTime ( ) ; this . _timeLastCycle = currentTime ; const percentComplete = ( currentTimeOffsetMs - this . state . sunRiseOffsetMs ) / this . state . sunDeltaMs ; this . _totalPlayTime = percentComplete * this . state . adjustedDuration ; } ) ; } private _onUpdate = ( values ) => { if ( ! this . state . isPlaying ) this . processSunTimeChange ( values [ 0 ] ) ; } ; private _onChange = ( values ) => { if ( ! this . state . isPlaying ) this . processSunTimeChange ( values [ 0 ] ) ; } ; private _onSpeedChange = ( value ) => { const adjustedDuration = this . state . duration / value ; this . setState ( { speed : value , adjustedDuration } ) ; } ; private _onToggleLoop = ( ) => { this . setState ( ( prevState ) => ( { loop : ! prevState . loop } ) ) ; } ; private _formatTime = ( millisec ) => { const date = new Date ( millisec ) ; const localTime = adjustDateToTimezone ( date , this . props . dataProvider . timeZoneOffset * 60 ) ; let hours = localTime . getHours ( ) ; const minutes = addZero ( date . getMinutes ( ) ) ; const abbrev = ( hours < 12 ) ? this . _amLabel : ( hours === 24 ) ? this . _amLabel : this . _pmLabel ; hours = ( hours > 12 ) ? hours - 12 : hours ; return `${ hours }:${ minutes } ${ abbrev }` ; } ; private _onPresetColorPick = ( shadowColor ) => { this . setState ( { shadowColor } , ( ) => this . props . dataProvider . shadowColor = shadowColor ) ; } ; private _handleHueOrSaturationChange = ( hueOrSaturation ) => { if ( hueOrSaturation . s === 0 ) hueOrSaturation = hueOrSaturation . clone ( undefined , 0.5 ) ; const shadowColor = hueOrSaturation . toColorDef ( ) ; this . setState ( { shadowColor } , ( ) => this . props . dataProvider . shadowColor = shadowColor ) ; } ; public getLocalTime ( ticks ) { const projectTime = new Date ( ticks ) ; return adjustDateToTimezone ( projectTime , this . props . dataProvider . timeZoneOffset * 60 ) ; } public override render ( ) { const { dataProvider } = this . props ; const { speed , loop , currentTimeOffsetMs , sunRiseOffsetMs , sunSetOffsetMs } = this . state ; const localTime = this . getLocalTime ( this . state . dayStartMs + this . state . currentTimeOffsetMs ) ; const formattedTime = this . _formatTime ( dataProvider . dayStartMs + currentTimeOffsetMs ) ; const formattedDate = `${ this . _months [ localTime . getMonth ( ) ] }, ${ localTime . getDate ( ) }` ; const colorSwatchStyle = { width : `100%` , height : `100%` } ; return ( < div className = { "solar-timeline-wrapper" } > < div className = "header" > < PlayButton tooltip = { this . _playLabel } className = "play-button" isPlaying = { this . state . isPlaying } onPlay = { this . _onPlay } onPause = { this . _onPause } / > < button data - testid = "solar-date-time-button" title = { this . _dateTimeLabel } className = "current-date" ref = { ( element ) => this . _datePicker = element } onClick = { this . _onOpenDayPicker } > < span > { formattedDate } < /span> < span > /</span > < span > { formattedTime } < /span> < span className = "icon icon-calendar" / > < /button> < Popup style = { { border : "none" } } offset = { 11 } target = { this . _datePicker } isOpen = { this . state . isDateOpened } onClose = { this . _onCloseDayPicker } position = { RelativePosition . Top } > < div className = "components-date-picker-calendar-popup-panel" data - testid = "components-date-picker-calendar-popup-panel" > < DatePicker selected = { localTime } onDateChange = { this . _onDayClick } showFocusOutline = { false } / > < div className = "time-container" > < BodyText className = "time-label" > { this . _timeLabel } < /BodyText> < TimeField time = { { hours : localTime . getHours ( ) , minutes : localTime . getMinutes ( ) , seconds : 0 } } timeDisplay = { TimeDisplay . H12MC } onTimeChange = { this . _onTimeChanged } / > < /div> < /div> < /Popup> < Timeline className = "solar-timeline" dayStartMs = { dataProvider . dayStartMs } sunSetOffsetMs = { sunSetOffsetMs } sunRiseOffsetMs = { sunRiseOffsetMs } currentTimeOffsetMs = { currentTimeOffsetMs } onChange = { this . _onChange } onUpdate = { this . _onUpdate } formatTime = { this . _formatTime } isPlaying = { this . state . isPlaying } / > < div className = "speed-container" > < span title = { this . _speedLabel } > { speed } x < /span> < SpeedTimeline className = "speed" onChange = { this . _onSpeedChange } speed = { this . state . speed } / > < /div> { } < span title = { this . _loopLabel } className = { classnames ( "icon" , "icon-media-controls-loop" , ! loop && "no-loop-playback" , loop && "loop-playback" ) } onClick = { this . _onToggleLoop } role = "button" tabIndex = { - 1 } > < /span> < button data - testid = "shadow-settings-button" title = { this . _settingLabel } className = "shadow-settings-button" ref = { ( element ) => this . _settings = element } onClick = { this . _onOpenSettingsPopup } > < span className = "icon icon-settings" / > < /button> < Popup className = "shadow-settings-popup" target = { this . _settings } offset = { 11 } isOpen = { this . state . isSettingsOpened } onClose = { this . _onCloseSettingsPopup } position = { RelativePosition . Top } > < div className = "shadow-settings-popup-container" > < div className = "shadow-settings-header" > { this . _settingsPopupTitle } < /div> < div className = "shadow-settings-color" > < div className = "shadow-settings-color-top" > < SaturationPicker hsv = { this . state . shadowColor . toHSV ( ) } onSaturationChange = { this . _handleHueOrSaturationChange } / > < /div> < div className = "shadow-settings-color-bottom" > < div className = "shadow-settings-color-bottom-left" > < HueSlider hsv = { this . state . shadowColor . toHSV ( ) } onHueChange = { this . _handleHueOrSaturationChange } isHorizontal = { true } / > < /div> < div className = "shadow-settings-color-bottom-right" > < ColorSwatch style = { colorSwatchStyle } colorDef = { this . state . shadowColor } round = { false } / > < /div> < /div> < /div> < div className = "shadow-settings-color-presets" > < ColorSwatch colorDef = { this . _presetColors [ 0 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 1 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 2 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 3 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 4 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 5 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 6 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < ColorSwatch colorDef = { this . _presetColors [ 7 ] } round = { false } onColorPick = { this . _onPresetColorPick } / > < /div> < /div> < /Popup> < /div> < /div>); } }