'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 1:[1]

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!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jmsapps