Merge "Backport implementation of AltitudeConverter to T." am: 7ac0b50c9a

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/2420963

Change-Id: I16ae659420197dc95897cd5bdfa330375cb08273
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Brian Julian 2023-02-07 18:41:03 +00:00 committed by Automerger Merge Worker
commit 7c96532a66
23 changed files with 1641 additions and 0 deletions

View File

@ -0,0 +1,2 @@
These binary protos are generated at runtime from the text protos in ../../geoid_height_map_assets
and using aprotoc.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
These text protos contain composite JPEG/PNG images representing the EGM2008 Earth Gravitational
Model[^1] published by the National Geospatial-Intelligence Agency.[^2]
[^1]: Pavlis, Nikolaos K., et al. "The development and evaluation of the Earth Gravitational Model
2008 (EGM2008)." Journal of geophysical research: solid earth 117.B4 (2012).
[^2]: National Geospatial-Intelligence Agency. “Office of Geomatics.” 2022.
URL: https://earth-info.nga.mil.

View File

@ -0,0 +1,6 @@
map_s2_level: 9
cache_tile_s2_level: 5
disk_tile_s2_level: 0
model_a_meters: 193.0
model_b_meters: -107.0
model_rmse_meters: 0.29

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -47,6 +47,15 @@ import java.util.StringTokenizer;
* <p>All locations generated through {@link LocationManager} are guaranteed to have a valid
* latitude, longitude, timestamp (both Unix epoch time and elapsed realtime since boot), and
* accuracy. All other parameters are optional.
*
* <p class="note">Note that Android provides the ability for applications to submit "mock" or faked
* locations through {@link LocationManager}, and that these locations can then be received by
* applications using LocationManager to obtain location information. These locations can be
* identified via the {@link #isMock()} API. Applications that wish to determine if a given location
* represents the best estimate of the real position of the device as opposed to a fake location
* coming from another application or the user should use this API. Keep in mind that the user may
* have a good reason for mocking their location, and thus apps should generally reject mock
* locations only when it is essential to their use case that only real locations are accepted.
*/
public class Location implements Parcelable {
@ -95,6 +104,8 @@ public class Location implements Parcelable {
private static final int HAS_SPEED_ACCURACY_MASK = 1 << 6;
private static final int HAS_BEARING_ACCURACY_MASK = 1 << 7;
private static final int HAS_ELAPSED_REALTIME_UNCERTAINTY_MASK = 1 << 8;
private static final int HAS_MSL_ALTITUDE_MASK = 1 << 9;
private static final int HAS_MSL_ALTITUDE_ACCURACY_MASK = 1 << 10;
// Cached data to make bearing/distance computations more efficient for the case
// where distanceTo and bearingTo are called in sequence. Assume this typically happens
@ -118,6 +129,8 @@ public class Location implements Parcelable {
private float mSpeedAccuracyMetersPerSecond;
private float mBearingDegrees;
private float mBearingAccuracyDegrees;
private double mMslAltitudeMeters;
private float mMslAltitudeAccuracyMeters;
private Bundle mExtras = null;
@ -156,6 +169,8 @@ public class Location implements Parcelable {
mSpeedAccuracyMetersPerSecond = location.mSpeedAccuracyMetersPerSecond;
mBearingDegrees = location.mBearingDegrees;
mBearingAccuracyDegrees = location.mBearingAccuracyDegrees;
mMslAltitudeMeters = location.mMslAltitudeMeters;
mMslAltitudeAccuracyMeters = location.mMslAltitudeAccuracyMeters;
mExtras = (location.mExtras == null) ? null : new Bundle(location.mExtras);
}
@ -178,6 +193,8 @@ public class Location implements Parcelable {
mAltitudeAccuracyMeters = 0;
mSpeedAccuracyMetersPerSecond = 0;
mBearingAccuracyDegrees = 0;
mMslAltitudeMeters = 0;
mMslAltitudeAccuracyMeters = 0;
mExtras = null;
}
@ -689,6 +706,91 @@ public class Location implements Parcelable {
mFieldsMask &= ~HAS_BEARING_ACCURACY_MASK;
}
/**
* Returns the Mean Sea Level altitude of this location in meters.
*
* <p>This is only valid if {@link #hasMslAltitude()} is true.
*
* @hide
*/
public @FloatRange double getMslAltitudeMeters() {
Preconditions.checkState(hasMslAltitude(),
"The Mean Sea Level altitude of this location is not set.");
return mMslAltitudeMeters;
}
/**
* Sets the Mean Sea Level altitude of this location in meters.
*
* @hide
*/
public void setMslAltitudeMeters(@FloatRange double mslAltitudeMeters) {
mMslAltitudeMeters = mslAltitudeMeters;
mFieldsMask |= HAS_MSL_ALTITUDE_MASK;
}
/**
* Returns true if this location has a Mean Sea Level altitude, false otherwise.
*
* @hide
*/
public boolean hasMslAltitude() {
return (mFieldsMask & HAS_MSL_ALTITUDE_MASK) != 0;
}
/**
* Removes the Mean Sea Level altitude from this location.
*
* @hide
*/
public void removeMslAltitude() {
mFieldsMask &= ~HAS_MSL_ALTITUDE_MASK;
}
/**
* Returns the estimated Mean Sea Level altitude accuracy in meters of this location at the 68th
* percentile confidence level. This means that there is 68% chance that the true Mean Sea Level
* altitude of this location falls within {@link #getMslAltitudeMeters()} +/- this uncertainty.
*
* <p>This is only valid if {@link #hasMslAltitudeAccuracy()} is true.
*
* @hide
*/
public @FloatRange(from = 0.0) float getMslAltitudeAccuracyMeters() {
Preconditions.checkState(hasMslAltitudeAccuracy(),
"The Mean Sea Level altitude accuracy of this location is not set.");
return mMslAltitudeAccuracyMeters;
}
/**
* Sets the Mean Sea Level altitude accuracy of this location in meters.
*
* @hide
*/
public void setMslAltitudeAccuracyMeters(
@FloatRange(from = 0.0) float mslAltitudeAccuracyMeters) {
mMslAltitudeAccuracyMeters = mslAltitudeAccuracyMeters;
mFieldsMask |= HAS_MSL_ALTITUDE_ACCURACY_MASK;
}
/**
* Returns true if this location has a Mean Sea Level altitude accuracy, false otherwise.
*
* @hide
*/
public boolean hasMslAltitudeAccuracy() {
return (mFieldsMask & HAS_MSL_ALTITUDE_ACCURACY_MASK) != 0;
}
/**
* Removes the Mean Sea Level altitude accuracy from this location.
*
* @hide
*/
public void removeMslAltitudeAccuracy() {
mFieldsMask &= ~HAS_MSL_ALTITUDE_ACCURACY_MASK;
}
/**
* Returns true if this is a mock location. If this location comes from the Android framework,
* this indicates that the location was provided by a test location provider, and thus may not
@ -839,6 +941,14 @@ public class Location implements Parcelable {
&& hasBearingAccuracy() == location.hasBearingAccuracy()
&& (!hasBearingAccuracy() || Float.compare(location.mBearingAccuracyDegrees,
mBearingAccuracyDegrees) == 0)
&& hasMslAltitude() == location.hasMslAltitude()
&& (!hasMslAltitude() || Double.compare(location.mMslAltitudeMeters,
mMslAltitudeMeters)
== 0)
&& hasMslAltitudeAccuracy() == location.hasMslAltitudeAccuracy()
&& (!hasMslAltitudeAccuracy() || Float.compare(
location.mMslAltitudeAccuracyMeters,
mMslAltitudeAccuracyMeters) == 0)
&& Objects.equals(mProvider, location.mProvider)
&& areExtrasEqual(mExtras, location.mExtras);
}
@ -876,6 +986,12 @@ public class Location implements Parcelable {
s.append(" vAcc=").append(mAltitudeAccuracyMeters);
}
}
if (hasMslAltitude()) {
s.append(" mslAlt=").append(mMslAltitudeMeters);
if (hasMslAltitudeAccuracy()) {
s.append(" mslAltAcc=").append(mMslAltitudeAccuracyMeters);
}
}
if (hasSpeed()) {
s.append(" vel=").append(mSpeedMetersPerSecond);
if (hasSpeedAccuracy()) {
@ -944,6 +1060,12 @@ public class Location implements Parcelable {
if (l.hasBearingAccuracy()) {
l.mBearingAccuracyDegrees = in.readFloat();
}
if (l.hasMslAltitude()) {
l.mMslAltitudeMeters = in.readDouble();
}
if (l.hasMslAltitudeAccuracy()) {
l.mMslAltitudeAccuracyMeters = in.readFloat();
}
l.mExtras = Bundle.setDefusable(in.readBundle(), true);
return l;
}
@ -991,6 +1113,12 @@ public class Location implements Parcelable {
if (hasBearingAccuracy()) {
parcel.writeFloat(mBearingAccuracyDegrees);
}
if (hasMslAltitude()) {
parcel.writeDouble(mMslAltitudeMeters);
}
if (hasMslAltitudeAccuracy()) {
parcel.writeFloat(mMslAltitudeAccuracyMeters);
}
parcel.writeBundle(mExtras);
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.location.altitude;
import android.annotation.NonNull;
import android.annotation.WorkerThread;
import android.content.Context;
import android.location.Location;
import com.android.internal.location.altitude.GeoidHeightMap;
import com.android.internal.location.altitude.S2CellIdUtils;
import com.android.internal.location.altitude.nano.MapParamsProto;
import com.android.internal.util.Preconditions;
import java.io.IOException;
/**
* Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
* into ones above Mean Sea Level.
*
* @hide
*/
public final class AltitudeConverter {
private static final double MAX_ABS_VALID_LATITUDE = 90;
private static final double MAX_ABS_VALID_LONGITUDE = 180;
/** Manages a mapping of geoid heights associated with S2 cells. */
private final GeoidHeightMap mGeoidHeightMap = new GeoidHeightMap();
/**
* Creates an instance that manages an independent cache to optimized conversions of locations
* in proximity to one another.
*
* @hide
*/
public AltitudeConverter() {
}
/**
* Throws an {@link IllegalArgumentException} if the {@code location} has an invalid latitude,
* longitude, or altitude above WGS84.
*/
private static void validate(@NonNull Location location) {
Preconditions.checkArgument(
isFiniteAndAtAbsMost(location.getLatitude(), MAX_ABS_VALID_LATITUDE),
"Invalid latitude: %f", location.getLatitude());
Preconditions.checkArgument(
isFiniteAndAtAbsMost(location.getLongitude(), MAX_ABS_VALID_LONGITUDE),
"Invalid longitude: %f", location.getLongitude());
Preconditions.checkArgument(location.hasAltitude(), "Missing altitude above WGS84");
Preconditions.checkArgument(Double.isFinite(location.getAltitude()),
"Invalid altitude above WGS84: %f", location.getAltitude());
}
private static boolean isFiniteAndAtAbsMost(double value, double rhs) {
return Double.isFinite(value) && Math.abs(value) <= rhs;
}
/**
* Returns the four S2 cell IDs for the map square associated with the {@code location}.
*
* <p>The first map cell contains the location, while the others are located horizontally,
* vertically, and diagonally, in that order, with respect to the S2 (i,j) coordinate system. If
* the diagonal map cell does not exist (i.e., the location is near an S2 cube vertex), its
* corresponding ID is set to zero.
*/
@NonNull
private static long[] findMapSquare(@NonNull MapParamsProto params,
@NonNull Location location) {
long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
location.getLongitude());
// (0,0) cell.
long s0 = S2CellIdUtils.getParent(s2CellId, params.mapS2Level);
long[] edgeNeighbors = new long[4];
S2CellIdUtils.getEdgeNeighbors(s0, edgeNeighbors);
// (1,0) cell.
int i1 = S2CellIdUtils.getI(s2CellId) > S2CellIdUtils.getI(s0) ? -1 : 1;
long s1 = edgeNeighbors[i1 + 2];
// (0,1) cell.
int i2 = S2CellIdUtils.getJ(s2CellId) > S2CellIdUtils.getJ(s0) ? 1 : -1;
long s2 = edgeNeighbors[i2 + 1];
// (1,1) cell.
S2CellIdUtils.getEdgeNeighbors(s1, edgeNeighbors);
long s3 = 0;
for (int i = 0; i < edgeNeighbors.length; i++) {
if (edgeNeighbors[i] == s0) {
int i3 = (i + i1 * i2 + edgeNeighbors.length) % edgeNeighbors.length;
s3 = edgeNeighbors[i3] == s2 ? 0 : edgeNeighbors[i3];
break;
}
}
// Reuse edge neighbors' array to avoid an extra allocation.
edgeNeighbors[0] = s0;
edgeNeighbors[1] = s1;
edgeNeighbors[2] = s2;
edgeNeighbors[3] = s3;
return edgeNeighbors;
}
/**
* Adds to {@code location} the bilinearly interpolated Mean Sea Level altitude. In addition, a
* Mean Sea Level altitude accuracy is added if the {@code location} has a valid vertical
* accuracy; otherwise, does not add a corresponding accuracy.
*/
private static void addMslAltitude(@NonNull MapParamsProto params, @NonNull long[] s2CellIds,
@NonNull double[] geoidHeightsMeters, @NonNull Location location) {
long s0 = s2CellIds[0];
double h0 = geoidHeightsMeters[0];
double h1 = geoidHeightsMeters[1];
double h2 = geoidHeightsMeters[2];
double h3 = s2CellIds[3] == 0 ? h0 : geoidHeightsMeters[3];
// Bilinear interpolation on an S2 square of size equal to that of a map cell. wi and wj
// are the normalized [0,1] weights in the i and j directions, respectively, allowing us to
// employ the simplified unit square formulation.
long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
location.getLongitude());
double sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - params.mapS2Level);
double wi = Math.abs(S2CellIdUtils.getI(s2CellId) - S2CellIdUtils.getI(s0)) / sizeIj;
double wj = Math.abs(S2CellIdUtils.getJ(s2CellId) - S2CellIdUtils.getJ(s0)) / sizeIj;
double offsetMeters = h0 + (h1 - h0) * wi + (h2 - h0) * wj + (h3 - h1 - h2 + h0) * wi * wj;
location.setMslAltitudeMeters(location.getAltitude() - offsetMeters);
if (location.hasVerticalAccuracy()) {
double verticalAccuracyMeters = location.getVerticalAccuracyMeters();
if (Double.isFinite(verticalAccuracyMeters) && verticalAccuracyMeters >= 0) {
location.setMslAltitudeAccuracyMeters(
(float) Math.hypot(verticalAccuracyMeters, params.modelRmseMeters));
}
}
}
/**
* Adds a Mean Sea Level altitude to the {@code location}. In addition, adds a Mean Sea Level
* altitude accuracy if the {@code location} has a finite and non-negative vertical accuracy;
* otherwise, does not add a corresponding accuracy.
*
* <p>Must be called off the main thread as data may be loaded from raw assets.
*
* @throws IOException if an I/O error occurs when loading data from raw assets.
* @throws IllegalArgumentException if the {@code location} has an invalid latitude, longitude,
* or altitude above WGS84. Specifically, the latitude must be
* between -90 and 90 (both inclusive), the longitude must be
* between -180 and 180 (both inclusive), and the altitude
* above WGS84 must be finite.
*
* @hide
*/
@WorkerThread
public void addMslAltitudeToLocation(@NonNull Context context, @NonNull Location location)
throws IOException {
validate(location);
MapParamsProto params = GeoidHeightMap.getParams(context);
long[] s2CellIds = findMapSquare(params, location);
double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, context, s2CellIds);
addMslAltitude(params, s2CellIds, geoidHeightsMeters, location);
}
/**
* Same as {@link #addMslAltitudeToLocation(Context, Location)} except that data will not be
* loaded from raw assets. Returns true if a Mean Sea Level altitude is added to the
* {@code location}; otherwise, returns false and leaves the {@code location} unchanged.
*
* @hide
*/
public boolean addMslAltitudeToLocation(@NonNull Location location) {
validate(location);
MapParamsProto params = GeoidHeightMap.getParams();
if (params == null) {
return false;
}
long[] s2CellIds = findMapSquare(params, location);
double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, s2CellIds);
if (geoidHeightsMeters == null) {
return false;
}
addMslAltitude(params, s2CellIds, geoidHeightsMeters, location);
return true;
}
}

View File

@ -0,0 +1,397 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.location.altitude;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.LruCache;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.location.altitude.nano.MapParamsProto;
import com.android.internal.location.altitude.nano.S2TileProto;
import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Objects;
/**
* Manages a mapping of geoid heights associated with S2 cells, referred to as MAP CELLS.
*
* <p>Tiles are used extensively to reduce the number of entries needed to be stored in memory and
* on disk. A tile associates geoid heights with all map cells of a common parent at a specified S2
* level.
*
* <p>Since bilinear interpolation considers at most four map cells at a time, at most four tiles
* are simultaneously stored in memory. These tiles, referred to as CACHE TILES, are each keyed by
* its common parent's S2 cell ID, referred to as a CACHE KEY.
*
* <p>Absent cache tiles needed for interpolation are constructed from larger tiles stored on disk.
* The latter tiles, referred to as DISK TILES, are each keyed by its common parent's S2 cell token,
* referred to as a DISK TOKEN.
*/
public final class GeoidHeightMap {
private static final Object sLock = new Object();
@GuardedBy("sLock")
@Nullable
private static MapParamsProto sParams;
/** Defines a cache large enough to hold all cache tiles needed for interpolation. */
private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4);
/**
* Returns the singleton parameter instance for a spherically projected geoid height map and its
* corresponding tile management.
*/
@NonNull
public static MapParamsProto getParams(@NonNull Context context) throws IOException {
synchronized (sLock) {
if (sParams == null) {
try (InputStream is = context.getApplicationContext().getAssets().open(
"geoid_height_map/map-params.pb")) {
sParams = MapParamsProto.parseFrom(is.readAllBytes());
}
}
return sParams;
}
}
/**
* Same as {@link #getParams(Context)} except that null is returned if the singleton parameter
* instance is not yet initialized.
*/
@Nullable
public static MapParamsProto getParams() {
synchronized (sLock) {
return sParams;
}
}
private static long getCacheKey(@NonNull MapParamsProto params, long s2CellId) {
return S2CellIdUtils.getParent(s2CellId, params.cacheTileS2Level);
}
@NonNull
private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) {
return S2CellIdUtils.getToken(
S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level));
}
/**
* Adds to {@code values} values in the unit interval [0, 1] for the map cells identified by
* {@code s2CellIds}. Returns true if values are present for all non-zero IDs; otherwise,
* returns false and adds NaNs for absent values.
*/
private static boolean getUnitIntervalValues(@NonNull MapParamsProto params,
@NonNull TileFunction tileFunction,
@NonNull long[] s2CellIds, @NonNull double[] values) {
int len = s2CellIds.length;
S2TileProto[] tiles = new S2TileProto[len];
for (int i = 0; i < len; i++) {
if (s2CellIds[i] != 0) {
long cacheKey = getCacheKey(params, s2CellIds[i]);
tiles[i] = tileFunction.getTile(cacheKey);
}
values[i] = Double.NaN;
}
for (int i = 0; i < len; i++) {
if (tiles[i] == null || !Double.isNaN(values[i])) {
continue;
}
mergeByteBufferValues(params, s2CellIds, tiles, i, values);
mergeByteJpegValues(params, s2CellIds, tiles, i, values);
mergeBytePngValues(params, s2CellIds, tiles, i, values);
}
boolean allFound = true;
for (int i = 0; i < len; i++) {
if (s2CellIds[i] == 0) {
continue;
}
if (Double.isNaN(values[i])) {
allFound = false;
} else {
values[i] = (((int) values[i]) & 0xFF) / 255.0;
}
}
return allFound;
}
@SuppressWarnings("ReferenceEquality")
private static void mergeByteBufferValues(@NonNull MapParamsProto params,
@NonNull long[] s2CellIds,
@NonNull S2TileProto[] tiles,
int tileIndex, @NonNull double[] values) {
byte[] bytes = tiles[tileIndex].byteBuffer;
if (bytes == null || bytes.length == 0) {
return;
}
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
int tileS2Level = params.mapS2Level - Integer.numberOfTrailingZeros(byteBuffer.limit()) / 2;
int numBitsLeftOfTile = 2 * tileS2Level + 3;
for (int i = tileIndex; i < tiles.length; i++) {
if (tiles[i] != tiles[tileIndex]) {
continue;
}
long maskedS2CellId = s2CellIds[i] & (-1L >>> numBitsLeftOfTile);
int numBitsRightOfMap = 2 * (S2CellIdUtils.MAX_LEVEL - params.mapS2Level) + 1;
int bufferIndex = (int) (maskedS2CellId >>> numBitsRightOfMap);
values[i] = Double.isNaN(values[i]) ? 0 : values[i];
values[i] += ((int) byteBuffer.get(bufferIndex)) & 0xFF;
}
}
private static void mergeByteJpegValues(@NonNull MapParamsProto params,
@NonNull long[] s2CellIds,
@NonNull S2TileProto[] tiles,
int tileIndex, @NonNull double[] values) {
mergeByteImageValues(params, tiles[tileIndex].byteJpeg, s2CellIds, tiles, tileIndex,
values);
}
private static void mergeBytePngValues(@NonNull MapParamsProto params,
@NonNull long[] s2CellIds,
@NonNull S2TileProto[] tiles,
int tileIndex, @NonNull double[] values) {
mergeByteImageValues(params, tiles[tileIndex].bytePng, s2CellIds, tiles, tileIndex, values);
}
@SuppressWarnings("ReferenceEquality")
private static void mergeByteImageValues(@NonNull MapParamsProto params, @NonNull byte[] bytes,
@NonNull long[] s2CellIds,
@NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values) {
if (bytes == null || bytes.length == 0) {
return;
}
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap == null) {
return;
}
for (int i = tileIndex; i < tiles.length; i++) {
if (s2CellIds[i] == 0 || tiles[i] != tiles[tileIndex]) {
continue;
}
values[i] = Double.isNaN(values[i]) ? 0 : values[i];
values[i] += bitmap.getPixel(getIndexX(params, s2CellIds[i], bitmap.getWidth()),
getIndexY(params, s2CellIds[i], bitmap.getHeight())) & 0xFF;
}
}
/** Returns the X index for an S2 cell within an S2 tile image of specified width. */
private static int getIndexX(@NonNull MapParamsProto params, long s2CellId, int width) {
return getIndexXOrY(params, S2CellIdUtils.getI(s2CellId), width);
}
/** Returns the Y index for an S2 cell within an S2 tile image of specified height. */
private static int getIndexY(@NonNull MapParamsProto params, long s2CellId, int height) {
return getIndexXOrY(params, S2CellIdUtils.getJ(s2CellId), height);
}
private static int getIndexXOrY(@NonNull MapParamsProto params, int iOrJ, int widthOrHeight) {
return (iOrJ >> (S2CellIdUtils.MAX_LEVEL - params.mapS2Level)) % widthOrHeight;
}
/**
* Throws an {@link IllegalArgumentException} if the {@code s2CellIds} has an invalid length or
* ID.
*/
private static void validate(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) {
Preconditions.checkArgument(s2CellIds.length == 4);
for (long s2CellId : s2CellIds) {
Preconditions.checkArgument(
s2CellId == 0 || S2CellIdUtils.getLevel(s2CellId) == params.mapS2Level);
}
}
/**
* Returns the geoid heights in meters associated with the map cells identified by
* {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for a
* non-zero ID.
*/
@NonNull
public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context,
@NonNull long[] s2CellIds) throws IOException {
validate(params, s2CellIds);
double[] heightsMeters = new double[s2CellIds.length];
if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
return heightsMeters;
}
TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds);
if (getGeoidHeights(params, loadedTiles, s2CellIds, heightsMeters)) {
return heightsMeters;
}
throw new IOException("Unable to calculate geoid heights from raw assets.");
}
/**
* Same as {@link #readGeoidHeights(MapParamsProto, Context, long[])} except that data will not
* be loaded from raw assets. Returns the heights if present for all non-zero IDs; otherwise,
* returns null.
*/
@Nullable
public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) {
validate(params, s2CellIds);
double[] heightsMeters = new double[s2CellIds.length];
if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
return heightsMeters;
}
return null;
}
/**
* Adds to {@code heightsMeters} the geoid heights in meters associated with the map cells
* identified by {@code s2CellIds}. Returns true if heights are present for all non-zero IDs;
* otherwise, returns false and adds NaNs for absent heights.
*/
private boolean getGeoidHeights(@NonNull MapParamsProto params,
@NonNull TileFunction tileFunction, @NonNull long[] s2CellIds,
@NonNull double[] heightsMeters) {
boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, heightsMeters);
for (int i = 0; i < heightsMeters.length; i++) {
// NaNs are properly preserved.
heightsMeters[i] *= params.modelAMeters;
heightsMeters[i] += params.modelBMeters;
}
return allFound;
}
@NonNull
private TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
@NonNull Context context, @NonNull long[] s2CellIds) throws IOException {
int len = s2CellIds.length;
// Enable batch loading by finding all cache keys upfront.
long[] cacheKeys = new long[len];
for (int i = 0; i < len; i++) {
if (s2CellIds[i] == 0) {
continue;
}
cacheKeys[i] = getCacheKey(params, s2CellIds[i]);
}
// Attempt to load tiles from cache.
S2TileProto[] loadedTiles = new S2TileProto[len];
String[] diskTokens = new String[len];
for (int i = 0; i < len; i++) {
if (s2CellIds[i] == 0 || diskTokens[i] != null) {
continue;
}
loadedTiles[i] = mCacheTiles.get(cacheKeys[i]);
diskTokens[i] = getDiskToken(params, cacheKeys[i]);
// Batch across common cache key.
for (int j = i + 1; j < len; j++) {
if (cacheKeys[j] == cacheKeys[i]) {
loadedTiles[j] = loadedTiles[i];
diskTokens[j] = diskTokens[i];
}
}
}
// Attempt to load tiles from disk.
for (int i = 0; i < len; i++) {
if (s2CellIds[i] == 0 || loadedTiles[i] != null) {
continue;
}
S2TileProto tile;
try (InputStream is = context.getApplicationContext().getAssets().open(
"geoid_height_map/tile-" + diskTokens[i] + ".pb")) {
tile = S2TileProto.parseFrom(is.readAllBytes());
}
mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles);
}
return cacheKey -> {
for (int i = 0; i < cacheKeys.length; i++) {
if (cacheKeys[i] == cacheKey) {
return loadedTiles[i];
}
}
return null;
};
}
private void mergeFromDiskTile(@NonNull MapParamsProto params, @NonNull S2TileProto diskTile,
@NonNull long[] cacheKeys, @NonNull String[] diskTokens, int diskTokenIndex,
@NonNull S2TileProto[] loadedTiles) throws IOException {
int len = cacheKeys.length;
int numMapCellsPerCacheTile = 1 << (2 * (params.mapS2Level - params.cacheTileS2Level));
// Reusable arrays.
long[] s2CellIds = new long[numMapCellsPerCacheTile];
double[] values = new double[numMapCellsPerCacheTile];
// Each cache key identifies a different sub-tile of the same disk tile.
TileFunction diskTileFunction = cacheKey -> diskTile;
for (int i = diskTokenIndex; i < len; i++) {
if (!Objects.equals(diskTokens[i], diskTokens[diskTokenIndex])
|| loadedTiles[i] != null) {
continue;
}
// Find all map cells within the current cache tile.
long s2CellId = S2CellIdUtils.getTraversalStart(cacheKeys[i], params.mapS2Level);
for (int j = 0; j < numMapCellsPerCacheTile; j++) {
s2CellIds[j] = s2CellId;
s2CellId = S2CellIdUtils.getTraversalNext(s2CellId);
}
if (!getUnitIntervalValues(params, diskTileFunction, s2CellIds, values)) {
throw new IOException("Corrupted disk tile of disk token: " + diskTokens[i]);
}
loadedTiles[i] = new S2TileProto();
loadedTiles[i].byteBuffer = new byte[numMapCellsPerCacheTile];
for (int j = 0; j < numMapCellsPerCacheTile; j++) {
loadedTiles[i].byteBuffer[j] = (byte) Math.round(values[j] * 0xFF);
}
// Batch across common cache key.
for (int j = i + 1; j < len; j++) {
if (cacheKeys[j] == cacheKeys[i]) {
loadedTiles[j] = loadedTiles[i];
}
}
// Side load into tile cache.
mCacheTiles.put(cacheKeys[i], loadedTiles[i]);
}
}
/** Defines a function-like object to retrieve tiles for cache keys. */
private interface TileFunction {
@Nullable
S2TileProto getTile(long cacheKey);
}
}

View File

@ -0,0 +1,653 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.location.altitude;
import android.annotation.NonNull;
import java.util.Arrays;
import java.util.Locale;
/**
* Provides lightweight S2 cell ID utilities without traditional geometry dependencies.
*
* <p>See <a href="https://s2geometry.io/">the S2 Geometry Library website</a> for more details.
*/
public final class S2CellIdUtils {
/** The level of all leaf S2 cells. */
public static final int MAX_LEVEL = 30;
private static final int MAX_SIZE = 1 << MAX_LEVEL;
private static final double ONE_OVER_MAX_SIZE = 1.0 / MAX_SIZE;
private static final int NUM_FACES = 6;
private static final int POS_BITS = 2 * MAX_LEVEL + 1;
private static final int SWAP_MASK = 0x1;
private static final int LOOKUP_BITS = 4;
private static final int LOOKUP_MASK = (1 << LOOKUP_BITS) - 1;
private static final int INVERT_MASK = 0x2;
private static final int LEAF_MASK = 0x1;
private static final int[] LOOKUP_POS = new int[1 << (2 * LOOKUP_BITS + 2)];
private static final int[] LOOKUP_IJ = new int[1 << (2 * LOOKUP_BITS + 2)];
private static final int[] POS_TO_ORIENTATION = {SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK};
private static final int[][] POS_TO_IJ =
{{0, 1, 3, 2}, {0, 2, 3, 1}, {3, 2, 0, 1}, {3, 1, 0, 2}};
private static final double UV_LIMIT = calculateUvLimit();
private static final UvTransform[] UV_TRANSFORMS = createUvTransforms();
private static final XyzTransform[] XYZ_TRANSFORMS = createXyzTransforms();
// Used to encode (i, j, o) coordinates into primitive longs.
private static final int I_SHIFT = 33;
private static final int J_SHIFT = 2;
private static final long J_MASK = (1L << 31) - 1;
static {
initLookupCells();
}
/** Prevents instantiation. */
private S2CellIdUtils() {
}
/**
* Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
* degrees.
*/
public static long fromLatLngDegrees(double latDegrees, double lngDegrees) {
return fromLatLngRadians(Math.toRadians(latDegrees), Math.toRadians(lngDegrees));
}
/**
* Returns the ID of the parent of the specified S2 cell at the specified parent level.
* Behavior is undefined for invalid S2 cell IDs or parent levels not in
* [0, {@code getLevel(s2CellId)}[.
*/
public static long getParent(long s2CellId, int level) {
long newLsb = getLowestOnBitForLevel(level);
return (s2CellId & -newLsb) | newLsb;
}
/**
* Inserts into {@code neighbors} the four S2 cell IDs corresponding to the neighboring
* cells adjacent across the specified cell's four edges. This array must be of minimum
* length four, and elements at the tail end of the array not corresponding to a neighbor
* are set to zero. A reference to this array is returned.
*
* <p>Inserts in the order of down, right, up, and left directions, in that order. All
* neighbors are guaranteed to be distinct.
*/
public static void getEdgeNeighbors(long s2CellId, @NonNull long[] neighbors) {
int level = getLevel(s2CellId);
int size = levelToSizeIj(level);
int face = getFace(s2CellId);
long ijo = toIjo(s2CellId);
int i = ijoToI(ijo);
int j = ijoToJ(ijo);
int iPlusSize = i + size;
int iMinusSize = i - size;
int jPlusSize = j + size;
int jMinusSize = j - size;
boolean iPlusSizeLtMax = iPlusSize < MAX_SIZE;
boolean iMinusSizeGteZero = iMinusSize >= 0;
boolean jPlusSizeLtMax = jPlusSize < MAX_SIZE;
boolean jMinusSizeGteZero = jMinusSize >= 0;
int index = 0;
// Down direction.
neighbors[index++] = getParent(fromFijSame(face, i, jMinusSize, jMinusSizeGteZero),
level);
// Right direction.
neighbors[index++] = getParent(fromFijSame(face, iPlusSize, j, iPlusSizeLtMax), level);
// Up direction.
neighbors[index++] = getParent(fromFijSame(face, i, jPlusSize, jPlusSizeLtMax), level);
// Left direction.
neighbors[index++] = getParent(fromFijSame(face, iMinusSize, j, iMinusSizeGteZero),
level);
// Pad end of neighbor array with zeros.
Arrays.fill(neighbors, index, neighbors.length, 0);
}
/** Returns the "i" coordinate for the specified S2 cell. */
public static int getI(long s2CellId) {
return ijoToI(toIjo(s2CellId));
}
/** Returns the "j" coordinate for the specified S2 cell. */
public static int getJ(long s2CellId) {
return ijoToJ(toIjo(s2CellId));
}
/**
* Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
* radians.
*/
private static long fromLatLngRadians(double latRadians, double lngRadians) {
double cosLat = Math.cos(latRadians);
double x = Math.cos(lngRadians) * cosLat;
double y = Math.sin(lngRadians) * cosLat;
double z = Math.sin(latRadians);
return fromXyz(x, y, z);
}
/**
* Returns the level of the specified S2 cell. The returned level is in [0, 30] for valid
* S2 cell IDs. Behavior is undefined for invalid S2 cell IDs.
*/
static int getLevel(long s2CellId) {
if (isLeaf(s2CellId)) {
return MAX_LEVEL;
}
return MAX_LEVEL - (Long.numberOfTrailingZeros(s2CellId) >> 1);
}
/** Returns the lowest-numbered bit that is on for the specified S2 cell. */
static long getLowestOnBit(long s2CellId) {
return s2CellId & -s2CellId;
}
/** Returns the lowest-numbered bit that is on for any S2 cell on the specified level. */
static long getLowestOnBitForLevel(int level) {
return 1L << (2 * (MAX_LEVEL - level));
}
/**
* Returns the ID of the first S2 cell in a traversal of the children S2 cells at the specified
* level, in Hilbert curve order.
*/
static long getTraversalStart(long s2CellId, int level) {
return s2CellId - getLowestOnBit(s2CellId) + getLowestOnBitForLevel(level);
}
/** Returns the ID of the next S2 cell at the same level along the Hilbert curve. */
static long getTraversalNext(long s2CellId) {
return s2CellId + (getLowestOnBit(s2CellId) << 1);
}
/**
* Encodes the S2 cell id to compact text strings suitable for display or indexing. Cells at
* lower levels (i.e., larger cells) are encoded into fewer characters.
*/
@NonNull
static String getToken(long s2CellId) {
if (s2CellId == 0) {
return "X";
}
// Convert to a hex string with as many digits as necessary.
String hex = Long.toHexString(s2CellId).toLowerCase(Locale.US);
// Prefix 0s to get a length 16 string.
String padded = padStart(hex);
// Trim zeroes off the end.
return padded.replaceAll("0*$", "");
}
private static String padStart(String string) {
if (string.length() >= 16) {
return string;
}
return "0".repeat(16 - string.length()) + string;
}
/** Returns the leaf S2 cell ID of the specified (x, y, z) coordinate. */
private static long fromXyz(double x, double y, double z) {
int face = xyzToFace(x, y, z);
UvTransform uvTransform = UV_TRANSFORMS[face];
double u = uvTransform.xyzToU(x, y, z);
double v = uvTransform.xyzToV(x, y, z);
return fromFuv(face, u, v);
}
/** Returns the leaf S2 cell ID of the specified (face, u, v) coordinate. */
private static long fromFuv(int face, double u, double v) {
int i = uToI(u);
int j = vToJ(v);
return fromFij(face, i, j);
}
/** Returns the leaf S2 cell ID of the specified (face, i, j) coordinate. */
private static long fromFij(int face, int i, int j) {
int bits = (face & SWAP_MASK);
// Update most significant bits.
long msb = ((long) face) << (POS_BITS - 33);
for (int k = 7; k >= 4; --k) {
bits = lookupBits(i, j, k, bits);
msb = updateBits(msb, k, bits);
bits = maskBits(bits);
}
// Update least significant bits.
long lsb = 0;
for (int k = 3; k >= 0; --k) {
bits = lookupBits(i, j, k, bits);
lsb = updateBits(lsb, k, bits);
bits = maskBits(bits);
}
return (((msb << 32) + lsb) << 1) + 1;
}
private static long fromFijWrap(int face, int i, int j) {
double u = iToU(i);
double v = jToV(j);
XyzTransform xyzTransform = XYZ_TRANSFORMS[face];
double x = xyzTransform.uvToX(u, v);
double y = xyzTransform.uvToY(u, v);
double z = xyzTransform.uvToZ(u, v);
int newFace = xyzToFace(x, y, z);
UvTransform uvTransform = UV_TRANSFORMS[newFace];
double newU = uvTransform.xyzToU(x, y, z);
double newV = uvTransform.xyzToV(x, y, z);
int newI = uShiftIntoI(newU);
int newJ = vShiftIntoJ(newV);
return fromFij(newFace, newI, newJ);
}
private static long fromFijSame(int face, int i, int j, boolean isSameFace) {
if (isSameFace) {
return fromFij(face, i, j);
}
return fromFijWrap(face, i, j);
}
/**
* Returns the face associated with the specified (x, y, z) coordinate. For a coordinate
* on a face boundary, the returned face is arbitrary but repeatable.
*/
private static int xyzToFace(double x, double y, double z) {
double absX = Math.abs(x);
double absY = Math.abs(y);
double absZ = Math.abs(z);
if (absX > absY) {
if (absX > absZ) {
return (x < 0) ? 3 : 0;
}
return (z < 0) ? 5 : 2;
}
if (absY > absZ) {
return (y < 0) ? 4 : 1;
}
return (z < 0) ? 5 : 2;
}
private static int uToI(double u) {
double s;
if (u >= 0) {
s = 0.5 * Math.sqrt(1 + 3 * u);
} else {
s = 1 - 0.5 * Math.sqrt(1 - 3 * u);
}
return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
}
private static int vToJ(double v) {
// Same calculation as uToI.
return uToI(v);
}
private static int lookupBits(int i, int j, int k, int bits) {
bits += ((i >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << (LOOKUP_BITS + 2);
bits += ((j >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << 2;
return LOOKUP_POS[bits];
}
private static long updateBits(long sb, int k, int bits) {
return sb | ((((long) bits) >> 2) << ((k & 0x3) * 2 * LOOKUP_BITS));
}
private static int maskBits(int bits) {
return bits & (SWAP_MASK | INVERT_MASK);
}
private static int getFace(long s2CellId) {
return (int) (s2CellId >>> POS_BITS);
}
private static boolean isLeaf(long s2CellId) {
return ((int) s2CellId & LEAF_MASK) != 0;
}
private static double iToU(int i) {
int satI = Math.max(-1, Math.min(MAX_SIZE, i));
return Math.max(
-UV_LIMIT,
Math.min(UV_LIMIT, ONE_OVER_MAX_SIZE * ((satI << 1) + 1 - MAX_SIZE)));
}
private static double jToV(int j) {
// Same calculation as iToU.
return iToU(j);
}
private static long toIjo(long s2CellId) {
int face = getFace(s2CellId);
int bits = face & SWAP_MASK;
int i = 0;
int j = 0;
for (int k = 7; k >= 0; --k) {
int nbits = (k == 7) ? (MAX_LEVEL - 7 * LOOKUP_BITS) : LOOKUP_BITS;
bits += ((int) (s2CellId >>> (k * 2 * LOOKUP_BITS + 1)) & ((1 << (2 * nbits))
- 1)) << 2;
bits = LOOKUP_IJ[bits];
i += (bits >> (LOOKUP_BITS + 2)) << (k * LOOKUP_BITS);
j += ((bits >> 2) & ((1 << LOOKUP_BITS) - 1)) << (k * LOOKUP_BITS);
bits &= (SWAP_MASK | INVERT_MASK);
}
int orientation =
((getLowestOnBit(s2CellId) & 0x1111111111111110L) != 0) ? (bits ^ SWAP_MASK)
: bits;
return (((long) i) << I_SHIFT) | (((long) j) << J_SHIFT) | orientation;
}
private static int ijoToI(long ijo) {
return (int) (ijo >>> I_SHIFT);
}
private static int ijoToJ(long ijo) {
return (int) ((ijo >>> J_SHIFT) & J_MASK);
}
private static int uShiftIntoI(double u) {
double s = 0.5 * (u + 1);
return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
}
private static int vShiftIntoJ(double v) {
// Same calculation as uShiftIntoI.
return uShiftIntoI(v);
}
private static int levelToSizeIj(int level) {
return 1 << (MAX_LEVEL - level);
}
private static void initLookupCells() {
initLookupCell(0, 0, 0, 0, 0, 0);
initLookupCell(0, 0, 0, SWAP_MASK, 0, SWAP_MASK);
initLookupCell(0, 0, 0, INVERT_MASK, 0, INVERT_MASK);
initLookupCell(0, 0, 0, SWAP_MASK | INVERT_MASK, 0, SWAP_MASK | INVERT_MASK);
}
private static void initLookupCell(
int level, int i, int j, int origOrientation, int pos, int orientation) {
if (level == LOOKUP_BITS) {
int ij = (i << LOOKUP_BITS) + j;
LOOKUP_POS[(ij << 2) + origOrientation] = (pos << 2) + orientation;
LOOKUP_IJ[(pos << 2) + origOrientation] = (ij << 2) + orientation;
} else {
level++;
i <<= 1;
j <<= 1;
pos <<= 2;
for (int subPos = 0; subPos < 4; subPos++) {
int ij = POS_TO_IJ[orientation][subPos];
int orientationMask = POS_TO_ORIENTATION[subPos];
initLookupCell(
level,
i + (ij >>> 1),
j + (ij & 0x1),
origOrientation,
pos + subPos,
orientation ^ orientationMask);
}
}
}
private static double calculateUvLimit() {
double machEps = 1.0;
do {
machEps /= 2.0f;
} while ((1.0 + (machEps / 2.0)) != 1.0);
return 1.0 + machEps;
}
@NonNull
private static UvTransform[] createUvTransforms() {
UvTransform[] uvTransforms = new UvTransform[NUM_FACES];
uvTransforms[0] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return y / x;
}
@Override
public double xyzToV(double x, double y, double z) {
return z / x;
}
};
uvTransforms[1] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return -x / y;
}
@Override
public double xyzToV(double x, double y, double z) {
return z / y;
}
};
uvTransforms[2] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return -x / z;
}
@Override
public double xyzToV(double x, double y, double z) {
return -y / z;
}
};
uvTransforms[3] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return z / x;
}
@Override
public double xyzToV(double x, double y, double z) {
return y / x;
}
};
uvTransforms[4] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return z / y;
}
@Override
public double xyzToV(double x, double y, double z) {
return -x / y;
}
};
uvTransforms[5] =
new UvTransform() {
@Override
public double xyzToU(double x, double y, double z) {
return -y / z;
}
@Override
public double xyzToV(double x, double y, double z) {
return -x / z;
}
};
return uvTransforms;
}
@NonNull
private static XyzTransform[] createXyzTransforms() {
XyzTransform[] xyzTransforms = new XyzTransform[NUM_FACES];
xyzTransforms[0] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return 1;
}
@Override
public double uvToY(double u, double v) {
return u;
}
@Override
public double uvToZ(double u, double v) {
return v;
}
};
xyzTransforms[1] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return -u;
}
@Override
public double uvToY(double u, double v) {
return 1;
}
@Override
public double uvToZ(double u, double v) {
return v;
}
};
xyzTransforms[2] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return -u;
}
@Override
public double uvToY(double u, double v) {
return -v;
}
@Override
public double uvToZ(double u, double v) {
return 1;
}
};
xyzTransforms[3] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return -1;
}
@Override
public double uvToY(double u, double v) {
return -v;
}
@Override
public double uvToZ(double u, double v) {
return -u;
}
};
xyzTransforms[4] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return v;
}
@Override
public double uvToY(double u, double v) {
return -1;
}
@Override
public double uvToZ(double u, double v) {
return -u;
}
};
xyzTransforms[5] =
new XyzTransform() {
@Override
public double uvToX(double u, double v) {
return v;
}
@Override
public double uvToY(double u, double v) {
return u;
}
@Override
public double uvToZ(double u, double v) {
return -1;
}
};
return xyzTransforms;
}
/**
* Transform from (x, y, z) coordinates to (u, v) coordinates, indexed by face. For a
* (x, y, z) coordinate within a face, each element of the resulting (u, v) coordinate
* should lie in the inclusive range [-1, 1], with the face center having a (u, v)
* coordinate equal to (0, 0).
*/
private interface UvTransform {
/**
* Returns for the specified (x, y, z) coordinate the corresponding u-coordinate
* (which may lie outside the range [-1, 1]).
*/
double xyzToU(double x, double y, double z);
/**
* Returns for the specified (x, y, z) coordinate the corresponding v-coordinate
* (which may lie outside the range [-1, 1]).
*/
double xyzToV(double x, double y, double z);
}
/**
* Transform from (u, v) coordinates to (x, y, z) coordinates, indexed by face. The
* resulting vectors are not necessarily of unit length.
*/
private interface XyzTransform {
/** Returns for the specified (u, v) coordinate the corresponding x-coordinate. */
double uvToX(double u, double v);
/** Returns for the specified (u, v) coordinate the corresponding y-coordinate. */
double uvToY(double u, double v);
/** Returns for the specified (u, v) coordinate the corresponding z-coordinate. */
double uvToZ(double u, double v);
}
}

View File

@ -2,3 +2,4 @@ per-file gnss.proto = file:/services/core/java/com/android/server/location/OWNER
per-file wifi.proto = file:/wifi/OWNERS
per-file camera.proto = file:/services/core/java/com/android/server/camera/OWNERS
per-file system_messages.proto = file:/core/res/OWNERS
per-file altitude.proto = file:/location/OWNERS

54
proto/src/altitude.proto Normal file
View File

@ -0,0 +1,54 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto2";
package com.android.internal.location.altitude;
option java_package = "com.android.internal.location.altitude";
option java_multiple_files = true;
// Defines parameters for a spherically projected geoid map and corresponding
// tile management.
message MapParamsProto {
// Defines the resolution of the map in terms of an S2 level.
optional int32 map_s2_level = 1;
// Defines the resolution of the tiles in cache in terms of an S2 level.
optional int32 cache_tile_s2_level = 2;
// Defines the resolution of the tiles on disk in terms of an S2 level.
optional int32 disk_tile_s2_level = 3;
// Defines the `a` coefficient in the expression `a * map_value + b` used to
// calculate a geoid height in meters.
optional double model_a_meters = 4;
// Defines the `b` coefficient in the expression `a * map_value + b` used to
// calculate a geoid height in meters.
optional double model_b_meters = 5;
// Defines the root mean square error in meters of the geoid height.
optional double model_rmse_meters = 6;
}
// A single tile associating values in the unit interval [0, 1] to map cells.
message S2TileProto {
// The S2 token associated with the common parent of all map cells in this
// tile.
optional string tile_key = 1;
// Encoded data that merge into a value in the unit interval [0, 1] for each
// map cell in this tile.
optional bytes byte_buffer = 2;
optional bytes byte_jpeg = 3;
optional bytes byte_png = 4;
}

View File

@ -0,0 +1,172 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.location.altitude;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.location.Location;
import android.location.altitude.AltitudeConverter;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class AltitudeConverterTest {
private AltitudeConverter mAltitudeConverter;
private Context mContext;
@Before
public void setUp() {
mAltitudeConverter = new AltitudeConverter();
mContext = ApplicationProvider.getApplicationContext();
}
@Test
public void testAddMslAltitudeToLocation_expectedBehavior() throws IOException {
// Interpolates between bffffc, 955554, and 000004.
Location location = new Location("");
location.setLatitude(-35.246789);
location.setLongitude(-44.962683);
location.setAltitude(-1);
location.setVerticalAccuracyMeters(1);
// Requires data to be loaded from raw assets.
assertThat(mAltitudeConverter.addMslAltitudeToLocation(location)).isFalse();
assertThat(location.hasMslAltitude()).isFalse();
assertThat(location.hasMslAltitudeAccuracy()).isFalse();
// Loads data from raw assets.
mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(5.1076);
assertThat(location.getMslAltitudeAccuracyMeters()).isGreaterThan(1f);
assertThat(location.getMslAltitudeAccuracyMeters()).isLessThan(1.1f);
// Again interpolates between bffffc, 955554, and 000004.
location = new Location("");
location.setLatitude(-35.246789);
location.setLongitude(-44.962683);
location.setAltitude(-1);
location.setVerticalAccuracyMeters(1);
// Requires no data to be loaded from raw assets.
assertThat(mAltitudeConverter.addMslAltitudeToLocation(location)).isTrue();
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(5.1076);
assertThat(location.getMslAltitudeAccuracyMeters()).isGreaterThan(1f);
assertThat(location.getMslAltitudeAccuracyMeters()).isLessThan(1.1f);
// Results in same outcome.
mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(5.1076);
assertThat(location.getMslAltitudeAccuracyMeters()).isGreaterThan(1f);
assertThat(location.getMslAltitudeAccuracyMeters()).isLessThan(1.1f);
// Interpolate between 955554, 000004, 00000c, and 95554c - no vertical accuracy.
location = new Location("");
location.setLatitude(-35.176383);
location.setLongitude(-44.962683);
location.setAltitude(-1);
location.setVerticalAccuracyMeters(-1); // Invalid vertical accuracy
// Requires no data to be loaded from raw assets.
assertThat(mAltitudeConverter.addMslAltitudeToLocation(location)).isTrue();
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(5.1919);
assertThat(location.hasMslAltitudeAccuracy()).isFalse();
// Results in same outcome.
mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(5.1919);
assertThat(location.hasMslAltitudeAccuracy()).isFalse();
// Interpolates somewhere else more interesting, i.e., Hawaii.
location = new Location("");
location.setLatitude(19.545519);
location.setLongitude(-155.998774);
location.setAltitude(-1);
location.setVerticalAccuracyMeters(1);
// Requires data to be loaded from raw assets.
assertThat(mAltitudeConverter.addMslAltitudeToLocation(location)).isFalse();
assertThat(location.hasMslAltitude()).isFalse();
assertThat(location.hasMslAltitudeAccuracy()).isFalse();
// Loads data from raw assets.
mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
assertThat(location.getMslAltitudeMeters()).isWithin(2).of(-19.2359);
assertThat(location.getMslAltitudeAccuracyMeters()).isGreaterThan(1f);
assertThat(location.getMslAltitudeAccuracyMeters()).isLessThan(1.1f);
}
@Test
public void testAddMslAltitudeToLocation_invalidLatitudeThrows() {
Location location = new Location("");
location.setLongitude(-44.962683);
location.setAltitude(-1);
location.setLatitude(Double.NaN);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setLatitude(91);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setLatitude(-91);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
}
@Test
public void testAddMslAltitudeToLocation_invalidLongitudeThrows() {
Location location = new Location("");
location.setLatitude(-35.246789);
location.setAltitude(-1);
location.setLongitude(Double.NaN);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setLongitude(181);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setLongitude(-181);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
}
@Test
public void testAddMslAltitudeToLocation_invalidAltitudeThrows() {
Location location = new Location("");
location.setLatitude(-35.246789);
location.setLongitude(-44.962683);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setAltitude(Double.NaN);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
location.setAltitude(Double.POSITIVE_INFINITY);
assertThrows(IllegalArgumentException.class,
() -> mAltitudeConverter.addMslAltitudeToLocation(location));
}
}