Commit 035622d5 by Dmitriy Stepanets

Merge branch 'master' into forecast-controller

parents 11ba9772 c897c67a
...@@ -15,7 +15,8 @@ public class LocationManager { ...@@ -15,7 +15,8 @@ public class LocationManager {
private let log = Logger(componentName: "LocationManager") private let log = Logger(componentName: "LocationManager")
private let delegates = MulticastDelegate<LocationManagerDelegate>() private let delegates = MulticastDelegate<LocationManagerDelegate>()
private let weatherUpdateSource: WeatherSource private let weatherUpdateSource: WeatherSource
private let defaultLocation = Location(coordinates: .init(latitude: 37.3230, longitude: -122.0322), private let defaultLocation = Location(lastTimeUpdated: Date(),
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
timeZone: TimeZone(abbreviation: "PST")!) timeZone: TimeZone(abbreviation: "PST")!)
private var _currentLocation: Location? { private var _currentLocation: Location? {
didSet { didSet {
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
import Foundation import Foundation
public struct CurrentWeather { public struct CurrentWeather: Equatable, Hashable {
public var lastTimeUpdated: Date
public var date: Date public var date: Date
public var timeZone: TimeZone public var timeZone: TimeZone
public var weekDay: WeekDay public var weekDay: WeekDay
...@@ -38,6 +39,7 @@ public struct CurrentWeather { ...@@ -38,6 +39,7 @@ public struct CurrentWeather {
extension CurrentWeather: UpdatableModelObjectInTime { extension CurrentWeather: UpdatableModelObjectInTime {
public func mergedWith(incrementalChanges: CurrentWeather) -> CurrentWeather { public func mergedWith(incrementalChanges: CurrentWeather) -> CurrentWeather {
var result = self var result = self
result.lastTimeUpdated = incrementalChanges.lastTimeUpdated
result.date = incrementalChanges.date result.date = incrementalChanges.date
result.timeZone = incrementalChanges.timeZone result.timeZone = incrementalChanges.timeZone
result.weekDay = incrementalChanges.weekDay result.weekDay = incrementalChanges.weekDay
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
import Foundation import Foundation
public struct DailyWeather { public struct DailyWeather: Equatable, Hashable {
public var lastTimeUpdated: Date
public var date: Date public var date: Date
public var timeZone: TimeZone public var timeZone: TimeZone
public var weekDay: WeekDay public var weekDay: WeekDay
...@@ -31,6 +32,7 @@ public struct DailyWeather { ...@@ -31,6 +32,7 @@ public struct DailyWeather {
extension DailyWeather: UpdatableModelObjectInTime { extension DailyWeather: UpdatableModelObjectInTime {
public func mergedWith(incrementalChanges: DailyWeather) -> DailyWeather { public func mergedWith(incrementalChanges: DailyWeather) -> DailyWeather {
var result = self var result = self
result.lastTimeUpdated = incrementalChanges.lastTimeUpdated
result.date = incrementalChanges.date result.date = incrementalChanges.date
result.timeZone = incrementalChanges.timeZone result.timeZone = incrementalChanges.timeZone
result.weekDay = incrementalChanges.weekDay result.weekDay = incrementalChanges.weekDay
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
import Foundation import Foundation
public struct DayTimeWeather { public struct DayTimeWeather: Equatable, Hashable {
public var lastTimeUpdated: Date
public var dayTime: DayTime public var dayTime: DayTime
public var date: Date public var date: Date
public var timeZone: TimeZone public var timeZone: TimeZone
...@@ -18,6 +19,7 @@ public struct DayTimeWeather { ...@@ -18,6 +19,7 @@ public struct DayTimeWeather {
public var temp: Temperature? public var temp: Temperature?
public init(dayTime: DayTime, hourly: HourlyWeather) { public init(dayTime: DayTime, hourly: HourlyWeather) {
self.lastTimeUpdated = hourly.lastTimeUpdated
self.dayTime = dayTime self.dayTime = dayTime
self.date = hourly.date self.date = hourly.date
self.timeZone = hourly.timeZone self.timeZone = hourly.timeZone
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
import Foundation import Foundation
public struct HourlyWeather { public struct HourlyWeather: Equatable, Hashable {
public var lastTimeUpdated: Date
public var date: Date public var date: Date
public var timeZone: TimeZone public var timeZone: TimeZone
public var weekDay: WeekDay public var weekDay: WeekDay
...@@ -27,6 +28,7 @@ public struct HourlyWeather { ...@@ -27,6 +28,7 @@ public struct HourlyWeather {
extension HourlyWeather: UpdatableModelObjectInTime { extension HourlyWeather: UpdatableModelObjectInTime {
public func mergedWith(incrementalChanges: HourlyWeather) -> HourlyWeather { public func mergedWith(incrementalChanges: HourlyWeather) -> HourlyWeather {
var result = self var result = self
result.lastTimeUpdated = incrementalChanges.lastTimeUpdated
result.date = incrementalChanges.date result.date = incrementalChanges.date
result.timeZone = incrementalChanges.timeZone result.timeZone = incrementalChanges.timeZone
result.weekDay = incrementalChanges.weekDay result.weekDay = incrementalChanges.weekDay
......
...@@ -8,8 +8,9 @@ ...@@ -8,8 +8,9 @@
import Foundation import Foundation
import CoreLocation import CoreLocation
public struct Location: CustomStringConvertible { public struct Location: Equatable, Hashable {
// MARK: - Data fields // MARK: - Data fields
public var lastTimeUpdated: Date
public var coordinates: CLLocationCoordinate2D? public var coordinates: CLLocationCoordinate2D?
public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL
...@@ -62,7 +63,10 @@ public struct Location: CustomStringConvertible { ...@@ -62,7 +63,10 @@ public struct Location: CustomStringConvertible {
public var cityId: String { public var cityId: String {
return "\(self.countryCode ?? ""):\(self.region ?? ""):\(self.cityName ?? "")" return "\(self.countryCode ?? ""):\(self.region ?? ""):\(self.cityName ?? "")"
} }
}
// MARK: - CustomStringConvertible implementation
extension Location: CustomStringConvertible {
public var description: String { public var description: String {
if let coordinates = self.coordinates { if let coordinates = self.coordinates {
return String(format: "%@ (%.3f, %.3f)", cityId, coordinates.latitude, coordinates.longitude) return String(format: "%@ (%.3f, %.3f)", cityId, coordinates.latitude, coordinates.longitude)
...@@ -73,12 +77,11 @@ public struct Location: CustomStringConvertible { ...@@ -73,12 +77,11 @@ public struct Location: CustomStringConvertible {
} }
} }
// MARK: - UpdatableModelObject implementation
extension Location: UpdatableModelObject { extension Location: UpdatableModelObject {
public func mergedWith(incrementalChanges: Location) -> Location { public func mergedWith(incrementalChanges: Location) -> Location {
var result = self var result = self
result.lastTimeUpdated = incrementalChanges.lastTimeUpdated
// The preferense is given to self values rather than incrementalChanges in this class // The preferense is given to self values rather than incrementalChanges in this class
result.coordinates = result.coordinates ?? incrementalChanges.coordinates result.coordinates = result.coordinates ?? incrementalChanges.coordinates
result.imageName = result.imageName ?? incrementalChanges.imageName result.imageName = result.imageName ?? incrementalChanges.imageName
result.countryCode = result.countryCode ?? incrementalChanges.countryCode result.countryCode = result.countryCode ?? incrementalChanges.countryCode
...@@ -95,3 +98,17 @@ extension Location: UpdatableModelObject { ...@@ -95,3 +98,17 @@ extension Location: UpdatableModelObject {
return result return result
} }
} }
extension CLLocationCoordinate2D: Hashable, Equatable {
private static let comparisonAccuracyLimit: CLLocationDegrees = 0.00001 // approximately 1 meter – more than enough for our purposes. https://en.wikipedia.org/wiki/Decimal_degrees
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
// no point in comparing hashes first, I think, since making the hash might be as computationally expensive as this whole thing (I didn't profile it though).
return fabs(lhs.latitude - rhs.latitude) + fabs(lhs.longitude - rhs.longitude) < CLLocationCoordinate2D.comparisonAccuracyLimit
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.latitude)
hasher.combine(self.longitude)
}
}
...@@ -18,10 +18,10 @@ struct WdtDailySummariesArray: Codable { ...@@ -18,10 +18,10 @@ struct WdtDailySummariesArray: Codable {
case items = "daily_summary" case items = "daily_summary"
} }
func toAppModel(timeZone: TimeZone) throws -> [DailyWeather] { func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> [DailyWeather] {
var result = [DailyWeather]() var result = [DailyWeather]()
for item in items { for item in items {
result.append(try item.toAppModel(timeZone: timeZone)) result.append(try item.toAppModel(timeZone: timeZone, updatedAt: updatedAt))
} }
return result return result
} }
......
...@@ -17,10 +17,10 @@ struct WdtHourlySummariesArray: Codable { ...@@ -17,10 +17,10 @@ struct WdtHourlySummariesArray: Codable {
case items = "hourly_summary" case items = "hourly_summary"
} }
func toAppModel(timeZone: TimeZone) throws -> [HourlyWeather] { func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> [HourlyWeather] {
var result = [HourlyWeather]() var result = [HourlyWeather]()
for item in items { for item in items {
result.append(try item.toAppModel(timeZone: timeZone)) result.append(try item.toAppModel(timeZone: timeZone, updatedAt: updatedAt))
} }
return result return result
} }
......
...@@ -41,7 +41,7 @@ struct WdtDailySummary: Codable { ...@@ -41,7 +41,7 @@ struct WdtDailySummary: Codable {
case moonsetUtc = "solunar_moonset_utc" case moonsetUtc = "solunar_moonset_utc"
} }
public func toAppModel(timeZone: TimeZone) throws -> DailyWeather { public func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> DailyWeather {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/YYYY" dateFormatter.dateFormat = "MM/dd/YYYY"
dateFormatter.timeZone = timeZone dateFormatter.timeZone = timeZone
...@@ -59,7 +59,7 @@ struct WdtDailySummary: Codable { ...@@ -59,7 +59,7 @@ struct WdtDailySummary: Codable {
throw WdtWeatherSourceError.dataMergeError("daily.weekday: \(self.weekDay)") throw WdtWeatherSourceError.dataMergeError("daily.weekday: \(self.weekDay)")
} }
var result = DailyWeather(date: date, timeZone: timeZone, weekDay: weekDayUnwrapped) var result = DailyWeather(lastTimeUpdated: updatedAt, date: date, timeZone: timeZone, weekDay: weekDayUnwrapped)
if let weatherCode = self.weatherCode { if let weatherCode = self.weatherCode {
result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown
} }
......
...@@ -34,7 +34,7 @@ struct WdtHourlySummary: Codable { ...@@ -34,7 +34,7 @@ struct WdtHourlySummary: Codable {
case weatherCode = "wx_code" case weatherCode = "wx_code"
} }
func toAppModel(timeZone: TimeZone) throws -> HourlyWeather { public func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> HourlyWeather {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
dateFormatter.timeZone = timeZone dateFormatter.timeZone = timeZone
...@@ -55,7 +55,7 @@ struct WdtHourlySummary: Codable { ...@@ -55,7 +55,7 @@ struct WdtHourlySummary: Codable {
let isDay = dayNight.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "day" let isDay = dayNight.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "day"
var result = HourlyWeather(date: date, timeZone: timeZone, weekDay: weekDayUnwrapped, isDay: isDay) var result = HourlyWeather(lastTimeUpdated: updatedAt, date: date, timeZone: timeZone, weekDay: weekDayUnwrapped, isDay: isDay)
if let weatherCode = self.weatherCode { if let weatherCode = self.weatherCode {
result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown
} }
......
...@@ -32,14 +32,14 @@ struct WdtLocation: Codable { ...@@ -32,14 +32,14 @@ struct WdtLocation: Codable {
case hourlySummaries = "hourly_summaries" case hourlySummaries = "hourly_summaries"
} }
public func toAppModel(timeZone: TimeZone) throws -> Location { public func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> Location {
var coordinates: CLLocationCoordinate2D? = nil var coordinates: CLLocationCoordinate2D? = nil
if let lat = CLLocationDegrees(lat ?? ""), let lon = CLLocationDegrees(lon ?? "") { if let lat = CLLocationDegrees(lat ?? ""), let lon = CLLocationDegrees(lon ?? "") {
coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon) coordinates = CLLocationCoordinate2D(latitude: lat, longitude: lon)
} }
var today = try surfaceObservation.toAppModel(timeZone: timeZone) var today = try surfaceObservation.toAppModel(timeZone: timeZone, updatedAt: updatedAt)
let dailyWeather = try dailySummaries?.toAppModel(timeZone: timeZone) ?? [DailyWeather]() let dailyWeather = try dailySummaries?.toAppModel(timeZone: timeZone, updatedAt: updatedAt) ?? [DailyWeather]()
let hourlyWeather = try hourlySummaries?.toAppModel(timeZone: timeZone) ?? [HourlyWeather]() let hourlyWeather = try hourlySummaries?.toAppModel(timeZone: timeZone, updatedAt: updatedAt) ?? [HourlyWeather]()
if let firstDay = dailyWeather.first { if let firstDay = dailyWeather.first {
today.minTemp = firstDay.minTemp today.minTemp = firstDay.minTemp
...@@ -50,7 +50,8 @@ struct WdtLocation: Codable { ...@@ -50,7 +50,8 @@ struct WdtLocation: Codable {
today.moonPhase = firstDay.moonPhase today.moonPhase = firstDay.moonPhase
} }
return Location(coordinates: coordinates, return Location(lastTimeUpdated: updatedAt,
coordinates: coordinates,
imageName: nil, imageName: nil,
countryCode: nil, countryCode: nil,
countryName: country, countryName: country,
......
...@@ -40,7 +40,7 @@ struct WdtSurfaceObservation: Codable { ...@@ -40,7 +40,7 @@ struct WdtSurfaceObservation: Codable {
case sunsetLocalTime = "sunset_local" case sunsetLocalTime = "sunset_local"
} }
public func toAppModel(timeZone: TimeZone) throws -> CurrentWeather { public func toAppModel(timeZone: TimeZone, updatedAt: Date) throws -> CurrentWeather {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
dateFormatter.timeZone = timeZone dateFormatter.timeZone = timeZone
...@@ -60,7 +60,7 @@ struct WdtSurfaceObservation: Codable { ...@@ -60,7 +60,7 @@ struct WdtSurfaceObservation: Codable {
let isDay = dayNight.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "day" let isDay = dayNight.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "day"
var result = CurrentWeather(date: date, timeZone: timeZone, weekDay: weekDayUnwrapped, isDay: isDay) var result = CurrentWeather(lastTimeUpdated: updatedAt, date: date, timeZone: timeZone, weekDay: weekDayUnwrapped, isDay: isDay)
if let weatherCode = self.weatherCode { if let weatherCode = self.weatherCode {
result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown result.type = WdtWeatherCode(rawValue: weatherCode)?.toAppModel() ?? .unknown
} }
......
...@@ -76,7 +76,8 @@ public class WdtWeatherSource: WeatherSource { ...@@ -76,7 +76,8 @@ public class WdtWeatherSource: WeatherSource {
guard let wdtLocation = locationResponse.locations.first else { guard let wdtLocation = locationResponse.locations.first else {
return nil return nil
} }
let incrementalChanges = try wdtLocation.toAppModel(timeZone: location.timeZone) let updateTime = Date()
let incrementalChanges = try wdtLocation.toAppModel(timeZone: location.timeZone, updatedAt: updateTime)
let updatedLocation = location.mergedWith(incrementalChanges: incrementalChanges) let updatedLocation = location.mergedWith(incrementalChanges: incrementalChanges)
return updatedLocation return updatedLocation
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment