001package jmri.util;
002
003/*
004 * <hr>
005 * This file is part of JMRI.
006 * <p>
007 * JMRI is free software; you can redistribute it and/or modify it under 
008 * the terms of version 2 of the GNU General Public License as published 
009 * by the Free Software Foundation. See the "COPYING" file for a copy
010 * of this license.
011 * <p>
012 * JMRI is distributed in the hope that it will be useful, but WITHOUT 
013 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
014 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License 
015 * for more details.
016 *
017 * @author Mark Underwood Copyright (C) 2011
018 */
019import java.util.regex.Matcher;
020import java.util.regex.Pattern;
021import java.util.regex.PatternSyntaxException;
022import javax.vecmath.Vector3d;
023import javax.vecmath.Vector3f;
024import jmri.NamedBean;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * PhysicalLocation
030 *
031 * Represents a physical location on the layout in 3D space.
032 *
033 * Dimension units are not specified, but should be kept consistent in all three
034 * dimensions for a given usage.
035 *
036 * Used by VSDecoder for spatially positioning sounds on the layout.
037 *
038 * Could also be used, for example, for velocity calculations between sensors,
039 * or for keying operations locations or panel icons to a physical map view of
040 * the layout.
041 *
042 */
043public class PhysicalLocation extends Vector3f {
044
045    private boolean _isTunnel;
046
047    // Class methods
048    /**
049     * Origin : constant representation of (0, 0, 0)
050     */
051    public static final PhysicalLocation Origin = new PhysicalLocation(0.0f, 0.0f, 0.0f);
052
053    /**
054     * NBPropertyKey : Key name used when storing a PhysicalLocation as a
055     * NamedBean Property
056     */
057    public static final String NBPropertyKey = "physical_location";
058
059    /**
060     * translate()
061     *
062     * Return a PhysicalLocation that represents the position of point "loc"
063     * relative to reference point "ref".
064     *
065     * @param loc : PhysicalLocation to translate
066     * @param ref : PhysicalLocation to use as new reference point (origin)
067     * @return PhysicalLocation
068     */
069    public static PhysicalLocation translate(PhysicalLocation loc, PhysicalLocation ref) {
070        if (loc == null || ref == null) {
071            return (loc);
072        }
073        PhysicalLocation rv = new PhysicalLocation();
074        rv.setX(loc.getX() - ref.getX());
075        rv.setY(loc.getY() - ref.getY());
076        rv.setZ(loc.getZ() - ref.getZ());
077        return (rv);
078    }
079
080    /**
081     * getBeanPhysicalLocation(NamedBean b)
082     *
083     * Extract the PhysicalLocation stored in NamedBean b, and return as a new
084     * PhysicalLocation object.
085     *
086     * If the given NamedBean does not have a PhysicalLocation property returns
087     * Origin. (should probably return null instead, but...)
088     *
089     * @param b : NamedBean
090     * @return PhysicalLocation
091     */
092    public static PhysicalLocation getBeanPhysicalLocation(NamedBean b) {
093        String s = (String) b.getProperty(PhysicalLocation.NBPropertyKey);
094        if ((s == null) || (s.isEmpty())) {
095            return (PhysicalLocation.Origin);
096        } else {
097            return (PhysicalLocation.parse(s));
098        }
099    }
100
101    /**
102     * setBeanPhysicalLocation(PhysicalLocation p, NamedBean b)
103     *
104     * Store PhysicalLocation p as a property in NamedBean b.
105     *
106     * @param p PhysicalLocation
107     * @param b NamedBean
108     */
109    public static void setBeanPhysicalLocation(PhysicalLocation p, NamedBean b) {
110        b.setProperty(PhysicalLocation.NBPropertyKey, p.toString());
111    }
112
113    /**
114     * Get a panel component that can be used to view and/or edit a location.
115     *
116     * @param title the title of the component
117     * @return a new component
118     */
119    static public PhysicalLocationPanel getPanel(String title) {
120        return (new PhysicalLocationPanel(title));
121    }
122
123    /**
124     * Parse a string representation (x,y,z) Returns a new PhysicalLocation
125     * object.
126     *
127     * @param pos : String "(X, Y, Z)"
128     * @return PhysicalLocation
129     */
130    static public PhysicalLocation parse(String pos) {
131        // position is stored as a tuple string "(x,y,z)"
132        // Optional flags come immediately after the (x,y,z) in the form of "(flag)".
133        // Flags are boolean. If they are present, they are true.
134        // Regex [-+]?[0-9]*\.?[0-9]+
135        // String syntax = "\\((\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+)\\)";
136        // String syntax = "\\((\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+),(\\s*[-+]?[0-9]*\\.?[0-9]+)\\)(\\([tunnel]\\))*";
137        String syntax = "\\((\\s*[-+]?[0-9]*\\.?[0-9]+), (\\s*[-+]?[0-9]*\\.?[0-9]+), (\\s*[-+]?[0-9]*\\.?[0-9]+)\\)\\(?([tunnel]*)\\)?";
138        try {
139            Pattern p = Pattern.compile(syntax);
140            Matcher m = p.matcher(pos);
141            if (!m.matches()) {
142                log.error("String does not match a valid position pattern. syntax= {} string = {}", syntax, pos);
143                return (null);
144            }
145            // ++debug
146            String xs = m.group(1);
147            String ys = m.group(2);
148            String zs = m.group(3);
149            log.debug("Loading position: x = {} y = {} z = {}", xs, ys, zs);
150            // --debug
151            boolean is_tunnel = false;
152            // Handle optional flags
153            for (int i = 4; i < m.groupCount() + 1; i++) {
154                if ((m.group(i) != null) && ("tunnel".equals(m.group(i)))) {
155                    is_tunnel = true;
156                }
157            }
158
159            return (new PhysicalLocation(Float.parseFloat(xs),
160                    Float.parseFloat(ys),
161                    Float.parseFloat(zs),
162                    is_tunnel));
163        } catch (PatternSyntaxException e) {
164            log.error("Malformed listener position syntax! {}", syntax);
165            return (null);
166        } catch (IllegalStateException e) {
167            log.error("Group called before match operation executed syntax={} string= {} {}", syntax, pos, e.toString());
168            return (null);
169        } catch (IndexOutOfBoundsException e) {
170            log.error("Index out of bounds {} string= {} {}", syntax, pos, e.toString());
171            return (null);
172        }
173    }
174
175    /**
176     * toString()
177     *
178     * Output a string representation (x,y,z)
179     *
180     * @return String "(X, Y, Z)"
181     */
182    @Override
183    public String toString() {
184        String s = "(" + this.getX() + ", " + this.getY() + ", " + this.getZ() + ")";
185        if (_isTunnel) {
186            s += "(tunnel)";
187        }
188        return (s);
189    }
190
191    public Vector3d toVector3d() {
192        return (new Vector3d(this));
193    }
194
195    // Instance methods
196    /**
197     * Default constructor
198     */
199    public PhysicalLocation() {
200        super();
201        _isTunnel = false;
202    }
203
204    /**
205     * Constructor from Vector3f.
206     *
207     * @param v the vector
208     */
209    public PhysicalLocation(Vector3f v) {
210        super(v);
211        _isTunnel = false;
212    }
213
214    public PhysicalLocation(Vector3d v) {
215        super(v);
216        _isTunnel = false;
217    }
218
219    /**
220     * Constructor from X, Y, Z (float) + is_tunnel (boolean)
221     *
222     * @param x        location on X axis
223     * @param y        location on Y axis
224     * @param z        location on Z axis
225     * @param isTunnel true is location is in a tunnel
226     */
227    public PhysicalLocation(float x, float y, float z, boolean isTunnel) {
228        super(x, y, z);
229        _isTunnel = isTunnel;
230
231    }
232
233    /**
234     * Constructor from X, Y, Z (float)
235     *
236     * @param x location on X axis
237     * @param y location on Y axis
238     * @param z location on Z axis
239     */
240    public PhysicalLocation(float x, float y, float z) {
241        this(x, y, z, false);
242    }
243
244    /**
245     * Constructor from X, Y, Z (double)
246     *
247     * @param x location on X axis
248     * @param y location on Y axis
249     * @param z location on Z axis
250     */
251    public PhysicalLocation(double x, double y, double z) {
252        this(x, y, z, false);
253    }
254
255    /**
256     * Constructor from X, Y, Z (double)
257     *
258     * @param x        location on X axis
259     * @param y        location on Y axis
260     * @param z        location on Z axis
261     * @param isTunnel true if location is in a tunnel
262     */
263    public PhysicalLocation(double x, double y, double z, boolean isTunnel) {
264        this((float) x, (float) y, (float) z, isTunnel);
265    }
266
267    /**
268     * Copy Constructor
269     *
270     * @param p the location to copy
271     */
272    public PhysicalLocation(PhysicalLocation p) {
273        this(p.getX(), p.getY(), p.getZ(), p.isTunnel());
274    }
275
276    public boolean isTunnel() {
277        return (_isTunnel);
278    }
279
280    public void setIsTunnel(boolean t) {
281        _isTunnel = t;
282    }
283
284    @Override
285    public boolean equals(Object o) {
286        if (o == null) {
287            return false;
288        }
289        if (this.getClass().equals(o.getClass())) {
290            if (this.hashCode() == o.hashCode()) {
291                return true;
292            }
293        }
294        return false;
295    }
296
297    @Override
298    public int hashCode() {
299        int hash = 7;
300        hash = 89 * hash + (this._isTunnel ? 1 : 0) + super.hashCode();
301        return hash;
302    }
303
304    /**
305     * translate()
306     *
307     * Translate this PhysicalLocation's coordinates to be relative to point
308     * "ref". NOTE: This is a "permanent" internal translation, and permanently
309     * changes this PhysicalLocation's value.
310     *
311     * If you want a new PhysicalLocation that represents the relative position,
312     * call the class method translate(loc, ref)
313     *
314     * @param ref new reference (origin) point
315     */
316    public void translate(PhysicalLocation ref) {
317        if (ref == null) {
318            return;
319        }
320
321        this.setX(this.getX() - ref.getX());
322        this.setY(this.getY() - ref.getY());
323        this.setZ(this.getZ() - ref.getZ());
324    }
325
326    private static final Logger log = LoggerFactory.getLogger(PhysicalLocation.class);
327}