001package jmri.jmrix.rps.trackingpanel;
002
003import java.awt.BasicStroke;
004import java.awt.Color;
005import java.awt.Graphics;
006import java.awt.Graphics2D;
007import java.awt.Point;
008import java.awt.Shape;
009import java.awt.Stroke;
010import java.awt.event.MouseEvent;
011import java.awt.geom.AffineTransform;
012import java.awt.geom.Ellipse2D;
013import java.awt.geom.Line2D;
014import java.awt.geom.Point2D;
015import java.util.ArrayList;
016import java.util.List;
017import javax.vecmath.Point3d;
018import jmri.jmrix.rps.Distributor;
019import jmri.jmrix.rps.Engine;
020import jmri.jmrix.rps.Measurement;
021import jmri.jmrix.rps.MeasurementListener;
022import jmri.jmrix.rps.Model;
023import jmri.jmrix.rps.Receiver;
024import jmri.jmrix.rps.Region;
025import jmri.jmrix.rps.RpsSystemConnectionMemo;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 *
031 * Pane to show a 2D representation of the RPS Model and Measurements.
032 *
033 * @see jmri.jmrix.rps.Model
034 * @see jmri.jmrix.rps.Measurement
035 *
036 * @author Bob Jacobsen Copyright (C) 2006, 2008
037 */
038public class RpsTrackingPanel extends javax.swing.JPanel
039        implements MeasurementListener {
040
041    RpsSystemConnectionMemo memo = null;
042
043    public RpsTrackingPanel(RpsSystemConnectionMemo _memo) {
044        super();
045        memo = _memo;
046        Distributor.instance().addMeasurementListener(this);
047        setToolTipText("<no item>");  // activates ToolTip, sets default
048    }
049
050    public void dispose() {
051        Distributor.instance().removeMeasurementListener(this);
052    }
053
054    /**
055     * Provide tool tip text that depends on what's under the cursor.
056     * <p>
057     * Names either a measurement point or a region.
058     *
059     * @return null if no object under mouse; this suppresses ToolTip
060     */
061    @Override
062    public String getToolTipText(MouseEvent e) {
063        // get mouse coordinates
064        try {
065            Point mouse = e.getPoint();
066            Point2D userPt = currentAT.inverseTransform(new Point2D.Double(mouse.x, mouse.y), null);
067            // find the path object containing it, if any
068            for (int i = measurementRepList.size() - 1; i >= 0; i--) {
069                MeasurementRep r = measurementRepList.get(i);
070                if (r.contains(userPt)) {
071                    Measurement m = r.measurement;
072                    return "ID " + m.getId() + " at " + m.getX() + "," + m.getY();
073                }
074            }
075
076            // find the region containing it, if any
077            // Go through backwards to find the top if overlaps
078            List<Region> l = Model.instance().getRegions();
079            for (int i = l.size() - 1; i >= 0; i--) {
080                Shape s = l.get(i).getPath();
081                if (s.contains(userPt)) {
082                    return "Region: " + l.get(i).toString() + ", at " + userPt.getX() + "," + userPt.getY();
083                }
084            }
085            // found nothing, just display location
086            return "" + userPt.getX() + "," + userPt.getY();
087        } catch (Exception ex) {
088        } // just skip to default
089        // or return default
090        return null;
091    }
092
093    /**
094     * Sets the coordinates of the lower left corner of the screen/paper.
095     * Note this is different from the usual Swing coordinate system!
096     * @param x distance from left.
097     * @param y distance from bottom.
098     */
099    public void setOrigin(double x, double y) {
100        xorigin = x;
101        yorigin = y;
102    }
103
104    void setShowErrors(boolean show) {
105        this.showErrors = show;
106    }
107
108    void setShowReceivers(boolean show) {
109        this.showReceivers = show;
110    }
111
112    void setShowRegions(boolean show) {
113        this.showRegions = show;
114    }
115
116    boolean showErrors = false;
117    boolean showReceivers = false;
118    boolean showRegions = false;
119
120    /**
121     * Sets the coordinates of the upper-right corner of the screen/paper.
122     * Note this is different from the usual Swing coordinate system!
123     * @param x distance from right.
124     * @param y distance from top.
125     */
126    public void setCoordMax(double x, double y) {
127        xmax = x;
128        ymax = y;
129    }
130
131    double xorigin, yorigin;
132    double xmax, ymax;
133
134    static final double MEASUREMENT_ACCURACY = 0.2; // in user units
135    static final double RECEIVER_SIZE = 0.75; // in user units
136    static final Color regionFillColor = Color.GRAY.brighter();
137    static final Color regionOutlineColor = Color.GRAY.darker();
138    // static final Color measurementColor = new Color(0,0,0);
139    int measurementColor = 0;
140
141    // current transform to graphics coordinates
142    AffineTransform currentAT;
143
144    @Override
145    public void paint(Graphics g) {
146        // draw everything else
147        super.paint(g);
148        log.debug("paint invoked");
149
150        // Now show regions
151        // First, Graphics2D setup
152        Graphics2D g2 = (Graphics2D) g;
153        double xscale = this.getWidth() / (xmax - xorigin);
154        double yscale = this.getHeight() / (ymax - yorigin);
155        Stroke stroke = new BasicStroke((float) (2. / xscale),
156                BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
157        g2.setStroke(stroke);
158        // Save the current transform
159        AffineTransform saveAT = g2.getTransform();
160        // Install the new one
161        currentAT = new AffineTransform();
162        currentAT.translate(0, this.getHeight());
163        currentAT.scale(xscale, -yscale);
164        currentAT.translate(-xorigin, -yorigin);  // put origin in bottom corner
165        g2.setTransform(currentAT);
166
167        if (showRegions) {
168            // Draw the regions
169            List<Region> l = Model.instance().getRegions();
170            for (int i = 0; i < l.size(); i++) {
171                g2.setPaint(regionOutlineColor);
172                g2.draw(l.get(i).getPath()); // border (same color)
173                g2.setPaint(regionFillColor);
174                g2.fill(l.get(i).getPath());
175            }
176        }
177
178        // Draw the measurements; changes graphics
179        for (int i = 0; i < measurementRepList.size(); i++) {
180            measurementRepList.get(i).draw(g2);
181        }
182        if (showReceivers) { // draw receivers
183            for (int i = 1; i < Engine.instance().getMaxReceiverNumber() + 1; i++) {  // indexed from 1
184                Receiver r = Engine.instance().getReceiver(i);
185                Point3d p = Engine.instance().getReceiverPosition(i);
186                if (p != null && r != null) {
187                    if (r.isActive()) {
188                        g2.setPaint(Color.BLACK);
189                    } else {
190                        g2.setPaint(Color.GRAY);
191                    }
192
193                    Shape s = new Ellipse2D.Double(p.x - RECEIVER_SIZE / 2,
194                            p.y - RECEIVER_SIZE / 2,
195                            RECEIVER_SIZE, RECEIVER_SIZE);
196                    g2.draw(s);
197                    g2.fill(s);
198                }
199            }
200        }
201        // restore original transform
202        g2.setTransform(saveAT);
203    }
204
205    ArrayList<MeasurementRep> measurementRepList = new ArrayList<MeasurementRep>();
206    java.util.HashMap<String, TransmitterStatus> transmitters = new java.util.HashMap<String, TransmitterStatus>(); // TransmitterStatus, keyed by Integer(measurement id)
207
208    /**
209     * Pick a color for the next set of measurement lines to draw
210     * @return Color for next line chosen via algorithm
211     */
212    Color nextColor() {
213        int red = Math.min(255, ((measurementColor >> 2) & 0x1) * 255 / 1);
214        int green = Math.min(255, ((measurementColor >> 1) & 0x1) * 255 / 1);
215        int blue = Math.min(255, ((measurementColor >> 0) & 0x1) * 255 / 1);
216        measurementColor++;
217        return new Color(red, green, blue);
218    }
219
220    @Override
221    public void notify(Measurement m) {
222        String id = m.getId();
223        TransmitterStatus transmitter = transmitters.get(id);
224        double xend = m.getX();
225        double yend = m.getY();
226        log.debug("notify {},{}", xend, yend);
227        if (transmitter == null) {
228            // create Transmitter status with current measurement
229            // so we can draw line next time
230            log.debug("create new TransmitterStatus for {}", m.getId());
231            transmitter = new TransmitterStatus();
232            transmitter.measurement = m;
233            transmitter.color = nextColor();
234
235            transmitters.put(id, transmitter);
236
237            // display just the point
238            MeasurementRep r = new MeasurementRep();
239            r.color = transmitter.color;
240            r.rep1 = new Ellipse2D.Double(xend - MEASUREMENT_ACCURACY / 2,
241                    yend - MEASUREMENT_ACCURACY / 2,
242                    MEASUREMENT_ACCURACY, MEASUREMENT_ACCURACY);
243            r.measurement = m;
244            measurementRepList.add(r);
245            pruneMeasurementRepList();
246
247            return;
248        }
249        Measurement lastMessage = transmitter.measurement;
250
251        MeasurementRep r = new MeasurementRep();
252        r.color = transmitter.color;
253        r.rep2 = new Ellipse2D.Double(xend - MEASUREMENT_ACCURACY / 2,
254                yend - MEASUREMENT_ACCURACY / 2,
255                MEASUREMENT_ACCURACY, MEASUREMENT_ACCURACY);
256
257        if (showErrors || (lastMessage.isOkPoint() && m.isOkPoint())) {
258            // also draw line
259            double xinit = lastMessage.getX();
260            double yinit = lastMessage.getY();
261            r.rep1 = new Line2D.Double(xinit, yinit, xend, yend);
262            r.measurement = m;
263            measurementRepList.add(r);
264            pruneMeasurementRepList();
265            // cause repaint of whole thing for now
266            repaint(getBounds());
267        }
268        // remember where now
269        transmitter.measurement = m;
270    }
271
272    static final int MAXREPLISTSIZE = 1000;
273
274    void pruneMeasurementRepList() {
275        while (measurementRepList.size() > MAXREPLISTSIZE) {
276            measurementRepList.remove(0);
277        }
278    }
279
280    /**
281     * Clear the measurement history
282     */
283    void clear() {
284        measurementRepList = new ArrayList<MeasurementRep>();
285        repaint(getBounds());
286    }
287
288    /**
289     * Simple tuple class for storing information about a single transmitter
290     * being tracked
291     */
292    static class TransmitterStatus {
293
294        Color color;
295        Measurement measurement;  // last seen location
296    }
297
298    /**
299     * Store draw representation of a measurement (set)
300     */
301    static class MeasurementRep {
302
303        void draw(Graphics2D g2) {
304            g2.setPaint(color);
305            g2.draw(rep1);
306            if (rep2 != null) {
307                g2.draw(rep2);
308            }
309        }
310
311        boolean contains(Point2D pt) {
312            if (rep1.contains(pt)) {
313                return true;
314            }
315            if (rep2 != null && rep2.contains(pt)) {
316                return true;
317            }
318            return false;
319        }
320        Color color;
321        Shape rep1;
322        Shape rep2;
323        Measurement measurement;
324    }
325
326    private final static Logger log = LoggerFactory.getLogger(RpsTrackingPanel.class);
327
328}