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