2026
NightSkiez
A cross-platform planetarium and observation planner for Android and Windows, built from scratch in C++ and Qt Quick.
Overview
NightSkiez is a stargazing app in the spirit of Stellarium: hold your phone up to the sky and it shows you what you are looking at. It runs from a single C++/Qt codebase on both Android and Windows, and it computes every position it draws — stars, Sun, Moon, planets and satellites — from first principles rather than querying a positions API.
What began as an exercise in real-time astronomical computation grew into an observation planner. Beyond identifying what is overhead right now, it answers the two questions a stargazer actually has before going outside: is tonight worth it? and when will something interesting fly over? A clear-sky forecast scores the coming night hour by hour, and an SGP4 orbit propagator predicts visible ISS passes days in advance.
At a glance
- Platforms — Android (arm64-v8a, API 28+) and Windows desktop, from one codebase
- Languages — C++17 for the simulation engine, QML for the interface
- Framework — Qt 6.11 (Quick, Positioning, Sensors, Network)
- Build — CMake and Ninja; MinGW for desktop, NDK 27 / Clang for Android
- Scale — roughly 2,600 hand-written lines, plus a 2,400-line vendored SGP4 propagator
- Data — an 8,920-star catalogue compiled to a 147 KB binary; 157 tracked satellites
- Built — July to August 2026
What it does
- A real-time sky map of 8,920 stars down to magnitude 6.5 — roughly what the unaided eye sees from a dark site — plus the Sun, Moon and the five naked-eye planets
- Point-at-sky mode, where the view tracks the phone's physical orientation
- Constellation figures, star labels and an altitude-azimuth grid, each independently toggleable
- Time scrubbing across ±24 hours, so tonight's sky can be previewed at midday
- Satellite tracking with motion trails and sunlit/eclipsed shading, plus a next visible ISS pass readout
- A clear-sky forecast that scores each dark hour of the coming night for observing quality
- GPS positioning with a graceful fallback, pinch-to-zoom and drag-to-pan
The astronomy engine
The core is a small, dependency-free C++ library:
- Time and rotation. Julian date from the system clock, then Greenwich Mean Sidereal Time via the IAU 1982 series — the quantity that turns a catalogue's fixed right ascension into a bearing over a specific place at a specific instant.
- Coordinates. Equatorial-to-horizontal transforms, with Meeus's south-referenced azimuth converted to compass convention, and Saemundsson atmospheric refraction applied so objects near the horizon are drawn where they actually appear rather than where geometry alone would put them.
- Ephemeris. Planetary positions from JPL's approximate Keplerian elements (Standish), with Kepler's equation solved by Newton iteration. The Moon comes from a low-precision lunar series plus a topocentric parallax correction — necessary because the Moon is close enough that the observer's position on the Earth's surface shifts its apparent place by up to a degree.
- Star catalogue. A Python build step filters the 34 MB HYG v4.1 database down to the 8,920 stars brighter than magnitude 6.5 and emits a 147 KB binary sorted by brightness. Sorting at build time rather than runtime is what lets the renderer's magnitude cutoff be a loop break instead of a filter pass.
Rendering the sky
The sky view is a custom-painted Qt Quick item rather than an off-the-shelf chart, which made a few things possible:
- Projection. Stereographic, computed in an east-north-up frame. It preserves the shapes of constellations toward the edges of a wide field, where a naive projection visibly distorts them.
- Level of detail. The magnitude limit tracks the zoom level: a wide field shows roughly magnitude 5, and zooming in reveals down to 6.5 — loosely mimicking how more stars resolve as you concentrate on a patch of sky. Because the catalogue is pre-sorted by brightness, applying this limit costs a single early exit.
- Batching. Stars fainter than magnitude 4.2 are grouped by colour and drawn as batched point runs rather than individual ellipses, which keeps panning smooth with thousands of stars on screen.
- Colour. Star colour is interpolated from each star's real B–V index, so hot stars render blue-white and cool ones amber, and the field reads as subtly varied rather than uniformly white. • Moon phase. The Moon is drawn with its true phase: the terminator is an ellipse whose semi-axis follows the illuminated fraction, rotated so the lit limb faces the Sun's actual position on screen.
- Twilight. Background colour is driven by the computed solar altitude, so the app fades from daylight blue through twilight to near-black without any hardcoded schedule.
Satellite pass prediction
Satellite positions come from SGP4, the standard orbital propagator, using David Vallado's public-domain reference implementation. The work was in what surrounds it:
- Orbital elements. Two-line element sets from CelesTrak's visual group — the ~157 brightest objects. The app ships a snapshot so a first launch works offline, caches a copy for 24 hours, and refreshes over HTTPS at startup, because TLE accuracy decays within days.
- Coordinate chain. SGP4 emits position in the TEME frame; the app rotates that through sidereal time into an Earth-fixed frame, then into east-north-up for an observer placed on the WGS-84 ellipsoid.
- Visibility. A satellite is only worth watching when it is sunlit while the observer is in darkness, so a cylindrical Earth-shadow test determines illumination and the display colours accordingly — cyan when sunlit against a dark sky, grey when eclipsed.
- Pass search. For the ISS, the app steps forward in 30-second increments across five days, looking for the first moment the station is above 10°, sunlit, and the Sun is more than 6° below the observer's horizon.
Each satellite draws a 90-second motion trail, so a pass reads as a streak rather than a dot — and because time scrubbing drives the same code path, a pass three days away can be replayed on demand.
Pointing at the sky
Qt's built-in Compass type turned out to be unreliable across Android devices, so orientation is computed directly from raw sensors. The accelerometer supplies the gravity vector and the magnetometer the magnetic field; their cross products build an orthonormal world frame — the same construction Android's own getRotationMatrix uses. That yields a tilt-compensated bearing and an altitude angle, so the map follows the phone in two axes rather than just spinning about vertical.
The subtler half is that a magnetometer measures bearings from magnetic north, which differs from true north by a location-dependent angle. In southern Arizona that is nearly 9° — about eighteen full-Moon widths, easily enough to identify the wrong star. Rather than bundling a World Magnetic Model that would silently go stale, the app queries Android's own GeomagneticField for the declination at the current position and date through a JNI call, so the correction stays current with OS updates and is correct anywhere on Earth.
Verifying correctness
Astronomy code fails quietly. A sign error or an omitted correction produces a sky that looks entirely plausible and is simply wrong, and no unit test written against the same assumptions will catch it. The two hardest calculations were therefore checked against independent references:
- ISS pass prediction was compared against Heavens-Above's published pass table for the same coordinates. The two agreed to the minute.
- Magnetic declination was checked against the British Geological Survey's WMM-2025 web service: 8.6° from the app against 8.792° from BGS — a 0.2° difference, well inside a phone magnetometer's own noise floor.
Every build was also deployed to a physical device and driven through its features, since a passing desktop build says little about whether the Android package actually contains the right plugins — as the next section shows.
Engineering challenges
The genuinely difficult problems sat at the platform boundary rather than in the mathematics.
A silent Android packaging failure. Point-at-sky mode did nothing on device despite being correct in principle. The cause: importing a Qt module in QML deploys the QML types but not the platform backend plugin — Qt::Sensors also has to be linked from CMake before the Android deploy tool bundles it. The failure was completely silent, with sensors simply reporting no backend, so the fix included surfacing that state in the UI rather than letting it fail invisibly.
Porting a decades-old propagator. The vendored SGP4 needed three changes to compile for both targets: a helper named fmod that collided with the C++ standard library, epoch fields typed long that overflow on Windows where long is 32-bit, and Java-style 24.0d float literals that GCC tolerates but the Android NDK's Clang rejects. Each patch is documented alongside the vendored code so it survives a future upstream refresh.
A misleading QML registration error. Adding a C++ class to QML failed with "class was not declared" inside generated code. Qt's type registrar emits #include <basename.h> guarded by __has_include, so a header outside the include path is skipped silently and the generated registration cannot see the class — the error surfaces far from its cause.
Icons modern Android would have ruined. With a minimum SDK of 28, every device applies adaptive-icon treatment, which shrinks a plain bitmap onto a white circle. The app ships a proper adaptive icon whose background colour is sampled from the artwork itself, so a circular badge design reads as a full-bleed tile under any launcher mask. A build script regenerates all thirteen derived assets from a single source image.
Licensing and attribution
Stellarium, the obvious reference implementation, is GPL-licensed. It was studied and deliberately not borrowed from, so the project's own licensing options stay open. Every data source and dependency was chosen for permissive terms, and all of them are credited on an in-app attribution screen:
- HYG star database — CC BY-SA 4.0
- CelesTrak orbital elements
- Vallado's SGP4 — public domain
- Open-Meteo forecasts — CC BY 4.0, whose licence specifically requires visible credit
- JPL/Standish planetary elements
- Qt 6 — LGPL v3
The same screen notes that positions are computed for display and planning, and are not suitable for navigation or precision astrometry. Limitations and next steps
Stated plainly, because knowing where a system's accuracy ends is part of building it:
- Positions are display-grade. The ephemeris uses low-precision series accurate to roughly arcminutes — invisible on a phone screen, unsuitable for telescope pointing or astrometry.
- Rendering is CPU-painted. Smooth at current star counts, but a deeper catalogue would justify moving to Qt's scene graph.
- Marginal satellite passes can be missed. Very low grazing passes can fall between the 30-second search steps.
- The bundled TLE snapshot ages. It exists only as a first-launch fallback and should be refreshed per release; normal use relies on the startup network refresh.
On the roadmap: a light-pollution overlay, an observation log backed by SQLite, an astrophotography planner weighing target altitude curves against moon interference, and an augmented-reality camera overlay.