001package jmri.jmrit.vsdecoder.listener;
002
003import java.util.regex.Matcher;
004import java.util.regex.Pattern;
005import java.util.regex.PatternSyntaxException;
006import javax.vecmath.Vector3d;
007import javax.vecmath.Vector3f;
008import jmri.util.PhysicalLocation;
009import org.jdom2.Element;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Represents a defined spot for viewing (and therefore listening to) a layout.
015 *
016 * <hr>
017 * This file is part of JMRI.
018 * <p>
019 * JMRI is free software; you can redistribute it and/or modify it under
020 * the terms of version 2 of the GNU General Public License as published
021 * by the Free Software Foundation. See the "COPYING" file for a copy
022 * of this license.
023 * <p>
024 * JMRI is distributed in the hope that it will be useful, but WITHOUT
025 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
026 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
027 * for more details.
028 *
029 * @author Mark Underwood Copyright (C) 2012
030 */
031public class ListeningSpot {
032
033    private Vector3d _location;
034    private Vector3d _up;
035    private Vector3d _lookAt;
036    private String _name;
037
038    private static final Vector3d _atVector = new Vector3d(0.0d, 1.0d, 0.0d);
039    private static final Vector3d _upVector = new Vector3d(0.0d, 0.0d, 1.0d);
040
041    public ListeningSpot() {
042        _name = null;
043        _location = new Vector3d();
044        _up = _upVector;
045        _lookAt = _atVector;
046    }
047
048    public ListeningSpot(Vector3f position) {
049        _name = null;
050        _location = new Vector3d(position);
051        _up = _upVector;
052        _lookAt = _atVector;
053    }
054
055    public ListeningSpot(Vector3d position) {
056        this(null, position);
057    }
058
059    public ListeningSpot(String name, Vector3d position) {
060        _name = name;
061        _location = position;
062        _lookAt = _atVector;
063        _up = _upVector;
064    }
065
066    public ListeningSpot(String name, Vector3d loc, Vector3d up, Vector3d at) {
067        _name = name;
068        _location = loc;
069        _up = up;
070        _lookAt = at;
071    }
072
073    public ListeningSpot(Element e) {
074        this.setXml(e);
075    }
076
077    public String getName() {
078        return _name;
079    }
080
081    public Vector3d getLocation() {
082        return _location;
083    }
084
085    public PhysicalLocation getPhysicalLocation() {
086        return (new PhysicalLocation(_location.x, _location.y, _location.z));
087    }
088
089    public Vector3d getUpVector() {
090        return _up;
091    }
092
093    public Vector3d getLookAtVector() {
094        return _lookAt;
095    }
096
097    /* TRig notes
098     * Trig x = map y
099     * Trig y = map x
100     * bearing = theta
101     * azimuth = 90 - rho
102     * map y = r sin (90-azimuth) cos bearing
103     * map x = r sin (90-azimuth) sin bearing
104     * map z = r cos (90-azimuth)
105     * r = sqrt( x^2 + y^2 + z^2 )
106     * bearing = theta = atan(map x / map y)
107     * azimuth = 90 - rho = 90 - acos(z / r)
108     */
109    public Double getBearing() {
110        // bearing = theta = atan(map x / map y)
111        Vector3d lav= getLookAtVector();
112        Double b = Math.toDegrees(Math.atan(lav.x / lav.y));
113
114        // lookAt point is behind listener
115        if (lav.y < 0.0d) {
116            b = b + 180.0d;
117        } else if (b < 0.0d) {
118            b = b + 360.0d;
119        }
120        return b;
121    }
122
123    public Double getAzimuth() {
124        // r = sqrt( x^2 + y^2 + z^2 )
125        // azimuth = 90 - rho = 90 - acos(z / r)
126        Vector3d lav = getLookAtVector();
127        Double r = Math.sqrt(lav.x * lav.x + lav.y * lav.y + lav.z * lav.z);
128        return 90 - Math.toDegrees(Math.acos(lav.z / r));
129    }
130
131    public void setName(String n) {
132        _name = n;
133    }
134
135    public void setLocation(Vector3d loc) {
136        _location = loc;
137    }
138
139    public void setLocation(Double x, Double y, Double z) {
140        if (x == null) {
141            x = 0.0d;
142        } else {
143            x = checkLimits(x);
144            x = roundDecimal(x);
145        }
146        if (y == null) {
147            y = 0.0d;
148        } else {
149            y = checkLimits(y);
150            y = roundDecimal(y);
151        }
152        if (z == null) {
153            z = 0.0d;
154        } else {
155            z = checkLimits(z);
156            z = roundDecimal(z);
157        }
158        _location = new Vector3d(x, y, z);
159    }
160
161    public void setLocation(PhysicalLocation l) {
162        _location = new Vector3d(l.getX(), l.getY(), l.getZ());
163    }
164
165    public void setUpVector(Vector3d up) {
166        _up = up;
167    }
168
169    public void setLookAtVector(Vector3d at) {
170        _lookAt = at;
171    }
172
173    public void setOrientation(PhysicalLocation target) {
174        Vector3d la = new Vector3d();
175        // Calculate the look-at vector
176        la.sub(target.toVector3d(), _location);  // la = target - location
177        la.normalize();
178        _lookAt = la;
179        // Calculate the up vector
180        _up = calcUpFromLookAt(la);
181    }
182
183    private Vector3d calcUpFromLookAt(Vector3d la) {
184        Vector3d _la = la;
185        _la.normalize();
186        Vector3d up = new Vector3d();
187        up.cross(_la, _upVector);
188        up.cross(up, _la);
189        up.normalize();
190        return up;
191    }
192
193    public void setOrientation(Double bearing, Double azimuth) {
194        // Convert bearing + azimuth to look-at and up vectors.
195        // Bearing measured clockwise from Y axis.
196        // Azimuth measured up (or down) from X/Y plane.
197        // map y = r sin (90-azimuth) cos bearing
198        // map x = r sin (90-azimuth) sin bearing
199        // map z = r cos (90-azimuth)
200        // Assumes r = 1;
201        if (bearing == null) {
202            bearing = 0.0d;
203        }
204
205        if (azimuth == null) {
206            azimuth = 0.0d;
207        }
208
209        if (azimuth > 90.0d) {
210            azimuth = 180.0d - azimuth;
211        } else if (azimuth < -90.0d) {
212           azimuth = -180.0d - azimuth;
213        }
214
215        double y = Math.sin(Math.toRadians(90 - azimuth)) * Math.cos(Math.toRadians(bearing)); 
216        double x = Math.sin(Math.toRadians(90 - azimuth)) * Math.sin(Math.toRadians(bearing));
217        double z = Math.cos(Math.toRadians(90 - azimuth));
218        _lookAt = new Vector3d(x, y, z);
219        _up = calcUpFromLookAt(_lookAt);
220 
221        _lookAt.x = roundDecimal(_lookAt.x);
222        _lookAt.y = roundDecimal(_lookAt.y);
223        _lookAt.z = roundDecimal(_lookAt.z);
224
225        _up.x = roundDecimal(_up.x);
226        _up.y = roundDecimal(_up.y);
227        _up.z = roundDecimal(_up.z);
228    }
229
230    public Boolean equals(ListeningSpot other) {
231        if ((this._name.equals(other.getName()))
232                && (this._location == other.getLocation())
233                && (this._up == other.getUpVector())
234                && (this._lookAt == other.getLookAtVector())) {
235            return true;
236        } else {
237            return false;
238        }
239    }
240
241    private Vector3d parseVector3d(String pos) {
242        if (pos == null) {
243            return null;
244        }
245
246        // position is stored as a tuple string "(x,y,z)"
247        // Regex [-+]?[0-9]*\.?[0-9]+
248        String syntax = "\\((\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+)\\)";
249        try {
250            Pattern p = Pattern.compile(syntax);
251            Matcher m = p.matcher(pos);
252            if (!m.matches()) {
253                log.error("String does not match a valid position pattern. syntax: {}, string: {}", syntax, pos);
254                return null;
255            }
256            // ++debug
257            String xs = m.group(1);
258            String ys = m.group(2);
259            String zs = m.group(3);
260            log.debug("Loading Vector3d: x = {} y = {} z = {}", xs, ys, zs);
261            // --debug
262            return (new Vector3d(Double.parseDouble(m.group(1)), Double.parseDouble(m.group(2)), Double.parseDouble(m.group(3))));
263        } catch (PatternSyntaxException e) {
264            log.error("Malformed Vector3d syntax! {}", syntax);
265            return null;
266        } catch (IllegalStateException e) {
267            log.error("Group called before match operation executed syntax: {}, string: {}, {}", syntax, pos, e.toString());
268            return null;
269        } catch (IndexOutOfBoundsException e) {
270            log.error("Index out of bounds: {}, string: {}, {}", syntax, pos, e.toString());
271            return null;
272        }
273    }
274
275    @Override
276    public String toString() {
277        if ((_location == null) || (_lookAt == null) || (_up == null)) {
278            return "ListeningSpot (undefined)";
279        } else {
280            return ("ListeningSpot Name: " + _name + " Location: " + _location.toString() + " LookAt: " + _lookAt.toString() + " Up: " + _up.toString());
281        }
282    }
283
284    public Element getXml(String elementName) {
285        Element me = new Element(elementName);
286        me.setAttribute("name", (_name == null ? "default" : _name));
287        me.setAttribute("location", _location.toString());
288        me.setAttribute("up", _up.toString());
289        me.setAttribute("look_at", _lookAt.toString());
290        return me;
291    }
292
293    public void setXml(Element e) {
294        if (e != null) {
295            log.debug("ListeningSpot: {}", e.getAttributeValue("name"));
296            _name = e.getAttributeValue("name");
297            _location = parseVector3d(e.getAttributeValue("location"));
298            _up = parseVector3d(e.getAttributeValue("up"));
299            _lookAt = parseVector3d(e.getAttributeValue("look_at"));
300        }
301    }
302
303    private static double roundDecimal(double value) {
304        return (double) Math.round(value * 100) / 100;
305    }
306
307    private static final double MAX_DIST = 9999.99d;
308
309    private static double checkLimits(double value) {
310        value = Math.max(-MAX_DIST, value);
311        value = Math.min(MAX_DIST, value);
312        return value;
313    }
314
315    private static final Logger log = LoggerFactory.getLogger(ListeningSpot.class);
316
317}