From a04615fe49d96ff299829273829489cf6f32922b Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Thu, 31 Jul 2025 11:38:59 +0200 Subject: [PATCH 1/6] Update pydeepskylog version to 1.6 in requirements.txt --- python/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/requirements.txt b/python/requirements.txt index 5f92091ab..dc2a4b35c 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -11,7 +11,7 @@ luma.lcd==2.11.0 pillow==10.4.0 numpy==1.26.2 pandas==1.5.3 -pydeepskylog==1.3.2 +pydeepskylog==1.6 pyjwt==2.8.0 python-libinput==0.3.0a0 pytz==2022.7.1 From 1d485114fbce8ce4433d6115bd47dcd7f6cef096 Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Thu, 31 Jul 2025 14:38:59 +0200 Subject: [PATCH 2/6] Calculate contrast reserve based on telescope and eyepiece parameters --- python/PiFinder/ui/object_details.py | 70 +++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/python/PiFinder/ui/object_details.py b/python/PiFinder/ui/object_details.py index 9dba9d4c5..18da7bc09 100644 --- a/python/PiFinder/ui/object_details.py +++ b/python/PiFinder/ui/object_details.py @@ -6,6 +6,8 @@ """ +from pydeepskylog.exceptions import InvalidParameterError + from PiFinder import cat_images from PiFinder.ui.marking_menus import MarkingMenuOption, MarkingMenu from PiFinder.obj_types import OBJ_TYPES @@ -25,6 +27,7 @@ from PiFinder.db.observations_db import ObservationsDatabase import numpy as np import time +import pydeepskylog as pds # Constants for display modes @@ -46,6 +49,7 @@ class UIObjectDetails(UIModule): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.contrast = None self.screen_direction = self.config_object.get_option("screen_direction") self.mount_type = self.config_object.get_option("mount_type") self.object = self.item_definition["object"] @@ -68,7 +72,7 @@ def __init__(self, *args, **kwargs): ), ) - # Used for displaying obsevation counts + # Used for displaying observation counts self.observations_db = ObservationsDatabase() self.simpleTextLayout = functools.partial( @@ -117,8 +121,15 @@ def _layout_designator(self): designator_color = 255 if not self.object.last_filtered_result: designator_color = 128 + + # layout the name - contrast reserve line + space_calculator = SpaceCalculatorFixed(14) + + _, typeconst = space_calculator.calculate_spaces( + self.object.display_name, self.contrast + ) return self.simpleTextLayout( - self.object.display_name, + typeconst, font=self.fonts.large, color=self.colors.get(designator_color), ) @@ -228,6 +239,61 @@ def update_object_info(self): magnification=magnification, ) + # TODO: Get the SQM from the shared state + # sqm = self.shared_state.get_sky_brightness() + sqm = 20.15 + # Check if a telescope and eyepiece are set + if ( + self.config_object.equipment.active_eyepiece is None + or self.config_object.equipment.active_eyepiece is None + ): + self.contrast = "" + else: + # Calculate contrast reserve. The object diameters are given in arc seconds. + magnification = self.config_object.equipment.calc_magnification( + self.config_object.equipment.active_telescope, + self.config_object.equipment.active_eyepiece, + ) + if self.object.mag_str == "-": + self.contrast = "" + else: + try: + if self.object.size: + # Check if the size contains 'x' + if "x" in self.object.size: + diameter1, diameter2 = map( + float, self.object.size.split("x") + ) + diameter1 = ( + diameter1 * 60.0 + ) # Convert arc seconds to arc minutes + diameter2 = diameter2 * 60.0 + elif "'" in self.object.size: + # Convert arc minutes to arc seconds + diameter1 = float(self.object.size.replace("'", "")) * 60.0 + diameter2 = diameter1 + else: + diameter1 = diameter2 = float(self.object.size) * 60.0 + else: + diameter1 = diameter2 = None + + self.contrast = pds.contrast_reserve( + sqm=sqm, + telescope_diameter=self.config_object.equipment.active_telescope.aperture_mm, + magnification=magnification, + surf_brightness=None, + magnitude=float(self.object.mag_str), + object_diameter1=diameter1, + object_diameter2=diameter2, + ) + except InvalidParameterError as e: + print(f"Error calculating contrast reserve: {e}") + self.contrast = "" + if self.contrast is not None and self.contrast != "": + self.contrast = f"{self.contrast: .1f}" + else: + self.contrast = "" + def active(self): self.activation_time = time.time() From e3a4b7924c0d613376ba273654b417a0e50178ca Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Mon, 4 Aug 2025 11:41:36 +0200 Subject: [PATCH 3/6] Implement sky brightness retrieval from shared state --- python/PiFinder/state.py | 9 +++++++++ python/PiFinder/ui/object_details.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/state.py b/python/PiFinder/state.py index 535c8f02f..cf661f58d 100644 --- a/python/PiFinder/state.py +++ b/python/PiFinder/state.py @@ -355,6 +355,15 @@ def ui_state(self): def set_ui_state(self, v): self.__ui_state = v + def get_sky_brightness(self): + """ + Returns the current sky brightness (SQM) value from the shared state. + Replace this stub with actual logic if available. + """ + # Example: If you have a sensor or calculation, use it here + # For now, return a placeholder value + return 20.15 + def __repr__(self): # A simple representation showing key attributes (adjust as needed) return ( diff --git a/python/PiFinder/ui/object_details.py b/python/PiFinder/ui/object_details.py index 18da7bc09..0ce8549f1 100644 --- a/python/PiFinder/ui/object_details.py +++ b/python/PiFinder/ui/object_details.py @@ -239,9 +239,9 @@ def update_object_info(self): magnification=magnification, ) - # TODO: Get the SQM from the shared state - # sqm = self.shared_state.get_sky_brightness() - sqm = 20.15 + # Get the SQM from the shared state + sqm = self.shared_state.get_sky_brightness() + # Check if a telescope and eyepiece are set if ( self.config_object.equipment.active_eyepiece is None From d3eed8ee6ba04ce0eacb563dd38f9ee098151341 Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Mon, 4 Aug 2025 11:48:36 +0200 Subject: [PATCH 4/6] Add SQM class to represent sky brightness and integrate with shared state --- python/PiFinder/state.py | 55 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/python/PiFinder/state.py b/python/PiFinder/state.py index cf661f58d..80b190d40 100644 --- a/python/PiFinder/state.py +++ b/python/PiFinder/state.py @@ -200,6 +200,48 @@ def from_json(cls, json_str): return cls.from_dict(data) +@dataclass +class SQM: + """ + Represents the sky brightness (SQM) value and its source. + """ + + value: float = 20.15 # Standard value set to 20.15 + source: str = "None" + last_update: Optional[str] = None + + def __str__(self): + return ( + f"SQM(value={self.value:.2f}, " + f"source={self.source}, " + f"last_update={self.last_update})" + ) + + def reset(self): + self.value = 0.0 + self.source = "None" + self.last_update = None + + def to_dict(self): + """Convert the SQM object to a dictionary.""" + return asdict(self) + + def to_json(self): + """Convert the SQM object to a JSON string.""" + return json.dumps(self.to_dict()) + + @classmethod + def from_dict(cls, data): + """Create a SQM object from a dictionary.""" + return cls(**data) + + @classmethod + def from_json(cls, json_str): + """Create a SQM object from a JSON string.""" + data = json.loads(json_str) + return cls.from_dict(data) + + class SharedStateObj: def __init__(self): self.__power_state = 1 @@ -215,6 +257,7 @@ def __init__(self): self.__sats = None self.__imu = None self.__location: Location = Location() + self.__sqm: SQM = SQM() self.__datetime = None self.__datetime_time = None self.__screen = None @@ -298,6 +341,13 @@ def set_location(self, v): v.timezone = self.__tz_finder.timezone_at(lat=v.lat, lng=v.lon) self.__location = v + def sqm(self): + """Return the current SQM object""" + return self.__sqm + + def set_sqm(self, sqm: SQM): + self.__sqm = sqm + def last_image_metadata(self): return self.__last_image_metadata @@ -358,11 +408,8 @@ def set_ui_state(self, v): def get_sky_brightness(self): """ Returns the current sky brightness (SQM) value from the shared state. - Replace this stub with actual logic if available. """ - # Example: If you have a sensor or calculation, use it here - # For now, return a placeholder value - return 20.15 + return self.__sqm.value def __repr__(self): # A simple representation showing key attributes (adjust as needed) From 32392607221dc4e1eedff8b1bf75c78346491450 Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Mon, 4 Aug 2025 14:45:34 +0200 Subject: [PATCH 5/6] Add SQM entry module and integrate into menu for manual SQM value setting --- python/PiFinder/ui/menu_manager.py | 20 ++- python/PiFinder/ui/menu_structure.py | 3 + python/PiFinder/ui/sqm.py | 67 +++++++++ python/PiFinder/ui/sqmentry.py | 203 +++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 python/PiFinder/ui/sqm.py create mode 100644 python/PiFinder/ui/sqmentry.py diff --git a/python/PiFinder/ui/menu_manager.py b/python/PiFinder/ui/menu_manager.py index f0e1e8e00..05f703c8c 100644 --- a/python/PiFinder/ui/menu_manager.py +++ b/python/PiFinder/ui/menu_manager.py @@ -5,6 +5,7 @@ from PiFinder import utils from PiFinder.ui.base import UIModule from PiFinder.ui import menu_structure +from PiFinder.ui.sqmentry import UISqmEntry from PiFinder.ui.object_details import UIObjectDetails from PiFinder.displays import DisplayBase from PiFinder.ui.text_menu import UITextMenu @@ -13,6 +14,7 @@ MarkingMenuOption, render_marking_menu, ) +from PiFinder.ui.textentry import UITextEntry def collect_preloads() -> list[dict]: @@ -104,6 +106,19 @@ def dyn_menu_equipment(cfg): equipment_menu_item["items"] = [telescope_menu, eyepiece_menu] +def dyn_menu_sqm(shared_state): + """ + Adds a submenu to the SQM page to manually set the SQM value + """ + sqm_menu_item = find_menu_by_label("sqm") + sqm_menu = { + "name": _("SQM Value"), + "class": UISqmEntry, + "label": "set_sqm", + } + sqm_menu_item["items"] = [sqm_menu] + + class MenuManager: def __init__( self, @@ -142,6 +157,7 @@ def __init__( self.ss_count = 0 dyn_menu_equipment(self.config_object) + dyn_menu_sqm(shared_state) self.preload_modules() def screengrab(self): @@ -326,12 +342,12 @@ def update_screen(self, screen_image: Image.Image) -> None: Put an image on the display """ screen_to_display = screen_image.convert(self.display_class.device.mode) - + if time.time() < self.ui_state.message_timeout(): return None self.display_class.device.display(screen_to_display) - + # Only update shared state when not in message timeout if self.shared_state: self.shared_state.set_screen(screen_to_display) diff --git a/python/PiFinder/ui/menu_structure.py b/python/PiFinder/ui/menu_structure.py index af6ea0189..d4c9f1496 100644 --- a/python/PiFinder/ui/menu_structure.py +++ b/python/PiFinder/ui/menu_structure.py @@ -1,11 +1,13 @@ from typing import Any from PiFinder.ui.timeentry import UITimeEntry +from PiFinder.ui.sqmentry import UISqmEntry from PiFinder.ui.text_menu import UITextMenu from PiFinder.ui.object_list import UIObjectList from PiFinder.ui.status import UIStatus from PiFinder.ui.console import UIConsole from PiFinder.ui.software import UISoftware from PiFinder.ui.gpsstatus import UIGPSStatus +from PiFinder.ui.sqm import UIsqm from PiFinder.ui.chart import UIChart from PiFinder.ui.align import UIAlign from PiFinder.ui.textentry import UITextEntry @@ -964,6 +966,7 @@ def _(key: str) -> Any: "items": [ {"name": _("Status"), "class": UIStatus}, {"name": _("Equipment"), "class": UIEquipment, "label": "equipment"}, + {"name": _("SQM"), "class": UIsqm, "label": "sqm"}, { "name": _("Place & Time"), "class": UITextMenu, diff --git a/python/PiFinder/ui/sqm.py b/python/PiFinder/ui/sqm.py new file mode 100644 index 000000000..fc9d65779 --- /dev/null +++ b/python/PiFinder/ui/sqm.py @@ -0,0 +1,67 @@ +from PiFinder.ui.base import UIModule +from PiFinder.state_utils import sleep_for_framerate +from PiFinder.ui.marking_menus import MarkingMenu, MarkingMenuOption +from PiFinder.ui.textentry import UITextEntry +from PiFinder import config + +class UIsqm(UIModule): + """ + Displays current SQM value and provides entry to manually set SQM value + """ + __title__ = "SQM" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.menu_index = 0 + self.marking_menu = MarkingMenu( + left=MarkingMenuOption(), + right=MarkingMenuOption(), + down=MarkingMenuOption(), + ) + + def update(self, force=False): + sleep_for_framerate(self.shared_state) + self.clear_screen() + sqm_value = self.shared_state.get_sky_brightness() + self.draw.text((10, 20), f"Current SQM: {sqm_value}", font=self.fonts.large.font, fill=self.colors.get(128)) + if sqm_value is not None: + self.draw.text((10, 40), f" {sqm_value}", font=self.fonts.large.font, fill=self.colors.get(128)) + else: + self.draw.text((10, 40), "No SQM value set", font=self.fonts.small.font, fill=self.colors.get(128)) + + self.draw.text( + (10, 80), + _("Manually..."), + font=self.fonts.large.font, + fill=self.colors.get(192), + ) + if self.menu_index == 0: + self.draw_menu_pointer(80) + + def key_down(self): + self.menu_index += 1 + if self.menu_index > 1: + self.menu_index = 1 + + def key_up(self): + self.menu_index -= 1 + if self.menu_index < 0: + self.menu_index = 0 + + def key_right(self): + if self.menu_index == 0: + self.jump_to_label("set_sqm") + + def draw_menu_pointer(self, horiz_position: int): + self.draw.text( + (2, horiz_position), + self._RIGHT_ARROW, + font=self.fonts.large.font, + fill=self.colors.get(255), + ) + + def active(self): + """ + Called when a module becomes active + i.e. foreground controlling display + """ diff --git a/python/PiFinder/ui/sqmentry.py b/python/PiFinder/ui/sqmentry.py new file mode 100644 index 000000000..a34a662fc --- /dev/null +++ b/python/PiFinder/ui/sqmentry.py @@ -0,0 +1,203 @@ +from PIL import Image, ImageDraw + +from PiFinder.state import SQM +from PiFinder.ui.base import UIModule +import math + +class UISqmEntry(UIModule): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Initialize two empty boxes + self.boxes = ["", ""] + self.current_box = 0 # Start with decimals + if self.shared_state: + # Get the current sky brightness from shared state + decimal_part, integer_part = math.modf(self.shared_state.get_sky_brightness()) + # Convert to string with 2 decimal places + decimal_part = f"{int(decimal_part * 100):02d}" # Convert + integer_part = f"{int(integer_part):02d}" # Convert to string with leading zeros + else: + # Default to 0.00 if no shared state is available + decimal_part, integer_part = "", "" + self.placeholders = [ + integer_part, + decimal_part, + ] + + # Screen setup + self.width = 128 + self.height = 128 + self.red = self.colors.get(255) + self.black = self.colors.get(0) + self.half_red = self.colors.get(128) + self.screen = Image.new("RGB", (self.width, self.height), "black") + self.draw = ImageDraw.Draw(self.screen) + self.bold = self.fonts.bold + + # Layout constants - updated to center the boxes + self.text_y = 25 + self.box_width = 25 + self.box_height = 20 + self.box_spacing = 15 + + # Calculate start_x to center the boxes on screen + total_width = (2 * self.box_width) + self.box_spacing + self.start_x = (self.width - total_width) // 2 + + def draw_boxes(self): + # Draw the two boxes with a decimal point between them + for i in range(2): + x = self.start_x + i * (self.box_width + self.box_spacing) + + # Draw box outline - highlight current box with a brighter outline + outline_color = self.red if i == self.current_box else self.half_red + outline_width = 2 if i == self.current_box else 1 + + self.draw.rectangle( + [x, self.text_y, x + self.box_width, self.text_y + self.box_height], + outline=outline_color, + width=outline_width, + ) + + # Draw text or placeholder + text = self.boxes[i] + if not text and i != self.current_box: + # Show placeholder if box is empty and not selected + text = self.placeholders[i] + color = self.colors.get(180) # Brighter color for placeholder + else: + color = self.red + + # Center text in box + text_width = self.bold.font.getbbox(text if text else "00")[2] + text_x = x + (self.box_width - text_width) // 2 + text_y = self.text_y + 2 + + self.draw.text((text_x, text_y), text, font=self.bold.font, fill=color) + + # Draw colon after first two boxes + if i < 1: + colon_x = x + self.box_width + self.box_spacing // 2 - 2 + self.draw.text( + (colon_x, self.text_y + 2), ".", font=self.bold.font, fill=self.red + ) + + # Draw cursor in current box if empty + if not self.boxes[self.current_box]: + x = self.start_x + self.current_box * (self.box_width + self.box_spacing) + cursor_x = x + 2 + self.draw.rectangle( + [ + cursor_x, + self.text_y + 2, + cursor_x + 8, + self.text_y + self.box_height - 2, + ], + fill=self.red, + ) + + def draw_separator(self, start_y): + # Draw a separator line before the legend + self.draw.line( + [(10, start_y), (self.width - 10, start_y)], fill=self.half_red, width=1 + ) + return start_y + 5 # Return the Y position after the separator + + def draw_legend(self, start_y): + legend_y = start_y + # Still using full red for better visibility but smaller font + legend_color = self.red + + self.draw.text( + (10, legend_y), + _(" Next box"), # Right + font=self.fonts.base.font, # Using base font + fill=legend_color, + ) + legend_y += 12 # Standard spacing + self.draw.text( + (10, legend_y), + _(" Done"), # Left + font=self.fonts.base.font, + fill=legend_color, + ) + legend_y += 12 # Standard spacing + self.draw.text( + (10, legend_y), + _("󰍴 Delete/Previous"), # minus + font=self.fonts.base.font, + fill=legend_color, + ) + + def validate_box(self, box_index, value): + """Validate the entered value for the given box""" + if not value: + return True + try: + num = int(value) + if box_index == 0: + return 14 <= num <= 22 + else: + return 0 <= num <= 99 + except ValueError: + return False + + def key_number(self, number): + current = self.boxes[self.current_box] + new_value = current + str(number) + + # Only allow 2 digits per box + if len(new_value) > 2: + return + + # Validate the new value + self.boxes[self.current_box] = new_value + # Auto-advance to next box if we have 2 digits + if len(new_value) == 2: + self.current_box = (self.current_box + 1) % 2 + + def key_minus(self): + """Delete last digit in current box or move to previous box if empty""" + if self.boxes[self.current_box]: + # Delete the last digit + self.boxes[self.current_box] = self.boxes[self.current_box][:-1] + else: + # If current box is empty, move to previous box + self.current_box = (self.current_box - 1) % 2 + + def key_right(self): + """Move to next box""" + self.current_box = (self.current_box + 1) % 2 + return False + + def inactive(self): + """Called when the module is no longer the active module""" + if not self.validate_box(0, self.boxes[0]) or not self.validate_box(1, self.boxes[1]): + # If any box has invalid value, do nothing + return + + if not self.boxes[0] or not self.boxes[1]: + # If both boxes are empty, do nothing + return + + # Create the SQM value from the boxes + sqm = float(self.boxes[1]) / 100.0 + float(self.boxes[0]) + + # Put the sqm value in a SQM object + sqm = SQM(sqm, "Manual") + self.shared_state.set_sqm(sqm) + + def update(self, force=False): + self.draw.rectangle((0, 0, 128, 128), fill=self.black) + + self.draw_boxes() + + # Draw additional elements with proper positioning + separator_y = self.draw_separator(65 + 15) + self.draw_legend(separator_y) + self.draw_legend(separator_y) + + if self.shared_state: + self.shared_state.set_screen(self.screen) + return self.screen_update() From a88770f2e63096a36a2248a61aa85c36743630f5 Mon Sep 17 00:00:00 2001 From: Wim De Meester Date: Mon, 4 Aug 2025 15:32:08 +0200 Subject: [PATCH 6/6] Add contrast reserve interpretation to object details display --- python/PiFinder/ui/object_details.py | 94 ++++++++++++++++++---------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/python/PiFinder/ui/object_details.py b/python/PiFinder/ui/object_details.py index 0ce8549f1..2e9a2a9e4 100644 --- a/python/PiFinder/ui/object_details.py +++ b/python/PiFinder/ui/object_details.py @@ -208,37 +208,6 @@ def update_object_info(self): scrollspeed=self._get_scrollspeed_config(), ) - # NGC description.... - logs = self.observations_db.get_logs_for_object(self.object) - desc = "" - if self.object.description: - desc = ( - self.object.description.replace("\t", " ") + "\n" - ) # I18N: Descriptions are not translated - if len(logs) == 0: - desc = desc + _("  Not Logged") - else: - desc = desc + _("  {logs} Logs").format(logs=len(logs)) - - self.descTextLayout.set_text(desc) - self.texts["desc"] = self.descTextLayout - - solution = self.shared_state.solution() - roll = 0 - if solution: - roll = solution["Roll"] - - magnification = self.config_object.equipment.calc_magnification() - self.object_image = cat_images.get_display_image( - self.object, - str(self.config_object.equipment.active_eyepiece), - self.config_object.equipment.calc_tfov(), - roll, - self.display_class, - burn_in=self.object_display_mode in [DM_POSS, DM_SDSS], - magnification=magnification, - ) - # Get the SQM from the shared state sqm = self.shared_state.get_sky_brightness() @@ -294,6 +263,59 @@ def update_object_info(self): else: self.contrast = "" + # Add contrast reserve line to details with interpretation + if self.contrast: + contrast_val = float(self.contrast) + if contrast_val < -0.2: + contrast_str = f"Object is not visible" + elif -0.2 <= contrast_val < 0.1: + contrast_str = f"Questionable detection" + elif 0.1 <= contrast_val < 0.35: + contrast_str = f"Difficult to see" + elif 0.35 <= contrast_val < 0.5: + contrast_str = f"Quite difficult to see" + elif 0.5 <= contrast_val < 1.0: + contrast_str = f"Easy to see" + elif contrast_val >= 1.0: + contrast_str = f"Very easy to see" + else: + contrast_str = f"" + self.texts["contrast_reserve"] = self.ScrollTextLayout( + contrast_str, font=self.fonts.base, color=self.colors.get(255), scrollspeed=self._get_scrollspeed_config(), + ) + + # NGC description.... + logs = self.observations_db.get_logs_for_object(self.object) + desc = "" + if self.object.description: + desc = ( + self.object.description.replace("\t", " ") + "\n" + ) # I18N: Descriptions are not translated + if len(logs) == 0: + desc = desc + _("  Not Logged") + else: + desc = desc + _("  {logs} Logs").format(logs=len(logs)) + + self.descTextLayout.set_text(desc) + self.texts["desc"] = self.descTextLayout + + solution = self.shared_state.solution() + roll = 0 + if solution: + roll = solution["Roll"] + + magnification = self.config_object.equipment.calc_magnification() + self.object_image = cat_images.get_display_image( + self.object, + str(self.config_object.equipment.active_eyepiece), + self.config_object.equipment.calc_tfov(), + roll, + self.display_class, + burn_in=self.object_display_mode in [DM_POSS, DM_SDSS], + magnification=magnification, + ) + + def active(self): self.activation_time = time.time() @@ -434,7 +456,7 @@ def update(self, force=True): if self.object_display_mode == DM_DESC or self.object_display_mode == DM_LOCATE: # catalog and entry field i.e. NGC-311 self.refresh_designator() - desc_available_lines = 4 + desc_available_lines = 3 desig = self.texts["designator"] desig.draw((0, 20)) @@ -475,6 +497,14 @@ def update(self, force=True): else: desc_available_lines += 1 # extra lines for description + contrast = self.texts.get("contrast_reserve") + + if contrast and contrast.text.strip(): + contrast.draw((0, posy)) + posy += 11 + else: + desc_available_lines +=1 + # Remaining lines with object description desc = self.texts.get("desc") if desc: