1 /* 2 * Copyright (C) 2009 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 17 package android.hardware; 18 19 import java.util.GregorianCalendar; 20 21 /** 22 * Estimates magnetic field at a given point on 23 * Earth, and in particular, to compute the magnetic declination from true 24 * north. 25 * 26 * <p>This uses the World Magnetic Model produced by the United States National 27 * Geospatial-Intelligence Agency. More details about the model can be found at 28 * <a href="http://www.ngdc.noaa.gov/geomag/WMM/DoDWMM.shtml">http://www.ngdc.noaa.gov/geomag/WMM/DoDWMM.shtml</a>. 29 * This class currently uses WMM-2015 which is valid until 2020, but should 30 * produce acceptable results for several years after that. Future versions of 31 * Android may use a newer version of the model. 32 */ 33 public class GeomagneticField { 34 // The magnetic field at a given point, in nanoteslas in geodetic 35 // coordinates. 36 private float mX; 37 private float mY; 38 private float mZ; 39 40 // Geocentric coordinates -- set by computeGeocentricCoordinates. 41 private float mGcLatitudeRad; 42 private float mGcLongitudeRad; 43 private float mGcRadiusKm; 44 45 // Constants from WGS84 (the coordinate system used by GPS) 46 static private final float EARTH_SEMI_MAJOR_AXIS_KM = 6378.137f; 47 static private final float EARTH_SEMI_MINOR_AXIS_KM = 6356.7523142f; 48 static private final float EARTH_REFERENCE_RADIUS_KM = 6371.2f; 49 50 // These coefficients and the formulae used below are from: 51 // NOAA Technical Report: The US/UK World Magnetic Model for 2015-2020 52 static private final float[][] G_COEFF = new float[][] { 53 { 0.0f }, 54 { -29438.5f, -1501.1f }, 55 { -2445.3f, 3012.5f, 1676.6f }, 56 { 1351.1f, -2352.3f, 1225.6f, 581.9f }, 57 { 907.2f, 813.7f, 120.3f, -335.0f, 70.3f }, 58 { -232.6f, 360.1f, 192.4f, -141.0f, -157.4f, 4.3f }, 59 { 69.5f, 67.4f, 72.8f, -129.8f, -29.0f, 13.2f, -70.9f }, 60 { 81.6f, -76.1f, -6.8f, 51.9f, 15.0f, 9.3f, -2.8f, 6.7f }, 61 { 24.0f, 8.6f, -16.9f, -3.2f, -20.6f, 13.3f, 11.7f, -16.0f, -2.0f }, 62 { 5.4f, 8.8f, 3.1f, -3.1f, 0.6f, -13.3f, -0.1f, 8.7f, -9.1f, -10.5f }, 63 { -1.9f, -6.5f, 0.2f, 0.6f, -0.6f, 1.7f, -0.7f, 2.1f, 2.3f, -1.8f, -3.6f }, 64 { 3.1f, -1.5f, -2.3f, 2.1f, -0.9f, 0.6f, -0.7f, 0.2f, 1.7f, -0.2f, 0.4f, 3.5f }, 65 { -2.0f, -0.3f, 0.4f, 1.3f, -0.9f, 0.9f, 0.1f, 0.5f, -0.4f, -0.4f, 0.2f, -0.9f, 0.0f } }; 66 67 static private final float[][] H_COEFF = new float[][] { 68 { 0.0f }, 69 { 0.0f, 4796.2f }, 70 { 0.0f, -2845.6f, -642.0f }, 71 { 0.0f, -115.3f, 245.0f, -538.3f }, 72 { 0.0f, 283.4f, -188.6f, 180.9f, -329.5f }, 73 { 0.0f, 47.4f, 196.9f, -119.4f, 16.1f, 100.1f }, 74 { 0.0f, -20.7f, 33.2f, 58.8f, -66.5f, 7.3f, 62.5f }, 75 { 0.0f, -54.1f, -19.4f, 5.6f, 24.4f, 3.3f, -27.5f, -2.3f }, 76 { 0.0f, 10.2f, -18.1f, 13.2f, -14.6f, 16.2f, 5.7f, -9.1f, 2.2f }, 77 { 0.0f, -21.6f, 10.8f, 11.7f, -6.8f, -6.9f, 7.8f, 1.0f, -3.9f, 8.5f }, 78 { 0.0f, 3.3f, -0.3f, 4.6f, 4.4f, -7.9f, -0.6f, -4.1f, -2.8f, -1.1f, -8.7f }, 79 { 0.0f, -0.1f, 2.1f, -0.7f, -1.1f, 0.7f, -0.2f, -2.1f, -1.5f, -2.5f, -2.0f, -2.3f }, 80 { 0.0f, -1.0f, 0.5f, 1.8f, -2.2f, 0.3f, 0.7f, -0.1f, 0.3f, 0.2f, -0.9f, -0.2f, 0.7f } }; 81 82 static private final float[][] DELTA_G = new float[][] { 83 { 0.0f }, 84 { 10.7f, 17.9f }, 85 { -8.6f, -3.3f, 2.4f }, 86 { 3.1f, -6.2f, -0.4f, -10.4f }, 87 { -0.4f, 0.8f, -9.2f, 4.0f, -4.2f }, 88 { -0.2f, 0.1f, -1.4f, 0.0f, 1.3f, 3.8f }, 89 { -0.5f, -0.2f, -0.6f, 2.4f, -1.1f, 0.3f, 1.5f }, 90 { 0.2f, -0.2f, -0.4f, 1.3f, 0.2f, -0.4f, -0.9f, 0.3f }, 91 { 0.0f, 0.1f, -0.5f, 0.5f, -0.2f, 0.4f, 0.2f, -0.4f, 0.3f }, 92 { 0.0f, -0.1f, -0.1f, 0.4f, -0.5f, -0.2f, 0.1f, 0.0f, -0.2f, -0.1f }, 93 { 0.0f, 0.0f, -0.1f, 0.3f, -0.1f, -0.1f, -0.1f, 0.0f, -0.2f, -0.1f, -0.2f }, 94 { 0.0f, 0.0f, -0.1f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.1f, -0.1f }, 95 { 0.1f, 0.0f, 0.0f, 0.1f, -0.1f, 0.0f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }; 96 97 static private final float[][] DELTA_H = new float[][] { 98 { 0.0f }, 99 { 0.0f, -26.8f }, 100 { 0.0f, -27.1f, -13.3f }, 101 { 0.0f, 8.4f, -0.4f, 2.3f }, 102 { 0.0f, -0.6f, 5.3f, 3.0f, -5.3f }, 103 { 0.0f, 0.4f, 1.6f, -1.1f, 3.3f, 0.1f }, 104 { 0.0f, 0.0f, -2.2f, -0.7f, 0.1f, 1.0f, 1.3f }, 105 { 0.0f, 0.7f, 0.5f, -0.2f, -0.1f, -0.7f, 0.1f, 0.1f }, 106 { 0.0f, -0.3f, 0.3f, 0.3f, 0.6f, -0.1f, -0.2f, 0.3f, 0.0f }, 107 { 0.0f, -0.2f, -0.1f, -0.2f, 0.1f, 0.1f, 0.0f, -0.2f, 0.4f, 0.3f }, 108 { 0.0f, 0.1f, -0.1f, 0.0f, 0.0f, -0.2f, 0.1f, -0.1f, -0.2f, 0.1f, -0.1f }, 109 { 0.0f, 0.0f, 0.1f, 0.0f, 0.1f, 0.0f, 0.0f, 0.1f, 0.0f, -0.1f, 0.0f, -0.1f }, 110 { 0.0f, 0.0f, 0.0f, -0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }; 111 112 static private final long BASE_TIME = 113 new GregorianCalendar(2015, 1, 1).getTimeInMillis(); 114 115 // The ratio between the Gauss-normalized associated Legendre functions and 116 // the Schmid quasi-normalized ones. Compute these once staticly since they 117 // don't depend on input variables at all. 118 static private final float[][] SCHMIDT_QUASI_NORM_FACTORS = 119 computeSchmidtQuasiNormFactors(G_COEFF.length); 120 121 /** 122 * Estimate the magnetic field at a given point and time. 123 * 124 * @param gdLatitudeDeg 125 * Latitude in WGS84 geodetic coordinates -- positive is east. 126 * @param gdLongitudeDeg 127 * Longitude in WGS84 geodetic coordinates -- positive is north. 128 * @param altitudeMeters 129 * Altitude in WGS84 geodetic coordinates, in meters. 130 * @param timeMillis 131 * Time at which to evaluate the declination, in milliseconds 132 * since January 1, 1970. (approximate is fine -- the declination 133 * changes very slowly). 134 */ GeomagneticField(float gdLatitudeDeg, float gdLongitudeDeg, float altitudeMeters, long timeMillis)135 public GeomagneticField(float gdLatitudeDeg, 136 float gdLongitudeDeg, 137 float altitudeMeters, 138 long timeMillis) { 139 final int MAX_N = G_COEFF.length; // Maximum degree of the coefficients. 140 141 // We don't handle the north and south poles correctly -- pretend that 142 // we're not quite at them to avoid crashing. 143 gdLatitudeDeg = Math.min(90.0f - 1e-5f, 144 Math.max(-90.0f + 1e-5f, gdLatitudeDeg)); 145 computeGeocentricCoordinates(gdLatitudeDeg, 146 gdLongitudeDeg, 147 altitudeMeters); 148 149 assert G_COEFF.length == H_COEFF.length; 150 151 // Note: LegendreTable computes associated Legendre functions for 152 // cos(theta). We want the associated Legendre functions for 153 // sin(latitude), which is the same as cos(PI/2 - latitude), except the 154 // derivate will be negated. 155 LegendreTable legendre = 156 new LegendreTable(MAX_N - 1, 157 (float) (Math.PI / 2.0 - mGcLatitudeRad)); 158 159 // Compute a table of (EARTH_REFERENCE_RADIUS_KM / radius)^n for i in 160 // 0..MAX_N-2 (this is much faster than calling Math.pow MAX_N+1 times). 161 float[] relativeRadiusPower = new float[MAX_N + 2]; 162 relativeRadiusPower[0] = 1.0f; 163 relativeRadiusPower[1] = EARTH_REFERENCE_RADIUS_KM / mGcRadiusKm; 164 for (int i = 2; i < relativeRadiusPower.length; ++i) { 165 relativeRadiusPower[i] = relativeRadiusPower[i - 1] * 166 relativeRadiusPower[1]; 167 } 168 169 // Compute tables of sin(lon * m) and cos(lon * m) for m = 0..MAX_N -- 170 // this is much faster than calling Math.sin and Math.com MAX_N+1 times. 171 float[] sinMLon = new float[MAX_N]; 172 float[] cosMLon = new float[MAX_N]; 173 sinMLon[0] = 0.0f; 174 cosMLon[0] = 1.0f; 175 sinMLon[1] = (float) Math.sin(mGcLongitudeRad); 176 cosMLon[1] = (float) Math.cos(mGcLongitudeRad); 177 178 for (int m = 2; m < MAX_N; ++m) { 179 // Standard expansions for sin((m-x)*theta + x*theta) and 180 // cos((m-x)*theta + x*theta). 181 int x = m >> 1; 182 sinMLon[m] = sinMLon[m-x] * cosMLon[x] + cosMLon[m-x] * sinMLon[x]; 183 cosMLon[m] = cosMLon[m-x] * cosMLon[x] - sinMLon[m-x] * sinMLon[x]; 184 } 185 186 float inverseCosLatitude = 1.0f / (float) Math.cos(mGcLatitudeRad); 187 float yearsSinceBase = 188 (timeMillis - BASE_TIME) / (365f * 24f * 60f * 60f * 1000f); 189 190 // We now compute the magnetic field strength given the geocentric 191 // location. The magnetic field is the derivative of the potential 192 // function defined by the model. See NOAA Technical Report: The US/UK 193 // World Magnetic Model for 2015-2020 for the derivation. 194 float gcX = 0.0f; // Geocentric northwards component. 195 float gcY = 0.0f; // Geocentric eastwards component. 196 float gcZ = 0.0f; // Geocentric downwards component. 197 198 for (int n = 1; n < MAX_N; n++) { 199 for (int m = 0; m <= n; m++) { 200 // Adjust the coefficients for the current date. 201 float g = G_COEFF[n][m] + yearsSinceBase * DELTA_G[n][m]; 202 float h = H_COEFF[n][m] + yearsSinceBase * DELTA_H[n][m]; 203 204 // Negative derivative with respect to latitude, divided by 205 // radius. This looks like the negation of the version in the 206 // NOAA Techincal report because that report used 207 // P_n^m(sin(theta)) and we use P_n^m(cos(90 - theta)), so the 208 // derivative with respect to theta is negated. 209 gcX += relativeRadiusPower[n+2] 210 * (g * cosMLon[m] + h * sinMLon[m]) 211 * legendre.mPDeriv[n][m] 212 * SCHMIDT_QUASI_NORM_FACTORS[n][m]; 213 214 // Negative derivative with respect to longitude, divided by 215 // radius. 216 gcY += relativeRadiusPower[n+2] * m 217 * (g * sinMLon[m] - h * cosMLon[m]) 218 * legendre.mP[n][m] 219 * SCHMIDT_QUASI_NORM_FACTORS[n][m] 220 * inverseCosLatitude; 221 222 // Negative derivative with respect to radius. 223 gcZ -= (n + 1) * relativeRadiusPower[n+2] 224 * (g * cosMLon[m] + h * sinMLon[m]) 225 * legendre.mP[n][m] 226 * SCHMIDT_QUASI_NORM_FACTORS[n][m]; 227 } 228 } 229 230 // Convert back to geodetic coordinates. This is basically just a 231 // rotation around the Y-axis by the difference in latitudes between the 232 // geocentric frame and the geodetic frame. 233 double latDiffRad = Math.toRadians(gdLatitudeDeg) - mGcLatitudeRad; 234 mX = (float) (gcX * Math.cos(latDiffRad) 235 + gcZ * Math.sin(latDiffRad)); 236 mY = gcY; 237 mZ = (float) (- gcX * Math.sin(latDiffRad) 238 + gcZ * Math.cos(latDiffRad)); 239 } 240 241 /** 242 * @return The X (northward) component of the magnetic field in nanoteslas. 243 */ getX()244 public float getX() { 245 return mX; 246 } 247 248 /** 249 * @return The Y (eastward) component of the magnetic field in nanoteslas. 250 */ getY()251 public float getY() { 252 return mY; 253 } 254 255 /** 256 * @return The Z (downward) component of the magnetic field in nanoteslas. 257 */ getZ()258 public float getZ() { 259 return mZ; 260 } 261 262 /** 263 * @return The declination of the horizontal component of the magnetic 264 * field from true north, in degrees (i.e. positive means the 265 * magnetic field is rotated east that much from true north). 266 */ getDeclination()267 public float getDeclination() { 268 return (float) Math.toDegrees(Math.atan2(mY, mX)); 269 } 270 271 /** 272 * @return The inclination of the magnetic field in degrees -- positive 273 * means the magnetic field is rotated downwards. 274 */ getInclination()275 public float getInclination() { 276 return (float) Math.toDegrees(Math.atan2(mZ, 277 getHorizontalStrength())); 278 } 279 280 /** 281 * @return Horizontal component of the field strength in nanoteslas. 282 */ getHorizontalStrength()283 public float getHorizontalStrength() { 284 return (float) Math.hypot(mX, mY); 285 } 286 287 /** 288 * @return Total field strength in nanoteslas. 289 */ getFieldStrength()290 public float getFieldStrength() { 291 return (float) Math.sqrt(mX * mX + mY * mY + mZ * mZ); 292 } 293 294 /** 295 * @param gdLatitudeDeg 296 * Latitude in WGS84 geodetic coordinates. 297 * @param gdLongitudeDeg 298 * Longitude in WGS84 geodetic coordinates. 299 * @param altitudeMeters 300 * Altitude above sea level in WGS84 geodetic coordinates. 301 * @return Geocentric latitude (i.e. angle between closest point on the 302 * equator and this point, at the center of the earth. 303 */ computeGeocentricCoordinates(float gdLatitudeDeg, float gdLongitudeDeg, float altitudeMeters)304 private void computeGeocentricCoordinates(float gdLatitudeDeg, 305 float gdLongitudeDeg, 306 float altitudeMeters) { 307 float altitudeKm = altitudeMeters / 1000.0f; 308 float a2 = EARTH_SEMI_MAJOR_AXIS_KM * EARTH_SEMI_MAJOR_AXIS_KM; 309 float b2 = EARTH_SEMI_MINOR_AXIS_KM * EARTH_SEMI_MINOR_AXIS_KM; 310 double gdLatRad = Math.toRadians(gdLatitudeDeg); 311 float clat = (float) Math.cos(gdLatRad); 312 float slat = (float) Math.sin(gdLatRad); 313 float tlat = slat / clat; 314 float latRad = 315 (float) Math.sqrt(a2 * clat * clat + b2 * slat * slat); 316 317 mGcLatitudeRad = (float) Math.atan(tlat * (latRad * altitudeKm + b2) 318 / (latRad * altitudeKm + a2)); 319 320 mGcLongitudeRad = (float) Math.toRadians(gdLongitudeDeg); 321 322 float radSq = altitudeKm * altitudeKm 323 + 2 * altitudeKm * (float) Math.sqrt(a2 * clat * clat + 324 b2 * slat * slat) 325 + (a2 * a2 * clat * clat + b2 * b2 * slat * slat) 326 / (a2 * clat * clat + b2 * slat * slat); 327 mGcRadiusKm = (float) Math.sqrt(radSq); 328 } 329 330 331 /** 332 * Utility class to compute a table of Gauss-normalized associated Legendre 333 * functions P_n^m(cos(theta)) 334 */ 335 static private class LegendreTable { 336 // These are the Gauss-normalized associated Legendre functions -- that 337 // is, they are normal Legendre functions multiplied by 338 // (n-m)!/(2n-1)!! (where (2n-1)!! = 1*3*5*...*2n-1) 339 public final float[][] mP; 340 341 // Derivative of mP, with respect to theta. 342 public final float[][] mPDeriv; 343 344 /** 345 * @param maxN 346 * The maximum n- and m-values to support 347 * @param thetaRad 348 * Returned functions will be Gauss-normalized 349 * P_n^m(cos(thetaRad)), with thetaRad in radians. 350 */ LegendreTable(int maxN, float thetaRad)351 public LegendreTable(int maxN, float thetaRad) { 352 // Compute the table of Gauss-normalized associated Legendre 353 // functions using standard recursion relations. Also compute the 354 // table of derivatives using the derivative of the recursion 355 // relations. 356 float cos = (float) Math.cos(thetaRad); 357 float sin = (float) Math.sin(thetaRad); 358 359 mP = new float[maxN + 1][]; 360 mPDeriv = new float[maxN + 1][]; 361 mP[0] = new float[] { 1.0f }; 362 mPDeriv[0] = new float[] { 0.0f }; 363 for (int n = 1; n <= maxN; n++) { 364 mP[n] = new float[n + 1]; 365 mPDeriv[n] = new float[n + 1]; 366 for (int m = 0; m <= n; m++) { 367 if (n == m) { 368 mP[n][m] = sin * mP[n - 1][m - 1]; 369 mPDeriv[n][m] = cos * mP[n - 1][m - 1] 370 + sin * mPDeriv[n - 1][m - 1]; 371 } else if (n == 1 || m == n - 1) { 372 mP[n][m] = cos * mP[n - 1][m]; 373 mPDeriv[n][m] = -sin * mP[n - 1][m] 374 + cos * mPDeriv[n - 1][m]; 375 } else { 376 assert n > 1 && m < n - 1; 377 float k = ((n - 1) * (n - 1) - m * m) 378 / (float) ((2 * n - 1) * (2 * n - 3)); 379 mP[n][m] = cos * mP[n - 1][m] - k * mP[n - 2][m]; 380 mPDeriv[n][m] = -sin * mP[n - 1][m] 381 + cos * mPDeriv[n - 1][m] - k * mPDeriv[n - 2][m]; 382 } 383 } 384 } 385 } 386 } 387 388 /** 389 * Compute the ration between the Gauss-normalized associated Legendre 390 * functions and the Schmidt quasi-normalized version. This is equivalent to 391 * sqrt((m==0?1:2)*(n-m)!/(n+m!))*(2n-1)!!/(n-m)! 392 */ 393 private static float[][] computeSchmidtQuasiNormFactors(int maxN) { 394 float[][] schmidtQuasiNorm = new float[maxN + 1][]; 395 schmidtQuasiNorm[0] = new float[] { 1.0f }; 396 for (int n = 1; n <= maxN; n++) { 397 schmidtQuasiNorm[n] = new float[n + 1]; 398 schmidtQuasiNorm[n][0] = 399 schmidtQuasiNorm[n - 1][0] * (2 * n - 1) / (float) n; 400 for (int m = 1; m <= n; m++) { 401 schmidtQuasiNorm[n][m] = schmidtQuasiNorm[n][m - 1] 402 * (float) Math.sqrt((n - m + 1) * (m == 1 ? 2 : 1) 403 / (float) (n + m)); 404 } 405 } 406 return schmidtQuasiNorm; 407 } 408 } 409