Skip to content

Commit

Permalink
Expose CustomGeometrySource and CustomRasterSource (#1992)
Browse files Browse the repository at this point in the history
* Initial implementation

* Clean up implementation, add tests

* Update tests

* Update tests and docs

* Address review comments

* Update options to an optional

* Remove whitespace

* Add trailing newline
  • Loading branch information
pjleonard37 authored Jan 26, 2024
1 parent e25eaa4 commit 711ac7f
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 18 deletions.
32 changes: 17 additions & 15 deletions Apps/Examples/Examples/All Examples/CustomRasterSourceExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,24 @@ final class CustomRasterSourceExample: UIViewController, ExampleProtocol {
}

private func setupExample() {
let rasterSourceOptions = CustomRasterSourceOptions(
fetchTileFunction: { [weak self] tileID in
guard let self else { return }

try! self.mapView.mapboxMap.setCustomRasterSourceTileData(
forSourceId: ID.customRasterSource,
tileId: tileID,
image: rasterImages[currentImageIndex])
},
cancelTileFunction: { _ in },
minZoom: 0,
maxZoom: 0,
tileSize: 256 // Image for raster tile must be of same dimensions as tile size of the source.
)
let customRasterSource = CustomRasterSource(id: ID.customRasterSource, options: rasterSourceOptions)

do {
let rasterSourceOptions = CustomRasterSourceOptions(
fetchTileFunction: { [weak self] tileID in
guard let self else { return }

try! self.mapView.mapboxMap.setCustomRasterSourceTileData(
forSourceId: ID.customRasterSource,
tileId: tileID,
image: rasterImages[currentImageIndex])
},
cancelTileFunction: { _ in },
minZoom: 0,
maxZoom: 0,
tileSize: 256 // Image for raster tile must be of same dimensions as tile size of the source.
)
try mapView.mapboxMap.addCustomRasterSource(forSourceId: ID.customRasterSource, options: rasterSourceOptions)
try mapView.mapboxMap.addSource(customRasterSource)

var rasterLayer = RasterLayer(id: ID.rasterLayer, source: ID.customRasterSource)
rasterLayer.rasterColorMix = .constant([1, 0, 0, 0])
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Mapbox welcomes participation and contributions from everyone.
Use them to configure respective map options after creating a map view.
* Expose `MapboxMap.reduceMemoryUse()` which can be used in situations when it is important to keep the memory footprint minimal.
* Expose `MapboxMap.isAnimationInProgress` and `MapboxMap.isGestureInProgress` to query current status of both built-in and custom camera animations and gestures.
* Expose experimental `CustomRasterSource` and non-experimental `CustomGeometrySource` as regular `Source`'s providing a better way to work with them and also allow for using them in Style DSL.

## 11.1.0 - 17 January, 2024

Expand Down
2 changes: 2 additions & 0 deletions Sources/MapboxMaps/Documentation.docc/API Catalogs/Sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
- ``PromoteId``
- ``Scheme``
- ``Encoding``
- ``CustomGeometrySource``
- ``CustomGeometrySourceOptions``
- ``CustomRasterSource``
- ``CustomRasterSourceOptions``
- ``TileFunctionCallback``
45 changes: 45 additions & 0 deletions Sources/MapboxMaps/Style/CustomSources/CustomGeometrySource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

/// Describes a Custom Geometry Source to be used in the style
///
/// A CustomGeometrySource uses a coalescing model for frequent data updates targeting the same tile id.
/// This means that the in-progress request as well as the last scheduled request are guaranteed to finish.
public struct CustomGeometrySource: Source {

/// The Source type
public let type: SourceType

/// Style source identifier.
public let id: String

/// Settings for the custom geometry, including a fetchTileFunction callback
public let options: CustomGeometrySourceOptions?

public init(id: String, options: CustomGeometrySourceOptions) {
self.type = .customGeometry
self.id = id
self.options = options
}
}

extension CustomGeometrySource {
enum CodingKeys: String, CodingKey {
case id
case type
}

/// Init from a decoder, note that the CustomGeometrySourceOptions are not decodable and need to be set separately
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
type = try container.decode(SourceType.self, forKey: .type)
options = nil
}

/// Encode, note that options will not be included
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(type, forKey: .type)
}
}
58 changes: 58 additions & 0 deletions Sources/MapboxMaps/Style/CustomSources/CustomRasterSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

/// Describes a Custom Raster Source to be used in the style.
///
/// To add the data, set options with a ``CustomRasterSourceOptions`` with a fetchTileFunction callback
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
@_spi(Experimental)
public struct CustomRasterSource: Source {

/// The Source type
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
public let type: SourceType = .customRaster

/// Style source identifier.
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
public let id: String

/// Settings for the custom raster source, including a fetchTileFunction callback
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
public let options: CustomRasterSourceOptions?

#if swift(>=5.8)
@_documentation(visibility: public)
#endif
public init(id: String, options: CustomRasterSourceOptions) {
self.id = id
self.options = options
}
}

extension CustomRasterSource {
enum CodingKeys: String, CodingKey {
case id
case type
}

/// Init from a decoder, note that the CustomRasterSourceOptions are not decodable and need to be set separately
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
options = nil
}

/// Encode, note that options will not be included
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(type, forKey: .type)
}
}
11 changes: 11 additions & 0 deletions Sources/MapboxMaps/Style/SourceType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public struct SourceType: ExpressibleByStringLiteral, RawRepresentable, Codable,
@_spi(Experimental)
public static let rasterArray: SourceType = "raster-array"

/// A custom geometry source.
public static let customGeometry: SourceType = "custom-geometry"

/// A custom raster source.
@_spi(Experimental)
public static let customRaster: SourceType = "custom-raster"

public init(stringLiteral type: String) {
self.rawValue = type
}
Expand All @@ -52,6 +59,10 @@ public struct SourceType: ExpressibleByStringLiteral, RawRepresentable, Codable,
return ImageSource.self
case .model:
return ModelSource.self
case .customGeometry:
return CustomGeometrySource.self
case .customRaster:
return CustomRasterSource.self
default:
return nil
}
Expand Down
21 changes: 18 additions & 3 deletions Sources/MapboxMaps/Style/StyleSourceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,24 @@ internal final class StyleSourceManager: StyleSourceManagerProtocol {
}

internal func addSource(_ source: Source, dataId: String? = nil) throws {
if let geoJSONSource = source as? GeoJSONSource {
try addGeoJSONSource(geoJSONSource, dataId: dataId)
} else {
switch source {
case let source as CustomRasterSource:
guard let options = source.options else {
throw StyleError(message: "CustomRasterSource does not have CustomRasterSourceOptions")
}
try handleExpected {
styleManager.addStyleCustomRasterSource(forSourceId: source.id, options: options)
}
case let source as CustomGeometrySource:
guard let options = source.options else {
throw StyleError(message: "CustomGeometrySource does not have CustomGeometrySourceOptions")
}
try handleExpected {
styleManager.addStyleCustomGeometrySource(forSourceId: source.id, options: options)
}
case let source as GeoJSONSource:
try addGeoJSONSource(source, dataId: dataId)
default:
try addSourceInternal(source)
}
}
Expand Down
67 changes: 67 additions & 0 deletions Tests/MapboxMapsTests/Style/CustomSourcesIntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import XCTest
@_spi(Experimental) @testable import MapboxMaps

final class CustomSourcesIntegrationTests: MapViewIntegrationTestCase {

func testCustomRasterSourceAdditionAndRemoval() {
let successfullyAddedSourceExpectation = XCTestExpectation(description: "Successfully added CustomRasterSource to Map")
successfullyAddedSourceExpectation.expectedFulfillmentCount = 1

let successfullyRetrievedSourceExpectation = XCTestExpectation(description: "Successfully retrieved CustomRasterSource from Map")
successfullyRetrievedSourceExpectation.expectedFulfillmentCount = 1

mapView.mapboxMap.styleURI = .standard

didFinishLoadingStyle = { mapView in
let source = CustomRasterSource(id: "test-source", options: CustomRasterSourceOptions(fetchTileFunction: { _ in }, cancelTileFunction: { _ in }))

// Add source
do {
try mapView.mapboxMap.addSource(source)
successfullyAddedSourceExpectation.fulfill()
} catch {
XCTFail("Failed to add CustomRasterSource because of error: \(error)")
}

// Retrieve the source
do {
_ = try mapView.mapboxMap.source(withId: "test-source", type: CustomRasterSource.self)
successfullyRetrievedSourceExpectation.fulfill()
} catch {
XCTFail("Failed to retrieve CustomRasterSource because of error: \(error)")
}
}
wait(for: [successfullyAddedSourceExpectation, successfullyRetrievedSourceExpectation], timeout: 5.0)
}

func testCustomGeometrySourceAdditionAndRemoval() {
let successfullyAddedSourceExpectation = XCTestExpectation(description: "Successfully added CustomGeometrySource to Map")
successfullyAddedSourceExpectation.expectedFulfillmentCount = 1

let successfullyRetrievedSourceExpectation = XCTestExpectation(description: "Successfully retrieved CustomGeometrySource from Map")
successfullyRetrievedSourceExpectation.expectedFulfillmentCount = 1

mapView.mapboxMap.styleURI = .standard

didFinishLoadingStyle = { mapView in
let source = CustomGeometrySource(id: "test-source", options: CustomGeometrySourceOptions(fetchTileFunction: { _ in }, cancelTileFunction: { _ in }, tileOptions: TileOptions()))

// Add source
do {
try mapView.mapboxMap.addSource(source)
successfullyAddedSourceExpectation.fulfill()
} catch {
XCTFail("Failed to add CustomGeometrySource because of error: \(error)")
}

// Retrieve the source
do {
_ = try mapView.mapboxMap.source(withId: "test-source", type: CustomGeometrySource.self)
successfullyRetrievedSourceExpectation.fulfill()
} catch {
XCTFail("Failed to retrieve CustomGeometrySource because of error: \(error)")
}
}
wait(for: [successfullyAddedSourceExpectation, successfullyRetrievedSourceExpectation], timeout: 5.0)
}
}
62 changes: 62 additions & 0 deletions Tests/MapboxMapsTests/Style/CustomSourcesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// This file is generated.
import XCTest
@_spi(Experimental) @testable import MapboxMaps

final class CustomSourcesSourceTests: XCTestCase {

func testRasterEncodingAndDecoding() {
let testCustomRasterSourceOptions = CustomRasterSourceOptions(fetchTileFunction: { _ in }, cancelTileFunction: { _ in })

var source = CustomRasterSource(id: "test-source", options: testCustomRasterSourceOptions)

var data: Data?
do {
data = try JSONEncoder().encode(source)
} catch {
XCTFail("Failed to encode CustomRasterSource.")
}

guard let validData = data else {
XCTFail("Failed to encode CustomRasterSource.")
return
}

do {
let decodedSource = try JSONDecoder().decode(CustomRasterSource.self, from: validData)
XCTAssert(decodedSource.type == SourceType.customRaster)
XCTAssert(decodedSource.id == "test-source")
XCTAssertNil(decodedSource.options)
} catch {
XCTFail("Failed to decode CustomRasterSource.")
}
}

func testGeometryEncodingAndDecoding() {
let testCustomGeometrySourceOptions = CustomGeometrySourceOptions(fetchTileFunction: { _ in }, cancelTileFunction: { _ in }, tileOptions: TileOptions())

var source = CustomGeometrySource(id: "test-source", options: testCustomGeometrySourceOptions)

var data: Data?
do {
data = try JSONEncoder().encode(source)
} catch {
XCTFail("Failed to encode CustomRasterSource.")
}

guard let validData = data else {
XCTFail("Failed to encode CustomRasterSource.")
return
}

do {
let decodedSource = try JSONDecoder().decode(CustomGeometrySource.self, from: validData)
XCTAssert(decodedSource.type == SourceType.customGeometry)
XCTAssert(decodedSource.id == "test-source")
XCTAssertNil(decodedSource.options)
} catch {
XCTFail("Failed to decode CustomRasterSource.")
}
}
}

// End of generated file
2 changes: 2 additions & 0 deletions Tests/TestPlans/IntegrationTests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"BasicCameraAnimatorImplIntegrationTests",
"CircleAnnotationIntegrationTests",
"CircleLayerIntegrationTests",
"CustomSourcesIntegrationTests",
"DidIdleFailureIntegrationTest",
"ExampleIntegrationTest",
"FeatureQueryingTest",
Expand Down Expand Up @@ -56,6 +57,7 @@
"PointAnnotationIntegrationTests",
"PolygonAnnotationIntegrationTests",
"PolylineAnnotationIntegrationTests",
"RasterArraySourceIntegrationTests",
"RasterDemSourceIntegrationTests",
"RasterLayerIntegrationTests",
"RasterSourceIntegrationTests",
Expand Down
2 changes: 2 additions & 0 deletions Tests/TestPlans/UnitTests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"BasicCameraAnimatorImplIntegrationTests",
"CircleAnnotationIntegrationTests",
"CircleLayerIntegrationTests",
"CustomSourcesIntegrationTests",
"DidIdleFailureIntegrationTest",
"ExampleIntegrationTest",
"FeatureQueryingTest",
Expand Down Expand Up @@ -69,6 +70,7 @@
"PointAnnotationIntegrationTests",
"PolygonAnnotationIntegrationTests",
"PolylineAnnotationIntegrationTests",
"RasterArraySourceIntegrationTests",
"RasterDemSourceIntegrationTests",
"RasterLayerIntegrationTests",
"RasterSourceIntegrationTests",
Expand Down

0 comments on commit 711ac7f

Please sign in to comment.