Files
MapLibreParticleLab/platform/darwin/test/MLNDocumentationExampleTests.swift

612 lines
25 KiB
Swift

import MapLibre
import XCTest
/**
Test cases that ensure the inline examples in the project documentation
compile.
To add an example:
1. Add a test case named in the form testMLNClass or testMLNClass$method.
2. Wrap the code you'd like to appear in the documentation within the
following comment blocks:
```
//#-example-code
...
//#-end-example-code
```
3. Insert an empty Swift code block inside the header file where you'd like the
example code to be inserted.
4. Run `make darwin-update-examples` to extract example code from the test
method below and insert it into the header.
*/
class MLNDocumentationExampleTests: XCTestCase, MLNMapViewDelegate {
var mapView: MLNMapView!
var styleLoadingExpectation: XCTestExpectation!
static let styleURL = Bundle(for: MLNDocumentationExampleTests.self).url(forResource: "one-liner", withExtension: "json")!
// Mock MLNOfflineStorage singleton so that it doesn't start long-running tasks that could interfere with other tests.
fileprivate class MLNOfflineStorageMock {
static let shared = MLNOfflineStorageMock()
func addPack(for: MLNOfflineRegion, withContext: Data, completionHandler: MLNOfflinePackAdditionCompletionHandler? = nil) {
XCTAssert(MLNOfflineStorage.shared.responds(to: #selector(MLNOfflineStorage.shared.addPack(for:withContext:completionHandler:))))
if let completionHandler {
completionHandler(nil, NSError(domain: "MLNDocumentationExampleError", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(#function) is mocked and not functional."]))
}
}
}
override func setUp() {
super.setUp()
mapView = MLNMapView(frame: CGRect(x: 0, y: 0, width: 256, height: 256), styleURL: MLNDocumentationExampleTests.styleURL)
mapView.delegate = self
styleLoadingExpectation = expectation(description: "Map view should finish loading style")
waitForExpectations(timeout: 10, handler: nil)
}
override func tearDown() {
mapView = nil
styleLoadingExpectation = nil
super.tearDown()
}
func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) {
styleLoadingExpectation.fulfill()
}
func testMLNLight() {
// #-example-code
let light = MLNLight()
let position = MLNSphericalPosition(radial: 5, azimuthal: 180, polar: 80)
light.position = NSExpression(forConstantValue: NSValue(mlnSphericalPosition: position))
light.anchor = NSExpression(forConstantValue: "map")
mapView.style?.light = light
// #-end-example-code
XCTAssertNotNil(mapView.style?.light)
}
func testMLNTilePyramidOfflineRegion() {
class MLNStyle {
static var lightStyleURL: URL {
MLNDocumentationExampleTests.styleURL
}
}
typealias MLNOfflineStorage = MLNOfflineStorageMock
// #-example-code
let northeast = CLLocationCoordinate2D(latitude: 40.989329, longitude: -102.062592)
let southwest = CLLocationCoordinate2D(latitude: 36.986207, longitude: -109.049896)
let bbox = MLNCoordinateBounds(sw: southwest, ne: northeast)
let region = MLNTilePyramidOfflineRegion(styleURL: MLNStyle.lightStyleURL, bounds: bbox, fromZoomLevel: 11, toZoomLevel: 14)
let context = "Tile Pyramid Region".data(using: .utf8)
MLNOfflineStorage.shared.addPack(for: region, withContext: context!)
// #-end-example-code
XCTAssertNotNil(region)
}
func testMLNShapeOfflineRegion() {
class MLNStyle {
static var lightStyleURL: URL {
MLNDocumentationExampleTests.styleURL
}
}
typealias MLNOfflineStorage = MLNOfflineStorageMock
// #-example-code
var coordinates = [
CLLocationCoordinate2D(latitude: 45.522585, longitude: -122.685699),
CLLocationCoordinate2D(latitude: 45.534611, longitude: -122.708873),
CLLocationCoordinate2D(latitude: 45.530883, longitude: -122.678833),
]
let triangle = MLNPolygon(coordinates: &coordinates, count: UInt(coordinates.count))
let region = MLNShapeOfflineRegion(styleURL: MLNStyle.lightStyleURL, shape: triangle, fromZoomLevel: 11, toZoomLevel: 14)
let context = "Triangle Region".data(using: .utf8)
MLNOfflineStorage.shared.addPack(for: region, withContext: context!)
// #-end-example-code
XCTAssertNotNil(region)
}
func testMLNOfflinePack() {
typealias MLNOfflineStorage = MLNOfflineStorageMock
let northeast = CLLocationCoordinate2D(latitude: 40.989329, longitude: -102.062592)
let southwest = CLLocationCoordinate2D(latitude: 36.986207, longitude: -109.049896)
let bbox = MLNCoordinateBounds(sw: southwest, ne: northeast)
let region = MLNTilePyramidOfflineRegion(styleURL: MLNDocumentationExampleTests.styleURL, bounds: bbox, fromZoomLevel: 11, toZoomLevel: 14)
let context = "Tile Pyramid Region".data(using: .utf8)!
// #-example-code
MLNOfflineStorage.shared.addPack(for: region, withContext: context) { pack, error in
guard let pack else {
// If adding the pack fails, log an error to console.
print("Error:", error?.localizedDescription ?? "unknown error adding pack at \(#file)(\(#line)) in \(#function)")
return
}
// Start an MLNOfflinePack download
pack.resume()
}
// #-end-example-code
}
func testMLNShape$shapeWithData_encoding_error_() {
let mainBundle = Bundle(for: MLNDocumentationExampleTests.self)
// #-example-code
let url = mainBundle.url(forResource: "amsterdam", withExtension: "geojson")!
let data = try! Data(contentsOf: url)
let feature = try! MLNShape(data: data, encoding: String.Encoding.utf8.rawValue) as! MLNShapeCollectionFeature
// #-end-example-code
XCTAssertNotNil(feature.shapes.first as? MLNPolygonFeature)
}
func testMLNShapeSource() {
// #-example-code
var coordinates: [CLLocationCoordinate2D] = [
CLLocationCoordinate2D(latitude: 37.77, longitude: -122.42),
CLLocationCoordinate2D(latitude: 38.91, longitude: -77.04),
]
let polyline = MLNPolylineFeature(coordinates: &coordinates, count: UInt(coordinates.count))
let source = MLNShapeSource(identifier: "lines", features: [polyline], options: nil)
mapView.style?.addSource(source)
// #-end-example-code
XCTAssertNotNil(mapView.style?.source(withIdentifier: "lines"))
}
func testMLNRasterTileSource() {
// #-example-code
let source = MLNRasterTileSource(identifier: "clouds", tileURLTemplates: ["https://example.com/raster-tiles/{z}/{x}/{y}.png"], options: [
.minimumZoomLevel: 9,
.maximumZoomLevel: 16,
.tileSize: 512,
.attributionInfos: [
MLNAttributionInfo(title: NSAttributedString(string: "© Mapbox"), url: URL(string: "https://mapbox.com")),
],
])
mapView.style?.addSource(source)
// #-end-example-code
XCTAssertNotNil(mapView.style?.source(withIdentifier: "clouds"))
}
func testMLNRasterDEMSource() {
// We want to use mapbox.terrain-rgb in the example, but using a mapbox:
// URL requires setting an access token. So this identically named
// subclass of MLNRasterDEMSource swaps in a nonexistent URL.
class MLNRasterDEMSource: MapLibre.MLNRasterDEMSource {
override init(identifier: String, configurationURL: URL, tileSize: CGFloat = 256) {
let bogusURL = URL(string: "https://example.com/raster-rgb.json")!
super.init(identifier: identifier, configurationURL: bogusURL, tileSize: tileSize)
}
}
// #-example-code
let terrainRGBURL = URL(string: "maptiler://sources/terrain-rgb")!
let source = MLNRasterDEMSource(identifier: "hills", configurationURL: terrainRGBURL)
mapView.style?.addSource(source)
// #-end-example-code
XCTAssertNotNil(mapView.style?.source(withIdentifier: "hills"))
}
func testMLNVectorTileSource() {
// #-example-code
let source = MLNVectorTileSource(identifier: "pois", tileURLTemplates: ["https://example.com/vector-tiles/{z}/{x}/{y}.mvt"], options: [
.minimumZoomLevel: 9,
.maximumZoomLevel: 16,
.attributionInfos: [
MLNAttributionInfo(title: NSAttributedString(string: "© Mapbox"), url: URL(string: "https://mapbox.com")),
],
])
mapView.style?.addSource(source)
// #-end-example-code
XCTAssertNotNil(mapView.style?.source(withIdentifier: "pois"))
}
func testMLNPolyline() {
// #-example-code
let coordinates = [
CLLocationCoordinate2D(latitude: 35.68476, longitude: -220.24257),
CLLocationCoordinate2D(latitude: 37.78428, longitude: -122.41310),
]
let polyline = MLNPolyline(coordinates: coordinates, count: UInt(coordinates.count))
// #-end-example-code
XCTAssertNotNil(polyline)
}
func testMLNImageSource() {
// #-example-code
let coordinates = MLNCoordinateQuad(
topLeft: CLLocationCoordinate2D(latitude: 46.437, longitude: -80.425),
bottomLeft: CLLocationCoordinate2D(latitude: 37.936, longitude: -80.425),
bottomRight: CLLocationCoordinate2D(latitude: 37.936, longitude: -71.516),
topRight: CLLocationCoordinate2D(latitude: 46.437, longitude: -71.516)
)
let source = MLNImageSource(identifier: "radar", coordinateQuad: coordinates, url: URL(string: "https://maplibre.org/maplibre-gl-js-docs/assets/radar.gif")!)
mapView.style?.addSource(source)
// #-end-example-code
XCTAssertNotNil(mapView.style?.source(withIdentifier: "radar"))
}
func testMLNCircleStyleLayer() {
let population = MLNVectorTileSource(identifier: "population", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(population)
// #-example-code
let layer = MLNCircleStyleLayer(identifier: "circles", source: population)
layer.sourceLayerIdentifier = "population"
#if os(macOS)
layer.circleColor = NSExpression(forConstantValue: NSColor.green)
#else
layer.circleColor = NSExpression(forConstantValue: UIColor.green)
#endif
let stops = NSExpression(forConstantValue: [12: 2,
22: 180])
layer.circleRadius = NSExpression(forMLNInterpolating: .zoomLevelVariable,
curveType: .exponential,
parameters: NSExpression(forConstantValue: 1.75),
stops: stops)
layer.circleOpacity = NSExpression(forConstantValue: 0.7)
layer.predicate = NSPredicate(format: "%K == %@", "marital-status", "married")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "circles"))
}
func testMLNLineStyleLayer() {
let trails = MLNVectorTileSource(identifier: "trails", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(trails)
// #-example-code
let layer = MLNLineStyleLayer(identifier: "trails-path", source: trails)
layer.sourceLayerIdentifier = "trails"
let stops = NSExpression(forConstantValue: [14: 2,
18: 20])
layer.lineWidth = NSExpression(forMLNInterpolating: .zoomLevelVariable,
curveType: .exponential,
parameters: NSExpression(forConstantValue: 1.5),
stops: stops)
#if os(macOS)
layer.lineColor = NSExpression(forConstantValue: NSColor.brown)
#else
layer.lineColor = NSExpression(forConstantValue: UIColor.brown)
#endif
layer.lineCap = NSExpression(forConstantValue: "round")
layer.predicate = NSPredicate(format: "%K == %@", "trail-type", "mountain-biking")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "trails-path"))
}
func testMLNFillStyleLayer() {
let parks = MLNVectorTileSource(identifier: "parks", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(parks)
// #-example-code
let layer = MLNFillStyleLayer(identifier: "parks", source: parks)
layer.sourceLayerIdentifier = "parks"
#if os(macOS)
layer.fillColor = NSExpression(forConstantValue: NSColor.green)
#else
layer.fillColor = NSExpression(forConstantValue: UIColor.green)
#endif
layer.predicate = NSPredicate(format: "type == %@", "national-park")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "parks"))
}
func testMLNFillExtrusionStyleLayer() {
let buildings = MLNVectorTileSource(identifier: "buildings", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(buildings)
// #-example-code
let layer = MLNFillExtrusionStyleLayer(identifier: "buildings", source: buildings)
layer.sourceLayerIdentifier = "building"
layer.fillExtrusionHeight = NSExpression(forKeyPath: "height")
layer.fillExtrusionBase = NSExpression(forKeyPath: "min_height")
layer.predicate = NSPredicate(format: "extrude == 'true'")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "buildings"))
}
func testMLNHeatmapStyleLayer() {
let earthquakes = MLNShapeSource(identifier: "earthquakes", url: URL(string: "https://example.com/earthquakes.json")!, options: [:])
mapView.style?.addSource(earthquakes)
// #-example-code
let layer = MLNHeatmapStyleLayer(identifier: "earthquake-heat", source: earthquakes)
layer.heatmapWeight = NSExpression(forMLNInterpolating: .zoomLevelVariable,
curveType: .linear,
parameters: nil,
stops: NSExpression(forConstantValue: [0: 0,
6: 1]))
layer.heatmapIntensity = NSExpression(forMLNInterpolating: .zoomLevelVariable,
curveType: .linear,
parameters: nil,
stops: NSExpression(forConstantValue: [0: 1,
9: 3]))
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "earthquake-heat"))
}
func testMLNSymbolStyleLayer() {
let pois = MLNVectorTileSource(identifier: "pois", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(pois)
// #-example-code
let layer = MLNSymbolStyleLayer(identifier: "coffeeshops", source: pois)
layer.sourceLayerIdentifier = "pois"
layer.iconImageName = NSExpression(forConstantValue: "coffee")
layer.iconScale = NSExpression(forConstantValue: 0.5)
layer.text = NSExpression(forKeyPath: "name")
#if os(macOS)
var vector = CGVector(dx: 10, dy: 0)
layer.textTranslation = NSExpression(forConstantValue: NSValue(bytes: &vector, objCType: "{CGVector=dd}"))
#else
layer.textTranslation = NSExpression(forConstantValue: NSValue(cgVector: CGVector(dx: 10, dy: 0)))
#endif
layer.textJustification = NSExpression(forConstantValue: "left")
layer.textAnchor = NSExpression(forConstantValue: "left")
layer.predicate = NSPredicate(format: "%K == %@", "venue-type", "coffee")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "coffeeshops"))
}
func testMLNRasterStyleLayer() {
let source = MLNRasterTileSource(identifier: "clouds", tileURLTemplates: ["https://example.com/raster-tiles/{z}/{x}/{y}.png"], options: [
.minimumZoomLevel: 9,
.maximumZoomLevel: 16,
.tileSize: 512,
.attributionInfos: [
MLNAttributionInfo(title: NSAttributedString(string: "© Mapbox"), url: URL(string: "https://mapbox.com")),
],
])
mapView.style?.addSource(source)
// #-example-code
let layer = MLNRasterStyleLayer(identifier: "clouds", source: source)
layer.rasterOpacity = NSExpression(forConstantValue: 0.5)
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "clouds"))
}
func testMLNHillshadeStyleLayer() {
let source = MLNRasterDEMSource(identifier: "dem", tileURLTemplates: ["https://example.com/raster-rgb/{z}/{x}/{y}.png"], options: [
.minimumZoomLevel: 9,
.maximumZoomLevel: 16,
.tileSize: 256,
.attributionInfos: [
MLNAttributionInfo(title: NSAttributedString(string: "© Mapbox"), url: URL(string: "https://mapbox.com")),
],
])
mapView.style?.addSource(source)
let canals = MLNVectorTileSource(identifier: "canals", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(canals)
let canalShadowLayer = MLNLineStyleLayer(identifier: "waterway-river-canal-shadow", source: canals)
mapView.style?.addLayer(canalShadowLayer)
// #-example-code
let layer = MLNHillshadeStyleLayer(identifier: "hills", source: source)
layer.hillshadeExaggeration = NSExpression(forConstantValue: 0.6)
if let canalShadowLayer = mapView.style?.layer(withIdentifier: "waterway-river-canal-shadow") {
mapView.style?.insertLayer(layer, below: canalShadowLayer)
}
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "hills"))
}
func testMLNVectorStyleLayer$predicate() {
let terrain = MLNVectorTileSource(identifier: "terrain", configurationURL: URL(string: "https://example.com/style.json")!)
mapView.style?.addSource(terrain)
// #-example-code
let layer = MLNLineStyleLayer(identifier: "contour", source: terrain)
layer.sourceLayerIdentifier = "contours"
layer.predicate = NSPredicate(format: "(index == 5 || index == 10) && CAST(ele, 'NSNumber') >= 1500.0")
mapView.style?.addLayer(layer)
// #-end-example-code
XCTAssertNotNil(mapView.style?.layer(withIdentifier: "contour"))
}
func testMLNMapView() {
// #-example-code
#if os(macOS)
class MapClickGestureRecognizer: NSClickGestureRecognizer {
override func shouldRequireFailure(of otherGestureRecognizer: NSGestureRecognizer) -> Bool {
otherGestureRecognizer is NSClickGestureRecognizer
}
}
#else
let mapTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(myCustomFunction))
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
mapTapGestureRecognizer.require(toFail: recognizer)
}
mapView.addGestureRecognizer(mapTapGestureRecognizer)
#endif
// #-end-example-code
}
func testMLNMapSnapshotter() throws {
throw XCTSkip("Snapshotter not implemented yet for Metal. See https://github.com/maplibre/maplibre-native/issues/1862")
let expectation = expectation(description: "MLNMapSnapshotter should produce a snapshot")
#if os(macOS)
var image: NSImage? {
didSet {
expectation.fulfill()
}
}
#else
var image: UIImage? {
didSet {
expectation.fulfill()
}
}
#endif
class MLNStyle {
static var satelliteStreetsStyleURL: URL {
MLNDocumentationExampleTests.styleURL
}
}
// #-example-code
let camera = MLNMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 37.7184, longitude: -122.4365), altitude: 100, pitch: 20, heading: 0)
let options = MLNMapSnapshotOptions(styleURL: MLNStyle.satelliteStreetsStyleURL, camera: camera, size: CGSize(width: 320, height: 480))
options.zoomLevel = 10
let snapshotter = MLNMapSnapshotter(options: options)
snapshotter.start { snapshot, error in
if let error {
fatalError(error.localizedDescription)
}
image = snapshot?.image
}
// #-end-example-code
wait(for: [expectation], timeout: 5)
_ = image
}
func testMLNCluster() {
enum ExampleError: Error {
case unexpectedFeatureType
case featureIsNotACluster
}
let geoJSON: [String: Any] = [
"type": "Feature",
"geometry": [
"coordinates": [
-77.00896639534831,
38.87031006108791,
0.0,
],
"type": "Point",
] as [String: Any],
"properties": [
"cluster": true,
"cluster_id": 123,
"point_count": 4567,
] as [String: Any],
]
let clusterShapeData = try! JSONSerialization.data(withJSONObject: geoJSON, options: [])
do {
// #-example-code
let shape = try! MLNShape(data: clusterShapeData, encoding: String.Encoding.utf8.rawValue)
guard let pointFeature = shape as? MLNPointFeature else {
throw ExampleError.unexpectedFeatureType
}
// Check for cluster conformance
guard let cluster = pointFeature as? MLNCluster else {
throw ExampleError.featureIsNotACluster
}
// Currently the only supported class that conforms to ``MLNCluster`` is
// ``MLNPointFeatureCluster``
guard cluster is MLNPointFeatureCluster else {
throw ExampleError.unexpectedFeatureType
}
// #-end-example-code
XCTAssert(cluster.clusterIdentifier == 123)
XCTAssert(cluster.clusterPointCount == 4567)
} catch {
XCTFail("Example failed with thrown error: \(error)")
}
}
func testMLNAttributedExpression() {
// #-example-code
#if os(macOS)
let redColor = NSColor.red
#else
let redColor = UIColor.red
#endif
let expression = NSExpression(forConstantValue: "Foo")
let attributes: [MLNAttributedExpressionKey: NSExpression] = [.fontNamesAttribute: NSExpression(forConstantValue: ["DIN Offc Pro Italic",
"Arial Unicode MS Regular"]),
.fontScaleAttribute: NSExpression(forConstantValue: 1.2),
.fontColorAttribute: NSExpression(forConstantValue: redColor)]
let attributedExpression = MLNAttributedExpression(expression, attributes: attributes)
// #-end-example-code
XCTAssertNotNil(attributedExpression)
}
func testMLNShapeSourceOptionClusterProperties() {
// #-example-code
let firstExpression = NSExpression(format: "sum:({$featureAccumulated, sumValue})")
let secondExpression = NSExpression(forKeyPath: "magnitude")
let clusterPropertiesDictionary = ["sumValue": [firstExpression, secondExpression]]
let options: [MLNShapeSourceOption: Any] = [.clustered: true,
.clusterProperties: clusterPropertiesDictionary]
// #-end-example-code
let geoJSON: [String: Any] = [
"type": "Feature",
"geometry": [
"coordinates": [
-77.00896639534831,
38.87031006108791,
0.0,
],
"type": "Point",
] as [String: Any],
"properties": [
"cluster": true,
"cluster_id": 123,
"point_count": 4567,
] as [String: Any],
]
let clusterShapeData = try! JSONSerialization.data(withJSONObject: geoJSON, options: [])
let shape = try! MLNShape(data: clusterShapeData, encoding: String.Encoding.utf8.rawValue)
let source = MLNShapeSource(identifier: "source", shape: shape, options: options)
mapView.style?.addSource(source)
}
// For testMLNMapView().
func myCustomFunction() {}
}