Limit amount of tile requests based on quota

1048
5
Jump to solution
05-05-2021 09:47 AM
YuriReis
New Contributor II

I'm attempting to restrict the number of fired requests that we make to our remote tile provider.

Currently, we're using a AGSArcGISTiledLayer, inited with the URL of the provider. This works fine and I can see all the tiles appearing in the layer. I noticed that after the initial configuration, the SDK requests tiles automatically, based on the map's view port. How should I go about restricting these tile requests for this specific layer? 

Here is the code we have so far:

let map = AGSMap(basemapType: .navigationVector, 
                 latitude: latitude, 
                 longitude: longitude, 
                 levelOfDetail: 18)
mapView.map = map
let tiledLayer = AGSArcGISTiledLayer(url: URL(string: urlPath)!)
tiledLayer.minScale = 8000
mapView.map?.operationalLayers.add(tiledLayer)

I tried including a tileRequestHandler to maybe cancel the tile request before it begins, but it isn't called at all.

Using ArcGIS-Runtime-SDK-iOS (100.7.1).

Any ideas? Thank you!

0 Kudos
1 Solution

Accepted Solutions
Nicholas-Furness
Esri Regular Contributor

Out of interest, what do you want the behavior to be when you hit your limiting criteria?

But you should be creating an AGSImageTiledLayer with an AGSTileInfo and a full extent. See AGSImageTiledLayer(CustomImageTiledLayer). Since you're working with an ArcGIS service for the layer, you can just lift that directly from the service. This code works well (you should probably modify the tileCount code - I've put no effort into making it thread safe and am relying on the tileRequestHandler being called on the main queue to force sequential access to tileCount) :

 

 

import UIKit
import ArcGIS

class ViewController: UIViewController {

    @IBOutlet weak var mapView: AGSMapView!
    
    let layerURL = URL(string: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer")!
    
    var tileCount = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let metadataLayer = AGSArcGISTiledLayer(url: layerURL)
        
        metadataLayer.load { [weak self] (error) in
            guard let self = self else { return }
            
            if let error = error {
                print("Error loading layer: \(error.localizedDescription)")
                return
            }

            if let tileInfo = metadataLayer.tileInfo,
               let fullExtent = metadataLayer.fullExtent {
                
                let customLayer = AGSImageTiledLayer(tileInfo: tileInfo, fullExtent: fullExtent)

                // This highlights how tiles are not retrieved
                customLayer.noDataTileBehavior = .blank
                
                customLayer.tileRequestHandler = { [weak self, layerURL = self.layerURL] tileKey in
                    guard let self = self else { return }
                    
                    guard self.tileCount < 4 else {
                        // Note, the first tile (global tile at zoom 0) might not be displayed as it
                        // is superceded by tiles at zoom level 1.
                        customLayer.respondWithNoDataTile(for: tileKey)
                        return
                    }

                    self.tileCount += 1
                    
                    let tileUrl = layerURL
                        .appendingPathComponent("tile")
                        .appendingPathComponent("\(tileKey.level)")
                        .appendingPathComponent("\(tileKey.row)")
                        .appendingPathComponent("\(tileKey.column)")
                    
//                    print("\(self.tileCount) \(tileUrl.path)")
                    
                    // Piggy back on ArcGIS Authentication with AGSRequestOperation
                    let tileOp = AGSRequestOperation(url: tileUrl)
                    tileOp.registerListener(customLayer) { [weak customLayer] (tileData, error) in
                        guard let customLayer = customLayer else { return }
                        guard let tileData = tileData as? Data else {
                            customLayer.respondWithNoDataTile(for: tileKey)
                            return
                        }
                        customLayer.respond(with: tileKey, data: tileData, error: error)
                    }
                    AGSOperationQueue.shared().addOperation(tileOp)
                }
                
                let basemap = AGSBasemap(baseLayer: customLayer)
                self.mapView.map = AGSMap(basemap: basemap)
            }
        }
    }
}

 

 

View solution in original post

5 Replies
Nicholas-Furness
Esri Regular Contributor

Out of interest, what do you want the behavior to be when you hit your limiting criteria?

But you should be creating an AGSImageTiledLayer with an AGSTileInfo and a full extent. See AGSImageTiledLayer(CustomImageTiledLayer). Since you're working with an ArcGIS service for the layer, you can just lift that directly from the service. This code works well (you should probably modify the tileCount code - I've put no effort into making it thread safe and am relying on the tileRequestHandler being called on the main queue to force sequential access to tileCount) :

 

 

import UIKit
import ArcGIS

class ViewController: UIViewController {

    @IBOutlet weak var mapView: AGSMapView!
    
    let layerURL = URL(string: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer")!
    
    var tileCount = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let metadataLayer = AGSArcGISTiledLayer(url: layerURL)
        
        metadataLayer.load { [weak self] (error) in
            guard let self = self else { return }
            
            if let error = error {
                print("Error loading layer: \(error.localizedDescription)")
                return
            }

            if let tileInfo = metadataLayer.tileInfo,
               let fullExtent = metadataLayer.fullExtent {
                
                let customLayer = AGSImageTiledLayer(tileInfo: tileInfo, fullExtent: fullExtent)

                // This highlights how tiles are not retrieved
                customLayer.noDataTileBehavior = .blank
                
                customLayer.tileRequestHandler = { [weak self, layerURL = self.layerURL] tileKey in
                    guard let self = self else { return }
                    
                    guard self.tileCount < 4 else {
                        // Note, the first tile (global tile at zoom 0) might not be displayed as it
                        // is superceded by tiles at zoom level 1.
                        customLayer.respondWithNoDataTile(for: tileKey)
                        return
                    }

                    self.tileCount += 1
                    
                    let tileUrl = layerURL
                        .appendingPathComponent("tile")
                        .appendingPathComponent("\(tileKey.level)")
                        .appendingPathComponent("\(tileKey.row)")
                        .appendingPathComponent("\(tileKey.column)")
                    
//                    print("\(self.tileCount) \(tileUrl.path)")
                    
                    // Piggy back on ArcGIS Authentication with AGSRequestOperation
                    let tileOp = AGSRequestOperation(url: tileUrl)
                    tileOp.registerListener(customLayer) { [weak customLayer] (tileData, error) in
                        guard let customLayer = customLayer else { return }
                        guard let tileData = tileData as? Data else {
                            customLayer.respondWithNoDataTile(for: tileKey)
                            return
                        }
                        customLayer.respond(with: tileKey, data: tileData, error: error)
                    }
                    AGSOperationQueue.shared().addOperation(tileOp)
                }
                
                let basemap = AGSBasemap(baseLayer: customLayer)
                self.mapView.map = AGSMap(basemap: basemap)
            }
        }
    }
}

 

 

YuriReis
New Contributor II

Hey @Nicholas-Furness !

When the limit is reached, we'll just not show anything for the subsequent tile requests. I'm assuming the .blank no tile behavior that you added in your code above does just that?

Thank you for the quick and detailed response!

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Correct. .blank will do that. By default any lower resolution tile that is available will be upscaled.

YuriReis
New Contributor II

@Nicholas-Furness By the way, is there a way to use the above code in conjunction with offline maps? I tried it using a AGSOfflineMapTask, but it takes a long time to download and completes with an error: "Layer type unsupported".

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Hmm. Do you mean the AGSGenerateOfflineMapResult you get back has layerErrors, and one of those errors is "Layer type unsupported"? If the entire job is passing an error to the completion block, make sure that AGSGenerateOfflineMapParameter.continueOnError is true when you create the download job.

The Offline Map Task works against a web map. This custom layer cannot be added to a web map, so I don't think it could be the cause there. You can see the types of layers that are supported listed here. Does your web map have some other type of layer, like a dynamic map service layer? Also, be aware that the service behind the layer must be configured to support offline workflows (although I would expect a different error message in that case).

Once you've got to the bottom of that, there are a couple of possible approaches I can think of.

  1. Save the web map with the original layer included, but leave the visibility off. When it comes time to generate the offline map, the original layer should be included, and you can turn the visibility on once you get the offline map.
  2. Configure the onlineOnlyServicesOption to download the offline map, but including references to layers that can't be taken offline.

Unfortunately, packing up image tile services can take a long time as the server must collect all the tiles and build a tile package from them.

Another option, if the service owner's data license allows it, is to modify the code above to write tiles to the local device. When asked for a tile, if the tile is already present, just read it from the device, otherwise go and retrieve it from the service (writing it to disk when you get it and before you hand it back to Runtime in the respond(with:data:error:) call). You would need to write cache control logic to delete tiles (you don't want to just fill up the device with downloaded tiles!). Note: this is against our terms for ArcGIS basemap services, but it sounds like you are using someone's custom tile service, so they might allow this.

0 Kudos