[education/kstars] /: Introduce Views: A way to quickly reorient the sky-map to match the view through an instrument

Jasem Mutlaq null at kde.org
Tue Mar 19 01:15:23 GMT 2024


Git commit 40f1d87fd9c86525b3c85da5eb4e75c763e66ab7 by Jasem Mutlaq, on behalf of Akarsh Simha.
Committed on 19/03/2024 at 01:15.
Pushed by mutlaqja into branch 'master'.

Introduce Views: A way to quickly reorient the sky-map to match the view through an instrument

This merge request improves the ability to orient the sky map to match the view through any instrument by introducing "Views"

A view is a collection of settings: the orientation of the sky map, how the orientation changes as the sky map is panned, whether it is mirrored or not, and optionally the field-of-view to set the map to.

If no views are defined, KStars introduces a set of standard / "demo" views by default. Existing views can be edited and new views can be added using the "Edit Views..." interface. They can also be re-ordered in the interface. The ordering of the views in the "Edit Views..." dialog defines the order in which views will be cycled through using the keyboard shortcuts Shift + Page Up and Shift + Page Down. Thus, you can set up the views for easily switching between naked eye / finder scope / telescope views for easy star-hopping.

M  +51   -7    doc/config.docbook
A  +-    --    doc/newview.png
A  +-    --    doc/viewmanager.png
M  +4    -0    kstars/CMakeLists.txt
M  +9    -1    kstars/auxiliary/dms.cpp
M  +3    -0    kstars/auxiliary/dms.h
M  +110  -10   kstars/auxiliary/ksuserdb.cpp
M  +17   -3    kstars/auxiliary/ksuserdb.h
A  +205  -0    kstars/auxiliary/skymapview.cpp     [License: GPL(v2.0+)]
A  +81   -0    kstars/auxiliary/skymapview.h     [License: GPL(v2.0+)]
M  +1    -0    kstars/data/kstars.qrc
M  +2    -0    kstars/data/kstarsui.rc
A  +-    --    kstars/data/observer.png
A  +399  -0    kstars/dialogs/newview.ui
A  +420  -0    kstars/dialogs/viewsdialog.cpp     [License: GPL(v2.0+)]
A  +104  -0    kstars/dialogs/viewsdialog.h     [License: GPL(v2.0+)]
A  +139  -0    kstars/dialogs/viewsdialog.ui
M  +32   -2    kstars/kstars.cpp
M  +16   -0    kstars/kstars.h
M  +14   -3    kstars/kstars.kcfg
M  +73   -16   kstars/kstarsactions.cpp
M  +66   -7    kstars/kstarsinit.cpp
M  +1    -0    kstars/skycomponents/hipscomponent.cpp
M  +43   -4    kstars/skymap.cpp
M  +3    -0    kstars/skymap.h
M  +18   -4    kstars/skymapevents.cpp
M  +24   -0    kstars/widgets/unitspinboxwidget.cpp
M  +34   -13   kstars/widgets/unitspinboxwidget.h
M  +23   -20   kstars/widgets/unitspinboxwidget.ui

https://invent.kde.org/education/kstars/-/commit/40f1d87fd9c86525b3c85da5eb4e75c763e66ab7

diff --git a/doc/config.docbook b/doc/config.docbook
index c107223f6a..f3e24af35d 100644
--- a/doc/config.docbook
+++ b/doc/config.docbook
@@ -1624,31 +1624,75 @@ from the <menuchoice><guimenu>Settings</guimenu><guisubmenu>FOV Symbols</guisubm
     You can tweak various settings to make the orientation of the sky map match the view through your optical instrument.
   </para>
   <para>
-    First, pick the coordinate system that matches your mount. For an equatorially mounted instrument, switch to the Equatorial Coordinate mode in the <guimenu>View</guimenu> menu. The option to toggle the coordinate system should read <guilabel>Switch to Horizontal View (Horizontal Coordinates)</guilabel> when the current mode is Equatorial Coordinates. For an altazimuth-mounted instrument or naked-eye viewing, switch to Horizontal Coordinates, so that the option in the <guimenu>View</guimenu> menu reads <guilabel>Switch to Star Globe View (Equatorial Coordinates)</guilabel>. This sets the base coordinate system used to render the sky map, and also sets the reference for the orientation of the skymap: zenith or north.
+    First, pick the coordinate system that matches your mount. For an equatorially mounted instrument, switch to the Equatorial Coordinate mode in the <guimenu>View</guimenu> menu or by pressing the <keycap>Space</keycap> key. The option to toggle the coordinate system should read <guilabel>Switch to Horizontal View (Horizontal Coordinates)</guilabel> when the current mode is Equatorial Coordinates. For an altazimuth-mounted instrument or naked-eye viewing, switch to Horizontal Coordinates, so that the option in the <guimenu>View</guimenu> menu reads <guilabel>Switch to Star Globe View (Equatorial Coordinates)</guilabel>. This sets the base coordinate system used to render the sky map, and also sets the reference for the orientation of the skymap: zenith or north.
   </para>
   <para>
-    If your instrument is using an erecting prism, typically used on Schmidt-Cassegrain and refracting type telescopes, the view through the eyepiece will be mirrored horizontally. You can have the sky map match this by checking the <guilabel>Mirrored View</guilabel> option under the <guimenu>View</guimenu> menu.
+    If your instrument is using an erecting prism, typically used on Schmidt-Cassegrain and refracting type telescopes, the view through the eyepiece will be mirrored horizontally. You can have the sky map match this by checking the <guilabel>Mirrored View</guilabel> option under the <guimenu>View</guimenu> menu, or using the key combination <keycombo>&Ctrl;&Shift;<keycap>M</keycap></keycombo>.
   </para>
   <para>
-    Next, to rotate the sky map freely, you can hold down the &Shift; key and drag the mouse on the sky map. A temporary overlay will appear showing the direction of north and zenith at the point, and displaying the angle they make with the vertical in a counterclockwise sense. The orientations of zenith and north will update as you rotate the sky map. Letting go of &Shift; or the mouse button will stop the rotation operation. As you pan the sky map or focus it on different objects, the rotation you set is retained as an offset from the reference direction. The reference direction is north when using Equatorial Coordinates and zenith when using Horizontal Coordinates. As a reminder, the reference direction is solid and brighter in the temporary overlay. For the two common orientations of erect and inverted, the rotation can be set / reset using the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu. Select "North Down" or "Zenith Down" as is applicable to set an orientation of 180 degrees.
+    Next, to rotate the sky map freely, you can hold down the &Shift; key and drag the mouse on the sky map. A temporary overlay will appear showing the direction of north and zenith at the point, and displaying the angle they make with the vertical in a counterclockwise sense. The orientations of zenith and north will update as you rotate the sky map. Letting go of &Shift; or the mouse button will stop the rotation operation. As you pan the sky map or focus it on different objects, the rotation you set is retained as an offset from the reference direction. The reference direction is north when using Equatorial Coordinates and zenith when using Horizontal Coordinates. As a reminder, the reference direction is solid and brighter in the temporary overlay. The temporary overlay also marks the East direction, which will be clockwise from north when mirrored and counter-clockwise when not mirrored. For the two common orientations of erect and inverted, the rotation can be set / reset using the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu. Select <guilabel>North Down</guilabel> or <guilabel>Zenith Down</guilabel> as is applicable to set an orientation of 180 degrees.
   </para>
   <para>
-    If you are visually observing through an eyepiece of an instrument, you may need to do some more correction. For the common case of a large Dobsonian telescope (or more generally a Newtonian design mounted on an altazimuth mount), a systematic additional correction is of help. This correction applies because we stand erect while using the telescope irrespective of the angle the telescope tube is making with the ground. So as we move the telescope in altitude, an additional correction depending on the altitude of the object needs to be applied to make the sky map match the view through the eyepiece. This correction is enabled by checking the <guilabel>Erect observer correction</guilabel> checkbox in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu. This correction only makes sense in Horizontal Coordinate mode and is disabled when using equatorial coordinates.
+    If you are visually observing through an eyepiece of an instrument, you may need to do some more correction. For the common case of a large Dobsonian telescope (or more generally a Newtonian design mounted on an altazimuth mount), an additional systematic correction is of help. This correction applies because we stand erect while using the telescope irrespective of the angle the telescope tube is making with the ground. So as we move the telescope in altitude, an additional correction depending on the altitude of the object needs to be applied to make the sky map match the view through the eyepiece where the observer is standing erect. This correction is enabled by choosing the appropriate "Erect observer correction" option in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu. The correction depends on which side the telescope's focuser is placed by the manufacturer. If when observing just above the horizon through the eyepiece, the sky is on the observer's right side (and the mirror to the left), pick the <guilabel>Erect observer correction, right-handed</guilabel> option. Similarly, if the sky is to the left of the observer, choose the <guilabel>Erect observer correct, left-handed</guilabel> option. This correction only makes sense in Horizontal Coordinate mode and is disabled when using equatorial coordinates.
   </para>
   <para>
-    Finally we provide some examples of how to use these settings for various instruments:
+    We now provide some examples of how to use these settings for various instruments:
     <itemizedlist>
       <listitem><para>Naked-eye observing: Choose Horizontal Coordinates and a <guilabel>Zenith Up</guilabel> orientation under <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice>.</para></listitem>
       <listitem><para>Camera on an equatorially mounted telescope: Choose Equatorial Coordinates and adjust the orientation of the sky map so that it matches your camera. As your mount points to different regions of the sky, the orientation should be rendered correctly.</para></listitem>
       <listitem><para>Using binoculars: Same settings as Naked-eye observing</para></listitem>
       <listitem><para>Eyepiece of an altazimuth Schmidt-Cassegrain telescope with an erecting prism: Under the <guimenu>View</guimenu> menu, choose <guilabel>Mirrored View</guilabel>, and under the <guisubmenu>Skymap Orientation</guisubmenu> sub-menu, choose <guilabel>Zenith Up</guilabel>. Finally, tweak the rotation manually to match the eyepiece view according to the angle you are using for your erecting prism.</para></listitem>
-      <listitem><para>Using a RACI finder scope on an altazimuth mounted telescope: Same settings as Naked-eye observing, except you may need to tweak the orientation manually once if you have it mounted at an angle</para></listitem>
+      <listitem><para>Using a RACI finder scope on an altazimuth mounted telescope, looking straight down into it: Same settings as Naked-eye observing, except you may need to tweak the orientation manually once if you have it mounted at an angle</para></listitem>
+      <listitem><para>Using a RACI finder scope on an altazimuth mounted telescope, looking through it from the side: In addition to the aforementioned, enable Erect observer correction for the appropriate side.</para></listitem>
       <listitem><para>Using a straight-through (inverted view) finder scope on an altazimuth mounted telescope: Choose Horizontal Coordinates and a sky-map orientation of <guilabel>Zenith Down</guilabel> in <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu</para></listitem>
-      <listitem><para>Eyepiece of a Dobsonian telescope: Choose Horizontal Coordinates, and in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu, select <guilabel>Zenith Down</guilabel> and check the <guilabel>Erect observer correction</guilabel> option. Then adjust the orientation manually once to match your telescope eyepiece view, and it should henceforth track it correctly.</para></listitem>
+      <listitem><para>Eyepiece of a Dobsonian telescope: Choose Horizontal Coordinates, and in the <menuchoice><guimenu>View</guimenu><guisubmenu>Skymap Orientation</guisubmenu></menuchoice> submenu, select <guilabel>Zenith Down</guilabel> and enable the erect observer correction, picking the left/right handed option as is appropriate for your telescope. Then adjust the orientation manually once to match your telescope eyepiece view, and it should henceforth track it correctly.</para></listitem>
     </itemizedlist>
   </para>
+  <para>
+    It is typical in visual astronomy to use at least three different instruments: the unaided eye, a finder scope, and the main telescope. The orientations of these three will have different settings and will need frequent modification of all the aforementioned options. To make it easy to adjust these settings together, KStars provides the <guilabel>Views</guilabel> feature. This feature is accessible through the <menuchoice><guimenu>View</guimenu><guisubmenu>Views</guisubmenu></menuchoice> menu and the options contained therein. The <guilabel>Arbitrary</guilabel> view is not a real view, but the option that gets selected when the sky-map orientation is modified manually through the previously described options. The rest of the views are bona fide views. New views may be added, or the existing views may be edited, removed, or re-ordered using the <menuchoice><guimenu>View</guimenu><guisubmenu>Views</guisubmenu><guimenuitem>Edit Views...</guimenuitem></menuchoice> option. Choosing this brings up a window to manage the views:
+  </para>
+  <screenshot>
+    <screeninfo>Dialog to manage sky map views</screeninfo>
+    <mediaobject>
+      <imageobject>
+	<imagedata fileref="viewmanager.png" format="PNG"/>
+      </imageobject>
+      <textobject>
+	<phrase>Manage Sky Map Views</phrase>
+      </textobject>
+    </mediaobject>
+  </screenshot>
+  <para>
+    To remove a view, simply select the view from the list and delete it using the <guibutton>Remove</guibutton> button. To re-order the views, use the mouse to drag the view you wish to move and drop it at its destination in-between two other entries. To edit a view, select the view from the list and click <guibutton>Edit...</guibutton>. To create a new view, click the <guibutton>New...</guibutton> button. The <guibutton>Edit...</guibutton> and <guibutton>New...</guibutton> options bring up a view editor interface:
+  </para>
+  <screenshot>
+    <screeninfo>Dialog to create a new view or edit an existing one</screeninfo>
+    <mediaobject>
+      <imageobject>
+	<imagedata fileref="newview.png" format="PNG"/>
+      </imageobject>
+      <textobject>
+	<phrase>Edit / Create View</phrase>
+      </textobject>
+    </mediaobject>
+  </screenshot>
+
+  <para>
+  The <guilabel>Name</guilabel> field carries a unique name for the View. The <guilabel>Mount Type</guilabel> determines whether the reference direction used for orientation will be north or zenith. Typically, one would set this to the type of mount used for the telescope. However, when using refractors and Schmidt-Cassegrain Telescopes (SCTs) with a rotatable diagonal, the observer will have a tendency to re-orient the eyepiece for comfort so that the eyepiece remains at a fixed angle with respect to the zenith. For this reason, it makes sense to choose <guilabel>Altazimuth</guilabel> mounting even when the telescope is actually on an equatorial mount. Choose <guilabel>Equatorial</guilabel> mounting when the focuser will not be re-oriented, such as when using a camera on an equatorially mounted telescope.
+
+  For Newtonian telescopes that invert (i.e. rotate by 180 degrees but do not change the handedness) of the view, pick the <guilabel>Inverted</guilabel> option. This is also the correct option for straight-through refractors and finder scopes. When using a erecting prism diagonal, the prism erects the inverted image by flipping it up-down. This results overall in a left-right mirrored image. Thus for telescopes that use an erecting prism, pick <guilabel>Mirrored</guilabel>. A special kind of prism called an Amici roof prism not only erects the image vertically, but it also prevents left-right mirroring of the image. Finder scopes incorporating such a diagonal are normally called "Right-Angle Correct Image" or RACI finder scopes. Such diagonals may also be used on refractors and SCTs. When using such a prism that produces a correct image, choose the <guilabel>Correct</guilabel> option. The <guilabel>Mirrored on the vertical axis</guilabel> option is not encountered in typical astronomical instruments, but is provided for completeness.
+
+  Two more factors need to be considered: one is the angle of the eyepiece with respect to the reference direction (north / zenith), and the other is the orientation of the observer's head (and notion of the vertical) which we explained when describing the erect observer correction feature. These two aspects are configured using the single slider titled <guilabel>Eyepiece Angle</guilabel>. Two illustrations below the slider show the interpretation of this setting; on the left, as seen from the front as is more convenient for Newtonian telescopes, and on the right as is seen from the back, more convenient for refractors and Cassegrains. The observer naturally stands on the side that makes it more convenient to look through the eyepiece, so the erect observer correction is automatically adjusted accordingly. For eyepiece angles that are less than -1 degree on the slider, the <guilabel>Erect observer correction, right-handed</guilabel> option is applied. Similarly, for eyepiece angles that are greater than +1 degree, the <guilabel>Erect observer correction, left-handed</guilabel> is applied. At 0 degrees, no erect observer correction is applied. This correction is indicated by a silhouette of a person standing on the appropriate side of the telescope. In our convention, most mass-manufactured Dobsonians seem to have a correction around +45 degrees. Incidentally, this correction is also useful for finder scopes with diagonals.
+
+  One may want to explicitly disable the erect observer correction even when the eyepiece angle is not zero. This is useful in case the view comes from a CCD camera that does not change angle with respect to the telescope body (unlike an observer's head), or if the display showing KStars' sky map is mounted on the telescope body itself. In this case the <guilabel>Display mounted on the telescope</guilabel> option can be checked. For the opposite effect, i.e. where the eyepiece angle is zero, but the observer is leaning to look through the eyepiece from one of the two sides, set the eyepiece angle to plus or minus 2 degrees to enable the erect observer correction; the minor difference will not be noticeable.
+
+  Finally, one may want triggering of the view to also set the field-of-view of the sky map to some value, for example to set the FOV of a finder scope. In this case, the <guilabel>Also set the field of view</guilabel> check-box may be checked, and an approximate field-of-view to adjust may be specified. If this is not enabled, the zoom level of the sky map is not altered when this view is applied.
+  </para>
+
 </sect1>
 
+
+
 &hips;
 
 </chapter>
diff --git a/doc/newview.png b/doc/newview.png
new file mode 100644
index 0000000000..296ce073ed
Binary files /dev/null and b/doc/newview.png differ
diff --git a/doc/viewmanager.png b/doc/viewmanager.png
new file mode 100644
index 0000000000..a96d59bfb3
Binary files /dev/null and b/doc/viewmanager.png differ
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 74791fa1d5..7cec6bace9 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -678,6 +678,7 @@ set(kstars_dialogs_SRCS
     dialogs/finddialog.cpp
     dialogs/focusdialog.cpp
     dialogs/fovdialog.cpp
+    dialogs/viewsdialog.cpp
     dialogs/locationdialog.cpp
     dialogs/timedialog.cpp
     dialogs/exportimagedialog.cpp
@@ -700,12 +701,14 @@ set(kstars_dialogsui_SRCS
     dialogs/finddialog.ui
     dialogs/focusdialog.ui
     dialogs/fovdialog.ui
+    dialogs/viewsdialog.ui
     dialogs/locationdialog.ui
     dialogs/wizwelcome.ui
     dialogs/wizlocation.ui
     dialogs/wizdownload.ui
     dialogs/wizdata.ui
     dialogs/newfov.ui
+    dialogs/newview.ui
     dialogs/exportimagedialog.ui
 )
 
@@ -924,6 +927,7 @@ SET(kstars_extra_kstars_SRCS
     auxiliary/imageviewer.cpp
     auxiliary/xplanetimageviewer.cpp
     auxiliary/fov.cpp
+    auxiliary/skymapview.cpp
     auxiliary/thumbnailpicker.cpp
     auxiliary/thumbnaileditor.cpp
     auxiliary/imageexporter.cpp
diff --git a/kstars/auxiliary/dms.cpp b/kstars/auxiliary/dms.cpp
index 348acb0d4c..4ad0abfc2b 100644
--- a/kstars/auxiliary/dms.cpp
+++ b/kstars/auxiliary/dms.cpp
@@ -251,11 +251,19 @@ int dms::msecond(void) const
 const dms dms::reduce(void) const
 {
     if (std::isnan(D))
-        return dms(0);
+        return dms(0); // FIXME: Why 0 and not NaN? -- asimha
 
     return dms(D - 360.0 * floor(D / 360.0));
 }
 
+double dms::reduce(const double D)
+{
+    if (std::isnan(D))
+        return D;
+
+    return (D - 360.0 * floor(D / 360.0));
+}
+
 const dms dms::deltaAngle(dms angle) const
 {
     double angleDiff = D - angle.Degrees();
diff --git a/kstars/auxiliary/dms.h b/kstars/auxiliary/dms.h
index 3cf7a43114..f5a349ca4f 100644
--- a/kstars/auxiliary/dms.h
+++ b/kstars/auxiliary/dms.h
@@ -405,6 +405,9 @@ class dms
          */
     static dms fromString(const QString &s, bool deg);
 
+    /** Reduce an angle in degrees expressed as a double */
+    static double reduce(const double D);
+
     inline dms operator-() { return dms(-D); }
 #ifdef COUNT_DMS_SINCOS_CALLS
     static long unsigned dms_constructor_calls; // counts number of DMS constructor calls
diff --git a/kstars/auxiliary/ksuserdb.cpp b/kstars/auxiliary/ksuserdb.cpp
index b9f69e6607..7887e250bb 100644
--- a/kstars/auxiliary/ksuserdb.cpp
+++ b/kstars/auxiliary/ksuserdb.cpp
@@ -281,7 +281,6 @@ bool KSUserDB::Initialize()
                         "settings TEXT DEFAULT NULL)"))
             qCWarning(KSTARS) << query.lastError();
 
-
         // Add DSLR lenses table
         if (!query.exec("CREATE TABLE dslrlens ( "
                         "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
@@ -551,7 +550,6 @@ bool KSUserDB::RebuildDB()
                   " sure to compensate for this effect the flux conserving clip-resampling option.', '9', 'equatorial', '512', 'jpeg fits',"
                   "'http://alaskybis.u-strasbg.fr/Fermi/Color', '1')");
 
-
     tables.append("CREATE TABLE dslr (id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
                   "Model TEXT DEFAULT NULL, "
                   "Width INTEGER DEFAULT NULL, "
@@ -559,7 +557,6 @@ bool KSUserDB::RebuildDB()
                   "PixelW REAL DEFAULT 5.0,"
                   "PixelH REAL DEFAULT 5.0)");
 
-
     tables.append("CREATE TABLE effectivefov ( "
                   "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
                   "Train TEXT DEFAULT NULL, "
@@ -855,7 +852,6 @@ bool KSUserDB::GetAllDarkFrames(QList<QVariantMap> &darkFrames)
     return true;
 }
 
-
 /* Effective FOV Section */
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1240,7 +1236,6 @@ bool KSUserDB::GetAllHIPSSources(QList<QMap<QString, QString>> &HIPSSources)
     return true;
 }
 
-
 /* DSLR Section */
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -2505,6 +2500,116 @@ bool KSUserDB::GetAllImageOverlays(QList<ImageOverlay> *imageOverlayList)
     return true;
 }
 
+void KSUserDB::CreateSkyMapViewTableIfNecessary()
+{
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    QString command = "CREATE TABLE IF NOT EXISTS SkyMapViews ( "
+                      "id INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT, "
+                      "name TEXT NOT NULL UNIQUE, "
+                      "data JSON)";
+    QSqlQuery query(db);
+    if (!query.exec(command))
+    {
+        qCDebug(KSTARS) << query.lastError();
+        qCDebug(KSTARS) << query.executedQuery();
+    }
+}
+
+bool KSUserDB::DeleteAllSkyMapViews()
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.setFilter("id >= 1");
+    views.select();
+    views.removeRows(0, views.rowCount());
+    views.submitAll();
+
+    QSqlQuery query(db);
+    QString dropQuery = QString("DROP TABLE SkyMapViews");
+    if (!query.exec(dropQuery))
+        qCWarning(KSTARS) << query.lastError().text();
+
+    return true;
+}
+
+bool KSUserDB::AddSkyMapView(const SkyMapView &view)
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.setFilter("name LIKE \'" + view.name + "\'");
+    views.select();
+
+    if (views.rowCount() > 0)
+    {
+        QSqlRecord record = views.record(0);
+        record.setValue("name", view.name);
+        record.setValue("data", QJsonDocument(view.toJson()).toJson(QJsonDocument::Compact));
+        views.setRecord(0, record);
+        views.submitAll();
+    }
+    else
+    {
+        int row = 0;
+        views.insertRows(row, 1);
+
+        views.setData(views.index(row, 1), view.name);  // row,0 is autoincerement ID
+        views.setData(views.index(row, 2), QJsonDocument(view.toJson()).toJson(QJsonDocument::Compact));
+        views.submitAll();
+    }
+    return true;
+}
+
+bool KSUserDB::GetAllSkyMapViews(QList<SkyMapView> &skyMapViewList)
+{
+    CreateSkyMapViewTableIfNecessary();
+    auto db = QSqlDatabase::database(m_ConnectionName);
+    if (!db.isValid())
+    {
+        qCCritical(KSTARS) << "Failed to open database:" << db.lastError();
+        return false;
+    }
+
+    skyMapViewList.clear();
+    QSqlTableModel views(nullptr, db);
+    views.setTable("SkyMapViews");
+    views.select();
+
+    for (int i = 0; i < views.rowCount(); ++i)
+    {
+        QSqlRecord record         = views.record(i);
+
+        const QString name        = record.value("name").toString();
+        const QJsonDocument data  = QJsonDocument::fromJson(record.value("data").toString().toUtf8());
+        Q_ASSERT((!data.isNull()) && data.isObject());
+        if (data.isNull() || !data.isObject())
+        {
+            qCCritical(KSTARS) << "Data associated with sky map view " << name << " is invalid!";
+            continue;
+        }
+        SkyMapView o(name, data.object());
+        skyMapViewList.append(o);
+    }
+
+    views.clear();
+    return true;
+}
+
 int KSUserDB::AddProfile(const QString &name)
 {
     auto db = QSqlDatabase::database(m_ConnectionName);
@@ -2544,8 +2649,6 @@ bool KSUserDB::DeleteProfile(const QSharedPointer<ProfileInfo> &pi)
     if (rc == false)
         qCWarning(KSTARS) << query.lastQuery() << query.lastError().text();
 
-
-
     return rc;
 }
 
@@ -2679,7 +2782,6 @@ bool KSUserDB::SaveProfile(const QSharedPointer<ProfileInfo> &pi)
     /*if (pi->customDrivers.isEmpty() == false && !query.exec(QString("INSERT INTO custom_driver (drivers, profile) VALUES('%1',%2)").arg(pi->customDrivers).arg(pi->id)))
         qDebug()  << query.lastQuery() << query.lastError().text();*/
 
-
     return true;
 }
 
@@ -3014,7 +3116,6 @@ void KSUserDB::AddProfileSettings(uint32_t profile, const QByteArray &settings)
     if (!profileSettings.submitAll())
         qCWarning(KSTARS) << profileSettings.lastError();
 
-
 }
 
 void KSUserDB::UpdateProfileSettings(uint32_t profile, const QByteArray &settings)
@@ -3036,7 +3137,6 @@ void KSUserDB::UpdateProfileSettings(uint32_t profile, const QByteArray &setting
         qCWarning(KSTARS) << profileSettings.lastError();
 }
 
-
 void KSUserDB::DeleteProfileSettings(uint32_t profile)
 {
     auto db = QSqlDatabase::database(m_ConnectionName);
diff --git a/kstars/auxiliary/ksuserdb.h b/kstars/auxiliary/ksuserdb.h
index c6f0d0fad7..b1c5b228bd 100644
--- a/kstars/auxiliary/ksuserdb.h
+++ b/kstars/auxiliary/ksuserdb.h
@@ -7,6 +7,7 @@
 #pragma once
 
 #include "auxiliary/profileinfo.h"
+#include "skymapview.h"
 #ifndef KSTARS_LITE
 #include "oal/oal.h"
 #endif
@@ -79,7 +80,6 @@ class KSUserDB
         bool DeleteDarkFrame(const QString &filename);
         bool GetAllDarkFrames(QList<QVariantMap> &darkFrames);
 
-
         /************************************************************************
          ******************************* Effective FOVs *************************
          ************************************************************************/
@@ -88,7 +88,6 @@ class KSUserDB
         bool DeleteEffectiveFOV(const QString &id);
         bool GetAllEffectiveFOVs(QList<QVariantMap> &effectiveFOVs);
 
-
         /************************************************************************
          ******************************* Driver Alias *************************
          ************************************************************************/
@@ -170,6 +169,19 @@ class KSUserDB
         /** @brief Gets all the image overlay rows from the database **/
         bool GetAllImageOverlays(QList<ImageOverlay> *imageOverlayList);
 
+        /************************************************************************
+         ****************************** Sky Map Views ***************************
+         ************************************************************************/
+
+        /** @brief Deletes all the sky map views stored in the database */
+        bool DeleteAllSkyMapViews();
+
+        /** @brief Adds a new sky map view row in the database */
+        bool AddSkyMapView(const SkyMapView &view);
+
+        /** @brief Gets all the sky map view rows from the database */
+        bool GetAllSkyMapViews(QList<SkyMapView> &skyMapViewList);
+
         /************************************************************************
          ********************************* Flags ********************************
          ************************************************************************/
@@ -394,7 +406,6 @@ class KSUserDB
          **/
         bool GetOpticalTrains(uint32_t profileID, QList<QVariantMap> &opticalTrains);
 
-
         /************************************************************************
          ******************************** Profile Settings **********************
          ************************************************************************/
@@ -479,6 +490,9 @@ class KSUserDB
         /** @brief creates the image overlay table if it doesn't already exist **/
         void CreateImageOverlayTableIfNecessary();
 
+        /** @brief creates the image overlay table if it doesn't already exist **/
+        void CreateSkyMapViewTableIfNecessary();
+
 #if 0
         /**
          * @brief Imports flags data from previous format
diff --git a/kstars/auxiliary/skymapview.cpp b/kstars/auxiliary/skymapview.cpp
new file mode 100644
index 0000000000..1b578dcdd7
--- /dev/null
+++ b/kstars/auxiliary/skymapview.cpp
@@ -0,0 +1,205 @@
+/*
+   SPDX-FileCopyrightText: 2024 Akarsh Simha <akarsh at kde.org>
+
+   SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "skymapview.h"
+#include "nan.h"
+#include "kstarsdata.h"
+#include "ksuserdb.h"
+#include <kstars_debug.h>
+
+SkyMapView::SkyMapView(const QString &name_, const QJsonObject &jsonData)
+    : name(name_)
+{
+    // Check version
+    if (jsonData["version"].toString() != "1.0.0")
+    {
+        qCCritical(KSTARS) << "Unhandled SkyMapView JSON schema version " << jsonData["version"].toString();
+        return;
+    }
+    useAltAz = jsonData["useAltAz"].toBool();
+    viewAngle = jsonData["viewAngle"].toDouble();
+    mirror = jsonData["mirror"].toBool();
+    inverted = jsonData["inverted"].toBool();
+    fov = jsonData["fov"].isNull() ? NaN::d : jsonData["fov"].toDouble();
+    erectObserver = jsonData["erectObserver"].toBool();
+}
+
+QJsonObject SkyMapView::toJson() const
+{
+    return QJsonObject
+    {
+        {"version", "1.0.0"},
+        {"useAltAz", useAltAz},
+        {"viewAngle", viewAngle},
+        {"mirror", mirror},
+        {"inverted", inverted},
+        {"fov", fov},
+        {"erectObserver", erectObserver}
+    };
+}
+
+// // // SkyMapViewManager // // //
+
+QList<SkyMapView> SkyMapViewManager::m_views;
+
+QList<SkyMapView> SkyMapViewManager::defaults()
+{
+    QList<SkyMapView> views;
+
+    SkyMapView view;
+
+    view.name = i18nc("Set the sky-map view to zenith up", "Zenith Up");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to zenith down", "Zenith Down");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to north up", "North Up");
+    view.useAltAz = false;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to north down", "North Down");
+    view.useAltAz = false;
+    view.viewAngle = 0;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to match a Schmidt-Cassegrain telescope with erecting prism pointed upwards",
+                      "SCT with upward diagonal");
+    view.useAltAz = true;
+    view.viewAngle = 0;
+    view.mirror = true;
+    view.inverted = false;
+    view.fov = NaN::d;
+    view.erectObserver = false;
+    views << view;
+
+    view.name = i18nc("Set the sky-map view to match the view through a typical Dobsonian telescope", "Typical Dobsonian");
+    view.useAltAz = true;
+    view.viewAngle = 45;
+    view.mirror = false;
+    view.inverted = true;
+    view.fov = NaN::d;
+    view.erectObserver = true;
+    views << view;
+
+    return views;
+}
+
+bool SkyMapViewManager::save()
+{
+    // FIXME, this is very inefficient
+    bool success = true;
+    Q_ASSERT(!!KStarsData::Instance());
+    if (!KStarsData::Instance())
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KStarsData instance.";
+        return false;
+    }
+    KSUserDB *db = KStarsData::Instance()->userdb();
+    Q_ASSERT(!!db);
+    if (!db)
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KSUserDB instance.";
+        return false;
+    }
+    success = success && db->DeleteAllSkyMapViews();
+    if (!success)
+    {
+        qCCritical(KSTARS) << "Failed to flush sky map views from the database";
+        return success;
+    }
+    for (const auto &view : m_views)
+    {
+        bool result = db->AddSkyMapView(view);
+        success = success && result;
+        Q_ASSERT(result);
+        if (!result)
+        {
+            qCCritical(KSTARS) << "Failed to commit Sky Map View " << view.name << " to the database!";
+        }
+    }
+    return success;
+}
+
+const QList<SkyMapView> &SkyMapViewManager::readViews()
+{
+    Q_ASSERT(!!KStarsData::Instance());
+    if (!KStarsData::Instance())
+    {
+        qCCritical(KSTARS) << "Cannot read sky map views, no KStarsData instance.";
+        return m_views;
+    }
+    KSUserDB *db = KStarsData::Instance()->userdb();
+    Q_ASSERT(!!db);
+    if (!db)
+    {
+        qCCritical(KSTARS) << "Cannot save sky map views, no KSUserDB instance.";
+        return m_views;
+    }
+    m_views.clear();
+    bool result = db->GetAllSkyMapViews(m_views);
+    Q_ASSERT(result);
+    if (!result)
+    {
+        qCCritical(KSTARS) << "Failed to read sky map views from the database!";
+    }
+    if (m_views.isEmpty())
+    {
+        m_views = defaults();
+    }
+    return m_views;
+}
+
+void SkyMapViewManager::drop()
+{
+    m_views.clear();
+}
+
+std::optional<SkyMapView> SkyMapViewManager::viewNamed(const QString &name)
+{
+    for (auto it = m_views.begin(); it != m_views.end(); ++it)
+    {
+        if (it->name == name)
+        {
+            return *it;
+        }
+    }
+    return std::nullopt;
+}
+
+bool SkyMapViewManager::removeView(const QString &name)
+{
+    for (auto it = m_views.begin(); it != m_views.end(); ++it)
+    {
+        if (it->name == name)
+        {
+            m_views.erase(it);
+            return true;
+        }
+    }
+    return false;
+}
diff --git a/kstars/auxiliary/skymapview.h b/kstars/auxiliary/skymapview.h
new file mode 100644
index 0000000000..b728bbef1d
--- /dev/null
+++ b/kstars/auxiliary/skymapview.h
@@ -0,0 +1,81 @@
+/*
+    SPDX-FileCopyrightText: 2024 Akarsh Simha <akarsh at kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include <QString>
+#include <QJsonObject>
+#include <QList>
+#include <optional>
+
+/**
+ * @struct SkyMapView
+ * Carries parameters of a sky map view
+ */
+struct SkyMapView
+{
+    explicit SkyMapView(const QString &name_, const QJsonObject &jsonData);
+    SkyMapView() = default;
+    QJsonObject toJson() const;
+
+    QString name; // Name of this view (can be empty)
+    bool useAltAz; // Mount type is alt-az when true
+    double viewAngle; // Focuser rotation in degrees, within [-90°, 90°]
+    bool mirror; // Mirrored left-right
+    bool inverted; // 180° rotation if true
+    double fov; // fov in degrees, NaN when disabled
+    bool erectObserver; // Erect observer correction
+};
+
+/**
+ * @class SkyMapViewManager
+ * Manages a list of sky map views
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class SkyMapViewManager
+{
+    public:
+        /** @short Read the list of views from the database */
+        static const QList<SkyMapView> &readViews();
+
+        /** @short Drop the list */
+        static void drop();
+
+        /** @short Add a view */
+        inline static void addView(const SkyMapView &newView)
+        {
+            m_views.append(newView);
+        }
+
+        /** @short Remove a view
+         * Note: This is currently an O(N) operation
+         */
+        static bool removeView(const QString &name);
+
+        /**
+         * @short Get the view with the given name
+         * @note This is currently an O(N) operation
+         */
+        static std::optional<SkyMapView> viewNamed(const QString &name);
+
+        /** @short Get the list of available views */
+        static const QList<SkyMapView> &getViews()
+        {
+            return m_views;
+        }
+
+        /** @short Commit the list of views to the database */
+        static bool save();
+
+    private:
+        SkyMapViewManager() = default;
+        ~SkyMapViewManager() = default;
+        static QList<SkyMapView> m_views;
+
+        /** @short Fill list with standard views */
+        static QList<SkyMapView> defaults();
+};
diff --git a/kstars/data/kstars.qrc b/kstars/data/kstars.qrc
index a1f67ea315..88cda34b91 100755
--- a/kstars/data/kstars.qrc
+++ b/kstars/data/kstars.qrc
@@ -81,6 +81,7 @@
         <file>noimage.png</file>
         <file>reticle12.png</file>
         <file>reticle24.png</file>
+        <file>observer.png</file>
     </qresource>
     <qresource prefix="/videos">
         <file>sm_animation.gif</file>
diff --git a/kstars/data/kstarsui.rc b/kstars/data/kstarsui.rc
index 8a59a30064..1ba9272f40 100644
--- a/kstars/data/kstarsui.rc
+++ b/kstars/data/kstarsui.rc
@@ -50,6 +50,8 @@
                 <Action name="coordsys" />
                 <Action name="mirror_skymap" />
                 <Action name="skymap_orientation" />
+                <Action name="views" />
+		<Separator />
                 <Action name="toggle_terrain" />
                 <Action name="toggle_image_overlays" />
                 <Menu name="projection"><text>&Projection</text>
diff --git a/kstars/data/observer.png b/kstars/data/observer.png
new file mode 100644
index 0000000000..cb941ab594
Binary files /dev/null and b/kstars/data/observer.png differ
diff --git a/kstars/dialogs/newview.ui b/kstars/dialogs/newview.ui
new file mode 100644
index 0000000000..fbe7b9f383
--- /dev/null
+++ b/kstars/dialogs/newview.ui
@@ -0,0 +1,399 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewView</class>
+ <widget class="QDialog" name="NewView">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>640</width>
+    <height>662</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Add / Edit View</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Name:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="viewNameLineEdit">
+       <property name="placeholderText">
+        <string>9x50 RACI finder on Dob</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Line" name="line_4">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Mount Type:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="mountTypeComboBox">
+       <property name="currentIndex">
+        <number>1</number>
+       </property>
+       <item>
+        <property name="text">
+         <string>Equatorial</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Altazimuth</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <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>
+    </layout>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_5">
+     <property name="text">
+      <string>**Note:** Choose mount type "Altazimuth" when visually observing through SCTs / refractors, irrespective of the actual mounting.</string>
+     </property>
+     <property name="textFormat">
+      <enum>Qt::MarkdownText</enum>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line_3">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="3" column="0">
+      <widget class="QRadioButton" name="correctViewType">
+       <property name="text">
+        <string>Correct (e.g. RACI finder or refractor with Amici roof prism)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QRadioButton" name="mirroredViewType">
+       <property name="text">
+        <string>Mirrored (e.g. Cassegrain or refractor with erecting prism)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QRadioButton" name="invertedViewType">
+       <property name="text">
+        <string>Inverted (e.g. straight through finder, Newtonian)</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>View Type:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="0">
+      <widget class="QRadioButton" name="invertedMirroredViewType">
+       <property name="text">
+        <string>Mirrored on the vertical axis (i.e. inverted and mirrored)</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>Eyepiece Angle:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QSlider" name="viewingAngleSlider">
+       <property name="minimum">
+        <number>-179</number>
+       </property>
+       <property name="maximum">
+        <number>179</number>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="tickPosition">
+        <enum>QSlider::TicksBothSides</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="viewingAngleLabel">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>40</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>###°</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QGridLayout" name="gridLayout_2">
+     <item row="1" column="1">
+      <widget class="QLabel" name="label_8">
+       <property name="text">
+        <string>Telescopes with the eyepiece at the bottom</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_7">
+       <property name="text">
+        <string>Telescopes with the eyepiece at the top</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="0" colspan="2">
+      <widget class="QLabel" name="label_6">
+       <property name="text">
+        <string>The human silhouette indicates on which side of the telescope the observer is assumed to stand.</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="viewingAnglePreviewBottom">
+       <property name="minimumSize">
+        <size>
+         <width>200</width>
+         <height>150</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="QLabel" name="viewingAnglePreviewTop">
+       <property name="minimumSize">
+        <size>
+         <width>200</width>
+         <height>150</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_9">
+       <property name="text">
+        <string>(Preview shows view down a Newtonian's tube)</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QLabel" name="label_10">
+       <property name="text">
+        <string>(Preview shows view of the back of an SCT)</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="disableErectObserverCheckBox">
+     <property name="text">
+      <string>Display mounted on the telescope (also check this if using a camera instead of visual observation)</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line_2">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_4">
+     <item>
+      <widget class="QCheckBox" name="fieldOfViewCheckBox">
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>30</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Also set the field of view</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="UnitSpinBoxWidget" name="fieldOfViewSpinBox" native="true">
+       <property name="minimumSize">
+        <size>
+         <width>195</width>
+         <height>0</height>
+        </size>
+       </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>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>UnitSpinBoxWidget</class>
+   <extends>QWidget</extends>
+   <header>widgets/unitspinboxwidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>NewView</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>250</x>
+     <y>647</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>NewView</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>318</x>
+     <y>647</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/kstars/dialogs/viewsdialog.cpp b/kstars/dialogs/viewsdialog.cpp
new file mode 100644
index 0000000000..79bdfbb8ed
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.cpp
@@ -0,0 +1,420 @@
+/*
+    SPDX-FileCopyrightText: 2003 Jason Harris <kstars at 30doradus.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "viewsdialog.h"
+#include <QPointer>
+#include <QPainter>
+#include <QMessageBox>
+#include <QPainterPath>
+#include <QPainterPathStroker>
+#include <kstars_debug.h>
+
+#include "Options.h"
+
+ViewsDialogUI::ViewsDialogUI(QWidget *parent) : QFrame(parent)
+{
+    setupUi(this);
+}
+
+//----ViewsDialogStringListModel-----//
+Qt::ItemFlags ViewsDialogStringListModel::flags(const QModelIndex &index) const
+{
+    Qt::ItemFlags defaultFlags = QStringListModel::flags(index);
+    if (index.isValid())
+    {
+        return defaultFlags & (~Qt::ItemIsDropEnabled);
+    }
+    return defaultFlags;
+}
+
+//---------ViewsDialog---------------//
+ViewsDialog::ViewsDialog(QWidget *p) : QDialog(p)
+{
+#ifdef Q_OS_OSX
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+#endif
+    ui = new ViewsDialogUI(this);
+
+    setWindowTitle(i18nc("@title:window", "Manage Sky Map Views"));
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    mainLayout->addWidget(ui);
+    setLayout(mainLayout);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Close);
+    mainLayout->addWidget(buttonBox);
+    connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+    connect(buttonBox, SIGNAL(rejected()), this, SLOT(close()));
+
+    // Read list of Views and for each view, create a listbox entry
+    m_model = new ViewsDialogStringListModel(this);
+    syncModel();
+    ui->ViewListBox->setModel(m_model);
+    ui->ViewListBox->setDragDropMode(QAbstractItemView::InternalMove);
+    ui->ViewListBox->setDefaultDropAction(Qt::MoveAction);
+    ui->ViewListBox->setDragDropOverwriteMode(false);
+
+    connect(ui->ViewListBox->selectionModel(), &QItemSelectionModel::currentChanged, this, &ViewsDialog::slotSelectionChanged);
+    connect(m_model, &ViewsDialogStringListModel::rowsMoved, this, &ViewsDialog::syncFromModel);
+    connect(ui->NewButton, SIGNAL(clicked()), SLOT(slotNewView()));
+    connect(ui->EditButton, SIGNAL(clicked()), SLOT(slotEditView()));
+    connect(ui->RemoveButton, SIGNAL(clicked()), SLOT(slotRemoveView()));
+
+}
+
+void ViewsDialog::syncModel()
+{
+    QStringList viewNames;
+    for(const auto &view : SkyMapViewManager::getViews())
+    {
+        viewNames.append(view.name);
+    }
+    m_model->setStringList(viewNames);
+}
+
+void ViewsDialog::syncFromModel()
+{
+    // FIXME: Inefficient code, but it's okay because number of items is small
+    QHash<QString, SkyMapView> nameToViewMap;
+    for(const auto &view : SkyMapViewManager::getViews())
+    {
+        nameToViewMap.insert(view.name, view);
+    }
+    QStringList updatedList = m_model->stringList();
+    SkyMapViewManager::drop();
+    for (const auto &view : updatedList)
+    {
+        SkyMapViewManager::addView(nameToViewMap[view]);
+    }
+    SkyMapViewManager::save();
+}
+
+void ViewsDialog::slotSelectionChanged(const QModelIndex &current, const QModelIndex &prev)
+{
+    Q_UNUSED(prev);
+    bool enable = current.isValid();
+    ui->RemoveButton->setEnabled(enable);
+    ui->EditButton->setEnabled(enable);
+}
+
+void ViewsDialog::slotNewView()
+{
+    QPointer<NewView> newViewDialog = new NewView(this);
+    if (newViewDialog->exec() == QDialog::Accepted)
+    {
+        const auto view = newViewDialog->getView();
+        SkyMapViewManager::addView(view);
+        m_model->insertRow(m_model->rowCount());
+        QModelIndex index = m_model->index(m_model->rowCount() - 1, 0);
+        m_model->setData(index, view.name);
+        ui->ViewListBox->setCurrentIndex(index);
+    }
+    delete newViewDialog;
+}
+
+void ViewsDialog::slotEditView()
+{
+    //Preload current values
+    QModelIndex currentIndex = ui->ViewListBox->currentIndex();
+    if (!currentIndex.isValid())
+        return;
+    const QString viewName = m_model->data(currentIndex).toString();
+    std::optional<SkyMapView> view = SkyMapViewManager::viewNamed(viewName);
+    Q_ASSERT(!!view);
+    if (!view)
+    {
+        qCCritical(KSTARS) << "Programming Error";
+        return; // Eh?
+    }
+
+    // Create dialog
+    QPointer<NewView> newViewDialog = new NewView(this, view);
+    if (newViewDialog->exec() == QDialog::Accepted)
+    {
+        // Overwrite Views
+        SkyMapViewManager::removeView(viewName);
+        const auto view = newViewDialog->getView();
+        SkyMapViewManager::addView(view);
+        syncModel();
+    }
+    delete newViewDialog;
+}
+
+void ViewsDialog::slotRemoveView()
+{
+    QModelIndex currentIndex = ui->ViewListBox->currentIndex();
+    if (!currentIndex.isValid())
+        return;
+    const QString viewName = m_model->data(currentIndex).toString();
+    if (SkyMapViewManager::removeView(viewName))
+    {
+        m_model->removeRow(currentIndex.row());
+    }
+}
+
+//-------------NewViews------------------//
+
+class SliderResetEventFilter : public QObject
+{
+    public:
+        SliderResetEventFilter(QSlider *slider, QObject *parent = nullptr)
+            : QObject(parent)
+            , m_slider(slider)
+        {
+            if (m_slider)
+            {
+                m_slider->installEventFilter(this);
+            }
+        }
+
+        bool eventFilter(QObject *obj, QEvent *event)
+        {
+            if (obj == m_slider && event->type() == QEvent::MouseButtonDblClick)
+            {
+                QMouseEvent *mouseEvent = dynamic_cast<QMouseEvent*>(event);
+                Q_ASSERT(!!mouseEvent);
+                if (mouseEvent->button() == Qt::LeftButton)
+                {
+                    m_slider->setValue(0);
+                    return true;
+                }
+            }
+            return QObject::eventFilter(obj, event);
+        }
+
+    private:
+        QSlider *m_slider;
+};
+
+NewView::NewView(QWidget *parent, std::optional<SkyMapView> _view) : QDialog(parent)
+{
+    setupUi(this);
+
+    if (_view)
+    {
+        setWindowTitle(i18nc("@title:window", "Edit View"));
+    }
+    else
+    {
+        setWindowTitle(i18nc("@title:window", "New View"));
+    }
+
+    fieldOfViewSpinBox->addUnit("degrees", 1.0);
+    fieldOfViewSpinBox->addUnit("arcmin", 1 / 60.);
+    fieldOfViewSpinBox->addUnit("arcsec", 1 / 3600.);
+    fieldOfViewSpinBox->doubleSpinBox->setMaximum(600.0);
+    fieldOfViewSpinBox->doubleSpinBox->setMinimum(0.01);
+    fieldOfViewSpinBox->setEnabled(false);
+    fieldOfViewSpinBox->doubleSpinBox->setValue(1.0);
+
+    // Enable the "OK" button only when the "Name" field is not empty
+    connect(viewNameLineEdit, &QLineEdit::textChanged, [&](const QString & text)
+    {
+        buttonBox->button(QDialogButtonBox::Ok)->setDisabled(text.isEmpty());
+    });
+
+    // Enable the FOV spin box and unit combo only when the Set FOV checkbox is checked
+    connect(fieldOfViewCheckBox, &QCheckBox::toggled, fieldOfViewSpinBox, &UnitSpinBoxWidget::setEnabled);
+
+    // Update the angle value and graphic when the viewing angle slider is changed
+    connect(viewingAngleSlider, &QSlider::valueChanged, [&](const double value)
+    {
+        viewingAngleLabel->setText(QString("%1°").arg(QString::number(value)));
+        this->updateViewingAnglePreviews();
+    });
+    viewingAngleSlider->setValue(0); // Force the updates
+
+    // Update the viewing angle graphic when the erect observer correction is enabled / disabled
+    connect(disableErectObserverCheckBox, &QCheckBox::toggled, this, &NewView::updateViewingAnglePreviews);
+
+    // Disable erect observer when using equatorial mount
+    connect(mountTypeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [&](const int index) {
+        if (index == 0)
+        {
+            // Equatorial
+            disableErectObserverCheckBox->setChecked(true);
+            disableErectObserverCheckBox->setEnabled(false);
+        }
+        else
+        {
+            // Altazimuth
+            disableErectObserverCheckBox->setEnabled(true);
+        }
+    });
+
+
+    // Set up everything else
+    m_topPreview = new QPixmap(400, 300);
+    m_bottomPreview = new QPixmap(400, 300);
+    m_observerPixmap = new QPixmap(":/images/observer.png");
+    new SliderResetEventFilter(viewingAngleSlider);
+
+    // Finally, initialize fields as required
+    if (_view)
+    {
+        const auto view = *_view;
+        m_originalName = view.name;
+        viewNameLineEdit->setText(view.name);
+        mountTypeComboBox->setCurrentIndex(view.useAltAz ? 1 : 0);
+        if (view.inverted && view.mirror)
+        {
+            invertedMirroredViewType->setChecked(true);
+        }
+        else if (view.inverted)
+        {
+            invertedViewType->setChecked(true);
+        }
+        else if (view.mirror)
+        {
+            mirroredViewType->setChecked(true);
+        }
+        else
+        {
+            correctViewType->setChecked(true);
+        }
+
+        viewingAngleSlider->setValue(view.viewAngle);
+        disableErectObserverCheckBox->setChecked(!view.erectObserver);
+        if (!std::isnan(view.fov))
+        {
+            fieldOfViewCheckBox->setChecked(true);
+            fieldOfViewSpinBox->doubleSpinBox->setValue(view.fov);
+        }
+    }
+
+}
+
+NewView::~NewView()
+{
+    delete m_topPreview;
+    delete m_bottomPreview;
+    delete m_observerPixmap;
+}
+
+const SkyMapView NewView::getView() const
+{
+    struct SkyMapView view;
+
+    view.name      = viewNameLineEdit->text();
+    view.useAltAz  = (mountTypeComboBox->currentIndex() > 0);
+    view.viewAngle = viewingAngleSlider->value();
+    view.mirror    = invertedMirroredViewType->isChecked() || mirroredViewType->isChecked();
+    view.inverted  = invertedMirroredViewType->isChecked() || invertedViewType->isChecked();
+    view.fov       = fieldOfViewCheckBox->isChecked() ? fieldOfViewSpinBox->value() : NaN::d;
+    view.erectObserver = !(disableErectObserverCheckBox->isChecked());
+
+    return view;
+}
+
+void NewView::done(int r)
+{
+    if (r == QDialog::Accepted)
+    {
+        const QString name = viewNameLineEdit->text();
+        if (name != m_originalName)
+        {
+            if (!!SkyMapViewManager::viewNamed(name))
+            {
+                QMessageBox::critical(this, i18n("Conflicting View Name"),
+                                      i18n("There already exists a view with the name you attempted to use. Please choose a different name for this view."));
+                return;
+            }
+        }
+    }
+    QDialog::done(r);
+    return;
+}
+
+void NewView::updateViewingAnglePreviews()
+{
+    Q_ASSERT(!!m_topPreview);
+    Q_ASSERT(!!m_bottomPreview);
+    Q_ASSERT(!!m_observerPixmap);
+
+    QPen pen(this->palette().color(QPalette::WindowText));
+    {
+        m_topPreview->fill(Qt::transparent);
+        float cx = m_topPreview->width() / 2., cy = m_topPreview->height() / 2.;
+        float size = std::min(m_topPreview->width(), m_topPreview->height());
+        float r = 0.75 * (size / 2.);
+        QPainter p(m_topPreview);
+
+        // Circle representing tube / secondary cage
+        pen.setWidth(5);
+        p.setPen(pen);
+        p.drawEllipse(QPointF(cx, cy), r, r);
+
+        // Cross hairs representing secondary vanes
+        pen.setWidth(3);
+        p.setPen(pen);
+        p.drawLine(cx - r, cy, cx + r, cy);
+        p.drawLine(cx, cy - r, cx, cy + r);
+
+        // Focuser
+        QPainterPathStroker stroker;
+        stroker.setWidth(20.f);
+        QPainterPath focuserPath;
+        double theta = dms::DegToRad * (viewingAngleSlider->value() - 90);
+        focuserPath.moveTo(cx + (r + 5.) * std::cos(theta), cy + (r + 5.) * std::sin(theta));
+        focuserPath.lineTo(cx + (r + 25.) * std::cos(theta), cy + (r + 25.) * std::sin(theta));
+        p.drawPath(stroker.createStroke(focuserPath));
+
+        // Observer
+        if (!disableErectObserverCheckBox->isChecked() && std::abs(viewingAngleSlider->value()) > 1)
+        {
+            p.drawPixmap(QPointF(
+                             viewingAngleSlider->value() > 0 ? m_topPreview->width() - m_observerPixmap->width() : 0,
+                             m_topPreview->height() - m_observerPixmap->height()),
+                         viewingAngleSlider->value() < 0 ?
+                         m_observerPixmap->transformed(QMatrix(-1, 0, 0, 1, 0, 0)) :
+                         *m_observerPixmap);
+        }
+        p.end();
+
+        // Display the pixmap to the QLabel
+        viewingAnglePreviewTop->setPixmap(m_topPreview->scaled(viewingAnglePreviewTop->width(), viewingAnglePreviewTop->height(),
+                                          Qt::KeepAspectRatio, Qt::SmoothTransformation));
+    }
+
+    {
+        m_bottomPreview->fill(Qt::transparent);
+        float cx = m_bottomPreview->width() / 2., cy = m_bottomPreview->height() / 2.;
+        float size = std::min(m_bottomPreview->width(), m_bottomPreview->height());
+        float r = 0.75 * (size / 2.);
+        QPainter p(m_bottomPreview);
+
+        // Circle representing the back of an SCT
+        pen.setWidth(5);
+        p.setPen(pen);
+        p.drawEllipse(QPointF(cx, cy), r, r);
+
+        // Focuser
+        QPainterPathStroker stroker;
+        stroker.setWidth(20.f);
+        QPainterPath focuserPath;
+        double theta = dms::DegToRad * (-viewingAngleSlider->value() - 90);
+        focuserPath.moveTo(cx, cy);
+        focuserPath.lineTo(cx + 25. * std::cos(theta), cy + 25. * std::sin(theta));
+        p.drawPath(stroker.createStroke(focuserPath));
+
+        // Observer
+        if (!disableErectObserverCheckBox->isChecked() && std::abs(viewingAngleSlider->value()) > 1)
+        {
+            p.drawPixmap(QPointF(
+                             viewingAngleSlider->value() < 0 ? m_bottomPreview->width() - m_observerPixmap->width() : 0,
+                             m_bottomPreview->height() - m_observerPixmap->height()),
+                         viewingAngleSlider->value() > 0 ?
+                         m_observerPixmap->transformed(QMatrix(-1, 0, 0, 1, 0, 0)) :
+                         *m_observerPixmap);
+        }
+
+        // Display the pixmap on the QLabel
+        p.end();
+        viewingAnglePreviewBottom->setPixmap(m_bottomPreview->scaled(
+                viewingAnglePreviewBottom->width(), viewingAnglePreviewBottom->height(),
+                Qt::KeepAspectRatio, Qt::SmoothTransformation));
+    }
+}
diff --git a/kstars/dialogs/viewsdialog.h b/kstars/dialogs/viewsdialog.h
new file mode 100644
index 0000000000..6db6003773
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.h
@@ -0,0 +1,104 @@
+/*
+    SPDX-FileCopyrightText: 2003 Jason Harris <kstars at 30doradus.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef VIEWSDIALOG_H_
+#define VIEWSDIALOG_H_
+
+#include <QPaintEvent>
+#include <QDialog>
+#include <QDoubleSpinBox>
+#include <QStringList>
+#include <QStringListModel>
+#include <optional>
+#include "skymapview.h"
+
+#include "ui_viewsdialog.h"
+#include "ui_newview.h"
+
+class ViewsDialogUI : public QFrame, public Ui::ViewsDialog
+{
+        Q_OBJECT
+    public:
+        explicit ViewsDialogUI(QWidget *parent = nullptr);
+};
+
+class ViewsDialogStringListModel : public QStringListModel
+{
+    public:
+        explicit ViewsDialogStringListModel(QObject* parent = nullptr)
+            : QStringListModel(parent) {}
+
+        Qt::ItemFlags flags(const QModelIndex &index) const override;
+};
+
+/**
+ * @class ViewsDialog
+ * @brief ViewsDialog is dialog to select a Sky Map View (or create a new one)
+ *
+ * A sky map view is a collection of settings that defines the
+ * orientation and scale of the sky map and how it changes as the user
+ * pans around.
+ *
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class ViewsDialog : public QDialog
+{
+        Q_OBJECT
+    public:
+        explicit ViewsDialog(QWidget *parent = nullptr);
+
+    private slots:
+        void slotNewView();
+        void slotEditView();
+        void slotRemoveView();
+        void slotSelectionChanged(const QModelIndex &, const QModelIndex &);
+
+    private:
+
+        /** Sync the model from the view manager */
+        void syncModel();
+
+        /** Sync the model to the view manager */
+        void syncFromModel();
+
+        QStringListModel* m_model;
+        unsigned int currentItem() const;
+        ViewsDialogUI *ui;
+        static int viewID;
+};
+
+/**
+ * @class NewView
+ * Dialog for defining a new View
+ * @author Akarsh Simha
+ * @version 1.0
+ */
+class NewView : public QDialog, private Ui::NewView
+{
+        Q_OBJECT
+    public:
+        /** Create new dialog
+             * @param parent parent widget
+             * @param view to copy data from. If it's empty will create empty one.
+             */
+        explicit NewView(QWidget *parent = nullptr, std::optional<SkyMapView> view = std::nullopt);
+        ~NewView() override;
+
+        /** Return the view struct. */
+        const SkyMapView getView() const;
+
+    public slots:
+        void updateViewingAnglePreviews();
+        virtual void done(int r) override;
+
+    private:
+        QString m_originalName;
+        QPixmap *m_observerPixmap; // Icon for an observer
+        QPixmap *m_topPreview, *m_bottomPreview;
+};
+
+#endif
diff --git a/kstars/dialogs/viewsdialog.ui b/kstars/dialogs/viewsdialog.ui
new file mode 100644
index 0000000000..527b31aea0
--- /dev/null
+++ b/kstars/dialogs/viewsdialog.ui
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ViewsDialog</class>
+ <widget class="QWidget" name="ViewsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>275</width>
+    <height>325</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Edit Sky Map Views</string>
+  </property>
+  <layout class="QHBoxLayout">
+   <property name="spacing">
+    <number>6</number>
+   </property>
+   <property name="leftMargin">
+    <number>10</number>
+   </property>
+   <property name="topMargin">
+    <number>10</number>
+   </property>
+   <property name="rightMargin">
+    <number>10</number>
+   </property>
+   <property name="bottomMargin">
+    <number>10</number>
+   </property>
+   <item>
+    <widget class="QListView" name="ViewListBox">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="defaultDropAction">
+      <enum>Qt::IgnoreAction</enum>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout">
+     <property name="spacing">
+      <number>6</number>
+     </property>
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="QPushButton" name="NewButton">
+       <property name="toolTip">
+        <string>Add a new FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Add a new field-of-view (FOV) symbol to the list.  You can define the size, shape, and color of the new symbol.</string>
+       </property>
+       <property name="text">
+        <string>New...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer>
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Fixed</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>85</width>
+         <height>16</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="EditButton">
+       <property name="toolTip">
+        <string>Modify the highlighted FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Press this button to modify the highlighted FOV symbol.  You can change its size, shape and color.</string>
+       </property>
+       <property name="text">
+        <string>Edit...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="RemoveButton">
+       <property name="toolTip">
+        <string>Remove highlighted FOV symbol</string>
+       </property>
+       <property name="whatsThis">
+        <string>Press this button to remove the highlighted FOV symbol from the list.</string>
+       </property>
+       <property name="text">
+        <string>Remove</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer>
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Expanding</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>85</width>
+         <height>126</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/kstars/kstars.cpp b/kstars/kstars.cpp
index 6cd2f27545..ccaf2086f7 100644
--- a/kstars/kstars.cpp
+++ b/kstars/kstars.cpp
@@ -166,7 +166,9 @@ KStars::KStars(bool doSplash, bool clockrun, const QString &startdate)
     telescopeGroup->setExclusive(false);
     domeGroup       = new QActionGroup(this);
     domeGroup->setExclusive(false);
+    viewsGroup      = new QActionGroup(this);
     skymapOrientationGroup = new QActionGroup(this);
+    erectObserverCorrectionGroup = new QActionGroup(this);
 
     m_KStarsData = KStarsData::Create();
     Q_ASSERT(m_KStarsData);
@@ -342,8 +344,10 @@ void KStars::applyConfig(bool doApplyFocus)
     actionCollection()->action("show_flags")->setChecked(Options::showFlags());
     actionCollection()->action("show_supernovae")->setChecked(Options::showSupernovae());
     actionCollection()->action("show_satellites")->setChecked(Options::showSatellites());
-    actionCollection()->action("erect_observer_correction")->setChecked(Options::erectObserverCorrection());
-    actionCollection()->action("erect_observer_correction")->setEnabled(Options::useAltAz());
+    erectObserverCorrectionGroup->setEnabled(Options::useAltAz());
+    actionCollection()->action("erect_observer_correction_off")->setChecked(Options::erectObserverCorrection() == 0);
+    actionCollection()->action("erect_observer_correction_left")->setChecked(Options::erectObserverCorrection() == 1);
+    actionCollection()->action("erect_observer_correction_right")->setChecked(Options::erectObserverCorrection() == 2);
     actionCollection()->action("mirror_skymap")->setChecked(Options::mirrorSkyMap());
     statusBar()->setVisible(Options::showStatusBar());
 
@@ -545,6 +549,32 @@ void KStars::selectPreviousFov()
     map()->update();
 }
 
+void KStars::selectNextView()
+{
+    QList<QAction*> actions = viewsGroup->actions();
+    int currentIndex = actions.indexOf(viewsGroup->checkedAction());
+    int newIndex = currentIndex + 1;
+    if (newIndex == actions.count() - 1)
+    {
+        newIndex++; // Skip "Arbitrary"
+    }
+    actions[newIndex % actions.count()]->activate(QAction::Trigger);
+    map()->slotDisplayFadingText(actions[newIndex % actions.count()]->data().toString());
+}
+
+void KStars::selectPreviousView()
+{
+    QList<QAction*> actions = viewsGroup->actions();
+    int currentIndex = actions.indexOf(viewsGroup->checkedAction());
+    int newIndex = currentIndex - 1;
+    if (currentIndex <= 0)
+    {
+        newIndex = actions.count() - 2; // Skip "Arbitrary"
+    }
+    actions[newIndex]->activate(QAction::Trigger);
+    map()->slotDisplayFadingText(actions[newIndex]->data().toString());
+}
+
 //FIXME Port to QML2
 //#if 0
 void KStars::showWISettingsUI()
diff --git a/kstars/kstars.h b/kstars/kstars.h
index 4604aff0e2..3f17cc298e 100644
--- a/kstars/kstars.h
+++ b/kstars/kstars.h
@@ -192,6 +192,10 @@ class KStars : public KXmlGuiWindow
 
         void selectPreviousFov();
 
+        void selectNextView();
+
+        void selectPreviousView();
+
         void showWISettingsUI();
 
         void showWI(ObsConditions *obs);
@@ -199,6 +203,9 @@ class KStars : public KXmlGuiWindow
         /** Load HIPS information and repopulate menu. */
         void repopulateHIPS();
 
+        /** Load Views and repopulate menu. */
+        void repopulateViews();
+
         void repopulateOrientation();
 
         WIEquipSettings *getWIEquipSettings()
@@ -732,9 +739,15 @@ class KStars : public KXmlGuiWindow
         /** Select the Target symbol (a.k.a. field-of-view indicator) */
         void slotTargetSymbol(bool flag);
 
+        /** Apply the provided sky map view */
+        void slotApplySkyMapView(const QString &viewName);
+
         /** Select the HIPS Source catalog. */
         void slotHIPSSource();
 
+        /** Invoke the Views editor window */
+        void slotEditViews();
+
         /** Invoke the Field-of-View symbol editor window */
         void slotFOVEdit();
 
@@ -855,6 +868,7 @@ class KStars : public KXmlGuiWindow
 
         KActionMenu *colorActionMenu { nullptr };
         KActionMenu *fovActionMenu { nullptr };
+        KActionMenu *viewsActionMenu { nullptr };
         KActionMenu *hipsActionMenu { nullptr };
         KActionMenu *orientationActionMenu { nullptr };
 
@@ -907,6 +921,8 @@ class KStars : public KXmlGuiWindow
         QActionGroup *hipsGroup { nullptr };
         QActionGroup *telescopeGroup { nullptr };
         QActionGroup *domeGroup { nullptr };
+        QActionGroup *erectObserverCorrectionGroup { nullptr };
+        QActionGroup *viewsGroup { nullptr };
 
         bool DialogIsObsolete { false };
         bool StartClockRunning { false };
diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg
index 4729deb9cc..2edf07e4a8 100644
--- a/kstars/kstars.kcfg
+++ b/kstars/kstars.kcfg
@@ -811,10 +811,21 @@
          <whatsthis>Enable this if you want the sky map to be mirrored left-right, e.g. to match the view through an erecting prism.</whatsthis>
          <default>false</default>
       </entry>
-      <entry name="ErectObserverCorrection" type="Bool">
+      <entry name="ErectObserverCorrection" type="Enum">
          <label>Orients the sky-map to account for an erect observer at the eyepiece</label>
-         <whatsthis>Enable this if you are using your eye at the eyepiece in an altazimuth mounted Newtonian telescope. This accounts for the fact that the observer stands erect as the telescope moves up and down, so that the orientation of the sky map will track what is seen in your eyepiece once it is set up correctly.</whatsthis>
-         <default>false</default>
+         <whatsthis>Enable this if you are using your eye at the eyepiece in an altazimuth mounted Newtonian telescope. This accounts for the fact that the observer stands erect as the telescope moves up and down, so that the orientation of the sky map will track what is seen in your eyepiece once it is set up correctly. Choose the handedness of the correction according which side of the telescope the eyepiece appears when looking from the back of the telescope</whatsthis>
+	 <choices>
+	   <choice name="Off">
+	     <label>Off</label>
+	   </choice>
+	   <choice name="Left">
+	     <label>Left</label>
+	   </choice>
+	   <choice name="Right">
+	     <label>Right</label>
+	   </choice>
+	 </choices>
+         <default>0</default> <!-- Off -->
       </entry>
       <entry name="ZoomScrollFactor" type="Double">
          <label>Zoom scroll sensitivity.</label>
diff --git a/kstars/kstarsactions.cpp b/kstars/kstarsactions.cpp
index c0f7c291fd..f3932f787a 100644
--- a/kstars/kstarsactions.cpp
+++ b/kstars/kstarsactions.cpp
@@ -20,6 +20,7 @@
 #include "dialogs/finddialog.h"
 #include "dialogs/focusdialog.h"
 #include "dialogs/fovdialog.h"
+#include "dialogs/viewsdialog.h"
 #include "dialogs/locationdialog.h"
 #include "dialogs/timedialog.h"
 #include "dialogs/catalogsdbui.h"
@@ -1705,9 +1706,7 @@ void KStars::slotCoordSys()
         actionCollection()
         ->action("down_orientation")
         ->setText(i18nc("Orientation of the sky map", "North &Down"));
-        actionCollection()
-        ->action("erect_observer_correction")
-        ->setEnabled(false);
+        erectObserverCorrectionGroup->setEnabled(false);
     }
     else
     {
@@ -1726,10 +1725,9 @@ void KStars::slotCoordSys()
         actionCollection()
         ->action("down_orientation")
         ->setText(i18nc("Orientation of the sky map", "Zenith &Down"));
-        actionCollection()
-        ->action("erect_observer_correction")
-        ->setEnabled(true);
+        erectObserverCorrectionGroup->setEnabled(true);
     }
+    actionCollection()->action("view:arbitrary")->setChecked(true);
     map()->forceUpdate();
 }
 
@@ -1743,18 +1741,12 @@ void KStars::slotSkyMapOrientation()
     {
         Options::setSkyRotation(180.0);
     }
-    else if (sender() == actionCollection()->action("mirror_skymap"))
-    {
-        ;
-    }
-    else
-    {
-        Q_ASSERT(false && "Unhandled orientation action");
-        qCWarning(KSTARS) << "Unhandled orientation action";
-    }
 
     Options::setMirrorSkyMap(actionCollection()->action("mirror_skymap")->isChecked());
-    Options::setErectObserverCorrection(actionCollection()->action("erect_observer_correction")->isChecked());
+    Options::setErectObserverCorrection(
+        actionCollection()->action("erect_observer_correction_off")->isChecked() ? 0 : (
+            actionCollection()->action("erect_observer_correction_left")->isChecked() ? 1 : 2));
+    actionCollection()->action("view:arbitrary")->setChecked(true);
     map()->forceUpdate();
 }
 
@@ -1810,6 +1802,60 @@ void KStars::slotTargetSymbol(bool flag)
     map()->forceUpdate();
 }
 
+void KStars::slotApplySkyMapView(const QString &viewName)
+{
+
+    auto view = SkyMapViewManager::viewNamed(viewName);
+    if (!view)
+    {
+        qCWarning(KSTARS) << "View named " << viewName << " not found!";
+        return;
+    }
+
+    // FIXME: Ugly hack to update the menus correctly...
+    // we set the opposite coordinate system setting and call slotCoordSys to toggle
+    Options::setUseAltAz(!view->useAltAz);
+    slotCoordSys();
+
+    Options::setMirrorSkyMap(view->mirror);
+    actionCollection()->action("mirror_skymap")->setChecked(Options::mirrorSkyMap());
+
+    int erectObserverCorrection = 0;
+    double viewAngle = view->viewAngle;
+    if (view->erectObserver && view->useAltAz)
+    {
+        if (viewAngle > 0.)
+        {
+            erectObserverCorrection = 1;
+            viewAngle -= 90.; // FIXME: Check
+        }
+        if (viewAngle < 0.)
+        {
+            erectObserverCorrection = 2;
+            viewAngle += 90.; // FIXME: Check
+        }
+    }
+    if (view->inverted)
+    {
+        viewAngle += 180.; // FIXME: Check
+    }
+
+    Options::setErectObserverCorrection(erectObserverCorrection);
+    Options::setSkyRotation(dms::reduce(viewAngle));
+    if (!std::isnan(view->fov))
+    {
+        Options::setZoomFactor(map()->width() / (3 * view->fov * dms::DegToRad));
+    }
+    repopulateOrientation(); // Update the menus
+    qCDebug(KSTARS) << "Alt/Az: " << Options::useAltAz()
+                    << "Mirror: " << Options::mirrorSkyMap()
+                    << "Rotation: " << Options::skyRotation()
+                    << "Erect Obs: " << Options::erectObserverCorrection()
+                    << "FOV: " << view->fov;
+    actionCollection()->action(QString("view:%1").arg(viewName))->setChecked(true);
+    map()->forceUpdate();
+}
+
 void KStars::slotHIPSSource()
 {
     QAction *selectedAction = qobject_cast<QAction *>(sender());
@@ -1827,6 +1873,17 @@ void KStars::slotHIPSSource()
     map()->forceUpdate();
 }
 
+void KStars::slotEditViews()
+{
+    QPointer<ViewsDialog> viewsDialog = new ViewsDialog(this);
+    if (viewsDialog->exec() == QDialog::Accepted)
+    {
+        SkyMapViewManager::save();
+        repopulateViews();
+    }
+    delete viewsDialog;
+}
+
 void KStars::slotFOVEdit()
 {
     QPointer<FOVDialog> fovdlg = new FOVDialog(this);
diff --git a/kstars/kstarsinit.cpp b/kstars/kstarsinit.cpp
index 8774628ac8..0fbc6b3e90 100644
--- a/kstars/kstarsinit.cpp
+++ b/kstars/kstarsinit.cpp
@@ -377,6 +377,14 @@ void KStars::initActions()
     FOVManager::readFOVs();
     repopulateFOV();
 
+    //Add Views menu actions
+    viewsActionMenu = actionCollection()->add<KActionMenu>("views");
+    viewsActionMenu->setText(i18n("&Views"));
+    viewsActionMenu->setDelayed(false);
+    viewsActionMenu->setIcon(QIcon::fromTheme("text_rotation"));
+    SkyMapViewManager::readViews();
+    repopulateViews();
+
     //Add HIPS Sources actions
     hipsActionMenu = actionCollection()->add<KActionMenu>("hipssources");
     hipsActionMenu->setText(i18n("HiPS All Sky Overlay"));
@@ -746,13 +754,64 @@ void KStars::repopulateOrientation()
                          "This mode is selected automatically if you manually rotated the sky map using Shift + Drag mouse action, to inform you that the orientation is arbitrary")));
 
     orientationActionMenu->addSeparator();
-    QAction *erectObserverAction = newToggleAction(
-                                       actionCollection(), "erect_observer_correction",
-                                       i18nc("Orient sky map for an erect observer", "Erect observer correction"),
-                                       this, SLOT(slotSkyMapOrientation()));
-    erectObserverAction << ToolTip(i18nc("Orient sky map for an erect observer",
-                                         "Enable this mode if you are visually using a Newtonian telescope on an altazimuth mount. It will correct the orientation of the sky-map to account for the observer remaining erect as the telescope moves up and down, unlike a camera which would rotate with the telescope. This only makes sense in Horizontal Coordinate mode and is disabled when using Equatorial Coordinates. Typically makes sense to combine this with Zenith Down orientation."));
-    orientationActionMenu->addAction(erectObserverAction);
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_off", this, SLOT(slotSkyMapOrientation()))
+        << i18nc("Do not adjust the orientation of the sky map for an erect observer", "No correction")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 0)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are using a camera on the telescope, or have the sky map display mounted on your telescope")));
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_left", this, SLOT(slotSkyMapOrientation()))
+        << i18nc("Adjust the orientation of the sky map for an erect observer, left-handed telescope",
+                 "Erect observer correction, left-handed")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 1)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are visually observing using a Dobsonian telescope which has the focuser appearing on the left side when looking up the telescope tube. This feature will correct the orientation of the sky-map to account for the observer remaining erect as the telescope moves up and down, unlike a camera which would rotate with the telescope. Typically makes sense to combine this with Zenith Down orientation.")));
+
+    orientationActionMenu->addAction(
+        actionCollection()->addAction(
+            "erect_observer_correction_right", this, SLOT(slotSkyMapOrientation()))
+        << i18nc("Adjust the orientation of the sky map for an erect observer, left-handed telescope",
+                 "Erect observer correction, right-handed")
+        << AddToGroup(erectObserverCorrectionGroup)
+        << Checked(Options::erectObserverCorrection() == 2)
+        << ToolTip(i18nc("Orientation of the sky map",
+                         "Select this if you are visually observing using a Dobsonian telescope which has the focuser appearing on the right side when looking up the telescope tube. This feature will correct the orientation of the sky-map to account for the observer remaining erect as the telescope moves up and down, unlike a camera which would rotate with the telescope. Typically makes sense to combine this with Zenith Down orientation.")));
+
+}
+
+void KStars::repopulateViews()
+{
+    viewsActionMenu->menu()->clear();
+
+    QList<QAction*> actions = viewsGroup->actions();
+    for (auto &action : actions)
+        viewsGroup->removeAction(action);
+
+    for (const auto &view : SkyMapViewManager::getViews())
+    {
+        QAction* action = actionCollection()->addAction(QString("view:%1").arg(view.name), this, [ = ]()
+        {
+            slotApplySkyMapView(view.name);
+        })
+                << view.name << AddToGroup(viewsGroup) << Checked(false);
+        viewsActionMenu->addAction(action);
+        action->setData(view.name);
+    }
+    viewsActionMenu->addAction(
+        actionCollection()->addAction("view:arbitrary")
+        << i18nc("Arbitrary Sky Map View", "Arbitrary") << AddToGroup(viewsGroup) << Checked(true)); // FIXME
+
+    // Add menu bottom
+    QAction *ka = actionCollection()->addAction("edit_views", this, SLOT(slotEditViews())) << i18n("Edit Views...");
+    viewsActionMenu->addSeparator();
+    viewsActionMenu->addAction(ka);
 }
 
 void KStars::repopulateFOV()
diff --git a/kstars/skycomponents/hipscomponent.cpp b/kstars/skycomponents/hipscomponent.cpp
index a42d444dc8..04f99cc172 100644
--- a/kstars/skycomponents/hipscomponent.cpp
+++ b/kstars/skycomponents/hipscomponent.cpp
@@ -44,6 +44,7 @@ void HIPSComponent::draw(SkyPainter *skyp)
                         view.height == m_previousViewParams.height &&
                         view.zoomFactor == m_previousViewParams.zoomFactor &&
                         view.rotationAngle == m_previousViewParams.rotationAngle &&
+                        view.mirror == m_previousViewParams.mirror &&
                         view.useAltAz == m_previousViewParams.useAltAz
                     );
     if (sameView && Options::isTracking() && SkyMap::IsFocused())
diff --git a/kstars/skymap.cpp b/kstars/skymap.cpp
index b0a48ecb99..955211a56a 100644
--- a/kstars/skymap.cpp
+++ b/kstars/skymap.cpp
@@ -56,6 +56,9 @@
 #include <QClipboard>
 #include <QInputDialog>
 #include <QDesktopServices>
+#include <QPropertyAnimation>
+#include <QGraphicsOpacityEffect>
+#include <QGraphicsSimpleTextItem>
 
 #include <QProcess>
 #include <QFileDialog>
@@ -722,8 +725,8 @@ void SkyMap::slotEndRulerMode()
                                     ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX()));
             }
             fov = nameToFovMap[QInputDialog::getItem(this, i18n("Star Hopper: Choose a field-of-view"),
-                                                           i18n("FOV to use for star hopping:"), nameToFovMap.keys(), 0,
-                                                           false, &ok)];
+                               i18n("FOV to use for star hopping:"), nameToFovMap.keys(), 0,
+                               false, &ok)];
         }
         else
         {
@@ -1206,8 +1209,14 @@ dms SkyMap::determineSkyRotation()
     // orientation of the field. This would not apply to a CCD camera
     // plugged into the same telescope, since the CCD would rotate as
     // seen from the ground when the telescope moves in altitude.
-    return dms(Options::skyRotation() - (
-                   (Options::erectObserverCorrection() && Options::useAltAz()) ? focus()->alt().Degrees() : 0.0));
+
+    double erectObserverCorrection = 0.;
+    if (Options::useAltAz() && Options::erectObserverCorrection() > 0)
+    {
+        erectObserverCorrection = (Options::erectObserverCorrection() == 1) ? focus()->alt().Degrees() : -focus()->alt().Degrees();
+    }
+
+    return dms(Options::skyRotation() + erectObserverCorrection);
 }
 
 void SkyMap::slotSetSkyRotation(double angle)
@@ -1229,6 +1238,7 @@ void SkyMap::slotSetSkyRotation(double angle)
         {
             kstars->actionCollection()->action("arbitrary_orientation")->setChecked(true);
         }
+        kstars->actionCollection()->action("view:arbitrary")->setChecked(true);
     }
     forceUpdate();
 }
@@ -1363,3 +1373,32 @@ void SkyMap::slotStartXplanetViewer()
     else
         new XPlanetImageViewer(i18n("Saturn"), this);
 }
+
+void SkyMap::slotDisplayFadingText(const QString &text)
+{
+    QLabel *fadingLabel = new QLabel(this);
+    fadingLabel->setText(text);
+    QFont font = fadingLabel->font();
+    QPalette palette = fadingLabel->palette();
+    font.setPointSize(32);
+    palette.setColor(fadingLabel->foregroundRole(), KStarsData::Instance()->colorScheme()->colorNamed("BoxTextColor"));
+    QColor backgroundColor = KStarsData::Instance()->colorScheme()->colorNamed("BoxBGColor");
+    backgroundColor.setAlpha(192);
+    palette.setColor(fadingLabel->backgroundRole(), backgroundColor);
+    fadingLabel->setFont(font);
+    fadingLabel->setAutoFillBackground(true);
+    fadingLabel->setPalette(palette);
+    fadingLabel->setAlignment(Qt::AlignCenter);
+    fadingLabel->adjustSize();
+    fadingLabel->move(QPoint((width() - fadingLabel->width()) / 2, (0.75 * height() - fadingLabel->height() / 2)));
+    QGraphicsOpacityEffect* fadingEffect = new QGraphicsOpacityEffect(fadingLabel);
+    fadingLabel->setGraphicsEffect(fadingEffect);
+    fadingLabel->show();
+
+    QPropertyAnimation* animation = new QPropertyAnimation(fadingEffect, "opacity", fadingLabel);
+    animation->setDuration(1500);
+    animation->setStartValue(1.0);
+    animation->setEndValue(0.0);
+    connect(animation, &QPropertyAnimation::finished, fadingLabel, &QLabel::deleteLater);
+    animation->start();
+}
diff --git a/kstars/skymap.h b/kstars/skymap.h
index 65542107ea..19c1079e57 100644
--- a/kstars/skymap.h
+++ b/kstars/skymap.h
@@ -480,6 +480,9 @@ class SkyMap : public QGraphicsView
              */
         void slotRemovePlanetTrail();
 
+        /** @short Render a fading text label on the screen to flash information */
+        void slotDisplayFadingText(const QString &text);
+
         /** Checks whether the timestep exceeds a threshold value.  If so, sets
              * ClockSlewing=true and sets the SimClock to ManualMode.
              */
diff --git a/kstars/skymapevents.cpp b/kstars/skymapevents.cpp
index ff53f7e348..ae2eb76bd1 100644
--- a/kstars/skymapevents.cpp
+++ b/kstars/skymapevents.cpp
@@ -363,13 +363,27 @@ void SkyMap::keyPressEvent(QKeyEvent *e)
 
         case Qt::Key_PageUp:
         {
-            KStars::Instance()->selectPreviousFov();
+            if (shiftPressed)
+            {
+                KStars::Instance()->selectPreviousView();
+            }
+            else
+            {
+                KStars::Instance()->selectPreviousFov();
+            }
             break;
         }
 
         case Qt::Key_PageDown:
         {
-            KStars::Instance()->selectNextFov();
+            if (shiftPressed)
+            {
+                KStars::Instance()->selectNextView();
+            }
+            else
+            {
+                KStars::Instance()->selectNextFov();
+            }
             break;
         }
 
@@ -409,7 +423,7 @@ bool SkyMap::event(QEvent *event)
     {
         QGestureEvent* gestureEvent = static_cast<QGestureEvent*>(event);
 
-        if (QPinchGesture *pinch = static_cast<QPinchGesture*>(gestureEvent->gesture(Qt::PinchGesture)))
+        if (QPinchGesture *pinch = static_cast<QPinchGesture * >(gestureEvent->gesture(Qt::PinchGesture)))
         {
             QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags();
 
@@ -435,7 +449,7 @@ bool SkyMap::event(QEvent *event)
                 }
             }
         }
-        if (QTapAndHoldGesture *tapAndHold = static_cast<QTapAndHoldGesture*>(gestureEvent->gesture(Qt::TapAndHoldGesture)))
+        if (QTapAndHoldGesture *tapAndHold = static_cast<QTapAndHoldGesture * >(gestureEvent->gesture(Qt::TapAndHoldGesture)))
         {
             m_tapAndHoldMode = true;
             if (tapAndHold->state() == Qt::GestureFinished)
diff --git a/kstars/widgets/unitspinboxwidget.cpp b/kstars/widgets/unitspinboxwidget.cpp
index 6ca2aa4ccc..c8164927b1 100644
--- a/kstars/widgets/unitspinboxwidget.cpp
+++ b/kstars/widgets/unitspinboxwidget.cpp
@@ -4,6 +4,7 @@
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 #include "unitspinboxwidget.h"
+#include<cmath>
 
 UnitSpinBoxWidget::UnitSpinBoxWidget(QWidget *parent) : QWidget(parent), ui(new Ui::UnitSpinBoxWidget)
 {
@@ -34,3 +35,26 @@ double UnitSpinBoxWidget::value() const
     double value            = doubleSpinBox->value();
     return value * conversionFactor;
 }
+
+void UnitSpinBoxWidget::setValue(const double value)
+{
+    if (value < 1e-20 && value > -1e-20)
+    {
+        // Practically zero
+        doubleSpinBox->setValue(value);
+        return;
+    }
+    std::vector<double> diffs;
+    for (int index = 0; index < comboBox->count(); ++index)
+    {
+        QVariant qv = comboBox->itemData(index);
+        double conversionFactor = qv.value<double>();
+        diffs.push_back(std::abs(std::abs(value) / conversionFactor - 1.));
+    }
+    auto it = std::min_element(diffs.cbegin(), diffs.cend());
+    int index = std::distance(diffs.cbegin(), it);
+    comboBox->setCurrentIndex(index);
+    QVariant qv = comboBox->itemData(index);
+    double conversionFactor = qv.value<double>();
+    doubleSpinBox->setValue(value / conversionFactor);
+}
diff --git a/kstars/widgets/unitspinboxwidget.h b/kstars/widgets/unitspinboxwidget.h
index 13ed1a9f15..97664b4eac 100644
--- a/kstars/widgets/unitspinboxwidget.h
+++ b/kstars/widgets/unitspinboxwidget.h
@@ -18,28 +18,49 @@
  */
 class UnitSpinBoxWidget : public QWidget
 {
-    Q_OBJECT
+        Q_OBJECT
 
-  public:
-    explicit UnitSpinBoxWidget(QWidget *parent = nullptr);
-    ~UnitSpinBoxWidget() override;
+    public:
+        QComboBox *comboBox;
+        QDoubleSpinBox *doubleSpinBox;
 
-    /**
+        explicit UnitSpinBoxWidget(QWidget *parent = nullptr);
+        ~UnitSpinBoxWidget() override;
+
+        /**
          * @brief addUnit Adds a item to the combo box
          * @param unitName The name of the unit to be displayed
          * @param conversionFactor The factor the value of a unit must be multiplied by
          */
-    void addUnit(const QString &unitName, double conversionFactor);
+        void addUnit(const QString &unitName, double conversionFactor);
+
+        /** @return whether the widget is enabled */
+        inline bool enabled()
+        {
+            Q_ASSERT(comboBox->isEnabled() == doubleSpinBox->isEnabled());
+            return doubleSpinBox->isEnabled();
+        }
 
-    /**
-         * @brief value Returns value upon conversion
+        /** @brief value Returns value upon conversion */
+        double value() const;
+
+    public slots:
+        /**
+         * @brief Sets the given value
+         * @param value The value to set
+         * @note Automatically optimizes the display to use the best unit for the given value
          */
-    double value() const;
+        void setValue(const double value);
+
+        /** @brief Enables the widget */
+        void setEnabled(bool enabled)
+        {
+            comboBox->setEnabled(enabled);
+            doubleSpinBox->setEnabled(enabled);
+        }
 
-  private:
-    Ui::UnitSpinBoxWidget *ui;
-    QComboBox *comboBox;
-    QDoubleSpinBox *doubleSpinBox;
+    private:
+        Ui::UnitSpinBoxWidget *ui;
 };
 
 #endif // UNITSPINBOXWIDGET_H
diff --git a/kstars/widgets/unitspinboxwidget.ui b/kstars/widgets/unitspinboxwidget.ui
index dea56360a9..b688e28ac5 100644
--- a/kstars/widgets/unitspinboxwidget.ui
+++ b/kstars/widgets/unitspinboxwidget.ui
@@ -7,31 +7,34 @@
     <x>0</x>
     <y>0</y>
     <width>179</width>
-    <height>44</height>
+    <height>32</height>
    </rect>
   </property>
-  <widget class="QComboBox" name="comboBox">
-   <property name="geometry">
-    <rect>
-     <x>90</x>
-     <y>10</y>
-     <width>80</width>
-     <height>25</height>
-    </rect>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <property name="spacing">
+    <number>0</number>
    </property>
-  </widget>
-  <widget class="QDoubleSpinBox" name="doubleSpinBox">
-   <property name="geometry">
-    <rect>
-     <x>10</x>
-     <y>10</y>
-     <width>66</width>
-     <height>26</height>
-    </rect>
+   <property name="leftMargin">
+    <number>0</number>
    </property>
-  </widget>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QDoubleSpinBox" name="doubleSpinBox"/>
+   </item>
+   <item>
+    <widget class="QComboBox" name="comboBox"/>
+   </item>
+  </layout>
  </widget>
- <layoutdefault spacing="6" margin="11"/>
+ <layoutdefault spacing="0" margin="0"/>
  <resources/>
  <connections/>
 </ui>


More information about the kde-doc-english mailing list