1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.uwb.correction.math;
17 
18 import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI;
19 import static com.android.server.uwb.correction.math.MathHelper.F_PI;
20 
21 import static java.lang.Math.abs;
22 import static java.lang.Math.acos;
23 import static java.lang.Math.asin;
24 import static java.lang.Math.atan2;
25 import static java.lang.Math.cos;
26 import static java.lang.Math.max;
27 import static java.lang.Math.min;
28 import static java.lang.Math.signum;
29 import static java.lang.Math.sin;
30 import static java.lang.Math.sqrt;
31 import static java.lang.Math.toDegrees;
32 import static java.lang.Math.toRadians;
33 
34 import androidx.annotation.NonNull;
35 
36 import com.android.internal.annotations.Immutable;
37 
38 import java.util.Locale;
39 import java.util.Objects;
40 
41 /**
42  * Represents a point in space represented as distance, azimuth and elevation.
43  * This uses OpenGL's right-handed coordinate system, where the origin is facing in the
44  * -Z direction. Increasing azimuth rotates around Y and increases X.  Increasing
45  * elevation rotates around X and increases Y.
46  * This class is invariant.
47  */
48 @Immutable
49 public class SphericalVector {
50     // If true, negative distances will be converted to a positive distance facing the opposite
51     //  direction.
52     private static final boolean NORMALIZE_NEGATIVE_DISTANCE = false;
53     public final float distance;
54     public final float azimuth;
55     public final float elevation;
56 
57     /**
58      * Creates a SphericalVector from the azimuth, elevation and distance of a viewpoint that is
59      * facing into the -Z axis.
60      *
61      * @param azimuth   The angle along the X axis, around the Y axis.
62      * @param elevation The angle along the Y axis, around the X axis.
63      * @param distance  The distance to the origin.
64      */
SphericalVector(float azimuth, float elevation, float distance)65     private SphericalVector(float azimuth, float elevation, float distance) {
66         elevation = MathHelper.normalizeRadians(elevation);
67         float ae = abs(elevation);
68         if (ae > F_HALF_PI) {
69             // Normalize elevation to be only +/-90 - if it's outside that, mirror and bound the
70             // elevation and flip the azimuth.
71             elevation = (F_PI - ae) * signum(elevation);
72             azimuth += F_PI;
73         }
74         if (NORMALIZE_NEGATIVE_DISTANCE && distance < 0) {
75             // Negative distance is equivalent to a flipped elevation and azimuth.
76             azimuth += F_PI; // turn 180deg.
77             elevation = -elevation; // Mirror top-to-bottom
78             distance = -distance;
79         }
80         azimuth = MathHelper.normalizeRadians(azimuth);
81 
82         this.distance = distance;
83         this.azimuth = azimuth;
84         this.elevation = elevation;
85     }
86 
87     /**
88      * Creates an SphericalVector from azimuth and elevation in radians.
89      *
90      * @param azimuth   The azimuth in degrees.
91      * @param elevation The elevation in degrees.
92      * @param distance  The distance in meters.
93      * @return A new SphericalVector.
94      */
95     @NonNull
fromRadians(float azimuth, float elevation, float distance)96     public static SphericalVector fromRadians(float azimuth, float elevation, float distance) {
97         return new SphericalVector(azimuth, elevation, distance);
98     }
99 
100     /**
101      * Creates an SphericalVector from azimuth and elevation in radians.
102      *
103      * @param azimuth   The azimuth in radians.
104      * @param elevation The elevation in radians.
105      * @param distance  The distance in meters.
106      * @return A new SphericalVector.
107      */
108     @NonNull
fromDegrees(float azimuth, float elevation, float distance)109     public static SphericalVector fromDegrees(float azimuth, float elevation, float distance) {
110         return new SphericalVector(
111                 (float) toRadians(azimuth),
112                 (float) toRadians(elevation),
113                 distance);
114     }
115 
116     /**
117      * Produces a SphericalVector from a cartesian vector, converting X, Y and Z values to
118      * azimuth, elevation and distance.
119      *
120      * @param position The cartesian representation to convert.
121      * @return An equivalent spherical vector representation.
122      */
123     @NonNull
fromCartesian(@onNull Vector3 position)124     public static SphericalVector fromCartesian(@NonNull Vector3 position) {
125         Objects.requireNonNull(position);
126         return fromCartesian(position.x, position.y, position.z);
127     }
128 
129     /**
130      * Produces a spherical vector from a cartesian vector, converting X, Y and Z values to
131      * azimuth, elevation and distance.
132      *
133      * @param x The cartesian x-coordinate to convert.
134      * @param y The cartesian y-coordinate to convert.
135      * @param z The cartesian z-coordinate to convert.
136      * @return An equivalent spherical vector representation.
137      */
138     @NonNull
fromCartesian(float x, float y, float z)139     public static SphericalVector fromCartesian(float x, float y, float z) {
140         float d = (float) sqrt(x * x + y * y + z * z);
141         if (d == 0) {
142             return new SphericalVector(0, 0, 0);
143         }
144         float azimuth = (float) atan2(x, -z);
145         float elevation = (float) asin(min(max(y / d, -1), 1));
146         return new SphericalVector(azimuth, elevation, d);
147     }
148 
149     /**
150      * Converts an AoAVector to a SphericalVector.
151      *
152      * @param vec The AoAVector to convert.
153      * @return An equivalent SphericalVector.
154      */
fromAoAVector(AoaVector vec)155     public static SphericalVector fromAoAVector(AoaVector vec) {
156         float azimuth = vec.azimuth;
157         boolean mirrored = abs(azimuth) > F_HALF_PI;
158         if (mirrored) {
159             azimuth = F_PI - azimuth;
160         }
161         double ca = cos(azimuth);
162         double se = sin(vec.elevation);
163         double azz = sqrt(max(ca * ca - se * se, 0)) / cos(vec.elevation);
164         double az = acos(min(max(azz, -1), 1)) * signum(vec.azimuth);
165         if (mirrored) {
166             return new SphericalVector(F_PI - (float) az, vec.elevation, vec.distance);
167         } else {
168             return new SphericalVector((float) az, vec.elevation, vec.distance);
169         }
170     }
171 
172     /**
173      * Converts the SphericalVector to an AoA vector.
174      *
175      * @return An equivalent AoA vector.
176      */
toAoAVector()177     public AoaVector toAoAVector() {
178         return AoaVector.fromSphericalVector(this);
179     }
180 
181     /**
182      * Converts to a Vector3.
183      * See {@link #SphericalVector} for orientation information.
184      *
185      * @return A Vector3 whose coordinates are at the indicated location.
186      */
187     @NonNull
toCartesian()188     public Vector3 toCartesian() {
189         float sa = (float) sin(azimuth);
190         float x = distance * (float) cos(elevation) * sa;
191         float y = distance * (float) sin(elevation);
192         float z = distance * (float) abs(cos(elevation) * cos(azimuth));
193         if (abs(azimuth) <= F_HALF_PI) {
194             z = -z;
195         }
196         return new Vector3(x, y, z);
197     }
198 
199     /**
200      * {@inheritDoc}
201      */
202     @NonNull
203     @Override
toString()204     public String toString() {
205         String format = "[⦡% 6.1f,⦨% 5.1f,⤠%5.2f]";
206         return String.format(
207                 Locale.getDefault(),
208                 format,
209                 toDegrees(azimuth),
210                 toDegrees(elevation),
211                 distance
212         );
213     }
214 
215     /**
216      * Converts this SphericalVector to an equivalent annotated Spherical Vector that has all 3
217      * components and null annotations.
218      *
219      * @return An equivalent {@link Annotated}.
220      */
toAnnotated()221     public Annotated toAnnotated() {
222         return new Annotated(this, true, true, true);
223     }
224 
225     /**
226      * Converts this SphericalVector to an equivalent annotated Spherical Vector, with the specified
227      * presence or absence of values.
228      *
229      * @param hasAzimuth   True if the vector includes azimuth.
230      * @param hasElevation True if the vector includes elevation.
231      * @param hasDistance  True if the vector includes distance.
232      * @return An equivalent {@link Annotated}.
233      */
toAnnotated(boolean hasAzimuth, boolean hasElevation, boolean hasDistance)234     public Annotated toAnnotated(boolean hasAzimuth, boolean hasElevation, boolean hasDistance) {
235         return new Annotated(
236                 this,
237                 hasAzimuth,
238                 hasElevation,
239                 hasDistance
240         );
241     }
242 
243     /**
244      * Represents a {@link SphericalVector} with annotations about the presence and quality of
245      * each of its values. While SphericalVector is invariant, the annotations on this class are not
246      * invariant.
247      */
248     public static class Annotated extends SphericalVector {
249         public final boolean hasAzimuth;
250         public final boolean hasElevation;
251         public final boolean hasDistance;
252         public double azimuthFom = 1;
253         public double elevationFom = 1;
254         public double distanceFom = 1;
255 
256         /**
257          * Creates a new instance of the {@link SphericalVector.Annotated}
258          *
259          * @param vector The source SphericalVector.
260          */
Annotated( @onNull SphericalVector vector )261         public Annotated(
262                 @NonNull SphericalVector vector
263         ) {
264             super(vector.azimuth, vector.elevation, vector.distance);
265 
266             this.hasAzimuth = true;
267             this.hasElevation = true;
268             this.hasDistance = true;
269         }
270 
271         /**
272          * Creates a new instance of the {@link SphericalVector.Annotated}
273          *
274          * @param vector       The source SphericalVector.
275          * @param hasAzimuth   True if the vector includes azimuth.
276          * @param hasElevation True if the vector includes elevation.
277          * @param hasDistance  True if the vector includes distance.
278          */
Annotated( @onNull SphericalVector vector, boolean hasAzimuth, boolean hasElevation, boolean hasDistance )279         public Annotated(
280                 @NonNull SphericalVector vector,
281                 boolean hasAzimuth,
282                 boolean hasElevation,
283                 boolean hasDistance
284         ) {
285             super(vector.azimuth, vector.elevation, vector.distance);
286             this.hasAzimuth = hasAzimuth;
287             this.hasElevation = hasElevation;
288             this.hasDistance = hasDistance;
289         }
290 
291         /**
292          * Determines if a sparse vector has all components.
293          *
294          * @return true if azimuth, elevation and distance are present.
295          */
isComplete()296         public boolean isComplete() {
297             return hasAzimuth && hasElevation && hasDistance;
298         }
299 
300         /**
301          * Copies the annotations from another {@link Annotated}. This updates the current class.
302          *
303          * @param basis The {@link Annotated} from which to copy the annotations.
304          * @return This object.
305          */
306         @NonNull
copyFomFrom(Annotated basis)307         public Annotated copyFomFrom(Annotated basis) {
308             azimuthFom = basis.azimuthFom;
309             elevationFom = basis.elevationFom;
310             distanceFom = basis.distanceFom;
311             return this;
312         }
313 
314         /**
315          * Returns a string representation of the object. In general, the
316          * {@code toString} method returns a string that
317          * "textually represents" this object. The result should
318          * be a concise but informative representation that is easy for a
319          * person to read.
320          * It is recommended that all subclasses override this method.
321          * <p>
322          * The {@code toString} method for class {@code Object}
323          * returns a string consisting of the name of the class of which the
324          * object is an instance, the at-sign character `{@code @}', and
325          * the unsigned hexadecimal representation of the hash code of the
326          * object. In other words, this method returns a string equal to the
327          * value of:
328          * <blockquote>
329          * <pre>
330          * getClass().getName() + '@' + Integer.toHexString(hashCode())
331          * </pre></blockquote>
332          *
333          * @return a string representation of the object.
334          */
335         @NonNull
336         @Override
toString()337         public String toString() {
338             String az = "   x  ", el = "  x  ", dist = "  x  ";
339             Locale dl = Locale.getDefault();
340             if (hasAzimuth) {
341                 az = String.format(dl, "% 6.1f %d%%", toDegrees(azimuth),
342                         (int) (azimuthFom * 100));
343             }
344             if (hasElevation) {
345                 el = String.format(dl, "% 5.1f %d%%", toDegrees(elevation),
346                         (int) (elevationFom * 100));
347             }
348             if (hasDistance) {
349                 dist = String.format(dl, "%5.2f %d%%", distance,
350                         (int) (distanceFom * 100));
351 
352             }
353             return String.format(dl, "[⦡%s,⦨%s,⤠%s]", az, el, dist);
354         }
355     }
356 }
357