[SOLVED] Getting latitude and longitude from a json file to create a map

Issue

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
        ]
    }
}

Solution

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)
                }
            }
        }
    }
    

Answered By – vadian

Answer Checked By – Candace Johnson (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.