├── +matmap3d ├── @referenceEllipsoid │ └── referenceEllipsoid.m ├── R3.m ├── aer2ecef.m ├── aer2eci.m ├── aer2enu.m ├── aer2geodetic.m ├── aer2ned.m ├── ecef2aer.m ├── ecef2eci.m ├── ecef2enu.m ├── ecef2enuv.m ├── ecef2geodetic.m ├── ecef2ned.m ├── eci2aer.m ├── eci2ecef.m ├── enu2aer.m ├── enu2ecef.m ├── enu2ecefv.m ├── enu2geodetic.m ├── enu2uvw.m ├── geodetic2aer.m ├── geodetic2ecef.m ├── geodetic2enu.m ├── geodetic2ned.m ├── get_radius_normal.m ├── greenwichsrt.m ├── lookAtSpheroid.m ├── private │ └── mustBeEqualSize.m ├── vdist.m ├── vreckon.m └── wgs84Ellipsoid.m ├── .archive └── juliantime.m ├── .github └── workflows │ ├── ci.yml │ ├── composite-buildtool │ └── action.yml │ ├── composite-install-matlab │ └── action.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── buildfile.m ├── codemeta.json ├── private └── publish_gen_index_html.m └── test └── TestUnit.m /+matmap3d/@referenceEllipsoid/referenceEllipsoid.m: -------------------------------------------------------------------------------- 1 | %% referenceEllipsoid Select available ellipsoid 2 | % 3 | %%% inputs 4 | % * name: string of model name. Default: 'wgs84' 5 | %%% outputs 6 | % * E: referenceEllipsoid 7 | 8 | classdef referenceEllipsoid 9 | properties 10 | Code 11 | Name 12 | LengthUnit 13 | SemimajorAxis 14 | Flattening 15 | SemiminorAxis 16 | Eccentricity 17 | MeanRadius 18 | Volume 19 | end 20 | 21 | methods 22 | function e = referenceEllipsoid(name, lengthUnit) 23 | arguments 24 | name (1,1) string = "wgs84" 25 | lengthUnit (1,1) string = "m" 26 | end 27 | 28 | mustBeMember(lengthUnit, ["m", "meters"]) 29 | 30 | switch name 31 | case 'wgs84' 32 | % WGS-84 ellipsoid parameters. 33 | % 34 | % 35 | e.Code = 7030; 36 | e.Name = 'World Geodetic System 1984'; 37 | e.LengthUnit = 'meter'; 38 | e.SemimajorAxis = 6378137.0; 39 | e.Flattening = 1/298.2572235630; 40 | e.SemiminorAxis = e.SemimajorAxis * (1 - e.Flattening); 41 | e.Eccentricity = get_eccentricity(e); 42 | e.MeanRadius = meanradius(e); 43 | e.Volume = spheroidvolume(e); 44 | case 'grs80' 45 | % GRS-80 ellipsoid parameters 46 | % (accessed 2018-01-22) 47 | e.Code = 7019; 48 | e.Name = 'Geodetic Reference System 1980'; 49 | e.LengthUnit = 'meter'; 50 | e.SemimajorAxis = 6378137.0; 51 | e.Flattening = 1/298.257222100882711243; 52 | e.SemiminorAxis = e.SemimajorAxis * (1 - e.Flattening); 53 | e.Eccentricity = get_eccentricity(e); 54 | e.MeanRadius = meanradius(e); 55 | e.Volume = spheroidvolume(e); 56 | otherwise, error(name + " not yet implemented") 57 | end 58 | end 59 | 60 | function v = spheroidvolume(E) 61 | v = 4*pi/3 * E.SemimajorAxis^2 * E.SemiminorAxis; 62 | 63 | assert(v>=0) 64 | end 65 | 66 | function r = meanradius(E) 67 | r = (2*E.SemimajorAxis + E.SemiminorAxis) / 3; 68 | 69 | assert(r>=0) 70 | end 71 | 72 | function ecc = get_eccentricity(E) 73 | ecc = sqrt ( (E.SemimajorAxis^2 - E.SemiminorAxis^2) / (E.SemimajorAxis^2)); 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /+matmap3d/R3.m: -------------------------------------------------------------------------------- 1 | function A = R3(x) 2 | %% R3 rotation matrix for ECI 3 | A = [cos(x), sin(x), 0; 4 | -sin(x), cos(x), 0; 5 | 0, 0, 1]; 6 | end 7 | -------------------------------------------------------------------------------- /+matmap3d/aer2ecef.m: -------------------------------------------------------------------------------- 1 | %% AER2ECEF convert azimuth, elevation, range to target from observer to ECEF coordinates 2 | % 3 | %%% Inputs 4 | % 5 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 6 | % * az: azimuth clockwise from local north 7 | % * el: elevation angle above local horizon 8 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 9 | % * spheroid: referenceEllipsoid 10 | % * angleUnit: string for angular units. Default 'd': degrees 11 | % 12 | %%% outputs 13 | % 14 | % * x,y,z: Earth Centered Earth Fixed (ECEF) coordinates of test point (meters) 15 | 16 | function [x,y,z] = aer2ecef(az, el, slantRange, lat0, lon0, alt0, spheroid, angleUnit) 17 | 18 | arguments 19 | az {mustBeReal} 20 | el {mustBeReal} 21 | slantRange {mustBeReal, mustBeNonnegative} 22 | lat0 {mustBeReal} 23 | lon0 {mustBeReal} 24 | alt0 {mustBeReal} 25 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 26 | angleUnit (1,1) string = "d" 27 | end 28 | 29 | % Origin of the local system in geocentric coordinates. 30 | [x0, y0, z0] = matmap3d.geodetic2ecef(spheroid, lat0, lon0, alt0, angleUnit); 31 | % Convert Local Spherical AER to ENU 32 | [e, n, u] = matmap3d.aer2enu(az, el, slantRange, angleUnit); 33 | % Rotating ENU to ECEF 34 | [dx, dy, dz] = matmap3d.enu2uvw(e, n, u, lat0, lon0, angleUnit); 35 | % Origin + offset from origin equals position in ECEF 36 | x = x0 + dx; 37 | y = y0 + dy; 38 | z = z0 + dz; 39 | 40 | end 41 | -------------------------------------------------------------------------------- /+matmap3d/aer2eci.m: -------------------------------------------------------------------------------- 1 | %% AER2ECI convert AER (azimuth, elevation, slant range) to ECI 2 | % 3 | % NOTE: because underlying ecef2eci() is rotation only, error can be order 4 | % 1..10% 5 | % 6 | %%% Inputs 7 | % * utc: datetime UTC 8 | % * az,el,rng: (degrees, meters) 9 | % * lat, lon, alt: latitude, longitude, altiude of observer (degrees, meters) 10 | % 11 | %%% Outputs 12 | % * x, y, z: ECI x, y, z 13 | function [x, y, z] = aer2eci(utc, az, el, rng, lat, lon, alt) 14 | arguments 15 | utc datetime 16 | az {mustBeReal} 17 | el {mustBeReal} 18 | rng {mustBeReal, mustBeNonnegative} 19 | lat {mustBeReal} 20 | lon {mustBeReal} 21 | alt {mustBeReal} 22 | end 23 | 24 | [x1, y1, z1] = matmap3d.aer2ecef(az, el, rng, lat, lon, alt); 25 | 26 | [x, y, z] = matmap3d.ecef2eci(utc, x1, y1, z1); 27 | 28 | end 29 | -------------------------------------------------------------------------------- /+matmap3d/aer2enu.m: -------------------------------------------------------------------------------- 1 | %% AER2ENU convert azimuth, elevation, range to ENU coordinates 2 | % 3 | %%% Inputs 4 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 5 | % * az: azimuth clockwise from local north 6 | % * el: elevation angle above local horizon 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% Outputs 10 | % * e,n,u: East, North, Up coordinates of test points (meters) 11 | function [e, n, u] = aer2enu (az, el, slantRange, angleUnit) 12 | arguments 13 | az {mustBeReal} 14 | el {mustBeReal} 15 | slantRange {mustBeReal} 16 | angleUnit (1,1) string = "d" 17 | end 18 | 19 | if startsWith(angleUnit,'d') 20 | az = deg2rad(az); 21 | el = deg2rad(el); 22 | end 23 | 24 | u = slantRange .* sin(el); 25 | r = slantRange .* cos(el); 26 | e = r .* sin(az); 27 | n = r .* cos(az); 28 | 29 | end 30 | -------------------------------------------------------------------------------- /+matmap3d/aer2geodetic.m: -------------------------------------------------------------------------------- 1 | %% aer2geodetic convert azimuth, elevation, range of target from observer to geodetic coordiantes 2 | % 3 | %%% Inputs 4 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 5 | % * az: azimuth clockwise from local north 6 | % * el: elevation angle above local horizon 7 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 8 | % * spheroid: referenceEllipsoid 9 | % * angleUnit: string for angular units. Default 'd': degrees 10 | % 11 | %%% Outputs 12 | % * lat1,lon1,alt1: geodetic coordinates of test points (degrees,degrees,meters) 13 | function [lat1, lon1, alt1] = aer2geodetic(az, el, slantRange, lat0, lon0, alt0, spheroid, angleUnit) 14 | arguments 15 | az {mustBeReal} 16 | el {mustBeReal} 17 | slantRange {mustBeReal,mustBeNonnegative} 18 | lat0 {mustBeReal} 19 | lon0 {mustBeReal} 20 | alt0 {mustBeReal} 21 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 22 | angleUnit (1,1) string = "d" 23 | end 24 | 25 | [x, y, z] = matmap3d.aer2ecef(az, el, slantRange, lat0, lon0, alt0, spheroid, angleUnit); 26 | 27 | [lat1, lon1, alt1] = matmap3d.ecef2geodetic(spheroid, x, y, z, angleUnit); 28 | 29 | end 30 | -------------------------------------------------------------------------------- /+matmap3d/aer2ned.m: -------------------------------------------------------------------------------- 1 | %% AER2NED convert azimuth, elevation, range to NED coordinates 2 | % 3 | %%% Inputs 4 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 5 | % * az: azimuth clockwise from local north 6 | % * el: elevation angle above local horizon 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% Outputs 10 | % * north, east, down: coordinates of points (meters) 11 | function [north, east, down] = aer2ned(az, el, slantRange, angleUnit) 12 | arguments 13 | az {mustBeReal} 14 | el {mustBeReal} 15 | slantRange {mustBeReal} 16 | angleUnit (1,1) string = "d" 17 | end 18 | 19 | [east, north, up] = matmap3d.aer2enu(az, el, slantRange, angleUnit); 20 | 21 | down = -up; 22 | 23 | end 24 | -------------------------------------------------------------------------------- /+matmap3d/ecef2aer.m: -------------------------------------------------------------------------------- 1 | %% ECEF2AER convert ECEF of target to azimuth, elevation, slant range from observer 2 | % 3 | %%% Inputs 4 | % * x,y,z: Earth Centered Earth Fixed (ECEF) coordinates of test point (meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% Outputs 10 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 11 | % * az: azimuth clockwise from local north 12 | % * el: elevation angle above local horizon 13 | function [az, el, slantRange] = ecef2aer(x, y, z, lat0, lon0, alt0, spheroid, angleUnit) 14 | 15 | arguments 16 | x {mustBeReal} 17 | y {mustBeReal} 18 | z {mustBeReal} 19 | lat0 {mustBeReal} 20 | lon0 {mustBeReal} 21 | alt0 {mustBeReal} 22 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 23 | angleUnit (1,1) string = "d" 24 | end 25 | 26 | [e, n, u] = matmap3d.ecef2enu(x, y, z, lat0, lon0, alt0, spheroid, angleUnit); 27 | [az,el,slantRange] = matmap3d.enu2aer(e, n, u, angleUnit); 28 | 29 | end 30 | -------------------------------------------------------------------------------- /+matmap3d/ecef2eci.m: -------------------------------------------------------------------------------- 1 | %% ECEF2ECI rotate ECEF coordinates to ECI 2 | % because this doesn't account for nutation, etc. error is often > 1% 3 | % 4 | %%% Inputs 5 | % x0, y0, z0: ECEF position (meters) 6 | % utc: time UTC 7 | %%% Outputs 8 | % * x,y,z: ECI position (meters) 9 | 10 | function [x,y,z] = ecef2eci(utc, x0, y0, z0) 11 | arguments 12 | utc (:,1) datetime 13 | x0 (:,1) {mustBeReal,mustBeEqualSize(utc,x0)} 14 | y0 (:,1) {mustBeReal,mustBeEqualSize(utc,y0)} 15 | z0 (:,1) {mustBeReal,mustBeEqualSize(utc,z0)} 16 | end 17 | 18 | % Greenwich hour angles (radians) 19 | gst = matmap3d.greenwichsrt(juliandate(utc)); 20 | 21 | % Convert into ECEF 22 | x = nan(size(gst)); 23 | y = nan(size(x)); 24 | z = nan(size(x)); 25 | 26 | for j = 1:length(x) 27 | eci = matmap3d.R3(gst(j)).' * [x0(j), y0(j), z0(j)].'; 28 | x(j) = eci(1); 29 | y(j) = eci(2); 30 | z(j) = eci(3); 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /+matmap3d/ecef2enu.m: -------------------------------------------------------------------------------- 1 | 2 | %% ECEF2ENU convert ECEF to ENU 3 | % 4 | %%% Inputs 5 | % * x,y,z: Earth Centered Earth Fixed (ECEF) coordinates of test point (meters) 6 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 7 | % * spheroid: referenceEllipsoid 8 | % * angleUnit: string for angular units. Default 'd': degrees 9 | % 10 | %%% outputs 11 | % * East, North, Up coordinates of test points (meters) 12 | 13 | function [east, north, up] = ecef2enu(x, y, z, lat0, lon0, alt0, spheroid, angleUnit) 14 | arguments 15 | x {mustBeReal} 16 | y {mustBeReal} 17 | z {mustBeReal} 18 | lat0 {mustBeReal} 19 | lon0 {mustBeReal} 20 | alt0 {mustBeReal} 21 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 22 | angleUnit (1,1) string = "d" 23 | end 24 | 25 | [x0, y0, z0] = matmap3d.geodetic2ecef(spheroid, lat0, lon0, alt0, angleUnit); 26 | [east, north, up] = matmap3d.ecef2enuv(x - x0, y - y0, z - z0, lat0, lon0, angleUnit); 27 | 28 | end 29 | -------------------------------------------------------------------------------- /+matmap3d/ecef2enuv.m: -------------------------------------------------------------------------------- 1 | %% ECEF2ENUV convert *vector projection* UVW to ENU 2 | % 3 | %%% Inputs 4 | % * u,v,w: meters 5 | % * lat0,lon0: geodetic latitude and longitude (degrees) 6 | % * angleUnit: string for angular system. Default 'd' degrees 7 | % 8 | %%% Outputs 9 | % * e,n,Up: East, North, Up vector 10 | 11 | function [e, n, Up] = ecef2enuv(u, v, w, lat0, lon0, angleUnit) 12 | arguments 13 | u {mustBeReal} 14 | v {mustBeReal} 15 | w {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | angleUnit (1,1) string = "d" 19 | end 20 | 21 | if startsWith(angleUnit, 'd') 22 | lat0 = deg2rad(lat0); 23 | lon0 = deg2rad(lon0); 24 | end 25 | 26 | t = cos(lon0) .* u + sin(lon0) .* v; 27 | e = -sin(lon0) .* u + cos(lon0) .* v; 28 | Up = cos(lat0) .* t + sin(lat0) .* w; 29 | n = -sin(lat0) .* t + cos(lat0) .* w; 30 | 31 | % 1mm precision 32 | if abs(e) < 1e-3, e=0; end 33 | if abs(n) < 1e-3, n=0; end 34 | if abs(Up) < 1e-3, Up=0; end 35 | end 36 | -------------------------------------------------------------------------------- /+matmap3d/ecef2geodetic.m: -------------------------------------------------------------------------------- 1 | %% ECEF2GEODETIC convert ECEF to geodetic coordinates 2 | % 3 | %%% Inputs 4 | % * x,y,z: ECEF coordinates of test point(s) (meters) 5 | % * spheroid: referenceEllipsoid 6 | % * angleUnit: string for angular units. Default 'd': degrees 7 | % 8 | %%% Outputs 9 | % * lat,lon, alt: ellipsoid geodetic coordinates of point(s) (degrees, degrees, meters) 10 | % 11 | % based on: 12 | % You, Rey-Jer. (2000). Transformation of Cartesian to Geodetic Coordinates without Iterations. 13 | % Journal of Surveying Engineering. doi: 10.1061/(ASCE)0733-9453 14 | 15 | function [lat,lon,alt] = ecef2geodetic(spheroid, x, y, z, angleUnit) 16 | arguments 17 | spheroid {mustBeScalarOrEmpty} 18 | x {mustBeReal} 19 | y {mustBeReal} 20 | z {mustBeReal} 21 | angleUnit (1,1) string = "d" 22 | end 23 | 24 | if isempty(spheroid) 25 | spheroid = matmap3d.wgs84Ellipsoid(); 26 | end 27 | 28 | a = spheroid.SemimajorAxis; 29 | b = spheroid.SemiminorAxis; 30 | 31 | r = sqrt(x.^2 + y.^2 + z.^2); 32 | 33 | E = sqrt(a.^2 - b.^2); 34 | 35 | % eqn. 4a 36 | u = sqrt(0.5 * (r.^2 - E.^2) + 0.5 * sqrt((r.^2 - E.^2).^2 + 4 * E.^2 .* z.^2)); 37 | 38 | Q = hypot(x, y); 39 | 40 | huE = hypot(u, E); 41 | 42 | % eqn. 4b 43 | Beta = atan(huE ./ u .* z ./ hypot(x, y)); 44 | 45 | % eqn. 13 46 | eps = ((b * u - a * huE + E.^2) .* sin(Beta)) ./ (a * huE ./ cos(Beta) - E.^2 .* cos(Beta)); 47 | 48 | Beta = Beta + eps; 49 | % final output 50 | lat = atan(a / b * tan(Beta)); 51 | 52 | lon = atan2(y, x); 53 | 54 | % eqn. 7 55 | alt = hypot(z - b * sin(Beta), Q - a * cos(Beta)); 56 | 57 | % inside ellipsoid? 58 | inside = (x.^2 ./ a.^2) + (y.^2 ./ a.^2) + (z.^2 ./ b.^2) < 1; 59 | alt(inside) = -alt(inside); 60 | 61 | 62 | if startsWith(angleUnit, 'd') 63 | lat = rad2deg(lat); 64 | lon = rad2deg(lon); 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /+matmap3d/ecef2ned.m: -------------------------------------------------------------------------------- 1 | %% ECEF2NED Convert ECEF coordinates to NED 2 | % 3 | %%% Inputs 4 | % * x,y,z: Earth Centered Earth Fixed (ECEF) coordinates of test point (meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% outputs 10 | % * North,East,Down: coordinates of points (meters) 11 | function [north, east, down] = ecef2ned(x, y, z, lat0, lon0, alt0, spheroid, angleUnit) 12 | arguments 13 | x {mustBeReal} 14 | y {mustBeReal} 15 | z {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | alt0 {mustBeReal} 19 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 20 | angleUnit (1,1) string = "d" 21 | end 22 | 23 | [east, north, up] = matmap3d.ecef2enu(x,y,z,lat0,lon0,alt0,spheroid,angleUnit); 24 | 25 | down = -up; 26 | 27 | end 28 | -------------------------------------------------------------------------------- /+matmap3d/eci2aer.m: -------------------------------------------------------------------------------- 1 | %% ECI2AER convert ECI to AER (azimuth, elevation, slant range) 2 | % 3 | % parameters: 4 | % utc: datetime UTC 5 | % x0, y0, z0: ECI x, y, z 6 | % lat, lon, alt: latitude, longitude, altiude of observer (degrees, meters) 7 | % 8 | % outputs: 9 | % az,el,rng: Azimuth (degrees), Elevation (degrees), Slant Range (meters) 10 | function [az, el, rng] = eci2aer(utc, x0, y0, z0, lat, lon, alt) 11 | arguments 12 | utc datetime 13 | x0 {mustBeReal} 14 | y0 {mustBeReal} 15 | z0 {mustBeReal} 16 | lat {mustBeReal} 17 | lon {mustBeReal} 18 | alt {mustBeReal} 19 | end 20 | 21 | [x, y, z] = matmap3d.eci2ecef(utc, x0, y0, z0); 22 | 23 | [az, el, rng] = matmap3d.ecef2aer(x, y, z, lat, lon, alt); 24 | 25 | end 26 | -------------------------------------------------------------------------------- /+matmap3d/eci2ecef.m: -------------------------------------------------------------------------------- 1 | %% ECI2ECEF rotate ECI coordinates to ECEF 2 | % because this doesn't account for nutation, etc. error is often > 1% 3 | % 4 | % x_eci, y_eci, z_eci: eci position vectors 5 | % utc: Matlab datetime UTC 6 | % 7 | % x,y,z: ECEF position (meters) 8 | function [x,y,z] = eci2ecef(utc, x_eci, y_eci, z_eci) 9 | arguments 10 | utc (:,1) datetime 11 | x_eci (:,1) {mustBeReal,mustBeEqualSize(utc,x_eci)} 12 | y_eci (:,1) {mustBeReal,mustBeEqualSize(utc,y_eci)} 13 | z_eci (:,1) {mustBeReal,mustBeEqualSize(utc,z_eci)} 14 | end 15 | 16 | % Greenwich hour angles (radians) 17 | gst = matmap3d.greenwichsrt(juliandate(utc)); 18 | 19 | x = nan(size(x_eci)); 20 | y = nan(size(x)); 21 | z = nan(size(x)); 22 | 23 | for j = 1:length(x) 24 | ecef = matmap3d.R3(gst(j)) * [x_eci(j), y_eci(j), z_eci(j)].'; 25 | x(j) = ecef(1); 26 | y(j) = ecef(2); 27 | z(j) = ecef(3); 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /+matmap3d/enu2aer.m: -------------------------------------------------------------------------------- 1 | %% ENU2AER convert ENU to azimuth, elevation, slant range 2 | % 3 | %%% Inputs 4 | % * e,n,u: East, North, Up coordinates of test points (meters) 5 | % * angleUnit: string for angular units. Default 'd': degrees 6 | % 7 | %%% outputs 8 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 9 | % * az: azimuth clockwise from local north 10 | % * el: elevation angle above local horizon 11 | function [az, elev, slantRange] = enu2aer(east, north, up, angleUnit) 12 | arguments 13 | east {mustBeReal} 14 | north {mustBeReal} 15 | up {mustBeReal} 16 | angleUnit (1,1) string = "d" 17 | end 18 | 19 | if abs(east) < 1e-3, east = 0.; end % singularity, 1 mm precision 20 | if abs(north) < 1e-3, north = 0.; end % singularity, 1 mm precision 21 | if abs(up) < 1e-3, up = 0.; end % singularity, 1 mm precision 22 | 23 | r = hypot(east, north); 24 | slantRange = hypot(r,up); 25 | % radians 26 | elev = atan2(up,r); 27 | az = mod(atan2(east, north), 2*pi); 28 | 29 | if startsWith(angleUnit,'d') 30 | elev = rad2deg(elev); 31 | az = rad2deg(az); 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /+matmap3d/enu2ecef.m: -------------------------------------------------------------------------------- 1 | %% ENU2ECEF convert from ENU to ECEF coordiantes 2 | % 3 | %%% Inputs 4 | % * east, north, up: coordinates of test points (meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% outputs 10 | % * x,y,z: Earth Centered Earth Fixed (ECEF) coordinates of test point (meters) 11 | function [x, y, z] = enu2ecef(east, north, up, lat0, lon0, alt0, spheroid, angleUnit) 12 | arguments 13 | east {mustBeReal} 14 | north {mustBeReal} 15 | up {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | alt0 {mustBeReal} 19 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 20 | angleUnit (1,1) string = "d" 21 | end 22 | 23 | [x0, y0, z0] = matmap3d.geodetic2ecef(spheroid, lat0, lon0, alt0, angleUnit); 24 | [dx, dy, dz] = matmap3d.enu2uvw(east, north, up, lat0, lon0, angleUnit); 25 | 26 | x = x0 + dx; 27 | y = y0 + dy; 28 | z = z0 + dz; 29 | end 30 | -------------------------------------------------------------------------------- /+matmap3d/enu2ecefv.m: -------------------------------------------------------------------------------- 1 | %% ENU2ECEFV convert from ENU to ECEF coordinates 2 | % 3 | %%% Inputs 4 | % * e,n,u: East, North, Up coordinates of point(s) (meters) 5 | % * lat0,lon0: geodetic coordinates of observer/reference point (degrees) 6 | % * angleUnit: string for angular units. Default 'd': degrees 7 | % 8 | %%% outputs 9 | % * u,v,w: coordinates of test point(s) (meters) 10 | 11 | function [u, v, w] = enu2ecefv(east, north, up, lat0, lon0, angleUnit) 12 | arguments 13 | east {mustBeReal} 14 | north {mustBeReal} 15 | up {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | angleUnit (1,1) string = "d" 19 | end 20 | 21 | 22 | if startsWith(angleUnit,'d') 23 | lat0 = deg2rad(lat0); 24 | lon0 = deg2rad(lon0); 25 | end 26 | 27 | t = cos(lat0) .* up - sin(lat0) .* north; 28 | w = sin(lat0) .* up + cos(lat0) .* north; 29 | 30 | u = cos(lon0) .* t - sin(lon0) .* east; 31 | v = sin(lon0) .* t + cos(lon0) .* east; 32 | 33 | end 34 | -------------------------------------------------------------------------------- /+matmap3d/enu2geodetic.m: -------------------------------------------------------------------------------- 1 | %% ENU2GEODETIC convert from ENU to geodetic coordinates 2 | % 3 | %%% Inputs 4 | % * east,north,up: ENU coordinates of point(s) (meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees, otherwise Radians 8 | % 9 | %%% outputs 10 | % * lat,lon,alt: geodetic coordinates of test points (degrees,degrees,meters) 11 | 12 | function [lat, lon, alt] = enu2geodetic(east, north, up, lat0, lon0, alt0, spheroid, angleUnit) 13 | arguments 14 | east {mustBeReal} 15 | north {mustBeReal} 16 | up {mustBeReal} 17 | lat0 {mustBeReal} 18 | lon0 {mustBeReal} 19 | alt0 {mustBeReal} 20 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 21 | angleUnit (1,1) string = "d" 22 | end 23 | 24 | [x, y, z] = matmap3d.enu2ecef(east, north, up, lat0, lon0, alt0, spheroid, angleUnit); 25 | [lat, lon, alt] = matmap3d.ecef2geodetic(spheroid, x, y, z, angleUnit); 26 | 27 | end 28 | -------------------------------------------------------------------------------- /+matmap3d/enu2uvw.m: -------------------------------------------------------------------------------- 1 | %% ENU2UVW convert from ENU to UVW coordinates 2 | % 3 | %%% Inputs 4 | % * e,n,up: East, North, Up coordinates of point(s) (meters) 5 | % * lat0,lon0: geodetic coordinates of observer/reference point (degrees) 6 | % * angleUnit: string for angular units. Default 'd': degrees 7 | % 8 | %%% outputs 9 | % * u,v,w: coordinates of test point(s) (meters) 10 | function [u,v,w] = enu2uvw(east,north,up,lat0,lon0,angleUnit) 11 | arguments 12 | east {mustBeReal} 13 | north {mustBeReal} 14 | up {mustBeReal} 15 | lat0 {mustBeReal} 16 | lon0 {mustBeReal} 17 | angleUnit (1,1) string = "d" 18 | end 19 | 20 | if startsWith(angleUnit, 'd') 21 | lat0 = deg2rad(lat0); 22 | lon0 = deg2rad(lon0); 23 | end 24 | 25 | t = cos(lat0) * up - sin(lat0) * north; 26 | w = sin(lat0) * up + cos(lat0) * north; 27 | 28 | u = cos(lon0) * t - sin(lon0) * east; 29 | v = sin(lon0) * t + cos(lon0) * east; 30 | 31 | end 32 | -------------------------------------------------------------------------------- /+matmap3d/geodetic2aer.m: -------------------------------------------------------------------------------- 1 | 2 | %% GEODETIC2AER converts geodetic coordinates to azimuth, elevation, slant range 3 | % from an observer's perspective, convert target coordinates to azimuth, elevation, slant range. 4 | % 5 | %%% Inputs 6 | % * lat,lon, alt: ellipsoid geodetic coordinates of point under test (degrees, degrees, meters) 7 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 8 | % * spheroid: referenceEllipsoid 9 | % * angleUnit: string for angular units. Default 'd': degrees, otherwise Radians 10 | % 11 | %%% Outputs 12 | % * az, el, slantrange: look angles and distance to point under test (degrees, degrees, meters) 13 | % * az: azimuth clockwise from local north 14 | % * el: elevation angle above local horizon 15 | function [az, el, slantRange] = geodetic2aer(lat, lon, alt, lat0, lon0, alt0, spheroid, angleUnit) 16 | arguments 17 | lat {mustBeReal} 18 | lon {mustBeReal} 19 | alt {mustBeReal} 20 | lat0 {mustBeReal} 21 | lon0 {mustBeReal} 22 | alt0 {mustBeReal} 23 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 24 | angleUnit (1,1) string = "d" 25 | end 26 | 27 | [e, n, u] = matmap3d.geodetic2enu(lat, lon, alt, lat0, lon0, alt0, spheroid, angleUnit); 28 | [az, el, slantRange] = matmap3d.enu2aer(e, n, u, angleUnit); 29 | 30 | end 31 | -------------------------------------------------------------------------------- /+matmap3d/geodetic2ecef.m: -------------------------------------------------------------------------------- 1 | %% GEODETIC2ECEF convert from geodetic to ECEF coordiantes 2 | % 3 | %%% Inputs 4 | % * lat,lon, alt: ellipsoid geodetic coordinates of point(s) (degrees, degrees, meters) 5 | % * spheroid: referenceEllipsoid 6 | % * angleUnit: string for angular units. Default 'd': degrees 7 | % 8 | %%% outputs 9 | % * x,y,z: ECEF coordinates of test point(s) (meters) 10 | function [x,y,z] = geodetic2ecef(spheroid, lat, lon, alt, angleUnit) 11 | arguments 12 | spheroid {mustBeScalarOrEmpty} 13 | lat {mustBeReal} 14 | lon {mustBeReal} 15 | alt {mustBeReal} 16 | angleUnit (1,1) string = "d" 17 | end 18 | 19 | %% defaults 20 | if isempty(spheroid) 21 | spheroid = matmap3d.wgs84Ellipsoid(); 22 | end 23 | 24 | if startsWith(angleUnit, 'd') 25 | lat = deg2rad(lat); 26 | lon = deg2rad(lon); 27 | end 28 | 29 | % Radius of curvature of the prime vertical section 30 | N = matmap3d.get_radius_normal(lat, spheroid); 31 | % Compute cartesian (geocentric) coordinates given (curvilinear) geodetic coordinates. 32 | 33 | x = (N + alt) .* cos(lat) .* cos(lon); 34 | y = (N + alt) .* cos(lat) .* sin(lon); 35 | z = (N .* (spheroid.SemiminorAxis / spheroid.SemimajorAxis)^2 + alt) .* sin(lat); 36 | 37 | end 38 | -------------------------------------------------------------------------------- /+matmap3d/geodetic2enu.m: -------------------------------------------------------------------------------- 1 | %% GEODETIC2ENU convert from geodetic to ENU coordinates 2 | % 3 | %%% Inputs 4 | % * lat,lon, alt: ellipsoid geodetic coordinates of point under test (degrees, degrees, meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | % 9 | %%% outputs 10 | % * east,north,up: coordinates of points (meters) 11 | function [east, north, up] = geodetic2enu(lat, lon, alt, lat0, lon0, alt0, spheroid, angleUnit) 12 | arguments 13 | lat {mustBeReal} 14 | lon {mustBeReal} 15 | alt {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | alt0 {mustBeReal} 19 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 20 | angleUnit (1,1) string = "d" 21 | end 22 | 23 | [x1,y1,z1] = matmap3d.geodetic2ecef(spheroid, lat,lon,alt,angleUnit); 24 | [x2,y2,z2] = matmap3d.geodetic2ecef(spheroid, lat0,lon0,alt0,angleUnit); 25 | 26 | dx = x1-x2; 27 | dy = y1-y2; 28 | dz = z1-z2; 29 | 30 | [east, north, up] = matmap3d.ecef2enuv(dx, dy, dz, lat0, lon0, angleUnit); 31 | 32 | end 33 | -------------------------------------------------------------------------------- /+matmap3d/geodetic2ned.m: -------------------------------------------------------------------------------- 1 | %% GEODETIC2NED convert from geodetic to NED coordinates 2 | % 3 | %%% Inputs 4 | % * lat,lon, alt: ellipsoid geodetic coordinates of point under test (degrees, degrees, meters) 5 | % * lat0, lon0, alt0: ellipsoid geodetic coordinates of observer/reference (degrees, degrees, meters) 6 | % * spheroid: referenceEllipsoid 7 | % * angleUnit: string for angular units. Default 'd': degrees 8 | %% outputs 9 | % * n,e,d: North, East, Down coordinates of test points (meters) 10 | 11 | function [north, east, down] = geodetic2ned(lat, lon, alt, lat0, lon0, alt0, spheroid, angleUnit) 12 | arguments 13 | lat {mustBeReal} 14 | lon {mustBeReal} 15 | alt {mustBeReal} 16 | lat0 {mustBeReal} 17 | lon0 {mustBeReal} 18 | alt0 {mustBeReal} 19 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 20 | angleUnit (1,1) string = "d" 21 | end 22 | 23 | [x1,y1,z1] = matmap3d.geodetic2ecef(spheroid, lat,lon,alt,angleUnit); 24 | [x2,y2,z2] = matmap3d.geodetic2ecef(spheroid, lat0,lon0,alt0,angleUnit); 25 | 26 | dx = x1-x2; 27 | dy = y1-y2; 28 | dz = z1-z2; 29 | 30 | [east, north, up] = matmap3d.ecef2enuv(dx, dy, dz, lat0, lon0, angleUnit); 31 | 32 | down = -up; 33 | 34 | end 35 | -------------------------------------------------------------------------------- /+matmap3d/get_radius_normal.m: -------------------------------------------------------------------------------- 1 | %% GET_RADIUS_NORMAL normal along the prime vertical section ellipsoidal radius of curvature 2 | % 3 | %%% Inputs 4 | % * lat: geodetic latitude in Radians 5 | % * ell: referenceEllipsoid 6 | % 7 | %%% Outputs 8 | % * N: normal along the prime vertical section ellipsoidal radius of curvature, at a given geodetic latitude. 9 | function N = get_radius_normal(lat, E) 10 | arguments 11 | lat {mustBeReal} 12 | E (1,1) matmap3d.referenceEllipsoid 13 | end 14 | 15 | if isempty(E) 16 | E = matmap3d.wgs84Ellipsoid(); 17 | end 18 | 19 | N = E.SemimajorAxis^2 ./ sqrt( E.SemimajorAxis^2 .* cos(lat).^2 + E.SemiminorAxis^2 .* sin(lat).^2 ); 20 | end 21 | -------------------------------------------------------------------------------- /+matmap3d/greenwichsrt.m: -------------------------------------------------------------------------------- 1 | %% GREENWICHSRT Compute greenwich sidereal time from Julian date 2 | % compute greenwich sidereal time from D. Vallado 4th edition 3 | % 4 | %%% Inputs 5 | % Jdate: Julian days from Jan 1, 4713 BCE from juliantime(utc) or juliandate(utc) 6 | %%% Outputs 7 | % gst: greenwich sidereal time [0, 2pi) 8 | function gst = greenwichsrt(Jdate) 9 | arguments 10 | Jdate {mustBeReal,mustBeNonnegative} 11 | end 12 | 13 | % Vallado Eq. 3-42 p. 184, Seidelmann 3.311-1 14 | tUT1 = (Jdate - 2451545) / 36525; 15 | % Eqn. 3-47 p. 188 16 | gmst_sec = 67310.54841 + (876600 * 3600 + 8640184.812866) * tUT1 + 0.093104 * tUT1 .^ 2 - 6.2e-6 * tUT1 .^ 3; 17 | % 1/86400 and %(2*pi) implied by units of radians 18 | gst = mod(gmst_sec * (2 * pi) / 86400.0, 2 * pi); 19 | 20 | end 21 | -------------------------------------------------------------------------------- /+matmap3d/lookAtSpheroid.m: -------------------------------------------------------------------------------- 1 | %% LOOKATSPHEROID 2 | % Calculates line-of-sight intersection with Earth (or other ellipsoid) surface from above surface ./ orbit 3 | % 4 | %%% Inputs 5 | % * lat0, lon0: latitude and longitude of starting point 6 | % * h0: altitude of starting point in meters 7 | % * az: azimuth angle of line-of-sight, clockwise from North 8 | % * tilt: tilt angle of line-of-sight with respect to local vertical (nadir = 0) 9 | % 10 | %%% Outputs 11 | % * lat, lon: latitude and longitude where the line-of-sight intersects with the Earth ellipsoid 12 | % * d: slant range in meters from the starting point to the intersect point 13 | % 14 | % Values will be NaN if the line of sight does not intersect. 15 | % 16 | % Algorithm based on: 17 | % https://medium.com/@stephenhartzell/satellite-line-of-sight-intersection-with-earth-d786b4a6a9b6 18 | % Stephen Hartzell 19 | function [lat, lon, d] = lookAtSpheroid(lat0, lon0, h0, az, tilt, spheroid, angleUnit) 20 | arguments 21 | lat0 {mustBeReal} 22 | lon0 {mustBeReal} 23 | h0 {mustBeReal,mustBeNonnegative} 24 | az {mustBeReal} 25 | tilt {mustBeReal} 26 | spheroid (1,1) matmap3d.referenceEllipsoid = matmap3d.wgs84Ellipsoid() 27 | angleUnit (1,1) string = "d" 28 | end 29 | 30 | if startsWith(angleUnit, 'd') 31 | el = tilt - 90; 32 | else 33 | el = tilt - pi/2; 34 | end 35 | 36 | a = spheroid.SemimajorAxis; 37 | b = a; 38 | c = spheroid.SemiminorAxis; 39 | 40 | [e, n, u] = matmap3d.aer2enu(az, el, 1., angleUnit); % fixed 1 km slant range 41 | [u, v, w] = matmap3d.enu2uvw(e, n, u, lat0, lon0, angleUnit); 42 | [x, y, z] = matmap3d.geodetic2ecef([], lat0, lon0, h0, angleUnit); 43 | 44 | value = -a.^2 .* b.^2 .* w .* z - a.^2 .* c.^2 .* v .* y - b.^2 .* c.^2 .* u .* x; 45 | radical = a.^2 .* b.^2 .* w.^2 + a.^2 .* c.^2 .* v.^2 - a.^2 .* v.^2 .* z.^2 + 2 .* a.^2 .* v .* w .* y .* z - ... 46 | a.^2 .* w.^2 .* y.^2 + b.^2 .* c.^2 .* u.^2 - b.^2 .* u.^2 .* z.^2 + 2 .* b.^2 .* u .* w .* x .* z - ... 47 | b.^2 .* w.^2 .* x.^2 - c.^2 .* u.^2 .* y.^2 + 2 .* c.^2 .* u .* v .* x .* y - c.^2 .* v.^2 .* x.^2; 48 | 49 | magnitude = a.^2 .* b.^2 .* w.^2 + a.^2 .* c.^2 .* v.^2 + b.^2 .* c.^2 .* u.^2; 50 | 51 | % Return nan if radical < 0 or d < 0 because LOS vector does not point towards Earth 52 | d = (value - a .* b .* c .* sqrt(radical)) ./ magnitude; 53 | d(radical < 0 | d < 0) = nan; % separate line 54 | 55 | % altitude should be zero 56 | [lat, lon] = matmap3d.ecef2geodetic([], x + d .* u, y + d .* v, z + d .* w, angleUnit); 57 | 58 | end 59 | -------------------------------------------------------------------------------- /+matmap3d/private/mustBeEqualSize.m: -------------------------------------------------------------------------------- 1 | function mustBeEqualSize(a,b) 2 | 3 | if ~isequal(size(a),size(b)) 4 | throwAsCaller(MException('MATLAB:validators:mustBeEqualSize','Size of inputs must equal each other')) 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /+matmap3d/vdist.m: -------------------------------------------------------------------------------- 1 | %% VDIST - Using the WGS-84 Earth ellipsoid, compute the distance between two points 2 | % 3 | % within a few millimeters of accuracy, compute forward 4 | % azimuth, and compute backward azimuth, all using a vectorized 5 | % version of Vincenty's algorithm. 6 | % 7 | % s = vdist(lat1,lon1,lat2,lon2) 8 | % [s,a12] = vdist(lat1,lon1,lat2,lon2) 9 | % [s,a12,a21] = vdist(lat1,lon1,lat2,lon2) 10 | % 11 | % * s = distance in meters (inputs may be scalars, vectors, or matrices) 12 | % * a12 = azimuth in degrees from first point to second point (forward) 13 | % * a21 = azimuth in degrees from second point to first point (backward) 14 | % (Azimuths are in degrees clockwise from north.) 15 | % * lat1 = GEODETIC latitude of first point (degrees) 16 | % * lon1 = longitude of first point (degrees) 17 | % * lat2, lon2 = second point (degrees) 18 | function varargout = vdist(lat1,lon1,lat2,lon2) 19 | % 20 | %%% Original algorithm source: 21 | % 22 | % T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations", 23 | % Survey Review, vol. 23, no. 176, April 1975, pp 88-93. 24 | % 25 | % Notes: 26 | % 27 | % # lat1,lon1,lat2,lon2 can be any (identical) size/shape. Outputs will have the same size and shape. 28 | % # Error correcting code, convergence failure traps, antipodal corrections, polar error corrections, WGS84 ellipsoid parameters, testing, and comments: Michael Kleder, 2004. 29 | % # Azimuth implementation (including quadrant abiguity resolution) and code vectorization, Michael Kleder, Sep 2005. 30 | % # Vectorization is convergence sensitive; that is, quantities which have already converged to within tolerance are not recomputed during subsequent iterations (while other quantities are still converging). 31 | % # Vincenty describes his distance algorithm as precise to within 0.01 millimeters, subject to the ellipsoidal model. 32 | % # For distance calculations, essentially antipodal points are 33 | % treated as exactly antipodal, potentially reducing accuracy 34 | % slightly. 35 | % # Distance failures for points exactly at the poles are 36 | % eliminated by moving the points by 0.6 millimeters. 37 | % # The Vincenty distance algorithm was transcribed verbatim by 38 | % Peter Cederholm, August 12, 2003. It was modified and 39 | % translated to English by Michael Kleder. 40 | % Mr. Cederholm's website is http://www.plan.aau.dk/~pce/ 41 | % # Distances agree with the Mapping Toolbox, version 2.2 (R14SP3) 42 | % with a max relative difference of about 5e-9, except when the 43 | % two points are nearly antipodal, and except when one point is 44 | % near the equator and the two longitudes are nearly 180 degrees 45 | % apart. This function (vdist) is more accurate in such cases. 46 | % For example, note this difference (as of this writing): 47 | % >>vdist(0.2,305,15,125) 48 | % 18322827.0131551 49 | % >>distance(0.2,305,15,125,[6378137 0.08181919]) 50 | % 0 51 | % # Azimuths FROM the north pole (either forward starting at the 52 | % north pole or backward when ending at the north pole) are set 53 | % to 180 degrees by convention. Azimuths FROM the south pole are 54 | % set to 0 degrees by convention. 55 | % # Azimuths agree with the Mapping Toolbox, version 2.2 (R14SP3) 56 | % to within about a hundred-thousandth of a degree, except when 57 | % traversing to or from a pole, where the convention for this 58 | % function is described in (10), and except in the cases noted 59 | % above in (9). 60 | % # No warranties; use at your own risk. 61 | 62 | arguments 63 | lat1 {mustBeReal} 64 | lon1 {mustBeReal,mustBeEqualSize(lat1,lon1)} 65 | lat2 {mustBeReal,mustBeEqualSize(lat1,lat2)} 66 | lon2 {mustBeReal,mustBeEqualSize(lat1,lon2)} 67 | end 68 | 69 | % Supply WGS84 earth ellipsoid axis lengths in meters: 70 | a = 6378137; % definitionally 71 | b = 6356752.31424518; % computed from WGS84 earth flattening coefficient 72 | % preserve true input latitudes: 73 | lat1tr = lat1; 74 | lat2tr = lat2; 75 | % convert inputs in degrees to radians: 76 | lat1 = lat1 * 0.0174532925199433; 77 | lon1 = lon1 * 0.0174532925199433; 78 | lat2 = lat2 * 0.0174532925199433; 79 | lon2 = lon2 * 0.0174532925199433; 80 | % correct for errors at exact poles by adjusting 0.6 millimeters: 81 | kidx = abs(pi/2-abs(lat1)) < 1e-10; 82 | if any(kidx) 83 | lat1(kidx) = sign(lat1(kidx))*(pi/2-(1e-10)); 84 | end 85 | kidx = abs(pi/2-abs(lat2)) < 1e-10; 86 | if any(kidx) 87 | lat2(kidx) = sign(lat2(kidx))*(pi/2-(1e-10)); 88 | end 89 | f = (a-b)/a; 90 | U1 = atan((1-f)*tan(lat1)); 91 | U2 = atan((1-f)*tan(lat2)); 92 | lon1 = mod(lon1,2*pi); 93 | lon2 = mod(lon2,2*pi); 94 | L = abs(lon2-lon1); 95 | kidx = L > pi; 96 | if any(kidx) 97 | L(kidx) = 2*pi - L(kidx); 98 | end 99 | lambda = L; 100 | lambdaold = 0*lat1; 101 | itercount = 0; 102 | notdone = logical(1+0*lat1); 103 | alpha = 0*lat1; 104 | sigma = 0*lat1; 105 | cos2sigmam = 0*lat1; 106 | C = 0*lat1; 107 | warninggiven = false; 108 | while any(notdone) % force at least one execution 109 | %disp(['lambda(21752) = ' num2str(lambda(21752),20)]); 110 | itercount = itercount+1; 111 | if itercount > 50 112 | if ~warninggiven 113 | warning(['Essentially antipodal points encountered. ' ... 114 | 'Precision may be reduced slightly.']); 115 | end 116 | lambda(notdone) = pi; 117 | break 118 | end 119 | lambdaold(notdone) = lambda(notdone); 120 | 121 | sinsigma(notdone) = sqrt((cos(U2(notdone)).*sin(lambda(notdone)))... 122 | .^2+(cos(U1(notdone)).*sin(U2(notdone))-sin(U1(notdone)).*... 123 | cos(U2(notdone)).*cos(lambda(notdone))).^2); %#ok<*AGROW> 124 | 125 | cossigma(notdone) = sin(U1(notdone)).*sin(U2(notdone))+... 126 | cos(U1(notdone)).*cos(U2(notdone)).*cos(lambda(notdone)); 127 | 128 | % eliminate rare imaginary portions at limit of numerical precision: 129 | sinsigma(notdone)=real(sinsigma(notdone)); 130 | cossigma(notdone)=real(cossigma(notdone)); 131 | sigma(notdone) = atan2(sinsigma(notdone),cossigma(notdone)); 132 | alpha(notdone) = asin(cos(U1(notdone)).*cos(U2(notdone)).*... 133 | sin(lambda(notdone))./sin(sigma(notdone))); 134 | cos2sigmam(notdone) = cos(sigma(notdone))-2*sin(U1(notdone)).*... 135 | sin(U2(notdone))./cos(alpha(notdone)).^2; 136 | C(notdone) = f/16*cos(alpha(notdone)).^2.*(4+f*(4-3*... 137 | cos(alpha(notdone)).^2)); 138 | lambda(notdone) = L(notdone)+(1-C(notdone)).*f.*sin(alpha(notdone))... 139 | .*(sigma(notdone)+C(notdone).*sin(sigma(notdone)).*... 140 | (cos2sigmam(notdone)+C(notdone).*cos(sigma(notdone)).*... 141 | (-1+2.*cos2sigmam(notdone).^2))); 142 | %disp(['then, lambda(21752) = ' num2str(lambda(21752),20)]); 143 | % correct for convergence failure in the case of essentially antipodal 144 | % points 145 | if any(lambda(notdone) > pi) 146 | warning('Essentially antipodal points encountered. Precision may be reduced slightly.') 147 | warninggiven = true; 148 | lambdaold(lambda>pi) = pi; 149 | lambda(lambda>pi) = pi; 150 | end 151 | notdone = abs(lambda-lambdaold) > 1e-12; 152 | end 153 | u2 = cos(alpha).^2.*(a^2-b^2)/b^2; 154 | A = 1+u2./16384.*(4096+u2.*(-768+u2.*(320-175.*u2))); 155 | B = u2./1024.*(256+u2.*(-128+u2.*(74-47.*u2))); 156 | deltasigma = B.*sin(sigma).*(cos2sigmam+B./4.*(cos(sigma).*(-1+2.*... 157 | cos2sigmam.^2)-B./6.*cos2sigmam.*(-3+4.*sin(sigma).^2).*(-3+4*... 158 | cos2sigmam.^2))); 159 | varargout{1} = b.*A.*(sigma-deltasigma); 160 | if nargout > 1 161 | % From point #1 to point #2 162 | % correct sign of lambda for azimuth calcs: 163 | lambda = abs(lambda); 164 | kidx=sign(sin(lon2-lon1)) .* sign(sin(lambda)) < 0; 165 | lambda(kidx) = -lambda(kidx); 166 | numer = cos(U2).*sin(lambda); 167 | denom = cos(U1).*sin(U2)-sin(U1).*cos(U2).*cos(lambda); 168 | a12 = atan2(numer,denom); 169 | kidx = a12<0; 170 | a12(kidx)=a12(kidx)+2*pi; 171 | % from poles: 172 | a12(lat1tr <= -90) = 0; 173 | a12(lat1tr >= 90 ) = pi; 174 | varargout{2} = rad2deg(a12); % to degrees 175 | end 176 | if nargout > 2 177 | % From point #2 to point #1 178 | % correct sign of lambda for azimuth calcs: 179 | lambda = abs(lambda); 180 | kidx=sign(sin(lon1-lon2)) .* sign(sin(lambda)) < 0; 181 | lambda(kidx)=-lambda(kidx); 182 | numer = cos(U1).*sin(lambda); 183 | denom = sin(U1).*cos(U2)-cos(U1).*sin(U2).*cos(lambda); 184 | a21 = atan2(numer,denom); 185 | kidx=a21<0; 186 | a21(kidx)= a21(kidx)+2*pi; 187 | % backwards from poles: 188 | a21(lat2tr >= 90) = pi; 189 | a21(lat2tr <= -90) = 0; 190 | varargout{3} = reshape(rad2deg(a21),keepsize); % to degrees 191 | end 192 | 193 | end 194 | -------------------------------------------------------------------------------- /+matmap3d/vreckon.m: -------------------------------------------------------------------------------- 1 | % VRECKON - Using the WGS-84 Earth ellipsoid, travel a given distance along 2 | % a given azimuth starting at a given initial point, and return 3 | % the endpoint within a few millimeters of accuracy, using 4 | % Vincenty's algorithm. 5 | % 6 | % USAGE: 7 | % [lat2,lon2] = vreckon(lat1, lon1, s, a12) 8 | % 9 | % VARIABLES: 10 | % lat1 = inital latitude (degrees) 11 | % lon1 = initial longitude (degrees) 12 | % s = distance (meters) 13 | % a12 = intial azimuth (degrees) 14 | % lat2, lon2 = second point (degrees) 15 | % a21 = reverse azimuth (degrees), at final point facing back toward the 16 | % intial point 17 | % 18 | % Original algorithm source: 19 | % T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid 20 | % with Application of Nested Equations", Survey Review, vol. 23, no. 176, 21 | % April 1975, pp 88-93. 22 | % Available at: http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf 23 | % 24 | % Notes: 25 | % (1) The Vincenty reckoning algorithm was transcribed verbatim into 26 | % JavaScript by Chris Veness. It was modified and translated to Matlab 27 | % by Michael Kleder. Mr. Veness's website is: 28 | % http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html 29 | % (2) Error correcting code, polar error corrections, WGS84 ellipsoid 30 | % parameters, testing, and comments by Michael Kleder. 31 | % (3) By convention, when starting at a pole, the longitude of the initial 32 | % point (otherwise meaningless) determines the longitude line along 33 | % which to traverse, and hence the longitude of the final point. 34 | % (4) The convention noted in (3) above creates a discrepancy with VDIST 35 | % when the the intial or final point is at a pole. In the VDIST 36 | % function, when traversing from a pole, the azimuth is 0 when 37 | % heading away from the south pole and 180 when heading away from the 38 | % north pole. In contrast, this VRECKON function uses the azimuth as 39 | % noted in (3) above when traversing away form a pole. 40 | % (5) In testing, where the traversal subtends no more than 178 degrees, 41 | % this function correctly inverts the VDIST function to within 0.2 42 | % millimeters of distance, 5e-10 degrees of forward azimuth, 43 | % and 5e-10 degrees of reverse azimuth. Precision reduces as test 44 | % points approach antipodal because the precision of VDIST is reduced 45 | % for nearly antipodal points. (A warning is given by VDIST.) 46 | % (6) Tested but no warranty. Use at your own risk. 47 | % (7) Ver 1.0, Michael Kleder, November 2007 48 | function [lat2,lon2,a21] = vreckon(lat1,lon1,s,a12) 49 | arguments 50 | lat1 {mustBeReal} 51 | lon1 {mustBeReal} 52 | s {mustBeReal,mustBeNonnegative} 53 | a12 {mustBeReal} 54 | end 55 | %% compute 56 | a = 6378137; % semimajor axis 57 | b = 6356752.31424518; % semiminor axis 58 | f = 1/298.257223563; % flattening coefficient 59 | lat1 = lat1 * .1745329251994329577e-1; % intial latitude in radians 60 | lon1 = lon1 * .1745329251994329577e-1; % intial longitude in radians 61 | % correct for errors at exact poles by adjusting 0.6 millimeters: 62 | kidx = abs(pi/2-abs(lat1)) < 1e-10; 63 | if any(kidx) 64 | lat1(kidx) = sign(lat1(kidx))*(pi/2-(1e-10)); 65 | end 66 | alpha1 = a12 * .1745329251994329577e-1; % inital azimuth in radians 67 | sinAlpha1 = sin(alpha1); 68 | cosAlpha1 = cos(alpha1); 69 | tanU1 = (1-f) * tan(lat1); 70 | cosU1 = 1 / sqrt(1 + tanU1*tanU1); 71 | sinU1 = tanU1*cosU1; 72 | sigma1 = atan2(tanU1, cosAlpha1); 73 | sinAlpha = cosU1 * sinAlpha1; 74 | cosSqAlpha = 1 - sinAlpha*sinAlpha; 75 | uSq = cosSqAlpha * (a*a - b*b) / (b*b); 76 | A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq))); 77 | B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq))); 78 | sigma = s / (b*A); 79 | sigmaP = 2*pi; 80 | while (abs(sigma-sigmaP) > 1e-12) 81 | cos2SigmaM = cos(2*sigma1 + sigma); 82 | sinSigma = sin(sigma); 83 | cosSigma = cos(sigma); 84 | deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+... 85 | 2*cos2SigmaM*cos2SigmaM)-... 86 | B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+... 87 | 4*cos2SigmaM*cos2SigmaM))); 88 | sigmaP = sigma; 89 | sigma = s / (b*A) + deltaSigma; 90 | end 91 | tmp = sinU1*sinSigma - cosU1*cosSigma*cosAlpha1; 92 | lat2 = atan2(sinU1*cosSigma + cosU1*sinSigma*cosAlpha1,... 93 | (1-f)*sqrt(sinAlpha*sinAlpha + tmp*tmp)); 94 | lambda = atan2(sinSigma*sinAlpha1, cosU1*cosSigma - ... 95 | sinU1*sinSigma*cosAlpha1); 96 | C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)); 97 | L = lambda - (1-C) * f * sinAlpha * (sigma + C*sinSigma*(cos2SigmaM+... 98 | C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))); 99 | lon2 = lon1 + L; 100 | % output degrees 101 | lat2 = rad2deg(lat2); 102 | lon2 = rad2deg(lon2); 103 | lon2 = mod(lon2,360); % follow [0,360] convention 104 | if nargout > 2 105 | a21 = atan2(sinAlpha, -tmp); 106 | a21 = 180 + rad2deg(a21); % note direction reversal 107 | a21=mod(a21,360); 108 | end 109 | 110 | end 111 | -------------------------------------------------------------------------------- /+matmap3d/wgs84Ellipsoid.m: -------------------------------------------------------------------------------- 1 | %% WGS84ELLIPSOID - generate a WGS84 referenceEllipsoid 2 | % 3 | %%% Inputs 4 | % 5 | % * lengthUnit: currently not implemented, for compatibility either nargin=0 or 'm' or 'meter' is accepted without conversion 6 | % 7 | %%% Outputs 8 | % 9 | % * E: referenceEllipsoid 10 | function E = wgs84Ellipsoid(lengthUnit) 11 | arguments 12 | lengthUnit (1,1) string = "m" 13 | end 14 | 15 | E = matmap3d.referenceEllipsoid('wgs84', lengthUnit); 16 | 17 | end 18 | -------------------------------------------------------------------------------- /.archive/juliantime.m: -------------------------------------------------------------------------------- 1 | function jd = juliantime(t) 2 | %% juliandate datetime to Julian time 3 | % 4 | % from D.Vallado Fundamentals of Astrodynamics and Applications p.187 5 | % and J. Meeus Astronomical Algorithms 1991 Eqn. 7.1 pg. 61 6 | % 7 | % parameters: 8 | % t: datetime vector 9 | % 10 | % results: 11 | % jd: julian date (days since Jan 1, 4173 BCE 12 | 13 | narginchk(1,1) 14 | validateattributes(t, {'numeric', 'datetime'}, {'nonempty'}, 1) 15 | 16 | if matmap3d.isoctave() 17 | pkg('load', 'tablicious') % provides datetime 18 | end 19 | 20 | t = datetime(t); 21 | 22 | y = year(t); 23 | m = month(t); 24 | i = m < 3; 25 | y(i) = y(i) - 1; 26 | m(i) = m(i) + 12; 27 | 28 | A = fix(y / 100.); 29 | B = 2 - A + fix(A / 4.); 30 | C = ((second(t) / 60. + minute(t)) / 60. + hour(t)) / 24.; 31 | 32 | jd = fix(365.25 * (y + 4716)) + fix(30.6001 * (m + 1)) + day(t) + B - 1524.5 + C; 33 | 34 | end 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.m" 7 | - ".github/workflows/ci.yml" 8 | 9 | 10 | jobs: 11 | 12 | matlab: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | 16 | strategy: 17 | matrix: 18 | release: [R2020b, latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: ./.github/workflows/composite-install-matlab 24 | 25 | - uses: ./.github/workflows/composite-buildtool 26 | -------------------------------------------------------------------------------- /.github/workflows/composite-buildtool/action.yml: -------------------------------------------------------------------------------- 1 | name: "matlab-buildtool" 2 | 3 | runs: 4 | 5 | using: "composite" 6 | 7 | steps: 8 | 9 | - name: Run Matlab buildtool 10 | if: ${{ matrix.release >= 'R2022b' || startsWith(matrix.release, 'latest') }} 11 | uses: matlab-actions/run-build@v2 12 | with: 13 | startup-options: ${{ matrix.startup-options }} 14 | tasks: check test 15 | 16 | 17 | - name: Run tests (manual) 18 | if: ${{ matrix.release < 'R2022b' && !startsWith(matrix.release, 'latest') }} 19 | uses: matlab-actions/run-tests@v2 20 | with: 21 | select-by-folder: test 22 | -------------------------------------------------------------------------------- /.github/workflows/composite-install-matlab/action.yml: -------------------------------------------------------------------------------- 1 | name: "install-matlab" 2 | 3 | runs: 4 | 5 | using: "composite" 6 | 7 | steps: 8 | 9 | - name: Install MATLAB 10 | uses: matlab-actions/setup-matlab@v2 11 | with: 12 | release: ${{ matrix.release }} 13 | cache: true 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | 26 | deploy: 27 | 28 | strategy: 29 | matrix: 30 | release: ["latest"] 31 | 32 | environment: 33 | name: github-pages 34 | url: ${{ steps.deployment.outputs.page_url }} 35 | 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Setup Pages 42 | uses: actions/configure-pages@v5 43 | 44 | - uses: ./.github/workflows/composite-install-matlab 45 | 46 | - name: Run Matlab buildtool 47 | uses: matlab-actions/run-build@v2 48 | with: 49 | tasks: test publish 50 | 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v3 53 | with: 54 | path: 'docs/' 55 | 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | resources/ 3 | *.asv 4 | *.m~ 5 | code-coverage.xml 6 | .buildtool/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2014-2022, SciVision, Inc. 4 | Copyright (c) 2013, Felipe Geremia Nievinski 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MatMap3d 2 | 3 | [![DOI](https://zenodo.org/badge/144219717.svg)](https://zenodo.org/badge/latestdoi/144219717) 4 | [![View matmap3d on File Exchange](https://www.mathworks.com/matlabcentral/images/matlab-file-exchange.svg)](https://www.mathworks.com/matlabcentral/fileexchange/68480-matmap3d) 5 | [![ci](https://github.com/geospace-code/matmap3d/actions/workflows/ci.yml/badge.svg)](https://github.com/geospace-code/matmap3d/actions/workflows/ci.yml) 6 | 7 | Matlab coordinate conversions for geospace ecef enu eci. 8 | Similar to Python 9 | [PyMap3D](https://github.com/geospace-code/pymap3d). 10 | 11 | ## Usage 12 | 13 | MatMap3D is setup as a 14 | [Matlab package](https://www.mathworks.com/help/matlab/matlab_oop/scoping-classes-with-packages.html), 15 | which means `import matmap3d` statements allow scoped use of this code. 16 | 17 | ```matlab 18 | 19 | [x,y,z] = matmap3d.geodetic2ecef([],lat,lon,alt) 20 | 21 | [az,el,range] = matmap3d.geodetic2aer(lat, lon, alt, observer_lat, observer_lon, observer_alt) 22 | ``` 23 | 24 | Optionally, run self-tests: 25 | 26 | ```matlab 27 | buildtool 28 | ``` 29 | 30 | ### Functions 31 | 32 | Popular mapping & aerospace toolbox functions ported to Matlab, where the source coordinate system (before the "2") is converted to the desired coordinate system: 33 | 34 | Abbreviations: 35 | 36 | * [AER: Azimuth, Elevation, Range](https://en.wikipedia.org/wiki/Spherical_coordinate_system) 37 | * [ECEF: Earth-centered, Earth-fixed](https://en.wikipedia.org/wiki/ECEF) 38 | * [ECI: Earth-centered Inertial](https://en.wikipedia.org/wiki/Earth-centered_inertial) 39 | * [ENU: East North Up](https://en.wikipedia.org/wiki/Axes_conventions#Ground_reference_frames:_ENU_and_NED) 40 | * [NED: North East Down](https://en.wikipedia.org/wiki/North_east_down) 41 | * [radec: right ascension, declination](https://en.wikipedia.org/wiki/Right_ascension) 42 | 43 | ### Caveats 44 | 45 | * Atmospheric effects neglected in all functions not invoking AstroPy. 46 | Would need to update code to add these input parameters (just start a GitHub Issue to request). 47 | * Planetary perturbations and nutation etc. not fully considered. 48 | 49 | These functions present a similar API of a subset of functions in the Mathworks Matlab: 50 | 51 | * [Mapping Toolbox](https://www.mathworks.com/products/mapping.html) 52 | * [Aerospace Blockset](https://www.mathworks.com/products/aerospace-blockset.html) 53 | 54 | ## Notes 55 | 56 | Python PyMap3d has more conversions. 57 | PyMap3d can be accessed from Matlab by commands like: 58 | 59 | ```matlab 60 | lla = py.pymap3d.geodetic2ecef(x,y,z) 61 | ``` 62 | 63 | In particular, since PyMap3D uses Astropy for ECI transformations the accuracy will generally be better for `eci2*` or `*2eci` functions. 64 | All other functions should have equivalent accuracy with Matlab vs. Python. 65 | 66 | ### GNU Octave 67 | 68 | GNU Octave users should consider the 69 | [Octave Mapping Toolbox](https://gnu-octave.github.io/packages/mapping/), 70 | which added similar functions in 71 | [version 1.4.2](https://sourceforge.net/p/octave/mapping/ci/Release-1.4.2/tree/NEWS). 72 | -------------------------------------------------------------------------------- /buildfile.m: -------------------------------------------------------------------------------- 1 | function plan = buildfile 2 | 3 | plan = buildplan(); 4 | 5 | plan.DefaultTasks = "test"; 6 | 7 | pkg_name = "+matmap3d"; 8 | 9 | if isMATLABReleaseOlderThan("R2023b") 10 | plan("test") = matlab.buildtool.Task(Actions=@legacy_test); 11 | else 12 | plan("check") = matlab.buildtool.tasks.CodeIssuesTask(pkg_name, IncludeSubfolders=true, ... 13 | WarningThreshold=0); 14 | plan("test") = matlab.buildtool.tasks.TestTask("test", Strict=false); 15 | end 16 | 17 | if ~isMATLABReleaseOlderThan("R2024a") 18 | plan("coverage") = matlab.buildtool.tasks.TestTask(Description="code coverage", SourceFiles="test", Strict=false, CodeCoverageResults="code-coverage.xml"); 19 | end 20 | 21 | plan("publish") = matlab.buildtool.Task(Description="HTML inline doc generate", Actions=@publish_html); 22 | 23 | 24 | end 25 | 26 | 27 | function legacy_test(context) 28 | r = runtests(fullfile(context.Plan.RootFolder, "test"), Strict=false); 29 | % Parallel Computing Toolbox takes more time to startup than is worth it for this task 30 | 31 | assert(~isempty(r), "No tests were run") 32 | assertSuccess(r) 33 | end 34 | 35 | 36 | function publish_html(context) 37 | outdir = fullfile(context.Plan.RootFolder, "docs"); 38 | 39 | publish_gen_index_html("matmap3d", ... 40 | "Geographic coordinate tranformation functions for Matlab.", ... 41 | "https://github.com/geospace-code/matmap3d", ... 42 | outdir) 43 | end 44 | -------------------------------------------------------------------------------- /codemeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://doi.org/10.5063/schema/codemeta-2.0", 3 | "@type": "SoftwareSourceCode", 4 | "license": "https://spdx.org/licenses/BSD-3-Clause", 5 | "codeRepository": "https://github.com/geospace-code/matmap3d", 6 | "contIntegration": "https://github.com/geospace-code/matmap3d/actions", 7 | "downloadUrl": "https://github.com/geospace-code/matmap3d/releases", 8 | "issueTracker": "https://github.com/geospace-code/matmap3d/issues", 9 | "name": "matmap3d", 10 | "identifier": "10.5281/zenodo.3966173", 11 | "description": "Matlab / GNU Octave coordinate conversions for geospace ecef enu eci. Similar to Python PyMap3D.", 12 | "applicationCategory": "compuation", 13 | "developmentStatus": "active", 14 | "keywords": [ 15 | "geodesy" 16 | ], 17 | "programmingLanguage": [ 18 | "Matlab" 19 | ], 20 | "author": [ 21 | { 22 | "@type": "Person", 23 | "@id": "https://orcid.org/0000-0002-1637-6526", 24 | "givenName": "Michael", 25 | "familyName": "Hirsch" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /private/publish_gen_index_html.m: -------------------------------------------------------------------------------- 1 | %% PUBLISH_GEN_INDEX_HTML generate index.html for package docs 2 | % publish (generate) docs from Matlab project 3 | % called from buildfile.m 4 | % 5 | % Ref: 6 | % * https://www.mathworks.com/help/matlab/ref/publish.html 7 | % * https://www.mathworks.com/help/matlab/matlab_prog/marking-up-matlab-comments-for-publishing.html 8 | % 9 | % for package code -- assumes no classes and depth == 1 10 | % 11 | % if *.mex* files are present, publish fails 12 | 13 | function publish_gen_index_html(pkg_name, tagline, project_url, outdir) 14 | arguments 15 | pkg_name (1,1) string 16 | tagline (1,1) string 17 | project_url (1,1) string 18 | outdir (1,1) string 19 | end 20 | 21 | pkg = what("+" + pkg_name); 22 | % "+" avoids picking up cwd of same name 23 | assert(~isempty(pkg), pkg_name + " is not detected as a Matlab directory or package") 24 | 25 | %% Git info 26 | repo = gitrepo(pkg.path); 27 | git_txt = "Git branch / commit: " + repo.CurrentBranch.Name + " " + extractBefore(repo.LastCommit.ID, 8); 28 | 29 | %% generate docs 30 | readme = fullfile(outdir, "index.html"); 31 | 32 | if ~isfolder(outdir) 33 | mkdir(outdir); 34 | end 35 | 36 | txt = ["", ... 37 | "",... 38 | '', ... 39 | '', ... 40 | '', ... 41 | "" + pkg_name + " API", ... 42 | "", ... 43 | "", ... 44 | "

" + pkg_name + " API

", ... 45 | "

" + tagline + "

", ... 46 | "

" + git_txt + "

", ... 47 | "

Project URL: " + project_url + "

", ... 48 | "

API Reference

"]; 49 | fid = fopen(readme, 'w'); 50 | fprintf(fid, join(txt, "\n")); 51 | 52 | for sub = pkg.m.' 53 | 54 | s = sub{1}; 55 | [~, name] = fileparts(s); 56 | 57 | doc_fn = publish(pkg_name + "." + name, evalCode=false, outputDir=outdir); 58 | disp(doc_fn) 59 | 60 | % inject summary for each function into Readme.md 61 | help_txt = split(string(help(pkg_name + "." + name)), newline); 62 | words = split(strip(help_txt(1)), " "); 63 | 64 | % error if no docstring 65 | fname = words(1); 66 | assert(lower(fname) == lower(name), "fname %s does not match name %s \nis there a docstring at the top of the .m file?", fname, name) 67 | 68 | line = "" + fname + " "; 69 | if(length(words) > 1) 70 | line = line + join(words(2:end)); 71 | end 72 | 73 | req = ""; 74 | 75 | if contains(help_txt(2), "requires:") || contains(help_txt(2), "optional:") 76 | req = "(" + strip(help_txt(2), " ") + ")"; 77 | end 78 | 79 | fprintf(fid, line + " " + req + "
\n"); 80 | 81 | end 82 | 83 | fprintf(fid, " "); 84 | 85 | fclose(fid); 86 | 87 | end 88 | -------------------------------------------------------------------------------- /test/TestUnit.m: -------------------------------------------------------------------------------- 1 | classdef TestUnit < matlab.unittest.TestCase 2 | 3 | properties 4 | TestData 5 | end 6 | 7 | 8 | 9 | methods(TestClassSetup) 10 | function setup_path(tc) 11 | import matlab.unittest.fixtures.PathFixture 12 | cwd = fileparts(mfilename("fullpath")); 13 | top = fullfile(cwd, ".."); 14 | tc.applyFixture(PathFixture(top)) 15 | end 16 | 17 | function setup(tc) 18 | tc.TestData.atol = 1e-9; 19 | tc.TestData.rtol = 1e-6; 20 | 21 | tc.TestData.E = matmap3d.wgs84Ellipsoid(); 22 | 23 | tc.TestData.lat = 42; 24 | tc.TestData.lon = -82; 25 | tc.TestData.alt = 200; 26 | tc.TestData.angleUnit='d'; 27 | tc.TestData.x0 = 660.6752518e3; 28 | tc.TestData.y0 = -4700.9486832e3; 29 | tc.TestData.z0 = 4245.7376622e3; 30 | 31 | tc.TestData.t0 = datetime(2014,4,6,8,0,0); 32 | 33 | tc.TestData.lat1 = 42.0026; 34 | tc.TestData.lon1 = -81.9978; 35 | tc.TestData.alt1 = 1.1397e3; 36 | 37 | tc.TestData.er = 186.277521; 38 | tc.TestData.nr = 286.84222; 39 | tc.TestData.ur = 939.69262; 40 | tc.TestData.az = 33; 41 | tc.TestData.el = 70; 42 | tc.TestData.srange = 1e3; 43 | 44 | tc.TestData.xl = 6.609301927610815e+5; 45 | tc.TestData.yl = -4.701424222957011e6; 46 | tc.TestData.zl = 4.246579604632881e+06; % aer2ecef 47 | 48 | tc.TestData.atol_dist = 1e-3; % 1 mm 49 | 50 | tc.TestData.a90 = 90; 51 | end 52 | end 53 | 54 | methods(Test) 55 | 56 | function test_ellipsoid(tc) 57 | E = matmap3d.wgs84Ellipsoid(); 58 | tc.verifyClass(E, 'matmap3d.referenceEllipsoid') 59 | end 60 | 61 | function test_geodetic2ecef(tc) 62 | 63 | atol = tc.TestData.atol; 64 | rtol = tc.TestData.rtol; 65 | E = tc.TestData.E; 66 | 67 | [x,y,z] = matmap3d.geodetic2ecef(E, tc.TestData.lat, tc.TestData.lon, tc.TestData.alt, tc.TestData.angleUnit); 68 | tc.verifyEqual([x,y,z], [tc.TestData.x0, tc.TestData.y0, tc.TestData.z0], 'AbsTol', atol, 'RelTol', rtol) 69 | 70 | [x,y,z] = matmap3d.geodetic2ecef([], 0,0,-1); 71 | tc.verifyEqual([x,y,z], [E.SemimajorAxis-1,0,0], 'AbsTol', atol, 'RelTol', rtol) 72 | 73 | [x,y,z] = matmap3d.geodetic2ecef(E, 0,90,-1); 74 | tc.verifyEqual([x,y,z], [0, E.SemimajorAxis-1,0], 'AbsTol', atol, 'RelTol', rtol) 75 | 76 | [x,y,z] = matmap3d.geodetic2ecef(E, 0,-90,-1); 77 | tc.verifyEqual([x,y,z], [0, -E.SemimajorAxis+1,0], 'AbsTol', atol, 'RelTol', rtol) 78 | 79 | [x,y,z] = matmap3d.geodetic2ecef(E, 90,0,-1); 80 | tc.verifyEqual([x,y,z], [0,0,E.SemiminorAxis-1], 'AbsTol', atol, 'RelTol', rtol) 81 | 82 | [x,y,z] = matmap3d.geodetic2ecef(E, 90,15,-1); 83 | tc.verifyEqual([x,y,z], [0,0,E.SemiminorAxis-1], 'AbsTol', atol, 'RelTol', rtol) 84 | 85 | [x,y,z] = matmap3d.geodetic2ecef(E, -90,0,-1); 86 | tc.verifyEqual([x,y,z], [0,0,-E.SemiminorAxis+1], 'AbsTol', atol, 'RelTol', rtol) 87 | 88 | end 89 | 90 | function test_ecef2geodetic(tc) 91 | 92 | atol = tc.TestData.atol; 93 | rtol = tc.TestData.rtol; 94 | 95 | E = tc.TestData.E; 96 | ea = E.SemimajorAxis; 97 | eb = E.SemiminorAxis; 98 | 99 | [lt, ln, at] = matmap3d.ecef2geodetic(E, tc.TestData.x0, tc.TestData.y0, tc.TestData.z0, tc.TestData.angleUnit); 100 | tc.verifyEqual([lt, ln, at], [tc.TestData.lat, tc.TestData.lon, tc.TestData.alt], 'AbsTol', atol, 'RelTol', rtol) 101 | 102 | [lt, ln, at] = matmap3d.ecef2geodetic([], ea-1, 0, 0); 103 | tc.verifyEqual([lt, ln, at], [0, 0, -1], 'AbsTol', atol, 'RelTol', rtol) 104 | 105 | [lt, ln, at] = matmap3d.ecef2geodetic(E, 0, ea-1, 0); 106 | tc.verifyEqual([lt, ln, at], [0, 90, -1], 'AbsTol', atol, 'RelTol', rtol) 107 | 108 | [lt, ln, at] = matmap3d.ecef2geodetic(E, 0, 0, eb-1); 109 | tc.verifyEqual([lt, ln, at], [90, 0, -1], 'AbsTol', atol, 'RelTol', rtol) 110 | 111 | [lt, ln, at] = matmap3d.ecef2geodetic(E, 0, 0, -eb+1); 112 | tc.verifyEqual([lt, ln, at], [-90, 0, -1], 'AbsTol', atol, 'RelTol', rtol) 113 | 114 | [lt, ln, at] = matmap3d.ecef2geodetic(E, -ea+1, 0, 0); 115 | tc.verifyEqual([lt, ln, at], [0, 180, -1], 'AbsTol', atol, 'RelTol', rtol) 116 | 117 | [lt, ln, at] = matmap3d.ecef2geodetic(E, (ea-1000)/sqrt(2), (ea-1000)/sqrt(2), 0); 118 | tc.verifyEqual([lt,ln,at], [0,45,-1000], 'AbsTol', atol, 'RelTol', rtol) 119 | 120 | end 121 | 122 | function test_enu2aer(tc) 123 | 124 | atol = tc.TestData.atol; 125 | rtol = tc.TestData.rtol; 126 | 127 | [a, e, r] = matmap3d.enu2aer(tc.TestData.er, tc.TestData.nr, tc.TestData.ur, tc.TestData.angleUnit); 128 | tc.verifyEqual([a,e,r], [tc.TestData.az, tc.TestData.el, tc.TestData.srange] , 'AbsTol', atol, 'RelTol', rtol) 129 | 130 | [a, e, r] = matmap3d.enu2aer(1, 0, 0, tc.TestData.angleUnit); 131 | tc.verifyEqual([a,e,r], [tc.TestData.a90, 0, 1], 'AbsTol', atol, 'RelTol', rtol) 132 | end 133 | 134 | function test_aer2enu(tc) 135 | atol = tc.TestData.atol; 136 | rtol = tc.TestData.rtol; 137 | 138 | [e,n,u] = matmap3d.aer2enu(tc.TestData.az, tc.TestData.el, tc.TestData.srange, tc.TestData.angleUnit); 139 | tc.verifyEqual([e,n,u], [tc.TestData.er, tc.TestData.nr, tc.TestData.ur], 'AbsTol', atol, 'RelTol', rtol) 140 | 141 | [n1,e1,d] = matmap3d.aer2ned(tc.TestData.az, tc.TestData.el, tc.TestData.srange, tc.TestData.angleUnit); 142 | tc.verifyEqual([e,n,u], [e1,n1,-d]) 143 | 144 | [a,e,r] = matmap3d.enu2aer(e,n,u, tc.TestData.angleUnit); 145 | tc.verifyEqual([a,e,r], [tc.TestData.az, tc.TestData.el, tc.TestData.srange], 'AbsTol', atol, 'RelTol', rtol) 146 | end 147 | 148 | function test_ecef2aer(tc) 149 | 150 | atol = tc.TestData.atol; 151 | rtol = tc.TestData.rtol; 152 | 153 | E = tc.TestData.E; 154 | a90 = tc.TestData.a90; 155 | angleUnit = tc.TestData.angleUnit; 156 | 157 | [a, e, r] = matmap3d.ecef2aer(tc.TestData.xl, tc.TestData.yl, tc.TestData.zl, tc.TestData.lat, tc.TestData.lon, tc.TestData.alt, E, angleUnit); 158 | % round-trip 159 | tc.verifyEqual([a,e,r], [tc.TestData.az, tc.TestData.el, tc.TestData.srange], 'AbsTol', atol, 'RelTol', rtol) 160 | 161 | % singularity check 162 | [a, e, r] = matmap3d.ecef2aer(E.SemimajorAxis-1, 0, 0, 0,0,0, E, angleUnit); 163 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 164 | 165 | [a, e, r] = matmap3d.ecef2aer(-E.SemimajorAxis+1, 0, 0, 0, 2*a90,0, E, angleUnit); 166 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 167 | 168 | [a, e, r] = matmap3d.ecef2aer(0, E.SemimajorAxis-1, 0,0, a90,0, E, angleUnit); 169 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 170 | 171 | [a, e, r] = matmap3d.ecef2aer(0, -E.SemimajorAxis+1, 0,0, -a90,0, E, angleUnit); 172 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 173 | 174 | [a, e, r] = matmap3d.ecef2aer(0, 0, E.SemiminorAxis-1, a90, 0, 0, E, angleUnit); 175 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 176 | 177 | [a, e, r] = matmap3d.ecef2aer(0, 0, -E.SemiminorAxis+1,-a90,0,0, E, angleUnit); 178 | tc.verifyEqual([a,e,r], [0, -a90, 1], 'AbsTol', atol, 'RelTol', rtol) 179 | 180 | [a, e, r] = matmap3d.ecef2aer((E.SemimajorAxis-1000)/sqrt(2), (E.SemimajorAxis-1000)/sqrt(2), 0, 0, 45, 0); 181 | tc.verifyEqual([a,e,r],[0,-90,1000], 'AbsTol', atol, 'RelTol', rtol) 182 | 183 | [x,y,z] = matmap3d.aer2ecef(tc.TestData.az, tc.TestData.el, tc.TestData.srange, tc.TestData.lat, tc.TestData.lon, tc.TestData.alt,E, angleUnit); 184 | tc.verifyEqual([x,y,z], [tc.TestData.xl, tc.TestData.yl, tc.TestData.zl], 'AbsTol', atol, 'RelTol', rtol) 185 | 186 | [a,e,r] = matmap3d.ecef2aer(x,y,z, tc.TestData.lat, tc.TestData.lon, tc.TestData.alt, E, angleUnit); 187 | tc.verifyEqual([a,e,r], [tc.TestData.az, tc.TestData.el, tc.TestData.srange], 'AbsTol', atol, 'RelTol', rtol) 188 | end 189 | 190 | function test_geodetic2aer(tc) 191 | 192 | atol = tc.TestData.atol; 193 | rtol = tc.TestData.rtol; 194 | 195 | E = tc.TestData.E; 196 | angleUnit = tc.TestData.angleUnit; 197 | lat = tc.TestData.lat; 198 | lon = tc.TestData.lon; 199 | alt = tc.TestData.alt; 200 | 201 | [lt,ln,at] = matmap3d.aer2geodetic(tc.TestData.az, tc.TestData.el, tc.TestData.srange, lat, lon, alt, E, angleUnit); 202 | tc.verifyEqual([lt,ln,at], [tc.TestData.lat1, tc.TestData.lon1, tc.TestData.alt1], 'AbsTol', 2*tc.TestData.atol_dist) 203 | 204 | [a, e, r] = matmap3d.geodetic2aer(lt,ln,at, lat, lon, alt, E, angleUnit); % round-trip 205 | tc.verifyEqual([a,e,r], [tc.TestData.az, tc.TestData.el, tc.TestData.srange], 'AbsTol', atol, 'RelTol', rtol) 206 | end 207 | 208 | function test_geodetic2enu(tc) 209 | 210 | atol = tc.TestData.atol; 211 | rtol = tc.TestData.rtol; 212 | E = tc.TestData.E; 213 | angleUnit = tc.TestData.angleUnit; 214 | lat = tc.TestData.lat; 215 | lon = tc.TestData.lon; 216 | alt = tc.TestData.alt; 217 | 218 | [e, n, u] = matmap3d.geodetic2enu(lat, lon, alt-1, lat, lon, alt, E, angleUnit); 219 | tc.verifyEqual([e,n,u], [0,0,-1], 'AbsTol', atol, 'RelTol', rtol) 220 | 221 | [lt, ln, at] = matmap3d.enu2geodetic(e,n,u,lat,lon,alt, E, angleUnit); % round-trip 222 | tc.verifyEqual([lt, ln, at],[lat, lon, alt-1], 'AbsTol', atol, 'RelTol', rtol) 223 | end 224 | 225 | function test_enu2ecef(tc) 226 | 227 | atol = tc.TestData.atol; 228 | rtol = tc.TestData.rtol; 229 | E = tc.TestData.E; 230 | angleUnit = tc.TestData.angleUnit; 231 | lat = tc.TestData.lat; 232 | lon = tc.TestData.lon; 233 | alt = tc.TestData.alt; 234 | 235 | [x, y, z] = matmap3d.enu2ecef(tc.TestData.er, tc.TestData.nr, tc.TestData.ur, lat,lon,alt, E, angleUnit); 236 | tc.verifyEqual([x,y,z],[tc.TestData.xl, tc.TestData.yl, tc.TestData.zl], 'AbsTol', atol, 'RelTol', rtol) 237 | 238 | [e,n,u] = matmap3d.ecef2enu(x,y,z,lat,lon,alt, E, angleUnit); % round-trip 239 | tc.verifyEqual([e,n,u],[tc.TestData.er, tc.TestData.nr, tc.TestData.ur], 'AbsTol', atol, 'RelTol', rtol) 240 | 241 | [n1, e1, d] = matmap3d.ecef2ned(x,y,z,lat,lon,alt, E, angleUnit); 242 | tc.verifyEqual([e,n,u],[e1,n1,-d]) 243 | end 244 | 245 | function test_lookAtSpheroid(tc) 246 | 247 | atol = tc.TestData.atol; 248 | rtol = tc.TestData.rtol; 249 | 250 | lat = tc.TestData.lat; 251 | lon = tc.TestData.lon; 252 | alt = tc.TestData.alt; 253 | 254 | az5 = [0., 10., 125.]; 255 | tilt = [30, 45, 90]; 256 | 257 | [lat5, lon5, rng5] = matmap3d.lookAtSpheroid(lat, lon, alt, az5, 0.); 258 | tc.verifyEqual(lat5, repmat(lat, 1, 3), 'AbsTol', atol, 'RelTol', rtol) 259 | tc.verifyEqual(lon5, repmat(lon, 1, 3), 'AbsTol', atol, 'RelTol', rtol) 260 | tc.verifyEqual(rng5, repmat(alt, 1, 3), 'AbsTol', atol, 'RelTol', rtol) 261 | 262 | 263 | [lat5, lon5, rng5] = matmap3d.lookAtSpheroid(lat, lon, alt, az5, tilt); 264 | 265 | truth = [42.00103959, lon, 230.9413173; 266 | 42.00177328, -81.9995808, 282.84715651; 267 | nan, nan, nan]; 268 | 269 | tc.verifyEqual([lat5, lon5, rng5], truth(:).', 'AbsTol', atol, 'RelTol', rtol) 270 | end 271 | 272 | function test_eci2ecef(tc) 273 | utc = datetime(2019, 1, 4, 12,0,0); 274 | eci = [-2981784, 5207055, 3161595]; 275 | [x, y, z] = matmap3d.eci2ecef(utc, eci(1), eci(2), eci(3)); 276 | tc.verifyEqual([x,y,z], [-5.7627e6, -1.6827e6, 3.1560e6], 'reltol', 0.02) 277 | end 278 | 279 | function test_eci2ecef_multiple(tc) 280 | utc = datetime(2019, 1, 4, 12,0,0); 281 | utc = [utc;utc]; 282 | eci = [-2981784, 5207055, 3161595]; eci = [eci; eci]; 283 | [x, y, z] = matmap3d.eci2ecef(utc, eci(:,1), eci(:,2), eci(:,3)); 284 | tc.verifyEqual([x(1,1), y(1,1), z(1,1)], [-5.7627e6, -1.6827e6, 3.1560e6], 'reltol', 0.02) 285 | tc.verifyEqual([x(2,1), y(2,1), z(2,1)], [-5.7627e6, -1.6827e6, 3.1560e6], 'reltol', 0.02) 286 | end 287 | 288 | function test_ecef2eci(tc) 289 | ecef = [-5762640, -1682738, 3156028]; 290 | utc = datetime(2019, 1, 4, 12,0,0); 291 | [x,y,z] = matmap3d.ecef2eci(utc, ecef(1), ecef(2), ecef(3)); 292 | tc.verifyEqual([x,y,z], [-2.9818e6, 5.2070e6, 3.1616e6], 'RelTol', 0.01) 293 | end 294 | 295 | function test_ecef2eci_multiple(tc) 296 | ecef = [-5762640, -1682738, 3156028]; ecef = [ecef; ecef]; 297 | utc = datetime(2019, 1, 4, 12, 0, 0); 298 | utc = [utc; utc]; 299 | [x,y,z] = matmap3d.ecef2eci(utc, ecef(:,1), ecef(:,2), ecef(:,3)); 300 | tc.verifyEqual([x(1,1), y(1,1) , z(1,1)], [-2.9818e6, 5.2070e6, 3.1616e6], 'RelTol', 0.01) 301 | tc.verifyEqual([x(2,1), y(2,1) , z(2,1)], [-2.9818e6, 5.2070e6, 3.1616e6], 'RelTol', 0.01) 302 | end 303 | 304 | function test_eci2aer(tc) 305 | eci = [-3.8454e8, -0.5099e8, -0.3255e8]; 306 | utc = datetime(1969, 7, 20, 21, 17, 40); 307 | lla = [28.4, -80.5, 2.7]; 308 | [a, e, r] = matmap3d.eci2aer(utc, eci(1), eci(2), eci(3), lla(1), lla(2), lla(3)); 309 | tc.verifyEqual([a, e, r], [162.55, 55.12, 384013940.9], 'RelTol', 0.01) 310 | end 311 | 312 | function test_aer2eci(tc) 313 | aer = [162.55, 55.12, 384013940.9]; 314 | lla = [28.4, -80.5, 2.7]; 315 | utc = datetime(1969, 7, 20, 21, 17, 40); 316 | [x,y,z] = matmap3d.aer2eci(utc, aer(1), aer(2), aer(3), lla(1), lla(2), lla(3)); 317 | tc.verifyEqual([x, y, z], [-3.8454e8, -0.5099e8, -0.3255e8], 'RelTol', 0.06) 318 | end 319 | 320 | end 321 | end 322 | --------------------------------------------------------------------------------