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!
Solved! Go to Solution.
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)
}
}
}
}
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)
}
}
}
}
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!
Correct. .blank will do that. By default any lower resolution tile that is available will be upscaled.
@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".
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.
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.