001/*
002 * ============================================================================
003 *  Copyright © 2002-2024 by Thomas Thrien.
004 *  All Rights Reserved.
005 * ============================================================================
006 *  Licensed to the public under the agreements of the GNU Lesser General Public
007 *  License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *       http://www.gnu.org/licenses/lgpl.html
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 *  License for the specific language governing permissions and limitations
015 *  under the License.
016 */
017
018package org.tquadrat.foundation.fx.control.skin;
019
020import static java.time.temporal.ChronoField.HOUR_OF_DAY;
021import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
022import static javafx.beans.binding.Bindings.createDoubleBinding;
023import static javafx.beans.binding.Bindings.createIntegerBinding;
024import static org.apiguardian.api.API.Status.INTERNAL;
025import static org.apiguardian.api.API.Status.STABLE;
026import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
027import static org.tquadrat.foundation.lang.Objects.nonNull;
028import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
029
030import java.time.Instant;
031import java.time.LocalDate;
032import java.time.OffsetDateTime;
033import java.time.OffsetTime;
034import java.time.ZoneId;
035import java.time.ZonedDateTime;
036import java.time.format.DateTimeFormatter;
037import java.time.format.DateTimeFormatterBuilder;
038
039import org.apiguardian.api.API;
040import org.tquadrat.foundation.annotation.ClassVersion;
041import org.tquadrat.foundation.fx.control.RangeSlider;
042import org.tquadrat.foundation.fx.control.TimeSlider;
043import javafx.beans.property.ObjectProperty;
044import javafx.beans.property.ReadOnlyObjectProperty;
045import javafx.scene.control.SkinBase;
046import javafx.util.StringConverter;
047
048/**
049 *  The default skin for instances of
050 *  {@link TimeSlider}.
051 *
052 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
053 *  @version $Id: TimeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $
054 *  @since 0.4.6
055 *
056 *  @UMLGraph.link
057 */
058@ClassVersion( sourceVersion = "$Id: TimeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $" )
059@API( status = STABLE, since = "0.4.6" )
060public class TimeSliderSkin extends SkinBase<TimeSlider>
061{
062        /*---------------*\
063    ====** Inner Classes **====================================================
064        \*---------------*/
065    /**
066     *  <p>{@summary The
067     *  {@link #toString(Object)}
068     *  method of this implementation of
069     *  {@link StringConverter}
070     *  takes a number representing the number of seconds  since the beginning
071     *  of the epoch, converts it to an instance of
072     *  {@link OffsetDateTime},
073     *  takes the
074     *  {@linkplain OffsetTime time}
075     *  portion of it and converts that to a
076     *  {@link String}.}</p>
077     *  <p>The method
078     *  {@link #fromString(String)}
079     *  will do nothing; therefore this implementation of
080     *  {@code StringConverter} is incomplete.</p>
081     *
082     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
083     *  @version $Id: TimeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $
084     *  @since 0.4.6
085     *
086     *  @UMLGraph.link
087     */
088    @ClassVersion( sourceVersion = "$Id: TimeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $" )
089    @API( status = INTERNAL, since = "0.4.6" )
090    private final class OffsetTimeConverter extends StringConverter<Number>
091    {
092            /*------------------------*\
093        ====** Static Initialisations **===========================================
094            \*------------------------*/
095        /**
096         *  The
097         *  {@link java.time.format.DateTimeFormatter}
098         *  that is used to convert the
099         *  {@link OffsetTime}
100         *  instances to Strings.
101         */
102        private static final DateTimeFormatter m_TimeFormatter;
103
104        static
105        {
106            m_TimeFormatter = new DateTimeFormatterBuilder()
107                .appendValue( HOUR_OF_DAY, 2 )
108                .appendLiteral( ':' )
109                .appendValue( MINUTE_OF_HOUR, 2 )
110                .toFormatter();
111        }
112
113            /*--------------*\
114        ====** Constructors **=================================================
115            \*--------------*/
116        /**
117         *  Creates a new instance of {@code OffsetTimeConverter}.
118         */
119        public OffsetTimeConverter() { /* Just exists */ }
120
121            /*---------*\
122        ====** Methods **======================================================
123            \*---------*/
124        /**
125         *  {@inheritDoc}
126         */
127        @Override
128        public final Number fromString( final String s ) { return null; }
129
130        /**
131         *  {@inheritDoc}
132         */
133        @Override
134        public final String toString( final Number number )
135        {
136            var retValue = EMPTY_STRING;
137            if( nonNull( number ) )
138            {
139                final var timeSlider = getSkinnable();
140                final var offsetTime = Instant.ofEpochSecond( number.longValue() )
141                    .atZone( timeSlider.getTimeZone() )
142                    .toOffsetDateTime()
143                    .toOffsetTime();
144                retValue = m_TimeFormatter.format( offsetTime );
145            }
146
147            //---* Done *----------------------------------------------------------
148            return retValue;
149        }   //  toString()
150    }
151    //  class OffsetTimeConverter
152
153        /*-----------*\
154    ====** Constants **========================================================
155        \*-----------*/
156    /**
157     *  Nearly 24h in seconds: {@value}.
158     */
159    public static final double TWENTY_FOUR_HOURS = 86_399.0;
160
161        /*------------*\
162    ====** Attributes **=======================================================
163        \*------------*/
164    /**
165     *  The
166     *  {@link RangeSlider}
167     *  instance that does the work for the {@code TimeSlider}.
168     */
169    private final RangeSlider m_Content;
170
171    /*--------------*\
172    ====** Constructors **=====================================================
173        \*--------------*/
174    /**
175     *  Creates a new instance of {@code TimeSliderSkin}.
176     *
177     *  @param  control The reference for the control.
178     */
179    public TimeSliderSkin( final TimeSlider control )
180    {
181        super( requireNonNullArgument( control, "control" ) );
182
183        //---* Create the children and add them *------------------------------
184        final var min = convertZonedDateTimeToSeconds( control.minValueProperty() );
185        final var max = convertZonedDateTimeToSeconds( control.maxValueProperty() );
186        final var lowValue = convertZonedDateTimeToSeconds( composeZonedDateTime( control.getDay(), control.getLowValue(), control.getTimeZone() ) );
187        final var highValue = convertZonedDateTimeToSeconds( composeZonedDateTime( control.getDay(), control.getHighValue(), control.getTimeZone() ) );
188        m_Content = new RangeSlider( min, max, lowValue, highValue );
189        getChildren().add( m_Content );
190
191        //---* Bind the children *---------------------------------------------
192        final var maxValueBinding = createDoubleBinding( () -> convertZonedDateTimeToSeconds( getSkinnable().maxValueProperty() ), getSkinnable().maxValueProperty() );
193        m_Content.maxProperty().bind( maxValueBinding );
194
195        final var minValueBinding = createDoubleBinding( () -> convertZonedDateTimeToSeconds( getSkinnable().minValueProperty() ), getSkinnable().minValueProperty() );
196        m_Content.minProperty().bind( minValueBinding );
197
198        final var minorTickCountBinding = createIntegerBinding( () -> control.getGranularity().getMinorTickCount(), control.granularityProperty() );
199        m_Content.minorTickCountProperty().bind( minorTickCountBinding );
200
201        m_Content.snapToTicksProperty().bind( control.snapToTicksProperty() );
202
203        m_Content.lowValueProperty().addListener( ($,oldValue,newValue) ->
204        {
205           if( nonNull( newValue ) && !newValue.equals( oldValue ) )
206           {
207               getSkinnable().lowValueProperty().set( convertSecondsToOffsetTime( newValue.longValue() ) );
208           }
209        });
210        m_Content.highValueProperty().addListener( ($,oldValue,newValue) ->
211        {
212           if( nonNull( newValue ) && !newValue.equals( oldValue ) )
213           {
214               getSkinnable().highValueProperty().set( convertSecondsToOffsetTime( newValue.longValue() ) );
215           }
216        });
217
218        control.lowValueProperty().addListener( $ ->
219        {
220            final var timeSlider = getSkinnable();
221            final var day = timeSlider.getDay();
222            final var timeZone = timeSlider.getTimeZone();
223            final var time = timeSlider.getLowValue();
224            m_Content.setLowValue( (double) day.atTime( time ).atZoneSameInstant( timeZone ).toEpochSecond() );
225        });
226
227        control.highValueProperty().addListener( $ ->
228        {
229            final var timeSlider = getSkinnable();
230            final var day = timeSlider.getDay();
231            final var timeZone = timeSlider.getTimeZone();
232            final var time = timeSlider.getHighValue();
233            m_Content.setHighValue( (double) day.atTime( time ).atZoneSameInstant( timeZone ).toEpochSecond() );
234        });
235
236        //---* Configure the range slider *------------------------------------
237        final var timeConverter = new OffsetTimeConverter();
238        m_Content.setLabelFormatter( timeConverter );
239        //noinspection MagicNumber
240        m_Content.setMajorTickUnit( 3_600.0 ); // one hour
241        m_Content.setShowTickLabels( true );
242        m_Content.setShowTickMarks( true );
243    }   //  TimeSliderSkin()
244
245        /*---------*\
246    ====** Methods **==========================================================
247        \*---------*/
248    /**
249     *  Composes an instance of
250     *  {@link ZonedDateTime}
251     *  from the given components.
252     *
253     *  @param  day The day.
254     *  @param  time    The time.
255     *  @param  timeZone    The time zone.
256     *  @return The zoned date time instance.
257     */
258    private final ZonedDateTime composeZonedDateTime( final LocalDate day, final OffsetTime time, final ZoneId timeZone )
259    {
260        final var retValue = day.atTime( time ).atZoneSameInstant( timeZone );
261
262        //---* Done *----------------------------------------------------------
263        return retValue;
264    }   //  composeZonedDateTime()
265
266    /**
267     *  Converts the given
268     *  {@link ZonedDateTime}
269     *  to seconds since the start of the epoch.
270     *
271     *  @param  dateTime    The time and date.
272     *  @return The seconds since the epoch.
273     */
274    private final double convertZonedDateTimeToSeconds( final ZonedDateTime dateTime )
275    {
276        final var retValue = (double) dateTime.toEpochSecond();
277
278        //---* Done *----------------------------------------------------------
279        return retValue;
280    }   //  convertZonedDateTimeToSeconds()
281
282    /**
283     *  Converts the
284     *  {@link ZonedDateTime}
285     *  value of the given
286     *  {@link ObjectProperty}
287     *  to seconds since the start of the epoch.
288     *
289     *  @param  dateTimeProperty    The property with the time and date.
290     *  @return The seconds since the epoch.
291     */
292    private final double convertZonedDateTimeToSeconds( final ReadOnlyObjectProperty<ZonedDateTime> dateTimeProperty )
293    {
294        final var retValue = convertZonedDateTimeToSeconds( requireNonNullArgument( dateTimeProperty, "dateTimeProperty" ).get() );
295
296        //---* Done *----------------------------------------------------------
297        return retValue;
298    }   //  convertZonedDateTimeToSeconds()
299
300    /**
301     *  Convert the given {@code long} value, representing the seconds since
302     *  the start of the epoch, to an instance of
303     *  {@link OffsetTime}.
304     *
305     *  @param  seconds The seconds.
306     *  @return The offset time.
307     */
308    private final OffsetTime convertSecondsToOffsetTime( final long seconds )
309    {
310        final var retValue = Instant.ofEpochSecond( seconds )
311            .atZone( getSkinnable().getTimeZone() )
312            .toOffsetDateTime()
313            .toOffsetTime();
314
315        //---* Done *----------------------------------------------------------
316        return retValue;
317    }   //  convertSecondsToOffsetTime()
318}
319//  class TimeSliderSkin
320
321/*
322 *  End of File
323 */