diff --git a/README.md b/README.md index 21908f4..0be056e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ # MetobsKit -A description of this package. +This package is a wrapper for the PMP3g API provided by [SMHI](https://smhi.se). + + +### Usage + +Note that this is just a simple example demonstrating how the package can be used. + +```swift +/// Get an instance of the weather forecast service +let service = ForecastService.shared + +/// Request the current weather forecast for Stockholm +service.get(latitude: 59.3258414, longitude: 17.7018733) + .observe { result in + switch result { + case let .value(observation): + /// Use the current forecast + guard let forecast = observation.current else { + return + } + + /// Get the air temperature + let temperature = forecast.get(parameter: .airTemperature) + + /// Use the air temperature in some way + case let .error(error): + /// This error should be handled in a real use-case + fatalError("Failed to get forecast!") + } + } +``` + +### Attribution + +This package utilizes data provided by [SMHI](https://smhi.se). + +### Terms of use + +Make sure to read SMHI:s [terms of use](https://www.smhi.se/data/oppna-data/villkor-for-anvandning) before using this package. + +### Legal disclaimer + +The developer and this package are not affiliated with or endorsed by SMHI. Any products and services provided through this package are not supported or warrantied by SMHI. + +### License + +See LICENSE for license details concerning this package and SMHI:s [terms of use](https://www.smhi.se/data/oppna-data/villkor-for-anvandning) for license details concerning the data provided by their API. diff --git a/Sources/MetobsKit/Helpers.swift b/Sources/MetobsKit/Helpers.swift deleted file mode 100644 index 065a225..0000000 --- a/Sources/MetobsKit/Helpers.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Helpers.swift -// -// Copyright (c) 2019 Maximilian Wendel -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the Software), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Foundation - -extension Double { - func rounded(toPrecision precision: Int) -> Double { - let multiplier: Double = pow(10, Double(precision)) - return (self * multiplier).rounded() / multiplier - } -} - -internal func buildURL(latitude: Double, longitude: Double) -> Promise { - // Remove decimals exceeding six positions as it will cause a 404 response - let values: (lat: Double, lon: Double) = ( - lat: latitude.rounded(toPrecision: 6), - lon: longitude.rounded(toPrecision: 6) - ) - - guard let url = URL(string: "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/\(values.lon)/lat/\(values.lat)/data.json") else { - return Promise(value: nil, error: URLError(.badURL)) - } - - return Promise(value: url) -} diff --git a/Sources/MetobsKit/Models/Forecast.swift b/Sources/MetobsKit/Models/Forecast.swift index bf3d1f9..1b7296e 100644 --- a/Sources/MetobsKit/Models/Forecast.swift +++ b/Sources/MetobsKit/Models/Forecast.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A `Forecast` is a collection of `Value`s for a set of `Parameter`s public struct Forecast: Codable { /// A timestamp for when the `Forecast` is valid @@ -31,4 +29,12 @@ public struct Forecast: Codable { /// An array of `Value` instances public let parameters: [Value] + + /// Get `Value` for a `Parameter` + /// - Parameter parameter: The `Parameter` to get `Value` for + public func get(parameter: Parameter) -> Value { + return self.parameters.first { (value) -> Bool in + value.name == parameter + } ?? .unknown + } } diff --git a/Sources/MetobsKit/Models/Geometry.swift b/Sources/MetobsKit/Models/Geometry.swift index 8d4d45b..7acc72d 100644 --- a/Sources/MetobsKit/Models/Geometry.swift +++ b/Sources/MetobsKit/Models/Geometry.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A typealias for an array of `Double` public typealias GeometryCoordinate = [Double] diff --git a/Sources/MetobsKit/Models/Level.swift b/Sources/MetobsKit/Models/Level.swift index 0b0cab4..2324c27 100644 --- a/Sources/MetobsKit/Models/Level.swift +++ b/Sources/MetobsKit/Models/Level.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A `Level` representing the measurement's base-level public enum Level: String { /// Above sea level diff --git a/Sources/MetobsKit/Models/Observation.swift b/Sources/MetobsKit/Models/Observation.swift index 019e116..87d5283 100644 --- a/Sources/MetobsKit/Models/Observation.swift +++ b/Sources/MetobsKit/Models/Observation.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// An `Observation` is a collection of `Forecast` instances public struct Observation: Codable { /// A timestamp for when the `Forecast` was approved @@ -37,4 +35,9 @@ public struct Observation: Codable { /// An array of `Forecast` instances that reflect the overall `Forecast` over time public let timeSeries: [Forecast] + + /// The current `Forecast` + public var current: Forecast? { + return self.timeSeries.first + } } diff --git a/Sources/MetobsKit/Models/Parameter.swift b/Sources/MetobsKit/Models/Parameter.swift index f08a64b..62aadbc 100644 --- a/Sources/MetobsKit/Models/Parameter.swift +++ b/Sources/MetobsKit/Models/Parameter.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A `Parameter` type /// - Note: See https://opendata.smhi.se/apidocs/metfcst/parameters.html public enum Parameter: String { @@ -147,7 +145,7 @@ extension Parameter: Codable { } } - // MARK: Public methods + // MARK: Public functions /// Encode `Parameter` using `Encoder` public func encode(to encoder: Encoder) throws { diff --git a/Sources/MetobsKit/Models/Value.swift b/Sources/MetobsKit/Models/Value.swift index 5e479c6..5bb00bd 100644 --- a/Sources/MetobsKit/Models/Value.swift +++ b/Sources/MetobsKit/Models/Value.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A `Value` value representation for a `Forecast` parameter public struct Value: Codable { /// A `Parameter` type representing the underlying value's type @@ -40,4 +38,14 @@ public struct Value: Codable { /// An array of raw parameter values public let values: [Double] + + /// The first value of the raw parameter values + public var value: Double { + return self.values.first ?? 0 + } + + /// Unknown `Value` + public static var unknown: Value { + return .init(name: .unknown, levelType: .unknownLevel, level: 0, unit: "", values: []) + } } diff --git a/Sources/MetobsKit/Service.swift b/Sources/MetobsKit/Service.swift index 0361cc0..e0dbb32 100644 --- a/Sources/MetobsKit/Service.swift +++ b/Sources/MetobsKit/Service.swift @@ -22,20 +22,31 @@ // SOFTWARE. // -import Foundation - +/// A service that provides weather forecasts public class ForecastService: NSObject { + // MARK: Private properties + + /// The service endpoint to send requests to + private var serviceEndpoint: String { + return "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/LONGITUDE/lat/LATITUDE/data.json" + } + + /// A shared instance of forecast service + private static var sharedInstance: ForecastService? + + // MARK: Private functions + + /// Wrapps a network request as a `Promise` private func request(url: URL) -> Promise { let promise = Promise() URLSession.shared.dataTask(with: url) { data, response, error in - - // Make sure request did not result in error + // Make sure request did not result in an error if let error = error { return promise.reject(with: error) } - // Make sure acceptable response code present on response + // Make sure an acceptable response code is present on the response if let response = response as? HTTPURLResponse { if 400 ... 599 ~= response.statusCode { let error = URLError(.init(rawValue: response.statusCode)) @@ -43,12 +54,12 @@ public class ForecastService: NSObject { } } - // Make sure the request returned data + // Make sure the request returned data (and not an empty response) guard let data = data else { return promise.reject(with: URLError(.zeroByteResource)) } - // Attempt to decode data + // Attempt to decode JSON data from the response do { let value: Value = try data.decoded() return promise.resolve(with: value) @@ -60,11 +71,45 @@ public class ForecastService: NSObject { return promise } + /// Constructs a `URL` to use when requesting forecasts + /// - Parameters: + /// - latitude: The latitude to use for request + /// - longitude: The longitude to use for request + private func buildURL(latitude: Double, longitude: Double) -> Promise { + // Remove decimals exceeding six positions as it will cause a 404 response + let values: (lat: Double, lon: Double) = ( + lat: latitude.rounded(toPrecision: 6), + lon: longitude.rounded(toPrecision: 6) + ) + + let constructedURL = self.serviceEndpoint + .replacingOccurrences(of: "LONGITUDE", with: "\(values.lon)") + .replacingOccurrences(of: "LATITUDE", with: "\(values.lat)") + + // Construct the URL + guard let url = URL(string: constructedURL) else { + return Promise(value: nil, error: URLError(.badURL)) + } + + return Promise(value: url) + } + + // MARK: Public functions + + /// Get the weather forecast `Observation` for a specific set of coordinates + /// - Note: See https://www.smhi.se/data/utforskaren-oppna-data/meteorologisk-prognosmodell-pmp3g-2-8-km-upplosning-api + /// for information about limitations (such as coordinate limitations) + /// - Parameters: + /// - latitude: The coordinate latitude + /// - longitude: The coordinate longitude + /// - Returns: A `Promise` with the value `Observation` public func get(latitude: Double, longitude: Double) -> Promise { let observationPromise: Promise = Promise() - let urlPromise = buildURL(latitude: latitude, longitude: longitude) + // Request the forecast for the provided coordinates + let urlPromise = self.buildURL(latitude: latitude, longitude: longitude) + // Observe the request and forward the result urlPromise.observe { urlResult in switch urlResult { case let .value(value): @@ -84,4 +129,16 @@ public class ForecastService: NSObject { return observationPromise } + + // MARK: Shared instance + + /// A shared forecast-service instance + public static var shared: ForecastService { + guard let existingInstance = ForecastService.sharedInstance else { + let instance = ForecastService() + ForecastService.sharedInstance = instance + return instance + } + return existingInstance + } } diff --git a/Sources/MetobsKit/Utils.swift b/Sources/MetobsKit/Utils.swift index 12ca0a2..7b6a90f 100644 --- a/Sources/MetobsKit/Utils.swift +++ b/Sources/MetobsKit/Utils.swift @@ -22,8 +22,6 @@ // SOFTWARE. // -import Foundation - /// A `Future` wrapper for a `Value` open class Future { internal var result: Result? { @@ -110,3 +108,14 @@ public extension Data { return try JSONDecoder().decode(T.self, from: self) } } + +/// An extension to add the rounded method +extension Double { + /// Rounds the `Double` to a specified precision-level (number of decimals) + /// - Note: This method is present as the forecast service only accepts a maximum of six decimals + /// - Parameter precision: The precision-level to use + func rounded(toPrecision precision: Int) -> Double { + let multiplier: Double = pow(10, Double(precision)) + return (self * multiplier).rounded() / multiplier + } +}