[education/kstars] /: Add Collimation overlay graphics to live view

Jasem Mutlaq null at kde.org
Sun Dec 24 18:05:43 GMT 2023


Git commit a47e630159a9fae475342a78897aef5d5b056727 by Jasem Mutlaq, on behalf of Ed Lee.
Committed on 24/12/2023 at 19:05.
Pushed by mutlaqja into branch 'master'.

Add Collimation overlay graphics to live view

Adds QPainter graphics overlay drawn on top of the live view video frame.
An arbitrary number of overlay 'Elements' may be defined which can be individually enabled or disabled.
Each Element can be one of four types - Ellipse, Rectangle, Line, and the invisible Anchor.
Each drawn Element has it's own name, enabled, type, size, color, offset, count, PCD, rotation and thickness field. Elements are stored in the userdb database.

M  +26   -0    doc/ekos-capture.docbook
A  +-    --    doc/ekos_live_overlay.png
A  +-    --    doc/ekos_live_overlay_options.png
M  +5    -1    kstars/CMakeLists.txt
M  +159  -0    kstars/auxiliary/ksuserdb.cpp
M  +26   -1    kstars/auxiliary/ksuserdb.h
A  +594  -0    kstars/indi/collimationOptions.ui
A  +517  -0    kstars/indi/collimationoverlayoptions.cpp     [License: GPL(v2.0+)]
A  +175  -0    kstars/indi/collimationoverlayoptions.h     [License: GPL(v2.0+)]
A  +24   -0    kstars/indi/collimationoverlaytypes.h     [License: GPL(v2.0+)]
A  +57   -0    kstars/indi/elementinfo.h     [License: GPL(v2.0+)]
M  +50   -0    kstars/indi/streamform.ui
M  +17   -4    kstars/indi/streamwg.cpp
M  +4    -1    kstars/indi/streamwg.h
M  +147  -0    kstars/indi/videowg.cpp
M  +23   -1    kstars/indi/videowg.h

https://invent.kde.org/education/kstars/-/commit/a47e630159a9fae475342a78897aef5d5b056727

diff --git a/doc/ekos-capture.docbook b/doc/ekos-capture.docbook
index c510993435..3779c182e9 100644
--- a/doc/ekos-capture.docbook
+++ b/doc/ekos-capture.docbook
@@ -356,6 +356,32 @@
                 </para>
             </caption>
         </mediaobject>
+        <para>
+            The live view also provides a graphical overlay tool to aid in collimation. This is toggled on/off by the crosshair button.
+        </para>
+            <mediaobject>
+                <imageobject>
+                    <imagedata fileref="ekos_live_overlay.png" format="PNG"/>
+                </imageobject>
+                <caption>
+                    <para>
+                        <phrase>Collimation overlay</phrase>
+                    </para>
+                </caption>
+            </mediaobject>
+        <para>
+            The overlay options button opens a dialog that allows arbitary and flexible creation of ellipses (including circles), rectangles and lines, as well as anchor points which act as global drawing offsets. Each defined element has it's own size, offset, repetition, thickness and color (including transparency).
+        </para>
+        <mediaobject>
+            <imageobject>
+                <imagedata fileref="ekos_live_overlay_options.png" format="PNG"/>
+            </imageobject>
+            <caption>
+                <para>
+                    <phrase>Collimation overlay options</phrase>
+                </para>
+            </caption>
+        </mediaobject>
     </sect2>
 
     <sect2 id="capture-fits-viewer">
diff --git a/doc/ekos_live_overlay.png b/doc/ekos_live_overlay.png
new file mode 100644
index 0000000000..4c845e65b5
Binary files /dev/null and b/doc/ekos_live_overlay.png differ
diff --git a/doc/ekos_live_overlay_options.png b/doc/ekos_live_overlay_options.png
new file mode 100644
index 0000000000..6ff768a85a
Binary files /dev/null and b/doc/ekos_live_overlay_options.png differ
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 64ea8aa4fc..4455bdd02b 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -106,6 +106,8 @@ if (INDI_FOUND)
         indi/videowg.cpp
         indi/indiwebmanager.cpp
         indi/customdrivers.cpp
+        indi/collimationoverlayoptions.cpp
+        indi/collimationoverlaytypes.h
     )
 
     if (CFITSIO_FOUND)
@@ -418,6 +420,7 @@ if (CFITSIO_FOUND)
         fitsviewer/fitsdebayer.ui
         indi/streamform.ui
         indi/recordingoptions.ui
+        indi/collimationOptions.ui
         fitsviewer/fitshistogramui.ui
         fitsviewer/fitsstretchui.ui
         fitsviewer/opsfits.ui
@@ -1237,7 +1240,8 @@ IF (NOT ANDROID)
         #skycomponents/notifyupdatesui.ui
     )
 
-    add_library(KStarsLib STATIC ${kstars_SRCS})
+    add_library(KStarsLib STATIC ${kstars_SRCS}
+        indi/collimationOptions.ui)
 
     if (BUILD_PYKSTARS)
       set_target_properties(KStarsLib PROPERTIES POSITION_INDEPENDENT_CODE ON)
diff --git a/kstars/auxiliary/ksuserdb.cpp b/kstars/auxiliary/ksuserdb.cpp
index 7cb5c306d1..b9f69e6607 100644
--- a/kstars/auxiliary/ksuserdb.cpp
+++ b/kstars/auxiliary/ksuserdb.cpp
@@ -326,7 +326,27 @@ bool KSUserDB::Initialize()
             qCWarning(KSTARS) << query.lastError();
     }
 
+    // Add collimationoverlayelements table
+    if (currentDBVersion < 314)
+    {
+        QSqlQuery query(db);
 
+        if (!query.exec("CREATE TABLE collimationoverlayelements ( "
+                        "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
+                        "Name TEXT DEFAULT NULL, "
+                        "Enabled INTEGER DEFAULT 0, "
+                        "Type INTEGER DEFAULT NULL, "
+                        "SizeX INTEGER DEFAULT NULL, "
+                        "SizeY INTEGER DEFAULT NULL, "
+                        "OffsetX INTEGER DEFAULT NULL, "
+                        "OffsetY INTEGER DEFAULT NULL, "
+                        "Count INTEGER DEFAULT 1, "
+                        "PCD INTEGER DEFAULT 100, "
+                        "Rotation REAL DEFAULT 0.0, "
+                        "Colour TEXT DEFAULT NULL, "
+                        "Thickness INTEGER DEFAULT 1)"))
+            qCWarning(KSTARS) << query.lastError();
+    }
     return true;
 }
 
@@ -563,6 +583,21 @@ bool KSUserDB::RebuildDB()
                   "Exec TEXT DEFAULT NULL, "
                   "Version TEXT DEFAULT 1.0)");
 
+    tables.append("CREATE TABLE IF NOT EXISTS collimationoverlayelements ( "
+                  "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
+                  "Name TEXT DEFAULT NULL, "
+                  "Enabled INTEGER DEFAULT 0, "
+                  "Type INTEGER DEFAULT NULL, "
+                  "SizeX INTEGER DEFAULT NULL, "
+                  "SizeY INTEGER DEFAULT NULL, "
+                  "OffsetX INTEGER DEFAULT NULL, "
+                  "OffsetY INTEGER DEFAULT NULL, "
+                  "Count INTEGER DEFAULT 1, "
+                  "PCD INTEGER DEFAULT 100, "
+                  "Rotation REAL DEFAULT 0.0, "
+                  "Colour TEXT DEFAULT NULL, "
+                  "Thickness INTEGER DEFAULT 1)");
+
     // Need to offset primary key by 100,000 to differential it from scopes and keep it backward compatible.
     tables.append("UPDATE SQLITE_SEQUENCE SET seq = 100000 WHERE name ='dslrlens'");
 
@@ -3146,3 +3181,127 @@ bool KSUserDB::GetOpticalTrainSettings(uint32_t train, QVariantMap &settings)
 
     return false;
 }
+
+/* Collimation Overlay Elements Section */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+bool KSUserDB::AddCollimationOverlayElement(const QVariantMap &oneElement)
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel collimationOverlayElement(nullptr, db);
+    collimationOverlayElement.setTable("collimationoverlayelements");
+    collimationOverlayElement.select();
+
+    QSqlRecord record = collimationOverlayElement.record();
+
+    // Remove PK so that it gets auto-incremented later
+    record.remove(0);
+
+    for (QVariantMap::const_iterator iter = oneElement.begin(); iter != oneElement.end(); ++iter)
+        record.setValue(iter.key(), iter.value());
+
+    collimationOverlayElement.insertRecord(-1, record);
+
+    if (!collimationOverlayElement.submitAll())
+    {
+        qCWarning(KSTARS) << collimationOverlayElement.lastError();
+        return false;
+    }
+
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+bool KSUserDB::UpdateCollimationOverlayElement(const QVariantMap &oneElement, int id)
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel collimationOverlayElement(nullptr, db);
+    collimationOverlayElement.setTable("collimationoverlayelements");
+    collimationOverlayElement.setFilter(QString("id=%1").arg(id));
+    collimationOverlayElement.select();
+
+    QSqlRecord record = collimationOverlayElement.record(0);
+
+    for (QVariantMap::const_iterator iter = oneElement.begin(); iter != oneElement.end(); ++iter)
+        record.setValue(iter.key(), iter.value());
+
+    collimationOverlayElement.setRecord(0, record);
+
+    if (!collimationOverlayElement.submitAll())
+    {
+        qCWarning(KSTARS) << collimationOverlayElement.lastError();
+        return false;
+    }
+
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+bool KSUserDB::DeleteCollimationOverlayElement(int id)
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel collimationOverlayElement(nullptr, db);
+    collimationOverlayElement.setTable("collimationoverlayelements");
+    collimationOverlayElement.setFilter(QString("id=%1").arg(id));
+
+    collimationOverlayElement.select();
+
+    collimationOverlayElement.removeRows(0, 1);
+    collimationOverlayElement.submitAll();
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+bool KSUserDB::GetCollimationOverlayElements(QList<QVariantMap> &collimationOverlayElements)
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    collimationOverlayElements.clear();
+
+    QSqlTableModel collimationOverlayElement(nullptr, db);
+    collimationOverlayElement.setTable("collimationoverlayelements");
+    collimationOverlayElement.select();
+
+    for (int i = 0; i < collimationOverlayElement.rowCount(); ++i)
+    {
+        QVariantMap recordMap;
+        QSqlRecord record = collimationOverlayElement.record(i);
+        for (int j = 0; j < record.count(); j++)
+            recordMap[record.fieldName(j)] = record.value(j);
+
+        collimationOverlayElements.append(recordMap);
+    }
+
+    return true;
+}
diff --git a/kstars/auxiliary/ksuserdb.h b/kstars/auxiliary/ksuserdb.h
index ff40f8e628..c6f0d0fad7 100644
--- a/kstars/auxiliary/ksuserdb.h
+++ b/kstars/auxiliary/ksuserdb.h
@@ -435,6 +435,31 @@ class KSUserDB
          **/
         bool GetOpticalTrainSettings(uint32_t train, QVariantMap &settings);
 
+        /************************************************************************
+         *********************** Collimation Overlay Elements *******************
+         ************************************************************************/
+
+        /**
+         * @brief Add a new collimation overlay element to the database
+         * @param oneElement collimation overlay element data
+         **/
+        bool AddCollimationOverlayElement(const QVariantMap &oneElement);
+
+        /**
+         * @brief Update an existing collimation overlay element
+         * @param oneElement collimation overlay element data
+         * @param id ID of element to replace in database
+         **/
+        bool UpdateCollimationOverlayElement(const QVariantMap &oneElement, int id);
+
+        bool DeleteCollimationOverlayElement(int id);
+
+        /**
+         * @brief Populate the reference passed with all collimation overlay elements
+         * @param collimationOverlayElements Reference to all elements list
+         **/
+        bool GetCollimationOverlayElements(QList<QVariantMap> &collimationOverlayElements);
+
     private:
         /**
          * @brief This function initializes a new database in the user's directory.
@@ -494,5 +519,5 @@ class KSUserDB
 
         QString m_ConnectionName;
 
-        static const uint16_t SCHEMA_VERSION = 313;
+        static const uint16_t SCHEMA_VERSION = 314;
 };
diff --git a/kstars/indi/collimationOptions.ui b/kstars/indi/collimationOptions.ui
new file mode 100644
index 0000000000..a114efba5e
--- /dev/null
+++ b/kstars/indi/collimationOptions.ui
@@ -0,0 +1,594 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>collimationOptions</class>
+ <widget class="QDialog" name="collimationOptions">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>435</width>
+    <height>356</height>
+   </rect>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>16777215</width>
+    <height>16777215</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Collimation Overlay Options</string>
+  </property>
+  <property name="inputMethodHints">
+   <set>Qt::ImhNoPredictiveText</set>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>3</number>
+   </property>
+   <property name="leftMargin">
+    <number>3</number>
+   </property>
+   <property name="topMargin">
+    <number>3</number>
+   </property>
+   <property name="rightMargin">
+    <number>3</number>
+   </property>
+   <property name="bottomMargin">
+    <number>3</number>
+   </property>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="spacing">
+      <number>3</number>
+     </property>
+     <item>
+      <widget class="QPushButton" name="addB">
+       <property name="maximumSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Create a new Collimation Overlay Element</string>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset theme="list-add">
+         <normaloff>../ekos/auxiliary</normaloff>../ekos/auxiliary</iconset>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="removeB">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Delete the selected Collimation Overlay Element</string>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset theme="list-remove">
+         <normaloff>../ekos/auxiliary</normaloff>../ekos/auxiliary</iconset>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QSplitter" name="splitter">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <widget class="QListWidget" name="elementNamesList">
+      <property name="minimumSize">
+       <size>
+        <width>175</width>
+        <height>0</height>
+       </size>
+      </property>
+      <property name="toolTip">
+       <string>Collimation Overlay Element name (double click to edit)</string>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
+      </property>
+      <property name="editTriggers">
+       <set>QAbstractItemView::NoEditTriggers</set>
+      </property>
+      <property name="showDropIndicator" stdset="0">
+       <bool>false</bool>
+      </property>
+     </widget>
+     <widget class="QGroupBox" name="elementConfigBox">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="title">
+       <string>Collimation Overlay Element</string>
+      </property>
+      <layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0">
+       <property name="leftMargin">
+        <number>3</number>
+       </property>
+       <property name="topMargin">
+        <number>3</number>
+       </property>
+       <property name="rightMargin">
+        <number>3</number>
+       </property>
+       <property name="bottomMargin">
+        <number>3</number>
+       </property>
+       <property name="spacing">
+        <number>3</number>
+       </property>
+       <item row="1" column="0">
+        <widget class="QLabel" name="enableLabel">
+         <property name="toolTip">
+          <string>Select whether this Element is enabled or not</string>
+         </property>
+         <property name="text">
+          <string>Enable:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="2">
+        <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <item>
+          <widget class="QCheckBox" name="EnableCheckBox">
+           <property name="text">
+            <string/>
+           </property>
+           <property name="checked">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="renameB">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string>Rename</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item row="7" column="2">
+        <widget class="QSpinBox" name="countSpinBox">
+         <property name="minimum">
+          <number>1</number>
+         </property>
+         <property name="maximum">
+          <number>12</number>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QLabel" name="nameLabel">
+         <property name="toolTip">
+          <string>Enter a name for this Collimation Overlay Element. If left empty a name will be generated based on the Type selected.</string>
+         </property>
+         <property name="text">
+          <string>Name:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="4" column="2">
+        <widget class="QComboBox" name="typeComboBox">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+        </widget>
+       </item>
+       <item row="11" column="0">
+        <widget class="QLabel" name="colourLabel">
+         <property name="toolTip">
+          <string>Select colour for the Element</string>
+         </property>
+         <property name="text">
+          <string>Colour:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="6" column="2">
+        <layout class="QHBoxLayout" name="horizontalLayout_5">
+         <item>
+          <widget class="QLabel" name="offsetXLabel">
+           <property name="text">
+            <string>X</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="offsetXSpinBox">
+           <property name="minimum">
+            <number>-9999</number>
+           </property>
+           <property name="maximum">
+            <number>9999</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLabel" name="offsetYLabel">
+           <property name="text">
+            <string>Y</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="offsetYSpinBox">
+           <property name="minimum">
+            <number>-9999</number>
+           </property>
+           <property name="maximum">
+            <number>9999</number>
+           </property>
+           <property name="singleStep">
+            <number>1</number>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item row="11" column="2">
+        <layout class="QHBoxLayout" name="horizontalLayout_7">
+         <item>
+          <widget class="KColorButton" name="colourButton">
+           <property name="toolTip">
+            <string/>
+           </property>
+           <property name="whatsThis">
+            <string>Select a color for the Collimation Overlay Element.</string>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+           <property name="color" stdset="0">
+            <color>
+             <red>255</red>
+             <green>255</green>
+             <blue>255</blue>
+            </color>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLabel" name="thicknessLabel">
+           <property name="toolTip">
+            <string>Set the line thickness used to draw the element.</string>
+           </property>
+           <property name="text">
+            <string>Thickness:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="thicknessSpinBox">
+           <property name="minimum">
+            <number>1</number>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item row="6" column="0">
+        <widget class="QLabel" name="offsetLabel">
+         <property name="toolTip">
+          <string>Set the Elements offset from it's Anchor. For an Anchor Element this is from the centre of the image.</string>
+         </property>
+         <property name="text">
+          <string>Offset:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QLabel" name="sizeLabel">
+         <property name="toolTip">
+          <string>Set the size of the Element</string>
+         </property>
+         <property name="text">
+          <string>Size:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="2">
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QLabel" name="sizeXLabel">
+           <property name="text">
+            <string>X</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="sizeXSpinBox">
+           <property name="minimum">
+            <number>1</number>
+           </property>
+           <property name="maximum">
+            <number>10000</number>
+           </property>
+           <property name="value">
+            <number>1000</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="linkXYB">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="maximumSize">
+            <size>
+             <width>20</width>
+             <height>16777215</height>
+            </size>
+           </property>
+           <property name="toolTip">
+            <string>Link X & Y sizes</string>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLabel" name="sizeYLabel">
+           <property name="text">
+            <string>Y</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QSpinBox" name="sizeYSpinBox">
+           <property name="minimum">
+            <number>1</number>
+           </property>
+           <property name="maximum">
+            <number>10000</number>
+           </property>
+           <property name="value">
+            <number>1000</number>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item row="4" column="0">
+        <widget class="QLabel" name="typeLabel">
+         <property name="toolTip">
+          <string>Select the type of Collimation Overlay Element</string>
+         </property>
+         <property name="text">
+          <string>Type:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="7" column="0">
+        <widget class="QLabel" name="countLabel">
+         <property name="toolTip">
+          <string>Select number of occurences of the object type within this element</string>
+         </property>
+         <property name="text">
+          <string>Count:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="2">
+        <layout class="QHBoxLayout" name="horizontalLayout_9">
+         <item>
+          <widget class="QSpinBox" name="pcdSpinBox">
+           <property name="maximum">
+            <number>9999</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLabel" name="rotationLabel">
+           <property name="toolTip">
+            <string>If there is more than one occurence set the base rotation angle.</string>
+           </property>
+           <property name="text">
+            <string>Rotation:</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QDoubleSpinBox" name="rotationDoubleSpinBox">
+           <property name="decimals">
+            <number>1</number>
+           </property>
+           <property name="maximum">
+            <double>359.899999999999977</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item row="8" column="0">
+        <widget class="QLabel" name="pcdLabel">
+         <property name="toolTip">
+          <string>Set the Pitch Circle Diameter where there are multiple occurences (Count >1)</string>
+         </property>
+         <property name="text">
+          <string>PCD:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="2">
+        <widget class="QLineEdit" name="nameLineEdit"/>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <property name="spacing">
+      <number>3</number>
+     </property>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QDialogButtonBox" name="buttonBox">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="standardButtons">
+        <set>QDialogButtonBox::Close</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KColorButton</class>
+   <extends>QPushButton</extends>
+   <header>kcolorbutton.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>addB</tabstop>
+  <tabstop>removeB</tabstop>
+  <tabstop>elementNamesList</tabstop>
+  <tabstop>nameLineEdit</tabstop>
+  <tabstop>renameB</tabstop>
+  <tabstop>EnableCheckBox</tabstop>
+  <tabstop>typeComboBox</tabstop>
+  <tabstop>sizeXSpinBox</tabstop>
+  <tabstop>linkXYB</tabstop>
+  <tabstop>sizeYSpinBox</tabstop>
+  <tabstop>offsetXSpinBox</tabstop>
+  <tabstop>offsetYSpinBox</tabstop>
+  <tabstop>countSpinBox</tabstop>
+  <tabstop>pcdSpinBox</tabstop>
+  <tabstop>rotationDoubleSpinBox</tabstop>
+  <tabstop>colourButton</tabstop>
+  <tabstop>thicknessSpinBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>collimationOptions</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>373</x>
+     <y>312</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>249</x>
+     <y>170</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>collimationOptions</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>373</x>
+     <y>312</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>249</x>
+     <y>170</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/kstars/indi/collimationoverlayoptions.cpp b/kstars/indi/collimationoverlayoptions.cpp
new file mode 100644
index 0000000000..5bbfcdbcf4
--- /dev/null
+++ b/kstars/indi/collimationoverlayoptions.cpp
@@ -0,0 +1,517 @@
+/*
+    SPDX-FileCopyrightText: 2023 Jasem Mutlaq <mutlaqja at ikarustech.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "collimationoverlayoptions.h"
+#include <kstars_debug.h>
+
+#include "kstarsdata.h"
+#include "kstars.h"
+#include "qmetaobject.h"
+
+#include <QTimer>
+#include <QSqlTableModel>
+#include <QSqlRecord>
+#include <basedevice.h>
+#include <algorithm>
+
+CollimationOverlayOptions *CollimationOverlayOptions::m_Instance = nullptr;
+
+CollimationOverlayOptions *CollimationOverlayOptions::Instance(QWidget *parent)
+{
+    if (m_Instance == nullptr) {
+        m_Instance = new CollimationOverlayOptions(parent);
+    }
+    return m_Instance;
+}
+
+void CollimationOverlayOptions::release()
+{
+    delete(m_Instance);
+    m_Instance = nullptr;
+}
+
+CollimationOverlayOptions::CollimationOverlayOptions(QWidget *parent) : QDialog(parent)
+{
+#ifdef Q_OS_OSX
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+#endif
+
+    setupUi(this);
+
+    // Enable Checkbox
+    connect(EnableCheckBox, static_cast<void (QCheckBox::*)(int)>(&QCheckBox::stateChanged), this,
+            [this](int state) {
+                updateValue(state, "Enabled");
+            });
+
+    // Type Combo
+    connect(typeComboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
+            [this]() {
+                updateValue(typeComboBox, "Type");
+
+                // Anchor types can't have a size, rotation, count, PCD, thickness, or colour
+                if (typeComboBox->currentIndex() == 0) {
+                    sizeXSpinBox->setValue(0);
+                    sizeYSpinBox->setValue(0);
+                    rotationDoubleSpinBox->setValue(0.0);
+                    countSpinBox->setValue(0);
+                    pcdSpinBox->setValue(0);
+                    thicknessSpinBox->setEnabled(false);
+                    sizeXSpinBox->setEnabled(false);
+                    sizeYSpinBox->setEnabled(false);
+                    rotationDoubleSpinBox->setEnabled(false);
+                    countSpinBox->setEnabled(false);
+                    pcdSpinBox->setEnabled(false);
+                    thicknessSpinBox->setEnabled(false);
+                    colourButton->setColor("Black");
+                    colourButton->setEnabled(false);
+                } else {
+                    sizeXSpinBox->setEnabled(true);
+                    sizeYSpinBox->setEnabled(true);
+                    rotationDoubleSpinBox->setEnabled(true);
+                    countSpinBox->setEnabled(true);
+                    pcdSpinBox->setEnabled(true);
+                    thicknessSpinBox->setEnabled(true);
+                    colourButton->setEnabled(true);
+                }
+
+                // Default to linked XY size for Ellipse types only
+                if (typeComboBox->currentIndex() == 1) {
+                    linkXYB->setIcon(QIcon::fromTheme("document-encrypt"));
+                } else {
+                    linkXYB->setIcon(QIcon::fromTheme("document-decrypt"));
+                }
+
+                // Allow sizeY = 0 for lines
+                if (typeComboBox->currentIndex() == 3){
+                    sizeYSpinBox->setMinimum(0);
+                } else {
+                    sizeYSpinBox->setMinimum(1);
+                }
+            });
+
+    //Populate typeComboBox
+    QStringList typeValues;
+    collimationoverlaytype m_types;
+    const QMetaObject *m_metaobject = m_types.metaObject();
+    QMetaEnum m_metaEnum = m_metaobject->enumerator(m_metaobject->indexOfEnumerator("Types"));
+    for (int i = 0; i < m_metaEnum.keyCount(); i++) {
+        typeValues << tr(m_metaEnum.key(i));
+    }
+    typeComboBox->clear();
+    typeComboBox->addItems(typeValues);
+    typeComboBox->setCurrentIndex(0);
+
+    // SizeX SpinBox
+    connect(sizeXSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "SizeX");
+                if (linkXYB->icon().name() == "document-encrypt") {
+                    sizeYSpinBox->setValue(sizeXSpinBox->value());
+                    updateValue(value, "SizeY");
+                }
+            });
+
+    // SizeY SpinBox
+    connect(sizeYSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "SizeY");
+            });
+
+    //LinkXY Button
+    linkXYB->setIcon(QIcon::fromTheme("document-decrypt"));
+    connect(linkXYB, &QPushButton::clicked, this, [this] {
+        if (linkXYB->icon().name() == "document-decrypt") {
+            sizeYSpinBox->setValue(sizeXSpinBox->value());
+            linkXYB->setIcon(QIcon::fromTheme("document-encrypt"));
+            sizeYSpinBox->setEnabled(false);
+        } else if (linkXYB->icon().name() == "document-encrypt") {
+            linkXYB->setIcon(QIcon::fromTheme("document-decrypt"));
+            sizeYSpinBox->setEnabled(true);
+        }
+    });
+
+    // OffsetX SpinBox
+    connect(offsetXSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "OffsetX");
+            });
+
+    // OffsetY SpinBox
+    connect(offsetYSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "OffsetY");
+            });
+
+    // Count SpinBox
+    connect(countSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "Count");
+                // Single count elements can't have rotation or PCD
+                if (value == 1) {
+                    pcdSpinBox->setEnabled(false);
+                    rotationDoubleSpinBox->setEnabled(false);
+                } else {
+                    pcdSpinBox->setEnabled(true);
+                    rotationDoubleSpinBox->setEnabled(true);
+                }
+            });
+
+    //PCD SpinBox
+    connect(pcdSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "PCD");
+            });
+
+    // Rotation DoubleSpinBox
+    connect(rotationDoubleSpinBox, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this,
+            [this](double value) {
+                updateValue(value, "Rotation");
+            });
+
+    // Colour KColorButton
+    colourButton->setAlphaChannelEnabled(true);
+    connect(colourButton, static_cast<void (KColorButton::*)(const QColor&)>(&KColorButton::changed), this,
+            [this](QColor value) {
+                updateValue(value, "Colour");
+            });
+
+    // Thickness SpinBox
+    connect(thicknessSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
+            [this](int value) {
+                updateValue(value, "Thickness");
+            });
+
+    connect(addB, &QPushButton::clicked, this, [this]() {
+        if (addB->icon().name() == "dialog-ok-apply") {
+            elementNamesList->clearSelection();
+            addB->setIcon(QIcon::fromTheme("list-add"));
+            selectCollimationOverlayElement("");
+        } else {
+            addElement(nameLineEdit->text());
+            m_CollimationOverlayElementsModel->select();
+            refreshModel();
+            elementNamesList->clearSelection();
+        }
+    });
+
+    connect(removeB, &QPushButton::clicked, this, [this]() {
+        if (elementNamesList->currentItem() != nullptr) {
+            removeCollimationOverlayElement(elementNamesList->currentItem()->text());
+            refreshElements();
+            elementNamesList->clearSelection();
+            addB->setIcon(QIcon::fromTheme("list-add"));
+        }
+    });
+
+    connect(elementNamesList, &QListWidget::itemClicked, this, [this](QListWidgetItem * item) {
+                Q_UNUSED(item);
+                addB->setIcon(QIcon::fromTheme("list-add"));
+                removeB->setEnabled(true);
+            });
+
+    connect(elementNamesList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem * item) {
+        selectCollimationOverlayElement(item);
+        addB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
+        if (typeComboBox->currentIndex() == 1) {
+            linkXYB->setIcon(QIcon::fromTheme("document-encrypt"));
+        } else {
+            linkXYB->setIcon(QIcon::fromTheme("document-decrypt"));
+        }
+    });
+
+    connect(renameB, &QPushButton::clicked, this, [this] {
+        renameCollimationOverlayElement(nameLineEdit->text());
+    })  ;
+
+    connect(elementNamesList, &QListWidget::currentRowChanged, this, [this](int row) {
+        Q_UNUSED(row);
+        selectCollimationOverlayElement("");
+    });
+
+    initModel();
+}
+
+void CollimationOverlayOptions::initModel()
+{
+    m_CollimationOverlayElements.clear();
+    auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
+    m_CollimationOverlayElementsModel = new QSqlTableModel(this, userdb);
+    connect(m_CollimationOverlayElementsModel, &QSqlTableModel::dataChanged, this, [this]() {
+        m_CollimationOverlayElements.clear();
+        for (int i = 0; i < m_CollimationOverlayElementsModel->rowCount(); ++i) {
+            QVariantMap recordMap;
+            QSqlRecord record = m_CollimationOverlayElementsModel->record(i);
+            for (int j = 0; j < record.count(); j++)
+                recordMap[record.fieldName(j)] = record.value(j);
+
+            m_CollimationOverlayElements.append(recordMap);
+        }
+        m_ElementNames.clear();
+        for (auto &oneElement : m_CollimationOverlayElements) {
+            m_ElementNames << oneElement["Name"].toString();
+        }
+        elementNamesList->clear();
+        elementNamesList->addItems(m_ElementNames);
+        elementNamesList->setEditTriggers(QAbstractItemView::AllEditTriggers);
+        emit updated();
+    });
+    refreshModel();
+}
+
+void CollimationOverlayOptions::refreshModel()
+{
+    m_CollimationOverlayElements.clear();
+    KStars::Instance()->data()->userdb()->GetCollimationOverlayElements(m_CollimationOverlayElements);
+    m_ElementNames.clear();
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        m_ElementNames << oneElement["Name"].toString();
+    }
+    elementNamesList->clear();
+    elementNamesList->addItems(m_ElementNames);
+}
+
+QString CollimationOverlayOptions::addElement(const QString &name)
+{
+    QVariantMap element;
+    element["Name"] = uniqueElementName(name, typeComboBox->currentText());
+    element["Enabled"] = EnableCheckBox->checkState();
+    element["Type"] = typeComboBox->currentText();
+    element["SizeX"] = sizeXSpinBox->value();
+    element["SizeY"] = sizeYSpinBox->value();
+    element["OffsetX"] = offsetXSpinBox->value();
+    element["OffsetY"] = offsetYSpinBox->value();
+    element["Count"] = countSpinBox->value();
+    element["PCD"] = pcdSpinBox->value();
+    element["Rotation"] = rotationDoubleSpinBox->value();
+    element["Colour"] = colourButton->color();
+    element["Thickness"] = thicknessSpinBox->value();
+
+    KStarsData::Instance()->userdb()->AddCollimationOverlayElement(element);
+    emit updated();
+    return element["Name"].toString();
+}
+
+bool CollimationOverlayOptions::setCollimationOverlayElementValue(const QString &name, const QString &field, const QVariant &value)
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["Name"].toString() == name) {
+            // If value did not change, just return true
+            if (oneElement[field] == value) {
+                return true;
+            }
+            // Update field and database.
+            oneElement[field] = value;
+            KStarsData::Instance()->userdb()->UpdateCollimationOverlayElement(oneElement, oneElement["id"].toInt());
+            emit updated();
+            return true;
+        }
+    }
+    return false;
+}
+
+void CollimationOverlayOptions::renameCollimationOverlayElement(const QString &name)
+{
+    if (m_CurrentElement != nullptr && (*m_CurrentElement)["Name"] != name) {
+        auto pos = elementNamesList->currentRow();
+        // ensure element name uniqueness
+        auto unique = uniqueElementName(name, (*m_CurrentElement)["Type"].toString());
+        // update the element database entry
+        setCollimationOverlayElementValue((*m_CurrentElement)["Name"].toString(), "Name", unique);
+        // propagate the unique name to the current selection
+        elementNamesList->currentItem()->setText(unique);
+        // refresh the trains
+        refreshElements();
+        // refresh selection
+        elementNamesList->setCurrentRow(pos);
+        selectCollimationOverlayElement(unique);
+    }
+}
+
+bool CollimationOverlayOptions::setCollimationOverlayElement(const QJsonObject &element)
+{
+    auto oneElement = getCollimationOverlayElement(element["id"].toInt());
+    if (!oneElement.empty()) {
+        KStarsData::Instance()->userdb()->UpdateCollimationOverlayElement(element.toVariantMap(), oneElement["id"].toInt());
+        refreshElements();
+        return true;
+    }
+    return false;
+}
+
+bool CollimationOverlayOptions::removeCollimationOverlayElement(const QString &name)
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["Name"].toString() == name) {
+            auto id = oneElement["id"].toInt();
+            KStarsData::Instance()->userdb()->DeleteCollimationOverlayElement(id);
+            refreshElements();
+            return true;
+        }
+    }
+    return false;
+}
+
+QString CollimationOverlayOptions::uniqueElementName(QString name, QString type)
+{
+    if ("" == name) name = type;
+    QString result = name;
+    int nr = 1;
+    while (m_ElementNames.contains(result)) {
+        result = QString("%1 (%2)").arg(name).arg(nr++);
+    }
+    return result;
+}
+
+bool CollimationOverlayOptions::selectCollimationOverlayElement(QListWidgetItem *item)
+{
+    if (item != nullptr && selectCollimationOverlayElement(item->text())) {
+        return true;
+    }
+    return false;
+}
+
+bool CollimationOverlayOptions::selectCollimationOverlayElement(const QString &name)
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["Name"].toString() == name) {
+            editing = true;
+            m_CurrentElement = &oneElement;
+            nameLineEdit->setText(oneElement["Name"].toString());
+            renameB->setEnabled(true);
+            EnableCheckBox->setChecked(oneElement["Enabled"].toUInt());
+            typeComboBox->setCurrentText(oneElement["Type"].toString());
+            sizeXSpinBox->setValue(oneElement["SizeX"].toUInt());
+            sizeYSpinBox->setValue(oneElement["SizeY"].toUInt());
+            offsetXSpinBox->setValue(oneElement["OffsetX"].toUInt());
+            offsetYSpinBox->setValue(oneElement["OffsetY"].toUInt());
+            countSpinBox->setValue(oneElement["Count"].toUInt());
+            pcdSpinBox->setValue(oneElement["PCD"].toUInt());
+            rotationDoubleSpinBox->setValue(oneElement["Rotation"].toDouble());
+            QColor tempColour;
+            tempColour.setNamedColor(oneElement["Colour"].toString());
+            colourButton->setColor(tempColour);
+            thicknessSpinBox->setValue(oneElement["Thickness"].toUInt());
+            removeB->setEnabled(m_CollimationOverlayElements.length() > 0);
+            elementConfigBox->setEnabled(true);
+            return true;
+        }
+    }
+
+    // none found
+    editing = false;
+    nameLineEdit->setText("");
+    renameB->setEnabled(false);
+    EnableCheckBox->setCheckState(Qt::Checked);
+    typeComboBox->setCurrentText("--");
+    sizeXSpinBox->setValue(0);
+    sizeYSpinBox->setValue(0);
+    offsetXSpinBox->setValue(0);
+    offsetYSpinBox->setValue(0);
+    countSpinBox->setValue(0);
+    pcdSpinBox->setValue(0);
+    rotationDoubleSpinBox->setValue(0.0);
+    QColor tempColour;
+    tempColour.setNamedColor("White");
+    colourButton->setColor(tempColour);
+    thicknessSpinBox->setValue(1);
+
+    removeB->setEnabled(false);
+    return false;
+}
+
+void CollimationOverlayOptions::openEditor()
+{
+    initModel();
+    show();
+}
+
+const QVariantMap CollimationOverlayOptions::getCollimationOverlayElement(uint8_t id) const
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["id"].toInt() == id)
+            return oneElement;
+    }
+    return QVariantMap();
+}
+
+bool CollimationOverlayOptions::exists(uint8_t id) const
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["id"].toInt() == id)
+            return true;
+    }
+    return false;
+}
+
+const QVariantMap CollimationOverlayOptions::getCollimationOverlayElement(const QString &name) const
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["Name"].toString() == name) {
+            return oneElement;
+        }
+    }
+    return QVariantMap();
+}
+
+void CollimationOverlayOptions::refreshElements()
+{
+    refreshModel();
+    emit updated();
+}
+
+int CollimationOverlayOptions::id(const QString &name) const
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["Name"].toString() == name)
+            return oneElement["id"].toUInt();
+    }
+    return -1;
+}
+
+QString CollimationOverlayOptions::name(int id) const
+{
+    for (auto &oneElement : m_CollimationOverlayElements) {
+        if (oneElement["id"].toInt() == id)
+            return oneElement["name"].toString();
+    }
+    return QString();
+}
+
+void CollimationOverlayOptions::updateValue(QComboBox *cb, const QString &element)
+{
+    if (elementNamesList->currentItem() != nullptr && editing == true) {
+        setCollimationOverlayElementValue(elementNamesList->currentItem()->text(), element, cb->currentText());
+    }
+}
+
+void CollimationOverlayOptions::updateValue(double value, const QString &element)
+{
+    if (elementNamesList->currentItem() != nullptr && editing == true) {
+        setCollimationOverlayElementValue(elementNamesList->currentItem()->text(), element, value);
+    }
+}
+
+void CollimationOverlayOptions::updateValue(int value, const QString &element)
+{
+    if (elementNamesList->currentItem() != nullptr && editing == true) {
+        setCollimationOverlayElementValue(elementNamesList->currentItem()->text(), element, value);
+    }
+}
+
+void CollimationOverlayOptions::updateValue(QColor value, const QString &element)
+{
+    if (elementNamesList->currentItem() != nullptr && editing == true) {
+        setCollimationOverlayElementValue(elementNamesList->currentItem()->text(), element, value);
+    }
+}
+
+void CollimationOverlayOptions::updateValue(QString value, const QString &element)
+{
+    if (elementNamesList->currentItem() != nullptr && editing == true) {
+        setCollimationOverlayElementValue(elementNamesList->currentItem()->text(), element, value);
+    }
+}
diff --git a/kstars/indi/collimationoverlayoptions.h b/kstars/indi/collimationoverlayoptions.h
new file mode 100644
index 0000000000..055edae3c6
--- /dev/null
+++ b/kstars/indi/collimationoverlayoptions.h
@@ -0,0 +1,175 @@
+/*
+    SPDX-FileCopyrightText: 2023 Jasem Mutlaq <mutlaqja at ikarustech.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include "ui_collimationOptions.h"
+#include "collimationoverlaytypes.h"
+
+#include <QDialog>
+#include <QSqlDatabase>
+#include <QQueue>
+#include <QPointer>
+#include <QMetaEnum>
+
+class QSqlTableModel;
+
+class CollimationOverlayOptions: public QDialog, public Ui::collimationOptions
+{
+        Q_OBJECT
+        Q_PROPERTY(QList <QString> elementNames READ getElementNames)
+
+    public:
+
+        static CollimationOverlayOptions *Instance(QWidget *parent);
+        static void release();
+
+        void checkElements();
+
+        bool exists(uint8_t id) const;
+        const QVariantMap getCollimationOverlayElement(uint8_t id) const;
+        const QVariantMap getCollimationOverlayElement(const QString &name) const;
+        const QList<QVariantMap> &getCollimationOverlayElements() const
+        {
+            return m_CollimationOverlayElements;
+        }
+        const QList <QString> &getElementNames() const
+        {
+            return m_ElementNames;
+        }
+
+        /**
+         * @brief Select a collimation overlay element and fill the field values in the element editor
+         *        with the appropriate values of the selected collimation overlay element.
+         * @param item collimation overlay element list item
+         * @return true if collimation overlay element found
+         */
+        bool selectCollimationOverlayElement(QListWidgetItem *item);
+
+        /**
+         * @brief Select a collimation overlay element and fill the field values in the element editor
+         *        with the appropriate values of the selected collimation overlay element.
+         * @param name collimation overlay element name
+         * @return true if collimation overlay element found
+         */
+        bool selectCollimationOverlayElement(const QString &name);
+
+        /**
+         * @brief Show the dialog and select a collimation overlay element for editing.
+         * @param name collimation overlay element name
+         */
+        void openEditor();
+
+        /**
+         * @brief setCollimationOverlayElementValue Set specific field of collimation overlay element
+         * @param name Name of collimation overlay element
+         * @param field Name of element field
+         * @param value Value of element field
+         * @return True if set is successful, false otherwise.
+         */
+        bool setCollimationOverlayElementValue(const QString &name, const QString &field, const QVariant &value);
+
+        /**
+         * @brief Change the name of the currently selected collimation overlay element to a new value
+         * @param name new element name
+         */
+        void renameCollimationOverlayElement(const QString &name);
+
+        /**
+         * @brief setCollimationOverlayElement Replaces collimation overlay element matching the name of the passed element.
+         * @param train element information, including name and database id
+         * @return True if element is successfully updated in the database.
+         */
+        bool setCollimationOverlayElement(const QJsonObject &element);
+
+        /**
+         * @brief removeCollimationOverlayElement Remove collimation overlay element from database and all associated settings
+         * @param name name of the element to remove
+         * @return True if successful, false if id is not found.
+         */
+        bool removeCollimationOverlayElement(const QString &name);
+
+        void refreshModel();
+        void refreshElements();
+
+        /**
+         * @brief syncValues Sync delegates and then update model accordingly.
+         */
+        void syncValues();
+
+        /**
+         * @brief id Get database ID for a given element
+         * @param name Name of element
+         * @return ID if exists, or -1 if not found.
+         */
+        int id(const QString &name) const;
+
+        /**
+         * @brief name Get database name for a given id
+         * @param id database ID for the element to get
+         * @return Element name, or empty string if not found.
+         */
+        QString name(int id) const;
+
+    signals:
+        void updated();
+
+    protected:
+        void initModel();
+
+    private slots:
+        /**
+         * @brief Update a value in the currently selected element
+         * @param cb combo box holding the new value
+         * @param element value name
+         */
+        void updateValue(QComboBox *cb, const QString &valueName);
+        /**
+         * @brief Update a value in the currently selected element
+         * @param value the new value
+         * @param element element name
+         */
+        void updateValue(double value, const QString &valueName);
+        void updateValue(int value, const QString &valueName);
+        void updateValue(QColor value, const QString &valueName);
+        void updateValue(QString value, const QString &valueName);
+
+    private:
+
+        CollimationOverlayOptions(QWidget *parent);
+        static CollimationOverlayOptions *m_Instance;
+
+        /**
+         * @brief generateOpticalTrains Automatically generate optical trains based on the current profile information.
+         * This happens when users use the tool for the first time.
+         */
+        void generateElement();
+
+        /**
+         * @brief Add a new collimation overlay element with the given name
+         * @param name element name
+         * @return unique element name
+         */
+        QString addElement(const QString &name);
+
+        /**
+         * @brief Create a unique element name
+         * @param name original element name
+         * @param type element type
+         * @return name, eventually added (i) to make the element name unique
+         */
+        QString uniqueElementName(QString name, QString type);
+
+        QList<QVariantMap> m_CollimationOverlayElements;
+        QVariantMap *m_CurrentElement = nullptr;
+
+        bool editing = false;
+
+        // Table model
+        QSqlTableModel *m_CollimationOverlayElementsModel = { nullptr };
+
+        QList <QString> m_ElementNames;
+};
diff --git a/kstars/indi/collimationoverlaytypes.h b/kstars/indi/collimationoverlaytypes.h
new file mode 100644
index 0000000000..2076b315e7
--- /dev/null
+++ b/kstars/indi/collimationoverlaytypes.h
@@ -0,0 +1,24 @@
+/*
+    SPDX-FileCopyrightText: 2022 Jasem Mutlaq <mutlaqja at ikarustech.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <QMetaEnum>
+
+#pragma once
+
+class collimationoverlaytype : QObject
+{
+    Q_OBJECT
+
+  public:
+        enum Types
+        {
+            Anchor,
+            Ellipse,
+            Rectangle,
+            Line
+        };
+        Q_ENUM(Types);
+};
diff --git a/kstars/indi/elementinfo.h b/kstars/indi/elementinfo.h
new file mode 100644
index 0000000000..e77e3bf568
--- /dev/null
+++ b/kstars/indi/elementinfo.h
@@ -0,0 +1,57 @@
+/*
+    SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja at ikarustech.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include <QMap>
+#include <QString>
+#include <QJsonObject>
+
+class ElementInfo
+{
+    public:
+        ElementInfo(int id, const QString &name);
+        ~ElementInfo() = default;
+
+        // Is connection local or remote
+//        bool isLocal()
+//        {
+//            return host.isEmpty();
+//        }
+//        QJsonObject toJson() const;
+
+//        QString mount() const;
+//        QString ccd() const;
+//        QString guider() const;
+//        QString focuser() const;
+//        QString filter() const;
+//        QString dome() const;
+//        QString ao() const;
+//        QString weather() const;
+//        QString aux1() const;
+//        QString aux2() const;
+//        QString aux3() const;
+//        QString aux4() const;
+//        QString remoteDrivers() const;
+//
+//        QString name;
+//        QString host;
+//        QString city;
+//        QString province;
+//        QString country;
+//        int guidertype { 0 };
+//        int guiderport { 0 };
+//        int indihub { 0 };
+//        QString remotedrivers;
+//        QString guiderhost;
+//        QByteArray scripts;
+//        int id { 0 };
+//        int port { -1 };
+//        bool autoConnect { false };
+//        bool portSelector {false};
+//        int INDIWebManagerPort { -1 };
+//        QMap<QString, QString> drivers;
+};
diff --git a/kstars/indi/streamform.ui b/kstars/indi/streamform.ui
index a30f12bf72..e074d69452 100644
--- a/kstars/indi/streamform.ui
+++ b/kstars/indi/streamform.ui
@@ -153,6 +153,56 @@
        </property>
       </widget>
      </item>
+     <item>
+      <widget class="QPushButton" name="collimationB">
+       <property name="minimumSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Toggle collimation overlay</string>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="iconSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="collimationOptionsB">
+       <property name="minimumSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>64</width>
+         <height>64</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Collimation overlay options</string>
+       </property>
+       <property name="whatsThis">
+        <string/>
+       </property>
+       <property name="iconSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+      </widget>
+     </item>
      <item>
       <widget class="QComboBox" name="zoomLevelCombo">
        <property name="sizePolicy">
diff --git a/kstars/indi/streamwg.cpp b/kstars/indi/streamwg.cpp
index 7ed493fda4..bc1e117b4a 100644
--- a/kstars/indi/streamwg.cpp
+++ b/kstars/indi/streamwg.cpp
@@ -8,12 +8,11 @@
 
 #include "streamwg.h"
 
-#include "indistd.h"
-#include "driverinfo.h"
-#include "clientmanager.h"
 #include "kstars.h"
 #include "Options.h"
 #include "kstars_debug.h"
+#include "collimationoverlayoptions.h"
+#include "qobjectdefs.h"
 
 #include <basedevice.h>
 
@@ -75,9 +74,18 @@ StreamWG::StreamWG(ISD::Camera *ccd) : QDialog(KStars::Instance())
     processStream = colorFrame = isRecording = false;
 
     options = new RecordOptions(this);
-
     connect(optionsB, SIGNAL(clicked()), options, SLOT(show()));
 
+    collimationOptionsB->setIcon(QIcon::fromTheme("run-build-prune"));
+    connect(collimationOptionsB, &QPushButton::clicked, this, [this]()
+            {
+                CollimationOverlayOptions::Instance(this)->openEditor();
+            });
+
+    collimationB->setIcon(QIcon::fromTheme("crosshairs"));
+    connect(CollimationOverlayOptions::Instance(this), SIGNAL(updated()), videoFrame, SLOT(modelChanged()));
+    connect(collimationB, &QPushButton::clicked, videoFrame, &VideoWG::toggleOverlay);
+
     QString filename, directory;
     ccd->getSERNameDirectory(filename, directory);
 
@@ -428,3 +436,8 @@ void StreamWG::updateFPS(double instantFPS, double averageFPS)
     //instFPS->setText(QString::number(instantFPS, 'f', 1));
     avgFPS->setText(QString::number(averageFPS, 'f', 1));
 }
+
+StreamWG::~StreamWG()
+{
+    CollimationOverlayOptions::Instance(this)->release();
+}
diff --git a/kstars/indi/streamwg.h b/kstars/indi/streamwg.h
index c3e2dff299..c4bab7e7ef 100644
--- a/kstars/indi/streamwg.h
+++ b/kstars/indi/streamwg.h
@@ -44,7 +44,7 @@ class StreamWG : public QDialog, public Ui::streamForm
 
     public:
         explicit StreamWG(ISD::Camera *ccd);
-        virtual ~StreamWG() override = default;
+        virtual ~StreamWG() override;
 
         void setColorFrame(bool color);
         void setSize(int wd, int ht);
@@ -91,6 +91,7 @@ class StreamWG : public QDialog, public Ui::streamForm
         bool processStream;
         int streamWidth, streamHeight;
         bool colorFrame, isRecording;
+        bool showOverlay = false;
         QIcon recordIcon, stopIcon;
         ISD::Camera *m_Camera {nullptr};
 
@@ -103,5 +104,7 @@ class StreamWG : public QDialog, public Ui::streamForm
 
         // For Canon DSLRs
         INDI::Property *eoszoom {nullptr}, *eoszoomposition {nullptr};
+
+        // Options panels
         RecordOptions *options;
 };
diff --git a/kstars/indi/videowg.cpp b/kstars/indi/videowg.cpp
index 8832454c3a..596b3f8313 100644
--- a/kstars/indi/videowg.cpp
+++ b/kstars/indi/videowg.cpp
@@ -5,13 +5,19 @@
 */
 
 #include "videowg.h"
+#include "collimationoverlaytypes.h"
 
 #include "kstars_debug.h"
+#include "kstarsdata.h"
+#include "kstars.h"
 
 #include <QImageReader>
 #include <QMouseEvent>
 #include <QResizeEvent>
 #include <QRubberBand>
+#include <QSqlTableModel>
+#include <QSqlRecord>
+#include <QtMath>
 
 VideoWG::VideoWG(QWidget *parent) : QLabel(parent)
 {
@@ -60,6 +66,9 @@ bool VideoWG::newFrame(IBLOB *bp)
     if (rc)
     {
         kPix = QPixmap::fromImage(streamImage->scaled(size(), Qt::KeepAspectRatio));
+
+        paintOverlay(kPix);
+
         setPixmap(kPix);
     }
 
@@ -168,6 +177,9 @@ bool VideoWG::debayer(const IBLOB *bp, const BayerParams &params)
     if (rc)
     {
         kPix = QPixmap::fromImage(streamImage->scaled(size(), Qt::KeepAspectRatio));
+
+        paintOverlay(kPix);
+
         setPixmap(kPix);
     }
 
@@ -177,3 +189,138 @@ bool VideoWG::debayer(const IBLOB *bp, const BayerParams &params)
     return rc;
 }
 
+void VideoWG::paintOverlay(QPixmap &imagePix)
+{
+    if (!overlayEnabled || m_EnabledOverlayElements.count() == 0) return;
+
+    // Anchor - default to centre of image
+    QPointF m_anchor (static_cast<float>(kPix.width() / 2), static_cast<float>(kPix.height()/2));
+    scale = (static_cast<float>(kPix.width()) / static_cast<float>(streamW));
+
+    // Apply any offset from (only) the first enabled anchor element
+    bool foundAnchor = false;
+    for (auto &oneElement : m_EnabledOverlayElements) {
+        if (oneElement["Type"] == "Anchor" && !foundAnchor) {
+            m_anchor.setX(m_anchor.x() + oneElement["OffsetX"].toInt());
+            m_anchor.setY(m_anchor.y() + oneElement["OffsetY"].toInt());
+            foundAnchor = true;
+        }
+    }
+
+    painter->begin(&imagePix);
+    painter->translate(m_anchor);
+    painter->scale(scale, scale);
+
+    for (auto &currentElement : m_EnabledOverlayElements) {
+
+        painter->save();
+        QPen m_pen = QPen(QColor(currentElement["Colour"].toString()));
+        m_pen.setWidth(currentElement["Thickness"].toUInt());
+        m_pen.setCapStyle(Qt::FlatCap);
+        m_pen.setJoinStyle(Qt::MiterJoin);
+        painter->setPen(m_pen);
+        painter->translate(currentElement["OffsetX"].toFloat(), currentElement["OffsetY"].toFloat());
+
+        int m_count = currentElement["Count"].toUInt();
+        float m_pcd = currentElement["PCD"].toFloat();
+
+        if (m_count == 1) {
+            PaintOneItem(currentElement["Type"].toString(), QPointF(0.0, 0.0), currentElement["SizeX"].toUInt(), currentElement["SizeY"].toUInt(), currentElement["Thickness"].toUInt());
+        } else if (m_count > 1) {
+            float slice = 360 / m_count;
+            for (int i = 0; i < m_count; i++) {
+                painter->save();
+                painter->rotate((slice * i) + currentElement["Rotation"].toFloat());
+                PaintOneItem(currentElement["Type"].toString(), QPointF((m_pcd / 2), 0.0), currentElement["SizeX"].toUInt(), currentElement["SizeY"].toUInt(), currentElement["Thickness"].toUInt());
+                painter->restore();
+            }
+        }
+
+        painter->restore();
+     }
+    painter->end();
+}
+
+void VideoWG::setupOverlay ()
+{
+    if (overlayEnabled) {
+        initOverlayModel();
+
+        typeValues = new QStringList;
+        collimationoverlaytype m_types;
+        const QMetaObject *m_metaobject = m_types.metaObject();
+        QMetaEnum m_metaEnum = m_metaobject->enumerator(m_metaobject->indexOfEnumerator("Types"));
+        for (int i = 0; i < m_metaEnum.keyCount(); i++) {
+            *typeValues << tr(m_metaEnum.key(i));
+        }
+
+        painter = new QPainter;
+    }
+}
+
+void VideoWG::initOverlayModel()
+{
+    m_CollimationOverlayElements.clear();
+    auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
+    m_CollimationOverlayElementsModel = new QSqlTableModel(this, userdb);
+    modelChanged();
+}
+
+void VideoWG::modelChanged()
+{
+    m_CollimationOverlayElements.clear();
+    m_EnabledOverlayElements.clear();
+    KStars::Instance()->data()->userdb()->GetCollimationOverlayElements(m_CollimationOverlayElements);
+    for (auto &oneElement : m_CollimationOverlayElements)
+        if (oneElement["Enabled"] == Qt::Checked)
+            m_EnabledOverlayElements.append(oneElement);
+}
+
+void VideoWG::PaintOneItem (QString type, QPointF position, int sizeX, int sizeY, int thickness)
+{
+    float m_sizeX = sizeX - (thickness / 2);
+    float m_sizeY = sizeY - (thickness / 2);
+
+    switch (typeValues->indexOf(type)) {
+case 0: // Anchor - ignore as we're not drawing it
+    break;
+
+case 1: // Ellipse
+    painter->drawEllipse(position, m_sizeX, m_sizeY);
+    break;
+
+case 2: // Rectangle
+{
+    QRect m_rect((position.x() - (m_sizeX / 2)), (position.y() - (m_sizeY / 2)), (m_sizeX - (thickness / 2)), (m_sizeY - (thickness / 2)));
+    painter->drawRect(m_rect);
+    break;
+}
+
+case 3: // Line
+    painter->drawLine(position.x(), position.y(), sizeX, sizeY);
+    break;
+
+default:
+    break;
+    };
+}
+
+void VideoWG::toggleOverlay()
+{
+    if (overlayEnabled == false) {
+        overlayEnabled = true;
+        if (m_CollimationOverlayElementsModel == nullptr) {
+            setupOverlay();
+        }
+    } else if (overlayEnabled == true) {
+        overlayEnabled = false;
+    }
+}
+
+VideoWG::~VideoWG()
+{
+    delete m_CollimationOverlayElementsModel;
+    delete m_CurrentElement;
+    delete typeValues;
+    delete painter;
+}
diff --git a/kstars/indi/videowg.h b/kstars/indi/videowg.h
index a2b1fcc069..7986eac3a4 100644
--- a/kstars/indi/videowg.h
+++ b/kstars/indi/videowg.h
@@ -14,12 +14,16 @@
 #include <QVector>
 #include <QColor>
 #include <QLabel>
+#include <QSqlDatabase>
+#include <QPen>
+#include <QPainter>
 
 #include <memory>
 #include <mutex>
 
 class QImage;
 class QRubberBand;
+class QSqlTableModel;
 
 class VideoWG : public QLabel
 {
@@ -27,7 +31,7 @@ class VideoWG : public QLabel
 
     public:
         explicit VideoWG(QWidget *parent = nullptr);
-        virtual ~VideoWG() override = default;
+        virtual ~VideoWG() override;
 
         bool newFrame(IBLOB *bp);
         bool newBayerFrame(IBLOB *bp, const BayerParams &params);
@@ -41,6 +45,11 @@ class VideoWG : public QLabel
         void mousePressEvent(QMouseEvent *event) override;
         void mouseMoveEvent(QMouseEvent *event) override;
         void mouseReleaseEvent(QMouseEvent *event) override;
+        void initOverlayModel();
+
+    public slots:
+        void modelChanged();
+        void toggleOverlay();
 
     signals:
         void newSelection(QRect);
@@ -59,4 +68,17 @@ class VideoWG : public QLabel
         QPoint origin;
         QString m_RawFormat;
         bool m_RawFormatSupported { false };
+
+        // Collimation Overlay
+        void setupOverlay();
+        void paintOverlay(QPixmap &imagePix);
+        bool overlayEnabled = false;
+        QSqlTableModel *m_CollimationOverlayElementsModel = { nullptr };
+        QList<QVariantMap> m_CollimationOverlayElements;
+        QList<QVariantMap> m_EnabledOverlayElements;
+        QVariantMap *m_CurrentElement = nullptr;
+        QStringList *typeValues = nullptr;
+        QPainter *painter = nullptr;
+        float scale;
+        void PaintOneItem (QString type, QPointF position, int sizeX, int sizeY, int thickness);
 };


More information about the kde-doc-english mailing list