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