1 /* 2 * Copyright (C) 2022 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 com.android.internal.location.altitude; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.util.LruCache; 25 26 import com.android.internal.annotations.GuardedBy; 27 import com.android.internal.location.altitude.nano.MapParamsProto; 28 import com.android.internal.location.altitude.nano.S2TileProto; 29 import com.android.internal.util.Preconditions; 30 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.nio.ByteBuffer; 34 import java.util.Objects; 35 36 /** 37 * Manages a mapping of geoid heights and expiration distances associated with S2 cells, referred to 38 * as MAP CELLS. 39 * 40 * <p>Tiles are used extensively to reduce the number of entries needed to be stored in memory and 41 * on disk. A tile associates geoid heights or expiration distances with all map cells of a common 42 * parent at a specified S2 level. 43 * 44 * <p>Since bilinear interpolation considers at most four map cells at a time, at most four tiles 45 * are simultaneously stored in memory. These tiles, referred to as CACHE TILES, are each keyed by 46 * its common parent's S2 cell ID, referred to as a CACHE KEY. 47 * 48 * <p>Absent cache tiles needed for interpolation are constructed from larger tiles stored on disk. 49 * The latter tiles, referred to as DISK TILES, are each keyed by its common parent's S2 cell token, 50 * referred to as a DISK TOKEN. 51 */ 52 public final class GeoidMap { 53 54 private static final String GEOID_HEIGHT_PREFIX = "geoid-height"; 55 56 private static final String EXPIRATION_DISTANCE_PREFIX = "expiration-distance"; 57 58 private static final Object GEOID_HEIGHT_PARAMS_LOCK = new Object(); 59 60 private static final Object EXPIRATION_DISTANCE_PARAMS_LOCK = new Object(); 61 62 @GuardedBy("GEOID_HEIGHT_PARAMS_LOCK") 63 @Nullable 64 private static MapParamsProto sGeoidHeightParams; 65 66 @GuardedBy("EXPIRATION_DISTANCE_PARAMS_LOCK") 67 @Nullable 68 private static MapParamsProto sExpirationDistanceParams; 69 70 /** 71 * Defines a cache large enough to hold all geoid height cache tiles needed for interpolation. 72 */ 73 private final LruCache<Long, S2TileProto> mGeoidHeightCacheTiles = new LruCache<>(4); 74 75 /** 76 * Defines a cache large enough to hold all expiration distance cache tiles needed for 77 * interpolation. 78 */ 79 private final LruCache<Long, S2TileProto> mExpirationDistanceCacheTiles = new LruCache<>(4); 80 81 /** 82 * Returns the singleton parameter instance for geoid height parameters of a spherically 83 * projected map. 84 */ 85 @NonNull getGeoidHeightParams(@onNull Context context)86 public static MapParamsProto getGeoidHeightParams(@NonNull Context context) throws IOException { 87 synchronized (GEOID_HEIGHT_PARAMS_LOCK) { 88 if (sGeoidHeightParams == null) { 89 sGeoidHeightParams = parseParams(context, GEOID_HEIGHT_PREFIX); 90 } 91 return sGeoidHeightParams; 92 } 93 } 94 95 /** 96 * Returns the singleton parameter instance for expiration distance parameters of a spherically 97 * projected 98 * map. 99 */ 100 @NonNull getExpirationDistanceParams(@onNull Context context)101 public static MapParamsProto getExpirationDistanceParams(@NonNull Context context) 102 throws IOException { 103 synchronized (EXPIRATION_DISTANCE_PARAMS_LOCK) { 104 if (sExpirationDistanceParams == null) { 105 sExpirationDistanceParams = parseParams(context, EXPIRATION_DISTANCE_PREFIX); 106 } 107 return sExpirationDistanceParams; 108 } 109 } 110 111 @NonNull parseParams(@onNull Context context, @NonNull String prefix)112 private static MapParamsProto parseParams(@NonNull Context context, @NonNull String prefix) 113 throws IOException { 114 try (InputStream is = context.getApplicationContext().getAssets().open( 115 "geoid_map/" + prefix + "-params.pb")) { 116 return MapParamsProto.parseFrom(is.readAllBytes()); 117 } 118 } 119 120 /** 121 * Same as {@link #getGeoidHeightParams(Context)} except that null is returned if the singleton 122 * parameter instance is not yet initialized. 123 */ 124 @Nullable getGeoidHeightParams()125 public static MapParamsProto getGeoidHeightParams() { 126 synchronized (GEOID_HEIGHT_PARAMS_LOCK) { 127 return sGeoidHeightParams; 128 } 129 } 130 getCacheKey(@onNull MapParamsProto params, long s2CellId)131 private static long getCacheKey(@NonNull MapParamsProto params, long s2CellId) { 132 return S2CellIdUtils.getParent(s2CellId, params.cacheTileS2Level); 133 } 134 135 @NonNull getDiskToken(@onNull MapParamsProto params, long s2CellId)136 private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) { 137 return S2CellIdUtils.getToken(S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level)); 138 } 139 140 /** 141 * Adds to {@code values} values in the unit interval [0, 1] for the map cells identified by 142 * {@code s2CellIds}. Returns true if values are present for all IDs; otherwise, adds NaNs for 143 * absent values and returns false. 144 */ getUnitIntervalValues(@onNull MapParamsProto params, @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, @NonNull double[] values)145 private static boolean getUnitIntervalValues(@NonNull MapParamsProto params, 146 @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, 147 @NonNull double[] values) { 148 int len = s2CellIds.length; 149 150 S2TileProto[] tiles = new S2TileProto[len]; 151 for (int i = 0; i < len; i++) { 152 long cacheKey = getCacheKey(params, s2CellIds[i]); 153 tiles[i] = tileFunction.getTile(cacheKey); 154 values[i] = Double.NaN; 155 } 156 157 for (int i = 0; i < len; i++) { 158 if (tiles[i] == null || !Double.isNaN(values[i])) { 159 continue; 160 } 161 162 mergeByteBufferValues(params, s2CellIds, tiles, i, values); 163 mergeByteJpegValues(params, s2CellIds, tiles, i, values); 164 mergeBytePngValues(params, s2CellIds, tiles, i, values); 165 } 166 167 boolean allFound = true; 168 for (int i = 0; i < len; i++) { 169 if (Double.isNaN(values[i])) { 170 allFound = false; 171 } else { 172 values[i] = (((int) values[i]) & 0xFF) / 255.0; 173 } 174 } 175 return allFound; 176 } 177 178 @SuppressWarnings("ReferenceEquality") mergeByteBufferValues(@onNull MapParamsProto params, @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values)179 private static void mergeByteBufferValues(@NonNull MapParamsProto params, 180 @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, 181 @NonNull double[] values) { 182 byte[] bytes = tiles[tileIndex].byteBuffer; 183 if (bytes == null || bytes.length == 0) { 184 return; 185 } 186 187 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer(); 188 int tileS2Level = params.mapS2Level - Integer.numberOfTrailingZeros(byteBuffer.limit()) / 2; 189 int numBitsLeftOfTile = 2 * tileS2Level + 3; 190 191 for (int i = tileIndex; i < tiles.length; i++) { 192 if (tiles[i] != tiles[tileIndex]) { 193 continue; 194 } 195 196 long maskedS2CellId = s2CellIds[i] & (-1L >>> numBitsLeftOfTile); 197 int numBitsRightOfMap = 2 * (S2CellIdUtils.MAX_LEVEL - params.mapS2Level) + 1; 198 int bufferIndex = (int) (maskedS2CellId >>> numBitsRightOfMap); 199 values[i] = Double.isNaN(values[i]) ? 0 : values[i]; 200 values[i] += ((int) byteBuffer.get(bufferIndex)) & 0xFF; 201 } 202 } 203 mergeByteJpegValues(@onNull MapParamsProto params, @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values)204 private static void mergeByteJpegValues(@NonNull MapParamsProto params, 205 @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, 206 @NonNull double[] values) { 207 mergeByteImageValues(params, tiles[tileIndex].byteJpeg, s2CellIds, tiles, tileIndex, 208 values); 209 } 210 mergeBytePngValues(@onNull MapParamsProto params, @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values)211 private static void mergeBytePngValues(@NonNull MapParamsProto params, 212 @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, 213 @NonNull double[] values) { 214 mergeByteImageValues(params, tiles[tileIndex].bytePng, s2CellIds, tiles, tileIndex, values); 215 } 216 217 @SuppressWarnings("ReferenceEquality") mergeByteImageValues(@onNull MapParamsProto params, @NonNull byte[] bytes, @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values)218 private static void mergeByteImageValues(@NonNull MapParamsProto params, @NonNull byte[] bytes, 219 @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, 220 @NonNull double[] values) { 221 if (bytes == null || bytes.length == 0) { 222 return; 223 } 224 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); 225 if (bitmap == null) { 226 return; 227 } 228 229 for (int i = tileIndex; i < tiles.length; i++) { 230 if (tiles[i] != tiles[tileIndex]) { 231 continue; 232 } 233 234 values[i] = Double.isNaN(values[i]) ? 0 : values[i]; 235 values[i] += bitmap.getPixel(getIndexX(params, s2CellIds[i], bitmap.getWidth()), 236 getIndexY(params, s2CellIds[i], bitmap.getHeight())) & 0xFF; 237 } 238 } 239 240 /** Returns the X index for an S2 cell within an S2 tile image of specified width. */ getIndexX(@onNull MapParamsProto params, long s2CellId, int width)241 private static int getIndexX(@NonNull MapParamsProto params, long s2CellId, int width) { 242 return getIndexXOrY(params, S2CellIdUtils.getI(s2CellId), width); 243 } 244 245 /** Returns the Y index for an S2 cell within an S2 tile image of specified height. */ getIndexY(@onNull MapParamsProto params, long s2CellId, int height)246 private static int getIndexY(@NonNull MapParamsProto params, long s2CellId, int height) { 247 return getIndexXOrY(params, S2CellIdUtils.getJ(s2CellId), height); 248 } 249 getIndexXOrY(@onNull MapParamsProto params, int iOrJ, int widthOrHeight)250 private static int getIndexXOrY(@NonNull MapParamsProto params, int iOrJ, int widthOrHeight) { 251 return (iOrJ >> (S2CellIdUtils.MAX_LEVEL - params.mapS2Level)) % widthOrHeight; 252 } 253 254 /** 255 * Throws an {@link IllegalArgumentException} if the {@code s2CellIds} has an invalid length or 256 * ID. 257 */ validate(@onNull MapParamsProto params, @NonNull long[] s2CellIds)258 private static void validate(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) { 259 Preconditions.checkArgument(s2CellIds.length <= 4); 260 for (long s2CellId : s2CellIds) { 261 Preconditions.checkArgument(S2CellIdUtils.getLevel(s2CellId) == params.mapS2Level); 262 } 263 } 264 265 /** 266 * Returns the geoid heights in meters associated with the map cells identified by 267 * {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for 268 * an ID. 269 */ 270 @NonNull readGeoidHeights(@onNull MapParamsProto params, @NonNull Context context, @NonNull long[] s2CellIds)271 public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context, 272 @NonNull long[] s2CellIds) throws IOException { 273 return readMapValues(params, context, s2CellIds, mGeoidHeightCacheTiles, 274 GEOID_HEIGHT_PREFIX); 275 } 276 277 /** 278 * Returns the expiration distances in meters associated with the map cells identified by 279 * {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for 280 * an ID. 281 */ 282 @NonNull readExpirationDistances(@onNull MapParamsProto params, @NonNull Context context, @NonNull long[] s2CellIds)283 public double[] readExpirationDistances(@NonNull MapParamsProto params, 284 @NonNull Context context, @NonNull long[] s2CellIds) throws IOException { 285 return readMapValues(params, context, s2CellIds, mExpirationDistanceCacheTiles, 286 EXPIRATION_DISTANCE_PREFIX); 287 } 288 289 /** 290 * Returns the map values in meters associated with the map cells identified by 291 * {@code s2CellIds}. Throws an {@link IOException} if a map value cannot be calculated for an 292 * ID. 293 */ 294 @NonNull readMapValues(@onNull MapParamsProto params, @NonNull Context context, @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles, @NonNull String prefix)295 private static double[] readMapValues(@NonNull MapParamsProto params, @NonNull Context context, 296 @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles, 297 @NonNull String prefix) throws IOException { 298 validate(params, s2CellIds); 299 double[] mapValuesMeters = new double[s2CellIds.length]; 300 if (getMapValues(params, cacheTiles::get, s2CellIds, mapValuesMeters)) { 301 return mapValuesMeters; 302 } 303 304 TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds, cacheTiles, 305 prefix); 306 if (getMapValues(params, loadedTiles, s2CellIds, mapValuesMeters)) { 307 return mapValuesMeters; 308 } 309 throw new IOException("Unable to calculate geoid heights from raw assets."); 310 } 311 312 /** 313 * Same as {@link #readGeoidHeights(MapParamsProto, Context, long[])} except that data will not 314 * be loaded from raw assets. Returns the heights if present for all IDs; otherwise, returns 315 * null. 316 */ 317 @Nullable readGeoidHeights(@onNull MapParamsProto params, @NonNull long[] s2CellIds)318 public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) { 319 validate(params, s2CellIds); 320 double[] heightsMeters = new double[s2CellIds.length]; 321 if (getMapValues(params, mGeoidHeightCacheTiles::get, s2CellIds, heightsMeters)) { 322 return heightsMeters; 323 } 324 return null; 325 } 326 327 /** 328 * Adds to {@code mapValuesMeters} the map values in meters associated with the map cells 329 * identified by {@code s2CellIds}. Returns true if heights are present for all IDs; otherwise, 330 * adds NaNs for absent heights and returns false. 331 */ getMapValues(@onNull MapParamsProto params, @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, @NonNull double[] mapValuesMeters)332 private static boolean getMapValues(@NonNull MapParamsProto params, 333 @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, 334 @NonNull double[] mapValuesMeters) { 335 boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, mapValuesMeters); 336 for (int i = 0; i < mapValuesMeters.length; i++) { 337 // NaNs are properly preserved. 338 mapValuesMeters[i] *= params.modelAMeters; 339 mapValuesMeters[i] += params.modelBMeters; 340 } 341 return allFound; 342 } 343 344 @NonNull loadFromCacheAndDisk(@onNull MapParamsProto params, @NonNull Context context, @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles, @NonNull String prefix)345 private static TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params, 346 @NonNull Context context, @NonNull long[] s2CellIds, 347 @NonNull LruCache<Long, S2TileProto> cacheTiles, @NonNull String prefix) 348 throws IOException { 349 int len = s2CellIds.length; 350 351 // Enable batch loading by finding all cache keys upfront. 352 long[] cacheKeys = new long[len]; 353 for (int i = 0; i < len; i++) { 354 cacheKeys[i] = getCacheKey(params, s2CellIds[i]); 355 } 356 357 // Attempt to load tiles from cache. 358 S2TileProto[] loadedTiles = new S2TileProto[len]; 359 String[] diskTokens = new String[len]; 360 for (int i = 0; i < len; i++) { 361 if (diskTokens[i] != null) { 362 continue; 363 } 364 loadedTiles[i] = cacheTiles.get(cacheKeys[i]); 365 diskTokens[i] = getDiskToken(params, cacheKeys[i]); 366 367 // Batch across common cache key. 368 for (int j = i + 1; j < len; j++) { 369 if (cacheKeys[j] == cacheKeys[i]) { 370 loadedTiles[j] = loadedTiles[i]; 371 diskTokens[j] = diskTokens[i]; 372 } 373 } 374 } 375 376 // Attempt to load tiles from disk. 377 for (int i = 0; i < len; i++) { 378 if (loadedTiles[i] != null) { 379 continue; 380 } 381 382 S2TileProto tile; 383 try (InputStream is = context.getApplicationContext().getAssets().open( 384 "geoid_map/" + prefix + "-disk-tile-" + diskTokens[i] + ".pb")) { 385 tile = S2TileProto.parseFrom(is.readAllBytes()); 386 } 387 mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles, cacheTiles); 388 } 389 390 return cacheKey -> { 391 for (int i = 0; i < cacheKeys.length; i++) { 392 if (cacheKeys[i] == cacheKey) { 393 return loadedTiles[i]; 394 } 395 } 396 return null; 397 }; 398 } 399 400 private static void mergeFromDiskTile(@NonNull MapParamsProto params, 401 @NonNull S2TileProto diskTile, @NonNull long[] cacheKeys, @NonNull String[] diskTokens, 402 int diskTokenIndex, @NonNull S2TileProto[] loadedTiles, 403 @NonNull LruCache<Long, S2TileProto> cacheTiles) throws IOException { 404 int len = cacheKeys.length; 405 int numMapCellsPerCacheTile = 1 << (2 * (params.mapS2Level - params.cacheTileS2Level)); 406 407 // Reusable arrays. 408 long[] s2CellIds = new long[numMapCellsPerCacheTile]; 409 double[] values = new double[numMapCellsPerCacheTile]; 410 411 // Each cache key identifies a different sub-tile of the same disk tile. 412 TileFunction diskTileFunction = cacheKey -> diskTile; 413 for (int i = diskTokenIndex; i < len; i++) { 414 if (!Objects.equals(diskTokens[i], diskTokens[diskTokenIndex]) 415 || loadedTiles[i] != null) { 416 continue; 417 } 418 419 // Find all map cells within the current cache tile. 420 long s2CellId = S2CellIdUtils.getTraversalStart(cacheKeys[i], params.mapS2Level); 421 for (int j = 0; j < numMapCellsPerCacheTile; j++) { 422 s2CellIds[j] = s2CellId; 423 s2CellId = S2CellIdUtils.getTraversalNext(s2CellId); 424 } 425 426 if (!getUnitIntervalValues(params, diskTileFunction, s2CellIds, values)) { 427 throw new IOException("Corrupted disk tile of disk token: " + diskTokens[i]); 428 } 429 430 loadedTiles[i] = new S2TileProto(); 431 loadedTiles[i].byteBuffer = new byte[numMapCellsPerCacheTile]; 432 for (int j = 0; j < numMapCellsPerCacheTile; j++) { 433 loadedTiles[i].byteBuffer[j] = (byte) Math.round(values[j] * 0xFF); 434 } 435 436 // Batch across common cache key. 437 for (int j = i + 1; j < len; j++) { 438 if (cacheKeys[j] == cacheKeys[i]) { 439 loadedTiles[j] = loadedTiles[i]; 440 } 441 } 442 443 // Side load into tile cache. 444 cacheTiles.put(cacheKeys[i], loadedTiles[i]); 445 } 446 } 447 448 /** Defines a function-like object to retrieve tiles for cache keys. */ 449 private interface TileFunction { 450 451 @Nullable 452 S2TileProto getTile(long cacheKey); 453 } 454 } 455