Source code for ephemeris_tools.viewer

"""Planet viewer tool: PostScript diagram and tables (port of viewer3_*.f)."""

from __future__ import annotations

import logging
import math
import sys
from typing import TextIO

import cspyce

from ephemeris_tools.config import get_starlist_candidate_paths
from ephemeris_tools.constants import (
    DEFAULT_ALIGN_LOC_POINTS,
    EARTH_ID,
    EPHEM_DESCRIPTIONS_BY_PLANET,
    MAX_FOV_DEGREES,
    SUN_ID,
    spacecraft_code_to_id,
    spacecraft_name_to_code,
)
from ephemeris_tools.params import ViewerParams
from ephemeris_tools.rendering.draw_view import (
    FOV_PTS,
    DrawPlanetaryViewOptions,
    draw_planetary_view,
    radec_to_plot,
)
from ephemeris_tools.rendering.planet_grid import compute_planet_grid
from ephemeris_tools.spice.geometry import (
    anti_sun,
    body_lonlat,
    body_phase,
    body_radec,
    limb_radius,
)
from ephemeris_tools.spice.load import load_spacecraft, load_spice_files
from ephemeris_tools.spice.observer import (
    observer_state,
    set_observer_id,
    set_observer_location,
)
from ephemeris_tools.spice.rings import ansa_radec
from ephemeris_tools.stars import read_stars
from ephemeris_tools.time_utils import parse_datetime, tai_from_day_sec, tdb_from_tai
from ephemeris_tools.viewer_helpers import (
    _DEG2RAD,
    _RAD2DEG,
    _compute_jupiter_torus_node,
    _compute_mars_deimos_ring_node,
    _compute_ring_center_offsets,
    _fortran_fixed,
    _fov_deg_from_unit,
    _label_points_from_selection,
    _neptune_arc_model_index,
    _propagated_neptune_arcs,
    _propagated_saturn_f_ring,
    _propagated_uranus_rings,
    _resolve_center_ansa_radius_km,
    _resolve_center_body_id,
    _resolve_viewer_ring_flags,
    _ring_method_from_opacity,
    _strip_leading_option_code,
    _viewer_call_kwargs_from_params,
    _write_fov_table,
    get_planet_config,
)

logger = logging.getLogger(__name__)

__all__ = [
    '_fov_deg_from_unit',
    '_propagated_saturn_f_ring',
    '_resolve_center_ansa_radius_km',
    '_resolve_viewer_ring_flags',
    '_viewer_call_kwargs_from_params',
    'get_planet_config',
    'run_viewer',
]


[docs] def run_viewer(params: ViewerParams) -> None: """Generate planet viewer PostScript diagram and FOV table (port of viewer3_*.f). Loads SPICE, sets observer, computes geometry, and draws planet/rings/moons. Writes PostScript to params.output_ps (if set) and FOV table to params.output_txt or stdout. Parameters: params: Structured viewer inputs (planet, time, FOV, center, observer, rings, moons, display options, output streams). Raises: ValueError: Unknown planet or invalid time. RuntimeError: SPICE load failure. """ kwargs = _viewer_call_kwargs_from_params(params) _run_viewer_impl(**kwargs)
def _run_viewer_impl( *, planet_num: int, time_str: str = '', fov: float = 1.0, center_mode: str = 'J2000', center_ra: float = 0.0, center_dec: float = 0.0, center_body_name: str | None = None, center_ansa_name: str | None = None, center_ansa_ew: str = 'east', center_star_name: str | None = None, viewpoint: str = 'Earth', ephem_version: int = 0, ephem_display: str | None = None, moon_ids: list[int] | None = None, moon_selection_display: str | None = None, blank_disks: bool = False, ring_selection: list[str] | None = None, ring_selection_display: str | None = None, output_ps: TextIO | None = None, output_txt: TextIO | None = None, fov_unit: str | None = None, observer_latitude: float | None = None, observer_longitude: float | None = None, observer_altitude: float | None = None, observer_lon_dir: str = 'east', viewpoint_display: str | None = None, labels: str | None = None, moon_points: float = 0.0, meridian_points: float = 0.0, opacity: str = 'Transparent', peris: str = 'None', peripts: float = 4.0, arcmodel: str | None = None, arcpts: float = 4.0, torus: bool = False, torus_inc: float = 6.8, torus_rad: float = 422000.0, moremoons: bool = False, show_standard_stars: bool = False, extra_star_name: str | None = None, extra_star_ra_deg: float | None = None, extra_star_dec_deg: float | None = None, other_bodies: list[str] | None = None, title: str = '', ) -> None: """Internal viewer implementation (flat kwargs from ViewerParams).""" cfg = get_planet_config(planet_num) if cfg is None: raise ValueError(f'Unknown planet number: {planet_num}') spacecraft_observer_id: int | None = None if observer_latitude is None and observer_longitude is None: viewpoint_code = spacecraft_name_to_code(viewpoint) if viewpoint_code is not None: sc_abbrev = spacecraft_code_to_id(viewpoint_code) if sc_abbrev: try: # FORTRAN loads spacecraft kernels before planet kernels. # This affects SPICE segment precedence for encounter geometry. if load_spacecraft(sc_abbrev, planet_num, ephem_version, set_obs=False): spacecraft_observer_id = viewpoint_code except Exception: spacecraft_observer_id = None # Match FORTRAN viewer: load "other" spacecraft kernels before planet kernels # so the user-selected planet ephemeris still takes precedence. for other_name in other_bodies or []: token = other_name.strip() if token == '': continue low = token.lower() if low in {'sun', 'anti-sun', 'antisun', 'earth', 'barycenter'}: continue sc_code = spacecraft_name_to_code(token) if sc_code is None: continue sc_abbrev = spacecraft_code_to_id(sc_code) if not sc_abbrev: continue try: load_spacecraft(sc_abbrev, planet_num, ephem_version, set_obs=False) except Exception: # Keep viewer behavior tolerant of unavailable spacecraft kernels. pass ok, reason = load_spice_files(planet_num, ephem_version) if not ok: raise RuntimeError(f'Failed to load SPICE kernels: {reason}') if observer_latitude is not None and observer_longitude is not None: set_observer_location( observer_latitude, observer_longitude, observer_altitude if observer_altitude is not None else 0.0, ) else: if spacecraft_observer_id is not None: set_observer_id(spacecraft_observer_id) else: use_earth = True code = spacecraft_name_to_code(viewpoint) if code is not None: sc_id = spacecraft_code_to_id(code) if sc_id: try: use_earth = not load_spacecraft( sc_id, planet_num, ephem_version, set_obs=True ) except Exception: # FORTRAN viewer keeps running for named observatories even when # no trajectory kernels exist for that name at the target epoch. pass if use_earth: set_observer_id(EARTH_ID) parsed = parse_datetime(time_str) if parsed is None: raise ValueError(f'Invalid time: {time_str!r}') day, sec = parsed et = tdb_from_tai(tai_from_day_sec(day, sec)) try: observer_state(et) except Exception: set_observer_id(EARTH_ID) fov_deg = _fov_deg_from_unit(fov, fov_unit, et=et, cfg=cfg) fov_rad = fov_deg * _DEG2RAD # FORTRAN clamps FOV to MAX_FOV_DEGREES to prevent projection singularities fov_rad = min(fov_rad, MAX_FOV_DEGREES * _DEG2RAD) _, _limb_rad_rad = limb_radius(et) planet_ra, planet_dec = body_radec(et, cfg.planet_id) caption_center_body_id = cfg.planet_id caption_center_body_name = cfg.planet_name if center_mode == 'J2000' and (center_ra != 0.0 or center_dec != 0.0): center_ra_rad = center_ra * _DEG2RAD center_dec_rad = center_dec * _DEG2RAD elif center_mode == 'body': center_body_id = _resolve_center_body_id(cfg, center_body_name) center_ra_rad, center_dec_rad = body_radec(et, center_body_id) caption_center_body_id = center_body_id if center_body_id == cfg.barycenter_id and cfg.barycenter_id is not None: caption_center_body_id = cfg.planet_id caption_center_body_name = cfg.planet_name elif center_body_id != cfg.planet_id: for moon in cfg.moons: if moon.id == center_body_id: caption_center_body_name = moon.name break elif center_mode == 'ansa': ring_radius_km = _resolve_center_ansa_radius_km(cfg, center_ansa_name) if ring_radius_km is None: center_ra_rad = planet_ra center_dec_rad = planet_dec else: # Match FORTRAN viewer3_*.f semantics: # center_ew='west' -> right ansa, 'east' -> left ansa. is_right_ansa = center_ansa_ew.strip().lower() == 'west' center_ra_rad, center_dec_rad = ansa_radec(et, ring_radius_km, is_right_ansa) if (center_ra_rad, center_dec_rad) == (0.0, 0.0): center_ra_rad = planet_ra center_dec_rad = planet_dec elif center_mode == 'star': target_star = (center_star_name or '').strip() if len(target_star) == 0: raise ValueError( 'CENTER_STAR is required when center_mode is "star"; provide a non-empty star name.' ) else: starlist_candidates = get_starlist_candidate_paths(cfg.starlist_file) center_ra_rad = planet_ra center_dec_rad = planet_dec found_center_star = False for starlist_path in starlist_candidates: if not starlist_path.exists(): continue try: for star in read_stars(starlist_path, max_stars=1000): if star.name.casefold().strip() == target_star.casefold().strip(): center_ra_rad = star.ra center_dec_rad = star.dec found_center_star = True break except OSError: continue if found_center_star: break if not found_center_star: raise ValueError(f'Invalid value found for variable CENTER_STAR: {target_star}') elif center_ra == 0.0 and center_dec == 0.0: center_ra_rad = planet_ra center_dec_rad = planet_dec else: center_ra_rad = center_ra * _DEG2RAD center_dec_rad = center_dec * _DEG2RAD track_moon_ids = [m.id for m in cfg.moons if m.id != cfg.planet_id] if moon_ids: track_moon_ids = [tid for tid in track_moon_ids if tid in moon_ids] if moremoons: track_moon_ids_set = set(track_moon_ids) for m in cfg.moons: if ( m.id != cfg.planet_id and getattr(m, 'is_irregular', False) and m.id not in track_moon_ids_set ): track_moon_ids.append(m.id) track_moon_ids_set.add(m.id) if moon_ids and not track_moon_ids: logger.warning( 'Moon selection %r matched no moons for planet %s; showing all moons.', moon_ids, planet_num, ) track_moon_ids = [m.id for m in cfg.moons if m.id != cfg.planet_id] id_to_name = {m.id: m.name for m in cfg.moons} # Table: planet first, then moons (same order as FORTRAN moon_flags). table_body_ids = [cfg.planet_id, *track_moon_ids] neptune_arc_model = _neptune_arc_model_index(arcmodel) if output_txt is None: _write_fov_table( sys.stdout, et=et, cfg=cfg, planet_ra=planet_ra, planet_dec=planet_dec, body_ids=table_body_ids, id_to_name=id_to_name, neptune_arc_model=neptune_arc_model, ring_names=ring_selection, ) elif output_txt is not None: _write_fov_table( output_txt, et=et, cfg=cfg, planet_ra=planet_ra, planet_dec=planet_dec, body_ids=table_body_ids, id_to_name=id_to_name, neptune_arc_model=neptune_arc_model, ring_names=ring_selection, ) # Plot: FOV_PTS diameter, scale = FOV_PTS / (2*tan(fov/2)) for camera projection. scale = FOV_PTS / (2.0 * math.tan(fov_rad / 2.0)) def to_plot(ra: float, dec: float) -> tuple[float, float] | None: """Convert (ra, dec) in radians to plot (x, y) via camera projection.""" return radec_to_plot(ra, dec, center_ra_rad, center_dec_rad, fov_rad) bodies: list[tuple[float, float, str, bool]] = [] plot_result = to_plot(planet_ra, planet_dec) if plot_result is not None: px, py = plot_result bodies.append((px, py, cfg.planet_name, True)) for mid in track_moon_ids: ra, dec = body_radec(et, mid) plot_result = to_plot(ra, dec) if plot_result is None: continue mx, my = plot_result name = id_to_name.get(mid, str(mid)) bodies.append((mx, my, name.upper(), False)) # Use CGI/CLI title directly; blank title remains blank. title = title or '' if not blank_disks: compute_planet_grid( et, cfg.planet_id, center_ra_rad, center_dec_rad, scale, ) if output_ps: # Build caption strings matching FORTRAN viewer3_*.f exactly # In FORTRAN, lcaptions use ':' suffix, rcaptions come from CGI params lc: list[str] = [] rc: list[str] = [] # Caption 1: Time (UTC) lc.append('Time (UTC):') rc.append(time_str) # Caption 2: Ephemeris — prefer raw CGI/legacy display string when present. lc.append('Ephemeris:') if ephem_display is not None and ephem_display.strip(): ephem_caption = _strip_leading_option_code(ephem_display.strip()) else: ephem_caption = EPHEM_DESCRIPTIONS_BY_PLANET.get(planet_num, 'DE440') rc.append(ephem_caption) # Caption 3: Viewpoint lc.append('Viewpoint:') viewpoint_text = ( "Earth's center" if 'earth' in viewpoint.lower() or viewpoint == 'observatory' else viewpoint ) if viewpoint_display: viewpoint_text = viewpoint_display elif ( viewpoint == 'latlon' and observer_latitude is not None and observer_longitude is not None and observer_altitude is not None ): lon_dir = observer_lon_dir.lower() viewpoint_text = ( f'({observer_latitude:.7f}, {observer_longitude:.7f} {lon_dir},' f' {observer_altitude:g})' ) rc.append(viewpoint_text) # Caption 4: Moon selection — prefer raw CGI display text when available. lc.append('Moon selection:') if moon_selection_display: rc.append(_strip_leading_option_code(moon_selection_display)) elif track_moon_ids: rc.append(', '.join(id_to_name.get(mid, str(mid)) for mid in track_moon_ids)) else: rc.append('') # Caption 5: Ring selection — from CLI/CGI rings value lc.append('Ring selection:') rc.append(ring_selection_display if ring_selection_display else '') # Caption 6: Center body (lon,lat) subobs_lon, subobs_lat, _sslon, _sslat = body_lonlat(et, caption_center_body_id) lon_dir = cfg.longitude_direction if hasattr(cfg, 'longitude_direction') else 'W' lon_deg = subobs_lon * _RAD2DEG lat_deg = subobs_lat * _RAD2DEG lc.append(f'{caption_center_body_name} center (lon,lat):') # FORTRAN format: ('(',f7.3,'\260 ',a1,',',f7.3,'\260)') lon_text = _fortran_fixed(lon_deg, 3) lat_text = _fortran_fixed(lat_deg, 3) rc.append(f'({lon_text:>7s}\260 {lon_dir},{lat_text:>7s}\260)') # Caption 7: Phase angle phase_rad = body_phase(et, caption_center_body_id) phase_deg = phase_rad * _RAD2DEG lc.append(f'{caption_center_body_name} phase angle: ') # FORTRAN format: (f7.3,'\260') → e.g. ' 2.920\260' rc.append(f'{phase_deg:7.3f}\260') if planet_num == 5 and torus: lc.append('Io torus:') torus_inc_text = f'{torus_inc:g}' if float(torus_rad).is_integer(): torus_rad_text = str(int(torus_rad)) else: torus_rad_text = f'{torus_rad:g}' rc.append(f'Inclination = {torus_inc_text} deg; Radius = {torus_rad_text} km') ncaptions = len(lc) # Build moon arrays for RSPK_DrawView # FORTRAN passes moon_flags(0:NMOONS) where 0 = planet (always False) all_moons = [m for m in cfg.moons if m.id != cfg.planet_id] f_nmoons = len(all_moons) + 1 # +1 because FORTRAN includes planet as index 0 f_moon_flags = [False] # index 0 = planet, always False f_moon_ids = [cfg.planet_id] f_moon_names = [' '] for m in all_moons: f_moon_flags.append(True) f_moon_ids.append(m.id) f_moon_names.append(m.name) if moon_ids: for i in range(1, len(f_moon_ids)): if f_moon_ids[i] not in moon_ids: f_moon_flags[i] = False # Fallback: if selection filtered out every moon, show all (track_moon_ids # was already expanded earlier so FOV table and caption match). if not any(f_moon_flags[1:]): for i in range(1, len(f_moon_flags)): f_moon_flags[i] = True if moremoons: for i in range(1, len(f_moon_ids)): if getattr(all_moons[i - 1], 'is_irregular', False): f_moon_flags[i] = True # Build ring arrays f_nrings = len(cfg.rings) if hasattr(cfg, 'rings') and cfg.rings else 0 f_ring_flags: list[bool] = [] f_ring_rads: list[float] = [] f_ring_elevs: list[float] = [] f_ring_eccs: list[float] = [] f_ring_incs: list[float] = [] f_ring_peris: list[float] = [] f_ring_nodes: list[float] = [] f_ring_offsets: list[list[float]] = [] f_ring_opaqs: list[bool] = [] f_ring_dashed: list[bool] = [] f_narcs = 0 f_arc_flags: list[bool] = [] f_arc_rings: list[int] = [] f_arc_minlons: list[float] = [] f_arc_maxlons: list[float] = [] if hasattr(cfg, 'rings') and cfg.rings: ring_offset_list = _compute_ring_center_offsets(et, cfg) mars_deimos_node = _compute_mars_deimos_ring_node(et) if planet_num == 4 else None uranus_peri_nodes: tuple[list[float], list[float]] | None = None if planet_num == 7: peri_deg_list, node_deg_list = _propagated_uranus_rings(et, cfg) uranus_peri_nodes = (peri_deg_list, node_deg_list) saturn_f_ring_peri_node: tuple[float, float] | None = None if planet_num == 6: saturn_f_ring_peri_node = _propagated_saturn_f_ring(et, cfg) resolved_flags: list[bool] | None = None if ring_selection: resolved_flags = _resolve_viewer_ring_flags(planet_num, ring_selection, cfg.rings) for i, r in enumerate(cfg.rings): if resolved_flags is not None: flag = resolved_flags[i] if i < len(resolved_flags) else False else: flag = not r.dashed # FORTRAN: dashed rings hidden by default f_ring_flags.append(flag) f_ring_rads.append(r.outer_km) f_ring_elevs.append(r.elev_km) f_ring_eccs.append(r.ecc) f_ring_incs.append(r.inc_rad) if ( planet_num == 7 and uranus_peri_nodes is not None and i < len(uranus_peri_nodes[0]) ): f_ring_peris.append(uranus_peri_nodes[0][i] * _DEG2RAD) elif ( planet_num == 6 and cfg.f_ring_index is not None and i == cfg.f_ring_index and saturn_f_ring_peri_node is not None ): f_ring_peris.append(saturn_f_ring_peri_node[0]) else: f_ring_peris.append(r.peri_rad) if planet_num == 4 and i in (2, 3) and mars_deimos_node is not None: f_ring_nodes.append(mars_deimos_node) elif ( planet_num == 7 and uranus_peri_nodes is not None and i < len(uranus_peri_nodes[1]) ): f_ring_nodes.append(uranus_peri_nodes[1][i] * _DEG2RAD) elif ( planet_num == 6 and cfg.f_ring_index is not None and i == cfg.f_ring_index and saturn_f_ring_peri_node is not None ): f_ring_nodes.append(saturn_f_ring_peri_node[1]) else: f_ring_nodes.append(r.node_rad) f_ring_offsets.append( list(ring_offset_list[i]) if i < len(ring_offset_list) else [0.0, 0.0, 0.0] ) f_ring_opaqs.append(r.opaque) f_ring_dashed.append(r.dashed) if planet_num == 5 and torus and len(f_ring_flags) >= 7: torus_idx = 6 # FORTRAN ring #7 f_ring_flags[torus_idx] = True f_ring_rads[torus_idx] = torus_rad f_ring_incs[torus_idx] = torus_inc * _DEG2RAD torus_node_rad = _compute_jupiter_torus_node(et) if torus_node_rad is not None: f_ring_nodes[torus_idx] = torus_node_rad # Pericenter markers: zero-length arcs at ring pericenter (FORTRAN style). peris_lower = (peris or '').strip().lower() if planet_num == 7 and peris_lower and peris_lower != 'none': # Uranus: FORTRAN arc_rings(1:7) = rings 1,2,3,4,5,10,11 (1-based). # Epsi: only rings 10,11. Else: rings 1,2,3 if selected; 4,5,10,11 always. n_rings = len(f_ring_flags) uranus_arc_ring_indices = (0, 1, 2, 3, 4, 9, 10) # 0-based if peris_lower.startswith('epsi'): for idx in (9, 10): if idx < n_rings and f_ring_flags[idx]: f_arc_flags.append(True) f_arc_rings.append(idx + 1) f_arc_minlons.append(f_ring_peris[idx]) f_arc_maxlons.append(f_ring_peris[idx]) else: for slot, idx in enumerate(uranus_arc_ring_indices): if idx >= n_rings: continue if slot < 3 and not f_ring_flags[idx]: # rings 1,2,3: only if selected continue f_arc_flags.append(True) f_arc_rings.append(idx + 1) f_arc_minlons.append(f_ring_peris[idx]) f_arc_maxlons.append(f_ring_peris[idx]) f_narcs = len(f_arc_flags) elif planet_num == 6 and peris_lower and peris_lower != 'none': # Saturn: single pericenter marker on F ring when selected. n_rings = len(f_ring_flags) if ( cfg.f_ring_index is not None and cfg.f_ring_index < n_rings and f_ring_flags[cfg.f_ring_index] ): f_arc_flags.append(True) f_arc_rings.append(cfg.f_ring_index + 1) f_arc_minlons.append(f_ring_peris[cfg.f_ring_index]) f_arc_maxlons.append(f_ring_peris[cfg.f_ring_index]) f_narcs = len(f_arc_flags) if planet_num == 8 and hasattr(cfg, 'arcs') and cfg.arcs: arc_minmax = _propagated_neptune_arcs(et, cfg, arc_model_index=neptune_arc_model) f_narcs = len(cfg.arcs) for i, arc in enumerate(cfg.arcs): f_arc_flags.append(True) f_arc_rings.append(int(arc.ring_index)) minlon_deg, maxlon_deg = arc_minmax[i] f_arc_minlons.append(minlon_deg * _DEG2RAD) f_arc_maxlons.append(maxlon_deg * _DEG2RAD) star_ras: list[float] = [] star_decs: list[float] = [] star_names: list[str] = [] if show_standard_stars: starlist_candidates = get_starlist_candidate_paths(cfg.starlist_file) for starlist_path in starlist_candidates: if not starlist_path.exists(): continue try: for star in read_stars(starlist_path, max_stars=200): star_ras.append(star.ra) star_decs.append(star.dec) star_names.append(star.name) except OSError: continue break if extra_star_ra_deg is not None and extra_star_dec_deg is not None: star_ras.append(extra_star_ra_deg * _DEG2RAD) star_decs.append(extra_star_dec_deg * _DEG2RAD) star_names.append(extra_star_name or '') for other_name in other_bodies or []: token = other_name.strip() if token == '': continue low = token.lower() try: if low == 'sun': ra, dec = body_radec(et, SUN_ID) elif low in {'anti-sun', 'antisun'}: ra, dec = anti_sun(et, cfg.planet_id) elif low == 'earth': ra, dec = body_radec(et, EARTH_ID) elif low == 'barycenter' and cfg.barycenter_id is not None: ra, dec = body_radec(et, cfg.barycenter_id) else: known_spacecraft_id = spacecraft_name_to_code(token) if known_spacecraft_id is not None: body_id = known_spacecraft_id else: body_id = cspyce.bodn2c(token) ra, dec = body_radec(et, body_id) except Exception: continue star_ras.append(ra) star_decs.append(dec) star_names.append(token) draw_options = DrawPlanetaryViewOptions( obs_time=et, fov=fov_rad, center_ra=center_ra_rad, center_dec=center_dec_rad, planet_name=cfg.planet_name, blank_disks=blank_disks, prime_pts=meridian_points, nmoons=f_nmoons, moon_flags=f_moon_flags, moon_ids=f_moon_ids, moon_names=f_moon_names, moon_labelpts=_label_points_from_selection(labels), moon_diampts=moon_points, nrings=f_nrings, ring_flags=f_ring_flags, ring_rads=f_ring_rads, ring_elevs=f_ring_elevs, ring_eccs=f_ring_eccs, ring_incs=f_ring_incs, ring_peris=f_ring_peris, ring_nodes=f_ring_nodes, ring_offsets=f_ring_offsets, ring_opaqs=f_ring_opaqs, ring_dashed=f_ring_dashed, ring_method=_ring_method_from_opacity(opacity), narcs=f_narcs, arc_flags=f_arc_flags, arc_rings=f_arc_rings, arc_minlons=f_arc_minlons, arc_maxlons=f_arc_maxlons, arc_width=peripts if (planet_num in (6, 7) and f_narcs > 0) else arcpts, nstars=len(star_ras), star_ras=star_ras, star_decs=star_decs, star_names=star_names, star_labels=_label_points_from_selection(labels) > 0.0, title=title, ncaptions=ncaptions, lcaptions=lc, rcaptions=rc, align_loc=DEFAULT_ALIGN_LOC_POINTS, ) draw_planetary_view(output_ps, draw_options)