class LocalStorageAcquirer(ImageSequenceAcquirer): IS_REPLICATED = True def __init__(self) -> None: super().__init__() self.bn_last_loaded_paths = BoxBindable( tuple()) # type: BoxBindable[Sequence[Path]] def load_image_paths(self, image_paths: Sequence[Union[Path, str]]) -> None: # Sort image paths in lexicographic order, and ignore paths to directories. image_paths = sorted( [p for p in map(Path, image_paths) if not p.is_dir()]) images = [] # type: MutableSequence[np.ndarray] for image_path in image_paths: image = cv2.imread(str(image_path)) if image is None: raise ValueError( "Failed to load image from path '{}'".format(image_path)) # OpenCV loads images in BGR mode, but the rest of the app works with images in RGB, so convert the read # image appropriately. image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) images.append(image) self.bn_images.set(images) self.bn_last_loaded_paths.set(tuple(image_paths))
class ErrorsPresenter(Generic[ErrorType]): def __init__(self, errors_in: Bindable[Set[ErrorType]], errors_out: Bindable[Set[ErrorType]]) -> None: self._errors_in = errors_in self.__destroyed = False self.__cleanup_tasks = [] self._is_showing_errors = BoxBindable(False) self.__cleanup_tasks.append(lambda: self._is_showing_errors.set(False)) data_bindings = [ errors_out.bind_from( bn_apply(lambda x, y: x if y else set(), self._errors_in, self._is_showing_errors)) ] self.__cleanup_tasks.extend(db.unbind for db in data_bindings) def show_errors(self) -> None: self._is_showing_errors.set(bool(self._errors_in.get())) def destroy(self) -> None: assert not self.__destroyed for f in self.__cleanup_tasks: f() self.__destroyed = True
class USBCamera(Camera): _PRECAPTURE = 5 _CAPTURE_TIMEOUT = 0.5 def __init__(self, camera_index: int) -> None: self._vc = cv2.VideoCapture(camera_index) self.bn_alive = BoxBindable(True) if not self.check_vc_works(timeout=5): raise ValueError('Camera failed to open.') # For some reason, on some cameras, the first few images captured will be dark. Consume those images now so the # camera will be "fully operational" after initialisation. for i in range(self._PRECAPTURE): self._vc.read() def check_vc_works(self, timeout: float) -> bool: start_time = time.time() while self._vc.isOpened() and (time.time() - start_time) < timeout: success = self._vc.grab() if success: # Camera still works return True else: return False def capture(self) -> np.ndarray: start_time = time.time() while self._vc.isOpened() and (time.time() - start_time) < self._CAPTURE_TIMEOUT: success, image = self._vc.read() if success: return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) self.release() raise CameraCaptureError def get_image_size_hint(self) -> Optional[Tuple[int, int]]: width = self._vc.get(cv2.CAP_PROP_FRAME_WIDTH) height = self._vc.get(cv2.CAP_PROP_FRAME_HEIGHT) return width, height def release_if_not_working(self, timeout=_CAPTURE_TIMEOUT) -> None: if not self.check_vc_works(timeout): self.release() def release(self) -> None: self._vc.release() self.bn_alive.set(False)
class WizardController: def __init__(self, pages: Iterable) -> None: self._pages = tuple(pages) self.bn_current_page = BoxBindable(self._pages[0]) def next_page(self) -> None: current_page = self.bn_current_page.get() next_page_idx = self._pages.index(current_page) + 1 next_page = self._pages[next_page_idx] self.bn_current_page.set(next_page) def prev_page(self) -> None: current_page = self.bn_current_page.get() prev_page_idx = self._pages.index(current_page) - 1 prev_page = self._pages[prev_page_idx] self.bn_current_page.set(prev_page)
class AppRootModel: def __init__(self, *, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self.bn_mode = BoxBindable(AppMode.MAIN_MENU) self.main_menu = MainMenuModel( do_launch_ift=( lambda: self.bn_mode.set(AppMode.IFT) ), do_launch_conan=( lambda: self.bn_mode.set(AppMode.CONAN) ), do_exit=( lambda: self.bn_mode.set(AppMode.QUIT) ), ) def new_ift_session(self) -> IFTSession: session = IFTSession( do_exit=( lambda: self.bn_mode.set(AppMode.MAIN_MENU) ), loop=self._loop, ) return session def new_conan_session(self) -> ConanSession: session = ConanSession( do_exit=( lambda: self.bn_mode.set(AppMode.MAIN_MENU) ), loop=self._loop, ) return session
class WizardModel: def __init__(self, pages: Iterable[Any]) -> None: self._pages = tuple(pages) self._interpage_actions = {} self.bn_current_page = BoxBindable(self._pages[0]) def next_page(self) -> None: current_page = self.bn_current_page.get() next_page_idx = self._pages.index(current_page) + 1 next_page = self._pages[next_page_idx] self.perform_interpage_action(current_page, next_page) self.bn_current_page.set(next_page) def prev_page(self) -> None: current_page = self.bn_current_page.get() prev_page_idx = self._pages.index(current_page) - 1 prev_page = self._pages[prev_page_idx] self.perform_interpage_action(current_page, prev_page) self.bn_current_page.set(prev_page) def perform_interpage_action(self, start_page: Any, end_page: Any) -> None: if (start_page, end_page) not in self._interpage_actions: return callback = self._interpage_actions[(start_page, end_page)] callback() def register_interpage_action(self, start_page: Any, end_page: Any, callback: Callable[[], Any]) -> None: self._interpage_actions[(start_page, end_page)] = callback
class ConanSession: def __init__(self, do_exit: Callable[[], Any], *, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self._do_exit = do_exit self._feature_extractor_params = FeatureExtractorParams() self._conancalc_params = ContactAngleCalculatorParams() self._bn_analyses = BoxBindable( tuple()) # type: Bindable[Sequence[ConanAnalysis]] self._analyses_saved = False self.image_acquisition = ImageAcquisitionModel() self.image_acquisition.use_acquirer_type(AcquirerType.LOCAL_STORAGE) self.image_processing = ConanImageProcessingModel( image_acquisition=self.image_acquisition, feature_extractor_params=self._feature_extractor_params, conancalc_params=self._conancalc_params, do_extract_features=self.extract_features, ) self.results = ConanResultsModel( in_analyses=self._bn_analyses, do_cancel_analyses=self.cancel_analyses, do_save_analyses=self.save_analyses, create_save_options=self._create_save_options, check_if_safe_to_discard=self.check_if_safe_to_discard_analyses, ) def start_analyses(self) -> None: assert len(self._bn_analyses.get()) == 0 new_analyses = [] input_images = self.image_acquisition.acquire_images() for input_image in input_images: new_analysis = ConanAnalysis( input_image=input_image, do_extract_features=self.extract_features, do_calculate_conan=self.calculate_contact_angle, ) new_analyses.append(new_analysis) self._bn_analyses.set(new_analyses) self._analyses_saved = False def cancel_analyses(self) -> None: analyses = self._bn_analyses.get() for analysis in analyses: analysis.cancel() def clear_analyses(self) -> None: self.cancel_analyses() self._bn_analyses.set(tuple()) def save_analyses(self, options: ConanAnalysisSaverOptions) -> None: analyses = self._bn_analyses.get() if len(analyses) == 0: return save_drops(analyses, options) self._analyses_saved = True def _create_save_options(self) -> ConanAnalysisSaverOptions: return ConanAnalysisSaverOptions() def check_if_safe_to_discard_analyses(self) -> bool: if self._analyses_saved: return True else: analyses = self._bn_analyses.get() if len(analyses) == 0: return True all_images_replicated = all(analysis.is_image_replicated for analysis in analyses) if not all_images_replicated: return False return True def extract_features(self, image: Bindable[np.ndarray]) -> FeatureExtractor: return FeatureExtractor( image=image, params=self._feature_extractor_params, loop=self._loop, ) def calculate_contact_angle( self, extracted_features: FeatureExtractor) -> ContactAngleCalculator: return ContactAngleCalculator( features=extracted_features, params=self._conancalc_params, ) def exit(self) -> None: self.clear_analyses() self.image_acquisition.destroy() self._do_exit()
class ContactAngleCalculator: def __init__(self, features: FeatureExtractor, params: ContactAngleCalculatorParams) -> None: self._features = features self.params = params self.bn_left_tangent = BoxBindable(np.poly1d((math.nan, math.nan))) self.bn_left_angle = BoxBindable(math.nan) self.bn_left_point = BoxBindable(Vector2(math.nan, math.nan)) self.bn_right_tangent = BoxBindable(np.poly1d((math.nan, math.nan))) self.bn_right_angle = BoxBindable(math.nan) self.bn_right_point = BoxBindable(Vector2(math.nan, math.nan)) # Recalculate when inputs change features.bn_drop_profile_px.on_changed.connect(self._recalculate) params.bn_surface_line_px.on_changed.connect(self._recalculate) self._recalculate() def _recalculate(self) -> None: features = self._features params = self.params drop_profile = features.bn_drop_profile_px.get() if drop_profile is None: return surface = params.bn_surface_line_px.get() if surface is None: return drop_profile = drop_profile.copy() surface_poly1d = np.poly1d((surface.gradient, surface.eval_at(x=0).y)) # ContactAngle expects the coordinates of drop profile to be such that the surface has a lower y-coordinate than # the drop, so mirror the drop in y-direction. (Remember drop profile is in 'image coordinates', where # increasing y-coordinate is 'downwards') drop_profile[:, 1] *= -1 surface_poly1d = -surface_poly1d conancalc = ContactAngle(drop_profile, surface_poly1d) # Mirror the tangents and contact points back to original coordinate system as well. left_tangent = -conancalc.left_tangent left_point = Vector2(x=conancalc.left_point.x, y=-conancalc.left_point.y) right_tangent = -conancalc.right_tangent right_point = Vector2(x=conancalc.right_point.x, y=-conancalc.right_point.y) self.bn_left_tangent.set(left_tangent) self.bn_left_angle.set(conancalc.left_angle) self.bn_left_point.set(left_point) self.bn_right_tangent.set(right_tangent) self.bn_right_angle.set(conancalc.right_angle) self.bn_right_point.set(right_point)
class GraphsModel: def __init__(self, in_analyses: Bindable[Sequence[IFTDropAnalysis]]) -> None: self._bn_analyses = in_analyses self._tracked_analyses = [] self._tracked_analysis_unbind_tasks = {} self.bn_ift_data = BoxBindable((tuple(), tuple())) self.bn_volume_data = BoxBindable((tuple(), tuple())) self.bn_surface_area_data = BoxBindable((tuple(), tuple())) self._bn_analyses.on_changed.connect(self._hdl_analyses_changed) self._hdl_analyses_changed() def _hdl_analyses_changed(self) -> None: analyses = self._bn_analyses.get() tracked_analyses = self._tracked_analyses to_track = set(analyses) - set(tracked_analyses) for analysis in to_track: self._track_analysis(analysis) to_untrack = set(tracked_analyses) - set(analyses) for analysis in to_untrack: self._untrack_analysis(analysis) def _track_analysis(self, analysis: IFTDropAnalysis) -> None: unbind_tasks = [] event_connections = [ analysis.bn_interfacial_tension.on_changed.connect( self._hdl_tracked_analysis_data_changed), analysis.bn_volume.on_changed.connect( self._hdl_tracked_analysis_data_changed), analysis.bn_surface_area.on_changed.connect( self._hdl_tracked_analysis_data_changed), ] unbind_tasks.extend(ec.disconnect for ec in event_connections) self._tracked_analyses.append(analysis) self._tracked_analysis_unbind_tasks[analysis] = unbind_tasks self._hdl_tracked_analysis_data_changed() def _untrack_analysis(self, analysis: IFTDropAnalysis) -> None: unbind_tasks = self._tracked_analysis_unbind_tasks[analysis] for task in unbind_tasks: task() del self._tracked_analysis_unbind_tasks[analysis] self._tracked_analyses.remove(analysis) def _hdl_tracked_analysis_data_changed(self) -> None: ift_data = [] vol_data = [] sur_data = [] analyses = self._bn_analyses.get() for analysis in analyses: timestamp = analysis.bn_image_timestamp.get() if timestamp is None or not math.isfinite(timestamp): continue ift_value = analysis.bn_interfacial_tension.get() vol_value = analysis.bn_volume.get() sur_value = analysis.bn_surface_area.get() if ift_value is not None and math.isfinite(ift_value): ift_data.append((timestamp, ift_value)) if vol_value is not None and math.isfinite(vol_value): vol_data.append((timestamp, vol_value)) if sur_value is not None and math.isfinite(sur_value): sur_data.append((timestamp, sur_value)) # Sort in ascending order of timestamp ift_data = sorted(ift_data, key=lambda x: x[0]) vol_data = sorted(vol_data, key=lambda x: x[0]) sur_data = sorted(sur_data, key=lambda x: x[0]) ift_data = tuple(zip(*ift_data)) vol_data = tuple(zip(*vol_data)) sur_data = tuple(zip(*sur_data)) if len(ift_data) == 0: ift_data = (tuple(), tuple()) if len(vol_data) == 0: vol_data = (tuple(), tuple()) if len(sur_data) == 0: sur_data = (tuple(), tuple()) self.bn_ift_data.set(ift_data) self.bn_volume_data.set(vol_data) self.bn_surface_area_data.set(sur_data)
class IFTSession: def __init__(self, do_exit: Callable[[], Any], *, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self._do_exit = do_exit self._feature_extractor_params = FeatureExtractorParams() self._physprops_calculator_params = PhysicalPropertiesCalculatorParams( ) self._bn_analyses = BoxBindable( tuple()) # type: Bindable[Sequence[IFTDropAnalysis]] self._analyses_saved = False self.image_acquisition = ImageAcquisitionModel() self.image_acquisition.use_acquirer_type(AcquirerType.LOCAL_STORAGE) self.physical_parameters = PhysicalParametersModel( physprops_calculator_params=self._physprops_calculator_params, ) self.image_processing = IFTImageProcessingModel( image_acquisition=self.image_acquisition, feature_extractor_params=self._feature_extractor_params, do_extract_features=self.extract_features, ) self.results = IFTResultsModel( in_analyses=self._bn_analyses, do_cancel_analyses=self.cancel_analyses, do_save_analyses=self.save_analyses, create_save_options=self._create_save_options, check_if_safe_to_discard=self.check_if_safe_to_discard_analyses, ) def start_analyses(self) -> None: assert len(self._bn_analyses.get()) == 0 new_analyses = [] input_images = self.image_acquisition.acquire_images() for input_image in input_images: new_analysis = IFTDropAnalysis( input_image=input_image, do_extract_features=self.extract_features, do_young_laplace_fit=self.young_laplace_fit, do_calculate_physprops=self.calculate_physprops) new_analyses.append(new_analysis) self._bn_analyses.set(new_analyses) self._analyses_saved = False def cancel_analyses(self) -> None: analyses = self._bn_analyses.get() for analysis in analyses: analysis.cancel() def clear_analyses(self) -> None: self.cancel_analyses() self._bn_analyses.set(tuple()) def save_analyses(self, options: IFTAnalysisSaverOptions) -> None: analyses = self._bn_analyses.get() if len(analyses) == 0: return save_drops(analyses, options) self._analyses_saved = True def _create_save_options(self) -> IFTAnalysisSaverOptions: return IFTAnalysisSaverOptions() def check_if_safe_to_discard_analyses(self) -> bool: if self._analyses_saved: return True else: analyses = self._bn_analyses.get() if len(analyses) == 0: return True all_images_replicated = all(analysis.is_image_replicated for analysis in analyses) if not all_images_replicated: return False return True def extract_features(self, image: Bindable[np.ndarray]) -> FeatureExtractor: return FeatureExtractor( image=image, params=self._feature_extractor_params, loop=self._loop, ) def young_laplace_fit( self, extracted_features: FeatureExtractor) -> YoungLaplaceFitter: return YoungLaplaceFitter(features=extracted_features, loop=self._loop) def calculate_physprops( self, extracted_features: FeatureExtractor, young_laplace_fit: YoungLaplaceFitter ) -> PhysicalPropertiesCalculator: return PhysicalPropertiesCalculator( features=extracted_features, young_laplace_fit=young_laplace_fit, params=self._physprops_calculator_params, ) def exit(self) -> None: self.clear_analyses() self.image_acquisition.destroy() self._do_exit()
class DetailPresenter(Presenter['DetailView']): def _do_init(self, in_analysis: Bindable[Optional[IFTDropAnalysis]]) -> None: self._bn_analysis = in_analysis self._analysis_unbind_tasks = [] self.bn_interfacial_tension = BoxBindable(math.nan) self.bn_volume = BoxBindable(math.nan) self.bn_surface_area = BoxBindable(math.nan) self.bn_worthington = BoxBindable(math.nan) self.bn_bond_number = BoxBindable(math.nan) self.bn_apex_coords = BoxBindable((math.nan, math.nan)) self.bn_image_angle = BoxBindable(math.nan) self.bn_drop_image = BoxBindable(None) self.bn_drop_profile_extract = BoxBindable(None) self.bn_drop_profile_fit = BoxBindable(None) self.bn_residuals = BoxBindable(None) self.bn_log_text = BoxBindable('') self.__event_connections = [] def view_ready(self) -> None: self.__event_connections.extend( [self._bn_analysis.on_changed.connect(self._hdl_analysis_changed)]) self._hdl_analysis_changed() def _hdl_analysis_changed(self) -> None: self._unbind_analysis() analysis = self._bn_analysis.get() if analysis is None: self.view.show_waiting_placeholder() return self.view.hide_waiting_placeholder() self._bind_analysis(analysis) def _bind_analysis(self, analysis: IFTDropAnalysis) -> None: assert len(self._analysis_unbind_tasks) == 0 data_bindings = [ analysis.bn_interfacial_tension.bind(self.bn_interfacial_tension), analysis.bn_volume.bind(self.bn_volume), analysis.bn_surface_area.bind(self.bn_surface_area), analysis.bn_worthington.bind(self.bn_worthington), analysis.bn_bond_number.bind(self.bn_bond_number), analysis.bn_apex_coords_px.bind(self.bn_apex_coords), analysis.bn_rotation.bind(self.bn_image_angle), analysis.bn_residuals.bind(self.bn_residuals), analysis.bn_log.bind(self.bn_log_text), ] self._analysis_unbind_tasks.extend(db.unbind for db in data_bindings) event_connections = [ analysis.bn_image.on_changed.connect( self._hdl_analysis_image_changed), analysis.bn_drop_profile_extract.on_changed.connect( self._hdl_analysis_drop_profile_extract_changed), analysis.bn_drop_profile_fit.on_changed.connect( self._hdl_analysis_drop_profile_fit_changed), ] self._analysis_unbind_tasks.extend(ec.disconnect for ec in event_connections) self._hdl_analysis_image_changed() self._hdl_analysis_drop_profile_extract_changed() self._hdl_analysis_drop_profile_fit_changed() def _hdl_analysis_image_changed(self) -> None: analysis = self._bn_analysis.get() image = analysis.bn_image.get() if image is None: self.view.show_waiting_placeholder() return self.view.hide_waiting_placeholder() drop_region = analysis.bn_drop_region.get() assert drop_region is not None drop_image = image[drop_region.y0:drop_region.y1, drop_region.x0:drop_region.x1] self.bn_drop_image.set(drop_image) def _hdl_analysis_drop_profile_extract_changed(self) -> None: analysis = self._bn_analysis.get() drop_profile_ext = analysis.bn_drop_profile_extract.get() if drop_profile_ext is None: self.bn_drop_profile_extract.set(None) return drop_region = analysis.bn_drop_region.get() assert drop_region is not None drop_profile_ext_rel_drop_image = drop_profile_ext - drop_region.pos self.bn_drop_profile_extract.set(drop_profile_ext_rel_drop_image) def _hdl_analysis_drop_profile_fit_changed(self) -> None: analysis = self._bn_analysis.get() drop_profile_fit = analysis.bn_drop_profile_fit.get() if drop_profile_fit is None: self.bn_drop_profile_fit.set(None) return drop_region = analysis.bn_drop_region.get() assert drop_region is not None drop_profile_fit_rel_drop_image = drop_profile_fit - drop_region.pos self.bn_drop_profile_fit.set(drop_profile_fit_rel_drop_image) def _unbind_analysis(self) -> None: for task in self._analysis_unbind_tasks: task() self._analysis_unbind_tasks.clear() def _do_destroy(self) -> None: for ec in self.__event_connections: ec.disconnect() self._unbind_analysis()
class PhysicalPropertiesCalculator: def __init__( self, features: FeatureExtractor, young_laplace_fit: YoungLaplaceFitter, params: PhysicalPropertiesCalculatorParams, ) -> None: self._extracted_features = features self._young_laplace_fit = young_laplace_fit self.params = params self.bn_interfacial_tension = BoxBindable(math.nan) self.bn_volume = BoxBindable(math.nan) self.bn_surface_area = BoxBindable(math.nan) self.bn_apex_radius = BoxBindable(math.nan) self.bn_worthington = BoxBindable(math.nan) features.bn_needle_width_px.on_changed.connect(self._recalculate) # Assume that bond number changes at the same time as other attributes young_laplace_fit.bn_bond_number.on_changed.connect(self._recalculate) params.bn_inner_density.on_changed.connect(self._recalculate) params.bn_outer_density.on_changed.connect(self._recalculate) params.bn_needle_width.on_changed.connect(self._recalculate) params.bn_gravity.on_changed.connect(self._recalculate) self._recalculate() def _recalculate(self) -> None: m_per_px = self._get_m_per_px() inner_density = self.params.bn_inner_density.get() outer_density = self.params.bn_outer_density.get() needle_width_m = self.params.bn_needle_width.get() gravity = self.params.bn_gravity.get() if inner_density is None or outer_density is None or needle_width_m is None or gravity is None: return bond_number = self._young_laplace_fit.bn_bond_number.get() apex_radius_px = self._young_laplace_fit.bn_apex_radius.get() apex_radius_m = m_per_px * apex_radius_px interfacial_tension = calculate_ift(inner_density=inner_density, outer_density=outer_density, bond_number=bond_number, apex_radius=apex_radius_m, gravity=gravity) volume_px3 = self._young_laplace_fit.bn_volume.get() volume_m3 = m_per_px**3 * volume_px3 surface_area_px2 = self._young_laplace_fit.bn_surface_area.get() surface_area_m2 = m_per_px**2 * surface_area_px2 worthington = calculate_worthington( inner_density=inner_density, outer_density=outer_density, gravity=gravity, ift=interfacial_tension, volume=volume_m3, needle_width=needle_width_m, ) self.bn_interfacial_tension.set(interfacial_tension) self.bn_volume.set(volume_m3) self.bn_surface_area.set(surface_area_m2) self.bn_apex_radius.set(apex_radius_m) self.bn_worthington.set(worthington) def _get_m_per_px(self) -> float: needle_width_px = self._extracted_features.bn_needle_width_px.get() needle_width_m = self.params.bn_needle_width.get() if needle_width_px is None or needle_width_m is None: return math.nan return needle_width_m / needle_width_px
class ConanResultsPresenter(Presenter['ConanResultsView']): UPDATE_TIME_INTERVAL = 1 def _do_init(self, model: ConanResultsModel, page_controls: WizardPageControls) -> None: self._loop = asyncio.get_event_loop() self._model = model self._page_controls = page_controls self.individual_model = model.individual self.graphs_model = model.graphs self.bn_footer_status = BoxBindable(ResultsFooterStatus.IN_PROGRESS) self.bn_completion_progress = model.bn_analyses_completion_progress self.bn_time_elapsed = BoxBindable(math.nan) self.bn_time_remaining = BoxBindable(math.nan) self._active_save_options = None self.__event_connections = [ model.bn_analyses_time_start.on_changed.connect( self._update_times), model.bn_analyses_time_est_complete.on_changed.connect( self._update_times), ] self._update_times() def view_ready(self) -> None: self.__event_connections.extend([ self._model.bn_fitting_status.on_changed.connect( self._hdl_model_fitting_status_changed), self._model.bn_analyses.on_changed.connect( self._hdl_model_analyses_changed) ]) self._hdl_model_fitting_status_changed() self._hdl_model_analyses_changed() def _hdl_model_analyses_changed(self) -> None: analyses = self._model.bn_analyses.get() if len(analyses) == 1: self.view.hide_graphs() else: self.view.show_graphs() def _hdl_model_fitting_status_changed(self) -> None: fitting_status = self._model.bn_fitting_status.get() if fitting_status is ConanResultsModel.Status.FITTING: footer_status = ResultsFooterStatus.IN_PROGRESS elif fitting_status is ConanResultsModel.Status.FINISHED: footer_status = ResultsFooterStatus.FINISHED self.view.hide_confirm_cancel_dialog() elif fitting_status is ConanResultsModel.Status.CANCELLED: footer_status = ResultsFooterStatus.CANCELLED self.view.hide_confirm_cancel_dialog() else: footer_status = ResultsFooterStatus.IN_PROGRESS self.bn_footer_status.set(footer_status) _update_times_handle = None def _update_times(self) -> None: if self._update_times_handle is not None: self._update_times_handle.cancel() self._update_times_handle = None time_elapsed = self._model.calculate_time_elapsed() self.bn_time_elapsed.set(time_elapsed) time_remaining = self._model.calculate_time_remaining() self.bn_time_remaining.set(time_remaining) fitting_status = self._model.bn_fitting_status.get() if fitting_status is ConanResultsModel.Status.FITTING: self._update_times_handle = self._loop.call_later( delay=self.UPDATE_TIME_INTERVAL, callback=self._update_times, ) def back(self) -> None: if self._model.is_safe_to_discard: self._back() else: self.view.show_confirm_discard_dialog() def hdl_confirm_discard_response(self, accept: bool) -> None: if accept: self._back() self.view.hide_confirm_discard_dialog() def _back(self) -> None: self._page_controls.prev_page() def cancel(self) -> None: if self._model.is_safe_to_discard: self._cancel() else: self.view.show_confirm_cancel_dialog() def hdl_confirm_cancel_response(self, accept: bool) -> None: if accept: self._cancel() self.view.hide_confirm_cancel_dialog() def _cancel(self): self._model.cancel_analyses() def save(self) -> None: if self._active_save_options is not None: return self._active_save_options = self._model.create_save_options() self.view.show_save_dialog(self._active_save_options) def hdl_save_dialog_response(self, should_save: bool) -> None: if self._active_save_options is None: return self.view.hide_save_dialog() save_options = self._active_save_options self._active_save_options = None if should_save: self._model.save_analyses(save_options) def _do_destroy(self) -> None: for ec in self.__event_connections: ec.disconnect() if self._update_times_handle is not None: self._update_times_handle.cancel()
class ResultsFooterPresenter(Presenter['ResultsFooterView']): def _do_init( self, in_status: Bindable[ResultsFooterStatus], in_progress: Bindable[float], in_time_elapsed: Bindable[float], in_time_remaining: Bindable[float], do_back: Callable[[], Any], do_cancel: Callable[[], Any], do_save: Callable[[], Any], ) -> None: self._bn_status = in_status self._bn_time_remaining = in_time_remaining self._do_back = do_back self._do_cancel = do_cancel self._do_save = do_save self.bn_progress = in_progress self.bn_progress_label = BoxBindable('') self.bn_time_elapsed = in_time_elapsed self.bn_time_remaining = BoxBindable(math.nan) self.__event_connections = [] def view_ready(self) -> None: self.__event_connections.extend([ self._bn_status.on_changed.connect(self._hdl_status_changed), self._bn_time_remaining.on_changed.connect( self._hdl_time_remaining_changed), ]) self._hdl_status_changed() self._hdl_time_remaining_changed() def _hdl_status_changed(self) -> None: status = self._bn_status.get() if status is ResultsFooterStatus.IN_PROGRESS: self.bn_progress_label.set('') self.view.show_cancel_btn() self.view.disable_save_btn() elif status is ResultsFooterStatus.FINISHED: self.bn_progress_label.set('Finished') self.view.show_back_btn() self.view.enable_save_btn() elif status is ResultsFooterStatus.CANCELLED: self.bn_progress_label.set('Cancelled') self.view.show_back_btn() self.view.enable_save_btn() def _hdl_time_remaining_changed(self) -> None: time_remaining = self._bn_time_remaining.get() self.bn_time_remaining.set(time_remaining) def back(self) -> None: self._do_back() def cancel(self) -> None: self._do_cancel() def save(self) -> None: self._do_save() def _do_destroy(self) -> None: for ec in self.__event_connections: ec.disconnect()
class GraphsModel: def __init__(self, in_analyses: Bindable[Sequence[ConanAnalysis]]) -> None: self._bn_analyses = in_analyses self._tracked_analyses = [] self._tracked_analysis_unbind_tasks = {} self.bn_left_angle_data = BoxBindable((tuple(), tuple())) self.bn_right_angle_data = BoxBindable((tuple(), tuple())) self._bn_analyses.on_changed.connect(self._hdl_analyses_changed) self._hdl_analyses_changed() def _hdl_analyses_changed(self) -> None: analyses = self._bn_analyses.get() tracked_analyses = self._tracked_analyses to_track = set(analyses) - set(tracked_analyses) for analysis in to_track: self._track_analysis(analysis) to_untrack = set(tracked_analyses) - set(analyses) for analysis in to_untrack: self._untrack_analysis(analysis) def _track_analysis(self, analysis: ConanAnalysis) -> None: unbind_tasks = [] event_connections = [ analysis.bn_left_angle.on_changed.connect( self._hdl_tracked_analysis_data_changed), analysis.bn_right_angle.on_changed.connect( self._hdl_tracked_analysis_data_changed), ] unbind_tasks.extend(ec.disconnect for ec in event_connections) self._tracked_analyses.append(analysis) self._tracked_analysis_unbind_tasks[analysis] = unbind_tasks self._hdl_tracked_analysis_data_changed() def _untrack_analysis(self, analysis: ConanAnalysis) -> None: unbind_tasks = self._tracked_analysis_unbind_tasks[analysis] for task in unbind_tasks: task() del self._tracked_analysis_unbind_tasks[analysis] self._tracked_analyses.remove(analysis) def _hdl_tracked_analysis_data_changed(self) -> None: left_angle_data = [] right_angle_data = [] analyses = self._bn_analyses.get() for analysis in analyses: timestamp = analysis.bn_image_timestamp.get() if timestamp is None or not math.isfinite(timestamp): continue left_angle_value = analysis.bn_left_angle.get() right_angle_value = analysis.bn_right_angle.get() if left_angle_value is not None and math.isfinite( left_angle_value): left_angle_data.append((timestamp, left_angle_value)) if right_angle_value is not None and math.isfinite( right_angle_value): right_angle_data.append((timestamp, right_angle_value)) # Sort in ascending order of timestamp left_angle_data = sorted(left_angle_data, key=lambda x: x[0]) right_angle_data = sorted(right_angle_data, key=lambda x: x[0]) left_angle_data = tuple(zip(*left_angle_data)) right_angle_data = tuple(zip(*right_angle_data)) if len(left_angle_data) == 0: left_angle_data = (tuple(), tuple()) if len(right_angle_data) == 0: right_angle_data = (tuple(), tuple()) self.bn_left_angle_data.set(left_angle_data) self.bn_right_angle_data.set(right_angle_data)