diff --git a/CMakeLists.txt b/CMakeLists.txt index f43af3b45..204398c3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -326,6 +326,8 @@ set(guiding_SRC ${phd_src_dir}/guide_algorithms.h ${phd_src_dir}/guider_multistar.cpp ${phd_src_dir}/guider_multistar.h + ${phd_src_dir}/guider_planetary.cpp + ${phd_src_dir}/guider_planetary.h ${phd_src_dir}/guider.cpp ${phd_src_dir}/guider.h ${phd_src_dir}/guiders.h @@ -427,6 +429,8 @@ set(phd2_SRC ${phd_src_dir}/phdupdate.h ${phd_src_dir}/pierflip_tool.cpp ${phd_src_dir}/pierflip_tool.h + ${phd_src_dir}/planetary_tool.cpp + ${phd_src_dir}/planetary_tool.h ${phd_src_dir}/polardrift_tool.h ${phd_src_dir}/polardrift_toolwin.h ${phd_src_dir}/polardrift_toolwin.cpp diff --git a/camera.cpp b/camera.cpp index 1b67dd900..94fcdfcba 100644 --- a/camera.cpp +++ b/camera.cpp @@ -681,6 +681,9 @@ bool GuideCamera::SetCameraGain(int cameraGain) pConfig->Profile.SetInt("/camera/gain", GuideCameraGain); + if (pFrame) + pFrame->UpdateCameraSettings(); + return bError; } diff --git a/gear_dialog.cpp b/gear_dialog.cpp index 4d9cda035..0a0cb4ecd 100644 --- a/gear_dialog.cpp +++ b/gear_dialog.cpp @@ -1093,6 +1093,9 @@ bool GearDialog::DoConnectCamera(bool autoReconnecting) throw THROW_INFO("DoConnectCamera: connect failed"); } + // Notify planetary module of camera connect + pFrame->pGuider->m_Planet.NotifyCameraConnect(true); + // update camera pixel size from the driver, cam must be connected for reliable results double prevPixelSize = m_pCamera->GetProfilePixelSize(); double pixelSize; @@ -1250,6 +1253,9 @@ void GearDialog::OnButtonDisconnectCamera(wxCommandEvent& event) m_pCamera->Disconnect(); + // Notify planetary module of camera disconnect + pFrame->pGuider->m_Planet.NotifyCameraConnect(false); + if (m_pScope && m_pScope->RequiresCamera() && m_pScope->IsConnected()) { Debug.Write("gear_dialog: scope requires camera so disconnecting scope\n"); diff --git a/guider.h b/guider.h index b99c21248..ceb37d2cf 100644 --- a/guider.h +++ b/guider.h @@ -40,6 +40,8 @@ #ifndef GUIDER_H_INCLUDED #define GUIDER_H_INCLUDED +#include "guider_planetary.h" + enum GUIDER_STATE { STATE_UNINITIALIZED = 0, @@ -279,6 +281,9 @@ class Guider : public wxWindow void SetAutoSelDownsample(unsigned int val); unsigned int GetAutoSelDownsample() const; + // Planetary disk detection parameters + GuiderPlanet m_Planet; + // virtual functions -- these CAN be overridden by a subclass, which should // consider whether they need to call the base class functions as part of // their operation diff --git a/guider_planetary.cpp b/guider_planetary.cpp new file mode 100644 index 000000000..9c43e0b34 --- /dev/null +++ b/guider_planetary.cpp @@ -0,0 +1,1012 @@ +/* + * guider_planetary.cpp + * PHD Guiding + * + * Planetary detection extensions by Leo Shatz + * Copyright (c) 2023-2024 Leo Shatz + * All rights reserved. + * + * This source code is distributed under the following "BSD" license + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of Craig Stark, Stark Labs nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "phd.h" +#include "guider_planetary.h" +#include "planetary_tool.h" + +#include +#include + +#if ((wxMAJOR_VERSION < 3) && (wxMINOR_VERSION < 9)) +#define wxPENSTYLE_DOT wxDOT +#endif + +// Using OpenCV namespace +using namespace cv; + +// Gaussian weights lookup table +#define GAUSSIAN_SIZE 2000 +static float gaussianWeight[GAUSSIAN_SIZE]; + +// Initialize planetary module +GuiderPlanet::GuiderPlanet() +{ + m_Planetary_enabled = false; + m_PlanetaryDetectionPaused = false; + m_prevCaptureActive = false; + m_detected = false; + m_radius = 0; + m_searchRegion = 0; + m_prevSearchRegion = 0; + m_starProfileSize = 50; + m_measuringSharpnessMode = false; + m_unknownHFD = true; + m_focusSharpness = 0; + + m_cameraSimulationMove = Point2f(0, 0); + m_cameraSimulationRefPoint = Point2f(0, 0); + m_cameraSimulationRefPointValid = false; + m_simulationZeroOffset = false; + m_center_x = m_center_y = 0; + m_origPoint = Point2f(0, 0); + + m_roiClicked = false; + m_roiActive = false; + m_detectionCounter = 0; + m_clicked_x = 0; + m_clicked_y = 0; + m_prevClickedPoint = Point2f(0, 0); + m_diskContour.clear(); + m_RoiEnabled = false; + m_Planetary_minRadius = PT_MIN_RADIUS_DEFAULT; + m_Planetary_maxRadius = PT_MAX_RADIUS_DEFAULT; + m_Planetary_lowThreshold = PT_HIGH_THRESHOLD_DEFAULT / 2; + m_Planetary_highThreshold = PT_HIGH_THRESHOLD_DEFAULT; + m_Planetary_ShowElementsButtonState = false; + m_Planetary_ShowElementsVisual = false; + m_draw_PlanetaryHelper = false; + m_frameWidth = 0; + m_frameHeight = 0; + m_PlanetEccentricity = 0; + m_PlanetAngle = 0; + + // Build gaussian weighting function table used for circle feature detection + float sigma = 1.0; + memset(gaussianWeight, 0, sizeof(gaussianWeight)); + for (double x = 0; x < 20; x += 0.01) + { + int i = x * 100 + 0.5; + if (i < GAUSSIAN_SIZE) + gaussianWeight[i] += exp(-(pow(x, 2) / (2 * pow(sigma, 2)))); + } + + // Enforce valid range limits on planetary detection parameters while restoring from configuration + m_Planetary_minRadius = pConfig->Profile.GetInt("/PlanetTool/min_radius", PT_MIN_RADIUS_DEFAULT); + m_Planetary_minRadius = wxMax(PT_RADIUS_MIN, wxMin(PT_RADIUS_MAX, m_Planetary_minRadius)); + m_Planetary_maxRadius = pConfig->Profile.GetInt("/PlanetTool/max_radius", PT_MAX_RADIUS_DEFAULT); + m_Planetary_maxRadius = wxMax(PT_RADIUS_MIN, wxMin(PT_RADIUS_MAX, m_Planetary_maxRadius)); + m_Planetary_lowThreshold = pConfig->Profile.GetInt("/PlanetTool/high_threshold", PT_HIGH_THRESHOLD_DEFAULT) / 2; + m_Planetary_lowThreshold = wxMax(PT_THRESHOLD_MIN, wxMin(PT_LOW_THRESHOLD_MAX, m_Planetary_lowThreshold)); + m_Planetary_highThreshold = pConfig->Profile.GetInt("/PlanetTool/high_threshold", PT_HIGH_THRESHOLD_DEFAULT); + m_Planetary_highThreshold = wxMax(PT_THRESHOLD_MIN, wxMin(PT_HIGH_THRESHOLD_MAX, m_Planetary_highThreshold)); + + // Remove the alert dialog setting for pausing planetary detection + pConfig->Global.DeleteEntry(PausePlanetDetectionAlertEnabledKey()); +} + +GuiderPlanet::~GuiderPlanet() +{ + // Save all detection parameters + pConfig->Profile.SetInt("/PlanetTool/min_radius", GetPlanetaryParam_minRadius()); + pConfig->Profile.SetInt("/PlanetTool/max_radius", GetPlanetaryParam_maxRadius()); + pConfig->Profile.SetInt("/PlanetTool/high_threshold", GetPlanetaryParam_highThreshold()); + pConfig->Flush(); +} + +// Planet/feature size depending on planetary detection mode +double GuiderPlanet::GetHFD() +{ + if (m_unknownHFD) + return std::nan("1"); + if (m_measuringSharpnessMode) + return m_focusSharpness; + else + return m_detected ? m_radius : 0; +} + +wxString GuiderPlanet::GetHfdLabel() +{ + if (m_measuringSharpnessMode) + return _("SHARPNESS: "); + else + return _("RADIUS: "); +} + +bool GuiderPlanet::IsPixelMetrics() +{ + return GetPlanetaryEnableState() ? !m_measuringSharpnessMode : true; +} + +// Toggle between sharpness and radius display +void GuiderPlanet::ToggleSharpness() +{ + m_measuringSharpnessMode = !m_measuringSharpnessMode; + m_unknownHFD = true; +} + +// The Sobel operator can be used to detect edges in an image, which are more pronounced in +// focused images. You can apply the Sobel operator to the image and calculate the sum or mean +// of the absolute values of the gradients. +double GuiderPlanet::ComputeSobelSharpness(const Mat& img) +{ + Mat grad_x, grad_y; + Sobel(img, grad_x, CV_32F, 1, 0); + Sobel(img, grad_y, CV_32F, 0, 1); + + Mat grad; + magnitude(grad_x, grad_y, grad); + + double sharpness = cv::mean(grad)[0]; + return sharpness; +} + +// Calculate focus metrics around the updated tracked position +double GuiderPlanet::CalcSharpness(Mat& FullFrame, Point2f& clickedPoint, bool detectionResult) +{ + double scaleFactor; + cv::Scalar meanSignal; + Mat focusRoi; + int focusX; + int focusY; + + if (detectionResult) + { + focusX = m_center_x; + focusY = m_center_y; + } + else if (norm(clickedPoint)) + { + focusX = clickedPoint.x; + focusY = clickedPoint.y; + } + else + { + // Compute scaling factor to normalize the signal + meanSignal = cv::mean(focusRoi); + scaleFactor = meanSignal[0] ? (65536.0 / 256) / meanSignal[0] : 1.0; + + // For failed auto selected star use entire frame for sharpness calculation + FullFrame.convertTo(focusRoi, CV_32F, scaleFactor); + return ComputeSobelSharpness(focusRoi); + } + + const int focusSize = m_Planetary_maxRadius * 3 / 2.0; + focusX = wxMax(0, focusX - focusSize / 2); + focusX = wxMax(0, wxMin(focusX, m_frameWidth - focusSize)); + focusY = wxMax(0, focusY - focusSize / 2); + focusY = wxMax(0, wxMin(focusY, m_frameHeight - focusSize)); + Rect focusSubFrame = Rect(focusX, focusY, focusSize, focusSize); + focusRoi = FullFrame(focusSubFrame); + + meanSignal = cv::mean(focusRoi); + scaleFactor = meanSignal[0] ? (65536.0 / 256) / meanSignal[0] : 1.0; + + focusRoi.convertTo(focusRoi, CV_32F, scaleFactor); + return ComputeSobelSharpness(focusRoi); +} + +// Get current detection status +void GuiderPlanet::GetDetectionStatus(wxString& statusMsg) +{ + statusMsg = wxString::Format(_("Object at (%.1f, %.1f) radius=%d"), m_center_x, m_center_y, m_radius); +} + +// Update state used to visualize internally detected features +void GuiderPlanet::SetPlanetaryElementsVisual(bool state) +{ + m_syncLock.Lock(); + m_diskContour.clear(); + m_Planetary_ShowElementsVisual = state; + m_syncLock.Unlock(); +} + +// Notification callback when PHD2 may change CaptureActive state +bool GuiderPlanet::UpdateCaptureState(bool CaptureActive) +{ + bool need_update = false; + if (m_prevCaptureActive != CaptureActive) + { + if (!CaptureActive) + { + // Clear selection symbols (green circle/target lock) and visual elements + if (GetPlanetaryEnableState()) + { + SetPlanetaryElementsVisual(false); + pFrame->pGuider->Reset(false); + } + need_update = true; + } + else + { + // In planetary tracking mode update the state used to + // control drawing of the internal detection elements. + if (GetPlanetaryEnableState() && GetPlanetaryElementsButtonState()) + SetPlanetaryElementsVisual(true); + RestartSimulatorErrorDetection(); + } + } + + // Reset the detection paused state if guiding has been cancelled + if (!pFrame->pGuider->IsGuiding()) + { + SetDetectionPausedState(false); + } + + m_prevCaptureActive = CaptureActive; + return need_update; +} + +// Notification callback when camera is connected/disconnected +void GuiderPlanet::NotifyCameraConnect(bool connected) +{ + m_roiClicked = false; +} + +void GuiderPlanet::RestartSimulatorErrorDetection() +{ + m_cameraSimulationRefPointValid = false; + m_simulationZeroOffset = true; +} + +// Helper for visualizing planet detection radius +void GuiderPlanet::PlanetVisualHelper(wxDC& dc, Star primaryStar, double scaleFactor) +{ + // Clip drawing region to displayed image frame + wxImage* pImg = pFrame->pGuider->DisplayedImage(); + if (pImg) + dc.SetClippingRegion(wxRect(0, 0, pImg->GetWidth(), pImg->GetHeight())); + + // Make sure to use transparent brush + dc.SetBrush(*wxTRANSPARENT_BRUSH); + + // Display internally detected elements (must be enabled in UI) + if (GetPlanetaryElementsVisual()) + { + m_syncLock.Lock(); + + // Draw contour points detected in planetary mode + if (m_diskContour.size()) + { + dc.SetPen(wxPen(wxColour(230, 0, 0), 2, wxPENSTYLE_SOLID)); + for (const Point2f& contourPoint : m_diskContour) + dc.DrawCircle((contourPoint.x + m_roiRect.x) * scaleFactor, (contourPoint.y + m_roiRect.y) * scaleFactor, 2); + } + + m_syncLock.Unlock(); + } + + // Reset clipping region (don't clip min/max circles) + dc.DestroyClippingRegion(); + + // Display min/max diameters for visual feedback + if (m_draw_PlanetaryHelper) + { + m_draw_PlanetaryHelper = false; + if (pFrame->CaptureActive) + { + const wxString labelTextMin("min diameter"); + const wxString labelTextMax("max diameter"); + int x = int(primaryStar.X * scaleFactor + 0.5); + int y = int(primaryStar.Y * scaleFactor + 0.5); + int radius = int(m_radius * scaleFactor + 0.5); + float minRadius = GetPlanetaryParam_minRadius() * scaleFactor; + float maxRadius = GetPlanetaryParam_maxRadius() * scaleFactor; + int minRadius_x = x + minRadius; + int maxRadius_x = x + maxRadius; + int lineMin_x = x; + int lineMax_x = x; + + // Center the elements at the tracking point + if (m_detected) + { + minRadius_x = maxRadius_x = x; + lineMin_x -= minRadius; + lineMax_x -= maxRadius; + } + + // Draw min and max diameters legends + dc.SetPen(wxPen(wxColour(230, 130, 30), 1, wxPENSTYLE_DOT)); + dc.SetTextForeground(wxColour(230, 130, 30)); + dc.DrawLine(lineMin_x, y - 5, lineMin_x + minRadius * 2, y - 5); + dc.DrawCircle(minRadius_x, y, minRadius); + dc.DrawText(labelTextMin, minRadius_x - dc.GetTextExtent(labelTextMin).GetWidth() / 2, y - 10 - dc.GetTextExtent(labelTextMin).GetHeight()); + + dc.SetPen(wxPen(wxColour(130, 230, 30), 1, wxPENSTYLE_DOT)); + dc.SetTextForeground(wxColour(130, 230, 30)); + dc.DrawLine(lineMax_x, y + 5, lineMax_x + maxRadius * 2, y + 5); + dc.DrawCircle(maxRadius_x, y, maxRadius); + dc.DrawText(labelTextMax, maxRadius_x - dc.GetTextExtent(labelTextMax).GetWidth() / 2, y + 5); + } + } +} + +void GuiderPlanet::CalcLineParams(CircleDescriptor p1, CircleDescriptor p2) +{ + float dx = p1.x - p2.x; + float dy = p1.y - p2.y; + if ((p1.radius == 0) || (p2.radius == 0) || (dx * dx + dy * dy < 3)) + { + m_DiameterLineParameters.valid = false; + m_DiameterLineParameters.vertical = false; + m_DiameterLineParameters.slope = 0; + m_DiameterLineParameters.b = 0; + return; + } + // Check to see if line is vertical + if (fabs(p1.x - p2.x) < 1) + { + // Vertical line, slope is undefined + m_DiameterLineParameters.valid = true; + m_DiameterLineParameters.vertical = true; + m_DiameterLineParameters.slope = std::numeric_limits::infinity(); + m_DiameterLineParameters.b = 0; + } + else + { + // Calculate slope (m) and y-intercept (b) for a non-vertical line + m_DiameterLineParameters.valid = true; + m_DiameterLineParameters.vertical = false; + m_DiameterLineParameters.slope = (p2.y - p1.y) / (p2.x - p1.x); + m_DiameterLineParameters.b = p1.y - (m_DiameterLineParameters.slope * p1.x); + } +} + +// Calculate score for given point +static float CalcContourScore(float& radius, Point2f pointToMeasure, std::vector& diskContour, int minRadius, int maxRadius) +{ + std::vector distances; + distances.reserve(diskContour.size()); + float minIt = FLT_MAX; + float maxIt = FLT_MIN; + + for (const auto& contourPoint : diskContour) + { + float distance = norm(contourPoint - pointToMeasure); + if (distance >= minRadius && distance <= maxRadius) + { + minIt = wxMin(minIt, distance); + maxIt = wxMax(maxIt, distance); + distances.push_back(distance); + } + } + + // Note: calculating histogram on 0-sized data can crash the application. + // Reject small sets of points as they usually aren't related to the features we are looking for. + if (distances.size() < 16) + { + radius = 0; + return 0; + } + + // Calculate the number of bins + int bins = int(std::sqrt(distances.size()) + 0.5) | 1; + float range[] = { std::floor(minIt), std::ceil(maxIt) }; + const float* histRange[] = { range }; + + // Calculate the histogram + Mat hist; + Mat distData(distances); // Use vector directly to create Mat object + cv::calcHist(&distData, 1, nullptr, Mat(), hist, 1, &bins, histRange, true, false); + + // Find the peak of the histogram + double max_value; + Point max_loc; + cv::minMaxLoc(hist, nullptr, &max_value, nullptr, &max_loc); + int max_idx = max_loc.y; + + // Middle of the bin + float peakDistance = range[0] + (max_idx + 0.5) * ((range[1] - range[0]) / bins); + + float scorePoints = 0; + for (float distance : distances) + { + int index = fabs(distance - peakDistance) * 100 + 0.5; + if (index < GAUSSIAN_SIZE) + scorePoints += gaussianWeight[index]; + } + + // Normalize score by total number points in the contour + radius = peakDistance; + return scorePoints / diskContour.size(); +} + +class AsyncCalcScoreThread : public wxThread +{ +public: + std::vector points; + std::vector contour; + Point2f center; + float radius; + float threadBestScore; + int minRadius; + int maxRadius; + +public: + AsyncCalcScoreThread(float bestScore, std::vector& diskContour, std::vector& workLoad, int min_radius, int max_radius) + : wxThread(wxTHREAD_JOINABLE), threadBestScore(bestScore), contour(diskContour), points(workLoad), minRadius(min_radius), maxRadius(max_radius) + { + radius = 0; + } + // A thread function to run HoughCircles method + wxThread::ExitCode Entry() + { + for (const Point2f& point : points) + { + float score = ::CalcContourScore(radius, point, contour, minRadius, maxRadius); + if (score > threadBestScore) + { + threadBestScore = score; + radius = radius; + center.x = point.x; + center.y = point.y; + } + } + return this; + } +}; + +/* Find best circle candidate */ +int GuiderPlanet::RefineDiskCenter(float& bestScore, CircleDescriptor& diskCenter, std::vector& diskContour, int minRadius, int maxRadius, float searchRadius, float resolution) +{ + const int maxWorkloadSize = 256; + const Point2f center = { diskCenter.x, diskCenter.y }; + std::vector threads; + + // Check all points within small circle for search of higher score + int threadCount = 0; + bool useThreads = true; + int workloadSize = 0; + std::vector workload; + workload.reserve(maxWorkloadSize); + for (float x = diskCenter.x - searchRadius; x < diskCenter.x + searchRadius; x += resolution) + for (float y = diskCenter.y - searchRadius; y < diskCenter.y + searchRadius; y += resolution) + { + Point2f pointToMeasure = { x, y }; + float dist = norm(pointToMeasure - center); + if (dist > searchRadius) + continue; + + // When finished creating a workload, create and run new processing thread + if (useThreads && (workloadSize++ >= maxWorkloadSize)) + { + AsyncCalcScoreThread *thread = new AsyncCalcScoreThread(bestScore, diskContour, workload, minRadius, maxRadius); + if ((thread->Create() == wxTHREAD_NO_ERROR) && (thread->Run() == wxTHREAD_NO_ERROR)) + { + threads.push_back(thread); + workload.clear(); + workloadSize = 0; + threadCount++; + } + else + { + useThreads = false; + Debug.Write(_("RefineDiskCenter: failed to start a thread\n")); + } + } + workload.push_back(pointToMeasure); + } + + // Process remaining points locally + for (const Point2f& point : workload) + { + float radius; + float score = ::CalcContourScore(radius, point, diskContour, minRadius, maxRadius); + if (score > bestScore) + { + bestScore = score; + diskCenter.radius = radius; + diskCenter.x = point.x; + diskCenter.y = point.y; + } + } + + // Wait for all threads to terminate and process their results + for (auto thread : threads) + { + thread->Wait(); + if (thread->threadBestScore > bestScore) + { + bestScore = thread->threadBestScore; + diskCenter.radius = thread->radius; + diskCenter.x = thread->center.x; + diskCenter.y = thread->center.y; + } + + delete thread; + } + return threadCount; +} + +// An algorithm to find contour center +float GuiderPlanet::FindContourCenter(CircleDescriptor& diskCenter, CircleDescriptor& circle, std::vector& diskContour, Moments& mu, int minRadius, int maxRadius) +{ + float score; + float maxScore = 0; + float bestScore = 0; + float radius = 0; + int searchRadius = circle.radius / 2; + Point2f pointToMeasure; + std::vector WeightedCircles; + WeightedCircles.reserve(searchRadius * 2); + + // When center of mass (centroid) wasn't found use smallest circle for measurement + if (!m_DiameterLineParameters.valid) + { + pointToMeasure.x = circle.x; + pointToMeasure.y = circle.y; + score = CalcContourScore(radius, pointToMeasure, diskContour, minRadius, maxRadius); + diskCenter = circle; + diskCenter.radius = radius; + return score; + } + + if (!m_DiameterLineParameters.vertical && (fabs(m_DiameterLineParameters.slope) <= 1.0)) + { + // Search along x-axis when line slope is below 45 degrees + for (pointToMeasure.x = circle.x - searchRadius; pointToMeasure.x <= circle.x + searchRadius; pointToMeasure.x++) + { + // Count number of points of the contour which are equidistant from pointToMeasure. + // The point with maximum score is identified as contour center. + pointToMeasure.y = m_DiameterLineParameters.slope * pointToMeasure.x + m_DiameterLineParameters.b; + score = CalcContourScore(radius, pointToMeasure, diskContour, minRadius, maxRadius); + maxScore = max(score, maxScore); + WeightedCircle wcircle = { pointToMeasure.x, pointToMeasure.y, radius, score }; + WeightedCircles.push_back(wcircle); + } + } + else + { + // Search along y-axis when slope is above 45 degrees + for (pointToMeasure.y = circle.y - searchRadius; pointToMeasure.y <= circle.y + searchRadius; pointToMeasure.y++) + { + // Count number of points of the contour which are equidistant from pointToMeasure. + // The point with maximum score is identified as contour center. + if (m_DiameterLineParameters.vertical) + pointToMeasure.x = circle.x; + else + pointToMeasure.x = (pointToMeasure.y - m_DiameterLineParameters.b) / m_DiameterLineParameters.slope; + score = CalcContourScore(radius, pointToMeasure, diskContour, minRadius, maxRadius); + maxScore = max(score, maxScore); + WeightedCircle wcircle = { pointToMeasure.x, pointToMeasure.y, radius, score }; + WeightedCircles.push_back(wcircle); + } + } + + // Find local maxima point closer to center of mass, + // this will help not to select center of the dark disk + int bestIndex = 0; + float bestCenterOfMassDistance = 999999; + Point2f centroid = { float(mu.m10 / mu.m00), float(mu.m01 / mu.m00) }; + for (int i = 1; i < WeightedCircles.size() - 1; i++) + { + if ((WeightedCircles[i].score > maxScore*0.65) && + (WeightedCircles[i].score > WeightedCircles[i - 1].score) && + (WeightedCircles[i].score > WeightedCircles[i + 1].score)) + { + WeightedCircle* localMax = &WeightedCircles[i]; + Point2f center = { localMax->x, localMax->y }; + float centerOfMassDistance = norm(centroid - center); + if (centerOfMassDistance < bestCenterOfMassDistance) + { + bestCenterOfMassDistance = centerOfMassDistance; + bestIndex = i; + } + } + } + if (WeightedCircles.size() < 3) + { + for (int i = 0; i < WeightedCircles.size(); i++) + if (WeightedCircles[i].score > bestScore) + { + bestScore = WeightedCircles[i].score; + bestIndex = i; + } + } + + bestScore = WeightedCircles[bestIndex].score; + diskCenter.radius = WeightedCircles[bestIndex].r; + diskCenter.x = WeightedCircles[bestIndex].x; + diskCenter.y = WeightedCircles[bestIndex].y; + + return bestScore; +} + +// Find a minimum enclosing circle of the contour and also its center of mass +void GuiderPlanet::FindCenters(Mat image, const std::vector& contour, CircleDescriptor& centroid, CircleDescriptor& circle, std::vector& diskContour, Moments& mu, int minRadius, int maxRadius) +{ + const std::vector* effectiveContour = &contour; + std::vector decimatedContour; + Point2f circleCenter; + float circle_radius = 0; + + // Add extra margins for min/max radii allowing inclusion of contours + // outside and inside the given range. + maxRadius = (maxRadius * 5) / 4; + minRadius = (minRadius * 3) / 4; + + m_PlanetEccentricity = 0; + m_PlanetAngle = 0; + circle.radius = 0; + centroid.radius = 0; + diskContour.clear(); + + // If input contour is too large, decimate it to avoid performance issues + int decimateRatio = contour.size() > 4096 ? contour.size() / 4096 : 1; + if (decimateRatio > 1) + { + decimatedContour.reserve(contour.size() / decimateRatio); + for (int i = 0; i < contour.size(); i += decimateRatio) + decimatedContour.push_back(contour[i]); + effectiveContour = &decimatedContour; + } + diskContour.reserve(effectiveContour->size()); + minEnclosingCircle(*effectiveContour, circleCenter, circle_radius); + + if ((circle_radius <= maxRadius) && (circle_radius >= minRadius)) + { + // Convert contour to vector of floating points + for (int i = 0; i < effectiveContour->size(); i++) + { + Point pt = (*effectiveContour)[i]; + diskContour.push_back(Point2f(pt.x, pt.y)); + } + + circle.x = circleCenter.x; + circle.y = circleCenter.y; + circle.radius = circle_radius; + + // Calculate center of mass based on contour points + mu = cv::moments(diskContour, false); + if (mu.m00 > 0) + { + centroid.x = mu.m10 / mu.m00; + centroid.y = mu.m01 / mu.m00; + centroid.radius = circle.radius; + + // Calculate eccentricity + double a = mu.mu20 + mu.mu02; + double b = sqrt(4 * mu.mu11 * mu.mu11 + (mu.mu20 - mu.mu02) * (mu.mu20 - mu.mu02)); + double major_axis = sqrt(2 * (a + b)); + double minor_axis = sqrt(2 * (a - b)); + m_PlanetEccentricity = sqrt(1 - (minor_axis * minor_axis) / (major_axis * major_axis)); + + // Calculate orientation (theta) in radians and convert to degrees + float theta = 0.5 * atan2(2 * mu.mu11, (mu.mu20 - mu.mu02)); + m_PlanetAngle = theta * (180.0 / CV_PI); + } + } +} + +// Find planet center using circle matching with contours +bool GuiderPlanet::FindPlanetCenter(Mat img8, int minRadius, int maxRadius, bool roiActive, Point2f& clickedPoint, Rect& roiRect, bool activeRoiLimits, float distanceRoiMax) +{ + int LowThreshold = GetPlanetaryParam_lowThreshold(); + int HighThreshold = GetPlanetaryParam_highThreshold(); + + // Apply Canny edge detection + Debug.Write(wxString::Format("Start detection of planetary disk (roi:%d low_tr=%d,high_tr=%d,minr=%d,maxr=%d)\n", roiActive, LowThreshold, HighThreshold, minRadius, maxRadius)); + Mat edges, dilatedEdges; + Canny(img8, edges, LowThreshold, HighThreshold, 5, true); + dilate(edges, dilatedEdges, Mat(), Point(-1, -1), 2); + + // Find contours + std::vector> contours; + cv::findContours(dilatedEdges, contours, RETR_LIST, CHAIN_APPROX_NONE); + + // Find total number of contours. If the number is too large, it means that + // edge detection threshold value is possibly too low, or we'll need to decimate number of points + // before further processing to avoid performance issues. + int totalPoints = 0; + for (const auto& contour : contours) + { + totalPoints += contour.size(); + } + if (totalPoints > 512 * 1024) + { + Debug.Write(wxString::Format("Too many contour points detected (%d)\n", totalPoints)); + m_statusMsg = _("Too many contour points detected. Please apply pixel binning, enable ROI, or increase the Edge Detection Threshold."); + pFrame->Alert(m_statusMsg, wxICON_WARNING); + return false; + } + + // Iterate between sets of contours to find the best match + int contourAllCount = 0; + int contourMatchingCount = 0; + float bestScore = 0; + std::vector bestContour; + CircleDescriptor bestCircle = { 0 }; + CircleDescriptor bestCentroid = { 0 }; + CircleDescriptor bestDiskCenter = { 0 }; + bestContour.clear(); + int maxThreadsCount = 0; + for (const auto& contour : contours) + { + // Ignore contours with small number of points + if (contour.size() < 32) + continue; + + // Find the smallest circle encompassing contour of the object + // and also center of mass within the contour. + cv::Moments mu; + std::vector diskContour; + CircleDescriptor circle = { 0 }; + CircleDescriptor centroid = { 0 }; + CircleDescriptor diskCenter = { 0 }; + FindCenters(img8, contour, centroid, circle, diskContour, mu, minRadius, maxRadius); + + // Skip circles not within radius range + if ((circle.radius == 0) || (diskContour.size() == 0)) + continue; + + // Look for a point along the line connecting centers of the smallest circle and center + // of mass which is equidistant from the outmost edge of the contour. Consider this point as + // the best match for contour central point. + CalcLineParams(circle, centroid); + float score = FindContourCenter(diskCenter, circle, diskContour, mu, minRadius, maxRadius); + + // When user clicks a point in the main window, discard detected features + // that are far away from it, similar to manual selection of stars in PHD2. + Point2f circlePoint = { roiRect.x + diskCenter.x, roiRect.y + diskCenter.y }; + if (activeRoiLimits && (norm(clickedPoint - circlePoint) > distanceRoiMax)) + score = 0; + + // Refine the best fit + if (score > 0.01) + { + float searchRadius = 20 * m_PlanetEccentricity + 3; + int threadCount = RefineDiskCenter(score, diskCenter, diskContour, minRadius, maxRadius, searchRadius); + maxThreadsCount = max(maxThreadsCount, threadCount); + if (score > bestScore * 0.8) + threadCount = RefineDiskCenter(score, diskCenter, diskContour, minRadius, maxRadius, 0.5, 0.1); + maxThreadsCount = max(maxThreadsCount, threadCount); + } + + // Select best fit based on highest score + if (score > bestScore) + { + bestScore = score; + bestDiskCenter = diskCenter; + bestCentroid = centroid; + bestContour = diskContour; + bestCircle = circle; + } + contourMatchingCount++; + } + + Debug.Write(wxString::Format("End detection of planetary disk (t=%d): r=%.1f, x=%.1f, y=%.1f, score=%.3f, contours=%d/%d, threads=%d\n", + m_PlanetWatchdog.Time(), bestDiskCenter.radius, roiRect.x + bestDiskCenter.x, roiRect.y + bestDiskCenter.y, bestScore, contourMatchingCount, contourAllCount, maxThreadsCount)); + + // For use by visual aid for parameter tuning + if (GetPlanetaryElementsVisual()) + { + m_syncLock.Lock(); + m_roiRect = roiRect; + m_diskContour = bestContour; + m_syncLock.Unlock(); + } + + if (bestDiskCenter.radius > 0) + { + m_center_x = roiRect.x + bestDiskCenter.x; + m_center_y = roiRect.y + bestDiskCenter.y; + m_radius = cvRound(bestDiskCenter.radius); + m_searchRegion = m_radius; + return true; + } + + return false; +} + +void GuiderPlanet::UpdateDetectionErrorInSimulator(Point2f& clickedPoint) +{ + if (pCamera && pCamera->Name == "Simulator") + { + bool errUnknown = true; + bool clicked = (m_prevClickedPoint != clickedPoint); + + if (m_detected) + { + if (m_cameraSimulationRefPointValid) + { + m_simulationZeroOffset = false; + m_cameraSimulationRefPointValid = false; + m_origPoint = Point2f(m_center_x, m_center_y); + } + else if (!m_simulationZeroOffset && !clicked) + { + Point2f delta = Point2f(m_center_x, m_center_y) - m_origPoint; + errUnknown = false; + } + } + + if (clicked) + { + RestartSimulatorErrorDetection(); + } + } +} + +// Find planet center of round/crescent shape in the given image +bool GuiderPlanet::FindPlanet(const usImage* pImage, bool autoSelect) +{ + m_PlanetWatchdog.Start(); + + // Default error status message + m_statusMsg = _("Object not found"); + + // Skip detection when paused + if (m_PlanetaryDetectionPaused) + { + m_syncLock.Lock(); + m_detected = false; + m_detectionCounter = 0; + m_diskContour.clear(); + m_syncLock.Unlock(); + return false; + } + + // Auto select star was requested + if (autoSelect) + { + m_clicked_x = 0; + m_clicked_y = 0; + m_roiClicked = false; + m_detectionCounter = 0; + RestartSimulatorErrorDetection(); + } + Point2f clickedPoint = Point2f(m_clicked_x, m_clicked_y); + + // Use ROI for CPU time optimization + bool roiActive = false; + int minRadius = (int)GetPlanetaryParam_minRadius(); + int maxRadius = (int)GetPlanetaryParam_maxRadius(); + int roiRadius = (int)(maxRadius * 3 / 2.0 + 0.5); + int roiOffsetX = 0; + int roiOffsetY = 0; + Mat FullFrame(pImage->Size.GetHeight(), pImage->Size.GetWidth(), CV_16UC1, pImage->ImageData); + + // Refuse to process images larger than 4096x4096 and request to use camera binning + if (FullFrame.cols > 4096 || FullFrame.rows > 4096) + { + Debug.Write(wxString::Format("Find planet: image is too large %dx%d\n", FullFrame.cols, FullFrame.rows)); + pFrame->Alert(_("ERROR: camera frame size exceeds maximum limit. Please apply binning to reduce the frame size."), wxICON_ERROR); + m_syncLock.Lock(); + m_detected = false; + m_detectionCounter = 0; + m_diskContour.clear(); + m_syncLock.Unlock(); + return false; + } + + Mat RoiFrame; + Rect roiRect(0, 0, pImage->Size.GetWidth(), pImage->Size.GetHeight()); + if (!autoSelect && GetRoiEnableState() && m_detected && + (m_center_x < m_frameWidth) && (m_center_y < m_frameHeight) && + (m_frameWidth == pImage->Size.GetWidth()) && (m_frameHeight == pImage->Size.GetHeight())) + { + roiOffsetX = wxMax(0, m_center_x - roiRadius); + roiOffsetY = wxMax(0, m_center_y - roiRadius); + int w = wxMin(roiRadius * 2, pImage->Size.GetWidth() - roiOffsetX); + int h = wxMin(roiRadius * 2, pImage->Size.GetHeight() - roiOffsetY); + roiRect = Rect(roiOffsetX, roiOffsetY, w, h); + RoiFrame = FullFrame(roiRect); + roiActive = true; + } + else + { + RoiFrame = FullFrame; + } + + // Make sure to use 8-bit gray image for feature detection + // pImage always has 16-bit pixels, but depending on camera bpp + // we should properly scale the image. + Mat img8; + int bppFactor = (pImage->BitsPerPixel >= 8) ? 1 << (pImage->BitsPerPixel - 8) : 1; + RoiFrame.convertTo(img8, CV_8U, 1.0 / bppFactor); + + // Save latest frame dimensions + m_frameWidth = pImage->Size.GetWidth(); + m_frameHeight = pImage->Size.GetHeight(); + + // ROI current state and limit + bool activeRoiLimits = m_roiClicked && GetRoiEnableState(); + float distanceRoiMax = maxRadius * 3 / 2.0; + + bool detectionResult = false; + try + { + // Do slight image blurring to decrease noise impact on results + Mat imgFiltered; + GaussianBlur(img8, imgFiltered, cv::Size(3, 3), 1.5); + + // Find planet center + detectionResult = FindPlanetCenter(imgFiltered, minRadius, maxRadius, roiActive, clickedPoint, roiRect, activeRoiLimits, distanceRoiMax); + + // Calculate sharpness of the image + if (m_measuringSharpnessMode) + m_focusSharpness = CalcSharpness(FullFrame, clickedPoint, detectionResult); + + if (detectionResult) + { + m_detected = true; + if (m_detectionCounter++ > 3) + { + // Smooth search region to avoid sudden jumps in star find stats + m_searchRegion = cvRound(m_searchRegion * 0.3 + m_prevSearchRegion * 0.7); + + // Forget about the clicked point after a few successful detections + m_roiClicked = false; + } + m_prevSearchRegion = m_searchRegion; + } + if (m_measuringSharpnessMode || detectionResult) + m_unknownHFD = false; + } + catch (const wxString& msg) + { + POSSIBLY_UNUSED(msg); + Debug.Write(wxString::Format("Find planet: exception %s\n", msg)); + } + catch (const cv::Exception& ex) + { + // Handle OpenCV exceptions + Debug.Write(wxString::Format("Find planet: OpenCV exception %s\n", ex.what())); + pFrame->Alert(_("ERROR: exception occurred during image processing: change detection parameters"), wxICON_ERROR); + } + catch (...) + { + // Handle any other exceptions + Debug.Write("Find planet: unknown exception\n"); + pFrame->Alert(_("ERROR: unknown exception occurred in planetary detection"), wxICON_ERROR); + } + + // For simulated camera, calculate detection error by comparing with the simulated position + UpdateDetectionErrorInSimulator(clickedPoint); + + // Update data shared with other thread + m_syncLock.Lock(); + m_roiRect = roiRect; + if (!detectionResult) + { + m_detected = false; + m_detectionCounter = 0; + m_diskContour.clear(); + } + m_roiActive = roiActive; + m_prevClickedPoint = clickedPoint; + m_syncLock.Unlock(); + + return detectionResult; +} \ No newline at end of file diff --git a/guider_planetary.h b/guider_planetary.h new file mode 100644 index 000000000..366bf3cde --- /dev/null +++ b/guider_planetary.h @@ -0,0 +1,165 @@ +/* + * guider_planetary.h + * PHD Guiding + * + * Planetary detection extensions by Leo Shatz + * Copyright (c) 2023-2024 Leo Shatz + * All rights reserved. + * + * This source code is distributed under the following "BSD" license + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of Craig Stark, Stark Labs nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#pragma once + +#include "opencv2/highgui.hpp" +#include "opencv2/imgproc.hpp" + +// Planetary guiding/tracking state and control class +class GuiderPlanet +{ +private: + // Planetary guiding parameters + bool m_Planetary_enabled; + bool m_PlanetaryDetectionPaused; + bool m_RoiEnabled; + bool m_prevCaptureActive; + + double m_Planetary_minRadius; + double m_Planetary_maxRadius; + int m_Planetary_lowThreshold; + int m_Planetary_highThreshold; + bool m_Planetary_ShowElementsButtonState; + bool m_Planetary_ShowElementsVisual; + + bool m_measuringSharpnessMode; + bool m_unknownHFD; + double m_focusSharpness; + int m_starProfileSize; + + float m_PlanetEccentricity; + float m_PlanetAngle; + + wxMutex m_syncLock; + cv::Point2f m_prevClickedPoint; + + std::vector m_diskContour; + int m_frameWidth; + int m_frameHeight; + + cv::Point2f m_origPoint; + cv::Point2f m_cameraSimulationMove; + cv::Point2f m_cameraSimulationRefPoint; + +public: + wxString m_statusMsg; + bool m_detected; + float m_center_x; + float m_center_y; + int m_radius; + int m_searchRegion; + float m_prevSearchRegion; + + bool m_roiActive; + cv::Rect m_roiRect; + bool m_roiClicked; + int m_clicked_x; + int m_clicked_y; + + int m_detectionCounter; + bool m_simulationZeroOffset; + bool m_cameraSimulationRefPointValid; + +public: + GuiderPlanet(); + ~GuiderPlanet(); + + bool FindPlanet(const usImage* pImage, bool autoSelect = false); + void RestartSimulatorErrorDetection(); + + double GetHFD(); + wxString GetHfdLabel(); + bool IsPixelMetrics(); + void ToggleSharpness(); + void GetDetectionStatus(wxString& statusMsg); + void NotifyCameraConnect(bool connected); + bool UpdateCaptureState(bool CaptureActive); + + bool GetPlanetaryEnableState() { return m_Planetary_enabled; } + void SetPlanetaryEnableState(bool enabled) { m_Planetary_enabled = enabled; } + bool GetDetectionPausedState() { return m_PlanetaryDetectionPaused; } + void SetDetectionPausedState(bool paused) { m_PlanetaryDetectionPaused = paused; } + void SetPlanetaryParam_minRadius(double val) { m_Planetary_minRadius = val; } + double GetPlanetaryParam_minRadius() { return m_Planetary_minRadius; } + void SetPlanetaryParam_maxRadius(double val) { m_Planetary_maxRadius = val; } + double GetPlanetaryParam_maxRadius() { return m_Planetary_maxRadius; } + bool GetRoiEnableState() { return m_RoiEnabled; } + void SetRoiEnableState(bool enabled) { m_RoiEnabled = enabled; } + void SetPlanetaryParam_lowThreshold(int value) { m_Planetary_lowThreshold = value; } + int GetPlanetaryParam_lowThreshold() { return m_Planetary_lowThreshold; } + void SetPlanetaryParam_highThreshold(int value) { m_Planetary_highThreshold = value; } + int GetPlanetaryParam_highThreshold() { return m_Planetary_highThreshold; } + + void SetPlanetaryElementsVisual(bool state); + bool GetPlanetaryElementsVisual() { return m_Planetary_ShowElementsVisual; } + void SetPlanetaryElementsButtonState(bool state) { m_Planetary_ShowElementsButtonState = state; } + bool GetPlanetaryElementsButtonState() { return m_Planetary_ShowElementsButtonState; } + +public: + // Displaying visual aid for planetary parameter tuning + bool m_draw_PlanetaryHelper; + void PlanetVisualRefresh() { m_draw_PlanetaryHelper = true; } + void PlanetVisualHelper(wxDC& dc, Star primaryStar, double scaleFactor); + +private: + wxStopWatch m_PlanetWatchdog; + typedef struct { + float x; + float y; + float radius; + } CircleDescriptor; + struct LineParameters { + bool valid; + bool vertical; + float slope; + float b; + } m_DiameterLineParameters; + typedef struct WeightedCircle { + float x; + float y; + float r; + float score; + } WeightedCircle; + +private: + double ComputeSobelSharpness(const cv::Mat& img); + double CalcSharpness(cv::Mat& FullFrame, cv::Point2f& clickedPoint, bool detectionResult); + void CalcLineParams(CircleDescriptor p1, CircleDescriptor p2); + int RefineDiskCenter(float& bestScore, CircleDescriptor& diskCenter, std::vector& diskContour, int minRadius, int maxRadius, float searchRadius, float resolution = 1.0); + float FindContourCenter(CircleDescriptor& diskCenter, CircleDescriptor& smallestCircle, std::vector& bestContourVector, cv::Moments& mu, int minRadius, int maxRadius); + void FindCenters(cv::Mat image, const std::vector& contour, CircleDescriptor& bestCentroid, CircleDescriptor& smallestCircle, std::vector& bestContour, cv::Moments& mu, int minRadius, int maxRadius); + bool FindPlanetCenter(cv::Mat img8, int minRadius, int maxRadius, bool roiActive, cv::Point2f& clickedPoint, cv::Rect& roiRect, bool activeRoiLimits, float distanceRoiMax); + void UpdateDetectionErrorInSimulator(cv::Point2f& clickedPoint); +}; diff --git a/icons/eclipse.png b/icons/eclipse.png new file mode 100644 index 000000000..b7fc92a66 Binary files /dev/null and b/icons/eclipse.png differ diff --git a/icons/eclipse.png.h b/icons/eclipse.png.h new file mode 100644 index 000000000..8c25f66b6 --- /dev/null +++ b/icons/eclipse.png.h @@ -0,0 +1,322 @@ +/* eclipse.png - 2556 bytes */ +static const unsigned char eclipse_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a, + 0xf4, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, + 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, + 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, + 0x06, 0x7a, 0x69, 0x54, 0x58, 0x74, 0x58, 0x4d, + 0x4c, 0x3a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64, + 0x6f, 0x62, 0x65, 0x2e, 0x78, 0x6d, 0x70, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3c, 0x3f, 0x78, 0x70, + 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65, + 0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf, + 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x57, 0x35, + 0x4d, 0x30, 0x4d, 0x70, 0x43, 0x65, 0x68, 0x69, + 0x48, 0x7a, 0x72, 0x65, 0x53, 0x7a, 0x4e, 0x54, + 0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x22, 0x3f, + 0x3e, 0x20, 0x3c, 0x78, 0x3a, 0x78, 0x6d, 0x70, + 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c, + 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, + 0x6f, 0x62, 0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, + 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20, 0x78, 0x3a, + 0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x41, + 0x64, 0x6f, 0x62, 0x65, 0x20, 0x58, 0x4d, 0x50, + 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x39, 0x2e, + 0x31, 0x2d, 0x63, 0x30, 0x30, 0x31, 0x20, 0x37, + 0x39, 0x2e, 0x31, 0x34, 0x36, 0x32, 0x38, 0x39, + 0x39, 0x2c, 0x20, 0x32, 0x30, 0x32, 0x33, 0x2f, + 0x30, 0x36, 0x2f, 0x32, 0x35, 0x2d, 0x32, 0x30, + 0x3a, 0x30, 0x31, 0x3a, 0x35, 0x35, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x3e, + 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, + 0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, + 0x72, 0x64, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, + 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, + 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, + 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, + 0x32, 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, + 0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, + 0x22, 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x64, 0x66, 0x3a, + 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, + 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, + 0x6d, 0x70, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, + 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, + 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, + 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, + 0x64, 0x63, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, + 0x3a, 0x2f, 0x2f, 0x70, 0x75, 0x72, 0x6c, 0x2e, + 0x6f, 0x72, 0x67, 0x2f, 0x64, 0x63, 0x2f, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, + 0x31, 0x2e, 0x31, 0x2f, 0x22, 0x20, 0x78, 0x6d, + 0x6c, 0x6e, 0x73, 0x3a, 0x70, 0x68, 0x6f, 0x74, + 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3d, 0x22, 0x68, + 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, + 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x70, 0x68, 0x6f, 0x74, 0x6f, + 0x73, 0x68, 0x6f, 0x70, 0x2f, 0x31, 0x2e, 0x30, + 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, + 0x3a, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3d, 0x22, + 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, + 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, + 0x31, 0x2e, 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x22, + 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x73, + 0x74, 0x45, 0x76, 0x74, 0x3d, 0x22, 0x68, 0x74, + 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, + 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, + 0x30, 0x2f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x2f, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x23, 0x22, 0x20, + 0x78, 0x6d, 0x70, 0x3a, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x6f, 0x72, 0x54, 0x6f, 0x6f, 0x6c, 0x3d, + 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50, + 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, + 0x20, 0x32, 0x35, 0x2e, 0x30, 0x20, 0x28, 0x57, + 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x29, 0x22, + 0x20, 0x78, 0x6d, 0x70, 0x3a, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x65, 0x3d, + 0x22, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x31, 0x32, + 0x2d, 0x32, 0x35, 0x54, 0x32, 0x32, 0x3a, 0x33, + 0x38, 0x3a, 0x35, 0x31, 0x2b, 0x30, 0x32, 0x3a, + 0x30, 0x30, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x3a, + 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x44, 0x61, + 0x74, 0x65, 0x3d, 0x22, 0x32, 0x30, 0x32, 0x33, + 0x2d, 0x31, 0x32, 0x2d, 0x32, 0x37, 0x54, 0x31, + 0x36, 0x3a, 0x34, 0x37, 0x3a, 0x33, 0x34, 0x2b, + 0x30, 0x32, 0x3a, 0x30, 0x30, 0x22, 0x20, 0x78, + 0x6d, 0x70, 0x3a, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x44, 0x61, 0x74, 0x65, 0x3d, + 0x22, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x31, 0x32, + 0x2d, 0x32, 0x37, 0x54, 0x31, 0x36, 0x3a, 0x34, + 0x37, 0x3a, 0x33, 0x34, 0x2b, 0x30, 0x32, 0x3a, + 0x30, 0x30, 0x22, 0x20, 0x64, 0x63, 0x3a, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x3d, 0x22, 0x69, + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, + 0x22, 0x20, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, + 0x68, 0x6f, 0x70, 0x3a, 0x43, 0x6f, 0x6c, 0x6f, + 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x3d, 0x22, 0x33, + 0x22, 0x20, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, + 0x68, 0x6f, 0x70, 0x3a, 0x48, 0x69, 0x73, 0x74, + 0x6f, 0x72, 0x79, 0x3d, 0x22, 0x32, 0x30, 0x32, + 0x33, 0x2d, 0x31, 0x32, 0x2d, 0x32, 0x37, 0x54, + 0x31, 0x36, 0x3a, 0x34, 0x35, 0x3a, 0x35, 0x31, + 0x2b, 0x30, 0x32, 0x3a, 0x30, 0x30, 0x26, 0x23, + 0x78, 0x39, 0x3b, 0x46, 0x69, 0x6c, 0x65, 0x20, + 0x66, 0x61, 0x76, 0x70, 0x6e, 0x67, 0x5f, 0x73, + 0x6f, 0x6c, 0x61, 0x72, 0x2d, 0x65, 0x63, 0x6c, + 0x69, 0x70, 0x73, 0x65, 0x2e, 0x70, 0x6e, 0x67, + 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x26, + 0x23, 0x78, 0x41, 0x3b, 0x32, 0x30, 0x32, 0x33, + 0x2d, 0x31, 0x32, 0x2d, 0x32, 0x37, 0x54, 0x31, + 0x36, 0x3a, 0x34, 0x37, 0x3a, 0x33, 0x34, 0x2b, + 0x30, 0x32, 0x3a, 0x30, 0x30, 0x26, 0x23, 0x78, + 0x39, 0x3b, 0x46, 0x69, 0x6c, 0x65, 0x20, 0x43, + 0x3a, 0x5c, 0x55, 0x73, 0x65, 0x72, 0x73, 0x5c, + 0x4c, 0x65, 0x6f, 0x5c, 0x42, 0x69, 0x6e, 0x54, + 0x6f, 0x41, 0x73, 0x63, 0x69, 0x69, 0x43, 0x5c, + 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, + 0x76, 0x33, 0x2e, 0x70, 0x6e, 0x67, 0x20, 0x73, + 0x61, 0x76, 0x65, 0x64, 0x26, 0x23, 0x78, 0x41, + 0x3b, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, + 0x3a, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, + 0x2e, 0x69, 0x69, 0x64, 0x3a, 0x65, 0x37, 0x33, + 0x64, 0x61, 0x65, 0x37, 0x33, 0x2d, 0x66, 0x35, + 0x33, 0x64, 0x2d, 0x65, 0x30, 0x34, 0x33, 0x2d, + 0x39, 0x65, 0x63, 0x36, 0x2d, 0x61, 0x63, 0x62, + 0x38, 0x35, 0x62, 0x31, 0x61, 0x64, 0x33, 0x39, + 0x65, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, + 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x49, 0x44, 0x3d, 0x22, 0x61, 0x64, 0x6f, + 0x62, 0x65, 0x3a, 0x64, 0x6f, 0x63, 0x69, 0x64, + 0x3a, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, + 0x6f, 0x70, 0x3a, 0x36, 0x61, 0x36, 0x35, 0x66, + 0x31, 0x39, 0x38, 0x2d, 0x33, 0x37, 0x62, 0x32, + 0x2d, 0x37, 0x64, 0x34, 0x34, 0x2d, 0x38, 0x31, + 0x31, 0x39, 0x2d, 0x61, 0x39, 0x32, 0x62, 0x65, + 0x36, 0x39, 0x64, 0x61, 0x63, 0x62, 0x39, 0x22, + 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, + 0x69, 0x64, 0x3a, 0x34, 0x64, 0x36, 0x34, 0x38, + 0x36, 0x62, 0x38, 0x2d, 0x33, 0x32, 0x37, 0x37, + 0x2d, 0x35, 0x62, 0x34, 0x33, 0x2d, 0x61, 0x64, + 0x37, 0x37, 0x2d, 0x65, 0x35, 0x33, 0x63, 0x63, + 0x39, 0x63, 0x65, 0x30, 0x32, 0x61, 0x34, 0x22, + 0x3e, 0x20, 0x3c, 0x78, 0x6d, 0x70, 0x4d, 0x4d, + 0x3a, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x53, + 0x65, 0x71, 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, + 0x3a, 0x6c, 0x69, 0x20, 0x73, 0x74, 0x45, 0x76, + 0x74, 0x3a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x3d, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, + 0x3a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, + 0x2e, 0x69, 0x69, 0x64, 0x3a, 0x34, 0x64, 0x36, + 0x34, 0x38, 0x36, 0x62, 0x38, 0x2d, 0x33, 0x32, + 0x37, 0x37, 0x2d, 0x35, 0x62, 0x34, 0x33, 0x2d, + 0x61, 0x64, 0x37, 0x37, 0x2d, 0x65, 0x35, 0x33, + 0x63, 0x63, 0x39, 0x63, 0x65, 0x30, 0x32, 0x61, + 0x34, 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, + 0x3a, 0x77, 0x68, 0x65, 0x6e, 0x3d, 0x22, 0x32, + 0x30, 0x32, 0x33, 0x2d, 0x31, 0x32, 0x2d, 0x32, + 0x35, 0x54, 0x32, 0x32, 0x3a, 0x33, 0x38, 0x3a, + 0x35, 0x31, 0x2b, 0x30, 0x32, 0x3a, 0x30, 0x30, + 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, + 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x3d, 0x22, 0x41, + 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50, 0x68, 0x6f, + 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x32, + 0x35, 0x2e, 0x30, 0x20, 0x28, 0x57, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x73, 0x29, 0x22, 0x2f, 0x3e, + 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x6c, 0x69, + 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x73, + 0x61, 0x76, 0x65, 0x64, 0x22, 0x20, 0x73, 0x74, + 0x45, 0x76, 0x74, 0x3a, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x49, 0x44, 0x3d, 0x22, + 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69, 0x64, 0x3a, + 0x65, 0x37, 0x33, 0x64, 0x61, 0x65, 0x37, 0x33, + 0x2d, 0x66, 0x35, 0x33, 0x64, 0x2d, 0x65, 0x30, + 0x34, 0x33, 0x2d, 0x39, 0x65, 0x63, 0x36, 0x2d, + 0x61, 0x63, 0x62, 0x38, 0x35, 0x62, 0x31, 0x61, + 0x64, 0x33, 0x39, 0x65, 0x22, 0x20, 0x73, 0x74, + 0x45, 0x76, 0x74, 0x3a, 0x77, 0x68, 0x65, 0x6e, + 0x3d, 0x22, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x31, + 0x32, 0x2d, 0x32, 0x37, 0x54, 0x31, 0x36, 0x3a, + 0x34, 0x37, 0x3a, 0x33, 0x34, 0x2b, 0x30, 0x32, + 0x3a, 0x30, 0x30, 0x22, 0x20, 0x73, 0x74, 0x45, + 0x76, 0x74, 0x3a, 0x73, 0x6f, 0x66, 0x74, 0x77, + 0x61, 0x72, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x3d, 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, + 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, + 0x70, 0x20, 0x32, 0x35, 0x2e, 0x30, 0x20, 0x28, + 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x29, + 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, + 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x3d, + 0x22, 0x2f, 0x22, 0x2f, 0x3e, 0x20, 0x3c, 0x2f, + 0x72, 0x64, 0x66, 0x3a, 0x53, 0x65, 0x71, 0x3e, + 0x20, 0x3c, 0x2f, 0x78, 0x6d, 0x70, 0x4d, 0x4d, + 0x3a, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x3e, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x3e, 0x20, 0x3c, 0x2f, 0x72, + 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x3e, 0x20, + 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, + 0x65, 0x74, 0x61, 0x3e, 0x20, 0x3c, 0x3f, 0x78, + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x65, + 0x6e, 0x64, 0x3d, 0x22, 0x72, 0x22, 0x3f, 0x3e, + 0x0c, 0xb4, 0x8e, 0xbf, 0x00, 0x00, 0x03, 0x28, + 0x49, 0x44, 0x41, 0x54, 0x58, 0x85, 0xc5, 0x57, + 0x3f, 0x48, 0x3a, 0x71, 0x1c, 0x7d, 0x45, 0x49, + 0x5a, 0x42, 0x50, 0x48, 0x1e, 0x65, 0x21, 0x34, + 0xa4, 0x44, 0x2d, 0x0d, 0xe5, 0x50, 0xd1, 0x20, + 0x35, 0x06, 0xfd, 0x5b, 0x82, 0xa2, 0x16, 0xa1, + 0x20, 0x68, 0x75, 0x68, 0x68, 0x70, 0x2b, 0x68, + 0x0e, 0x0c, 0x32, 0xa2, 0x1a, 0xda, 0x2a, 0x22, + 0x68, 0xb1, 0x29, 0x6a, 0xd2, 0x42, 0x2c, 0x28, + 0x15, 0xa1, 0x3c, 0xc2, 0xc3, 0xe2, 0x24, 0x78, + 0xbf, 0x21, 0xa4, 0xec, 0xee, 0xe2, 0xd4, 0xfa, + 0xf9, 0xe0, 0x40, 0xde, 0xf7, 0x7b, 0xdf, 0xf7, + 0xf8, 0xde, 0xe7, 0x9f, 0x15, 0x24, 0xe3, 0x00, + 0xcc, 0x28, 0x0f, 0xa4, 0x0a, 0x92, 0x2c, 0x93, + 0x38, 0x00, 0xa0, 0x12, 0x80, 0x54, 0x46, 0x7d, + 0xa9, 0x52, 0xef, 0x4e, 0xbf, 0xdf, 0x8f, 0x40, + 0x20, 0xa0, 0xe0, 0xdf, 0xdf, 0xdf, 0xe1, 0xf3, + 0xf9, 0x90, 0x4c, 0x26, 0x8b, 0xb3, 0x40, 0x32, + 0x4d, 0x1d, 0x98, 0x99, 0x99, 0x61, 0x63, 0x63, + 0xa3, 0x82, 0x3f, 0x3d, 0x3d, 0x25, 0x00, 0xa6, + 0x52, 0x29, 0x3d, 0xc7, 0x7c, 0x47, 0x5a, 0xb7, + 0x81, 0x68, 0x34, 0x4a, 0x00, 0x0c, 0x87, 0xc3, + 0x79, 0xbc, 0xdb, 0xed, 0x66, 0x57, 0x57, 0x57, + 0x31, 0xe2, 0x24, 0x99, 0xae, 0xfa, 0x7e, 0x23, + 0xcf, 0xcf, 0xcf, 0x38, 0x3b, 0x3b, 0xc3, 0xf8, + 0xf8, 0x78, 0x1e, 0x6f, 0xb7, 0xdb, 0xe1, 0xf1, + 0x78, 0x60, 0x30, 0x18, 0xf2, 0x78, 0x97, 0xcb, + 0x85, 0x96, 0x96, 0x96, 0xe2, 0xae, 0x1f, 0x40, + 0x05, 0xc9, 0x34, 0xbe, 0xa4, 0x61, 0x32, 0x99, + 0x44, 0x73, 0x73, 0x33, 0xc6, 0xc6, 0xc6, 0xb0, + 0xb3, 0xb3, 0x93, 0xb7, 0x39, 0x93, 0xc9, 0xc0, + 0xef, 0xf7, 0xe3, 0xfa, 0xfa, 0x1a, 0x6f, 0x6f, + 0x6f, 0xb0, 0x5a, 0xad, 0x18, 0x19, 0x19, 0xc1, + 0xc0, 0xc0, 0x00, 0x00, 0x40, 0x92, 0x24, 0x04, + 0x83, 0x41, 0x44, 0x22, 0x11, 0xc8, 0xb2, 0x0c, + 0x8b, 0xc5, 0x82, 0xee, 0xee, 0x6e, 0x74, 0x76, + 0x76, 0x6a, 0xe9, 0x4b, 0x0a, 0x03, 0x00, 0xf0, + 0xf8, 0xf8, 0x08, 0x9b, 0xcd, 0x86, 0xa9, 0xa9, + 0x29, 0x04, 0x02, 0x01, 0x88, 0xa2, 0x88, 0xe9, + 0xe9, 0x69, 0x84, 0x42, 0x21, 0xd4, 0xd7, 0xd7, + 0x43, 0x10, 0x04, 0x54, 0x57, 0x57, 0xe3, 0xe5, + 0xe5, 0x05, 0x06, 0x83, 0x01, 0xb5, 0xb5, 0xb5, + 0xb0, 0x58, 0x2c, 0xd8, 0xdb, 0xdb, 0x83, 0x28, + 0x8a, 0x0a, 0x95, 0xde, 0xde, 0x5e, 0x2c, 0x2f, + 0x2f, 0x63, 0x74, 0x74, 0x54, 0x61, 0x40, 0x33, + 0x06, 0x6e, 0x6f, 0x6f, 0x79, 0x78, 0x78, 0xc8, + 0x93, 0x93, 0x13, 0x02, 0xa0, 0xdb, 0xed, 0xe6, + 0xd5, 0xd5, 0x95, 0x62, 0x5f, 0x30, 0x18, 0x24, + 0x00, 0x5d, 0xcf, 0xdc, 0xdc, 0x9c, 0x22, 0x06, + 0x7e, 0x0c, 0xc2, 0x78, 0x3c, 0x4e, 0x00, 0x5c, + 0x5f, 0x5f, 0x57, 0x5d, 0x0f, 0x85, 0x42, 0xac, + 0xa9, 0xa9, 0xd1, 0x6d, 0x00, 0x00, 0x27, 0x27, + 0x27, 0xf5, 0x19, 0x90, 0x65, 0x99, 0x00, 0xb8, + 0xba, 0xba, 0xaa, 0xe5, 0x8f, 0x56, 0xab, 0xb5, + 0x20, 0xf1, 0xdc, 0xb3, 0xb9, 0xb9, 0x99, 0x6f, + 0xe0, 0xfc, 0xfc, 0x9c, 0x3d, 0x3d, 0x3d, 0x5c, + 0x58, 0x58, 0xe0, 0xd1, 0xd1, 0x11, 0x49, 0x72, + 0x69, 0x69, 0x89, 0x82, 0x20, 0x68, 0x8a, 0x6f, + 0x6c, 0x6c, 0x14, 0x25, 0x0e, 0x80, 0x46, 0xa3, + 0x91, 0xb2, 0x2c, 0x7f, 0x1a, 0x88, 0xc5, 0x62, + 0xf4, 0x7a, 0xbd, 0xec, 0xeb, 0xeb, 0xa3, 0xd3, + 0xe9, 0xa4, 0x28, 0x8a, 0x74, 0x38, 0x1c, 0xdc, + 0xda, 0xda, 0xd2, 0x34, 0xd0, 0xd1, 0xd1, 0x51, + 0xb4, 0x01, 0x00, 0xdc, 0xdd, 0xdd, 0xd5, 0xfe, + 0x04, 0x97, 0x97, 0x97, 0xb4, 0xd9, 0x6c, 0xcc, + 0x64, 0x32, 0xaa, 0xe2, 0x89, 0x44, 0xa2, 0x24, + 0x71, 0x00, 0x9c, 0x9f, 0x9f, 0x27, 0xc9, 0xb4, + 0x6a, 0x2f, 0x88, 0x44, 0x22, 0x30, 0x9b, 0xcd, + 0x30, 0x99, 0x4c, 0x6a, 0xcb, 0xb8, 0xbf, 0xbf, + 0x57, 0xe5, 0x0b, 0x41, 0x2c, 0x16, 0x03, 0xf0, + 0xd1, 0x0d, 0xcb, 0x0a, 0x55, 0x03, 0xed, 0xed, + 0xed, 0x90, 0x24, 0x09, 0xaf, 0xaf, 0xaf, 0xaa, + 0x2f, 0xd9, 0xed, 0xf6, 0x92, 0x85, 0x73, 0xe5, + 0xbb, 0x12, 0x00, 0xe2, 0xf1, 0x38, 0xbc, 0x5e, + 0x2f, 0x5c, 0x2e, 0x17, 0x9c, 0x4e, 0x27, 0xda, + 0xda, 0xda, 0x50, 0x57, 0x57, 0x87, 0x83, 0x83, + 0x03, 0xd5, 0x97, 0x9b, 0x9a, 0x9a, 0xe0, 0x70, + 0x38, 0x4a, 0x32, 0x30, 0x34, 0x34, 0xf4, 0xf1, + 0xe3, 0x6b, 0x1a, 0x2e, 0x2e, 0x2e, 0xf2, 0xf8, + 0xf8, 0xf8, 0xcf, 0xd3, 0xd0, 0x64, 0x32, 0x31, + 0x9b, 0xcd, 0x6a, 0x67, 0x01, 0xa9, 0xaf, 0x10, + 0x09, 0x82, 0xf0, 0x3b, 0x85, 0x48, 0x4b, 0x20, + 0x57, 0x8a, 0xd7, 0xd6, 0xd6, 0x54, 0xd7, 0xc3, + 0xe1, 0x30, 0x8d, 0x46, 0xe3, 0xdf, 0x94, 0xe2, + 0x9b, 0x9b, 0x1b, 0x5d, 0xcd, 0xe8, 0xe2, 0xe2, + 0xa2, 0xa4, 0x66, 0xa4, 0xda, 0x8e, 0x1f, 0x1e, + 0x1e, 0xd0, 0xda, 0xda, 0x5a, 0x70, 0x3b, 0xde, + 0xdf, 0xdf, 0x47, 0x2a, 0x95, 0x52, 0x04, 0xdc, + 0x4f, 0xed, 0x58, 0x73, 0x20, 0x99, 0x98, 0x98, + 0xc0, 0xf6, 0xf6, 0x76, 0xde, 0x6e, 0xb5, 0x81, + 0x64, 0x78, 0x78, 0x18, 0x83, 0x83, 0x83, 0x1f, + 0xa7, 0x15, 0x31, 0x90, 0x28, 0x3e, 0xc1, 0xd3, + 0xd3, 0x53, 0xae, 0x4e, 0x2b, 0xe0, 0xf1, 0x78, + 0x18, 0x8d, 0x46, 0xf3, 0xb8, 0x95, 0x95, 0x95, + 0xaf, 0x41, 0x55, 0x28, 0xf4, 0x0f, 0xa5, 0x77, + 0x77, 0x77, 0x04, 0x7e, 0x7f, 0x28, 0xd5, 0x6d, + 0x60, 0x76, 0x76, 0x96, 0x0d, 0x0d, 0x0d, 0x0a, + 0xbe, 0xd4, 0xb1, 0x5c, 0x31, 0x15, 0x6b, 0xa1, + 0xbf, 0xbf, 0xff, 0xb3, 0x7a, 0x7d, 0xe3, 0x7d, + 0x3e, 0x1f, 0xb2, 0xd9, 0xac, 0xde, 0xa3, 0xf2, + 0xa0, 0x9a, 0x05, 0xff, 0x11, 0xe5, 0xff, 0x73, + 0x5a, 0x05, 0x20, 0x81, 0x32, 0xde, 0xc0, 0x3f, + 0x46, 0x34, 0x33, 0x71, 0x1d, 0x0c, 0x85, 0xf8, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, + 0xae, 0x42, 0x60, 0x82 }; diff --git a/myframe.cpp b/myframe.cpp index 7d7d0b45c..b5ac123a1 100644 --- a/myframe.cpp +++ b/myframe.cpp @@ -38,6 +38,7 @@ #include "aui_controls.h" #include "comet_tool.h" +#include "planetary_tool.h" #include "config_indi.h" #include "guiding_assistant.h" #include "phdupdate.h" @@ -95,6 +96,7 @@ wxBEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_MENU(MENU_POLARDRIFTTOOL, MyFrame::OnPolarDriftTool) EVT_MENU(MENU_STATICPATOOL, MyFrame::OnStaticPaTool) EVT_MENU(MENU_COMETTOOL, MyFrame::OnCometTool) + EVT_MENU(MENU_PLANETARY, MyFrame::OnPlanetTool) EVT_MENU(MENU_GUIDING_ASSISTANT, MyFrame::OnGuidingAssistant) EVT_MENU(MENU_HELP_UPGRADE, MyFrame::OnUpgrade) EVT_MENU(MENU_HELP_ONLINE, MyFrame::OnHelpOnline) @@ -147,6 +149,7 @@ wxBEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_TOOL(BUTTON_AUTOSTAR, MyFrame::OnButtonAutoStar) EVT_MENU(MENU_STOP, MyFrame::OnButtonStop) EVT_TOOL(BUTTON_ADVANCED, MyFrame::OnAdvanced) + EVT_TOOL(BUTTON_PLANETARY, MyFrame::OnPlanetTool) EVT_MENU(MENU_BRAIN, MyFrame::OnAdvanced) EVT_TOOL(BUTTON_GUIDE,MyFrame::OnButtonGuide) EVT_MENU(MENU_GUIDE,MyFrame::OnButtonGuide) @@ -210,6 +213,7 @@ struct FileDropTarget : public wxFileDropTarget MyFrame::MyFrame() : wxFrame(nullptr, wxID_ANY, wxEmptyString), + pPlanetTool(nullptr), m_showBookmarksAccel(0), m_bookmarkLockPosAccel(0), pStatsWin(nullptr) @@ -360,6 +364,7 @@ MyFrame::MyFrame() pStarCrossDlg = nullptr; pNudgeLock = nullptr; pCometTool = nullptr; + pPlanetTool = nullptr; pGuidingAssistant = nullptr; pRefineDefMap = nullptr; pCalSanityCheckDlg = nullptr; @@ -367,6 +372,7 @@ MyFrame::MyFrame() pCalibrationAssistant = nullptr; pierFlipToolWin = nullptr; m_starFindMode = Star::FIND_CENTROID; + m_StarFindMode_Saved = m_starFindMode; m_rawImageMode = false; m_rawImageModeWarningDone = false; @@ -467,6 +473,8 @@ MyFrame::~MyFrame() pStarCrossDlg->Destroy(); if (pierFlipToolWin) pierFlipToolWin->Destroy(); + if (pPlanetTool) + pPlanetTool->Destroy(); m_mgr.UnInit(); @@ -520,6 +528,7 @@ void MyFrame::SetupMenuBar() tools_menu->Append(EEGG_MANUALLOCK, _("Adjust &Lock Position"), _("Adjust the lock position")); tools_menu->Append(MENU_COMETTOOL, _("&Comet Tracking"), _("Run the Comet Tracking tool")); + m_PlanetaryMenuItem = tools_menu->AppendCheckItem(MENU_PLANETARY, _("&Planetary Guiding\tCtrl-P"), _("Run the Planetary Guiding tool")); tools_menu->Append(MENU_STARCROSS_TEST, _("Star-Cross Test"), _("Run a star-cross test for mount diagnostics")); tools_menu->Append(MENU_PIERFLIP_TOOL, _("Calibrate meridian flip"), _("Automatically determine the correct meridian flip settings")); tools_menu->Append(MENU_GUIDING_ASSISTANT, _("&Guiding Assistant"), _("Run the Guiding Assistant")); @@ -638,7 +647,7 @@ int MyFrame::GetExposureDelay() void MyFrame::SetComboBoxWidth(wxComboBox *pComboBox, unsigned int extra) { unsigned int i; - int width=-1; + int width = GetTextWidth(pComboBox, _("Custom: 0.999 s")); for (i = 0; i < pComboBox->GetCount(); i++) { @@ -677,18 +686,21 @@ bool MyFrame::SetCustomExposureDuration(int ms) return false; } -void MyFrame::GetExposureInfo(int *currExpMs, bool *autoExp) const +bool MyFrame::GetExposureInfo(int *currExpMs, bool *autoExp) const { + bool bEerr = false; if (!pCamera || !pCamera->Connected) { *currExpMs = 0; *autoExp = false; + bEerr = true; } else { *currExpMs = m_exposureDuration; *autoExp = m_autoExp.enabled; } + return bEerr; } static int dur_index(int duration) @@ -699,7 +711,7 @@ static int dur_index(int duration) return -1; } -bool MyFrame::SetExposureDuration(int val) +bool MyFrame::SetExposureDuration(int val, bool updateCustom) { if (val < 0) { @@ -710,7 +722,15 @@ bool MyFrame::SetExposureDuration(int val) { int idx = dur_index(val); if (idx == -1) - return false; + { + if (updateCustom) + { + SetCustomExposureDuration(val); + idx = dur_index(val); + } + if (idx == -1) + return false; + } Dur_Choice->SetSelection(idx + 1); // skip Auto } @@ -801,6 +821,16 @@ wxString MyFrame::ExposureDurationLabel(int duration) return wxString::Format(_("%.*f s"), digits, (double) duration / 1000.); } +void MyFrame::SaveStarFindMode() +{ + m_StarFindMode_Saved = m_starFindMode; +} + +void MyFrame::RestoreStarFindMode() +{ + m_starFindMode = m_StarFindMode_Saved; +} + Star::FindMode MyFrame::SetStarFindMode(Star::FindMode mode) { Star::FindMode prev = m_starFindMode; @@ -972,6 +1002,11 @@ void MyFrame::SetupToolBar() # include "icons/cam_setup_disabled.png.h" wxBitmap cam_setup_bmp_disabled(wxBITMAP_PNG_FROM_DATA(cam_setup_disabled)); + // This mage sourced from https://www.pngegg.com/en/png-zffyt + // The image is used under its "Non-commercial use, DMCA" terms. +# include "icons/eclipse.png.h" + wxBitmap eclipse_bmp(wxBITMAP_PNG_FROM_DATA(eclipse)); + int dur_values[] = { 10, 20, 50, 100, 200, 500, 1000, 1500, @@ -992,7 +1027,7 @@ void MyFrame::SetupToolBar() Dur_Choice = new wxComboBox(MainToolbar, BUTTON_DURATION, wxEmptyString, wxDefaultPosition, wxDefaultSize, durs, wxCB_READONLY); Dur_Choice->SetToolTip(_("Camera exposure duration")); - SetComboBoxWidth(Dur_Choice, 10); + SetComboBoxWidth(Dur_Choice, 35); Gamma_Slider = new wxSlider(MainToolbar, CTRL_GAMMA, GAMMA_DEFAULT, GAMMA_MIN, GAMMA_MAX, wxPoint(-1,-1), wxSize(160,-1)); Gamma_Slider->SetBackgroundColour(wxColor(60, 60, 60)); // Slightly darker than toolbar background @@ -1009,6 +1044,7 @@ void MyFrame::SetupToolBar() MainToolbar->AddSeparator(); MainToolbar->AddTool(BUTTON_ADVANCED, _("Advanced Settings"), brain_bmp, _("Advanced Settings")); MainToolbar->AddTool(BUTTON_CAM_PROPERTIES, cam_setup_bmp, cam_setup_bmp_disabled, false, 0, _("Camera settings")); + MainToolbar->AddTool(BUTTON_PLANETARY, _("Planetary Guiding"), eclipse_bmp, _("Planetary Guiding")); MainToolbar->EnableTool(BUTTON_CAM_PROPERTIES, false); MainToolbar->EnableTool(BUTTON_LOOP, false); MainToolbar->EnableTool(BUTTON_AUTOSTAR, false); @@ -1093,9 +1129,22 @@ static bool cond_update_tool(wxAuiToolBar *tb, int toolId, wxMenuItem *mi, bool return ret; } +void MyFrame::UpdateCameraSettings() +{ + eventLock.Lock(); + if (pPlanetTool) + { + wxCommandEvent event(APPSTATE_NOTIFY_EVENT, GetId()); + event.SetEventObject(this); + wxPostEvent(pPlanetTool, event); + } + eventLock.Unlock(); +} + void MyFrame::UpdateButtonsStatus() { assert(wxThread::IsMain()); + assert(pGuider); bool need_update = false; @@ -1122,7 +1171,7 @@ void MyFrame::UpdateButtonsStatus() need_update = true; } - bool guiding_active = pGuider && pGuider->IsCalibratingOrGuiding(); // Not the same as 'bGuideable below + bool guiding_active = pGuider->IsCalibratingOrGuiding(); // Not the same as 'bGuideable below if (!guiding_active ^ m_autoSelectStarMenuItem->IsEnabled()) { @@ -1187,6 +1236,9 @@ void MyFrame::UpdateButtonsStatus() if (pierFlipToolWin) PierFlipTool::UpdateUIControls(); + if (pGuider->m_Planet.UpdateCaptureState(CaptureActive)) + need_update = true; + if (need_update) { if (pGuider->GetState() < STATE_SELECTED) @@ -2705,6 +2757,8 @@ bool MyFrame::SetTimeLapse(int timeLapse) pConfig->Profile.SetInt("/frame/timeLapse", m_timeLapse); + UpdateCameraSettings(); + return bError; } diff --git a/myframe.h b/myframe.h index b906d2934..c403010bd 100644 --- a/myframe.h +++ b/myframe.h @@ -178,11 +178,13 @@ class MyFrame : public wxFrame int GetTimeLapse() const; int GetExposureDelay(); + wxMutex eventLock; bool SetFocalLength(int focalLength); friend class MyFrameConfigDialogPane; friend class MyFrameConfigDialogCtrlSet; friend class WorkerThread; + friend class PlanetToolWin; private: @@ -224,6 +226,7 @@ class MyFrame : public wxFrame wxMenuItem *m_cameraMenuItem; wxMenuItem *m_autoSelectStarMenuItem; wxMenuItem *m_takeDarksMenuItem; + wxMenuItem *m_PlanetaryMenuItem; wxMenuItem *m_useDarksMenuItem; wxMenuItem *m_refineDefMapMenuItem; wxMenuItem *m_useDefectMapMenuItem; @@ -250,6 +253,7 @@ class MyFrame : public wxFrame wxDialog *pStarCrossDlg; wxWindow *pNudgeLock; wxWindow *pCometTool; + wxWindow *pPlanetTool; wxWindow *pGuidingAssistant; wxWindow *pierFlipToolWin; RefineDefMap *pRefineDefMap; @@ -263,6 +267,7 @@ class MyFrame : public wxFrame wxDateTime m_guidingStarted; wxStopWatch m_guidingElapsed; Star::FindMode m_starFindMode; + Star::FindMode m_StarFindMode_Saved; double m_minStarHFD; bool m_rawImageMode; bool m_rawImageModeWarningDone; @@ -306,6 +311,7 @@ class MyFrame : public wxFrame void OnStaticPaTool(wxCommandEvent& evt); void OnCalibrationAssistant(wxCommandEvent& evt); void OnCometTool(wxCommandEvent& evt); + void OnPlanetTool(wxCommandEvent& evt); void OnGuidingAssistant(wxCommandEvent& evt); void OnSetupCamera(wxCommandEvent& evt); void OnExposureDurationSelected(wxCommandEvent& evt); @@ -350,8 +356,8 @@ class MyFrame : public wxFrame const std::vector& GetExposureDurations() const; bool SetCustomExposureDuration(int ms); - void GetExposureInfo(int *currExpMs, bool *autoExp) const; - bool SetExposureDuration(int val); + bool GetExposureInfo(int *currExpMs, bool *autoExp) const; + bool SetExposureDuration(int val, bool updateCustom = false); const AutoExposureCfg& GetAutoExposureCfg() const { return m_autoExp; } bool SetAutoExposureCfg(int minExp, int maxExp, double targetSNR); void ResetAutoExposure(); @@ -366,6 +372,9 @@ class MyFrame : public wxFrame static double GetDitherAmount(int ditherType); Star::FindMode GetStarFindMode() const; Star::FindMode SetStarFindMode(Star::FindMode mode); + void SaveStarFindMode(); + void RestoreStarFindMode(); + bool GetRawImageMode() const; bool SetRawImageMode(bool force); @@ -430,6 +439,7 @@ class MyFrame : public wxFrame void NotifyUpdateButtonsStatus(); // can be called from any thread void UpdateButtonsStatus(); + void UpdateCameraSettings(); static double GetPixelScale(double pixelSizeMicrons, int focalLengthMm, int binning); double GetCameraPixelScale() const; @@ -546,6 +556,7 @@ enum { BUTTON_AUTOSTAR, BUTTON_DURATION, BUTTON_ADVANCED, + BUTTON_PLANETARY, BUTTON_CAM_PROPERTIES, BUTTON_ALERT_ACTION, BUTTON_ALERT_CLOSE, @@ -632,6 +643,7 @@ enum { MENU_POLARDRIFTTOOL, MENU_STATICPATOOL, MENU_COMETTOOL, + MENU_PLANETARY, MENU_GUIDING_ASSISTANT, MENU_SAVESETTINGS, MENU_LOADSETTINGS, diff --git a/myframe_events.cpp b/myframe_events.cpp index 342320348..33711f3f5 100644 --- a/myframe_events.cpp +++ b/myframe_events.cpp @@ -44,6 +44,7 @@ #include "Refine_DefMap.h" #include "starcross_test.h" #include "calibration_assistant.h" +#include "planetary_tool.h" #include #include @@ -132,6 +133,7 @@ void MyFrame::NotifyExposureChanged() { NotifyGuidingParam("Exposure", ExposureDurationSummary()); pConfig->Profile.SetInt("/ExposureDurationMs", m_autoExp.enabled ? -1 : m_exposureDuration); + UpdateCameraSettings(); } int MyFrame::RequestedExposureDuration() @@ -1312,3 +1314,22 @@ void MyFrame::OnCharHook(wxKeyEvent& evt) evt.Skip(); } } + +void MyFrame::OnPlanetTool(wxCommandEvent& evt) +{ + if (!pPlanetTool) + { + pPlanetTool = PlanetTool::CreatePlanetToolWindow(); + } + + if (pPlanetTool) + { + // Reset dialog position when opened when any of Alt/Ctrl/Shift is pressed while clicking the button + if ((evt.GetId() == BUTTON_PLANETARY) && + (wxGetKeyState(WXK_ALT) || wxGetKeyState(WXK_CONTROL) || wxGetKeyState(WXK_SHIFT))) + { + PlaceWindowOnScreen(pPlanetTool, -1, -1); + } + pPlanetTool->Show(); + } +} \ No newline at end of file diff --git a/planetary_tool.cpp b/planetary_tool.cpp new file mode 100644 index 000000000..3f26dcd32 --- /dev/null +++ b/planetary_tool.cpp @@ -0,0 +1,683 @@ +/* + * planetary_tool.cpp + * PHD Guiding + + * Created by Leo Shatz. + * Copyright (c) 2023-2024 Leo Shatz, openphdguiding.org + * All rights reserved. +* + * This source code is distributed under the following "BSD" license + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of Craig Stark, Stark Labs nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "phd.h" +#include "planetary_tool.h" + +#include +#include + +static bool pauseAlert = false; + +struct PlanetToolWin : public wxDialog +{ + GuiderPlanet* pPlanet; + + wxTimer m_planetaryTimer; + + wxNotebook* m_tabs; + wxPanel* m_planetTab; + wxCheckBox* m_enableCheckBox; + + wxSpinCtrlDouble *m_minRadius; + wxSpinCtrlDouble *m_maxRadius; + + wxSlider *m_thresholdSlider; + + // Controls for camera settings, duplicating the ones from camera setup dialog and exposure time dropdown. + // Used for streamlining the planetary guiding user experience. + wxSpinCtrlDouble* m_ExposureCtrl; + wxSpinCtrlDouble* m_DelayCtrl; + wxSpinCtrlDouble* m_GainCtrl; + wxChoice* m_BinningCtrl; + + wxButton *m_CloseButton; + wxButton *m_PauseButton; + wxCheckBox *m_RoiCheckBox; + wxCheckBox *m_ShowElements; + bool m_MouseHoverFlag; + + PlanetToolWin(); + ~PlanetToolWin(); + + void OnAppStateNotify(wxCommandEvent& event); + void OnPlanetaryTimer(wxTimerEvent& event); + void OnPauseButton(wxCommandEvent& event); + void OnClose(wxCloseEvent& event); + void OnCloseButton(wxCommandEvent& event); + void OnKeyDown(wxKeyEvent& event); + void OnKeyUp(wxKeyEvent& event); + void OnMouseEnterCloseBtn(wxMouseEvent& event); + void OnMouseLeaveCloseBtn(wxMouseEvent& event); + void OnThresholdChanged(wxCommandEvent& event); + + void OnEnableToggled(wxCommandEvent& event); + void OnSpinCtrl_minRadius(wxSpinDoubleEvent& event); + void OnSpinCtrl_maxRadius(wxSpinDoubleEvent& event); + void OnRoiModeClick(wxCommandEvent& event); + void OnShowElementsClick(wxCommandEvent& event); + + void OnExposureChanged(wxSpinDoubleEvent& event); + void OnDelayChanged(wxSpinDoubleEvent& event); + void OnGainChanged(wxSpinDoubleEvent& event); + void OnBinningSelected(wxCommandEvent& event); + + void SyncCameraExposure(bool init = false); + void CheckMinExposureDuration(); + void UpdateStatus(); +}; + +static wxString TITLE = wxTRANSLATE("Planetary guiding | disabled"); +static wxString TITLE_ACTIVE = wxTRANSLATE("Planetary guiding | enabled"); +static wxString TITLE_PAUSED = wxTRANSLATE("Planetary guiding | paused"); + +static void SetEnabledState(PlanetToolWin* win, bool active) +{ + bool paused = win->pPlanet->GetDetectionPausedState(); + win->SetTitle(wxGetTranslation(active ? (paused ? TITLE_PAUSED : TITLE_ACTIVE) : TITLE)); + win->UpdateStatus(); +} + +// Utility function to add the pairs to a flexgrid +static void AddTableEntryPair(wxWindow* parent, wxFlexGridSizer* pTable, const wxString& label, wxWindow* pControl, const wxString& tooltip) +{ + wxStaticText* pLabel = new wxStaticText(parent, wxID_ANY, label + _(": "), wxPoint(-1, -1), wxSize(-1, -1)); + pLabel->SetToolTip(tooltip); + pTable->Add(pLabel, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + pTable->Add(pControl, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); +} + +// Utility function to add the pairs to a boxsizer +static void AddTableEntryPair(wxWindow* parent, wxBoxSizer* pSizer, const wxString& label, int spacer1, wxWindow* pControl, int spacer2, const wxString& tooltip) +{ + wxStaticText* pLabel = new wxStaticText(parent, wxID_ANY, label + _(": "), wxPoint(-1, -1), wxSize(-1, -1)); + pLabel->SetToolTip(tooltip); + pSizer->Add(pLabel, 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, 10); + pSizer->AddSpacer(spacer1); + pSizer->Add(pControl, 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, 10); + pSizer->AddSpacer(spacer2); +} + +static wxSpinCtrlDouble* NewSpinner(wxWindow* parent, wxString formatstr, double val, double minval, double maxval, double inc) +{ + wxSize sz = pFrame->GetTextExtent(wxString::Format(formatstr, maxval)); + wxSpinCtrlDouble* pNewCtrl = pFrame->MakeSpinCtrlDouble(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, + sz, wxSP_ARROW_KEYS, minval, maxval, val, inc); + pNewCtrl->SetDigits(0); + return pNewCtrl; +} + +PlanetToolWin::PlanetToolWin() + : wxDialog(pFrame, wxID_ANY, wxGetTranslation(TITLE), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX), + m_planetaryTimer(this), pPlanet(&pFrame->pGuider->m_Planet), m_MouseHoverFlag(false) + +{ + SetSizeHints(wxDefaultSize, wxDefaultSize); + + // Set custom duration of tooltip display to 10 seconds + wxToolTip::SetAutoPop(10000); + + m_tabs = new wxNotebook(this, wxID_ANY); + m_planetTab = new wxPanel(m_tabs, wxID_ANY); + m_tabs->AddPage(m_planetTab, "Planetary guiding", true); + + m_enableCheckBox = new wxCheckBox(this, wxID_ANY, _("Enable planetary guiding")); + m_enableCheckBox->SetToolTip(_("Toggle star/planetary guiding mode")); + + wxString radiusTooltip = _("For initial guess of possible radius range connect the gear and set correct focal length."); + if (pCamera) + { + // arcsec/pixel + double pixelScale = pFrame->GetPixelScale(pCamera->GetCameraPixelSize(), pFrame->GetFocalLength(), pCamera->Binning); + if ((pFrame->GetFocalLength() > 1) && pixelScale > 0) + { + double radiusGuessMax = 990.0 / pixelScale; + double raduisGuessMin = 870.0 / pixelScale; + radiusTooltip = wxString::Format(_("Hint: for solar/lunar detection (pixel size=%.2f, binning=x%d, FL=%d mm) set the radius to approximately %.0f-%.0f."), + pCamera->GetCameraPixelSize(), pCamera->Binning, pFrame->GetFocalLength(), raduisGuessMin-10, radiusGuessMax+10); + } + } + + wxStaticText* minRadius_Label = new wxStaticText(m_planetTab, wxID_ANY, _("min radius:")); + m_minRadius = new wxSpinCtrlDouble(m_planetTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(80, -1), wxSP_ARROW_KEYS, PT_RADIUS_MIN, PT_RADIUS_MAX, PT_MIN_RADIUS_DEFAULT); + minRadius_Label->SetToolTip(_("Minimum planet radius (in pixels). Set this a few pixels lower than the actual planet radius. ") + radiusTooltip); + + wxStaticText* maxRadius_Label = new wxStaticText(m_planetTab, wxID_ANY, _("max radius:")); + m_maxRadius = new wxSpinCtrlDouble(m_planetTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(80, -1), wxSP_ARROW_KEYS, PT_RADIUS_MIN, PT_RADIUS_MAX, PT_MAX_RADIUS_DEFAULT); + maxRadius_Label->SetToolTip(_("Maximum planet radius (in pixels). Set this a few pixels higher than the actual planet radius. ") + radiusTooltip); + + wxBoxSizer *x_radii = new wxBoxSizer(wxHORIZONTAL); + x_radii->Add(0, 0, 1, wxEXPAND, 5); + x_radii->Add(minRadius_Label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + x_radii->Add(m_minRadius, 0, wxALIGN_CENTER_VERTICAL, 5); + x_radii->Add(0, 0, 1, wxEXPAND, 5); + x_radii->Add(maxRadius_Label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + x_radii->Add(m_maxRadius, 0, wxALIGN_CENTER_VERTICAL, 5); + x_radii->Add(0, 0, 1, wxEXPAND, 5); + + // Planetary disk detection stuff + wxStaticText* ThresholdLabel = new wxStaticText(m_planetTab, wxID_ANY, wxT("Edge Detection Threshold:"), wxDefaultPosition, wxDefaultSize, 0); + m_thresholdSlider = new wxSlider(m_planetTab, wxID_ANY, PT_HIGH_THRESHOLD_DEFAULT, PT_THRESHOLD_MIN, PT_HIGH_THRESHOLD_MAX, wxPoint(20, 20), wxSize(400, -1), wxSL_HORIZONTAL | wxSL_LABELS); + ThresholdLabel->SetToolTip(_("Higher values reduce sensitivity to weaker edges, resulting in cleaner contour. This is displayed in red when the display of internal contour edges is enabled.")); + m_thresholdSlider->Bind(wxEVT_SLIDER, &PlanetToolWin::OnThresholdChanged, this); + m_RoiCheckBox = new wxCheckBox(m_planetTab, wxID_ANY, _("Enable ROI")); + m_RoiCheckBox->SetToolTip(_("Enable automatically selected Region Of Interest (ROI) for improved processing speed and reduced CPU usage.")); + + // Add all planetary tab elements + wxStaticBoxSizer *planetSizer = new wxStaticBoxSizer(new wxStaticBox(m_planetTab, wxID_ANY, _("")), wxVERTICAL); + planetSizer->AddSpacer(10); + planetSizer->Add(m_RoiCheckBox, 0, wxLEFT | wxALIGN_LEFT, 10); + planetSizer->AddSpacer(10); + planetSizer->Add(x_radii, 0, wxEXPAND, 5); + planetSizer->AddSpacer(10); + planetSizer->Add(ThresholdLabel, 0, wxLEFT | wxTOP, 10); + planetSizer->Add(m_thresholdSlider, 0, wxALL, 10); + m_planetTab->SetSizer(planetSizer); + m_planetTab->Layout(); + + // Show/hide detected elements + m_ShowElements = new wxCheckBox(this, wxID_ANY, _("Display internal contour edges")); + m_ShowElements->SetToolTip(_("Toggle the visibility of internally detected contour edges and adjust detection parameters to " + "maintain a smooth contour closely aligned with the planetary limb.")); + + // Camera settings group + wxStaticBoxSizer* pCamGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Camera settings")); + wxBoxSizer* pCamSizer1 = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer* pCamSizer2 = new wxBoxSizer(wxHORIZONTAL); + m_ExposureCtrl = NewSpinner(this, _T("%5.0f"), 1000, PT_CAMERA_EXPOSURE_MIN, PT_CAMERA_EXPOSURE_MAX, 1); + m_GainCtrl = NewSpinner(this, _T("%3.0f"), 0, 0, 100, 1); + m_DelayCtrl = NewSpinner(this, _T("%5.0f"), 100, 0, 60000, 100); + int maxBinning = pCamera ? (pCamera->Name == "Simulator" ? 1 : pCamera->MaxBinning) : 1; + wxArrayString binningOpts; + GuideCamera::GetBinningOpts(maxBinning, &binningOpts); + m_BinningCtrl = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, binningOpts); + m_ExposureCtrl->Bind(wxEVT_SPINCTRLDOUBLE, &PlanetToolWin::OnExposureChanged, this); + m_GainCtrl->Bind(wxEVT_SPINCTRLDOUBLE, &PlanetToolWin::OnGainChanged, this); + m_DelayCtrl->Bind(wxEVT_SPINCTRLDOUBLE, &PlanetToolWin::OnDelayChanged, this); + m_BinningCtrl->Bind(wxEVT_CHOICE, &PlanetToolWin::OnBinningSelected, this); + pCamSizer1->AddSpacer(5); + AddTableEntryPair(this, pCamSizer1, _("Exposure (ms)"), 20, m_ExposureCtrl, 20, _("Camera exposure in milliseconds)")); + AddTableEntryPair(this, pCamSizer1, _("Gain"), 35, m_GainCtrl, 0, _("Camera gain (0-100)")); + pCamSizer2->AddSpacer(5); + AddTableEntryPair(this, pCamSizer2, _("Time Lapse (ms)"), 5, m_DelayCtrl, 20, + _("How long should PHD wait between guide frames? Useful when using very short exposures but wanting to send guide commands less frequently")); + AddTableEntryPair(this, pCamSizer2, _("Binning"), 10, m_BinningCtrl, 0, _("Camera binning. For planetary guiding 1x1 is recommended.")); + pCamGroup->Add(pCamSizer1); + pCamGroup->AddSpacer(10); + pCamGroup->Add(pCamSizer2); + pCamGroup->AddSpacer(10); + + // Buttons + wxBoxSizer *ButtonSizer = new wxBoxSizer(wxHORIZONTAL); + m_CloseButton = new wxButton(this, wxID_ANY, _("Close")); + m_PauseButton = new wxButton(this, wxID_ANY, _("Pause")); + m_PauseButton->SetToolTip(_("Use this button to pause/resume detection during clouds or totality instead of stopping guiding. " + "It preserves object lock position, allowing PHD2 to realign the object without losing its original position")); + ButtonSizer->Add(m_PauseButton, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + ButtonSizer->Add(m_CloseButton, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + // All top level controls + wxBoxSizer *topSizer = new wxBoxSizer(wxVERTICAL); + topSizer->AddSpacer(10); + topSizer->Add(m_enableCheckBox, 0, wxLEFT | wxALIGN_LEFT, 20); + topSizer->AddSpacer(5); + topSizer->AddSpacer(5); + topSizer->Add(m_tabs, 0, wxEXPAND | wxALL, 5); + topSizer->AddSpacer(5); + topSizer->Add(m_ShowElements, 0, wxLEFT | wxALIGN_LEFT, 20); + topSizer->AddSpacer(5); + topSizer->Add(pCamGroup, 0, wxEXPAND | wxALL, 5); + topSizer->Add(ButtonSizer, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 5); + + SetSizer(topSizer); + Layout(); + topSizer->Fit(this); + + // Connect Events + Bind(wxEVT_TIMER, &PlanetToolWin::OnPlanetaryTimer, this, wxID_ANY); + m_enableCheckBox->Bind(wxEVT_CHECKBOX, &PlanetToolWin::OnEnableToggled, this); + m_CloseButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &PlanetToolWin::OnCloseButton, this); + m_CloseButton->Bind(wxEVT_KEY_DOWN, &PlanetToolWin::OnKeyDown, this); + m_CloseButton->Bind(wxEVT_KEY_UP, &PlanetToolWin::OnKeyUp, this); + m_CloseButton->Bind(wxEVT_ENTER_WINDOW, &PlanetToolWin::OnMouseEnterCloseBtn, this); + m_CloseButton->Bind(wxEVT_LEAVE_WINDOW, &PlanetToolWin::OnMouseLeaveCloseBtn, this); + m_PauseButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &PlanetToolWin::OnPauseButton, this); + m_RoiCheckBox->Bind(wxEVT_CHECKBOX, &PlanetToolWin::OnRoiModeClick, this); + m_ShowElements->Bind(wxEVT_CHECKBOX, &PlanetToolWin::OnShowElementsClick, this); + Bind(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(PlanetToolWin::OnClose), this); + + m_minRadius->Connect(wxEVT_SPINCTRLDOUBLE, wxSpinDoubleEventHandler(PlanetToolWin::OnSpinCtrl_minRadius), NULL, this); + m_maxRadius->Connect(wxEVT_SPINCTRLDOUBLE, wxSpinDoubleEventHandler(PlanetToolWin::OnSpinCtrl_maxRadius), NULL, this); + + pPlanet->SetPlanetaryElementsButtonState(false); + pPlanet->SetPlanetaryElementsVisual(false); + + m_minRadius->SetValue(pPlanet->GetPlanetaryParam_minRadius()); + m_maxRadius->SetValue(pPlanet->GetPlanetaryParam_maxRadius()); + m_thresholdSlider->SetValue(pPlanet->GetPlanetaryParam_highThreshold()); + m_RoiCheckBox->SetValue(pPlanet->GetRoiEnableState()); + m_enableCheckBox->SetValue(pPlanet->GetPlanetaryEnableState()); + m_BinningCtrl->Select(pCamera ? pCamera->Binning - 1 : 0); + SetEnabledState(this, pPlanet->GetPlanetaryEnableState()); + + // Set the initial state of the pause button + m_PauseButton->SetLabel(pPlanet->GetDetectionPausedState() ? _("Resume") : _("Pause")); + + // Update mount states + wxTimerEvent dummyEvent; + OnPlanetaryTimer(dummyEvent); + + // Update camera settings + m_DelayCtrl->SetValue(pFrame->GetTimeLapse()); + if (pCamera) + m_GainCtrl->SetValue(pCamera->GetCameraGain()); + SyncCameraExposure(true); + + Connect(APPSTATE_NOTIFY_EVENT, wxCommandEventHandler(PlanetToolWin::OnAppStateNotify)); + + int xpos = pConfig->Profile.GetInt("/PlanetTool/pos.x", -1); + int ypos = pConfig->Profile.GetInt("/PlanetTool/pos.y", -1); + if (wxGetKeyState(WXK_ALT)) + { + xpos = -1; + ypos = -1; + } + MyFrame::PlaceWindowOnScreen(this, xpos, ypos); + + UpdateStatus(); + m_planetaryTimer.Start(1000); +} + +PlanetToolWin::~PlanetToolWin(void) +{ + // Stop the timer + m_planetaryTimer.Stop(); + + pFrame->eventLock.Lock(); + pFrame->pPlanetTool = nullptr; + pFrame->eventLock.Unlock(); +} + +void PlanetToolWin::OnEnableToggled(wxCommandEvent& event) +{ + GuiderMultiStar* pMultiGuider = dynamic_cast(pFrame->pGuider); + + if (m_enableCheckBox->IsChecked()) + { + pFrame->SaveStarFindMode(); + pFrame->SetStarFindMode(Star::FIND_PLANET); + pPlanet->SetPlanetaryEnableState(true); + pFrame->m_PlanetaryMenuItem->Check(true); + SetEnabledState(this, true); + + if (pMultiGuider) + { + // Save the current state of the mass change threshold and disable it + bool prev = pMultiGuider->GetMassChangeThresholdEnabled(); + pMultiGuider->SetMassChangeThresholdEnabled(false); + pConfig->Profile.SetBoolean("/guider/onestar/MassChangeThresholdEnabled", prev); + } + + // Make sure lock position shift is disabled + pFrame->pGuider->EnableLockPosShift(false); + + // Disable subframes + if (pCamera) + { + pConfig->Profile.SetBoolean("/camera/UseSubframes", pCamera->UseSubframes); + pCamera->UseSubframes = false; + } + + // Disable multi-star mode + bool prev = pFrame->pGuider->GetMultiStarMode(); + pFrame->pGuider->SetMultiStarMode(false); + pConfig->Profile.SetBoolean("/guider/multistar/enabled", prev); + + Debug.Write(_("Planetary guiding mode: enabled\n")); + } + else + { + pFrame->RestoreStarFindMode(); + pPlanet->SetPlanetaryEnableState(false); + pFrame->m_PlanetaryMenuItem->Check(false); + SetEnabledState(this, false); + + // Restore the previous state of the mass change threshold + if (pMultiGuider) + { + bool prev = pConfig->Profile.GetBoolean("/guider/onestar/MassChangeThresholdEnabled", false); + pMultiGuider->SetMassChangeThresholdEnabled(prev); + } + + // Restore subframes + if (pCamera) + { + pCamera->UseSubframes = pConfig->Profile.GetBoolean("/camera/UseSubframes", false); + } + + // Restore multi-star mode + bool prev = pConfig->Profile.GetBoolean("/guider/multistar/enabled", false); + pFrame->pGuider->SetMultiStarMode(prev); + + Debug.Write(_("Planetary guiding mode: disabled\n")); + } + + // Update elements display state + OnShowElementsClick(event); +} + +void PlanetToolWin::OnSpinCtrl_minRadius(wxSpinDoubleEvent& event) +{ + int v = m_minRadius->GetValue(); + pPlanet->SetPlanetaryParam_minRadius(v < 1 ? 1 : v); + pPlanet->PlanetVisualRefresh(); +} + +void PlanetToolWin::OnSpinCtrl_maxRadius(wxSpinDoubleEvent& event) +{ + int v = m_maxRadius->GetValue(); + pPlanet->SetPlanetaryParam_maxRadius(v < 1 ? 1 : v); + pPlanet->PlanetVisualRefresh(); +} + +void PlanetToolWin::OnRoiModeClick(wxCommandEvent& event) +{ + bool enabled = m_RoiCheckBox->IsChecked(); + pPlanet->SetRoiEnableState(enabled); + Debug.Write(wxString::Format("Planetary guiding mode ROI: %s\n", enabled ? "enabled" : "disabled")); +} + +void PlanetToolWin::OnShowElementsClick(wxCommandEvent& event) +{ + bool enabled = m_ShowElements->IsChecked(); + pPlanet->SetPlanetaryElementsButtonState(enabled); + if (pPlanet->GetPlanetaryEnableState() && enabled) + pPlanet->SetPlanetaryElementsVisual(true); + else + pPlanet->SetPlanetaryElementsVisual(false); + pFrame->pGuider->Refresh(); + pFrame->pGuider->Update(); +} + +// Called once in a while to update the UI controls +void PlanetToolWin::OnPlanetaryTimer(wxTimerEvent& event) +{ + bool need_update = false; + + // Update pause button state to sync with guiding state + bool paused = pPlanet->GetDetectionPausedState() && pFrame->pGuider->IsGuiding(); + pPlanet->SetDetectionPausedState(paused); + m_PauseButton->SetLabel(paused ? _("Resume") : _("Pause")); + SetEnabledState(this, pPlanet->GetPlanetaryEnableState()); + if (!paused && pauseAlert) + { + pauseAlert = false; + pFrame->ClearAlert(); + } + + // Update camera binning + int localBinning = m_BinningCtrl->GetSelection(); + if (pCamera->Binning != localBinning + 1) + { + m_BinningCtrl->Select(pCamera->Binning - 1); + } +} + +void PlanetToolWin::OnExposureChanged(wxSpinDoubleEvent& event) +{ + int expMsec = m_ExposureCtrl->GetValue(); + expMsec = wxMin(expMsec, PT_CAMERA_EXPOSURE_MAX); + expMsec = wxMax(expMsec, PT_CAMERA_EXPOSURE_MIN); + pFrame->SetExposureDuration(expMsec, true); + CheckMinExposureDuration(); +} + +void PlanetToolWin::OnDelayChanged(wxSpinDoubleEvent& event) +{ + int delayMsec = m_DelayCtrl->GetValue(); + delayMsec = wxMin(delayMsec, 60000); + delayMsec = wxMax(delayMsec, 0); + pFrame->SetTimeLapse(delayMsec); + CheckMinExposureDuration(); +} + +void PlanetToolWin::OnGainChanged(wxSpinDoubleEvent& event) +{ + int gain = m_GainCtrl->GetValue(); + gain = wxMin(gain, 100.0); + gain = wxMax(gain, 0.0); + if (pCamera) + pCamera->SetCameraGain(gain); +} + +void PlanetToolWin::OnBinningSelected(wxCommandEvent& event) +{ + int sel = m_BinningCtrl->GetSelection(); + AdvancedDialog* pAdvancedDlg = pFrame->pAdvancedDialog; + if (pAdvancedDlg) + { + pAdvancedDlg->SetBinning(sel + 1); + if (pCamera && pCamera->Connected && (pCamera->Binning != sel + 1)) + pAdvancedDlg->MakeImageScaleAdjustments(); + } + if (pCamera) + { + pCamera->SetBinning(sel + 1); + } +} + +void PlanetToolWin::UpdateStatus() +{ + bool enabled = pPlanet->GetPlanetaryEnableState(); + + // Update planetary guiding controls + m_minRadius->Enable(enabled); + m_maxRadius->Enable(enabled); + m_RoiCheckBox->Enable(enabled); + m_ShowElements->Enable(enabled); + + // Update slider states + m_thresholdSlider->Enable(enabled); + + // Update tabs state + m_planetTab->Enable(true); + + // Pause planetary guiding can be enabled only when guiding is still active + m_PauseButton->Enable(enabled && pFrame->pGuider->IsGuiding()); +} + +void PlanetToolWin::OnKeyDown(wxKeyEvent& event) +{ + if (event.AltDown() && m_MouseHoverFlag) { + m_CloseButton->SetLabel(wxT("Reset")); + } + event.Skip(); // Ensure that other key handlers are not skipped +} + +void PlanetToolWin::OnKeyUp(wxKeyEvent& event) +{ + m_CloseButton->SetLabel(wxT("Close")); + event.Skip(); +} + +void PlanetToolWin::OnMouseEnterCloseBtn(wxMouseEvent& event) +{ + m_MouseHoverFlag = true; + if (wxGetKeyState(WXK_ALT)) + { + m_CloseButton->SetLabel(wxT("Reset")); + } + event.Skip(); +} + +void PlanetToolWin::OnMouseLeaveCloseBtn(wxMouseEvent& event) +{ + m_MouseHoverFlag = false; + m_CloseButton->SetLabel(wxT("Close")); + event.Skip(); +} + +void PlanetToolWin::OnThresholdChanged(wxCommandEvent& event) +{ + int highThreshold = event.GetInt(); + highThreshold = wxMin(highThreshold, PT_HIGH_THRESHOLD_MAX); + highThreshold = wxMax(highThreshold, PT_THRESHOLD_MIN); + int lowThreshold = wxMax(highThreshold / 2, PT_THRESHOLD_MIN); + pPlanet->SetPlanetaryParam_lowThreshold(lowThreshold); + pPlanet->SetPlanetaryParam_highThreshold(highThreshold); + pPlanet->RestartSimulatorErrorDetection(); +} + +static void SuppressPausePlanetDetection(long) +{ + pConfig->Global.SetBoolean(PausePlanetDetectionAlertEnabledKey(), false); +} + +void PlanetToolWin::OnPauseButton(wxCommandEvent& event) +{ + // Toggle planetary detection pause state depending if guiding is actually active + bool paused = !pPlanet->GetDetectionPausedState() && pFrame->pGuider->IsGuiding(); + pPlanet->SetDetectionPausedState(paused); + m_PauseButton->SetLabel(paused ? _("Resume") : _("Pause")); + SetEnabledState(this, pPlanet->GetPlanetaryEnableState()); + + // Display special message if detection is paused + if (paused) + { + pauseAlert = true; + pFrame->SuppressableAlert(PausePlanetDetectionAlertEnabledKey(), _("Planetary detection paused : do not stop guiding to keep the original lock position!"), + SuppressPausePlanetDetection, 0); + } + else if (pauseAlert) + { + pauseAlert = false; + pFrame->ClearAlert(); + } +} + +void PlanetToolWin::OnClose(wxCloseEvent& evt) +{ + pFrame->m_PlanetaryMenuItem->Check(pPlanet->GetPlanetaryEnableState()); + pPlanet->SetPlanetaryElementsButtonState(false); + pPlanet->SetPlanetaryElementsVisual(false); + pFrame->pGuider->Refresh(); + pFrame->pGuider->Update(); + + // save the window position + int x, y; + GetPosition(&x, &y); + pConfig->Profile.SetInt("/PlanetTool/pos.x", x); + pConfig->Profile.SetInt("/PlanetTool/pos.y", y); + + // Revert to a default duration of tooltip display (apparently 5 seconds) + wxToolTip::SetAutoPop(5000); + + Destroy(); +} + +void PlanetToolWin::OnCloseButton(wxCommandEvent& event) +{ + // Reset all to defaults + if (wxGetKeyState(WXK_ALT)) + { + pPlanet->SetPlanetaryParam_minRadius(PT_MIN_RADIUS_DEFAULT); + pPlanet->SetPlanetaryParam_maxRadius(PT_MAX_RADIUS_DEFAULT); + pPlanet->SetPlanetaryParam_lowThreshold(PT_HIGH_THRESHOLD_DEFAULT/2); + pPlanet->SetPlanetaryParam_highThreshold(PT_HIGH_THRESHOLD_DEFAULT); + + m_minRadius->SetValue(pPlanet->GetPlanetaryParam_minRadius()); + m_maxRadius->SetValue(pPlanet->GetPlanetaryParam_maxRadius()); + m_thresholdSlider->SetValue(pPlanet->GetPlanetaryParam_highThreshold()); + } + else + this->Close(); +} + +void PlanetToolWin::CheckMinExposureDuration() +{ + int delayMsec = m_DelayCtrl->GetValue(); + int exposureMsec = m_ExposureCtrl->GetValue(); + if (delayMsec + exposureMsec < 500) + { + pFrame->Alert(_("Warning: the sum of camera exposure and time lapse duration must be at least 500 msec (recommended 500-5000 msec)!")); + } +} + +void PlanetToolWin::SyncCameraExposure(bool init) +{ + int exposureMsec; + bool auto_exp; + if (!pFrame->GetExposureInfo(&exposureMsec, &auto_exp)) + { + exposureMsec = wxMax(exposureMsec, PT_CAMERA_EXPOSURE_MIN); + exposureMsec = wxMin(exposureMsec, PT_CAMERA_EXPOSURE_MAX); + pFrame->SetExposureDuration(exposureMsec, true); + } + else + { + exposureMsec = pConfig->Profile.GetInt("/ExposureDurationMs", 1000); + } + if (init || exposureMsec != m_ExposureCtrl->GetValue()) + { + m_ExposureCtrl->SetValue(exposureMsec); + if (exposureMsec != m_ExposureCtrl->GetValue()) + { + exposureMsec = m_ExposureCtrl->GetValue(); + pFrame->SetExposureDuration(exposureMsec, true); + } + } + CheckMinExposureDuration(); +} + +// Sync local camera settings with the main frame changes +void PlanetToolWin::OnAppStateNotify(wxCommandEvent& event) +{ + SyncCameraExposure(); + + int const delayMsec = pFrame->GetTimeLapse(); + if (delayMsec != m_DelayCtrl->GetValue()) + m_DelayCtrl->SetValue(delayMsec); + + if (pCamera) + { + int const gain = pCamera->GetCameraGain(); + if (gain != m_GainCtrl->GetValue()) + m_GainCtrl->SetValue(gain); + } +} + +wxWindow *PlanetTool::CreatePlanetToolWindow() +{ + return new PlanetToolWin(); +} \ No newline at end of file diff --git a/planetary_tool.h b/planetary_tool.h new file mode 100644 index 000000000..d78298e7c --- /dev/null +++ b/planetary_tool.h @@ -0,0 +1,62 @@ +/* + * planetary_tool.h + * PHD Guiding + * + * Created by Leo Shatz + * Copyright (c) 2023-2024 Leo Shatz, openphdguiding.org + * All rights reserved. + * + * This source code is distributed under the following "BSD" license + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of Craig Stark, Stark Labs nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#pragma once + +// Default planetary detection parameters values +#define PT_MIN_RADIUS_DEFAULT 100 +#define PT_MAX_RADIUS_DEFAULT 200 +#define PT_RADIUS_MIN 1 +#define PT_RADIUS_MAX 2000 + +#define PT_HIGH_THRESHOLD_DEFAULT 200 +#define PT_THRESHOLD_MIN 1 +#define PT_HIGH_THRESHOLD_MAX 400 +#define PT_LOW_THRESHOLD_MAX 200 + +#define PT_CAMERA_EXPOSURE_MIN 1 +#define PT_CAMERA_EXPOSURE_MAX 30000 + +static inline wxString PausePlanetDetectionAlertEnabledKey() +{ + // we want the key to be under "/Confirm" so ConfirmDialog::ResetAllDontAskAgain() resets it, but we also want the setting to be per-profile + return wxString::Format("/Confirm/%d/PausePlanetDetectionAlertEnabled", pConfig->GetCurrentProfileId()); +} + +class PlanetTool +{ + PlanetTool(); +public: + static wxWindow *CreatePlanetToolWindow(); +}; diff --git a/star.cpp b/star.cpp index dae904c32..66d169127 100644 --- a/star.cpp +++ b/star.cpp @@ -123,16 +123,16 @@ static double hfr(std::vector& vec, double cx, double cy, double mass) return hfr; } -bool Star::Find(const usImage *pImg, int searchRegion, int base_x, int base_y, FindMode mode, double minHFD, double maxHFD, unsigned short maxADU, StarFindLogType loggingControl) +bool Star::Find(const usImage *pImg, int searchRegion, double base_x, double base_y, FindMode mode, double minHFD, double maxHFD, unsigned short maxADU, StarFindLogType loggingControl) { FindResult Result = STAR_OK; - double newX = base_x; - double newY = base_y; + double newX = (int) base_x; + double newY = (int) base_y; try { if (loggingControl == FIND_LOGGING_VERBOSE) - Debug.Write(wxString::Format("Star::Find(%d, %d, %d, %d, (%d,%d,%d,%d), %.1f, %0.1f, %hu) frame %u\n", searchRegion, base_x, base_y, mode, + Debug.Write(wxString::Format("Star::Find(%d, %.2f, %.2f, %d, (%d,%d,%d,%d), %.1f, %0.1f, %hu) frame %u\n", searchRegion, base_x, base_y, mode, pImg->Subframe.x, pImg->Subframe.y, pImg->Subframe.width, pImg->Subframe.height, minHFD, maxHFD, maxADU, pImg->FrameNum)); int minx, miny, maxx, maxy; @@ -188,7 +188,7 @@ bool Star::Find(const usImage *pImg, int searchRegion, int base_x, int base_y, F PeakVal = peak_val; } - else + else if (mode == FIND_CENTROID) { // find the peak value within the search region using a smoothing function // also check for saturation @@ -228,6 +228,11 @@ bool Star::Find(const usImage *pImg, int searchRegion, int base_x, int base_y, F PeakVal = max3[0]; // raw peak val peak_val /= 16; // smoothed peak value } + else // FIND_PLANET + { + Result = STAR_ERROR; + goto done; + } // meaure noise in the annulus with inner radius A and outer radius B int const A = 7; // inner radius diff --git a/star.h b/star.h index 22fb02d0f..44b26ee00 100644 --- a/star.h +++ b/star.h @@ -47,6 +47,7 @@ class Star : public PHD_Point { FIND_CENTROID, FIND_PEAK, + FIND_PLANET }; enum FindResult @@ -81,7 +82,7 @@ class Star : public PHD_Point * error */ bool Find(const usImage *pImg, int searchRegion, FindMode mode, double min_hfd, double max_hfd, unsigned short saturation, StarFindLogType loggingControl); - bool Find(const usImage *pImg, int searchRegion, int X, int Y, FindMode mode, double min_hfd, double max_hfd, unsigned short saturation, StarFindLogType loggingControl); + bool Find(const usImage *pImg, int searchRegion, double X, double Y, FindMode mode, double min_hfd, double max_hfd, unsigned short saturation, StarFindLogType loggingControl); static bool WasFound(FindResult result); bool WasFound() const;