diff --git a/Localizable.xcstrings b/Localizable.xcstrings
index fd900f83a..3081ac4e3 100644
--- a/Localizable.xcstrings
+++ b/Localizable.xcstrings
@@ -311,6 +311,9 @@
}
}
}
+ },
+ "Altitude" : {
+
},
"Appearance" : {
"localizations" : {
diff --git a/Solstice.xcodeproj/project.pbxproj b/Solstice.xcodeproj/project.pbxproj
index a86a3288b..15a4ad5e6 100644
--- a/Solstice.xcodeproj/project.pbxproj
+++ b/Solstice.xcodeproj/project.pbxproj
@@ -1809,7 +1809,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Solstice uses your location to calculate local sunrise and sunset times";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.1.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -1871,7 +1871,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Solstice uses your location to calculate local sunrise and sunset times";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.1.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
diff --git a/Solstice.xcodeproj/xcuserdata/dte.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Solstice.xcodeproj/xcuserdata/dte.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
index bc810b64f..b6512d828 100644
--- a/Solstice.xcodeproj/xcuserdata/dte.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ b/Solstice.xcodeproj/xcuserdata/dte.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -3,52 +3,4 @@
uuid = "694A27F2-72FC-45F0-A4E5-28318A89FE23"
type = "1"
version = "2.0">
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Solstice/Charts/DaylightChart.swift b/Solstice/Charts/DaylightChart.swift
index 6cf4e8ab4..329bbad15 100644
--- a/Solstice/Charts/DaylightChart.swift
+++ b/Solstice/Charts/DaylightChart.swift
@@ -12,21 +12,21 @@ import Suite
struct DaylightChart: View {
@Environment(\.isLuminanceReduced) var isLuminanceReduced
@Environment(\.colorScheme) var colorScheme
-
+
@State private var selectedEvent: NTSolar.Event?
@State private var currentX: Date?
-
+
var solar: NTSolar
var timeZone: TimeZone
var showEventTypes = true
-
+
var appearance = Appearance.simple
var includesSummaryTitle = true
var hideXAxis = false
var scrubbable = false
var markSize: CGFloat = 6
- var yScale = -1.5...1.5
-
+ var yScale: ClosedRange? = nil
+
var plotDate: Date {
currentX ?? solar.date
}
@@ -81,12 +81,23 @@ struct DaylightChart: View {
private var chartContent: some View {
Chart {
+ ForEach(hours, id: \.self) { hour in
+ LineMark(
+ x: .value("Time", hour),
+ y: .value("Altitude", yValue(for: hour))
+ )
+ .interpolationMethod(.catmullRom)
+ .foregroundStyle(solarPathGradient)
+ .lineStyle(StrokeStyle(lineWidth: markSize, lineCap: .round, lineJoin: .round))
+ }
+
ForEach(filteredEvents, id: \.id) { solarEvent in
eventPointMark(for: solarEvent)
}
}
+ .chartLegend(.hidden)
.chartYAxis(.hidden)
- .chartYScale(domain: yScale)
+ .chartYScale(domain: effectiveYScale)
.chartXAxis(hideXAxis ? .hidden : .automatic)
.chartXAxis {
if !hideXAxis {
@@ -103,9 +114,7 @@ struct DaylightChart: View {
chartOverlayContent(proxy: proxy, geo: geo)
}
}
- .chartBackground { proxy in
- solarPathView(proxy: proxy)
- }
+ .animation(nil, value: solar.date)
}
private var filteredEvents: [NTSolar.Event] {
@@ -130,7 +139,7 @@ struct DaylightChart: View {
@ViewBuilder
private func chartOverlayContent(proxy: ChartProxy, geo: GeometryProxy) -> some View {
- let horizonY: CGFloat = proxy.position(forY: yValue(for: solar.safeSunrise)) ?? 0
+ let horizonY: CGFloat = proxy.position(forY: 0.0) ?? 0
Group {
horizonLine(width: geo.size.width, yOffset: horizonY)
@@ -141,6 +150,7 @@ struct DaylightChart: View {
sunAboveHorizon(proxy: proxy, horizonY: horizonY)
}
}
+ .animation(nil, value: solar.date)
scrubHitArea(geo: geo, proxy: proxy)
}
@@ -248,30 +258,6 @@ struct DaylightChart: View {
}
}
- private func solarPathView(proxy: ChartProxy) -> some View {
- Path { path in
- if let firstPoint = hours.first,
- let x = proxy.position(forX: firstPoint),
- let y = proxy.position(forY: yValue(for: firstPoint)) {
- path.move(to: CGPoint(x: x, y: y))
- }
-
- hours.forEach { hour in
- let x: CGFloat = proxy.position(forX: hour) ?? 0
- let y: CGFloat = proxy.position(forY: yValue(for: hour)) ?? 0
- path.addLine(to: CGPoint(x: x, y: y))
- }
-
- if let lastPoint = hours.last,
- let x = proxy.position(forX: lastPoint),
- let y = proxy.position(forY: yValue(for: lastPoint)) {
- path.move(to: CGPoint(x: x, y: y))
- }
- }
- .strokedPath(StrokeStyle(lineWidth: markSize, lineCap: .round, lineJoin: .round))
- .fill(solarPathGradient)
- }
-
private var solarPathGradient: LinearGradient {
LinearGradient(stops: [
Gradient.Stop(color: .secondary.opacity(0), location: 0),
@@ -285,19 +271,9 @@ extension DaylightChart {
var hours: Array {
stride(from: range.lowerBound, through: range.upperBound, by: 60 * 30).compactMap { $0 }
}
-
+
var startOfDay: Date { range.lowerBound }
- var endOfDay: Date { range.upperBound }
- var dayLength: TimeInterval { .twentyFourHours }
-
- var noonish: Date { startOfDay.addingTimeInterval(TimeInterval.twentyFourHours / 2) }
-
- var culminationDelta: TimeInterval { solar.solarNoon?.distance(to: noonish) ?? 0 }
-
- var daylightProportion: Double {
- solar.daylightDuration / dayLength
- }
-
+
func pointMarkColor(for eventPhase: NTSolar.Phase) -> HierarchicalShapeStyle {
switch eventPhase {
case .astronomical:
@@ -310,7 +286,7 @@ extension DaylightChart {
return .primary
}
}
-
+
func resetSelectedEvent() {
selectedEvent = solar.events.filter {
$0.phase == .sunset || $0.phase == .sunrise
@@ -318,15 +294,27 @@ extension DaylightChart {
a.date.compare(.now) == .orderedDescending
}).first
}
-
- func progressValue(for date: Date) -> Double {
- return (date.distance(to: startOfDay) - culminationDelta) / dayLength
+
+ /// The y-scale to use for the chart, fitted to the actual min/max altitudes
+ /// across all sampled hours with padding so the path never crowds the edges.
+ /// Callers may override via the `yScale` property.
+ private var effectiveYScale: ClosedRange {
+ if let yScale { return yScale }
+ let altitudes = hours.map { yValue(for: $0) }
+ guard let minAlt = altitudes.min(), let maxAlt = altitudes.max() else {
+ return -90.0...90.0
+ }
+ let span = maxAlt - minAlt
+ let padding = span * 0.1
+ return (minAlt - padding)...(maxAlt + padding)
}
-
+
+ /// The sun's actual altitude in degrees at `date` for the current solar.
+ /// 0° is the geometric horizon; positive = above, negative = below.
func yValue(for date: Date) -> Double {
- return sin(progressValue(for: date) * .pi * 2 - .pi / 2)
+ solar.altitude(at: date)
}
-
+
func scrub(to point: CGPoint, in geo: GeometryProxy, proxy: ChartProxy) {
var start: Double = 0
diff --git a/Solstice/Detail View/DailyOverview.swift b/Solstice/Detail View/DailyOverview.swift
index 7d68a1f62..da7a9dbbd 100644
--- a/Solstice/Detail View/DailyOverview.swift
+++ b/Solstice/Detail View/DailyOverview.swift
@@ -95,9 +95,6 @@ struct DailyOverview: View {
.background(Color("listRowBackgroundColor"))
)
#endif
- #if !os(macOS)
- .menuActionDismissBehavior(.disabled)
- #endif
#endif
Group {
diff --git a/Solstice/Extensions/AppStorage++.swift b/Solstice/Extensions/AppStorage++.swift
index 1eff036b6..3b9a6a7ae 100644
--- a/Solstice/Extensions/AppStorage++.swift
+++ b/Solstice/Extensions/AppStorage++.swift
@@ -137,6 +137,7 @@ struct Preferences {
static let timeTravelAppearance: Value = ("timeTravelAppearance", .expanded)
static let chartType: Value = ("chartType", .classic)
+ static let showSolsticesInChart: Value = ("showSolsticesInChart", false)
}
enum TimeTravelAppearance: String, CaseIterable, RawRepresentable, Identifiable {
diff --git a/Solstice/Helpers/Globals.swift b/Solstice/Helpers/Globals.swift
index 13aa0bb84..e41fc457e 100644
--- a/Solstice/Helpers/Globals.swift
+++ b/Solstice/Helpers/Globals.swift
@@ -9,7 +9,7 @@ import Foundation
var chartHeight: CGFloat = {
#if !os(watchOS)
- 300
+ 360
#else
200
#endif
@@ -19,7 +19,7 @@ var chartMarkSize: Double = {
#if os(watchOS)
4
#else
- 8
+ 6
#endif
}()
diff --git a/Solstice/Helpers/NTSolar.swift b/Solstice/Helpers/NTSolar.swift
index f55f70b67..6c91fff2b 100644
--- a/Solstice/Helpers/NTSolar.swift
+++ b/Solstice/Helpers/NTSolar.swift
@@ -679,3 +679,40 @@ struct NTSolar {
} /* GMST0 */
}
+
+extension NTSolar {
+ /// Returns the sun's altitude above the horizon in degrees at the given instant.
+ /// Positive values are above the horizon, negative values are below.
+ ///
+ /// Uses the same astronomical algorithms as the rest of NTSolar (Schlyter's
+ /// method), so results are consistent with the sunrise/sunset times already
+ /// computed by this struct.
+ func altitude(at date: Date) -> Double {
+ var utcCal = Calendar(identifier: .gregorian)
+ utcCal.timeZone = TimeZone(secondsFromGMT: 0)!
+ let comps = utcCal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
+ guard let year = comps.year, let month = comps.month, let day = comps.day,
+ let hour = comps.hour, let minute = comps.minute, let second = comps.second
+ else { return 0 }
+
+ // UT as a decimal hour
+ let UT = Double(hour) + Double(minute) / 60.0 + Double(second) / 3600.0
+
+ // Days since J2000.0 at the exact instant (used for both sun position and sidereal time)
+ let d = Double(NTSolar.days_since_2000_Jan_0(y: year, m: month, d: day)) + UT / 24.0
+
+ // Sun's equatorial coordinates at this instant
+ let (sRA, sdec, _) = NTSolar.sun_RA_dec(d: d)
+
+ // Local Mean Sidereal Time in degrees: GMST0(d) + UT_in_degrees + longitude
+ let LMST = NTSolar.revolution(x: NTSolar.GMST0(d: d) + UT * 15.0 + coordinate.longitude)
+
+ // Local Hour Angle: how far the sun has moved past the meridian
+ let HA = LMST - sRA
+
+ // Standard altitude formula: sin(alt) = sin(lat)sin(dec) + cos(lat)cos(dec)cos(HA)
+ let sin_alt = NTSolar.sind(x: coordinate.latitude) * NTSolar.sind(x: sdec)
+ + NTSolar.cosd(x: coordinate.latitude) * NTSolar.cosd(x: sdec) * NTSolar.cosd(x: HA)
+ return NTSolar.asind(x: sin_alt)
+ }
+}
diff --git a/Widget/Solar Chart Widget/SolarChartWidgetView.swift b/Widget/Solar Chart Widget/SolarChartWidgetView.swift
index c50050d8e..414076b27 100644
--- a/Widget/Solar Chart Widget/SolarChartWidgetView.swift
+++ b/Widget/Solar Chart Widget/SolarChartWidgetView.swift
@@ -44,8 +44,7 @@ struct SolarChartWidgetView: SolsticeWidgetView {
timeZone: location.timeZone,
showEventTypes: false,
includesSummaryTitle: false,
- markSize: 3,
- yScale: -1.0...1.5
+ markSize: 3
)
}
} else if needsReconfiguration {