Reputation: 97
I wanted to modify apple's earthquakes project to display a map together with the location magnitude and time. In the json file I can see the coordinates but for the life of me I cannot read them and use them as latitude and longitude for the map. I succeeded to display the map by using the address (title) but the format changes and there are too many possibilities to account for. The earthquake project can be downloaded at https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed I post the Quake.swift file below so you may have an idea of what I tried. I added a coordinates characteristic to their magnitude, place and time first as an array and then as a string but I always fail to read it and use it to display the map as latitude and longitude.
Thanks in advance for your help.
The json file is long so I post a few lines here to give you an idea of the format:
{"type":"FeatureCollection","metadata":{"generated":1648109722000,"url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson","title":"USGS All Earthquakes, Past Month","status":200,"api":"1.10.3","count":9406},"features":[{"type":"Feature","properties":{"mag":4.5,"place":"south of the Fiji Islands","time":1648106910967,"updated":1648108178040,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/us7000gwsr","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000gwsr.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"reviewed","tsunami":0,"sig":312,"net":"us","code":"7000gwsr","ids":",us7000gwsr,","sources":",us,","types":",origin,phase-data,","nst":null,"dmin":5.374,"rms":1.03,"gap":102,"magType":"mb","type":"earthquake","title":"M 4.5 - south of the Fiji Islands"},"geometry":{"type":"Point","coordinates":[179.1712,-24.5374,534.35]},"id":"us7000gwsr"},
{"type":"Feature","properties":{"mag":1.95000005,"place":"2 km NE of Pāhala, Hawaii","time":1648106708550,"updated":1648106923140,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/hv72960677","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/hv72960677.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":59,"net":"hv","code":"72960677","ids":",hv72960677,","sources":",hv,","types":",origin,phase-data,","nst":33,"dmin":null,"rms":0.109999999,"gap":136,"magType":"md","type":"earthquake","title":"M 2.0 - 2 km NE of Pāhala, Hawaii"},"geometry":{"type":"Point","coordinates":[-155.463333129883,19.2151660919189,34.9500007629395]},"id":"hv72960677"},
{"type":"Feature","properties":{"mag":1.75,"place":"4km SE of Calabasas, CA","time":1648106545420,"updated":1648109717670,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/ci39976447","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ci39976447.geojson","felt":7,"cdi":3.1,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":49,"net":"ci","code":"39976447","ids":",ci39976447,","sources":",ci,","types":",dyfi,nearby-cities,origin,phase-data,scitech-link,","nst":33,"dmin":0.04554,"rms":0.27,"gap":56,"magType":"ml","type":"earthquake","title":"M 1.8 - 4km SE of Calabasas, CA"},"geometry":{"type":"Point","coordinates":[-118.61,34.1285,2.92]},"id":"ci39976447"},
The Quake.swift file:
import CoreData
import SwiftUI
import OSLog
// MARK: - Core Data
/// Managed object subclass for the Quake entity.
class Quake: NSManagedObject, Identifiable {
// The characteristics of a quake.
@NSManaged var magnitude: Float
@NSManaged var place: String
@NSManaged var time: Date
@NSManaged var coordinates: String
// A unique identifier used to avoid duplicates in the persistent store.
// Constrain the Quake entity on this attribute in the data model editor.
@NSManaged var code: String
/// Updates a Quake instance with the values from a QuakeProperties.
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newCoordinates = dictionary["coordinates"] as? String
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
coordinates = newCoordinates
}
}
// MARK: - SwiftUI
extension Quake {
/// The color which corresponds with the quake's magnitude.
var color: Color {
switch magnitude {
case 0..<1:
return .green
case 1..<2:
return .yellow
case 2..<3:
return .orange
case 3..<5:
return .red
case 5..<Float.greatestFiniteMagnitude:
return .init(red: 0.8, green: 0.2, blue: 0.7)
default:
return .gray
}
}
/// An earthquake for use with canvas previews.
static var preview: Quake {
let quakes = Quake.makePreviews(count: 1)
return quakes[0]
}
@discardableResult
static func makePreviews(count: Int) -> [Quake] {
var quakes = [Quake]()
let viewContext = QuakesProvider.preview.container.viewContext
for index in 0..<count {
let quake = Quake(context: viewContext)
quake.code = UUID().uuidString
quake.time = Date().addingTimeInterval(Double(index) * -300)
quake.magnitude = .random(in: -1.1...10.0)
quake.place = "15km SSW of Cupertino, CA"
quake.coordinates = "-117.7153333,35.8655,7.59"
quakes.append(quake)
}
return quakes
}
}
// MARK: - Codable
/// creating or updating Quake instances.
struct GeoJSON: Decodable {
private enum RootCodingKeys: String, CodingKey {
case features
}
private enum FeatureCodingKeys: String, CodingKey {
case properties
}
private(set) var quakePropertiesList = [QuakeProperties]()
init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self)
var featuresContainer = try rootContainer.nestedUnkeyedContainer(forKey: .features)
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if let properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties) {
quakePropertiesList.append(properties)
}
}
}
}
/// A struct encapsulating the properties of a Quake.
struct QuakeProperties: Decodable {
// MARK: Codable
private enum CodingKeys: String, CodingKey {
case magnitude = "mag"
case place
case time
case code
case coordinates
}
let magnitude: Float // 1.9
let place: String // "21km ENE of Honaunau-Napoopoo, Hawaii"
let time: Double // 1539187727610
let code: String // "70643082"
let coordinates: String // [-117.7153333,35.8655,7.59]
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let rawMagnitude = try? values.decode(Float.self, forKey: .magnitude)
let rawPlace = try? values.decode(String.self, forKey: .place)
let rawTime = try? values.decode(Double.self, forKey: .time)
let rawCode = try? values.decode(String.self, forKey: .code)
let rawCoordinates = try? values.decode(String.self, forKey: .coordinates)
// Ignore earthquakes with missing data.
guard let magntiude = rawMagnitude,
let place = rawPlace,
let time = rawTime,
let code = rawCode,
let coordinates = rawCoordinates
else {
let values = "code = \(rawCode?.description ?? "nil"), "
+ "mag = \(rawMagnitude?.description ?? "nil"), "
+ "place = \(rawPlace?.description ?? "nil"), "
+ "time = \(rawTime?.description ?? "nil"), "
+ "coordinates = \(rawCoordinates?.description ?? "nil")"
let logger = Logger(subsystem: "com.example.apple-samplecode.Earthquakes", category: "parsing")
logger.debug("Ignored: \(values)")
throw QuakeError.missingData
}
self.magnitude = magntiude
self.place = place
self.time = time
self.code = code
self.coordinates = coordinates
}
// The keys must have the same name as the attributes of the Quake entity.
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"coordinates": coordinates
]
}
}
Upvotes: 1
Views: 1988
Reputation: 285039
The coordinates are not on the same level as properties
, they are in a sibling geometry
. The basic pattern is
{
"features": [
{
"properties": {
"mag":1.9,
"place":"21km ENE of Honaunau-Napoopoo, Hawaii",
"time":1539187727610,"updated":1539187924350,
"code":"70643082"
},
"geometry" : {
"coordinates": [-122.8096695,38.8364983,1.96]
}
}
]
}
You have to decode the coordinates in GeoJSON
by adding geometry
to FeatureCodingKeys
. And you have to extend the Core Data model to preserve the coordinates.
In the Core Data model add two properties
longitude - Double - non-optional, use scalar type
latitude - Double - non-optional, use scalar type
In Quake.swift
import CoreLocation
In the class Quake
add
@NSManaged var latitude: CLLocationDegrees
@NSManaged var longitude: CLLocationDegrees
and replace update(from
with
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newLatitude = dictionary["latitude"] as? CLLocationDegrees,
let newLongitude = dictionary["longitude"] as? CLLocationDegrees
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
latitude = newLatitude
longitude = newLongitude
}
In the Quake
extension add
var coordinate : CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
In GeoJSON extend FeatureCodingKeys
private enum FeatureCodingKeys: String, CodingKey {
case properties, geometry
}
and replace the while
loop with
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if var properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties),
let geometry = try? propertiesContainer.decode(QuakeGeometry.self, forKey: .geometry) {
let coordinates = geometry.coordinates
properties.longitude = coordinates[0]
properties.latitude = coordinates[1]
quakePropertiesList.append(properties)
}
}
Add the struct
struct QuakeGeometry: Decodable {
let coordinates : [Double]
}
In QuakeProperties
add
var latitude : CLLocationDegrees = 0.0
var longitude : CLLocationDegrees = 0.0
and replace dictionaryValue
with
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"latitude": latitude,
"longitude": longitude
]
}
Finally in DetailView.swift
import MapKit
and replace QuakeDetail
with
struct QuakeDetail: View {
var quake: Quake
@State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.3, longitudeDelta: 0.3)))
}
var body: some View {
VStack {
QuakeMagnitude(quake: quake)
Text(quake.place)
.font(.title3)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) - \(quake.longitude)")
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
}
}
}
}
Upvotes: 1