This post is part of the Singapore Breathe Development Diary Series.
Housekeeping
Before we get into this week’s update, some repository updates:
- A
README
has been added - Continuous Integration (via Bitrise), Code Coverage (via Codecov), and Code
NitpickingQuality (via Codacy) have been implemented - The Notes folder contains some additional information on Localisation and the component parts of the air quality measurement
PSI and PM2.5 Data
There are two APIs that I’m using in Singapore Breathe:
- PSI: This is updated hourly and contains PM10, PM2.5 (24-hour and sub-index), O3, CO, NO2, and SO2 values, with the exception of the PM2.5 hourly readings.
- PM2.5: This contains the hourly PM2.5 readings only.
The only difference in structure between the two API responses is that PSI API returns readings for six regions (North, East, South, West, Central, and National), while the PM2.5 API returns results for five (it excludes National).
{
// PSI
"items": [
"timestamp": "2020-12-15T08:00:00+08:00",
"update_timestamp": "2020-12-15T08:03:50+08:00",
"readings": {
"o3_sub_index": {
"west": 4,
"national": 4,
"east": 2,
"central": 2,
"south": 3,
"north": 4
},
"pm10_twenty_four_hourly": {
"west": 20,
"national": 25,
"east": 25,
"central": 21,
"south": 24,
"north": 19
},
...
]
}
{
// PM2.5
"items": [
"timestamp": "2020-12-15T08:00:00+08:00",
"update_timestamp": "2020-12-15T08:03:50+08:00",
"readings": {
"pm25_one_hourly": {
"west": 5,
"east": 3,
"central": 1,
"south": 5,
"north": 2
}
]
}
Given the similarities, both PSI and PM2.5 responses can be modelled using the same struct
.
A Combined RegionalAQM
The way I will display the readings to users is via annotations on a Map
. For example, there will be an annotation for the West region which, when tapped, will show the air quality measurements for that region. As such, I’m not using the data as-is from the API. Instead, for each region, I am creating a RegionalAQM
1 NSManagedObject
which contains the distinct readings for that region and timestamp. In order to ensure duplicate readings are not saved to the database, the RegionalAQM
has an id
property that is a concatenation of region
and timestamp
, and the NSManagedObjectContext
has a merge policy of mergeByPropertyObjectTrump
.
An example RegionalAQM
looks like this:
let aqm = RegionalAQM(context: PersistenceController.shared.container.viewContext)
let label = psi.regionMetadata.filter { $0.name == region }.first!
aqm.latitude = label.labelLocation!.latitude
aqm.longitude = label.labelLocation!.longitude
aqm.co_eight_hour_max = item.readings!["co_eight_hour_max"]!.north ?? 0.0
aqm.co_sub_index = item.readings!["co_sub_index"]!.north ?? 0.0
aqm.no2_one_hour_max = item.readings!["no2_one_hour_max"]!.north ?? 0.0
aqm.o3_eight_hour_max = item.readings!["o3_eight_hour_max"]!.north ?? 0.0
aqm.o3_sub_index = item.readings!["o3_sub_index"]!.north ?? 0.0
aqm.pm10_sub_index = item.readings!["pm10_sub_index"]!.north ?? 0.0
aqm.pm10_twenty_four_hourly = item.readings!["pm10_twenty_four_hourly"]!.north ?? 0.0
aqm.pm25_one_hourly = pm25.items.first!.readings!["pm25_one_hourly"]?.north ?? 0.0
aqm.pm25_sub_index = item.readings!["pm25_sub_index"]!.north ?? 0.0
aqm.pm25_twenty_four_hourly = item.readings!["pm25_twenty_four_hourly"]!.north ?? 0.0
aqm.psi_twenty_four_hourly = item.readings!["psi_twenty_four_hourly"]!.north ?? 0.0
aqm.so2_sub_index = item.readings!["so2_sub_index"]!.north ?? 0.0
aqm.so2_twenty_four_hourly = item.readings!["so2_twenty_four_hourly"]!.north ?? 0.0
Once the latest readings are converted to RegionalAQM
s, they are saved, and the Map view’s @FetchRequest
updates the map with the latest data via annotations. (I’ve yet to decide what I’ll do with the historical readings or what the retention policy will be.)
Annotations
Viewing additional data related to the PSI reading can be accomplished by tapping on the annotations. When tapped a modal sheet is presented with all six data points and explanations of each reading. You can tap the (?) button2 to hide the explanations3.
Up Next
I am going to read-up a little bit more on using Core Data with SwiftUI make any necessary changes to the PersistenceController
code. I also intend to add a Dock style menu to the bottom of the map with a refresh button, settings, and latest readings timestamp.