• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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