curryer.compute.spatial ======================= .. py:module:: curryer.compute.spatial .. autoapi-nested-parse:: Spatial computations. .. rubric:: Notes Terms: - ECEF - Earth Centered Earth Fixed, called ITRF93 in SPICE. - ECI - Earth Centered Inertial, called J2000 in SPICE. - Rectangular coordinates - X/Y/Z offset from center. - Geodetic coordinates - Lon/Lat/Alt from a reference ellipsoid. - WGS84 - Standard ellipsoidal representation of the Earth. - Geoid height - Modeled height above the ellipsoid that's typically called sea-level, based on gravitational equipotential surface. - Orthometric height - Surface elevation above (not including) the geoid. - Terrain correct - Process of accounting for elevated surfaces (terrain) that may have been intersected before reaching the ellipsoid, resulting in a "horizontal" (lon/lat) offset, increasing based on off-nadir angle. @author: Brandon Stone Attributes ---------- .. autoapisummary:: curryer.compute.spatial.logger curryer.compute.spatial.EARTH_FRAME Classes ------- .. autoapisummary:: curryer.compute.spatial.SpatialQueries curryer.compute.spatial.Geolocate Functions --------- .. autoapisummary:: curryer.compute.spatial.get_instrument_kernel_pointing_vectors curryer.compute.spatial.pixel_vectors curryer.compute.spatial.instrument_pointing_state curryer.compute.spatial.compute_ellipsoid_intersection curryer.compute.spatial.instrument_intersect_ellipsoid curryer.compute.spatial.ray_intersect_ellipsoid curryer.compute.spatial.ecef_to_geodetic curryer.compute.spatial.geodetic_to_ecef curryer.compute.spatial.terrain_correct curryer.compute.spatial.calc_azimuth curryer.compute.spatial.calc_zenith curryer.compute.spatial.surface_angles curryer.compute.spatial.minmax_lon curryer.compute.spatial.spice_angles curryer.compute.spatial.terrain_correct_single curryer.compute.spatial.legacy_intersect_ellipsoid Module Contents --------------- .. py:data:: logger .. py:data:: EARTH_FRAME :value: 'ITRF93' .. py:class:: SpatialQueries .. py:method:: _query_rotation_and_position_raw(sample_et, instrument, perspective_correction, observer_id, fixed_frame_name) :staticmethod: Raw SPICE query without error interception. .. py:attribute:: _query_rotation_and_position_safe .. py:method:: query_rotation_and_position(sample_et, instrument, perspective_correction, observer_id: int = spicierpy.obj.Body('EARTH').id, allow_nans=False, fixed_frame_name=EARTH_FRAME) :classmethod: Query SPICE for rotation and position, optionally suppressing errors. :param sample_et: Ephemeris time to query. :type sample_et: float :param instrument: Instrument to query. :type instrument: spicierpy.obj.Body :param perspective_correction: SPICE perspective correction to use. :type perspective_correction: str :param observer_id: Observer NAIF ID (default is Earth). :type observer_id: int, optional :param allow_nans: If True, suppress SPICE errors and return NaNs instead. Default is False. :type allow_nans: bool, optional :param fixed_frame_name: Fixed reference frame name (default is ECEF/ITRF93). :type fixed_frame_name: str, optional :returns: * *tuple* -- Rotation matrix (3x3) and position vector (3,) in fixed frame. * *SpatialQualityFlags* -- Quality flag indicating results. .. py:function:: get_instrument_kernel_pointing_vectors(instrument: int | str | curryer.spicierpy.obj.Body) -> tuple[int, numpy.ndarray] Load the pixel or boresight vector(s) for a given instrument from the instrument kernel. Boresight vector is queried from the instrument kernel, but superseded by pixel vectors if they are available. Pixel vectors are queried from the SPICE kernel variable pool using the non-standard (but established) variables: INS{instrument_id}_PIXEL_COUNT INS{instrument_id}_PIXEL_VECTORS where the former is the pixel index and the latter is three doubles. :param instrument: Instrument to query details about. :type instrument: spicierpy.obj.Body or int or str :returns: * *int* -- Number of boresight (1) or pixel vectors (1+). * *np.ndarray* -- 2D array of boresight (1x3) or pixel vectors (1+,3). .. py:function:: pixel_vectors(instrument: int | str | curryer.spicierpy.obj.Body) -> tuple[int, numpy.ndarray] Load the pixel or boresight vector(s) for a given instrument. Boresight vector is queried from the instrument kernel, but superseded by pixel vectors if they are available. Pixel vectors are queried from the SPICE kernel variable pool using the non-standard (but established) variables: INS{instrument_id}_PIXEL_COUNT INS{instrument_id}_PIXEL_VECTORS where the former is the pixel index and the latter is three doubles. :param instrument: Instrument to query details about. :type instrument: spicierpy.obj.Body or int or str :returns: * *int* -- Number of boresight (1) or pixel vectors (1+). * *np.ndarray* -- 2D array of boresight (1x3) or pixel vectors (1+,3). .. py:function:: instrument_pointing_state(ugps_times: numpy.ndarray, instrument: int | str | curryer.spicierpy.obj.Body, correction: str = None, allow_nans=True, boresight_vector=None, pointing_frame='J2000') -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.Series] Compute the boresight pointing and state (ECEF or ECI). :param ugps_times: One or more times in GPS microseconds to compute the pointing. Relevant SPICE kernels must be loaded for these times. :type ugps_times: np.ndarray :param instrument: Instrument name or ID containing the boresight to process. If `boresight_vector` is None (default), the SPICE instrument kernel and/or SPICE variables must be loaded; see `pixel_vectors`. :type instrument: spicierpy.obj.Body or int or str :param correction: Type of SPICE perspective correction to use. Default is "None". :type correction: str, optional :param allow_nans: Convert invalid returns (e.g., data gaps) into NaNs (default), otherwise throws SPICE errors. :type allow_nans: bool, optional :param boresight_vector: Array of one or more boresight vectors in the instrument frame. Default is None, instead loading them from SPICE kernels using `pixel_vectors`. :type boresight_vector: np.ndarray, optional :param pointing_frame: Pointing frame. Use "ITRF93" for ECEF and "J2000" for ECI (default). :type pointing_frame: str, optional :returns: * *pd.DataFrame* -- Pointing in rectangular coordinates in kilometers. Invalid points are set to NaN unless `allow_nans` was False. The index is the product of `ugps_times` and `boresight_vector`. * *pd.DataFrame* -- Instrument position, velocity, and attitude in pointing_frame as rectangular x/y/z coordinates and Euler angles in degrees. The index is the `ugps_times`. * *pd.Series* -- Quality flags indicating why one or both of the other returns were set to NaNs (e.g., missing kernel data). The index is the product of `ugps_times` and `boresight_vector`. .. py:function:: compute_ellipsoid_intersection(ugps_times: numpy.ndarray, instrument: int | str | curryer.spicierpy.obj.Body, perspective_correction: str = None, allow_nans: bool = True, custom_pointing_vectors: numpy.ndarray | None = None, give_geodetic_output: bool = False, give_lat_lon_in_degrees: bool = True, observer_id: int = spicierpy.obj.Body('EARTH').id, fixed_frame_name: str = EARTH_FRAME) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.Series] Compute the intersection points of an instrument's pointing vectors (boresight or pixels) with the Earth's surface, modeled as the WGS84 reference ellipsoid. This function geolocates where the instrument (e.g., a satellite sensor) is "looking" on the ellipsoid at given times, optionally applying perspective corrections and custom pointing vectors. This is the recommended API replacing `instrument_intersect_ellipsoid`. :param ugps_times: Array of times (in UNIX GPS seconds) at which to compute the intersection points. :type ugps_times: np.ndarray :param instrument: The instrument or spacecraft identifier. Can be a NAIF integer code, a string name, or a `spicierpy.obj.Body` object. :type instrument: int, str, or spicierpy.obj.Body :param perspective_correction: If specified, applies a perspective correction model to the pointing vectors. Default is None (no correction). :type perspective_correction: str, optional :param allow_nans: If True, allows NaN values in the output for cases where intersection cannot be computed. Default is True. :type allow_nans: bool, optional :param custom_pointing_vectors: If provided, overrides the instrument kernel pointing vectors with a custom array of shape (N, 3) or (3,). If None, uses the instrument's default boresight or pixel vectors. Default is None. :type custom_pointing_vectors: np.ndarray or None, optional :param give_geodetic_output: If True, output geodetic coordinates (longitude, latitude, altitude) instead of ECEF XYZ. Default is False. :type give_geodetic_output: bool, optional :param give_lat_lon_in_degrees: If True, longitude and latitude are returned in degrees (if geodetic output is requested). Default is True. :type give_lat_lon_in_degrees: bool, optional :returns: * **surface_points** (*pd.DataFrame*) -- DataFrame of intersection points on the ellipsoid, either in ECEF XYZ (columns: x, y, z) or geodetic coordinates (columns: lon, lat, alt), depending on `give_geodetic_output`. * **spacecraft_positions** (*pd.DataFrame*) -- DataFrame of spacecraft positions at each time and pixel, in ECEF XYZ coordinates (columns: x, y, z). * **quality_flags** (*pd.Series*) -- Series of integer quality flags for each intersection, indicating success or failure modes. .. rubric:: Examples >>> times = np.array([1_600_000_000.0, 1_600_000_100.0]) >>> surface, sc_pos, flags = compute_ellipsoid_intersection( ... times, ... instrument="MRO_HIRISE", ... custom_pointing_vectors=np.array([0, 0, 1]), ... give_geodetic_output=True ... ) >>> print(surface.head()) .. py:function:: instrument_intersect_ellipsoid(ugps_times: numpy.ndarray, instrument: int | str | curryer.spicierpy.obj.Body, correction: str = None, allow_nans=True, boresight_vector=None, geodetic=False, degrees=False) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.Series] Geolocate the boresight on the Earth's surface (WGS84 ellipsoid). :param ugps_times: One or more times in GPS microseconds to compute the boresight to WGS84 ellipsoid intersection. Relevant SPICE kernels must be loaded for these times. :type ugps_times: np.ndarray :param instrument: Instrument name or ID containing the boresight to process. If `boresight_vector` is None (default), the SPICE instrument kernel and/or SPICE variables must be loaded; see `pixel_vectors`. :type instrument: spicierpy.obj.Body or int or str :param correction: Type of SPICE perspective correction to use. Default is "None". :type correction: str, optional :param allow_nans: Convert invalid returns (e.g., data gaps) into NaNs (default), otherwise throws SPICE errors. :type allow_nans: bool, optional :param boresight_vector: Array of one or more boresight vectors in the instrument frame. Default is None, instead loading them from SPICE kernels using `pixel_vectors`. :type boresight_vector: np.ndarray, optional :param geodetic: If True, intersections are in geodetic lon/lat/alt coordinates, otherwise (default) they are in rectangular x/y/z coordinates. :type geodetic: bool, optional :param degrees: Returns geodetic coordinates in degrees instead of radians (default). Ignored if `geodetic` is False (default). :type degrees: bool, optional :returns: * *pd.DataFrame* -- Ellipsoidal intersection in rectangular coordinates, or geodetic lon/lat/alt if `geodetic` was True. The latter defaults to radians unless `degrees` was True. The former and "alt" are in kilometers. Invalid intersections are set to NaN unless `allow_nans` was False. The index is the product of `ugps_times` and `boresight_vector`. * *pd.DataFrame* -- Spacecraft position in ECEF as rectangular x/y/z coordinates. The index is the product of `ugps_times` and `boresight_vector`. * *pd.Series* -- Quality flags indicating why one or both of the other returns were set to NaNs (e.g., missing kernel data, failed to intersect ellipsoid). The index is the product of `ugps_times` and `boresight_vector`. .. py:function:: ray_intersect_ellipsoid(vector: numpy.ndarray, position: numpy.ndarray, geodetic=False, degrees=False, a: float = None, b: float = None, e2: float = None) -> numpy.ndarray Intersect a pointing vector to an ellipsoid (vectorized). :param vector: Array of pointing vectors in an ellipsoid-centered-fixed reference frame (i.e., ECEF for EGS84). Units must match `a`; default is kilometers. :type vector: np.ndarray :param position: Array of position vectors in an ellipsoid-centered-fixed reference frame. Must either be a single vector or the same shape as `vector`. Units must match `vector`. :type position: np.ndarray :param geodetic: If True, intersections are in geodetic lon/lat/alt coordinates, otherwise (default) they are in rectangular x/y/z coordinates. :type geodetic: bool, optional :param degrees: Returns geodetic coordinates in degrees instead of radians (default). Ignored if `geodetic` is False (default). :type degrees: bool, optional :param a: Ellipsoid's major axis. Default is WGS84 in kilometers. :type a: float, optional :param b: Ellipsoid's minor axis. Default is WGS84 in kilometers. :type b: float, optional :param e2: Ellipsoid's squared eccentricity. Default is WGS84 in kilometers. :type e2: float, optional :returns: Ellipsoidal intersection in rectangular coordinates, or geodetic lon/lat/alt if `geodetic` was True. The latter defaults to radians unless `degrees` was True. The former and "alt" are in the same units as the `vector` input (default is kilometer). Non-intersections are set to NaNs. :rtype: np.ndarray .. rubric:: References MODIS ATBD https://modis.gsfc.nasa.gov/data/atbd/atbd_mod28_v3.pdf .. py:function:: ecef_to_geodetic(xyz: numpy.ndarray, meters=False, degrees=True) -> numpy.ndarray Convert Earth Centered Earth Fixed rectangular coordinates to geodetic latitude longitude and altitude (WGS84 ellipsoid). Vectorized implementation (e.g., 1k points takes ~0.2 ms, vs. SPICE ~20ms). :param xyz: Rectangular coordinates in ECEF. :type xyz: np.ndarray :param meters: If True, inputs are in meters, otherwise (default) kilometers. :type meters: bool, optional :param degrees: If True (default), outputs are in degrees, otherwise radians. :type degrees: bool, optional :returns: Geodetic latitude longitude and altitude (WGS84 ellipsoid). :rtype: np.ndarray .. rubric:: References https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#The_application_of_Ferrari's_solution .. py:function:: geodetic_to_ecef(lon_lat_alt: numpy.ndarray, meters=False, degrees=True) -> numpy.ndarray Convert geodetic latitude longitude and altitude (WGS84 ellipsoid) to Earth Centered Earth Fixed rectangular coordinates (vectorized). :param lon_lat_alt: Geodetic latitude longitude and altitude (WGS84 ellipsoid). :type lon_lat_alt: np.ndarray :param meters: If True, outputs are in meters, otherwise (default) kilometers. :type meters: bool, optional :param degrees: If True (default), inputs are in degrees, otherwise radians. :type degrees: bool, optional :returns: Rectangular coordinates in ECEF. :rtype: np.ndarray .. rubric:: References https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates .. py:function:: terrain_correct(elev: curryer.compute.elevation.Elevation, ec_srf_pos: numpy.ndarray, ec_sat_pos: numpy.ndarray, local_minmax: tuple[float, float] = None) -> tuple[numpy.ndarray, numpy.ndarray] Perform terrain correction on ellipsoidal intersections (vectorized). :param elev: Source of geoid and orthometric heights in kilometers and degrees. :type elev: elevation.Elevation :param ec_srf_pos: Surface position in Earth Centered Earth Fixed rectangular coordinates in kilometers. :type ec_srf_pos: np.ndarray :param ec_sat_pos: Spacecraft position in Earth Centered Earth Fixed rectangular coordinates in kilometers. :type ec_sat_pos: np.ndarray :param local_minmax: Assumed local min and max elevation in kilometers, otherwise (default) query the min/max from `elev`. :type local_minmax: (float, float), optional :returns: * *np.ndarray* -- Corrected surface position in geodetic latitude longitude and altitude. Latitude and latitude are in degrees. Altitude is kilometers above the WGS84 ellipsoid (geoid height + orthometric (DEM) height). Points that can not be corrected (e.g., >85 degree off-nadir) are set to NaNs. * *np.ndarray* -- Quality flags indicating why the correction failed. .. rubric:: References MODIS ATBD https://modis.gsfc.nasa.gov/data/atbd/atbd_mod28_v3.pdf .. py:function:: calc_azimuth(obs_position: numpy.ndarray, trg_position: numpy.ndarray, degrees=False, signed=False) -> numpy.ndarray Compute the azimuth angle (vectorized). :param obs_position: Observer (surface) positions in rectangular coordinates. :type obs_position: np.ndarray :param trg_position: Target positions in rectangular coordinates. Must be a single point or the same length as `obs_position`. :type trg_position: np.ndarray :param degrees: If True, returns are in degrees, otherwise (default) radians. :type degrees: bool, optional :param signed: Return azimuth as a signed value ranging from 0 (North), 90 (East), 180/-180 (South), and -90 (West). Default is 0 to 360 clockwise. :type signed: bool, optional :returns: Azimuth angle between the observer, target and +Z-axis. :rtype: np.ndarray .. py:function:: calc_zenith(obs_position: numpy.ndarray, trg_position: numpy.ndarray, degrees=False, geocentric=False) -> numpy.ndarray Compute the zenith angle (vectorized). :param obs_position: Observer (surface) positions in rectangular coordinates. :type obs_position: np.ndarray :param trg_position: Target positions in rectangular coordinates. Must be a single point or the same length as `obs_position`. :type trg_position: np.ndarray :param degrees: If True, returns are in degrees, otherwise (default) radians. :type degrees: bool, optional :param geocentric: Compute geocentric zenith (angle between target and body center), or geodetic zenith (angle between target and surface normal). :type geocentric: bool, optional :returns: Zenith angle between the observer, target and reference frame center. :rtype: np.ndarray .. py:function:: surface_angles(surface_positions: pandas.DataFrame, target_positions: pandas.DataFrame = None, target_obj: int | str | curryer.spicierpy.obj.Body = None, degrees=False, allow_nans=False, signed=False, geocentric=False) -> pandas.DataFrame Compute the azimuth and zenith surface angles (vectorized). :param surface_positions: Surface positions in rectangular coordinates. Index must be time in GPS microseconds if `target_positions` is not supplied. :type surface_positions: pd.DataFrame :param target_positions: Target positions in rectangular coordinates. Required unless `target_obj` is supplied. Must be a single point or the same length as `surface_positions`. :type target_positions: pd.DataFrame, optional :param target_obj: Target to query positions for at `surface_position` times. Typically, a spacecraft or the Sun. :type target_obj: spicierpy.obj.Body or int or str, optional :param degrees: If True, returns are in degrees, otherwise (default) radians. :type degrees: bool, optional :param allow_nans: Convert invalid returns (e.g., data gaps) into NaNs (default), otherwise throws SPICE errors. :type allow_nans: bool, optional :param signed: Return azimuth as a signed value ranging from 0 (North), 90 (East), 180/-180 (South), and -90 (West). Default is 0 to 360 clockwise. :type signed: bool, optional :param geocentric: Compute geocentric zenith (angle between target and body center), or geodetic zenith (angle between target and surface normal). :type geocentric: bool, optional :returns: Azimuth and zenith angles. Invalid values are set to NaNs unless `allow_nans` was False. :rtype: pd.DataFrame .. py:function:: minmax_lon(lons: numpy.ndarray, degrees=False) -> (float, float) Compute min/max longitude, accounting for the international dateline. :param lons: :type lons: np.ndarray :param degrees: If True, inputs and returns are in degrees, otherwise (default) radians. Must range -180 to 180, otherwise -pi, pi. :type degrees: bool, optional :returns: Min and max longitude. Max may be numerically larger than min if it was determined to wrap the international dateline. :rtype: float, float .. py:class:: Geolocate(instrument: str | int | curryer.spicierpy.obj.Body, dem_data_dir=None) High-level class to manage the geolocation processing steps. .. py:attribute:: instrument .. py:attribute:: sc_state_frame .. py:attribute:: elevation .. py:attribute:: _step_time :value: None .. py:method:: _log_step(msg: str, qf_ds: pandas.Series = None) Log an individual processing step, summarizing quality flag results. .. py:method:: __call__(ugps_times: numpy.ndarray) -> xarray.Dataset Perform geolocation processing. :param ugps_times: One or more times in GPS microseconds to geolocate. Relevant SPICE kernels must already be loaded into memory. :type ugps_times: np.ndarray :returns: Geolocation results and ancillary statistics. :rtype: xr.Dataset .. py:method:: intersect_ellipsoid(ugps_times: numpy.ndarray) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.Series] Intersect the pixels to the WGS84 ellipsoid. :param ugps_times: One or more times in GPS microseconds. :type ugps_times: np.ndarray :returns: * *pd.DataFrame* -- Ellipsoidal intersection in geodetic lon/lat/alt coordinates (degrees, kilometers). Invalid intersections are set to NaN. The index is the product of `ugps_times` and pixels across-track. * *pd.DataFrame* -- Spacecraft position in ECEF as rectangular x/y/z coordinates. The index is the product of `ugps_times` and pixels across-track. * *pd.Series* -- Quality flags indicating why one or both of the other returns were NaNs (e.g., missing kernel data, failed to intersect ellipsoid). The index is the product of `ugps_times` and pixels across-track. .. py:method:: correct_terrain(ellips_lla_df: pandas.DataFrame, sc_xyz_df: pandas.DataFrame, ellips_qf_ds: pandas.Series, pad_degrees: float = 1.0) -> tuple[pandas.DataFrame, pandas.Series] Perform terrain correction on ellipsoidal intersections. :param ellips_lla_df: Ellipsoidal intersection in geodetic lon/lat/alt coordinates (degrees, kilometers). Invalid intersections are set to NaN. The index is the product of `ugps_times` and pixels across-track. :type ellips_lla_df: pd.DataFrame :param sc_xyz_df: Spacecraft position in ECEF as rectangular x/y/z coordinates. The index is the product of `ugps_times` and pixels across-track. :type sc_xyz_df: pd.DataFrame :param ellips_qf_ds: Quality flags indicating why one or both of the other returns were NaNs (e.g., missing kernel data, failed to intersect ellipsoid). The index is the product of `ugps_times` and pixels across-track. :type ellips_qf_ds: pd.Series :param pad_degrees: Number of lon/lat degrees to pad the intersections when loading the regional extent of the surface elevation. :type pad_degrees: float, optional :returns: * *pd.DataFrame* -- Terrain intersection in geodetic lon/lat/alt coordinates (degrees, kilometers). Invalid intersections are set to NaN. The index matches the input index from `ellips_lla_df`. * *pd.Series* -- Quality flags indicating why the returns were NaNs. The index matches the input index from `ellips_lla_df`. .. py:method:: calc_ancillary(terrain_lla_df: pandas.DataFrame, sc_xyz_df: pandas.DataFrame) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.DataFrame, pandas.DataFrame, pandas.Series] Compute ancillary data fields. :param terrain_lla_df: Terrain intersection in geodetic lon/lat/alt coordinates (degrees, kilometers). Invalid intersections are set to NaN. The index is the product of `ugps_times` and pixels across-track. :type terrain_lla_df: pd.DataFrame :param sc_xyz_df: Spacecraft position in ECEF as rectangular x/y/z coordinates. The index is the product of `ugps_times` and pixels across-track. :type sc_xyz_df: pd.DataFrame :returns: * *pd.DataFrame* -- Instrument pointing in frame `sc_state_frame` (e.g. ECI) in KM. The index matches the input index from `terrain_lla_df`. * *pd.DataFrame* -- Solar azimuth and zenith angles (degrees). The index matches the input index from `terrain_lla_df`. * *pd.DataFrame* -- View azimuth and zenith angles (degrees). The index matches the input index from `terrain_lla_df`. * *pd.DataFrame* -- Spacecraft position (x/y/z), velocity (vx/vy/vz) and attitude (ex/ey/ez). The latter is from the instrument perspective. The index is the UGPS times from `terrain_lla_df`. * *pd.Series* -- Quality flags indicating why any of the returns were NaNs. The index matches the input index from `terrain_lla_df`. .. py:function:: spice_angles(ugps_times, surface_positions, target_obj, degrees=False) Compute azimuth and zenith angles using SPICE. .. py:function:: terrain_correct_single(elev: curryer.compute.elevation.Elevation, ec_srf_pos, ec_sat_pos, local_minmax=None) Apply terrain correction to a single point. .. py:function:: legacy_intersect_ellipsoid(ugps_times, instrument, correction=None, allow_nans=True, boresight_vector=None, geodetic=False) Geolocate the boresight on the Earth's surface (WGS84 ellipsoid).