| /* |
| * Copyright (C) 2025 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; |
| |
| import android.annotation.FlaggedApi; |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.SystemApi; |
| import android.location.flags.Flags; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Represents an ionospheric model as a single-layer 2D grid with a constant height. This is also |
| * referred to as a thin-shell model. |
| * |
| * <p>The format is a simplified adaptation of the <strong>IONEX</strong> (IONosphere Map EXchange) |
| * specification. |
| * |
| * <p><strong>See Also:</strong> |
| * |
| * <ul> |
| * <li><a href="https://igs.org/formats-and-standards/">IONEX: The IONosphere Map EXchange Format |
| * Version 1</a> |
| * <li>For interpolation procedures, refer to the "Application of IONEX TEC Maps" section within |
| * the IONEX specification. |
| * </ul> |
| * |
| * @hide |
| */ |
| @FlaggedApi(Flags.FLAG_SUPPORT_IONEX_ASSISTANCE) |
| @SystemApi |
| public final class IonexAssistance implements Parcelable { |
| |
| /** |
| * Metadata describing the grid dimensions, height, and mapping function that applies to the |
| * tecMapSnapshot in this model. |
| */ |
| @NonNull private final Header mHeader; |
| |
| /** A TEC map at a moment in time. */ |
| @NonNull private final TecMapSnapshot mTecMapSnapshot; |
| |
| private IonexAssistance(Builder builder) { |
| Preconditions.checkNotNull(builder.mHeader, "Header cannot be null"); |
| Preconditions.checkNotNull(builder.mTecMapSnapshot, "TecMapSnapshots cannot be null"); |
| // Extract nested objects for readability. |
| final Header header = builder.mHeader; |
| final TecMapSnapshot snapshot = builder.mTecMapSnapshot; |
| final int numLatPoints = header.getAxesInfo().getLatitudeAxis().getNumPoints(); |
| final int numLonPoints = header.getAxesInfo().getLongitudeAxis().getNumPoints(); |
| final int expectedGridSize = numLatPoints * numLonPoints; |
| |
| // Check that the TEC map size matches the grid dimensions from the header. |
| Preconditions.checkArgument( |
| snapshot.getTecMap().size() == expectedGridSize, |
| "TEC map size (%s) must match the grid size defined in the header (%s x %s = %s).", |
| snapshot.getTecMap().size(), |
| numLatPoints, |
| numLonPoints, |
| expectedGridSize); |
| |
| // If the optional RMS map is present, it must also have the correct size. |
| if (!snapshot.getRmsMap().isEmpty()) { |
| Preconditions.checkArgument( |
| snapshot.getRmsMap().size() == expectedGridSize, |
| "RMS map size (%s) must match the grid size defined in the header (%s x %s =" |
| + " %s).", |
| snapshot.getRmsMap().size(), |
| numLatPoints, |
| numLonPoints, |
| expectedGridSize); |
| } |
| mHeader = builder.mHeader; |
| mTecMapSnapshot = builder.mTecMapSnapshot; |
| } |
| |
| /** Returns the header. */ |
| @NonNull |
| public Header getHeader() { |
| return mHeader; |
| } |
| |
| /** Returns the tecMapSnapshot. */ |
| @NonNull |
| public TecMapSnapshot getTecMapSnapshot() { |
| return mTecMapSnapshot; |
| } |
| |
| public static final @NonNull Creator<IonexAssistance> CREATOR = |
| new Creator<IonexAssistance>() { |
| @Override |
| @NonNull |
| public IonexAssistance createFromParcel(Parcel in) { |
| return new Builder() |
| .setHeader(in.readTypedObject(Header.CREATOR)) |
| .setTecMapSnapshot(in.readTypedObject(TecMapSnapshot.CREATOR)) |
| .build(); |
| } |
| |
| @Override |
| public IonexAssistance[] newArray(int size) { |
| return new IonexAssistance[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel parcel, int flags) { |
| parcel.writeTypedObject(mHeader, flags); |
| parcel.writeTypedObject(mTecMapSnapshot, flags); |
| } |
| |
| @Override |
| @NonNull |
| public String toString() { |
| StringBuilder builder = new StringBuilder("IonexAssistance["); |
| builder.append("header=").append(mHeader); |
| builder.append(", tecMapSnapshot=").append(mTecMapSnapshot); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| |
| /** Builder for {@link IonexAssistance}. */ |
| public static final class Builder { |
| private Header mHeader; |
| private TecMapSnapshot mTecMapSnapshot; |
| |
| /** Sets the header. */ |
| @NonNull |
| public Builder setHeader(@NonNull Header header) { |
| mHeader = header; |
| return this; |
| } |
| |
| /** Sets the tecMapSnapshot. */ |
| @NonNull |
| public Builder setTecMapSnapshot(@NonNull TecMapSnapshot tecMapSnapshot) { |
| mTecMapSnapshot = tecMapSnapshot; |
| return this; |
| } |
| |
| /** Builds an {@link IonexAssistance} instance. */ |
| @NonNull |
| public IonexAssistance build() { |
| return new IonexAssistance(this); |
| } |
| } |
| |
| /** Defines the metadata for the ionospheric grid model. */ |
| public static final class Header implements Parcelable { |
| |
| /** |
| * Mapping functions for TEC determination. |
| * |
| * @hide |
| */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({MAPPING_FUNCTION_NONE, MAPPING_FUNCTION_COSZ, MAPPING_FUNCTION_QFAC}) |
| public @interface MappingFunction {} |
| |
| /** |
| * The following enumerations must be in sync with the values declared in MappingFunction in |
| * IonexAssistance.aidl. |
| */ |
| /** No mapping function. */ |
| public static final int MAPPING_FUNCTION_NONE = 0; |
| |
| /** 1/cos(z) mapping function. */ |
| public static final int MAPPING_FUNCTION_COSZ = 1; |
| |
| /** Q-factor mapping function. */ |
| public static final int MAPPING_FUNCTION_QFAC = 2; |
| |
| /** |
| * The mapping function adopted for TEC determination. e.g. MAPPING_FUNCTION_NONE (no |
| * mapping function), MAPPING_FUNCTION_COSZ (1/cos(z)). |
| */ |
| private final @MappingFunction int mMappingFunction; |
| |
| /** The mean earth radius in kilometers. */ |
| private final float mBaseRadiusKm; |
| |
| /** |
| * The height of the ionospheric layer, measured from the surface of the earth in |
| * kilometers. |
| */ |
| private final float mHeightKm; |
| |
| /** The definition of the latitude and longitude axes for the grid. */ |
| @NonNull private final Axes mAxesInfo; |
| |
| private Header(Builder builder) { |
| Preconditions.checkArgumentInRange( |
| builder.mMappingFunction, |
| MAPPING_FUNCTION_NONE, |
| MAPPING_FUNCTION_QFAC, |
| "MappingFunction"); |
| Preconditions.checkArgument( |
| builder.mBaseRadiusKm > 0, "Base radius should be positive"); |
| Preconditions.checkArgument(builder.mHeightKm > 0, "Height should be positive"); |
| Preconditions.checkNotNull(builder.mAxesInfo, "Axes info cannot be null"); |
| mMappingFunction = builder.mMappingFunction; |
| mBaseRadiusKm = builder.mBaseRadiusKm; |
| mHeightKm = builder.mHeightKm; |
| mAxesInfo = builder.mAxesInfo; |
| } |
| |
| /** |
| * Returns the mapping function adopted for TEC determination. e.g. MAPPING_FUNCTION_NONE |
| * (no mapping function), MAPPING_FUNCTION_COSZ (1/cos(z)). |
| */ |
| @MappingFunction |
| public int getMappingFunction() { |
| return mMappingFunction; |
| } |
| |
| /** Returns the mean earth radius in kilometers. */ |
| @FloatRange(from = 0.0, fromInclusive = false) |
| public float getBaseRadiusKm() { |
| return mBaseRadiusKm; |
| } |
| |
| /** |
| * Returns height of the ionospheric layer, measured from the surface of the earth in |
| * kilometers. |
| */ |
| @FloatRange(from = 0.0, fromInclusive = false) |
| public float getHeightKm() { |
| return mHeightKm; |
| } |
| |
| /** Returns the definition of the latitude and longitude axes for the grid. */ |
| @NonNull |
| public Axes getAxesInfo() { |
| return mAxesInfo; |
| } |
| |
| public static final @NonNull Creator<Header> CREATOR = |
| new Creator<Header>() { |
| @Override |
| @NonNull |
| public Header createFromParcel(Parcel in) { |
| return new Builder() |
| .setMappingFunction(in.readInt()) |
| .setBaseRadiusKm(in.readFloat()) |
| .setHeightKm(in.readFloat()) |
| .setAxesInfo(in.readTypedObject(Axes.CREATOR)) |
| .build(); |
| } |
| |
| @Override |
| public Header[] newArray(int size) { |
| return new Header[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel parcel, int flags) { |
| parcel.writeInt(mMappingFunction); |
| parcel.writeFloat(mBaseRadiusKm); |
| parcel.writeFloat(mHeightKm); |
| parcel.writeTypedObject(mAxesInfo, flags); |
| } |
| |
| @Override |
| @NonNull |
| public String toString() { |
| StringBuilder builder = new StringBuilder("Header["); |
| builder.append("mappingFunction=").append(mMappingFunction); |
| builder.append(", baseRadiusKm=").append(mBaseRadiusKm); |
| builder.append(", heightKm=").append(mHeightKm); |
| builder.append(", axesInfo=").append(mAxesInfo); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| |
| /** Builder for {@link Header}. */ |
| public static final class Builder { |
| private int mMappingFunction; |
| private float mBaseRadiusKm; |
| private float mHeightKm; |
| private Axes mAxesInfo; |
| |
| /** Sets the mapping function adopted for TEC determination. */ |
| @NonNull |
| public Builder setMappingFunction(@MappingFunction int mappingFunction) { |
| mMappingFunction = mappingFunction; |
| return this; |
| } |
| |
| /** Sets the mean earth radius in kilometers. */ |
| @NonNull |
| public Builder setBaseRadiusKm( |
| @FloatRange(from = 0.0, fromInclusive = false) float baseRadiusKm) { |
| mBaseRadiusKm = baseRadiusKm; |
| return this; |
| } |
| |
| /** |
| * Sets the height of the ionospheric layer, measured from the surface of the earth in |
| * kilometers. |
| */ |
| @NonNull |
| public Builder setHeightKm( |
| @FloatRange(from = 0.0, fromInclusive = false) float heightKm) { |
| mHeightKm = heightKm; |
| return this; |
| } |
| |
| /** Sets the definition of the latitude and longitude axes for the grid. */ |
| @NonNull |
| public Builder setAxesInfo(@NonNull Axes axesInfo) { |
| mAxesInfo = axesInfo; |
| return this; |
| } |
| |
| /** Builds a {@link Header} instance as specified by this builder. */ |
| @NonNull |
| public Header build() { |
| return new Header(this); |
| } |
| } |
| } |
| |
| /** |
| * Defines the limits and resolution of a single grid axis (latitude or longitude). |
| * |
| * <p>For example, an axis with the sequence of points {@code [87.5, 85.0, ..., -87.5]} is |
| * defined by: |
| * |
| * <ul> |
| * <li>startDeg: 87.5 |
| * <li>deltaDeg: -2.5 |
| * <li>numPoints: 71 |
| * </ul> |
| */ |
| public static final class Axis implements Parcelable { |
| /** The starting value of the axis, in degrees. */ |
| private final double mStartDeg; |
| |
| /** The step size between grid points, in degrees. May be negative. */ |
| private final double mDeltaDeg; |
| |
| /** The total number of grid points along this axis. */ |
| private final int mNumPoints; |
| |
| /** |
| * Constructs an {@link Axis} instance. |
| * |
| * @param startDeg The starting value of the axis, in degrees. |
| * @param deltaDeg The step size between grid points, in degrees. May be negative. |
| * @param numPoints The total number of grid points along this axis. Must be positive. |
| */ |
| public Axis( |
| @FloatRange(from = -180.0f, to = 180.0f) double startDeg, |
| @FloatRange(from = -180.0f, to = 180.0f) double deltaDeg, |
| @IntRange(from = 0) int numPoints) { |
| Preconditions.checkArgument( |
| startDeg >= -180.0f && startDeg <= 180.0f, "startDeg out of range"); |
| Preconditions.checkArgument( |
| deltaDeg >= -180.0f && deltaDeg <= 180.0f, "deltaDeg out of range"); |
| Preconditions.checkArgument(numPoints > 0, "Number of points should be positive"); |
| this.mStartDeg = startDeg; |
| this.mDeltaDeg = deltaDeg; |
| this.mNumPoints = numPoints; |
| } |
| |
| /** Returns the starting value of the axis, in degrees. */ |
| @FloatRange(from = -180.0f, to = 180.0f) |
| public double getStartDeg() { |
| return mStartDeg; |
| } |
| |
| /** Returns the step size between grid points, in degrees. This may be negative. */ |
| @FloatRange(from = -180.0f, to = 180.0f) |
| public double getDeltaDeg() { |
| return mDeltaDeg; |
| } |
| |
| /** Returns the total number of grid points along this axis. */ |
| @IntRange(from = 1) |
| public int getNumPoints() { |
| return mNumPoints; |
| } |
| |
| public static final @NonNull Creator<Axis> CREATOR = |
| new Creator<Axis>() { |
| @Override |
| @NonNull |
| public Axis createFromParcel(Parcel in) { |
| double startDeg = in.readDouble(); |
| double deltaDeg = in.readDouble(); |
| int numPoints = in.readInt(); |
| return new Axis(startDeg, deltaDeg, numPoints); |
| } |
| |
| @Override |
| public Axis[] newArray(int size) { |
| return new Axis[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel parcel, int flags) { |
| parcel.writeDouble(mStartDeg); |
| parcel.writeDouble(mDeltaDeg); |
| parcel.writeInt(mNumPoints); |
| } |
| |
| @Override |
| @NonNull |
| public String toString() { |
| StringBuilder builder = new StringBuilder("Axis["); |
| builder.append("startDeg=").append(mStartDeg); |
| builder.append(", deltaDeg=").append(mDeltaDeg); |
| builder.append(", numPoints=").append(mNumPoints); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| } |
| |
| /** Defines the geographic latitude-longitude axes for the TEC maps. */ |
| public static final class Axes implements Parcelable { |
| |
| /** The definition for the latitude axis. */ |
| @NonNull private final Axis mLatitudeAxis; |
| |
| /** The definition for the longitude axis. */ |
| @NonNull private final Axis mLongitudeAxis; |
| |
| /** |
| * Constructs an {@link Axes} instance. |
| * |
| * @param latitudeAxis An {@link Axis} object defining the latitude points. Must not be |
| * null. |
| * @param longitudeAxis An {@link Axis} object defining the longitude points. Must not be |
| * null. |
| */ |
| public Axes(@NonNull Axis latitudeAxis, @NonNull Axis longitudeAxis) { |
| Preconditions.checkNotNull(latitudeAxis, "Latitude axis cannot be null."); |
| Preconditions.checkNotNull(longitudeAxis, "Longitude axis cannot be null."); |
| // Check if the corner points of the grid are within the valid range |
| // of latitude [-90, 90] and longitude [-180, 180] |
| double latStart = latitudeAxis.getStartDeg(); |
| double latEnd = |
| latStart + (latitudeAxis.getNumPoints() - 1) * latitudeAxis.getDeltaDeg(); |
| Preconditions.checkArgument( |
| latStart >= -90.0 && latStart <= 90.0, |
| "Latitude start %.1f out of range [-90, 90]", |
| latStart); |
| Preconditions.checkArgument( |
| latEnd >= -90.0 && latEnd <= 90.0, |
| "Latitude end %.1f out of range [-90, 90]", |
| latEnd); |
| double lonStart = longitudeAxis.getStartDeg(); |
| double lonEnd = |
| lonStart + (longitudeAxis.getNumPoints() - 1) * longitudeAxis.getDeltaDeg(); |
| // The Axis constructor already checks lonStart is within [-180, 180]. |
| Preconditions.checkArgument( |
| lonEnd >= -180.0 && lonEnd <= 180.0, |
| "Longitude end %.1f out of range [-180, 180]", |
| lonEnd); |
| this.mLatitudeAxis = latitudeAxis; |
| this.mLongitudeAxis = longitudeAxis; |
| } |
| |
| /** Returns the definition for the latitude axis. */ |
| @NonNull |
| public Axis getLatitudeAxis() { |
| return mLatitudeAxis; |
| } |
| |
| /** Returns the definition for the longitude axis. */ |
| @NonNull |
| public Axis getLongitudeAxis() { |
| return mLongitudeAxis; |
| } |
| |
| public static final @NonNull Creator<Axes> CREATOR = |
| new Creator<Axes>() { |
| @Override |
| @NonNull |
| public Axes createFromParcel(Parcel in) { |
| Axis latitudeAxis = in.readTypedObject(Axis.CREATOR); |
| Axis longitudeAxis = in.readTypedObject(Axis.CREATOR); |
| return new Axes(latitudeAxis, longitudeAxis); |
| } |
| |
| @Override |
| public Axes[] newArray(int size) { |
| return new Axes[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel parcel, int flags) { |
| parcel.writeTypedObject(mLatitudeAxis, flags); |
| parcel.writeTypedObject(mLongitudeAxis, flags); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder("Axes["); |
| builder.append("latitudeAxis=").append(mLatitudeAxis); |
| builder.append(", longitudeAxis=").append(mLongitudeAxis); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| } |
| |
| /** Represents a Total Electron Content (TEC) map at a specific moment in time. */ |
| public static final class TecMapSnapshot implements Parcelable { |
| /** The epoch of the TEC map, in seconds since the Unix epoch (UTC) */ |
| private final long mEpochTimeSeconds; |
| |
| /** |
| * A flattened representation of a 2D geographical map of Total Electron Content values |
| * (TECU), where <strong>1 TECU = 10¹⁶ electrons/m²</strong>. |
| * |
| * <p>The ionospheric delay, in meters, of a signal propagating from the zenith is given by |
| * the following formula: |
| * |
| * <pre> |
| * Delay = (40.3 / f²) * (VTEC * 10¹⁶) |
| * </pre> |
| * |
| * Where: |
| * |
| * <ul> |
| * <li><i>f</i> is the signal frequency in Hz. |
| * <li><i>VTEC</i> is the Vertical Total Electron Content in TECU. |
| * <li>The constant 40.3 is in m³/s². |
| * </ul> |
| * |
| * <p>See: "NTCM-G Ionospheric Model Description." (2022) |
| * |
| * <h3>Data Organization</h3> |
| * |
| * The data is organized in <strong>row-major order</strong> (latitudes are rows, longitudes |
| * are columns): <br> |
| * {@code (lat1, lon1), (lat1, lon2), ..., (lat1, lonN), (lat2, lon1), ...} |
| * |
| * <p>The total number of values in the list must be equal to: <br> |
| * {@code latitudeAxis.numPoints * longitudeAxis.numPoints} |
| * |
| * <p>Non-available TEC values are represented as {@link java.lang.Float#NaN}. |
| * |
| * <h3>Index Calculation</h3> |
| * |
| * To compute the latitude and longitude for a given zero-based index {@code i}: |
| * |
| * <pre> |
| * n = longitudeAxis.numPoints; |
| * latitude_deg = latitudeAxis.startDeg + (i / n) * latitudeAxis.deltaDeg; |
| * longitude_deg = longitudeAxis.startDeg + (i % n) * longitudeAxis.deltaDeg; |
| * </pre> |
| */ |
| @NonNull private final List<Float> mTecMap; |
| |
| /** |
| * An optional flattened 2D list of TEC Root-Mean-Square (RMS) error values in TECU. Values |
| * are formatted exactly in the same way as TEC values. |
| * |
| * <p>If not available, this list will be empty. |
| */ |
| @NonNull private final List<Float> mRmsMap; |
| |
| private TecMapSnapshot(Builder builder) { |
| Preconditions.checkArgument( |
| builder.mEpochTimeSeconds >= 0, "Epoch time must be non-negative"); |
| Preconditions.checkNotNull(builder.mTecMap, "TEC map cannot be null"); |
| mEpochTimeSeconds = builder.mEpochTimeSeconds; |
| mTecMap = Collections.unmodifiableList(new ArrayList<>(builder.mTecMap)); |
| mRmsMap = Collections.unmodifiableList(new ArrayList<>(builder.mRmsMap)); |
| } |
| |
| /** Returns the epoch of the TEC map, in seconds since the Unix epoch (UTC). */ |
| @IntRange(from = 0) |
| public long getEpochTimeSeconds() { |
| return mEpochTimeSeconds; |
| } |
| |
| /** |
| * Returns the flattened list of values in Total Electron Content units (TECU). |
| * |
| * <p>See {@link TecMapSnapshot#mTecMap} for details on data organization and |
| * interpretation. |
| */ |
| @NonNull |
| public List<Float> getTecMap() { |
| return mTecMap; |
| } |
| |
| /** |
| * Returns the list of TEC Root-Mean-Square (RMS) error values in TECU. |
| * |
| * <p>If not available, this list will be empty. |
| */ |
| @NonNull |
| public List<Float> getRmsMap() { |
| return mRmsMap; |
| } |
| |
| public static final @NonNull Creator<TecMapSnapshot> CREATOR = |
| new Creator<TecMapSnapshot>() { |
| @Override |
| @NonNull |
| public TecMapSnapshot createFromParcel(Parcel in) { |
| Builder builder = new Builder().setEpochTimeSeconds(in.readLong()); |
| |
| float[] tecMapArray = in.createFloatArray(); |
| List<Float> tecMapList = new ArrayList<>(tecMapArray.length); |
| for (float val : tecMapArray) { |
| tecMapList.add(val); |
| } |
| builder.setTecMap(tecMapList); |
| |
| float[] rmsMapArray = in.createFloatArray(); |
| List<Float> rmsMapList = new ArrayList<>(rmsMapArray.length); |
| for (float val : rmsMapArray) { |
| rmsMapList.add(val); |
| } |
| builder.setRmsMap(rmsMapList); |
| |
| return builder.build(); |
| } |
| |
| @Override |
| public TecMapSnapshot[] newArray(int size) { |
| return new TecMapSnapshot[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel parcel, int flags) { |
| parcel.writeLong(mEpochTimeSeconds); |
| |
| float[] tecMapArray = new float[mTecMap.size()]; |
| for (int i = 0; i < mTecMap.size(); i++) { |
| tecMapArray[i] = mTecMap.get(i); |
| } |
| parcel.writeFloatArray(tecMapArray); |
| |
| float[] rmsMapArray = new float[mRmsMap.size()]; |
| for (int i = 0; i < mRmsMap.size(); i++) { |
| rmsMapArray[i] = mRmsMap.get(i); |
| } |
| parcel.writeFloatArray(rmsMapArray); |
| } |
| |
| @Override |
| @NonNull |
| public String toString() { |
| StringBuilder builder = new StringBuilder("TecMapSnapshot["); |
| builder.append("epochTimeSeconds=").append(mEpochTimeSeconds); |
| builder.append(", tecMap.size=").append(mTecMap.size()); |
| builder.append(", rmsMap.size=").append(mRmsMap.size()); |
| builder.append("]"); |
| return builder.toString(); |
| } |
| |
| /** Builder for {@link TecMapSnapshot}. */ |
| public static final class Builder { |
| private long mEpochTimeSeconds; |
| private List<Float> mTecMap; |
| private List<Float> mRmsMap = Collections.emptyList(); |
| |
| /** Sets the epoch of the TEC map, in seconds since the Unix epoch (UTC). */ |
| @NonNull |
| @IntRange(from = 0) |
| public Builder setEpochTimeSeconds(@IntRange(from = 0) long epochTimeSeconds) { |
| mEpochTimeSeconds = epochTimeSeconds; |
| return this; |
| } |
| |
| /** |
| * Sets the flattened representation of a 2D geographical map of Total Electron Content |
| * values (TECU). where <strong>1 TECU = 10¹⁶ electrons/m²</strong>. |
| * |
| * <p>The ionospheric delay, in meters, of a signal propagating from the zenith is given |
| * by the following formula: |
| * |
| * <pre> |
| * Delay = (40.3 / f²) * (VTEC * 10¹⁶) |
| * </pre> |
| * |
| * Where: |
| * |
| * <ul> |
| * <li><i>f</i> is the signal frequency in Hz. |
| * <li><i>VTEC</i> is the Vertical Total Electron Content in TECU. |
| * <li>The constant 40.3 is in m³/s². |
| * </ul> |
| * |
| * <p>See: "NTCM-G Ionospheric Model Description." (2022) |
| * |
| * <h3>Data Organization</h3> |
| * |
| * The data is organized in <strong>row-major order</strong> (latitudes are rows, |
| * longitudes are columns): <br> |
| * {@code (lat1, lon1), (lat1, lon2), ..., (lat1, lonN), (lat2, lon1), ...} |
| * |
| * <p>The total number of values in the list must be equal to: <br> |
| * {@code latitudeAxis.numPoints * longitudeAxis.numPoints} |
| * |
| * <p>Non-available TEC values are represented as {@link java.lang.Float#NaN}. |
| * |
| * <h3>Index Calculation</h3> |
| * |
| * To compute the latitude and longitude for a given zero-based index {@code i}: |
| * |
| * <pre> |
| * n = longitudeAxis.numPoints; |
| * latitude_deg = latitudeAxis.startDeg + (i / n) * latitudeAxis.deltaDeg; |
| * longitude_deg = longitudeAxis.startDeg + (i % n) * longitudeAxis.deltaDeg; |
| * </pre> |
| */ |
| @NonNull |
| public Builder setTecMap(@NonNull List<Float> tecMap) { |
| mTecMap = tecMap; |
| return this; |
| } |
| |
| /** |
| * Sets the optional, flattened list of TEC Root-Mean-Square (RMS) error values in TECU. |
| */ |
| @NonNull |
| public Builder setRmsMap(@NonNull List<Float> rmsMap) { |
| mRmsMap = rmsMap; |
| return this; |
| } |
| |
| /** Builds a {@link TecMapSnapshot} instance as specified by this builder. */ |
| @NonNull |
| public TecMapSnapshot build() { |
| return new TecMapSnapshot(this); |
| } |
| } |
| } |
| } |