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