Merge "Backport implementation of AltitudeConverter to T."
This commit is contained in:
2
core/res/assets/geoid_height_map/README.md
Normal file
2
core/res/assets/geoid_height_map/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
These binary protos are generated at runtime from the text protos in ../../geoid_height_map_assets
|
||||||
|
and using aprotoc.
|
||||||
BIN
core/res/assets/geoid_height_map/map-params.pb
Normal file
BIN
core/res/assets/geoid_height_map/map-params.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-1.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-1.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-3.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-3.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-5.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-5.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-7.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-7.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-9.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-9.pb
Normal file
Binary file not shown.
BIN
core/res/assets/geoid_height_map/tile-b.pb
Normal file
BIN
core/res/assets/geoid_height_map/tile-b.pb
Normal file
Binary file not shown.
8
core/res/geoid_height_map_assets/README.md
Normal file
8
core/res/geoid_height_map_assets/README.md
Normal 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.
|
||||||
6
core/res/geoid_height_map_assets/map-params.textpb
Normal file
6
core/res/geoid_height_map_assets/map-params.textpb
Normal 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
|
||||||
3
core/res/geoid_height_map_assets/tile-1.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-1.textpb
Normal file
File diff suppressed because one or more lines are too long
3
core/res/geoid_height_map_assets/tile-3.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-3.textpb
Normal file
File diff suppressed because one or more lines are too long
3
core/res/geoid_height_map_assets/tile-5.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-5.textpb
Normal file
File diff suppressed because one or more lines are too long
3
core/res/geoid_height_map_assets/tile-7.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-7.textpb
Normal file
File diff suppressed because one or more lines are too long
3
core/res/geoid_height_map_assets/tile-9.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-9.textpb
Normal file
File diff suppressed because one or more lines are too long
3
core/res/geoid_height_map_assets/tile-b.textpb
Normal file
3
core/res/geoid_height_map_assets/tile-b.textpb
Normal file
File diff suppressed because one or more lines are too long
@@ -47,6 +47,15 @@ import java.util.StringTokenizer;
|
|||||||
* <p>All locations generated through {@link LocationManager} are guaranteed to have a valid
|
* <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
|
* latitude, longitude, timestamp (both Unix epoch time and elapsed realtime since boot), and
|
||||||
* accuracy. All other parameters are optional.
|
* 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 {
|
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_SPEED_ACCURACY_MASK = 1 << 6;
|
||||||
private static final int HAS_BEARING_ACCURACY_MASK = 1 << 7;
|
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_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
|
// Cached data to make bearing/distance computations more efficient for the case
|
||||||
// where distanceTo and bearingTo are called in sequence. Assume this typically happens
|
// 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 mSpeedAccuracyMetersPerSecond;
|
||||||
private float mBearingDegrees;
|
private float mBearingDegrees;
|
||||||
private float mBearingAccuracyDegrees;
|
private float mBearingAccuracyDegrees;
|
||||||
|
private double mMslAltitudeMeters;
|
||||||
|
private float mMslAltitudeAccuracyMeters;
|
||||||
|
|
||||||
private Bundle mExtras = null;
|
private Bundle mExtras = null;
|
||||||
|
|
||||||
@@ -156,6 +169,8 @@ public class Location implements Parcelable {
|
|||||||
mSpeedAccuracyMetersPerSecond = location.mSpeedAccuracyMetersPerSecond;
|
mSpeedAccuracyMetersPerSecond = location.mSpeedAccuracyMetersPerSecond;
|
||||||
mBearingDegrees = location.mBearingDegrees;
|
mBearingDegrees = location.mBearingDegrees;
|
||||||
mBearingAccuracyDegrees = location.mBearingAccuracyDegrees;
|
mBearingAccuracyDegrees = location.mBearingAccuracyDegrees;
|
||||||
|
mMslAltitudeMeters = location.mMslAltitudeMeters;
|
||||||
|
mMslAltitudeAccuracyMeters = location.mMslAltitudeAccuracyMeters;
|
||||||
mExtras = (location.mExtras == null) ? null : new Bundle(location.mExtras);
|
mExtras = (location.mExtras == null) ? null : new Bundle(location.mExtras);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +193,8 @@ public class Location implements Parcelable {
|
|||||||
mAltitudeAccuracyMeters = 0;
|
mAltitudeAccuracyMeters = 0;
|
||||||
mSpeedAccuracyMetersPerSecond = 0;
|
mSpeedAccuracyMetersPerSecond = 0;
|
||||||
mBearingAccuracyDegrees = 0;
|
mBearingAccuracyDegrees = 0;
|
||||||
|
mMslAltitudeMeters = 0;
|
||||||
|
mMslAltitudeAccuracyMeters = 0;
|
||||||
mExtras = null;
|
mExtras = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +706,91 @@ public class Location implements Parcelable {
|
|||||||
mFieldsMask &= ~HAS_BEARING_ACCURACY_MASK;
|
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,
|
* 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
|
* 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() == location.hasBearingAccuracy()
|
||||||
&& (!hasBearingAccuracy() || Float.compare(location.mBearingAccuracyDegrees,
|
&& (!hasBearingAccuracy() || Float.compare(location.mBearingAccuracyDegrees,
|
||||||
mBearingAccuracyDegrees) == 0)
|
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)
|
&& Objects.equals(mProvider, location.mProvider)
|
||||||
&& areExtrasEqual(mExtras, location.mExtras);
|
&& areExtrasEqual(mExtras, location.mExtras);
|
||||||
}
|
}
|
||||||
@@ -876,6 +986,12 @@ public class Location implements Parcelable {
|
|||||||
s.append(" vAcc=").append(mAltitudeAccuracyMeters);
|
s.append(" vAcc=").append(mAltitudeAccuracyMeters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasMslAltitude()) {
|
||||||
|
s.append(" mslAlt=").append(mMslAltitudeMeters);
|
||||||
|
if (hasMslAltitudeAccuracy()) {
|
||||||
|
s.append(" mslAltAcc=").append(mMslAltitudeAccuracyMeters);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (hasSpeed()) {
|
if (hasSpeed()) {
|
||||||
s.append(" vel=").append(mSpeedMetersPerSecond);
|
s.append(" vel=").append(mSpeedMetersPerSecond);
|
||||||
if (hasSpeedAccuracy()) {
|
if (hasSpeedAccuracy()) {
|
||||||
@@ -944,6 +1060,12 @@ public class Location implements Parcelable {
|
|||||||
if (l.hasBearingAccuracy()) {
|
if (l.hasBearingAccuracy()) {
|
||||||
l.mBearingAccuracyDegrees = in.readFloat();
|
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);
|
l.mExtras = Bundle.setDefusable(in.readBundle(), true);
|
||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
@@ -991,6 +1113,12 @@ public class Location implements Parcelable {
|
|||||||
if (hasBearingAccuracy()) {
|
if (hasBearingAccuracy()) {
|
||||||
parcel.writeFloat(mBearingAccuracyDegrees);
|
parcel.writeFloat(mBearingAccuracyDegrees);
|
||||||
}
|
}
|
||||||
|
if (hasMslAltitude()) {
|
||||||
|
parcel.writeDouble(mMslAltitudeMeters);
|
||||||
|
}
|
||||||
|
if (hasMslAltitudeAccuracy()) {
|
||||||
|
parcel.writeFloat(mMslAltitudeAccuracyMeters);
|
||||||
|
}
|
||||||
parcel.writeBundle(mExtras);
|
parcel.writeBundle(mExtras);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
202
location/java/android/location/altitude/AltitudeConverter.java
Normal file
202
location/java/android/location/altitude/AltitudeConverter.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 wifi.proto = file:/wifi/OWNERS
|
||||||
per-file camera.proto = file:/services/core/java/com/android/server/camera/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 system_messages.proto = file:/core/res/OWNERS
|
||||||
|
per-file altitude.proto = file:/location/OWNERS
|
||||||
|
|||||||
54
proto/src/altitude.proto
Normal file
54
proto/src/altitude.proto
Normal 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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user