001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso.gui;
020
021import org.jpos.iso.ISOMsg;
022
023import javax.swing.*;
024import java.awt.*;
025import java.awt.event.MouseAdapter;
026import java.awt.event.MouseEvent;
027import java.awt.event.MouseListener;
028
029/**
030 * ISOMsgPanel
031 * Swing based GUI to ISOMsg
032 * @author apr@cs.com.uy
033 * @author Kris Leite (kleite at imcsoftware.com)
034 * @see org.jpos.iso.ISOMsg
035 */
036@SuppressWarnings("deprecation")
037public class ISOMeter extends JComponent implements Runnable {
038
039    private static final long serialVersionUID = -1770533267122111538L;
040    /** Background color of the meter.
041     * @serial
042     */
043    Color color = new Color (255, 255, 255);
044    /** Off-screen image buffer.
045     * @serial
046     */
047    Image im;
048    /** Graphics context for the off-screen buffer.
049     * @serial
050     */
051    Graphics img;
052    /** Large and small fonts used for text rendering.
053     * @serial
054     */
055    Font fontBig, fontSmall;
056    /** Label shown for positive (inbound) activity.
057     * @serial
058     */
059    String positiveText;
060    /** Label shown for negative (outbound) activity.
061     * @serial
062     */
063    String negativeText;
064    /** Swing timer controlling periodic refresh.
065     * @serial
066     */
067    Timer ti;
068    /** Positive counter string (managed externally to reduce int-to-String conversions).
069     * @serial
070     */
071    String positiveCounter;
072    /** Negative counter string.
073     * @serial
074     */
075    String negativeCounter;
076    /** Age counter for the positive text label.
077     * @serial
078     */
079    int lastPositive;
080    /** Age counter for the negative text label.
081     * @serial
082     */
083    int lastNegative;
084    /** Whether the channel is currently connected.
085     * @serial
086     */
087    boolean connected;
088    /** Parent channel panel that owns this meter.
089     * @serial
090     */
091    ISOChannelPanel parent;
092
093    final static int width    = 200;
094    final static int height   = 60;
095    final static int mass     = height/2;
096    final static int MAX_VALUE = 1000;
097
098    /** Y-coordinate samples for the waveform plot.
099     * @serial
100     */
101    int[] yPoints;
102    /** X-coordinate positions for the waveform plot.
103     * @serial
104     */
105    int[] xPoints;
106    /**
107     * counter to keep the scrolling active
108     */
109    int continueScroll;
110    /**
111     * used to determine if to scroll mark to end of graph
112     */
113    boolean scroll = true;
114    /**
115     * Refresh panel in millseconds
116     */
117    int refreshPanel = 50;
118
119    /** Off-screen image buffer used during repaint operations. */
120    private Image imb;
121    /** Background thread that advances the animated meter display. */
122    private Thread repaintThread;
123
124    /** Constructs an ISOMeter for the given channel panel.
125     * @param parent the parent ISOChannelPanel
126     */
127    public ISOMeter(ISOChannelPanel parent) {
128        super();
129        this.parent = parent;
130
131        fontBig   = new Font ("Helvetica", Font.ITALIC, mass*3/4);
132        fontSmall = new Font ("Helvetica", Font.PLAIN, 10);
133        yPoints = new int[width];
134        xPoints = new int[width];
135        for (int i=0; i<width; i++) {
136            xPoints[i] = i;
137            yPoints[i] = mass;
138        }
139        positiveText  = null;
140        negativeText  = null;
141        positiveCounter = negativeCounter = "";
142        connected     = false;
143
144        MouseListener mouseListener = new MouseAdapter() {
145            public void mouseClicked(MouseEvent e) {
146                showLogList();
147            }
148        };
149        addMouseListener(mouseListener);
150    }
151
152    /** Starts the meter update thread. */
153    public synchronized void start() {
154        if (repaintThread == null) {
155            repaintThread = new Thread (this,"ISOMeter");
156            repaintThread.setPriority (Thread.NORM_PRIORITY-1);
157            repaintThread.start();
158        }
159    }
160
161    /** Shows the log list panel. */
162    public void showLogList() {
163        JFrame f = new JFrame(parent.getSymbolicName());
164        f.getContentPane().add(createLogList());
165        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
166        f.validate();
167        f.pack();
168        f.setSize(width,width+50);
169        f.show();
170    }
171
172    /** Creates and returns the log list component.
173     * @return the log list JComponent
174     */
175    public JComponent createLogList() {
176        final JList logList = new JList(parent.getLog());
177        JPanel A = new JPanel();
178        A.setLayout(new BorderLayout());
179
180        MouseListener mouseListener = new MouseAdapter() {
181            public void mouseClicked(MouseEvent e) {
182                ISOMsg m = (ISOMsg) logList.getSelectedValue();
183                if (m != null) {
184                    JFrame f = new JFrame(
185                        parent.getSymbolicName()+":"+m.toString());
186                    ISOMsgPanel p = new ISOMsgPanel(m);
187                    f.getContentPane().add(p);
188                    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
189                    f.pack();
190                    f.show();
191                }
192            }
193        };
194        logList.addMouseListener(mouseListener);
195
196        logList.setPrototypeCellValue("9999 99999999 999999");
197        JScrollPane scrollPane = new JScrollPane(logList);
198        A.add(scrollPane, BorderLayout.CENTER);
199        return A;
200    }
201
202    /** Sets the current meter value.
203     * @param val the value to display
204     */
205    public void setValue(int val) {
206        int y = mass - val%1000 * height / 2000;
207        yPoints[width-1] = y;
208        continueScroll = width;
209        scroll();
210    }
211
212    /** Sets whether the log list auto-scrolls.
213     * @param scroll if true, auto-scroll the log
214     */
215    public void setScroll (boolean scroll) {
216        this.scroll = scroll;
217    }
218    /** Sets the refresh interval in milliseconds.
219     * @param refreshPanel the refresh interval
220     */
221    public void setRefresh (int refreshPanel) {
222        if (refreshPanel > 0)
223            this.refreshPanel = refreshPanel;
224    }
225    /** Sets whether the channel is connected.
226     * @param connected true if connected
227     */
228    public void setConnected(boolean connected) {
229        if (this.connected != connected) {
230            if (!scroll)
231                if (connected)
232                    continueScroll = width;
233                else
234                    continueScroll = 1;
235            repaint();
236        }
237        this.connected = connected;
238    }
239    /** Sets the positive counter display string.
240     * @param s the counter string
241     */
242    public void setPositiveCounter(String s) {
243        positiveCounter = s;
244    }
245    /** Sets the negative counter display string.
246     * @param s the counter string
247     */
248    public void setNegativeCounter(String s){
249        negativeCounter = s;
250    }
251    /** Sets the meter value with a text label.
252     * @param val        the numeric value
253     * @param textString the display text
254     */
255    public void setValue(int val, String textString) {
256        setValue(val);
257        if (val < 0) {
258            negativeText = textString;
259            lastNegative = 0;
260        }
261        else {
262            positiveText = textString;
263            lastPositive = 0;
264        }
265    }
266    /** Paints the meter component by rendering the off-screen buffer.
267     * @param g the graphics context
268     */
269    public void paint (Graphics g) {
270        if (repaintThread == null)
271            start();
272        plot();
273        g.drawImage (im, 0, 0, null);
274    }
275    /** Returns the preferred size of this meter component.
276     * @return preferred dimensions
277     */
278    public Dimension getPreferredSize() {
279        return new Dimension(width, height);
280    }
281    private void scroll() {
282        System.arraycopy(yPoints, 1, yPoints, 0, width - 1);
283        if (continueScroll > 0)
284            continueScroll--;
285    }
286    /** Renders the meter display. */
287    public void plot() {
288       if (im == null) {
289            im = createImage(width, height);
290            img = im.getGraphics ();
291            img.setColor (Color.black);
292            img.fillRoundRect (0, 0, width, height, 10, 10);
293            img.clipRect (0, 0, width, height);
294            plotGrid();
295
296            /* save a copy of the image */
297            imb = createImage(width, height);
298            Graphics imbCopy = imb.getGraphics();
299            imbCopy.drawImage (im, 0, 0, this);
300        }
301        img.drawImage (imb, 0, 0, this);
302        if (continueScroll > 0)
303            scroll();
304        plotText(positiveText, lastPositive++, 3, mass-3);
305        plotText(negativeText, lastNegative++, 3, height-3);
306        plotCounters(positiveCounter, negativeCounter);
307        img.setColor (connected ? Color.green : Color.red);
308        img.drawPolyline(xPoints, yPoints, width);
309    }
310    private void plotGrid() {
311        img.setColor(Color.blue);
312        for (int i=0; i<width; i++)
313            if (i % 20 == 0) 
314                img.drawLine(i,0,i,height);
315        for (int i=-1000; i<1000; i+= 200) {
316            int y = mass + i*height/2000;
317            img.drawLine(0,y,width,y);
318        }
319    }
320    private void plotText(String t, int l, int x, int y) {
321        if (t != null && l < 20) {
322            img.setColor(Color.lightGray);
323            img.setFont(fontBig);
324            img.drawString (t, x, y);
325        }
326    }
327    private void plotCounters(String p, String n) {
328        img.setColor(Color.lightGray);
329        img.setFont(fontSmall);
330        img.drawString (p, width-55, 13);
331        img.drawString (n, width-55, height-3);
332    }
333    /** Repaint loop run by the background thread; triggers repaints at the configured interval. */
334    public void run () {
335        while (isShowing()) {
336            if (continueScroll > 0)
337                repaint();
338            try { 
339                Thread.sleep(refreshPanel);
340            } catch (InterruptedException e) {
341                // OK to ignore
342            }
343        }
344        repaintThread = null;
345    }
346    /** Updates the component by delegating to {@link #paint(Graphics)}.
347     * @param g the graphics context
348     */
349    public void update (Graphics g) {
350        paint (g);
351    }
352}