001package jmri.jmrit.etcs.dmi.swing;
002
003import jmri.jmrit.etcs.TrackCondition;
004import jmri.jmrit.etcs.ResourceUtil;
005import jmri.jmrit.etcs.TrackSection;
006import jmri.jmrit.etcs.MovementAuthority;
007
008import java.awt.Color;
009import java.awt.Font;
010import java.awt.FontMetrics;
011import java.awt.Graphics;
012import java.awt.Graphics2D;
013import java.awt.RenderingHints;
014import java.awt.event.ActionEvent;
015import java.awt.image.BufferedImage;
016import java.util.*;
017
018import javax.annotation.Nonnull;
019import javax.swing.*;
020
021/**
022 * Class to demonstrate features of ERTMS DMI Panel D, the Planning Area.
023 * @author Steve Young Copyright (C) 2024
024 */
025public class DmiPanelD extends JPanel {
026
027    private static final Color PASP_DARK = new Color(33,49,74);
028    private static final Color PASP_LIGHT = new Color(41,74,107);
029
030    private final DmiPanel dmiPanel;
031    private final JPanel trackAheadFreeQuestion;
032    private final JPanel planningPanel;
033
034    private final JButton plusButton;
035    private final JButton minusButton;
036
037    private static final int[] scaleLineYPx = new int[]{284,283,250,206,182,164,150,149,107,64,21,20};
038    private static final boolean[] scaleLineLight = new boolean[]{true, true, false, false, false, false,
039        true, true, false, false, true, true};
040
041    private static final int[] scaleDistanceBase = new int[]{0, 125, 250, 500, 1000};
042    private static final int[] scaleycords = new int[]{287, 155, 111, 68, 25};
043    private static final int[] scales = new int[]{ 1, 2, 4, 8, 16, 32};
044
045    private final List<MovementAuthority> maList = new ArrayList<>();
046    private boolean loopGradientLimitReached = false;
047    private int nextAdviceChange = -1;
048    private int indicationDistance = -1;
049    private int indicationSpeedChange = -1;
050
051    private static final BufferedImage speedDownImage = ResourceUtil.getTransparentImage("PL_22.bmp");
052    private static final BufferedImage speedDownImageTargetIndication = ResourceUtil.getTransparentImage("PL_23.bmp");
053    private static final BufferedImage speedDownImageTargetIndicationAto = ResourceUtil.getTransparentImage("PL_37.bmp");
054    private static final BufferedImage speedUpImage = ResourceUtil.getTransparentImage("PL_21.bmp");
055
056    private int currentScale = 0;
057
058    public DmiPanelD(@Nonnull DmiPanel mainPanel){
059        super();
060        setLayout(null);
061        dmiPanel = mainPanel;
062        trackAheadFreeQuestion = this.trackAheadFreeQuestionPanel();
063
064        plusButton = new TransparentButton( true );
065        minusButton = new TransparentButton( false );
066
067        setButtonsToState(); // set initial state
068
069        planningPanel = getPlanningPanel();
070        planningPanel.setLayout(null);
071
072        setBackground(DmiPanel.BACKGROUND_COLOUR);
073        setBounds(334, 15, 246, 300);
074
075        add(trackAheadFreeQuestion);
076        add(planningPanel);
077
078        DmiPanelD.this.setTrackAheadFreeQuestionVisible(false);
079    }
080
081    private JPanel getPlanningPanel(){
082        JPanel p = new JPanel() {
083            @Override
084            protected void paintComponent(Graphics g) {
085
086                if (trackAheadFreeQuestion.isVisible() || maList.isEmpty()  ) {
087                    plusButton.setVisible(false);
088                    minusButton.setVisible(false);
089                    return;
090                }
091                if (!(g instanceof Graphics2D) ) {
092                    throw new IllegalArgumentException("Graphics object passed is not the correct type");
093                }
094                Graphics2D g2 = (Graphics2D) g;
095
096                RenderingHints  hints =new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
097                hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
098                hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
099                g2.setRenderingHints(hints);
100                
101                drawBackground(g2);
102                drawScale(g2);
103                loopGradientLimitReached = false;
104                drawGradientBar(g2);
105                drawSpeedChanges(g2);
106                drawAnnouncementsAndOrders(g2);
107                drawNextAdviceChange(g2);
108                drawIndicationMarkerLine(g2);
109            }
110        };
111        p.setLayout(null);
112        p.setBounds(0, 0, 246, 300); // includes top and bottom margins
113        p.setOpaque(false);
114
115        p.add(plusButton);
116        p.add(minusButton);
117
118        return p;
119    }
120
121    private void drawAnnouncementsAndOrders( Graphics2D g2 ) {
122        int metresInPreviousSections = 0;
123        int nextColumn = 2;
124        Comparator<TrackCondition> lengthComparator = Comparator.comparingInt(TrackCondition::getDistanceFromStart);
125        for (MovementAuthority ma : maList ) {
126            for ( TrackSection section : ma.getTrackSections() ) {
127                List<TrackCondition> l = section.getAnnouncements();
128                Collections.sort(l, lengthComparator );
129                for ( TrackCondition da : l) {
130                    log.debug("found trackCondition {}", da );
131                    int distance = metresInPreviousSections+da.getDistanceFromStart();
132                    // no need to render all of the icons
133                    if ( distance <= ( scaleDistanceBase[4]*scales[currentScale] *1.5 ) ) {
134                        nextColumn = ensureSlotNumber(da, nextColumn);
135                        int startPx = ( calculatePositionOnScale(distance));
136                        log.debug("marker dist:{} px:{}", distance, startPx);
137                        log.debug("drawing image {} at x:{} y:{} ",
138                            da.getSmlImage(), getAnnouncementColumnPx(da.getColumnNum()),265-startPx);
139                        g2.drawImage(da.getSmlImage(), getAnnouncementColumnPx(da.getColumnNum()), 265-startPx, this);
140                    }
141                } 
142                metresInPreviousSections += section.getLength();
143            }
144        }
145    }
146
147    private static int getAnnouncementColumnPx(int column){
148        switch (column) {
149            case 1:
150                return 42;
151            case 2:
152                return 67;
153            case 3:
154                return 92;
155            default:
156                throw new IllegalArgumentException();
157        }
158    }
159
160    private static int ensureSlotNumber(TrackCondition da, int nextColumn){
161        int tempCol = nextColumn;
162        if ( da.getColumnNum() == 0 ) {
163            da.setColumnNum(tempCol);
164        } else {
165            tempCol = da.getColumnNum();
166        }
167        tempCol++;
168        if ( tempCol == 4 ) {
169            tempCol = 1;
170        }
171        return tempCol;
172    }
173
174    private void drawSpeedChanges( Graphics2D g2 ) {
175
176        List<TrackSection> speedChangeList = MovementAuthority.getTrackSectionList(maList, true);
177        speedChangeList.forEach(ts -> log.debug("track section speed {} length {}", ts.getSpeed(), ts.getLength()));
178
179        Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 14);
180        g2.setFont(unitsSizedFont);
181        g2.setColor(DmiPanel.GREY);
182
183        int loopMetres = 0;
184        int stopPx = 0;
185        boolean increaseDisplayed = false;
186        int reductionsDisplayed = 0;
187
188        for (int i = 0; i < speedChangeList.size(); i++){
189            int startPx = calculatePositionOnScale(loopMetres);
190            loopMetres += speedChangeList.get(i).getLength();
191            stopPx = calculatePositionOnScale(loopMetres);
192            if ( i == 0 ){
193                continue;
194            }
195            int speed = speedChangeList.get(i).getSpeed();
196            int change = speed - speedChangeList.get(i-1).getSpeed();
197
198            if ( change < 0 ) {
199                g2.setColor(getIndicationColor(i));
200                if ( !increaseDisplayed && reductionsDisplayed < 4 ) {
201                    g2.drawImage(getIndicationImg(i), 136, 280-startPx, this);
202                    g2.drawString(String.valueOf(speed), 155, 295-startPx);
203                    reductionsDisplayed++;
204                }
205            } else {
206                g2.setColor(DmiPanel.GREY);
207                g2.drawImage(speedUpImage, 136, 266-startPx, this);
208                g2.drawString(String.valueOf(speed), 155, 281-startPx);
209                // TODO check in v4 - increaseDisplayed rule 8.3.7.9 clarification
210                // increaseDisplayed = true;
211            }
212        }
213
214        g2.setColor(getIndicationColor(0));
215        // add 0 stop speed marker
216        g2.drawImage(getIndicationImg(0), 136, 281-stopPx, this);
217        g2.drawString(String.valueOf("0"), 155, 295-stopPx);
218    }
219
220    private BufferedImage getIndicationImg(int order){
221        if ( indicationSpeedChange != order ) {
222            return speedDownImage;
223        }
224        if ( dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING ){
225            return DmiPanelD.speedDownImageTargetIndicationAto;
226        } else {
227            return DmiPanelD.speedDownImageTargetIndication;
228        }
229    }
230
231    private Color getIndicationColor(int order){
232        if ( indicationSpeedChange != order ) {
233            return DmiPanel.GREY;
234        }
235        if ( dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING ){
236            return DmiPanel.WHITE;
237        } else {
238            return DmiPanel.YELLOW;
239        }
240    }
241
242    private void drawBackground( Graphics2D g2 ) {
243        g2.setColor( PASP_DARK );
244        g2.fillRect(147, 0, 99, 270+15);
245
246        List<TrackSection> speedChangeList = MovementAuthority.getTrackSectionList(maList, true);
247        speedChangeList.forEach(ts -> log.debug("track section speed {} length {}", ts.getSpeed(), ts.getLength()));
248
249        int loopSpeedPlanningMetresFromStart = 0;
250        int loopWidth = 4;
251
252        for (int i = 0; i < speedChangeList.size(); i++){
253            int startPx = calculatePositionOnScale(loopSpeedPlanningMetresFromStart);
254            loopSpeedPlanningMetresFromStart += speedChangeList.get(i).getLength();
255            int stopPx = calculatePositionOnScale(loopSpeedPlanningMetresFromStart);
256
257            if (speedChangeList.get(i) != speedChangeList.get(0)){
258                double percentageOfFirstSection = (double)
259                    speedChangeList.get(i).getSpeed() / speedChangeList.get(0).getSpeed() * 100;
260                if (percentageOfFirstSection < 50 ){
261                    loopWidth = Math.min(loopWidth, 1);
262                } else if (percentageOfFirstSection < 75 ){
263                    loopWidth = Math.min(loopWidth, 2);
264                } else if ( percentageOfFirstSection < 100 ){
265                    loopWidth = Math.min(loopWidth, 3);
266                }
267            }
268
269            int w = (94 * loopWidth / 4);
270            log.debug("draw rect y:{} width:{} height:{}", 282-stopPx, w, stopPx-startPx );
271            g2.setColor( PASP_LIGHT );
272            g2.fillRect(147, 282-stopPx, w, stopPx-startPx);
273        }
274    }
275
276    private void drawScale(Graphics2D g2){
277        for ( int i = 0; i< scaleLineYPx.length; i++ ){
278            g2.setColor(scaleLineLight[i] ? DmiPanel.MEDIUM_GREY: DmiPanel.DARK_GREY );
279            g2.drawLine(40, scaleLineYPx[i], 240, scaleLineYPx[i]);
280        }
281        Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 12);
282        g2.setFont(unitsSizedFont);
283        FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont);
284        g2.setColor(DmiPanel.MEDIUM_GREY );
285
286        for ( int i = 0; i< scaleDistanceBase.length; i++ ){
287            String s = String.valueOf(scaleDistanceBase[i]*(scales[currentScale]));
288            int width = 38- unitsFontM.stringWidth(s);
289            g2.drawString(s, width, scaleycords[i]);
290        }
291
292        plusButton.setVisible(true);
293        minusButton.setVisible(true);
294    }
295
296    private void drawNextAdviceChange(Graphics2D g2) {
297        if ( nextAdviceChange < 0 ) {
298            return;
299        }
300        g2.setColor(DmiPanel.GREY);
301        int startPx = 282 - calculatePositionOnScale(nextAdviceChange);
302
303        g2.fillRect(147, startPx, 10, 2);
304        g2.fillRect(167, startPx, 10, 2);
305        g2.fillRect(187, startPx, 10, 2);
306        g2.fillRect(207, startPx, 10, 2);
307        g2.fillRect(227, startPx, 10, 2);
308    }
309
310    private void drawIndicationMarkerLine(Graphics2D g2) {
311        if ( indicationDistance < 0 ) {
312            return;
313        }
314        g2.setColor(dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING
315            ? DmiPanel.WHITE : DmiPanel.YELLOW);
316        int startPx = 282 - calculatePositionOnScale(indicationDistance);
317        g2.fillRect(147, startPx, 93, 2);
318    }
319
320    // Calculate the position on the scale for a given length in meters
321    private int calculatePositionOnScale(int lengthInMeters) {
322
323        int linearScaleMaxDistance = (scaleDistanceBase[1]*(scales[currentScale])/5);
324        double logScaleMaxDistance = (scaleDistanceBase[4]*(scales[currentScale]));
325        final int TOTAL_PIXELS = 262;
326        final long FIRST_LINEAR_SCALE_PIXELS = 33;
327        long logScaleWidthInPixels = TOTAL_PIXELS-FIRST_LINEAR_SCALE_PIXELS;
328
329        int position;
330        
331        if ( lengthInMeters <= linearScaleMaxDistance ) {
332            position = (int)((lengthInMeters) /  (float)linearScaleMaxDistance * FIRST_LINEAR_SCALE_PIXELS );
333        } else {
334            // Logarithmic scale for lengths beyond 100 meters
335            double logScaleFactor = logScaleWidthInPixels / (Math.log(logScaleMaxDistance)
336                - Math.log(linearScaleMaxDistance));
337            position = (int)(FIRST_LINEAR_SCALE_PIXELS + (Math.log(lengthInMeters)
338                - Math.log(linearScaleMaxDistance)) * logScaleFactor);
339        }
340        log.debug("at distance {} px: {}",lengthInMeters,position);
341        return position;
342    }
343
344    protected void extendMovementAuthorities( MovementAuthority a ){
345        maList.add(a);
346        repaint();
347    }
348
349    protected void resetMovementAuthorities( final List<MovementAuthority> a ){
350        maList.clear();
351        maList.addAll(a);
352        repaint();
353    }
354
355    /**
356     * Get Unmodifiable List of Movement Authorities.
357     * @return List of movement Authorities.
358     */
359    protected List<MovementAuthority> getMovementAuthorities() {
360        return Collections.unmodifiableList(maList);
361    }
362
363    protected void advance(int distance) {
364        MovementAuthority.advanceForward(maList, distance);
365        nextAdviceChange -= distance;
366        repaint();
367    }
368
369    private void drawGradientBar( Graphics2D g2 ){
370        List<TrackSection> gradientList = MovementAuthority.getTrackSectionList(maList, false);
371        // gradientList.forEach(ts -> log.debug("track section gradient {} length {}", ts.getGradient(), ts.getLength()));
372
373        int drawingMetresFromStart = 0;
374        for ( TrackSection gradientTs: gradientList) {
375
376            int startMetres = drawingMetresFromStart;
377            int endMetres = drawingMetresFromStart + gradientTs.getLength();
378
379            int startPx = calculatePositionOnScale(startMetres);
380            int stopPx = calculatePositionOnScale(endMetres);
381
382            g2.setColor( gradientTs.getGradient() < 0 ? DmiPanel.DARK_GREY : DmiPanel.GREY);
383            g2.fillRect(116, 283-stopPx, 18-1, stopPx-startPx-2);
384
385            g2.setColor( gradientTs.getGradient() < 0 ? DmiPanel.GREY : DmiPanel.WHITE);
386            g2.drawLine(115+1, 283-stopPx, 115+17-1, 283-stopPx);
387            g2.drawLine(115, 283-startPx, 115, 283-stopPx);
388
389            g2.setColor(DmiPanel.BLACK);
390            g2.drawLine(115, 283-startPx-1, 115+17, 283-startPx-1);
391
392            drawIconsOnGradient(g2, gradientTs, startPx, stopPx);
393            drawingMetresFromStart = endMetres;
394        }
395    }
396
397    private void drawIconsOnGradient( Graphics2D g2, TrackSection gradientTs, int startPx, int stopPx ){
398        if (loopGradientLimitReached) {
399            return;
400        }
401
402        int stopHeight = 283-stopPx+12;
403        if ( stopHeight < 0 ){
404            stopHeight = 12;
405            loopGradientLimitReached = true;
406        }
407
408        int usableHeight = stopPx-startPx-4;
409        if ( usableHeight > 14 ) {
410
411            Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 13);
412            g2.setFont(unitsSizedFont);
413            FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont);
414
415            g2.setColor(gradientTs.getGradient()<0 ? DmiPanel.WHITE : DmiPanel.BLACK );
416
417            String toDraw = gradientTs.getGradient()<0 ? "   -   " : "   +   ";
418            int width = 124 - (unitsFontM.stringWidth(toDraw)/2);
419            log.debug("stopHeight:{}",stopHeight);
420            g2.drawString(toDraw,width, stopHeight);
421
422            if (usableHeight> 20 ) {
423                g2.drawString(toDraw,width, 283-startPx-2);
424            }
425
426            if (usableHeight> 30 ) {
427
428                unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 12);
429                g2.setFont(unitsSizedFont);
430                unitsFontM = g2.getFontMetrics(unitsSizedFont);
431
432                toDraw = String.valueOf(gradientTs.getGradient()).replace(String.valueOf("-"), "");
433                width = 124 - (unitsFontM.stringWidth(toDraw)/2);
434                int centre = (startPx + stopPx)/2;
435                int drawHeight = 283-centre+4;
436                g2.drawString(toDraw,width, drawHeight);
437            }
438        }
439    }
440
441    private void setButtonsToState(){
442        plusButton.setEnabled(currentScale != 0);
443        minusButton.setEnabled(currentScale != 5);
444    }
445
446    /**
447     * Set the Scale on the Planning Area.
448     * 0 : 0 - 1000
449     * 1 : 0 - 2000
450     * 2 : 0 - 4000
451     * 3 : 0 - 8000
452     * 4 : 0 - 16000
453     * 5 : 0 - 32000
454     * @param scale the scale to use.
455     */
456    protected void setScale(int scale){
457        currentScale = scale;
458        setButtonsToState();
459        repaint();
460    }
461
462    protected void setTrackAheadFreeQuestionVisible(boolean newVal) {
463        trackAheadFreeQuestion.setVisible(newVal);
464    }
465
466    protected void setNextAdviceChange(int distance) {
467        nextAdviceChange = distance;
468        repaint();
469    }
470
471    // Section 8.3.8
472    protected void setIndicationMarkerLine(int distance, int whichSpeedChange ) {
473        indicationDistance = distance;
474        indicationSpeedChange = whichSpeedChange;
475        repaint();
476    }
477
478    private JPanel trackAheadFreeQuestionPanel() {
479        JPanel p = new JPanel();
480        p.setLayout(null);
481        p.setBounds(0,50,246,50);
482        p.setBackground(DmiPanel.DARK_GREY);
483        JLabel trackAheadFreeQuestionLogo = new JLabel(
484        ResourceUtil.getImageIcon( "DR_02.bmp")
485        );
486
487        trackAheadFreeQuestionLogo.setBounds(0,0,162,50);
488        trackAheadFreeQuestionLogo.setBackground(DmiPanel.DARK_GREY);
489        trackAheadFreeQuestionLogo.setBorder(javax.swing.BorderFactory.createLineBorder(DmiPanel.MEDIUM_GREY, 1));
490
491        p.add(trackAheadFreeQuestionLogo);
492
493        JButton trackAheadFreeButton = new JButton(Bundle.getMessage("ButtonYes"));
494        trackAheadFreeButton.setFocusable(false);
495        trackAheadFreeButton.setBounds(162, 0, 246-162, 50);
496        trackAheadFreeButton.setBackground(DmiPanel.MEDIUM_GREY);
497        trackAheadFreeButton.setForeground(DmiPanel.BLACK);
498        trackAheadFreeButton.setActionCommand(DmiPanel.PROP_CHANGE_TRACK_AHEAD_FREE_TRUE);
499        trackAheadFreeButton.addActionListener(this::buttonClicked);
500
501        p.add(trackAheadFreeButton);
502        return p;
503    }
504
505    void buttonClicked(ActionEvent e){
506        dmiPanel.firePropertyChange(e.getActionCommand(), false, true);
507        setTrackAheadFreeQuestionVisible(false);
508    }
509
510    private class TransparentButton extends JButton {
511
512        private final boolean plus;
513
514        private TransparentButton(boolean plusIcon) {
515            super();
516            setOpaque(false);
517            setBorderPainted(false);
518            setFocusable(false);
519            plus = plusIcon;
520
521            if (plus){
522                setIcon(ResourceUtil.getImageIcon( "NA_03.bmp"));
523                setDisabledIcon(ResourceUtil.getImageIcon( "NA_05.bmp"));
524            } else {
525                setIcon(ResourceUtil.getImageIcon( "NA_04.bmp"));
526                setDisabledIcon(ResourceUtil.getImageIcon( "NA_06.bmp"));
527            }
528
529            setBounds(0, (plus ? 246 : 0 ), 50, 50);
530            addActionListener(this::changeScale);
531        }
532
533        private void changeScale(ActionEvent e){
534            log.debug("scale changed {}", e.paramString());
535            currentScale+= ( plus ? -1 : 1 );
536            setButtonsToState();
537            planningPanel.repaint();
538        }
539
540        @Override
541        protected void paintComponent(Graphics g) {
542            Icon i = ( isEnabled() ? getIcon() : getDisabledIcon());
543            if (i != null) {
544                i.paintIcon(this, g, 15, plus ? 36 : 0);
545            }
546        }
547    }
548
549    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DmiPanelD.class);
550
551}