swiftmapkitmkmapviewopenweathermapmkoverlay

Adding Overlay to MKMapView using OpenWeatherMap (Swift)


I have no idea where to begin.

Here's the documentation: https://openweathermap.org/api/weathermaps

Following that, and searching what I could online I tried the following, but it gives me a fatal error and never goes past that. (Note: I'm not sure what to put for the z, x, and y values either, so I left them, in addition to my API Key, blank here, but in my code I just put 1/1/1) My attempt, inserting temp_new to receive the temperature overlay:

    Service.shared.getInfoCompletionHandler(requestURL: "https://tile.openweathermap.org/map/temp_new/{z}/{x}/{y}.png?appid={myKey}") { data in
        
        if let data = data{
            var geoJson = [MKGeoJSONObject]()
            do{
                geoJson = try MKGeoJSONDecoder().decode(data)
            }
            catch{
                fatalError("Could not decode GeoJson")
            }
            
                var overlays = [MKOverlay]()
                for item in geoJson{
                    if let feature = item as? MKGeoJSONFeature{
                        for geo in feature.geometry{
                            if let polygon = geo as? MKPolygon{
                                overlays.append(polygon)
                            }
                        }
                    }
                }
                
                DispatchQueue.main.async {
                [unowned self] in
                //set to global variable
                self.overlays = overlays
                }
        }
    }

My thought process was to simply extract the overlays and then add it to the MKMapView like this:

mapView.addOverlays(self.overlays)

If its relevant, this is the completion handler I have in my Service.swift for making the API call:

//Get Info API
func getInfoCompletionHandler(requestURL: String, completion: @escaping (Data?)->Void){
    guard let url = URL(string: requestURL) else {return}
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        if error == nil {
            if let data = String(data: data!, encoding: .utf8), let response = 
            response{
                print(data)
                print(response)
            }
        } else{
            if let error = error {
                print(error)
            }
        }
            completion(data)
        
    }.resume()

Am I on the right track?

Any help is appreciated, thank you!

EDIT:

After playing around I noticed I can simply parse the data as imageData with the following code:

class ViewController: UIViewController {

//Properties
var imgData = Data()
let imageView = UIImageView()

override func viewDidLoad() {
    super.viewDidLoad()
    
    //imageView frame
    view.addSubview(imageView)
    imageView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
    imageView.center = view.center
    imageView.contentMode = .scaleAspectFit
    
    
    //image string
    let imgString = "https://tile.openweathermap.org/map/temp_new/0/0/0.png?appid={myKey}"
    //convert string to url object (needed to decode image data)
    let imgUrl = URL(string: imgString)
    //convert url to data
    self.imgData = try! Data(contentsOf: imgUrl!)
    //set to imageView
    self.imageView.image = UIImage(data: self.imgData)
}


}

Giving me this result:

enter image description here

So now the only question that remains is how do I add this imageView as an overlay on the mapView?


Solution

  • Okay so I finally got it thanks to this tutorial:

    https://www.raywenderlich.com/9956648-mapkit-tutorial-overlay-views#toc-anchor-003

    I'll try to do my best to explain everything here the best I can, I copied a bunch of the code from Ray's website so I don't understand everything 100%. That being said, the main meat of what needs to be done is to layout the coordinates for the overlay. This was done in a custom class. The idea here is to parse coordinate data which was written in a plist in a Dictionary. For my project this was easy because I simply had to set the maximum coordinates for the Earth ((90, 180), (90, -180), (-90, -180), (-90, 180)). The mid coordinate only worked when I set it as (100, 0), not sure why, but the full code for parsing the plist is below.

    class WorldMap {
      var boundary: [CLLocationCoordinate2D] = []
    
      var midCoordinate = CLLocationCoordinate2D()
      var overlayTopLeftCoordinate = CLLocationCoordinate2D()
      var overlayTopRightCoordinate = CLLocationCoordinate2D()
      var overlayBottomLeftCoordinate = CLLocationCoordinate2D()
      var overlayBottomRightCoordinate: CLLocationCoordinate2D {
    return CLLocationCoordinate2D(
      latitude: overlayBottomLeftCoordinate.latitude,
      longitude: overlayTopRightCoordinate.longitude)
      }
    
      var overlayBoundingMapRect: MKMapRect {
        let topLeft = MKMapPoint(overlayTopLeftCoordinate)
        let topRight = MKMapPoint(overlayTopRightCoordinate)
        let bottomLeft = MKMapPoint(overlayBottomLeftCoordinate)
    
    return MKMapRect(
      x: topLeft.x,
      y: topLeft.y,
      width: fabs(topLeft.x - topRight.x),
      height: fabs(topLeft.y - bottomLeft.y))
      }
    
    init(filename: String){
        guard
          let properties = WorldMap.plist(filename) as? [String: Any]
          else { return }
        
        midCoordinate = WorldMap.parseCoord(dict: properties, fieldName: "midCoord")
        overlayTopLeftCoordinate = WorldMap.parseCoord(
          dict: properties,
          fieldName: "overlayTopLeftCoord")
        overlayTopRightCoordinate = WorldMap.parseCoord(
          dict: properties,
          fieldName: "overlayTopRightCoord")
        overlayBottomLeftCoordinate = WorldMap.parseCoord(
          dict: properties,
          fieldName: "overlayBottomLeftCoord")
    }
    
    static func plist(_ plist: String) -> Any? {
      guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist"),
        let data = FileManager.default.contents(atPath: filePath) else { return nil }
    
      do {
        return try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
      } catch {
        return nil
      }
    }
    
    static func parseCoord(dict: [String: Any], fieldName: String) -> CLLocationCoordinate2D {
      if let coord = dict[fieldName] as? String {
        let point = NSCoder.cgPoint(for: coord)
        return CLLocationCoordinate2D(
          latitude: CLLocationDegrees(point.x),
          longitude: CLLocationDegrees(point.y))
      }
      return CLLocationCoordinate2D()
    }
    

    After that I had to make it conform to NSObject (not very clear on this concept)

    class MapOverlay: NSObject, MKOverlay{
    let coordinate: CLLocationCoordinate2D
    let boundingMapRect: MKMapRect
    
    init(worldMap: WorldMap) {
        boundingMapRect = worldMap.overlayBoundingMapRect
        coordinate = worldMap.midCoordinate
    }
    }
    

    Then created a class that conforms to MKOverlayRenderer to give it instructions on how to draw the overlay.

    class MapOverlayView: MKOverlayRenderer{
    
    let overlayImage: UIImage
    
    init(overlay: MKOverlay, overlayImage: UIImage){
        self.overlayImage = overlayImage
        super.init(overlay: overlay)
    }
    
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        
        
        guard let imageReference = overlayImage.cgImage else {return}
        let rect = self.rect(for: overlay.boundingMapRect)
        context.scaleBy(x: 1.0, y: -1.0)
        context.translateBy(x: 0.0, y: -rect.size.height)
        context.draw(imageReference, in: rect)
    }
    }
    

    Next I simply had to create a function which called the above classes:

    func addOverlay() {
        
        //its a good idea to remove any overlays first 
        //In my case I will add overlays for temperature and precipitation
        let overlays = mapView.overlays
        mapView.removeOverlays(overlays)
      
        //get overlay and add it to the mapView
        let worldMap = WorldMap(filename: "WorldCoordinates")
        let overlay = MapOverlay(worldMap: worldMap)
        mapView.addOverlay(overlay)
    }
    

    Once that was done I just had to fill out the MKOverlayRenderer delegate as follows:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        var overlayType: String
        
        //change API call depending on which overlay button has been pressed
        if self.tempOverlaySelected == true{
            overlayType = "temp_new"
        } else{
            overlayType = "precipitation_new"
        }
        
        //image string
        let imgString = "https://tile.openweathermap.org/map/\(overlayType)/0/0/0.png?{myKey}"
        //convert string to url object (needed to decode image data)
        let imgUrl = URL(string: imgString)
        //convert url to data and guard
        guard let imageData = try? Data(contentsOf: imgUrl!) else  {return  MKOverlayRenderer()}
        //set to imageView
        self.mapOverlay.image = UIImage(data: imageData)
    
        //return the map Overlay
        if overlay is MapOverlay {
            if let image = self.mapOverlay.image{
                return MapOverlayView(overlay: overlay, overlayImage: image)
            }
           
        }
        
        return MKOverlayRenderer()
    }
    

    I hope this helps anyone who might come across this problem in the future. If anyone can further help explain these concepts as it is new to me, feel free!