Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@
}
}
}
},
"Altitude" : {

},
"Appearance" : {
"localizations" : {
Expand Down
4 changes: 2 additions & 2 deletions Solstice.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,4 @@
uuid = "694A27F2-72FC-45F0-A4E5-28318A89FE23"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "D80AA32B-93C1-4B79-8BCD-7882590E25ED"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Solstice/Helpers/DeepLinkResolver.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "58"
endingLineNumber = "58"
landmarkName = "body(content:)"
landmarkType = "7">
<Locations>
<Location
uuid = "D80AA32B-93C1-4B79-8BCD-7882590E25ED - 87f91cbc3384d0c9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.URL) -&gt; () in Solstice.DeepLinkResolver.body(content: SwiftUI._ViewModifier_Content&lt;Solstice.DeepLinkResolver&gt;) -&gt; some"
moduleName = "Solstice.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/dte/Developer/Solstice/Solstice/Helpers/DeepLinkResolver.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "58"
endingLineNumber = "58">
</Location>
<Location
uuid = "D80AA32B-93C1-4B79-8BCD-7882590E25ED - 24c23c16d8a7bd00"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.URL) -&gt; () in Solstice_watchOS_Watch_App.DeepLinkResolver.body(content: SwiftUI._ViewModifier_Content&lt;Solstice_watchOS_Watch_App.DeepLinkResolver&gt;) -&gt; some"
moduleName = "Solstice watchOS Watch App.debug.dylib"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/dte/Developer/Solstice/Solstice/Helpers/DeepLinkResolver.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "58"
endingLineNumber = "58">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>
94 changes: 41 additions & 53 deletions Solstice/Charts/DaylightChart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Double>? = nil

var plotDate: Date {
currentX ?? solar.date
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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] {
Expand All @@ -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)
Expand All @@ -141,6 +150,7 @@ struct DaylightChart: View {
sunAboveHorizon(proxy: proxy, horizonY: horizonY)
}
}
.animation(nil, value: solar.date)

scrubHitArea(geo: geo, proxy: proxy)
}
Expand Down Expand Up @@ -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),
Expand All @@ -285,19 +271,9 @@ extension DaylightChart {
var hours: Array<Date> {
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:
Expand All @@ -310,23 +286,35 @@ extension DaylightChart {
return .primary
}
}

func resetSelectedEvent() {
selectedEvent = solar.events.filter {
$0.phase == .sunset || $0.phase == .sunrise
}.sorted(by: { a, b in
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<Double> {
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

Expand Down
3 changes: 0 additions & 3 deletions Solstice/Detail View/DailyOverview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ struct DailyOverview<Location: AnyLocation>: View {
.background(Color("listRowBackgroundColor"))
)
#endif
#if !os(macOS)
.menuActionDismissBehavior(.disabled)
#endif
#endif

Group {
Expand Down
1 change: 1 addition & 0 deletions Solstice/Extensions/AppStorage++.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ struct Preferences {
static let timeTravelAppearance: Value<TimeTravelAppearance> = ("timeTravelAppearance", .expanded)

static let chartType: Value<ChartType> = ("chartType", .classic)
static let showSolsticesInChart: Value<Bool> = ("showSolsticesInChart", false)
}

enum TimeTravelAppearance: String, CaseIterable, RawRepresentable, Identifiable {
Expand Down
4 changes: 2 additions & 2 deletions Solstice/Helpers/Globals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

var chartHeight: CGFloat = {
#if !os(watchOS)
300
360
#else
200
#endif
Expand All @@ -19,7 +19,7 @@ var chartMarkSize: Double = {
#if os(watchOS)
4
#else
8
6
#endif
}()

Expand Down
37 changes: 37 additions & 0 deletions Solstice/Helpers/NTSolar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
3 changes: 1 addition & 2 deletions Widget/Solar Chart Widget/SolarChartWidgetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down