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}