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