/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2016-2020 Gerrit Grunwald.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package eu.hansolo.medusa.tools;

import eu.hansolo.medusa.Alarm;
import eu.hansolo.medusa.Clock;
import eu.hansolo.medusa.Fonts;
import eu.hansolo.medusa.Gauge;
import eu.hansolo.medusa.Gauge.ScaleDirection;
import eu.hansolo.medusa.Gauge.SkinType;
import eu.hansolo.medusa.Section;
import eu.hansolo.medusa.TickLabelLocation;
import eu.hansolo.medusa.TickLabelOrientation;
import eu.hansolo.medusa.TickMarkType;
import eu.hansolo.medusa.TimeSection;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.ImagePattern;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;

import java.math.BigDecimal;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;


/**
 * Created by hansolo on 11.12.15.
 */
public class Helper {
    public static final double MAX_TICK_MARK_LENGTH = 0.125;
    public static final double MAX_TICK_MARK_WIDTH  = 0.02;
    public static final Color  INACTIVE_ALARM_COLOR = Color.rgb(90, 90, 90, 0.5);
    public static final double MIN_FONT_SIZE        = 5;


    public static final <T extends Number> T clamp(final T MIN, final T MAX, final T VALUE) {
        if (VALUE.doubleValue() < MIN.doubleValue()) return MIN;
        if (VALUE.doubleValue() > MAX.doubleValue()) return MAX;
        return VALUE;
    }

    public static final int clamp(final int MIN, final int MAX, final int VALUE) {
        if (VALUE < MIN) return MIN;
        if (VALUE > MAX) return MAX;
        return VALUE;
    }
    public static final long clamp(final long MIN, final long MAX, final long VALUE) {
        if (VALUE < MIN) return MIN;
        if (VALUE > MAX) return MAX;
        return VALUE;
    }
    public static final double clamp(final double MIN, final double MAX, final double VALUE) {
        if (Double.compare(VALUE, MIN) < 0) return MIN;
        if (Double.compare(VALUE, MAX) > 0) return MAX;
        return VALUE;
    }

    public static final double clampMin(final double MIN, final double VALUE) {
        if (VALUE < MIN) return MIN;
        return VALUE;
    }
    public static final double clampMax(final double MAX, final double VALUE) {
        if (VALUE > MAX) return MAX;
        return VALUE;
    }

    public static final double round(final double VALUE, final int PRECISION) {
        final int SCALE = (int) Math.pow(10, PRECISION);
        return (double) Math.round(VALUE * SCALE) / SCALE;
    }

    public static final double roundTo(final double VALUE, final double TARGET) { return TARGET * (Math.round(VALUE / TARGET)); }

    public static final double roundToHalf(final double VALUE) { return Math.round(VALUE * 2) / 2.0; }


    public static final double nearest(final double SMALLER, final double VALUE, final double LARGER) {
        return (VALUE - SMALLER) < (LARGER - VALUE) ? SMALLER : LARGER;
    }

    public static final int roundDoubleToInt(final double VALUE){
        double dAbs = Math.abs(VALUE);
        int    i      = (int) dAbs;
        double result = dAbs - (double) i;
        if (result < 0.5) {
            return VALUE < 0 ? -i : i;
        } else {
            return VALUE < 0 ? -(i + 1) : i + 1;
        }
    }

    public static final double[] calcAutoScale(final double MIN_VALUE, final double MAX_VALUE) {
        double maxNoOfMajorTicks = 10;
        double maxNoOfMinorTicks = 10;
        double niceMinValue;
        double niceMaxValue;
        double niceRange;
        double majorTickSpace;
        double minorTickSpace;
        niceRange      = (calcNiceNumber((MAX_VALUE - MIN_VALUE), false));
        majorTickSpace = calcNiceNumber(niceRange / (maxNoOfMajorTicks - 1), true);
        niceMinValue   = (Math.floor(MIN_VALUE / majorTickSpace) * majorTickSpace);
        niceMaxValue   = (Math.ceil(MAX_VALUE / majorTickSpace) * majorTickSpace);
        minorTickSpace = calcNiceNumber(majorTickSpace / (maxNoOfMinorTicks - 1), true);
        return new double[]{ niceMinValue, niceMaxValue, majorTickSpace, minorTickSpace };
    }

    /**
     * Calculates nice minValue, maxValue and stepSize for given MIN and MAX values
     * @param MIN
     * @param MAX
     * @return array of doubles with [niceMin, niceMax, niceRange, niceStep]
     */
    public static final double[] getNiceScale(final double MIN, final double MAX) {
        return getNiceScale(MIN, MAX, 20);
    }
    /**
     * Calculates nice minValue, maxValue and stepSize for given MIN and MAX values
     * @param MIN
     * @param MAX
     * @param MAX_NO_OF_TICKS
     * @return array of doubles with [niceMin, niceMax, niceRange, niceStep]
     */
    public static final double[] getNiceScale(final double MIN, final double MAX, final int MAX_NO_OF_TICKS) {
        // Minimal increment to avoid round extreme values to be on the edge of the chart
        double minimum = MIN;
        double maximum = MAX;
        double epsilon = (MAX - MIN) / 1e6;
        maximum += epsilon;
        minimum -= epsilon;
        double range = maximum - minimum;

        // Target number of values to be displayed on the Y axis (it may be less)
        int stepCount = MAX_NO_OF_TICKS;
        // First approximation
        double roughStep = range / (stepCount - 1);

        // Set best niceStep for the range
        //double[] goodNormalizedSteps = { 1, 1.5, 2, 2.5, 5, 7.5, 10 }; // keep the 10 at the end
        double[] goodNormalizedSteps = { 1, 2, 5, 10 };

        // Normalize rough niceStep to find the normalized one that fits best
        double stepPower          = Math.pow(10, -Math.floor(Math.log10(Math.abs(roughStep))));
        double normalizedStep     = roughStep * stepPower;
        double goodNormalizedStep = Arrays.stream(goodNormalizedSteps).filter(n -> Double.compare(n, normalizedStep) >= 0).findFirst().getAsDouble();
        double niceStep           = goodNormalizedStep / stepPower;

        // Determine the scale limits based on the chosen niceStep.
        double niceMin = minimum < 0 ? Math.floor(minimum / niceStep) * niceStep : Math.ceil(minimum / niceStep) * niceStep;
        double niceMax = maximum < 0 ? Math.floor(maximum / niceStep) * niceStep : Math.ceil(maximum / niceStep) * niceStep;

        if (MIN % niceStep == 0) { niceMin = MIN; }
        if (MAX % niceStep == 0) { niceMax = MAX; }

        double niceRange = niceMax - niceMin;

        return new double[] { niceMin, niceMax, niceRange, niceStep };
    }

    /**
     * Can be used to implement discrete steps e.g. on a slider.
     * @param MIN_VALUE          The min value of the range
     * @param MAX_VALUE          The max value of the range
     * @param VALUE              The value to snap
     * @param MINOR_TICK_COUNT   The number of ticks between 2 major tick marks
     * @param MAJOR_TICK_UNIT    The distance between 2 major tick marks
     * @return The value snapped to the next tick mark defined by the given parameters
     */
    public static final double snapToTicks(final double MIN_VALUE, final double MAX_VALUE, final double VALUE, final int MINOR_TICK_COUNT, final double MAJOR_TICK_UNIT) {
        double v = VALUE;
        int    minorTickCount = clamp(0, 10, MINOR_TICK_COUNT);
        double majorTickUnit  = Double.compare(MAJOR_TICK_UNIT, 0.0) <= 0 ? 0.25 : MAJOR_TICK_UNIT;
        double tickSpacing;

        if (minorTickCount != 0) {
            tickSpacing = majorTickUnit / (Math.max(minorTickCount, 0) + 1);
        } else {
            tickSpacing = majorTickUnit;
        }

        int    prevTick      = (int) ((v - MIN_VALUE) / tickSpacing);
        double prevTickValue = prevTick * tickSpacing + MIN_VALUE;
        double nextTickValue = (prevTick + 1) * tickSpacing + MIN_VALUE;

        v = nearest(prevTickValue, v, nextTickValue);

        return clamp(MIN_VALUE, MAX_VALUE, v);
    }

    /**
     * Returns a "niceScaling" number approximately equal to the range.
     * Rounds the number if ROUND == true.
     * Takes the ceiling if ROUND = false.
     *
     * @param RANGE the value range (maxValue - minValue)
     * @param ROUND whether to round the result or ceil
     * @return a "niceScaling" number to be used for the value range
     */
    public static final double calcNiceNumber(final double RANGE, final boolean ROUND) {
        double niceFraction;
        double exponent = Math.floor(Math.log10(RANGE));   // exponent of range
        double fraction = RANGE / Math.pow(10, exponent);  // fractional part of range

        if (ROUND) {
            if (fraction < 1.5) {
                niceFraction = 1;
            } else if (fraction < 3) {
                niceFraction = 2;
            } else if (fraction < 7) {
                niceFraction = 5;
            } else {
                niceFraction = 10;
            }
        } else {
            if (fraction <= 1) {
                niceFraction = 1;
            } else if (fraction <= 2) {
                niceFraction = 2;
            } else if (fraction <= 5) {
                niceFraction = 5;
            } else {
                niceFraction = 10;
            }
        }
        return niceFraction * Math.pow(10, exponent);
    }

    public static final Color getColorOfSection(final List<Section> SECTIONS, final double VALUE, final Color DEFAULT_COLOR) {
        for (Section section : SECTIONS) {
            if (section.contains(VALUE)) return section.getColor();
        }
        return DEFAULT_COLOR;
    }

    public static final void rotateContextForText(final GraphicsContext CTX, final double START_ANGLE, final double ANGLE, final TickLabelOrientation ORIENTATION) {
        switch (ORIENTATION) {
            case ORTHOGONAL:
                if ((360 - START_ANGLE - ANGLE) % 360 > 90 && (360 - START_ANGLE - ANGLE) % 360 < 270) {
                    CTX.rotate((180 - START_ANGLE - ANGLE) % 360);
                } else {
                    CTX.rotate((360 - START_ANGLE - ANGLE) % 360);
                }
                break;
            case TANGENT:
                if ((360 - START_ANGLE - ANGLE - 90) % 360 > 90 && (360 - START_ANGLE - ANGLE - 90) % 360 < 270) {
                    CTX.rotate((90 - START_ANGLE - ANGLE) % 360);
                } else {
                    CTX.rotate((270 - START_ANGLE - ANGLE) % 360);
                }
                break;
            case HORIZONTAL:
            default:
                break;
        }
    }

    public static final void adjustTextSize(final Text TEXT, final double MAX_WIDTH, final double FONT_SIZE) {
        final String FONT_NAME          = TEXT.getFont().getName();
        double       adjustableFontSize = FONT_SIZE;
        while (TEXT.getLayoutBounds().getWidth() > MAX_WIDTH && adjustableFontSize > MIN_FONT_SIZE) {
            adjustableFontSize -= 0.1;
            TEXT.setFont(new Font(FONT_NAME, adjustableFontSize));
        }
    }
    public static final void adjustTextSize(final Label TEXT, final double MAX_WIDTH, final double FONT_SIZE) {
        final String FONT_NAME          = TEXT.getFont().getName();
        double       adjustableFontSize = FONT_SIZE;

        while (TEXT.getLayoutBounds().getWidth() > MAX_WIDTH && adjustableFontSize > MIN_FONT_SIZE) {
            adjustableFontSize -= 0.1;
            TEXT.setFont(new Font(FONT_NAME, adjustableFontSize));
        }
    }

    public static final DateTimeFormatter getDateFormat(final Locale LOCALE) {
        if (Locale.US == LOCALE) {
            return DateTimeFormatter.ofPattern("MM/dd/YYYY");
        } else if (Locale.CHINA == LOCALE) {
            return DateTimeFormatter.ofPattern("YYYY.MM.dd");
        } else {
            return DateTimeFormatter.ofPattern("dd.MM.YYYY");
        }
    }
    public static final DateTimeFormatter getLocalizedDateFormat(final Locale LOCALE) {
        return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(LOCALE);
    }

    public static final void enableNode(final Node NODE, final boolean ENABLE) {
        NODE.setManaged(ENABLE);
        NODE.setVisible(ENABLE);
    }

    public static final String colorToCss(final Color COLOR) {
        return COLOR.toString().replace("0x", "#");
    }

    public static final ThreadFactory getThreadFactory(final String THREAD_NAME, final boolean IS_DAEMON) {
        return runnable -> {
            Thread thread = new Thread(runnable, THREAD_NAME);
            thread.setDaemon(IS_DAEMON);
            return thread;
        };
    }

    public static final void stopTask(ScheduledFuture<?> task) {
        if (null == task) return;
        task.cancel(true);
        task = null;
    }

    public static final ImagePattern createCarbonPattern() {
        final double          SIZE   = 12;
        final Canvas          CANVAS = new Canvas(SIZE, SIZE);
        final GraphicsContext CTX    = CANVAS.getGraphicsContext2D();

        CTX.setFill(new LinearGradient(0, 0, 0, 0.5 * SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(35, 35, 35)),
                                       new Stop(1, Color.rgb(23, 23, 23))));
        CTX.fillRect(0, 0, SIZE * 0.5, SIZE * 0.5);

        CTX.setFill(new LinearGradient(0, 0, 0, 0.416666 * SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(38, 38, 38)),
                                       new Stop(1, Color.rgb(30, 30, 30))));
        CTX.fillRect(SIZE * 0.083333, 0, SIZE * 0.333333, SIZE * 0.416666);

        CTX.setFill(new LinearGradient(0, 0.5 * SIZE, 0, SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(35, 35, 35)),
                                       new Stop(1, Color.rgb(23, 23, 23))));
        CTX.fillRect(SIZE * 0.5, SIZE * 0.5, SIZE * 0.5, SIZE * 0.5);

        CTX.setFill(new LinearGradient(0, 0.5 * SIZE, 0, 0.916666 * SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(38, 38, 38)),
                                       new Stop(1, Color.rgb(30, 30, 30))));
        CTX.fillRect(SIZE * 0.583333, SIZE * 0.5, SIZE * 0.333333, SIZE * 0.416666);

        CTX.setFill(new LinearGradient(0, 0, 0, 0.5 * SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(48, 48, 48)),
                                       new Stop(1, Color.rgb(40, 40, 40))));
        CTX.fillRect(SIZE * 0.5, 0, SIZE * 0.5, SIZE * 0.5);

        CTX.setFill(new LinearGradient(0, 0.083333 * SIZE, 0, 0.5 * SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(53, 53, 53)),
                                       new Stop(1, Color.rgb(45, 45, 45))));
        CTX.fillRect(SIZE * 0.583333, SIZE * 0.083333, SIZE * 0.333333, SIZE * 0.416666);

        CTX.setFill(new LinearGradient(0, 0.5 * SIZE, 0, SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(48, 48, 48)),
                                       new Stop(1, Color.rgb(40, 40, 40))));
        CTX.fillRect(0, SIZE * 0.5, SIZE * 0.5, SIZE * 0.5);

        CTX.setFill(new LinearGradient(0, 0.583333 * SIZE, 0, SIZE,
                                       false, CycleMethod.NO_CYCLE,
                                       new Stop(0, Color.rgb(53, 53, 53)),
                                       new Stop(1, Color.rgb(45, 45, 45))));
        CTX.fillRect(SIZE * 0.083333, SIZE * 0.583333, SIZE * 0.333333, SIZE * 0.416666);

        final Image        PATTERN_IMAGE = CANVAS.snapshot(new SnapshotParameters(), null);
        final ImagePattern PATTERN       = new ImagePattern(PATTERN_IMAGE, 0, 0, SIZE, SIZE, false);

        return PATTERN;
    }

    public static final void drawTrapezoid(final GraphicsContext CTX,
                                           final double PI1X, final double PI1Y, final double PI2X, final double PI2Y,
                                           final double PO1X, final double PO1Y, final double PO2X, final double PO2Y) {
        CTX.beginPath();
        CTX.moveTo(PI2X, PI2Y);
        CTX.lineTo(PI1X, PI1Y);
        CTX.lineTo(PO1X, PO1Y);
        CTX.lineTo(PO2X, PO2Y);
        CTX.closePath();
        CTX.fill();
    }
    public static final void drawTriangle(final GraphicsContext CTX,
                                          final double PIX, final double PIY, final double PO1X, final double PO1Y, final double PO2X, final double PO2Y) {
        CTX.beginPath();
        CTX.moveTo(PIX, PIY);
        CTX.lineTo(PO1X, PO1Y);
        CTX.lineTo(PO2X, PO2Y);
        CTX.closePath();
        CTX.fill();
    }
    public static final void drawDot(final GraphicsContext CTX, final double CENTER_X, final double CENTER_Y, final double SIZE) {
        CTX.fillOval(CENTER_X, CENTER_Y, SIZE, SIZE);
    }
    public static final void drawLine(final GraphicsContext CTX, final double P1X, final double P1Y, final double P2X, final double P2Y) {
        CTX.strokeLine(P1X, P1Y, P2X, P2Y);
    }

    public static final boolean isMonochrome(final Color COLOR) {
        return Double.compare(COLOR.getRed(), COLOR.getGreen()) == 0 && Double.compare(COLOR.getGreen(), COLOR.getBlue()) == 0;
    }

    public static final double colorDistance(final Color COLOR_1, final Color COLOR_2) {
        final double DELTA_R = (COLOR_2.getRed() - COLOR_1.getRed());
        final double DELTA_G = (COLOR_2.getGreen() - COLOR_1.getGreen());
        final double DELTA_B = (COLOR_2.getBlue() - COLOR_1.getBlue());

        return Math.sqrt(DELTA_R * DELTA_R + DELTA_G * DELTA_G + DELTA_B * DELTA_B);
    }

    public static final boolean isBright(final Color COLOR) { return !isDark(COLOR); }
    public static final boolean isDark(final Color COLOR) {
        final double DISTANCE_TO_WHITE = colorDistance(COLOR, Color.WHITE);
        final double DISTANCE_TO_BLACK = colorDistance(COLOR, Color.BLACK);
        return DISTANCE_TO_BLACK < DISTANCE_TO_WHITE;
    }

    public static final Color getTranslucentColorFrom(final Color COLOR, final double FACTOR) {
        return Color.color(COLOR.getRed(), COLOR.getGreen(), COLOR.getBlue(), Helper.clamp(0.0, 1.0, FACTOR));
    }

    public static final void drawRadialTickMarks(final Gauge GAUGE, final GraphicsContext CTX,
                                                 final double MIN_VALUE, final double MAX_VALUE,
                                                 final double START_ANGLE, final double ANGLE_RANGE, final double ANGLE_STEP,
                                                 final double CENTER_X, final double CENTER_Y, final double SIZE) {
        double               sinValue;
        double               cosValue;
        double               centerX               = CENTER_X;
        double               centerY               = CENTER_Y;
        int                  tickLabelDecimals     = GAUGE.getTickLabelDecimals();
        String               tickLabelFormatString = "%." + tickLabelDecimals + "f";
        double               minorTickSpace        = GAUGE.getMinorTickSpace();
        double               tmpAngleStep          = ANGLE_STEP * minorTickSpace;
        TickLabelOrientation tickLabelOrientation  = GAUGE.getTickLabelOrientation();
        TickLabelLocation    tickLabelLocation     = GAUGE.getTickLabelLocation();
        BigDecimal           minorTickSpaceBD      = BigDecimal.valueOf(minorTickSpace);
        BigDecimal           majorTickSpaceBD      = BigDecimal.valueOf(GAUGE.getMajorTickSpace());
        BigDecimal           mediumCheck2          = BigDecimal.valueOf(2 * minorTickSpace);
        BigDecimal           mediumCheck5          = BigDecimal.valueOf(5 * minorTickSpace);
        BigDecimal           counterBD             = BigDecimal.valueOf(MIN_VALUE);
        double               counter               = MIN_VALUE;

        List<Section> tickMarkSections             = GAUGE.getTickMarkSections();
        List<Section> tickLabelSections            = GAUGE.getTickLabelSections();
        Color         tickMarkColor                = GAUGE.getTickMarkColor();
        Color         majorTickMarkColor           = GAUGE.getMajorTickMarkColor().equals(tickMarkColor) ? tickMarkColor : GAUGE.getMajorTickMarkColor();
        Color         mediumTickMarkColor          = GAUGE.getMediumTickMarkColor().equals(tickMarkColor) ? tickMarkColor : GAUGE.getMediumTickMarkColor();
        Color         minorTickMarkColor           = GAUGE.getMinorTickMarkColor().equals(tickMarkColor) ? tickMarkColor : GAUGE.getMinorTickMarkColor();
        double        majorTickMarkLengthFactor    = GAUGE.getMajorTickMarkLengthFactor();
        double        majorTickMarkWidthFactor     = GAUGE.getMajorTickMarkWidthFactor();
        double        mediumTickMarkLengthFactor   = GAUGE.getMediumTickMarkLengthFactor();
        double        mediumTickMarkWidthFactor    = GAUGE.getMediumTickMarkWidthFactor();
        double        minorTickMarkLengthFactor    = GAUGE.getMinorTickMarkLengthFactor();
        double        minorTickMarkWidthFactor     = GAUGE.getMinorTickMarkWidthFactor();
        Color         tickLabelColor               = GAUGE.getTickLabelColor();
        Color         zeroColor                    = GAUGE.getZeroColor();
        boolean       isNotZero                    = true;
        TickMarkType  majorTickMarkType            = GAUGE.getMajorTickMarkType();
        TickMarkType  mediumTickMarkType           = GAUGE.getMediumTickMarkType();
        TickMarkType  minorTickMarkType            = GAUGE.getMinorTickMarkType();
        boolean       tickMarkSectionsVisible      = GAUGE.getTickMarkSectionsVisible();
        boolean       tickLabelSectionsVisible     = GAUGE.getTickLabelSectionsVisible();
        boolean       majorTickMarksVisible        = GAUGE.getMajorTickMarksVisible();
        boolean       mediumTickMarksVisible       = GAUGE.getMediumTickMarksVisible();
        boolean       minorTickMarksVisible        = GAUGE.getMinorTickMarksVisible();
        boolean       tickLabelsVisible            = GAUGE.getTickLabelsVisible();
        boolean       onlyFirstAndLastLabelVisible = GAUGE.isOnlyFirstAndLastTickLabelVisible();
        boolean       customTickLabelsEnabled      = GAUGE.getCustomTickLabelsEnabled();
        Locale        locale                       = GAUGE.getLocale();
        List<String>  customTickLabels             = customTickLabelsEnabled ? GAUGE.getCustomTickLabels() : null;
        double        textDisplacementFactor       = majorTickMarkType == TickMarkType.DOT ? (TickLabelLocation.OUTSIDE == tickLabelLocation ? 0.95 : 1.05) : 1.0;
        double        majorDotSize;
        double        majorHalfDotSize;
        double        mediumDotSize;
        double        mediumHalfDotSize;
        double        minorDotSize;
        double        minorHalfDotSize;

        double orthTextFactor;
        if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
            orthTextFactor    = 0.45 * textDisplacementFactor;//TickLabelOrientation.ORTHOGONAL == tickLabelOrientation ? 0.45 * textDisplacementFactor : 0.45 * textDisplacementFactor;
            majorDotSize      = 0.02 * SIZE;
            majorHalfDotSize  = majorDotSize * 0.5;
            mediumDotSize     = 0.01375 * SIZE;
            mediumHalfDotSize = mediumDotSize * 0.5;
            minorDotSize      = 0.0075 * SIZE;
            minorHalfDotSize  = minorDotSize * 0.5;
        } else {
            orthTextFactor    = TickLabelOrientation.ORTHOGONAL == tickLabelOrientation ? 0.38 * textDisplacementFactor : 0.37 * textDisplacementFactor;
            majorDotSize      = 0.025 * SIZE;
            majorHalfDotSize  = majorDotSize * 0.5;
            mediumDotSize     = 0.01875 * SIZE;
            mediumHalfDotSize = mediumDotSize * 0.5;
            minorDotSize      = 0.0125 * SIZE;
            minorHalfDotSize  = minorDotSize * 0.5;
        };

        double  customFontSizeFactor       = GAUGE.getCustomTickLabelFontSize() / 400;
        boolean fullRange                  = (MIN_VALUE < 0 && MAX_VALUE > 0);
        double  tickLabelFontSize          = tickLabelDecimals == 0 ? 0.054 * SIZE : 0.051 * SIZE;
        tickLabelFontSize                  = GAUGE.getCustomTickLabelsEnabled() ? customFontSizeFactor * SIZE : tickLabelFontSize;
        double  tickMarkFontSize           = tickLabelDecimals == 0 ? 0.047 * SIZE: 0.044 * SIZE;
        double  tickLabelOrientationFactor = TickLabelOrientation.HORIZONTAL == tickLabelOrientation ? 0.9 : 1.0;

        Font tickLabelFont     = Fonts.robotoCondensedRegular(tickLabelFontSize * tickLabelOrientationFactor);
        Font tickMarkFont      = Fonts.robotoCondensedRegular(tickMarkFontSize * tickLabelOrientationFactor);
        Font tickLabelZeroFont = fullRange ? Fonts.robotoCondensedBold(tickLabelFontSize * tickLabelOrientationFactor) : tickLabelFont;
        Font tickMarkZeroFont  = fullRange ? Fonts.robotoCondensedBold(tickMarkFontSize * tickLabelOrientationFactor) : tickMarkFont;

        // Variables needed for tickmarks
        double innerPointX;
        double innerPointY;
        double innerMediumPointX;
        double innerMediumPointY;
        double innerMinorPointX;
        double innerMinorPointY;
        double outerPointX;
        double outerPointY;
        double outerMediumPointX;
        double outerMediumPointY;
        double outerMinorPointX;
        double outerMinorPointY;
        double textPointX;
        double textPointY;
        double dotCenterX;
        double dotCenterY;
        double dotMediumCenterX;
        double dotMediumCenterY;
        double dotMinorCenterX;
        double dotMinorCenterY;
        double tickLabelTickMarkX;
        double tickLabelTickMarkY;

        double trapezoidMajorInnerAngle1;
        double trapezoidMajorInnerAngle2;
        double trapezoidMajorOuterAngle1;
        double trapezoidMajorOuterAngle2;
        double trapezoidMajorInnerPoint1X;
        double trapezoidMajorInnerPoint1Y;
        double trapezoidMajorInnerPoint2X;
        double trapezoidMajorInnerPoint2Y;
        double trapezoidMajorOuterPoint1X;
        double trapezoidMajorOuterPoint1Y;
        double trapezoidMajorOuterPoint2X;
        double trapezoidMajorOuterPoint2Y;

        double trapezoidMediumInnerAngle1;
        double trapezoidMediumInnerAngle2;
        double trapezoidMediumOuterAngle1;
        double trapezoidMediumOuterAngle2;
        double trapezoidMediumInnerPoint1X;
        double trapezoidMediumInnerPoint1Y;
        double trapezoidMediumInnerPoint2X;
        double trapezoidMediumInnerPoint2Y;
        double trapezoidMediumOuterPoint1X;
        double trapezoidMediumOuterPoint1Y;
        double trapezoidMediumOuterPoint2X;
        double trapezoidMediumOuterPoint2Y;

        double trapezoidMinorInnerAngle1;
        double trapezoidMinorInnerAngle2;
        double trapezoidMinorOuterAngle1;
        double trapezoidMinorOuterAngle2;
        double trapezoidMinorInnerPoint1X;
        double trapezoidMinorInnerPoint1Y;
        double trapezoidMinorInnerPoint2X;
        double trapezoidMinorInnerPoint2Y;
        double trapezoidMinorOuterPoint1X;
        double trapezoidMinorOuterPoint1Y;
        double trapezoidMinorOuterPoint2X;
        double trapezoidMinorOuterPoint2Y;

        double triangleMajorInnerPointX;
        double triangleMajorInnerPointY;
        double triangleMajorOuterPointX;
        double triangleMajorOuterPointY;

        double triangleMediumInnerPointX;
        double triangleMediumInnerPointY;
        double triangleMediumOuterPointX;
        double triangleMediumOuterPointY;

        double triangleMinorInnerPointX;
        double triangleMinorInnerPointY;
        double triangleMinorOuterPointX;
        double triangleMinorOuterPointY;

        ScaleDirection scaleDirection = GAUGE.getScaleDirection();

        // Draw tickmark ring
        if (GAUGE.isTickMarkRingVisible()) {
            SkinType skinType     = GAUGE.getSkinType();
            double   xy           = TickLabelLocation.INSIDE == tickLabelLocation ? SIZE * 0.026 : SIZE * 0.14;
            double   wh           = TickLabelLocation.INSIDE == tickLabelLocation ? SIZE * 0.948 : SIZE * 0.72;
            double   offset       = -90 + START_ANGLE;
            double   horVerOffset = SIZE * 0.055555555;
            CTX.setLineWidth(SIZE * 0.004);
            CTX.setLineCap(StrokeLineCap.SQUARE);
            CTX.save();
            CTX.setStroke(tickMarkColor);
            switch(skinType) {
                case HORIZONTAL: CTX.strokeArc(xy + horVerOffset, xy, wh, wh, offset, -ANGLE_RANGE, ArcType.OPEN); break;
                case VERTICAL  : CTX.strokeArc(xy, xy + horVerOffset, wh, wh, offset, -ANGLE_RANGE, ArcType.OPEN); break;
                default        : CTX.strokeArc(xy, xy, wh, wh, offset, -ANGLE_RANGE, ArcType.OPEN); break;
            }

            CTX.restore();
            if (tickMarkSections.size() > 0) {
                int    listSize = tickMarkSections.size();
                double sectionStartAngle;
                for (int i = 0; i < listSize; i++) {
                    Section section = tickMarkSections.get(i);
                    if (Double.compare(section.getStart(), MIN_VALUE) < 0 && Double.compare(section.getStop(), MAX_VALUE) < 0) {
                        sectionStartAngle = 0;
                    } else {
                        sectionStartAngle = ScaleDirection.CLOCKWISE == scaleDirection ? (section.getStart() - MIN_VALUE) * ANGLE_STEP : -(section.getStart() - MIN_VALUE) * ANGLE_STEP;
                    }
                    double sectionAngleExtend;
                    if (Double.compare(section.getStop(), MAX_VALUE) > 0) {
                        sectionAngleExtend = ScaleDirection.CLOCKWISE == scaleDirection ? (MAX_VALUE - section.getStart()) * ANGLE_STEP : -(MAX_VALUE - section.getStart()) * ANGLE_STEP;
                    } else {
                        sectionAngleExtend = ScaleDirection.CLOCKWISE == scaleDirection ? (section.getStop() - section.getStart()) * ANGLE_STEP : -(section.getStop() - section.getStart()) * ANGLE_STEP;
                    }
                    CTX.save();
                    CTX.setStroke(section.getColor());
                    switch(skinType) {
                        case HORIZONTAL: CTX.strokeArc(xy + horVerOffset, xy, wh, wh, offset - sectionStartAngle, -sectionAngleExtend, ArcType.OPEN); break;
                        case VERTICAL  : CTX.strokeArc(xy, xy + horVerOffset, wh, wh, offset - sectionStartAngle, -sectionAngleExtend, ArcType.OPEN); break;
                        default        : CTX.strokeArc(xy, xy, wh, wh, offset - sectionStartAngle, -sectionAngleExtend, ArcType.OPEN); break;
                    }
                    CTX.restore();
                }
            }
        }

        // Main loop
        BigDecimal     tmpStepBD      = new BigDecimal(tmpAngleStep);
        tmpStepBD                     = tmpStepBD.setScale(3, BigDecimal.ROUND_HALF_UP);
        double tmpStep                = tmpStepBD.doubleValue();
        double angle                  = 0;
        int    customTickLabelCounter = 0;
        for (double i = 0 ; Double.compare(-ANGLE_RANGE - tmpStep, i) <= 0 ; i -= tmpStep) {
            sinValue = Math.sin(Math.toRadians(angle + START_ANGLE));
            cosValue = Math.cos(Math.toRadians(angle + START_ANGLE));

            switch(tickLabelLocation) {
                case OUTSIDE:
                    innerPointX                 = centerX + SIZE * 0.3585 * sinValue;
                    innerPointY                 = centerY + SIZE * 0.3585 * cosValue;
                    innerMediumPointX           = innerPointX;
                    innerMediumPointY           = innerPointY;
                    innerMinorPointX            = innerPointX;
                    innerMinorPointY            = innerPointY;
                    outerPointX                 = centerX + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * majorTickMarkLengthFactor) * sinValue;
                    outerPointY                 = centerY + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * majorTickMarkLengthFactor) * cosValue;
                    outerMediumPointX           = centerX + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * mediumTickMarkLengthFactor) * sinValue;
                    outerMediumPointY           = centerY + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * mediumTickMarkLengthFactor) * cosValue;
                    outerMinorPointX            = centerX + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * minorTickMarkLengthFactor) * sinValue;
                    outerMinorPointY            = centerY + SIZE * (0.3585 + MAX_TICK_MARK_LENGTH * minorTickMarkLengthFactor) * cosValue;
                    textPointX                  = centerX + SIZE * orthTextFactor * sinValue;
                    textPointY                  = centerY + SIZE * orthTextFactor * cosValue;
                    dotCenterX                  = centerX + SIZE * 0.3685 * sinValue;
                    dotCenterY                  = centerY + SIZE * 0.3685 * cosValue;
                    dotMediumCenterX            = centerX + SIZE * 0.365375 * sinValue;
                    dotMediumCenterY            = centerY + SIZE * 0.365375 * cosValue;
                    dotMinorCenterX             = centerX + SIZE * 0.36225 * sinValue;
                    dotMinorCenterY             = centerY + SIZE * 0.36225 * cosValue;
                    tickLabelTickMarkX          = centerX + SIZE * 0.3805 * sinValue;
                    tickLabelTickMarkY          = centerY + SIZE * 0.3805 * cosValue;

                    trapezoidMajorInnerAngle1   = Math.toRadians(angle - 1.2 + START_ANGLE);
                    trapezoidMajorInnerAngle2   = Math.toRadians(angle + 1.2 + START_ANGLE);
                    trapezoidMajorOuterAngle1   = Math.toRadians(angle - 0.8 + START_ANGLE);
                    trapezoidMajorOuterAngle2   = Math.toRadians(angle + 0.8 + START_ANGLE);
                    trapezoidMajorInnerPoint1X  = centerX + SIZE * 0.3585 * Math.sin(trapezoidMajorInnerAngle1);
                    trapezoidMajorInnerPoint1Y  = centerY + SIZE * 0.3585 * Math.cos(trapezoidMajorInnerAngle1);
                    trapezoidMajorInnerPoint2X  = centerX + SIZE * 0.3585 * Math.sin(trapezoidMajorInnerAngle2);
                    trapezoidMajorInnerPoint2Y  = centerY + SIZE * 0.3585 * Math.cos(trapezoidMajorInnerAngle2);
                    trapezoidMajorOuterPoint1X  = centerX + SIZE * 0.4105 * Math.sin(trapezoidMajorOuterAngle1);
                    trapezoidMajorOuterPoint1Y  = centerY + SIZE * 0.4105 * Math.cos(trapezoidMajorOuterAngle1);
                    trapezoidMajorOuterPoint2X  = centerX + SIZE * 0.4105 * Math.sin(trapezoidMajorOuterAngle2);
                    trapezoidMajorOuterPoint2Y  = centerY + SIZE * 0.4105 * Math.cos(trapezoidMajorOuterAngle2);

                    trapezoidMediumInnerAngle1  = Math.toRadians(angle - 1.0 + START_ANGLE);
                    trapezoidMediumInnerAngle2  = Math.toRadians(angle + 1.0 + START_ANGLE);
                    trapezoidMediumOuterAngle1  = Math.toRadians(angle - 0.7 + START_ANGLE);
                    trapezoidMediumOuterAngle2  = Math.toRadians(angle + 0.7 + START_ANGLE);
                    trapezoidMediumInnerPoint1X = centerX + SIZE * 0.3585 * Math.sin(trapezoidMediumInnerAngle1);
                    trapezoidMediumInnerPoint1Y = centerY + SIZE * 0.3585 * Math.cos(trapezoidMediumInnerAngle1);
                    trapezoidMediumInnerPoint2X = centerX + SIZE * 0.3585 * Math.sin(trapezoidMediumInnerAngle2);
                    trapezoidMediumInnerPoint2Y = centerY + SIZE * 0.3585 * Math.cos(trapezoidMediumInnerAngle2);
                    trapezoidMediumOuterPoint1X = centerX + SIZE * 0.3985 * Math.sin(trapezoidMajorOuterAngle1);
                    trapezoidMediumOuterPoint1Y = centerY + SIZE * 0.3985 * Math.cos(trapezoidMediumOuterAngle1);
                    trapezoidMediumOuterPoint2X = centerX + SIZE * 0.3985 * Math.sin(trapezoidMediumOuterAngle2);
                    trapezoidMediumOuterPoint2Y = centerY + SIZE * 0.3985 * Math.cos(trapezoidMediumOuterAngle2);

                    trapezoidMinorInnerAngle1   = Math.toRadians(angle - 0.8 + START_ANGLE);
                    trapezoidMinorInnerAngle2   = Math.toRadians(angle + 0.8 + START_ANGLE);
                    trapezoidMinorOuterAngle1   = Math.toRadians(angle - 0.6 + START_ANGLE);
                    trapezoidMinorOuterAngle2   = Math.toRadians(angle + 0.6 + START_ANGLE);
                    trapezoidMinorInnerPoint1X  = centerX + SIZE * 0.3585 * Math.sin(trapezoidMinorInnerAngle1);
                    trapezoidMinorInnerPoint1Y  = centerY + SIZE * 0.3585 * Math.cos(trapezoidMinorInnerAngle1);
                    trapezoidMinorInnerPoint2X  = centerX + SIZE * 0.3585 * Math.sin(trapezoidMinorInnerAngle2);
                    trapezoidMinorInnerPoint2Y  = centerY + SIZE * 0.3585 * Math.cos(trapezoidMinorInnerAngle2);
                    trapezoidMinorOuterPoint1X  = centerX + SIZE * 0.3975 * Math.sin(trapezoidMinorOuterAngle1);
                    trapezoidMinorOuterPoint1Y  = centerY + SIZE * 0.3975 * Math.cos(trapezoidMinorOuterAngle1);
                    trapezoidMinorOuterPoint2X  = centerX + SIZE * 0.3975 * Math.sin(trapezoidMinorOuterAngle2);
                    trapezoidMinorOuterPoint2Y  = centerY + SIZE * 0.3975 * Math.cos(trapezoidMinorOuterAngle2);

                    triangleMajorInnerPointX    = centerX + SIZE * 0.3585 * sinValue;
                    triangleMajorInnerPointY    = centerY + SIZE * 0.3585 * cosValue;
                    triangleMajorOuterPointX    = centerX + SIZE * 0.4105 * sinValue;
                    triangleMajorOuterPointY    = centerY + SIZE * 0.4105 * cosValue;

                    triangleMediumInnerPointX   = triangleMajorInnerPointX;
                    triangleMediumInnerPointY   = triangleMajorInnerPointY;
                    triangleMediumOuterPointX   = centerX + SIZE * 0.4045 * sinValue;
                    triangleMediumOuterPointY   = centerY + SIZE * 0.4045 * cosValue;

                    triangleMinorInnerPointX    = triangleMajorInnerPointX;
                    triangleMinorInnerPointY    = triangleMajorInnerPointY;
                    triangleMinorOuterPointX    = centerX + SIZE * 0.3975 * sinValue;
                    triangleMinorOuterPointY    = centerY + SIZE * 0.3975 * cosValue;
                    break;
                case INSIDE:
                default:
                    innerPointX                 = centerX + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * majorTickMarkLengthFactor) * sinValue;
                    innerPointY                 = centerY + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * majorTickMarkLengthFactor) * cosValue;
                    innerMediumPointX           = centerX + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * mediumTickMarkLengthFactor) * sinValue;
                    innerMediumPointY           = centerY + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * mediumTickMarkLengthFactor) * cosValue;
                    innerMinorPointX            = centerX + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * minorTickMarkLengthFactor) * sinValue;
                    innerMinorPointY            = centerY + SIZE * (0.475 - MAX_TICK_MARK_LENGTH * minorTickMarkLengthFactor) * cosValue;
                    outerPointX                 = centerX + SIZE * 0.475 * sinValue;
                    outerPointY                 = centerY + SIZE * 0.475 * cosValue;
                    outerMediumPointX           = outerPointX;
                    outerMediumPointY           = outerPointY;
                    outerMinorPointX            = outerPointX;
                    outerMinorPointY            = outerPointY;
                    textPointX                  = centerX + SIZE * orthTextFactor * sinValue;
                    textPointY                  = centerY + SIZE * orthTextFactor * cosValue;
                    dotCenterX                  = centerX + SIZE * 0.4625 * sinValue;
                    dotCenterY                  = centerY + SIZE * 0.4625 * cosValue;
                    dotMediumCenterX            = centerX + SIZE * 0.465625 * sinValue;
                    dotMediumCenterY            = centerY + SIZE * 0.465625 * cosValue;
                    dotMinorCenterX             = centerX + SIZE * 0.46875 * sinValue;
                    dotMinorCenterY             = centerY + SIZE * 0.46875 * cosValue;
                    tickLabelTickMarkX          = centerX + SIZE * 0.445 * sinValue;
                    tickLabelTickMarkY          = centerY + SIZE * 0.445 * cosValue;

                    trapezoidMajorInnerAngle1   = Math.toRadians(angle - 0.8 + START_ANGLE);
                    trapezoidMajorInnerAngle2   = Math.toRadians(angle + 0.8 + START_ANGLE);
                    trapezoidMajorOuterAngle1   = Math.toRadians(angle - 1.2 + START_ANGLE);
                    trapezoidMajorOuterAngle2   = Math.toRadians(angle + 1.2 + START_ANGLE);
                    trapezoidMajorInnerPoint1X  = centerX + SIZE * 0.423 * Math.sin(trapezoidMajorInnerAngle1);
                    trapezoidMajorInnerPoint1Y  = centerY + SIZE * 0.423 * Math.cos(trapezoidMajorInnerAngle1);
                    trapezoidMajorInnerPoint2X  = centerX + SIZE * 0.423 * Math.sin(trapezoidMajorInnerAngle2);
                    trapezoidMajorInnerPoint2Y  = centerY + SIZE * 0.423 * Math.cos(trapezoidMajorInnerAngle2);
                    trapezoidMajorOuterPoint1X  = centerX + SIZE * 0.475 * Math.sin(trapezoidMajorOuterAngle1);
                    trapezoidMajorOuterPoint1Y  = centerY + SIZE * 0.475 * Math.cos(trapezoidMajorOuterAngle1);
                    trapezoidMajorOuterPoint2X  = centerX + SIZE * 0.475 * Math.sin(trapezoidMajorOuterAngle2);
                    trapezoidMajorOuterPoint2Y  = centerY + SIZE * 0.475 * Math.cos(trapezoidMajorOuterAngle2);

                    trapezoidMediumInnerAngle1  = Math.toRadians(angle - 0.7 + START_ANGLE);
                    trapezoidMediumInnerAngle2  = Math.toRadians(angle + 0.7 + START_ANGLE);
                    trapezoidMediumOuterAngle1  = Math.toRadians(angle - 1.0 + START_ANGLE);
                    trapezoidMediumOuterAngle2  = Math.toRadians(angle + 1.0 + START_ANGLE);
                    trapezoidMediumInnerPoint1X = centerX + SIZE * 0.435 * Math.sin(trapezoidMediumInnerAngle1);
                    trapezoidMediumInnerPoint1Y = centerY + SIZE * 0.435 * Math.cos(trapezoidMediumInnerAngle1);
                    trapezoidMediumInnerPoint2X = centerX + SIZE * 0.435 * Math.sin(trapezoidMediumInnerAngle2);
                    trapezoidMediumInnerPoint2Y = centerY + SIZE * 0.435 * Math.cos(trapezoidMediumInnerAngle2);
                    trapezoidMediumOuterPoint1X = centerX + SIZE * 0.475 * Math.sin(trapezoidMajorOuterAngle1);
                    trapezoidMediumOuterPoint1Y = centerY + SIZE * 0.475 * Math.cos(trapezoidMediumOuterAngle1);
                    trapezoidMediumOuterPoint2X = centerX + SIZE * 0.475 * Math.sin(trapezoidMediumOuterAngle2);
                    trapezoidMediumOuterPoint2Y = centerY + SIZE * 0.475 * Math.cos(trapezoidMediumOuterAngle2);

                    trapezoidMinorInnerAngle1   = Math.toRadians(angle - 0.6 + START_ANGLE);
                    trapezoidMinorInnerAngle2   = Math.toRadians(angle + 0.6 + START_ANGLE);
                    trapezoidMinorOuterAngle1   = Math.toRadians(angle - 0.8 + START_ANGLE);
                    trapezoidMinorOuterAngle2   = Math.toRadians(angle + 0.8 + START_ANGLE);
                    trapezoidMinorInnerPoint1X  = centerX + SIZE * 0.440 * Math.sin(trapezoidMinorInnerAngle1);
                    trapezoidMinorInnerPoint1Y  = centerY + SIZE * 0.440 * Math.cos(trapezoidMinorInnerAngle1);
                    trapezoidMinorInnerPoint2X  = centerX + SIZE * 0.440 * Math.sin(trapezoidMinorInnerAngle2);
                    trapezoidMinorInnerPoint2Y  = centerY + SIZE * 0.440 * Math.cos(trapezoidMinorInnerAngle2);
                    trapezoidMinorOuterPoint1X  = centerX + SIZE * 0.475 * Math.sin(trapezoidMinorOuterAngle1);
                    trapezoidMinorOuterPoint1Y  = centerY + SIZE * 0.475 * Math.cos(trapezoidMinorOuterAngle1);
                    trapezoidMinorOuterPoint2X  = centerX + SIZE * 0.475 * Math.sin(trapezoidMinorOuterAngle2);
                    trapezoidMinorOuterPoint2Y  = centerY + SIZE * 0.475 * Math.cos(trapezoidMinorOuterAngle2);

                    triangleMajorInnerPointX    = centerX + SIZE * 0.423 * sinValue;
                    triangleMajorInnerPointY    = centerY + SIZE * 0.423 * cosValue;
                    triangleMajorOuterPointX    = centerX + SIZE * 0.475 * sinValue;
                    triangleMajorOuterPointY    = centerY + SIZE * 0.475 * cosValue;

                    triangleMediumInnerPointX   = centerX + SIZE * 0.43 * sinValue;
                    triangleMediumInnerPointY   = centerY + SIZE * 0.43 * cosValue;
                    triangleMediumOuterPointX   = triangleMajorOuterPointX;
                    triangleMediumOuterPointY   = triangleMajorOuterPointY;

                    triangleMinorInnerPointX    = centerX + SIZE * 0.436 * sinValue;
                    triangleMinorInnerPointY    = centerY + SIZE * 0.436 * cosValue;
                    triangleMinorOuterPointX    = triangleMajorOuterPointX;
                    triangleMinorOuterPointY    = triangleMajorOuterPointY;
                    break;
            }

            // Set the general tickmark color
            CTX.setStroke(tickMarkColor);
            CTX.setFill(tickMarkColor);
            CTX.setLineCap(StrokeLineCap.BUTT);

            if (Double.compare(counterBD.remainder(majorTickSpaceBD).doubleValue(), 0.0) == 0) {
                // Draw major tick mark
                isNotZero = Double.compare(0.0, counter) != 0;
                TickMarkType tickMarkType = null;
                if (majorTickMarksVisible) {
                    tickMarkType = majorTickMarkType;
                    CTX.setFill(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, majorTickMarkColor) : majorTickMarkColor);
                    CTX.setStroke(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, majorTickMarkColor) : majorTickMarkColor);
                    CTX.setLineWidth(SIZE * (TickMarkType.BOX == tickMarkType || TickMarkType.PILL == tickMarkType ? 0.016 : MAX_TICK_MARK_WIDTH * majorTickMarkWidthFactor));
                    CTX.setLineCap(TickMarkType.PILL == tickMarkType ? StrokeLineCap.ROUND : StrokeLineCap.BUTT);
                } else if (minorTickMarksVisible) {
                    tickMarkType = minorTickMarkType;
                    CTX.setFill(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, minorTickMarkColor) : minorTickMarkColor);
                    CTX.setStroke(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, minorTickMarkColor) : minorTickMarkColor);
                    CTX.setLineWidth(SIZE * (TickMarkType.BOX == tickMarkType || TickMarkType.PILL == tickMarkType ? 0.007 : MAX_TICK_MARK_WIDTH * minorTickMarkWidthFactor));
                    CTX.setLineCap(TickMarkType.PILL == tickMarkType ? StrokeLineCap.ROUND : StrokeLineCap.BUTT);
                }
                if (fullRange && !isNotZero) {
                    CTX.setFill(zeroColor);
                    CTX.setStroke(zeroColor);
                }
                if (null != tickMarkType) {
                    switch (tickMarkType) {
                        case TRAPEZOID:
                            if (majorTickMarksVisible) {
                                Helper.drawTrapezoid(CTX, trapezoidMajorInnerPoint1X, trapezoidMajorInnerPoint1Y, trapezoidMajorInnerPoint2X, trapezoidMajorInnerPoint2Y,
                                                    trapezoidMajorOuterPoint1X, trapezoidMajorOuterPoint1Y, trapezoidMajorOuterPoint2X, trapezoidMajorOuterPoint2Y);
                            } else if (minorTickMarksVisible) {
                                Helper.drawTrapezoid(CTX, trapezoidMinorInnerPoint1X, trapezoidMinorInnerPoint1Y, trapezoidMinorInnerPoint2X, trapezoidMinorInnerPoint2Y,
                                                    trapezoidMinorOuterPoint1X, trapezoidMinorOuterPoint1Y, trapezoidMinorOuterPoint2X, trapezoidMinorOuterPoint2Y);
                            }
                            break;
                        case TRIANGLE:
                            if (majorTickMarksVisible) {
                                if (TickLabelLocation.INSIDE == tickLabelLocation) {
                                    Helper.drawTriangle(CTX, triangleMajorInnerPointX, triangleMajorInnerPointY, trapezoidMajorOuterPoint1X, trapezoidMajorOuterPoint1Y, trapezoidMajorOuterPoint2X, trapezoidMajorOuterPoint2Y);
                                } else {
                                    Helper.drawTriangle(CTX, triangleMajorOuterPointX, triangleMajorOuterPointY, trapezoidMajorInnerPoint1X, trapezoidMajorInnerPoint1Y, trapezoidMajorInnerPoint2X, trapezoidMajorInnerPoint2Y);
                                }
                            } else if (minorTickMarksVisible) {
                                if (TickLabelLocation.INSIDE == tickLabelLocation) {
                                    Helper.drawTriangle(CTX, triangleMinorInnerPointX, triangleMinorInnerPointY, trapezoidMinorOuterPoint1X, trapezoidMinorOuterPoint1Y, trapezoidMinorOuterPoint2X, trapezoidMinorOuterPoint2Y);
                                } else {
                                    Helper.drawTriangle(CTX, triangleMinorOuterPointX, triangleMinorOuterPointY, trapezoidMinorInnerPoint1X, trapezoidMinorInnerPoint1Y, trapezoidMinorInnerPoint2X, trapezoidMinorInnerPoint2Y);
                                }
                            }
                            break;
                        case DOT:
                            if (majorTickMarksVisible) {
                                Helper.drawDot(CTX, dotCenterX - majorHalfDotSize, dotCenterY - majorHalfDotSize, majorDotSize);
                            } else if (minorTickMarksVisible) {
                                Helper.drawDot(CTX, dotMinorCenterX - minorHalfDotSize, dotMinorCenterY - minorHalfDotSize, minorDotSize);
                            }
                            break;
                        case TICK_LABEL:
                            if (majorTickMarksVisible) {
                                CTX.save();
                                CTX.translate(tickLabelTickMarkX, tickLabelTickMarkY);

                                Helper.rotateContextForText(CTX, START_ANGLE, angle, tickLabelOrientation);

                                CTX.setFont(isNotZero ? tickMarkFont : tickMarkZeroFont);
                                CTX.setTextAlign(TextAlignment.CENTER);
                                CTX.setTextBaseline(VPos.CENTER);
                                CTX.fillText(String.format(locale, tickLabelFormatString, counter), 0, 0);
                                CTX.restore();
                            }
                            break;
                        case LINE:
                        default:
                            if (majorTickMarksVisible) {
                                Helper.drawLine(CTX, innerPointX, innerPointY, outerPointX, outerPointY);
                            } else if (minorTickMarksVisible) {
                                if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                                    Helper.drawLine(CTX, innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
                                } else {
                                    Helper.drawLine(CTX, innerMinorPointX, innerMinorPointY, outerPointX, outerPointY);
                                }
                            }
                            break;
                    }
                }

                // Draw tick label text
                if (tickLabelsVisible) {
                    CTX.save();
                    CTX.translate(textPointX, textPointY);

                    Helper.rotateContextForText(CTX, START_ANGLE, angle, tickLabelOrientation);
                    CTX.setFont(isNotZero ? tickLabelFont : tickLabelZeroFont);
                    CTX.setTextAlign(TextAlignment.CENTER);
                    CTX.setTextBaseline(VPos.CENTER);

                    if (!onlyFirstAndLastLabelVisible) {
                        if (isNotZero) {
                            CTX.setFill(tickLabelSectionsVisible ? Helper.getColorOfSection(tickLabelSections, counter, tickLabelColor) : tickLabelColor);
                        } else {
                            CTX.setFill(tickLabelSectionsVisible ? Helper.getColorOfSection(tickLabelSections, counter, tickLabelColor) : fullRange ? zeroColor : tickLabelColor);
                        }
                    } else {
                        if ((Double.compare(counter, MIN_VALUE) == 0 || Double.compare(counter, MAX_VALUE) == 0)) {
                            if (isNotZero) {
                                CTX.setFill(tickLabelSectionsVisible ? Helper.getColorOfSection(tickLabelSections, counter, tickLabelColor) : tickLabelColor);
                            } else {
                                CTX.setFill(tickLabelSectionsVisible ? Helper.getColorOfSection(tickLabelSections, counter, tickLabelColor) : fullRange ? zeroColor : tickLabelColor);
                            }
                        } else {
                            CTX.setFill(Color.TRANSPARENT);
                        }
                    }

                    if (customTickLabelsEnabled) {
                        if (customTickLabelCounter >= 0) {
                            CTX.fillText(customTickLabels.get(customTickLabelCounter), 0, 0);
                            customTickLabelCounter++;
                        }
                        if (customTickLabelCounter > customTickLabels.size() - 1) customTickLabelCounter = -1;
                    } else {
                        CTX.fillText(String.format(locale, tickLabelFormatString, counter), 0, 0);
                    }
                    CTX.restore();
                }
            } else if (mediumTickMarksVisible &&
                       Double.compare(minorTickSpaceBD.remainder(mediumCheck2).doubleValue(), 0.0) != 0.0 &&
                       Double.compare(counterBD.remainder(mediumCheck5).doubleValue(), 0.0) == 0.0) {
                // Draw medium tick mark
                CTX.setFill(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, mediumTickMarkColor) : mediumTickMarkColor);
                CTX.setStroke(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, mediumTickMarkColor) : mediumTickMarkColor);
                switch(mediumTickMarkType) {
                    case TRAPEZOID:
                        Helper.drawTrapezoid(CTX, trapezoidMediumInnerPoint1X, trapezoidMediumInnerPoint1Y, trapezoidMediumInnerPoint2X, trapezoidMediumInnerPoint2Y,
                                            trapezoidMediumOuterPoint1X, trapezoidMediumOuterPoint1Y, trapezoidMediumOuterPoint2X, trapezoidMediumOuterPoint2Y);
                        break;
                    case TRIANGLE:
                        if (TickLabelLocation.INSIDE == tickLabelLocation) {
                            Helper.drawTriangle(CTX, triangleMediumInnerPointX, triangleMediumInnerPointY, trapezoidMediumOuterPoint1X, trapezoidMediumOuterPoint1Y, trapezoidMediumOuterPoint2X, trapezoidMediumOuterPoint2Y);
                        } else {
                            Helper.drawTriangle(CTX, triangleMediumOuterPointX, triangleMediumOuterPointY, trapezoidMediumInnerPoint1X, trapezoidMediumInnerPoint1Y, trapezoidMediumInnerPoint2X, trapezoidMediumInnerPoint2Y);
                        }
                        break;
                    case DOT:
                        Helper.drawDot(CTX, dotMediumCenterX - mediumHalfDotSize, dotMediumCenterY - mediumHalfDotSize, mediumDotSize);
                        break;
                    case BOX:
                        CTX.setLineWidth(SIZE * 0.009);
                        if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                            Helper.drawLine(CTX, innerPointX, innerPointY, outerMediumPointX, outerMediumPointY);
                        } else {
                            Helper.drawLine(CTX, innerMediumPointX, innerMediumPointY, outerPointX, outerPointY);
                        }
                        break;
                    case PILL:
                        CTX.setLineCap(StrokeLineCap.ROUND);
                        CTX.setLineWidth(SIZE * 0.009);
                        if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                            Helper.drawLine(CTX, innerPointX, innerPointY, outerMediumPointX, outerMediumPointY);
                        } else {
                            Helper.drawLine(CTX, innerMediumPointX, innerMediumPointY, outerPointX, outerPointY);
                        }
                        break;
                    case LINE:
                    default:
                        CTX.setLineWidth(SIZE * MAX_TICK_MARK_WIDTH * mediumTickMarkWidthFactor);
                        if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                            Helper.drawLine(CTX, innerPointX, innerPointY, outerMediumPointX, outerMediumPointY);
                        } else {
                            Helper.drawLine(CTX, innerMediumPointX, innerMediumPointY, outerPointX, outerPointY);
                        }
                        break;
                }
            } else if (minorTickMarksVisible) {
                boolean drawMinorTicks = false;
                if (minorTickSpaceBD.stripTrailingZeros().scale() <= 0) {
                    if (Double.compare(counterBD.remainder(minorTickSpaceBD).doubleValue(), 0.0) == 0) {
                        drawMinorTicks = true;
                    }
                } else {
                    drawMinorTicks = true;
                }

                // Draw minor tick mark
                if (drawMinorTicks && TickMarkType.TICK_LABEL != majorTickMarkType) {
                    CTX.setFill(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, minorTickMarkColor) : minorTickMarkColor);
                    CTX.setStroke(tickMarkSectionsVisible ? Helper.getColorOfSection(tickMarkSections, counter, minorTickMarkColor) : minorTickMarkColor);
                    switch (minorTickMarkType) {
                        case TRAPEZOID:
                            Helper.drawTrapezoid(CTX, trapezoidMinorInnerPoint1X, trapezoidMinorInnerPoint1Y, trapezoidMinorInnerPoint2X, trapezoidMinorInnerPoint2Y,
                                                trapezoidMinorOuterPoint1X, trapezoidMinorOuterPoint1Y, trapezoidMinorOuterPoint2X, trapezoidMinorOuterPoint2Y);
                            break;
                        case TRIANGLE:
                            if (TickLabelLocation.INSIDE == tickLabelLocation) {
                                Helper.drawTriangle(CTX, triangleMinorInnerPointX, triangleMinorInnerPointY, trapezoidMinorOuterPoint1X, trapezoidMinorOuterPoint1Y, trapezoidMinorOuterPoint2X, trapezoidMinorOuterPoint2Y);
                            } else {
                                Helper.drawTriangle(CTX, triangleMinorOuterPointX, triangleMinorOuterPointY, trapezoidMinorInnerPoint1X, trapezoidMinorInnerPoint1Y, trapezoidMinorInnerPoint2X, trapezoidMinorInnerPoint2Y);
                            }
                            break;
                        case DOT:
                            Helper.drawDot(CTX, dotMinorCenterX - minorHalfDotSize, dotMinorCenterY - minorHalfDotSize, minorDotSize);
                            break;
                        case BOX:
                            CTX.setLineWidth(SIZE * 0.007);
                            if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                                Helper.drawLine(CTX, innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
                            } else {
                                Helper.drawLine(CTX, innerMinorPointX, innerMinorPointY, outerPointX, outerPointY);
                            }
                            break;
                        case PILL:
                            CTX.setLineCap(StrokeLineCap.ROUND);
                            CTX.setLineWidth(SIZE * 0.007);
                            if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                                Helper.drawLine(CTX, innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
                            } else {
                                Helper.drawLine(CTX, innerMinorPointX, innerMinorPointY, outerPointX, outerPointY);
                            }
                            break;
                        case LINE:
                        default:
                            CTX.setLineWidth(SIZE * MAX_TICK_MARK_WIDTH * minorTickMarkWidthFactor);
                            if (TickLabelLocation.OUTSIDE == tickLabelLocation) {
                                Helper.drawLine(CTX, innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
                            } else {
                                Helper.drawLine(CTX, innerMinorPointX, innerMinorPointY, outerPointX, outerPointY);
                            }
                            break;
                    }
                }
            }
            counterBD = counterBD.add(minorTickSpaceBD);
            counter   = counterBD.doubleValue();
            if (counter > MAX_VALUE) break;
            angle     = ScaleDirection.CLOCKWISE == scaleDirection ? (angle - tmpAngleStep) : (angle + tmpAngleStep);
        }
    }

    public static final Image createNoiseImage(final double WIDTH, final double HEIGHT, final Color DARK_COLOR, final Color BRIGHT_COLOR, final double ALPHA_VARIATION_IN_PERCENT) {
        if (Double.compare(WIDTH, 0) <= 0 || Double.compare(HEIGHT, 0) <= 0) return null;
        int                 width                   = (int) WIDTH;
        int                 height                  = (int) HEIGHT;
        double              alphaVariationInPercent = Helper.clamp(0.0, 100.0, ALPHA_VARIATION_IN_PERCENT);
        final WritableImage IMAGE                   = new WritableImage(width, height);
        final PixelWriter   PIXEL_WRITER            = IMAGE.getPixelWriter();
        final Random        BW_RND                  = new Random();
        final Random        ALPHA_RND               = new Random();
        final double        ALPHA_START             = alphaVariationInPercent / 100 / 2;
        final double        ALPHA_VARIATION         = alphaVariationInPercent / 100;
        for (int y = 0 ; y < height ; y++) {
            for (int x = 0 ; x < width ; x++) {
                final Color  NOISE_COLOR = BW_RND.nextBoolean() ? BRIGHT_COLOR : DARK_COLOR;
                final double NOISE_ALPHA = Helper.clamp(0.0, 1.0, ALPHA_START + ALPHA_RND.nextDouble() * ALPHA_VARIATION);
                PIXEL_WRITER.setColor(x, y, Color.color(NOISE_COLOR.getRed(), NOISE_COLOR.getGreen(), NOISE_COLOR.getBlue(), NOISE_ALPHA));
            }
        }
        return IMAGE;
    }

    public static final void drawTimeSections(final Clock CLOCK, final GraphicsContext CTX, final List<TimeSection> SECTIONS, final double SIZE,
                                              final double XY_INSIDE, final double XY_OUTSIDE, final double WH_INSIDE, final double WH_OUTSIDE,
                                              final double LINE_WIDTH) {
        if (SECTIONS.isEmpty()) return;
        TickLabelLocation tickLabelLocation = CLOCK.getTickLabelLocation();
        ZonedDateTime     time              = CLOCK.getTime();
        boolean           isAM              = time.get(ChronoField.AMPM_OF_DAY) == 0;
        double            xy                = TickLabelLocation.INSIDE == tickLabelLocation ? XY_INSIDE * SIZE : XY_OUTSIDE * SIZE;
        double            wh                = TickLabelLocation.INSIDE == tickLabelLocation ? WH_INSIDE * SIZE : WH_OUTSIDE * SIZE;
        double            offset            = 90;
        int               listSize          = SECTIONS.size();
        double            angleStep         = 360.0 / 60.0;
        boolean           highlightSections = CLOCK.isHighlightSections();
        for (int i = 0 ; i < listSize ; i++) {
            TimeSection section   = SECTIONS.get(i);
            LocalTime   start     = section.getStart();
            LocalTime   stop      = section.getStop();
            boolean     isStartAM = start.get(ChronoField.AMPM_OF_DAY) == 0;
            boolean     isStopAM  = stop.get(ChronoField.AMPM_OF_DAY) == 0;
            boolean     draw      = isAM ? (isStartAM || isStopAM) :(!isStartAM || !isStopAM);
            if (draw) {
                double sectionStartAngle = (start.getHour() % 12 * 5.0 + start.getMinute() / 12.0 + start.getSecond() / 300.0) * angleStep + 180;
                double sectionAngleExtend = ((stop.getHour() - start.getHour()) % 12 * 5.0 + (stop.getMinute() - start.getMinute()) / 12.0 + (stop.getSecond() - start.getSecond()) / 300.0) * angleStep;
                //TODO: Add an indicator to the section like -1 or similar
                // check if start was already yesterday
                if (start.getHour() > stop.getHour()) { sectionAngleExtend = (360.0 - Math.abs(sectionAngleExtend)); }
                CTX.save();
                if (highlightSections) {
                    CTX.setStroke(section.contains(time.toLocalTime()) ? section.getHighlightColor() : section.getColor());
                } else {
                    CTX.setStroke(section.getColor());
                }
                CTX.setLineWidth(SIZE * LINE_WIDTH);
                CTX.setLineCap(StrokeLineCap.BUTT);
                CTX.strokeArc(xy, xy, wh, wh, -(offset + sectionStartAngle), -sectionAngleExtend, ArcType.OPEN);
                CTX.restore();
            }
        }
    }

    public static final void drawTimeAreas(final Clock CLOCK, final GraphicsContext CTX, final List<TimeSection> AREAS, final double SIZE,
                                           final double XY_INSIDE, final double XY_OUTSIDE, final double WH_INSIDE, final double WH_OUTSIDE) {
        if (AREAS.isEmpty()) return;
        TickLabelLocation tickLabelLocation = CLOCK.getTickLabelLocation();
        ZonedDateTime     time              = CLOCK.getTime();
        boolean           isAM              = time.get(ChronoField.AMPM_OF_DAY) == 0;
        double            xy                = TickLabelLocation.OUTSIDE == tickLabelLocation ? XY_OUTSIDE * SIZE : XY_INSIDE * SIZE;
        double            wh                = TickLabelLocation.OUTSIDE == tickLabelLocation ? WH_OUTSIDE * SIZE : WH_INSIDE * SIZE;
        double            offset            = 90;
        double            angleStep         = 360.0 / 60.0;
        int               listSize          = AREAS.size();
        boolean           highlightAreas    = CLOCK.isHighlightAreas();
        for (int i = 0; i < listSize ; i++) {
            TimeSection area      = AREAS.get(i);
            LocalTime   start     = area.getStart();
            LocalTime   stop      = area.getStop();
            boolean     isStartAM = start.get(ChronoField.AMPM_OF_DAY) == 0;
            boolean     isStopAM  = stop.get(ChronoField.AMPM_OF_DAY) == 0;
            boolean     draw      = isAM ? (isStartAM || isStopAM) :(!isStartAM || !isStopAM);
            if (draw) {
                double areaStartAngle  = (start.getHour() % 12 * 5.0 + start.getMinute() / 12.0 + start.getSecond() / 300.0) * angleStep + 180;;
                double areaAngleExtend = ((stop.getHour() - start.getHour()) % 12 * 5.0 + (stop.getMinute() - start.getMinute()) / 12.0 + (stop.getSecond() - start.getSecond()) / 300.0) * angleStep;
                //TODO: Add an indicator to the area like -1 or similar
                // check if start was already yesterday
                if (start.getHour() > stop.getHour()) { areaAngleExtend = (360.0 - Math.abs(areaAngleExtend)); }
                CTX.save();
                if (highlightAreas) {
                    CTX.setFill(area.contains(time.toLocalTime()) ? area.getHighlightColor() : area.getColor());
                } else {
                    CTX.setFill(area.getColor());
                }
                CTX.fillArc(xy, xy, wh, wh, -(offset + areaStartAngle), -areaAngleExtend, ArcType.ROUND);
                CTX.restore();
            }
        }
    }

    public static final void drawAlarms(final Clock CLOCK, final double SIZE, final double ALARM_MARKER_SIZE, final double ALARM_MARKER_RADIUS, final Map<Alarm, Circle> ALARM_MAP, final DateTimeFormatter DATE_TIME_FORMATTER, final ZonedDateTime TIME) {
        if (CLOCK.isAlarmsVisible()) {
            double alarmSize = ALARM_MARKER_SIZE * SIZE;
            double center    = SIZE * 0.5;
            double angleStep = 360.0 / 60.0;
            for (Map.Entry<Alarm, Circle> entry : ALARM_MAP.entrySet()) {
                Alarm         alarm      = entry.getKey();
                ZonedDateTime alarmTime  = alarm.getTime();
                double        alarmAngle = (alarmTime.getMinute() + alarmTime.getSecond() / 60.0) * angleStep + 180;
                double        sinValue   = Math.sin(Math.toRadians((-alarmAngle)));
                double        cosValue   = Math.cos(Math.toRadians((-alarmAngle)));
                Color         alarmColor = alarm.isArmed() ? alarm.getColor() : INACTIVE_ALARM_COLOR;
                Circle        dot        = entry.getValue();
                dot.setRadius(alarmSize);
                dot.setCenterX(center + SIZE * ALARM_MARKER_RADIUS * sinValue);
                dot.setCenterY(center + SIZE * ALARM_MARKER_RADIUS * cosValue);
                dot.setFill(alarmColor);
                dot.setStroke(alarmColor.darker());
                dot.setPickOnBounds(false);
                dot.setOnMousePressed(e -> alarm.fireAlarmMarkerEvent(alarm.ALARM_MARKER_PRESSED_EVENT));
                dot.setOnMouseReleased(e -> alarm.fireAlarmMarkerEvent(alarm.ALARM_MARKER_RELEASED_EVENT));
                if (alarmTime.getDayOfMonth() == TIME.getDayOfMonth() &&
                    alarmTime.getMonthValue() == TIME.getMonthValue() &&
                    alarmTime.getYear() == TIME.getYear() &&
                    alarmTime.getHour() == TIME.getHour() &&
                    alarmTime.getMinute() >= TIME.getMinute()) {
                    dot.setManaged(true);
                    dot.setVisible(true);
                } else {
                    dot.setManaged(false);
                    dot.setVisible(false);
                }
                Tooltip alarmTooltip;
                if (alarm.getText().isEmpty()) {
                    alarmTooltip = new Tooltip(DATE_TIME_FORMATTER.format(alarm.getTime()));
                } else {
                    alarmTooltip = new Tooltip(new StringBuilder(alarm.getText()).append("\n").append(DATE_TIME_FORMATTER.format(alarm.getTime())).toString());
                }
                alarmTooltip.setTextAlignment(TextAlignment.CENTER);
                Tooltip.install(dot, alarmTooltip);
            }
        }
    }

    public static final String formatNumber(final Gauge GAUGE, final double VALUE) {
        return formatNumber(GAUGE.getLocale(), GAUGE.getFormatString(), GAUGE.getDecimals(), VALUE);
    }

    public static final String formatNumber(final Locale LOCALE, final String FORMAT_STRING, final int DECIMALS, final double VALUE) {
        double value = VALUE;
        if (value > 0) {
            value = Math.floor(value * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS);
        } else if (value < 0) {
            value = Math.ceil(value * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS);
        }
        return String.format(LOCALE, FORMAT_STRING, value);
    }

    public static final String formatNumber(final Locale LOCALE, final double MIN_VALUE, final double MAX_VALUE, final int DECIMALS, final double VALUE) {
        StringBuilder sb        = new StringBuilder("%.").append(DECIMALS).append("f");
        String        f         = sb.toString();
        int           minLength = String.format(Locale.US, f, MIN_VALUE).length();
        int           maxLength = String.format(Locale.US, f, MAX_VALUE).length();
        int           length    = Math.max(minLength, maxLength);

        StringBuilder formatStringBuilder = new StringBuilder("%").append(length).append(".").append(DECIMALS).append("f");
        String        formatString        = formatStringBuilder.toString();

        double value = VALUE;
        if (value > 0) {
            value = Math.floor(VALUE * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS);
        } else if (value < 0) {
            value = Math.ceil(VALUE * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS);
        }

        return String.format(LOCALE, formatString, value);
    }
}