1212import os
1313import time
1414import datetime
15+ import math
1516
1617# blender
1718try :
125126# utm projections for locations
126127transformer_to_utm = None
127128utm_zone = None
129+ use_moon_ltm = False
128130
129131# timing info
130132time_string = None
@@ -158,6 +160,21 @@ def __exit__(self, type, value, traceback):
158160
159161def 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+
244496def 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
26242876def 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