Skip to content

Commit 2cd3770

Browse files
committed
adds lunar projections to scripts
1 parent 32d2579 commit 2cd3770

2 files changed

Lines changed: 596 additions & 19 deletions

File tree

utils/Visualization/Blender/python_blender/plot_with_blender.py

Lines changed: 256 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import os
1313
import time
1414
import datetime
15+
import math
1516

1617
# blender
1718
try:
@@ -125,6 +126,7 @@
125126
# utm projections for locations
126127
transformer_to_utm = None
127128
utm_zone = None
129+
use_moon_ltm = False
128130

129131
# timing info
130132
time_string = None
@@ -158,6 +160,21 @@ def __exit__(self, type, value, traceback):
158160

159161
def convert_latlon_to_UTM(lat, lon):
160162
global transformer_to_utm
163+
global use_moon_ltm
164+
global utm_zone
165+
166+
if use_moon_ltm:
167+
print(" converting coordinates to Moon LTM/LPS...")
168+
# determine zone if not specified
169+
if utm_zone == None:
170+
utm_zone = geo2ltm(lon, lat, zone=None, iway=2)
171+
hemisphere_auto = 'N' if utm_zone > 0 else 'S'
172+
print(" detected LTM/LPS zone: {} - {} hemisphere".format(utm_zone,hemisphere_auto))
173+
print("")
174+
175+
# convert to LTM/LPS
176+
ltm_x, ltm_y = geo2ltm(lon, lat, utm_zone, iway=2)
177+
return ltm_x, ltm_y
161178

162179
# transform lat/lon to UTM
163180
if transformer_to_utm == None:
@@ -241,6 +258,241 @@ def convert_latlon_to_UTM(lat, lon):
241258

242259
return x_utm,y_utm
243260

261+
262+
def geo2ltm(lon, lat, zone=None, iway=2):
263+
"""
264+
Lunar Transverse Mercator (LTM) and Lunar Polar Stereographic (LPS) projection for polar regions
265+
266+
this routine assumes a perfectly spherical Moon.
267+
LTM implementation is a simplified Spherical Transverse Mercator, 8-degree zones.
268+
269+
note: by default, the LTM implementation in the python script LGRS_Coordinate_Conversion_mk7.2.py provided by USGS astrogeology site
270+
https://astrogeology.usgs.gov/search/map/lunar-map-projections-and-grid-reference-system-for-artemis-astronaut-surface-navigation
271+
uses a spherical Moon for their LTM projection.
272+
273+
However, they still use the formula for a Gauss-Schreiber projection,
274+
that combines a projection of an ellipsoid to a sphere followed by a spherical transverse Mercator formula.
275+
Assuming the ellipsoid is perfectly spherical, then the first projection is an identity transform and only the second,
276+
spherical transverse Mercator formula is needed.
277+
Here, we use this simplification of applying directly the spherical transverse Mercator projection, assuming a spherical Moon.
278+
279+
lon - longitude in degree range [-180,180]
280+
lat - latitude in degree range [-90,90]
281+
282+
zone - LTM zones use 1 to 45, positive for Northern hemisphere, negative for Southern hemisphere
283+
LPS zones use 46 for North pole, -46 for South pole
284+
285+
iway = 1 from LTM to lon/lat
286+
iway = 2 from lon/lat to LTM
287+
"""
288+
289+
PI = math.pi
290+
degrad = PI / 180.0
291+
raddeg = 180.0 / PI
292+
293+
# ---- Lunar spherical radius ----
294+
R = 1737400.0 # mean lunar radius (meters)
295+
296+
# ---- Scale factor ----
297+
# Lunar map projection scale factors
298+
scfa = 0.999 # transverse Mercator (LTM)
299+
scfa_polar = 0.994 # polar stereographic (LPS)
300+
301+
# ---- False origins ----
302+
false_east = 250000.0
303+
false_north = 2500000.0
304+
305+
false_east_polar = 500000.0
306+
false_north_polar = 500000.0
307+
308+
ILTM2LONGLAT = 1
309+
ILONGLAT2LTM = 2
310+
311+
#----- Set Zone parameters
312+
# determine zone
313+
# for convenience, if zone is unspecified in forward mode, this computes it for the given longitude/latitude position
314+
# and returns only the zone number
315+
if iway == ILONGLAT2LTM and (zone is None or zone == 0):
316+
# checks latitute range for LTM [-82,82], beyond is polar region
317+
if lat >= -82.0 and lat <= 82.0:
318+
# LTM
319+
# longitudinal zone
320+
zone = int((lon + 180.0) // 8) + 1 # note: uses +1 because USGS's LGRS_Coordinate_Conversion_mk7.2.py has zone index starting at 1
321+
# 180-degree values sometimes come up as 46. We assign it back to zone 1
322+
if zone > 45:
323+
zone -= 45
324+
# we use negative values for Southern hemisphere
325+
if lat < 0.0:
326+
zone = -zone
327+
else:
328+
# LPS
329+
# polar region
330+
if lat >= 0.0:
331+
zone = 46 # north pole
332+
else:
333+
zone = -46 # south pole
334+
# just return zone
335+
return zone
336+
337+
# zone is given as input, check if valid
338+
if zone is None or zone == 0 or int(abs(zone)) > 46:
339+
print(f"error: geo2ltm routine has as input zone {zone}, which is invalid. zone must be +/- 1-45 for LTM and +/- 46 for LPS")
340+
sys.exit(1)
341+
342+
# zone index absolute
343+
z = int(abs(zone))
344+
345+
# polar region
346+
use_polar = False
347+
# check if LPS or LTM
348+
if z == 46: use_polar = True
349+
350+
# Lunar Polar Stereographic (LPS) projection
351+
if use_polar:
352+
if iway == ILONGLAT2LTM:
353+
# Forward transformation: lon/lat to LPS
354+
# Snyder (1987) spherical equations
355+
rlon = lon * degrad
356+
rlat = lat * degrad
357+
358+
# Calculate polar stereographic spherical scale error
359+
A = 2.0 * R * scfa_polar
360+
361+
if zone < 0:
362+
# South pole
363+
t = math.tan(PI/4.0 + rlat/2.0)
364+
else:
365+
# North pole
366+
t = math.tan(PI/4.0 - rlat/2.0)
367+
368+
# stereographic map projection, Snyder (1987)
369+
# note: here, we use +cos(lon) as done in routine spherical_stereographic_map_y() of the USGS script LGRS_Coordinate_Conversion_mk7.2.py
370+
# for both, North and South poles. the standard polar stereographic projection defined by Snyder would use
371+
# North Pole: x = 2 R k0 tan(pi/4 - phi/2) sin(lambda - lambda0)
372+
# y = - 2 R k0 tan(pi/4 - phi/2) cos(lambda - lambda0)
373+
# South Pole: x = 2 R k0 tan(pi/4 + phi/2) sin(lambda - lambda0)
374+
# y = 2 R k0 tan(pi/4 + phi/2) cos(lambda - lambda0)
375+
# (see Snyder 1987, page 158, chapter 21 "Stereographic Projection", section "Formulas for the Sphere",
376+
# eqs. 21-5, 21-6 and 21-9,21-10.
377+
# https://pubs.usgs.gov/pp/1395/report.pdf ).
378+
# we'll take the convention from the USGS implementation to use +cos for both poles.
379+
#
380+
# X coordinate for a s
381+
x = A * t * math.sin(rlon)
382+
# Y coordinate for a stereographic map projection
383+
y = A * t * math.cos(rlon)
384+
385+
# Add false Eastings and Northings
386+
x += false_east_polar
387+
y += false_north_polar
388+
389+
# all done
390+
return x, y
391+
392+
else:
393+
# Inverse transformation: LPS t0 lon/lat
394+
# remove false origins (must be same values used in forward)
395+
x = lon - false_east_polar
396+
y = lat - false_north_polar
397+
398+
# radial distance from projection origin
399+
rho = math.hypot(x, y)
400+
401+
# at the pole: rho == 0
402+
if rho == 0.0:
403+
deglat = -90.0 if zone < 0 else 90.0
404+
deglon = 0.0
405+
return deglon, deglat
406+
407+
A = 2.0 * R * scfa_polar
408+
409+
# t = tan( PI/4 ± lat/2 ) where sign depends on hemisphere in forward
410+
t = rho / A
411+
412+
if zone < 0:
413+
# South pole
414+
# forward used: tan(PI/4 + lat/2) = t -> lat = 2*atan(t) - PI/2
415+
latr = 2.0 * math.atan(t) - PI/2.0
416+
else:
417+
# North pole
418+
# forward used: tan(PI/4 - lat/2) = t -> lat = PI/2 - 2*atan(t)
419+
latr = PI/2.0 - 2.0 * math.atan(t)
420+
421+
# longitude from sin/cos ordering used in forward: x = factor*sin(lon), y = factor*cos(lon)
422+
lonr = math.atan2(x, y)
423+
424+
deglon = lonr * raddeg
425+
deglat = latr * raddeg
426+
427+
# normalize lon to -180..180
428+
if deglon > 180.0:
429+
deglon -= 360.0
430+
if deglon < -180.0:
431+
deglon += 360.0
432+
433+
return deglon, deglat
434+
435+
436+
# Lunar Transverse Mercator (LTM) Projection
437+
# ---- Central meridian of 8-degree zone ----
438+
# central meridian
439+
cm = -180.0 + ((z - 1) + 0.5) * 8.0 # longitude of central meridian (note: z-1 because zones start at 1)
440+
cmr = cm * degrad
441+
442+
# ----------------------------------------------------------------------
443+
# Forward transformation: lon/lat to LTM
444+
# ----------------------------------------------------------------------
445+
if iway == ILONGLAT2LTM:
446+
rlon = lon * degrad
447+
rlat = lat * degrad
448+
449+
dlam = rlon - cmr
450+
451+
# ---- Spherical Transverse Mercator (forward) ----
452+
B = math.cos(rlat) * math.sin(dlam)
453+
454+
x = 0.5 * R * scfa * math.log((1.0 + B) / (1.0 - B))
455+
y = R * scfa * math.atan2(math.tan(rlat), math.cos(dlam))
456+
457+
# False origins
458+
x += false_east
459+
# only for South sections
460+
if zone < 0:
461+
y += false_north
462+
463+
return x, y
464+
465+
# ----------------------------------------------------------------------
466+
# Inverse transformation: LTM to lon/lat
467+
# ----------------------------------------------------------------------
468+
else:
469+
# remove false origins
470+
x = lon - false_east
471+
if zone < 0:
472+
# only for South sections
473+
y = lat - false_north
474+
else:
475+
y = lat
476+
477+
# ---- Spherical TM inverse ----
478+
D = y / (R * scfa)
479+
T = x / (R * scfa)
480+
481+
lonr = cmr + math.atan2(math.sinh(T), math.cos(D))
482+
latr = math.asin(math.sin(D) / math.cosh(T))
483+
484+
deglon = lonr * raddeg
485+
deglat = latr * raddeg
486+
487+
# normalize lon to -180..180
488+
if deglon > 180.0:
489+
deglon -= 360.0
490+
if deglon < -180.0:
491+
deglon += 360.0
492+
493+
return deglon, deglat
494+
495+
244496
def setup_color_table(colormap, data_min, data_max):
245497
# creates a color table
246498
lut = vtk.vtkLookupTable()
@@ -2624,7 +2876,7 @@ def plot_with_blender(vtk_file: str="", image_title: str="", colormap: int=0, co
26242876
def usage() -> None:
26252877
print("usage: ./plot_with_blender.py [--vtk_file=file] [--title=my_mesh_name] [--colormap=val] [--color-max=val] [--vertical-exaggeration=val]")
26262878
print(" [--buildings=file] [--shift-buildings=val] [--borders=file]")
2627-
print(" [--locations=file] [--utm-zone=ZoneNumber] [--time='val']")
2879+
print(" [--locations=file] [--utm-zone=ZoneNumber] [--moon] [--time='val']")
26282880
print(" [--no-sea-level] [--transparent-sea-level] [--sea-level-separation=val]")
26292881
print(" [--centered] [--closeup] [--small] [--anim] [--matte]")
26302882
print(" [--with-cycles/--no-cycles] [--suppress]")
@@ -2647,6 +2899,7 @@ def usage() -> None:
26472899
print(" --time - a time string to add to the display (e.g. '13:24:00')")
26482900
print(" --white-labels - use white text for location labels")
26492901
print(" --utm_zone - use specified UTM zone number (1-60) with (+) for Northern (-) for Southern hemisphere (e.g., -58)")
2902+
print(" --moon - use Moon projections (LTM/LPS) instead of UTM")
26502903
print(" --no-sea-level - turns off sea-level plane")
26512904
print(" --transparent-sea-level - turns on transparency for sea-level plane")
26522905
print(" --sea-level-separation - shift factor to move up/down points at sea-level for better sea-level separation")
@@ -2709,6 +2962,8 @@ def usage() -> None:
27092962
locations_file = arg.split('=')[1]
27102963
elif "--matte" in arg:
27112964
use_matte_material = True
2965+
elif "--moon" in arg:
2966+
use_moon_ltm = True
27122967
elif "--no-cycles" in arg:
27132968
use_cycles_renderer = False
27142969
elif "--no-dive-in" in arg:

0 commit comments

Comments
 (0)