| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 
 | public class TimelineView extends Region {
 
    private final Region axis = new Region();
    private final Path ticks = new Path();
    private final Pane tickLabels = new Pane();
 
    public TimelineView() {
        super();
        setId("timelimeView");
        getStyleClass().add("timeline-view");
        axis.setId("axis");
        axis.getStyleClass().add("axis");
        ticks.setId("ticks");
        ticks.getStyleClass().add("ticks");
        tickLabels.setId("tickLabels");
        tickLabels.getStyleClass().add("tick-labels");
        getChildren().addAll(axis, ticks, tickLabels);
        startDateProperty().addListener(timelineChangeListener);
        endDateProperty().addListener(timelineChangeListener);
        tickUnitProperty().addListener(timelineChangeListener);
        tickLengthProperty().addListener(timelineChangeListener);
        widthProperty().addListener(timelineChangeListener);
        heightProperty().addListener(timelineChangeListener);
    }
 
    @Override
    public String getUserAgentStylesheet() {
        final URL url = getClass().getResource("timelineview.css");
        return (url == null) ? null : url.toExternalForm();
    }
 
    @Override
    protected void layoutChildren() {
        super.layoutChildren();
        final double width = getWidth();
        final double height = getHeight();
        final Insets insets = getInsets();
        final double areaX = insets.getLeft();
        final double areaY = insets.getTop();
        final double areaW = Math.max(0, width - insets.getLeft() - insets.getRight());
        final double areaH = Math.max(0, height - insets.getTop() - insets.getBottom());
        layoutChildrenInArea(areaX, areaY, areaW, areaH);
    }
 
    private int clamp(final double position) {
        return (int) Math.round(position);
    }
 
    private void layoutChildrenInArea(final double areaX, final double areaY, final double areaW, final double areaH) {
        final int axisX = clamp(areaX);
        final int axisY = clamp(areaY + areaH * 2 / 3d);
        final int axisW = clamp(areaW);
        final int axisH = clamp(axis.getPrefHeight());
        layoutInArea(axis, axisX, axisY, axisW, axisH, 0, HPos.LEFT, VPos.TOP);
        //
        final Period tickUnit = getTickUnit();
        LocalDate startDate = getStartDate();
        LocalDate endDate = getEndDate();
        if (startDate.isAfter(endDate)) {
            LocalDate tmp = startDate;
            startDate = endDate;
            endDate = tmp;
        }
        final List<LocalDate> calendar = new LinkedList();
        for (LocalDate currentDate = startDate; currentDate.isBefore(endDate) || currentDate.isEqual(endDate); currentDate = currentDate.plus(tickUnit)) {
            calendar.add(currentDate);
        }
        final int tickCount = calendar.size();
        if (tickCount > 0 && ticks.getElements().isEmpty()) {
            final DateTimeFormatter yearExtractor = DateTimeFormatter.ofPattern("YYYY");
            final double tickDistance = axisW / Math.max(1, tickCount - 1);
            final int tickY = axisY + axisH;
            final double tickLength = Math.max(0, getTickLength());
            for (int tickIndex = 0; tickIndex < tickCount; tickIndex++) {
                final int tickX = clamp(axisX + tickIndex * tickDistance);
                if (tickLength > 0) {
                    ticks.getElements().add(new MoveTo(tickX, tickY));
                    ticks.getElements().add(new LineTo(tickX, tickY + tickLength));
                }
                final LocalDate currentDate = calendar.get(tickIndex);
                final String text = currentDate.format(yearExtractor);
                final Text tickLabel = new Text(text);
                tickLabel.setId("tickLabel");
                tickLabel.getStyleClass().add("tick-label");
                tickLabel.setTextOrigin(VPos.TOP);
                tickLabels.getChildren().add(tickLabel);
            }
            // We need to apply CSS 1st for proper layout.
            applyCss();
            for (int tickIndex = 0; tickIndex < tickCount; tickIndex++) {
                final int tickX = clamp(axisX + tickIndex * tickDistance);
                final Text tickLabel = (Text) tickLabels.getChildren().get(tickIndex);
                final int labelX = clamp(tickX - tickLabel.getBoundsInLocal().getWidth() / 2d);
                final int labelY = clamp(tickY + tickLength + 5);
                tickLabel.setLayoutX(labelX);
                tickLabel.setLayoutY(labelY);
            }
        }
    }
 
    private final ReadOnlyObjectWrapper<LocalDate> startDate = new ReadOnlyObjectWrapper<>(this, "startDate", LocalDate.parse("1900-01-01"));
 
    public final LocalDate getStartDate() {
        return startDate.get();
    }
 
    public final void setStartDate(final LocalDate value) throws NullPointerException {
        Objects.requireNonNull(value);
        startDate.set(value);
    }
 
    public final ReadOnlyObjectProperty<LocalDate> startDateProperty() {
        return startDate.getReadOnlyProperty();
    }
 
    private final ReadOnlyObjectWrapper<LocalDate> endDate = new ReadOnlyObjectWrapper<>(this, "endDate", LocalDate.parse("2000-01-01"));
 
    public final LocalDate getEndDate() {
        return endDate.get();
    }
 
    public final void setEndDate(final LocalDate value) throws NullPointerException {
        Objects.requireNonNull(value);
        endDate.set(value);
    }
 
    public final ReadOnlyObjectProperty<LocalDate> endDateProperty() {
        return endDate.getReadOnlyProperty();
    }
 
    private final ReadOnlyObjectWrapper<Period> tickUnit = new ReadOnlyObjectWrapper<>(this, "tickUnit", Period.ofYears(10));
 
    public final Period getTickUnit() {
        return tickUnit.get();
    }
 
    public final void setTickUnit(final Period value) throws NullPointerException {
        Objects.requireNonNull(value);
        tickUnit.set(value);
    }
 
    public final ReadOnlyObjectProperty<Period> tickUnitProperty() {
        return tickUnit.getReadOnlyProperty();
    }
 
    private final DoubleProperty tickLength = new SimpleDoubleProperty(this, "tickLength", 15);
 
    public final double getTickLength() {
        return tickLength.get();
    }
 
    public final void setTickLength(final double value) {
        tickLength.set(value);
    }
 
    public final DoubleProperty tickLengthProperty() {
        return tickLength;
    }
 
    private final ChangeListener timelineChangeListener = (observable, oldValue, newValue) -> {
        ticks.getElements().clear();
        tickLabels.getChildren().clear();
        requestLayout();
    };
} |