Jeshua Lacock
Jeshua Lacock

Reputation: 6658

Determine MapKit tiles for visibleMapRect

I saw simple code for calculating a grid of tile bounds needed for a given MKMapRect but I can't find it now. IIRC it was a function built-in to MapKit.

Given a MKMapRect like mapview.visibleMapRect and zoom level, how can I calculate an array of tile paths that will be used for the given rect?

import MapKit

class ViewController: UIViewController {

    @IBOutlet weak var mapview: MKMapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        mapview.delegate = self

        let location = CLLocationCoordinate2D(latitude: 44.0, longitude: -120.0)
        let span = MKCoordinateSpanMake(1.0, 1.0)
        let region = MKCoordinateRegion(center: location, span: span)
        mapview.setRegion(region, animated: true)

        let rect = mapview.visibleMapRect
    
        let requiredTiles = ?
    }
}

Upvotes: 0

Views: 119

Answers (2)

RL2000
RL2000

Reputation: 1011

I've been working with MapKit and MKTileOverlays for years, and I don't recall ever seeing a built-in way to calculate tiles in an area. But there are some libraries out there that can help with it. For example, gis-tools, and the MapTile struct.

You can initialize a MapTile with a coordinate and zoom level, so you can calculate the northwest and southeast corners of your MKMapRect, convert them to coordinates and then to MapTiles, and then use a simple nested for loop to go through the tile keys.

That would look something like this:

let nwCoord = Coordinate3D(mapRect.origin.coordinate)
let seCoord = Coordinate3D(MKMapPoint(x: mapRect.maxX, y: mapRect.maxY).coordinate)
let nwTile = MapTile(coordinate: nwCoord, atZoom: zoom)
let seTile = MapTile(coordinate: seCoord, atZoom: zoom)

var result = Set<MapTile>()
for x in nwTile.x...seTile.x {
    for y in nwTile.y...seTile.y {
        result.insert(MapTile(x: x, y: y, z: zoom))
    }
}

Maybe not the exact elegant solution you were looking for, but hopefully it's helpful.

Upvotes: 0

Gerd Castan
Gerd Castan

Reputation: 6849

The simplest answer for most use cases is that you don't do the calculation, and let MapKit do the work:

You define your own subclass of MKTileOverlay, set

class DigitransitTileOverlay: MKTileOverlay {
    
    override func url(forTilePath path: MKTileOverlayPath) -> URL {
        var urlComponents = URLComponents()
        urlComponents.scheme = "https"
        urlComponents.host = "cdn.digitransit.fi"
        // standard tile size for MKTileOverlay is 265
        //let startPath = "/map/v1/hsl-map-256" // the hsl-map part can be customized with language parameters
        // standard tile size for digitransit is 512
        let startPath = "/map/v1/hsl-map" // the hsl-map part can be customized with language parameters
        let size: String = ""  // @2x for retina, empty for normal
        urlComponents.path = startPath + "/\(path.z)/\(path.x)/\(path.y)\(size).png"
        
        let url: URL = urlComponents.url!

        return url
    }
}  

Then you set

digiTransitTileOverlay.canReplaceMapContent = false
digiTransitTileOverlay.tileSize = CGSize(width: 512, height: 512)
mapView.addOverlay(digiTransitTileOverlay, level: .aboveRoads)

tileSize is not necessary, but appears to make some servers faster, as popular web libraries prefer 512 x 512 so this tile size is cached my many servers.

and define a renderer:

public override func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer? {
    if let tileOverlay = overlay as? DigitransitTileOverlay {
        let tileOverlayRenderer = MKTileOverlayRenderer(tileOverlay: tileOverlay)
        return tileOverlayRenderer
    }
    return nil
}

As you see, url(forTilePath is called by MapKit for each necessary tile in the region you have set. You get parameters for each tile in MKTileOverlayPath

Upvotes: -1

Related Questions