diff --git a/Apps/Examples/Examples/All Examples/Sample Data/fragment-realestate-NY.json b/Apps/Examples/Examples/All Examples/Sample Data/fragment-realestate-NY.json index e9c61a8208c..8721e2319bc 100644 --- a/Apps/Examples/Examples/All Examples/Sample Data/fragment-realestate-NY.json +++ b/Apps/Examples/Examples/All Examples/Sample Data/fragment-realestate-NY.json @@ -1,18 +1,4 @@ { - "imports": [ - { - "id": "standard", - "url": "mapbox://styles/mapbox/standard", - "config": { - "font": "Montserrat", - "lightPreset": "dusk", - "showPointOfInterestLabels": true, - "showTransitLabels": false, - "showPlaceLabels": true, - "showRoadLabels": false - } - } - ], "version": 8, "sprite": "mapbox://sprites/examples/clkiip5x600b501pb4al5g7wv/du1h5vs55f48z8rgjod0dggl9", "projection": { "name": "globe" }, @@ -31,26 +17,6 @@ } }, "layers": [ - { - "id": "water", - "type": "fill", - "slot": "bottom", - "source": "water_source", - "source-layer": "water", - "layout": { }, - "paint": { - "fill-emissive-strength": 1, - "fill-color": [ - "interpolate", - [ "linear" ], - [ "measure-light", "brightness" ], - 0.1, - "hsl(250, 50%, 40%)", - 0.4, - "hsl(250, 50%, 80%)" - ] - } - }, { "id": "subway-lines", "type": "line", diff --git a/Apps/Examples/Examples/All Examples/StandardStyleExample.swift b/Apps/Examples/Examples/All Examples/StandardStyleExample.swift index b8e6a8eadc3..ff94da4d87a 100644 --- a/Apps/Examples/Examples/All Examples/StandardStyleExample.swift +++ b/Apps/Examples/Examples/All Examples/StandardStyleExample.swift @@ -6,27 +6,18 @@ final class StandardStyleExample: UIViewController, ExampleProtocol { private var cancelables = Set() private var lightPreset = StandardLightPreset.night private var labelsSetting = true + private var showRealEstate = true - // The fragment-realestate-NY.json style imports standard style with "standard" import id. - // Here we specify import config to that style. private var mapStyle: MapStyle { - MapStyle( - uri: StyleURI(url: styleURL)!, - importConfigurations: [ - .standard( - importId: "standard", - lightPreset: lightPreset, - showPointOfInterestLabels: labelsSetting, - showTransitLabels: labelsSetting, - showPlaceLabels: labelsSetting, - showRoadLabels: labelsSetting) - ] + .standard( + lightPreset: lightPreset, + showPointOfInterestLabels: labelsSetting, + showTransitLabels: labelsSetting, + showPlaceLabels: labelsSetting, + showRoadLabels: labelsSetting ) } - // Load a style which imports Mapbox Standard as a basemap - private let styleURL = Bundle.main.url(forResource: "fragment-realestate-NY", withExtension: "json")! - override func viewDidLoad() { super.viewDidLoad() @@ -39,6 +30,7 @@ final class StandardStyleExample: UIViewController, ExampleProtocol { // When the style has finished loading add a line layer representing the border between New York and New Jersey mapView.mapboxMap.onStyleLoaded.observe { [weak self] _ in + guard let self else { return } // Create and apply basic styling to the line layer, assign the layer to the "middle" slot var layer = LineLayer(id: "line-layer", source: "line-layer") @@ -59,20 +51,23 @@ final class StandardStyleExample: UIViewController, ExampleProtocol { ]))) do { - try self?.mapView.mapboxMap.addSource(source) - try self?.mapView.mapboxMap.addLayer(layer) + try mapView.mapboxMap.addSource(source) + try mapView.mapboxMap.addLayer(layer) } catch { print(error) } + toggleRealEstate(isOn: showRealEstate) + // The below line is used for internal testing purposes only. - self?.finish() + finish() }.store(in: &cancelables) // Add buttons to control the light presets and labels let lightButton = changeLightButton() let labelsButton = changeLabelsButton() - navigationItem.rightBarButtonItems = [lightButton, labelsButton] + let realEstateButton = changeRealEstateButton() + navigationItem.rightBarButtonItems = [lightButton, labelsButton, realEstateButton] } private func changeLightButton() -> UIBarButtonItem { @@ -91,6 +86,14 @@ final class StandardStyleExample: UIViewController, ExampleProtocol { } } + private func changeRealEstateButton() -> UIBarButtonItem { + if #available(iOS 13, *) { + return UIBarButtonItem(image: UIImage(systemName: "building.2.fill"), style: .plain, target: self, action: #selector(changeRealEstateSettings)) + } else { + return UIBarButtonItem(title: "Build", style: .plain, target: self, action: #selector(changeLabelsSetting)) + } + } + @objc private func changeLightSetting() { // When a user clicks the light setting button change the `lightPreset` config property on the Standard style import. @@ -107,4 +110,24 @@ final class StandardStyleExample: UIViewController, ExampleProtocol { mapView.mapboxMap.mapStyle = mapStyle } + + @objc private func changeRealEstateSettings() { + // When a user clicks show real estate button. Style import with real estate is added or deleted. + showRealEstate.toggle() + toggleRealEstate(isOn: showRealEstate) + } + + private func toggleRealEstate(isOn: Bool) { + do { + if isOn { + try mapView.mapboxMap.addStyleImport(withId: "real-estate", uri: StyleURI(url: styleURL)!) + } else { + try mapView.mapboxMap.removeStyleImport(withId: "real-estate") + } + } catch { + print(error) + } + } } + +private let styleURL = Bundle.main.url(forResource: "fragment-realestate-NY", withExtension: "json")! diff --git a/Apps/Examples/Examples/Models/Examples.swift b/Apps/Examples/Examples/Models/Examples.swift index 580a5654083..c3ab6c2b643 100644 --- a/Apps/Examples/Examples/Models/Examples.swift +++ b/Apps/Examples/Examples/Models/Examples.swift @@ -63,7 +63,7 @@ struct Examples { Example(title: "Display a map view", description: "Create and display a map that uses the default Mapbox Standard style.", type: BasicMapExample.self), - Example(title: "Work with the Standard style", + Example(title: "Standard Style", description: "Use the Standard style and modify settings at runtime.", type: StandardStyleExample.self), Example(title: "Debug Map", diff --git a/Apps/Examples/Examples/SwiftUI Examples/StandardStyleImportExample.swift b/Apps/Examples/Examples/SwiftUI Examples/StandardStyleImportExample.swift index af0a5aeaa64..ba266ecde84 100644 --- a/Apps/Examples/Examples/SwiftUI Examples/StandardStyleImportExample.swift +++ b/Apps/Examples/Examples/SwiftUI Examples/StandardStyleImportExample.swift @@ -3,46 +3,33 @@ import SwiftUI @available(iOS 14.0, *) struct StandardStyleImportExample: View { - @State private var lightPreset: StandardLightPreset = .night + @State private var lightPreset: StandardLightPreset? = .night @State private var showLabels = true @State private var priceAlertMessage: String? @State private var panelHeight: CGFloat = 0 - - var style: MapStyle { - let styleURI = StyleURI(url: styleURL)! - return MapStyle( - uri: styleURI, - importConfigurations: [ - // The 'fragment-realestate-NY.json' style imports standard style with "standard" import id. - // Here we specify import config to that style. - .standard( - importId: "standard", - lightPreset: lightPreset, - showPointOfInterestLabels: showLabels, - showTransitLabels: showLabels, - showPlaceLabels: showLabels, - showRoadLabels: showLabels) - ] - ) - } + @State private var showRealEstate = false var body: some View { - Map(initialViewport: .camera(center: .init(latitude: 40.72, longitude: -73.99), zoom: 11, pitch: 45)) - .mapStyle(style) - .onLayerTapGesture("NY-hotels-price") { queriedFeature, _ in - // Show house price by tap to the 'NY-hotels-price' layer defined in 'fragment-realestate-NY.json' style. - // The QueriedFeature represents the rendered GeoJSON feature that was tapped. - guard let price = queriedFeature.feature.properties?["price"], - case .number(let priceNum) = price else { return true } - priceAlertMessage = "Price: $\(String(format: "%.2f", priceNum))" - return true - } - .additionalSafeAreaInsets(.bottom, panelHeight) - .ignoresSafeArea() - .safeOverlay(alignment: .bottom) { - settingsPanel.onChangeOfSize { panelHeight = $0.height } + Map(initialViewport: .camera(center: .init(latitude: 40.72, longitude: -73.99), zoom: 11, pitch: 45)) { + StyleImport(style: .standard( + lightPreset: lightPreset, + showPointOfInterestLabels: showLabels, + showTransitLabels: showLabels, + showPlaceLabels: showLabels, + showRoadLabels: showLabels) + ) + + if showRealEstate { + StyleImport(uri: StyleURI(url: styleURL)!) } - .simpleAlert(message: $priceAlertMessage) + } + .mapStyle(.empty) + .additionalSafeAreaInsets(.bottom, panelHeight) + .ignoresSafeArea() + .safeOverlay(alignment: .bottom) { + settingsPanel.onChangeOfSize { panelHeight = $0.height } + } + .simpleAlert(message: $priceAlertMessage) } @ViewBuilder @@ -51,13 +38,15 @@ struct StandardStyleImportExample: View { HStack { Text("Light") Picker("Light preset", selection: $lightPreset) { - Text("Dawn").tag(StandardLightPreset.dawn) - Text("Day").tag(StandardLightPreset.day) - Text("Dusk").tag(StandardLightPreset.dusk) - Text("Night").tag(StandardLightPreset.night) + Text("Dawn").tag(Optional(StandardLightPreset.dawn)) + Text("Day").tag(Optional(StandardLightPreset.day)) + Text("Dusk").tag(Optional(StandardLightPreset.dusk)) + Text("Night").tag(Optional(StandardLightPreset.night)) + Text("None").tag(Optional.none) }.pickerStyle(.segmented) } Toggle("Labels", isOn: $showLabels) + Toggle("Show Real Estate", isOn: $showRealEstate) } .padding(10) .floating(RoundedRectangle(cornerRadius: 10)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3cca7893c..28f309e9fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,17 @@ Mapbox welcomes participation and contributions from everyone. ## main + +### Experimental API breaking changes ⚠️ +* `StyleImportConfiguration` now accepts only the configuration JSONObject without `importId`. And has `JSONObject` as `rawValue` +* `MapStyle` now accepts `importConfigurations` as a map where the key is `importId` instead of an array +* `StyleImportConfiguration.standard` is no more exposed, use `MapStyle.standard()` to provide configuration for Standard style + * Allow to assign slot to 2D and 3D location indicators. * Allow to add slots at runtime. +* Expose API to interact with styile imports using Declarative Styling and regular imperative API. +* Expose `StyleImport` for declarative styling as `MapStyleContent`. +* Expose `removeStyleImport`, `moveStyleImport`, `updateStyleImport`, `addStyleImport` methods on `StyleManager` ## 11.3.0 - 10 April, 2024 diff --git a/MapboxMaps.podspec b/MapboxMaps.podspec index 0e3a2a97b85..c73df02778a 100644 --- a/MapboxMaps.podspec +++ b/MapboxMaps.podspec @@ -21,7 +21,7 @@ Pod::Spec.new do |m| m.source_files = 'Sources/MapboxMaps/**/*.{swift,h}' m.resource_bundles = { 'MapboxMapsResources' => ['Sources/**/*.{xcassets,strings}', 'Sources/MapboxMaps/MapboxMaps.json', 'Sources/MapboxMaps/PrivacyInfo.xcprivacy'] } - m.dependency 'MapboxCoreMaps', '11.3.0' + m.dependency 'MapboxCoreMaps', '11.4.0-SNAPSHOT.0417T1228Z.b5204fc' m.dependency 'MapboxCommon', '24.3.1' m.dependency 'Turf', '2.8.0' diff --git a/Package.resolved b/Package.resolved index ced1016e115..fd736c349b6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,15 +9,6 @@ "version" : "24.3.1" } }, - { - "identity" : "mapbox-core-maps-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mapbox/mapbox-core-maps-ios.git", - "state" : { - "revision" : "3638d2841f8a2ef6a995c659457d6133a6f0d1be", - "version" : "11.3.0" - } - }, { "identity" : "turf-swift", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 66dd6918d92..569c81e30e0 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription import Foundation -let coreMaps = MapsDependency.coreMaps(version: "11.3.0") +let coreMaps = MapsDependency.coreMaps(version: "11.4.0-SNAPSHOT.0417T1228Z.b5204fc", checksum: "5ad1d2c4f0e027bb23b149784dac08c14ac9d9f19f9e8a972e34fd809d188da9") let common = MapsDependency.common(version: "24.3.1") let mapboxMapsPath: String? = nil diff --git a/Sources/MapboxMaps/ContentBuilders/MapContent/MapStyleReconciler.swift b/Sources/MapboxMaps/ContentBuilders/MapContent/MapStyleReconciler.swift index a33a58f73b9..8f758849c92 100644 --- a/Sources/MapboxMaps/ContentBuilders/MapContent/MapStyleReconciler.swift +++ b/Sources/MapboxMaps/ContentBuilders/MapContent/MapStyleReconciler.swift @@ -36,7 +36,7 @@ final class MapStyleReconciler { ) { let oldMapStyle = _mapStyle - if _mapStyle?.loadMethod != style.loadMethod { + if _mapStyle?.data != style.data { _mapStyle = style let callbacks = RuntimeStylingCallbacks( @@ -45,7 +45,7 @@ final class MapStyleReconciler { /// can continue loading. /// We can safely add new content via style DSL. guard let self else { return } - self.reconcileStyleImports(from: oldMapStyle?.importConfigurations) + self.reconcileBasemapConfiguration(from: oldMapStyle?.configuration) self._isStyleRootLoaded.value = true if let transition { self.styleManager.setStyleTransitionFor(transition.coreOptions) @@ -67,7 +67,7 @@ final class MapStyleReconciler { self._isStyleRootLoaded.value = false - switch style.loadMethod { + switch style.data { case let .json(json): styleManager.setStyleJSON(json, callbacks: callbacks) case let .uri(uri): @@ -78,7 +78,7 @@ final class MapStyleReconciler { _mapStyle = style if _isStyleRootLoaded.value { - reconcileStyleImports(from: oldMapStyle?.importConfigurations) + reconcileBasemapConfiguration(from: oldMapStyle?.configuration) } if styleManager.isStyleLoaded() { @@ -97,35 +97,34 @@ final class MapStyleReconciler { } } - private func reconcileStyleImports(from old: [StyleImportConfiguration]?) { + private func reconcileBasemapConfiguration(from old: JSONObject?) { guard let mapStyle else { return } - Self.reconcileStyleImports( + Self.reconcileBasemapConfiguration( from: old, - to: mapStyle.importConfigurations, - styleManager: styleManager) + to: mapStyle.configuration, + styleManager: styleManager + ) } } extension MapStyleReconciler { - static func reconcileStyleImports( - from old: [StyleImportConfiguration]?, - to new: [StyleImportConfiguration], + static func reconcileBasemapConfiguration( + from old: JSONObject?, + to new: JSONObject?, styleManager: StyleManagerProtocol ) { - for importConfig in new { - let oldImportConfig = old?.first { $0.importId == importConfig.importId } - do { - for (key, value) in importConfig.config { - guard let value, value != oldImportConfig?.config[key] else { - continue - } - try handleExpected { - styleManager.setStyleImportConfigPropertyForImportId(importConfig.importId, config: key, value: value.rawValue) - } + guard let new else { return } + do { + for (key, value) in new { + guard let value, value != old?[key] else { + continue + } + try handleExpected { + styleManager.setStyleImportConfigPropertyForImportId("basemap", config: key, value: value.rawValue) } - } catch { - Log.error(forMessage: "Failed updating import config properties, \(error)") } + } catch { + Log.error(forMessage: "Failed updating import config properties, \(error)") } } } diff --git a/Sources/MapboxMaps/ContentBuilders/MapStyleContent/MountedStyleImport.swift b/Sources/MapboxMaps/ContentBuilders/MapStyleContent/MountedStyleImport.swift new file mode 100644 index 00000000000..e39f003ad4a --- /dev/null +++ b/Sources/MapboxMaps/ContentBuilders/MapStyleContent/MountedStyleImport.swift @@ -0,0 +1,100 @@ +import os.log + +@available(iOS 13.0, *) +@_spi(Experimental) +extension StyleImport: MapStyleContent, PrimitiveMapContent { + func visit(_ node: MapContentNode) { + node.mount(MountedStyleImport( + id: id ?? node.id.stringId, + styleData: style.data, + configuration: style.configuration + )) + } +} + +struct MountedStyleImport: MapContentMountedComponent { + let id: String + let styleData: MapStyle.Data + let configuration: JSONObject? + + func mount(with context: MapContentNodeContext) throws { + let importPosition = context.resolveImportPosition() + os_log(.debug, log: .contentDSL, "style import %s insert %s", id, importPosition.asString()) + + switch styleData { + case .uri(let styleURI): + try handleExpected { + context.style.styleManager.addStyleImportFromURI( + forImportId: id, + uri: styleURI.rawValue, + config: configuration?.rawValue as? [String: Any], + importPosition: importPosition.corePosition) + } + case .json(let json): + try handleExpected { + context.style.styleManager.addStyleImportFromJSON( + forImportId: id, + json: json, + config: configuration?.rawValue as? [String: Any], + importPosition: importPosition.corePosition) + } + } + } + + func unmount(with context: MapContentNodeContext) throws { + os_log(.debug, log: .contentDSL, "remove style import %s", id) + + guard context.style.styleManager.getStyleImports().map(\.id).contains(id) else { + return + } + + try handleExpected { + context.style.styleManager.removeStyleImport(forImportId: id) + } + } + + func tryUpdate(from old: MapContentMountedComponent, with context: MapContentNodeContext) throws -> Bool { + os_log(.debug, log: .contentDSL, "update style import %s", id) + + guard let old = old as? Self, old.id == id else { + return false + } + + if styleData != old.styleData { + switch styleData { + case .uri(let styleURI): + try handleExpected { + context.style.styleManager.updateStyleImportWithURI( + forImportId: id, + uri: styleURI.rawValue, + config: configuration?.rawValue as? [String: Any] + ) + } + case .json(let json): + try handleExpected { + context.style.styleManager.updateStyleImportWithJSON( + forImportId: id, + json: json, + config: configuration?.rawValue as? [String: Any] + ) + } + } + } else if configuration != old.configuration, let config = configuration?.rawValue as? [String: Any] { + try handleExpected { + context.style.styleManager.setStyleImportConfigPropertiesForImportId(id, configs: config) + } + } + + return true + } + + func updateMetadata(with context: MapContentNodeContext) { + context.lastImportId = id + } +} + +private extension ImportPosition { + func asString() -> String { + (try? toString()) ?? "" + } +} diff --git a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNodeContext.swift b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNodeContext.swift index b4b6ab20e35..8bff1042d48 100644 --- a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNodeContext.swift +++ b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNodeContext.swift @@ -21,6 +21,10 @@ final class MapContentNodeContext { var lastLayerId: String? var initialStyleLayers: [String] = [] + + var lastImportId: String? + var initialStyleImports: [String] = [] + var uniqueProperties = MapContentUniqueProperties() init( @@ -40,4 +44,14 @@ final class MapContentNodeContext { let lastStyleLayer = initialStyleLayers.last(where: style.styleManager.styleLayerExists) return lastStyleLayer.map { .above($0) } ?? .at(0) } + + func resolveImportPosition() -> ImportPosition { + if let lastImportId { + return .above(lastImportId) + } + + let currentImports = style.styleManager.getStyleImports().map(\.id) + let lastStyleLayer = initialStyleImports.last(where: currentImports.contains) + return lastStyleLayer.map { .above($0) } ?? .at(0) + } } diff --git a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift index 24d2459ae91..9f8534d2ae6 100644 --- a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift +++ b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift @@ -59,6 +59,7 @@ private extension MapContentNodeContext { func update(mapContent: any MapContent, root: MapContentNode) { let oldProperties = uniqueProperties lastLayerId = nil + lastImportId = nil uniqueProperties = MapContentUniqueProperties() mapContent.update(root) @@ -78,6 +79,7 @@ private extension MapContentNodeContext { /// Position must take into account only non-persistent layers, which was not added in runtime isEqualContent = { _, _ in false } initialStyleLayers = getInitialStyleLayers() ?? [] + initialStyleImports = style.styleManager.getStyleImports().map(\.id) mapContent.update(root) isEqualContent = arePropertiesEqual diff --git a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md index 3d4c9f03106..88288253069 100644 --- a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md +++ b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md @@ -19,7 +19,6 @@ - ``LightInfo`` - ``LightType`` - ``StandardLightPreset`` -- ``StyleImportConfiguration`` - ``StyleProjection`` - ``StyleProjectionName`` - ``CancelError`` @@ -28,6 +27,8 @@ ### Declarative Map Styling - ``MapStyleContent`` +- ``StyleImport`` +- ``ImportPosition`` - ``TupleMapStyleContent`` - ``EmptyMapStyleContent`` - ``OptionalMapStyleContent`` diff --git a/Sources/MapboxMaps/Foundation/CoreAliases.swift b/Sources/MapboxMaps/Foundation/CoreAliases.swift index e5e4400b4ce..9c9e34cbb3b 100644 --- a/Sources/MapboxMaps/Foundation/CoreAliases.swift +++ b/Sources/MapboxMaps/Foundation/CoreAliases.swift @@ -8,6 +8,7 @@ typealias CoreMapSnapshotOptions = MapboxCoreMaps_Private.MapSnapshotOptions typealias CoreMapSnapshot = MapboxCoreMaps_Private.MapSnapshot typealias CoreViewAnnotationOptions = MapboxCoreMaps_Private.ViewAnnotationOptions typealias CoreLayerPosition = MapboxCoreMaps_Private.LayerPosition +typealias CoreImportPosition = MapboxCoreMaps_Private.ImportPosition typealias CoreTracing = MapboxCoreMaps_Private.Tracing typealias CoreAnnotatedFeature = MapboxCoreMaps_Private.AnnotatedFeature typealias CoreAnnotatedLayerFeature = MapboxCoreMaps_Private.AnnotatedLayerFeature diff --git a/Sources/MapboxMaps/Foundation/Extensions/Core/ImportPosition.swift b/Sources/MapboxMaps/Foundation/Extensions/Core/ImportPosition.swift new file mode 100644 index 00000000000..eb8f0e069b7 --- /dev/null +++ b/Sources/MapboxMaps/Foundation/Extensions/Core/ImportPosition.swift @@ -0,0 +1,38 @@ +/// Specifies the position at which an import will be added when using `Style.addImport` +public enum ImportPosition: Equatable, Codable { + /// Default behavior; add to the top of the imports stack. + case `default` + + /// Import should be positioned above the specified import id. + case above(String) + + /// Import should be positioned below the specified import id. + case below(String) + + /// Import should be positioned at the specified index in the imports stack. + case at(Int) + + var corePosition: CoreImportPosition { + switch self { + case .default: + return CoreImportPosition() + case .above(let importId): + return CoreImportPosition(above: importId) + case .below(let importId): + return CoreImportPosition(below: importId) + case .at(let index): + return CoreImportPosition(at: index) + } + } +} + +// MARK: - CoreImportPosition conveniences + +extension CoreImportPosition { + convenience init(above: String? = nil, below: String? = nil, at: Int? = nil) { + self.init(__above: above, below: below, at: at?.NSNumber) + } + + /// Import should be positioned at a specified index in the imports stack + var at: UInt32? { __at?.uint32Value } +} diff --git a/Sources/MapboxMaps/ContentBuilders/MapStyleContent/MapStyle.swift b/Sources/MapboxMaps/Style/MapStyle.swift similarity index 68% rename from Sources/MapboxMaps/ContentBuilders/MapStyleContent/MapStyle.swift rename to Sources/MapboxMaps/Style/MapStyle.swift index c0df1196602..1aaf713cd45 100644 --- a/Sources/MapboxMaps/ContentBuilders/MapStyleContent/MapStyle.swift +++ b/Sources/MapboxMaps/Style/MapStyle.swift @@ -35,6 +35,9 @@ import MapboxCoreMaps /// /// The ``MapStyle/standard(lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:)`` factory method lists the predefined parameters that Standard Style supports. You can also use the Classic Mapbox-designed styles such as ``MapStyle/satelliteStreets``, ``MapStyle/outdoors``, and many more. Or use custom styles that you design with [Mapbox Studio](https://www.mapbox.com/mapbox-studio). /// +/// +/// - Important: Configuration can be applied only to `.standard` style or styles that uses `.standard` as import. For any other styles configuration will make no effect. +/// /// ```swift /// // Use of Mapbox Satellite Streets style. /// Map().mapStyle(.satelliteStreets) @@ -53,30 +56,31 @@ import MapboxCoreMaps /// mapView.mapboxMap.mapStyle = .standard(stylePreset: .dusk) /// ``` /// -/// The style reloads only when the actual ``StyleURI`` or JSON (when loaded with ``MapStyle/init(json:importConfigurations:)`` is changed. To observe the result of the style load you can subscribe to ``MapboxMap/onStyleLoaded`` or ``Snapshotter/onStyleLoaded`` events, or use use ``StyleManager/load(mapStyle:transition:completion:)`` method. - @_documentation(visibility: public) +/// The style reloads only when the actual ``StyleURI`` or JSON (when loaded with ``MapStyle/init(json:configuration:)`` is changed. To observe the result of the style load you can subscribe to ``MapboxMap/onStyleLoaded`` or ``Snapshotter/onStyleLoaded`` events, or use use ``StyleManager/load(mapStyle:transition:completion:)`` method. +@_documentation(visibility: public) @_spi(Experimental) public struct MapStyle { - enum LoadMethod: Equatable { + enum Data: Equatable { case uri(StyleURI) case json(String) } - var loadMethod: LoadMethod - var importConfigurations: [StyleImportConfiguration] + var data: Data + var configuration: JSONObject? /// Creates a map style using a Mapbox Style JSON. /// /// Please consult [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/) describing the JSON format. /// - /// - Important: For the better performance with large local Style JSON please consider loading style from the file system via the ``MapStyle/init(uri:importConfigurations:)`` initializer. + /// - Important: For the better performance with large local Style JSON please consider loading style from the file system via the ``MapStyle/init(uri:configuration:)`` initializer. /// /// - Parameters: /// - json: A Mapbox Style JSON string. - /// - importConfigurations: Style import configurations to be applied on style load. + /// - configuration: Style import configuration to be applied on style load. + /// Providing `nil` configuration will make no effect and previous configuration will stay in place. In order to change previous value, you should explicitly override it with the new value. @_documentation(visibility: public) - public init(json: String, importConfigurations: [StyleImportConfiguration] = []) { - self.loadMethod = .json(json) - self.importConfigurations = importConfigurations + public init(json: String, configuration: JSONObject? = nil) { + self.data = .json(json) + self.configuration = configuration } /// Creates a map style using a Style URI. @@ -85,49 +89,17 @@ public struct MapStyle { /// /// - Parameters: /// - uri: An instance of ``StyleURI`` pointing to a Mapbox Style URI (mapbox://styles/{user}/{style}), a full HTTPS URI, or a path to a local file. - /// - importConfigurations: Style import configurations to be applied on style load. - @_documentation(visibility: public) - public init(uri: StyleURI, importConfigurations: [StyleImportConfiguration] = []) { - self.loadMethod = .uri(uri) - self.importConfigurations = importConfigurations - } - - /// [Mapbox Standard](https://www.mapbox.com/blog/standard-core-style) is a general-purpose style with 3D visualization. + /// - configuration: Style import configuration to be applied on style load. + /// Providing `nil` configuration will make no effect and previous configuration will stay in place. In order to change previous value, you should explicitly override it with the new value. @_documentation(visibility: public) - public static var standard: MapStyle { - MapStyle(uri: .standard) + public init(uri: StyleURI, configuration: JSONObject? = nil) { + self.data = .uri(uri) + self.configuration = configuration } /// [Mapbox Standard](https://www.mapbox.com/blog/standard-core-style) is a general-purpose style with 3D visualization. - /// - /// When the returned ``MapStyle`` is set to the map, the Standard Style will be loaded, and specified import configurations will be applied to the basemap. - /// - /// - Parameters: - /// - lightPreset: Switches between 4 time-of-day states: ``StandardLightPreset/dusk``, ``StandardLightPreset/dawn``, ``StandardLightPreset/day``, and ``StandardLightPreset/night``. By default, the Day preset is applied. - /// - font: Defines font family for the style from predefined options. The possible options are `Alegreya`, `Alegreya SC`, `Asap`, `Barlow`, `DIN Pro`, `EB Garamond`, `Faustina`, `Frank Ruhl Libre`, `Heebo`, `Inter`, `League Mono`, `Montserrat`, `Poppins`, `Raleway`, `Roboto`, `Roboto Mono`, `Rubik`, `Source`, `Code Pro`, `Spectral`, `Ubuntu`, `Noto Sans CJK JP`, `Open Sans`, `Manrope`, `Source Sans Pro`, `Lato`. - /// - showPointOfInterestLabels: Shows or hides all POI icons and text. Default value is `true`. - /// - showTransitLabels: Shows or hides all transit icons and text. Default value is `true`. - /// - showPlaceLabels: Shows and hides place label layers, such as house numbers. Default value is `true`. - /// - showRoadLabels: Shows and hides all road labels, including road shields. Default value is `true`. @_documentation(visibility: public) - public static func standard( - lightPreset: StandardLightPreset?, - font: String? = nil, - showPointOfInterestLabels: Bool? = nil, - showTransitLabels: Bool? = nil, - showPlaceLabels: Bool? = nil, - showRoadLabels: Bool? = nil - ) -> MapStyle { - MapStyle(uri: .standard, importConfigurations: [ - .standard(importId: "basemap", // Import configuration will be applied to the root style. - lightPreset: lightPreset, - font: font, - showPointOfInterestLabels: showPointOfInterestLabels, - showTransitLabels: showTransitLabels, - showPlaceLabels: showPlaceLabels, - showRoadLabels: showRoadLabels) - ]) - } + public static var standard: MapStyle { MapStyle(uri: .standard) } /// [Mapbox Streets](https://www.mapbox.com/maps/streets) is a general-purpose style with detailed road and transit networks. @_documentation(visibility: public) @@ -154,4 +126,71 @@ public struct MapStyle { /// and translucent roads from Mapbox Streets. @_documentation(visibility: public) public static var satelliteStreets: MapStyle { MapStyle(uri: .satelliteStreets) } + + /// Empty map style. Allows to load map without any predefined sources or layers. + /// Allows to construct the whole style in runtime by composition of `StyleImport`. + @_documentation(visibility: public) + public static var empty: MapStyle { MapStyle(json: "{ \"layers\": [], \"sources\": {} }") } + + /// [Mapbox Standard](https://www.mapbox.com/blog/standard-core-style) is a general-purpose style with 3D visualization. + /// + /// When the returned ``MapStyle`` is set to the map, the Standard Style will be loaded, and specified import configurations will be applied to the basemap. + /// + /// - Parameters: + /// - lightPreset: Switches between 4 time-of-day states: ``StandardLightPreset/dusk``, ``StandardLightPreset/dawn``, ``StandardLightPreset/day``, and ``StandardLightPreset/night``. By default, the Day preset is applied. + /// - font: Defines font family for the style from predefined options. The possible options are `Alegreya`, `Alegreya SC`, `Asap`, `Barlow`, `DIN Pro`, `EB Garamond`, `Faustina`, `Frank Ruhl Libre`, `Heebo`, `Inter`, `League Mono`, `Montserrat`, `Poppins`, `Raleway`, `Roboto`, `Roboto Mono`, `Rubik`, `Source`, `Code Pro`, `Spectral`, `Ubuntu`, `Noto Sans CJK JP`, `Open Sans`, `Manrope`, `Source Sans Pro`, `Lato`. + /// - showPointOfInterestLabels: Shows or hides all POI icons and text. Default value is `true`. + /// - showTransitLabels: Shows or hides all transit icons and text. Default value is `true`. + /// - showPlaceLabels: Shows and hides place label layers, such as house numbers. Default value is `true`. + /// - showRoadLabels: Shows and hides all road labels, including road shields. Default value is `true`. + @_documentation(visibility: public) + public static func standard( + lightPreset: StandardLightPreset?, + font: String? = nil, + showPointOfInterestLabels: Bool? = nil, + showTransitLabels: Bool? = nil, + showPlaceLabels: Bool? = nil, + showRoadLabels: Bool? = nil + ) -> MapStyle { + MapStyle(uri: .standard, configuration: standardConfiguration( + lightPreset: lightPreset, + font: font, + showPointOfInterestLabels: showPointOfInterestLabels, + showTransitLabels: showTransitLabels, + showPlaceLabels: showPlaceLabels, + showRoadLabels: showRoadLabels) + ) + } + + private static func standardConfiguration( + lightPreset: StandardLightPreset?, + font: String? = nil, + showPointOfInterestLabels: Bool? = nil, + showTransitLabels: Bool? = nil, + showPlaceLabels: Bool? = nil, + showRoadLabels: Bool? = nil + ) -> JSONObject { + var config = JSONObject() + + if let lightPreset { + config["lightPreset"] = .string(lightPreset.rawValue) + } + if let font { + config["font"] = .string(font) + } + if let showPointOfInterestLabels { + config["showPointOfInterestLabels"] = .boolean(showPointOfInterestLabels) + } + if let showTransitLabels { + config["showTransitLabels"] = .boolean(showTransitLabels) + } + if let showPlaceLabels { + config["showPlaceLabels"] = .boolean(showPlaceLabels) + } + if let showRoadLabels { + config["showRoadLabels"] = .boolean(showRoadLabels) + } + + return config + } } diff --git a/Sources/MapboxMaps/Style/StandardLightPreset.swift b/Sources/MapboxMaps/Style/StandardLightPreset.swift new file mode 100644 index 00000000000..3b35c992d2d --- /dev/null +++ b/Sources/MapboxMaps/Style/StandardLightPreset.swift @@ -0,0 +1,28 @@ +/// Defines the available light presets in the Mapbox Standard Style. +@_documentation(visibility: public) +@_spi(Experimental) +public struct StandardLightPreset: RawRepresentable, Hashable { + @_documentation(visibility: public) + public let rawValue: String + + @_documentation(visibility: public) + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// Day light preset. + @_documentation(visibility: public) + public static let day = StandardLightPreset(rawValue: "day") + + /// Night light preset. + @_documentation(visibility: public) + public static let night = StandardLightPreset(rawValue: "night") + + /// Dusk light preset. + @_documentation(visibility: public) + public static let dusk = StandardLightPreset(rawValue: "dusk") + + /// Dawn light preset. + @_documentation(visibility: public) + public static let dawn = StandardLightPreset(rawValue: "dawn") +} diff --git a/Sources/MapboxMaps/Style/StyleImport.swift b/Sources/MapboxMaps/Style/StyleImport.swift new file mode 100644 index 00000000000..85f4078b0bc --- /dev/null +++ b/Sources/MapboxMaps/Style/StyleImport.swift @@ -0,0 +1,84 @@ +/// Map style import. +/// +/// Imports can be used to add the contents of other styles to the current style. Instead of copying the individual layers, only the other style URL is referenced. +/// +/// ``StyleImport`` is intended to be used as part of . +/// +/// It's possible to put style import above the style loaded by default. +/// ```swift +/// let mapView = MapView() +/// +/// mapView.mapboxMap.setMapStyleContent { +/// StyleImport(uri: StyleURI(url: "custom_style_fragment")!) +/// } +/// ``` +/// +/// Or you may explicitly specify empty base style and fully configure the style of the map by composing style imports. +/// ```swift +/// let mapView = MapView() +/// +/// mapView.mapboxMap.mapStyle = .empty +/// +/// mapView.mapboxMap.setMapStyleContent { +/// StyleImport(style: .standard(lightPreset: .dusk)) +/// +/// StyleImport(uri: StyleURI(url: "mapbox://custom_style_fragment")!) +/// } +/// ``` +/// - Important: ``StyleImport`` encapsulates all implementation details of the style. +/// Therefore, any layers defined inside the import won't be accessible from API. More information in [v11 Migration Guide](https://docs.mapbox.com/ios/maps/guides/migrate-to-v11/#211-style-imports) +/// +/// More information [Mapbox Style Specification](https://docs.mapbox.com/style-spec/reference/imports) +@_documentation(visibility: public) +@_spi(Experimental) +public struct StyleImport { + let id: String? + let style: MapStyle + + /// Creates a ``StyleImport`` using a Mapbox Style JSON. + /// + /// - Important: For the better performance with large local Style JSON please consider loading style from the file system via the ``StyleImport/init(id:uri:configuration:)`` initializer. + /// + /// - Parameters: + /// - id: Import id string, will be automatically generated if not explicitly specified. + /// - json: A Mapbox Style JSON string. + /// - configuration: Style import configurations to be applied on style load. + @_documentation(visibility: public) + public init( + id: String? = nil, + json: String, + configuration: JSONObject? = nil + ) { + self.id = id + self.style = MapStyle(json: json, configuration: configuration) + } + + /// Creates a ``StyleImport`` using``StyleURI`` + /// + /// Use this initializer to make use of pre-defined Mapbox Styles, or load a custom style bundled with the application, or over the network. + /// + /// - Parameters: + /// - id: Import id string, will be automatically generated if not explicitly specified. + /// - uri: An instance of ``StyleURI`` pointing to a Mapbox Style URI (mapbox://styles/{user}/{style}), a full HTTPS URI, or a path to a local file. + /// - configuration: Style import configuration to be applied on style load. + @_documentation(visibility: public) + public init( + id: String? = nil, + uri: StyleURI, + configuration: JSONObject? = nil + ) { + self.id = id + self.style = MapStyle(uri: uri, configuration: configuration) + } + + /// Creates a ``StyleImport`` using ``MapStyle``. + /// + /// - Parameters: + /// - id: Import id string, will be automatically generated if not explicitly specified. + /// - style: ``MapStyle`` instance containing the URI to style or JSON comforming to [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/). + @_documentation(visibility: public) + public init(id: String? = nil, style: MapStyle) { + self.id = id + self.style = style + } +} diff --git a/Sources/MapboxMaps/Style/StyleImportConfiguration.swift b/Sources/MapboxMaps/Style/StyleImportConfiguration.swift deleted file mode 100644 index 286699cf774..00000000000 --- a/Sources/MapboxMaps/Style/StyleImportConfiguration.swift +++ /dev/null @@ -1,85 +0,0 @@ -/// Defines the available light presets in the Mapbox Standard Style. - @_documentation(visibility: public) -@_spi(Experimental) -public struct StandardLightPreset: RawRepresentable, Hashable { - @_documentation(visibility: public) - public let rawValue: String - - @_documentation(visibility: public) - public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Day light preset. - @_documentation(visibility: public) - public static let day = StandardLightPreset(rawValue: "day") - - /// Night light preset. - @_documentation(visibility: public) - public static let night = StandardLightPreset(rawValue: "night") - - /// Dusk light preset. - @_documentation(visibility: public) - public static let dusk = StandardLightPreset(rawValue: "dusk") - - /// Dawn light preset. - @_documentation(visibility: public) - public static let dawn = StandardLightPreset(rawValue: "dawn") -} - -/// Specifies configuration parameters for style imports. - @_documentation(visibility: public) -@_spi(Experimental) -public struct StyleImportConfiguration: Equatable { - /// Style import identifier. - @_documentation(visibility: public) - public var importId: String - - /// JSON dictionary of parameters. - @_documentation(visibility: public) - public var config: JSONObject - - /// Creates a configuration. - /// - Parameters: - /// - importId: A style import id to which the configuration will be applied. If not specified, the import configuration will be applied to `basemap` style import (root style). - /// - config: Style import configuration parameters. - @_documentation(visibility: public) - public init(importId: String?, config: JSONObject) { - self.importId = importId ?? "basemap" - self.config = config - } - - /// Creates a configuration for the Mapbox Standard Style. - @_documentation(visibility: public) - public static func standard( - importId: String?, - lightPreset: StandardLightPreset? = nil, - font: String? = nil, - showPointOfInterestLabels: Bool? = nil, - showTransitLabels: Bool? = nil, - showPlaceLabels: Bool? = nil, - showRoadLabels: Bool? = nil - ) -> StyleImportConfiguration { - var config = JSONObject() - if let lightPreset { - config["lightPreset"] = .string(lightPreset.rawValue) - } - if let font { - config["font"] = .string(font) - } - if let showPointOfInterestLabels { - config["showPointOfInterestLabels"] = .boolean(showPointOfInterestLabels) - } - if let showTransitLabels { - config["showTransitLabels"] = .boolean(showTransitLabels) - } - if let showPlaceLabels { - config["showPlaceLabels"] = .boolean(showPlaceLabels) - } - if let showRoadLabels { - config["showRoadLabels"] = .boolean(showRoadLabels) - } - - return StyleImportConfiguration(importId: importId, config: config) - } -} diff --git a/Sources/MapboxMaps/Style/StyleManager.swift b/Sources/MapboxMaps/Style/StyleManager.swift index a0fd3f8201e..b95658940fd 100644 --- a/Sources/MapboxMaps/Style/StyleManager.swift +++ b/Sources/MapboxMaps/Style/StyleManager.swift @@ -568,19 +568,6 @@ public class StyleManager { return styleManager.getStyleImports() } - /// Removes an existing style import. - /// - /// - Parameters: - /// - importId: Identifier of the style import to remove. - /// - /// - Throws: - /// - An error describing why the operation was unsuccessful. - public func removeStyleImport(for importId: String) throws { - try handleExpected { - styleManager.removeStyleImport(forImportId: importId) - } - } - /// Gets the style import schema. /// /// - Parameters: @@ -781,11 +768,16 @@ public class StyleManager { /// The ordered list of the current style layers' identifiers and types public var allLayerIdentifiers: [LayerInfo] { - return styleManager.getStyleLayers().map { info in + styleManager.getStyleLayers().map { info in LayerInfo(id: info.id, type: LayerType(rawValue: info.type)) } } + /// The ordered list of the current style imports' identifiers + var allImportIdentifiers: [String] { + styleManager.getStyleImports().map(\.id) + } + // MARK: - Layer Properties /// Gets the value of style layer property. @@ -1481,6 +1473,114 @@ public class StyleManager { return styleManager.invalidateStyleCustomRasterSourceRegion(forSourceId: sourceId, bounds: bounds) } } + + /// Adds style import with specified id using pre-defined Mapbox Style, or custom style bundled with the application, or over the network. + /// + /// - Parameters: + /// - id: Identifier of the style import to move + /// - uri: An instance of ``StyleURI`` pointing to a Mapbox Style URI (mapbox://styles/{user}/{style}), a full HTTPS URI, or a path to a local file. + /// - config: Style import configuration to be applied on style load. + /// - importPosition: Position at which import will be added in the imports stack. By default it will be added above everything. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func addStyleImport(withId id: String, uri: StyleURI, config: [String: Any]? = nil, importPosition: ImportPosition? = nil) throws { + try handleExpected { + return styleManager.addStyleImportFromURI(forImportId: id, uri: uri.rawValue, config: config, importPosition: importPosition?.corePosition) + } + } + + /// Adds style import with specified id using style JSON string and configuration. + /// + /// - Parameters: + /// - id: Identifier of the style import to move + /// - json: Style JSON conforming to [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/). + /// - config: Style import configuration to be applied on style load. + /// - importPosition: Position at which import will be added in the imports stack. By default it will be added above everything. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func addStyleImport(withId id: String, json: String, config: [String: Any]? = nil, importPosition: ImportPosition? = nil) throws { + try handleExpected { + return styleManager.addStyleImportFromJSON(forImportId: id, json: json, config: config, importPosition: importPosition?.corePosition) + } + } + + /// Updates style import with specified id using pre-defined Mapbox Style, or custom style bundled with the application, or over the network. + /// + /// - Important: For performance reasons, if you only need to update only configuration, use ``StyleManager/setStyleImportConfigProperties(for:configs:)`` or ``StyleManager/setStyleImportConfigProperty(for:config:value:)``` + /// + /// - Parameters: + /// - id: Identifier of the style import to move + /// - uri: An instance of ``StyleURI`` pointing to a Mapbox Style URI (mapbox://styles/{user}/{style}), a full HTTPS URI, or a path to a local file. + /// - config: Style import configuration to be applied on style load. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func updateStyleImport(withId id: String, uri: StyleURI, config: [String: Any]? = nil) throws { + try handleExpected { + return styleManager.updateStyleImportWithURI(forImportId: id, uri: uri.rawValue, config: config) + } + } + + /// Updates style import with specified id using style JSON string and configuration. + /// + /// - Important: For performance reasons, if you only need to update only configuration, use ``StyleManager/setStyleImportConfigProperties(for:configs:)`` or ``StyleManager/setStyleImportConfigProperty(for:config:value:)``` + /// + /// - Parameters: + /// - id: Identifier of the style import to move + /// - json: Style JSON conforming to [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/). + /// - config: Style import configuration to be applied on style load. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func updateStyleImport(withId id: String, json: String, config: [String: Any]? = nil) throws { + try handleExpected { + return styleManager.updateStyleImportWithJSON(forImportId: id, json: json, config: config) + } + } + + /// Move an existing style import to specified position in imports stack. + /// + /// - Parameters: + /// - id: Identifier of the style import to move + /// - position: Position in the imports stack. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func moveStyleImport(withId id: String, to position: ImportPosition) throws { + try handleExpected { + return styleManager.moveStyleImport(forImportId: id, importPosition: position.corePosition) + } + } + + /// Removes an existing style import. + /// + /// - Parameters: + /// - id: Identifier of the style import to remove. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + public func removeStyleImport(withId id: String) throws { + try handleExpected { + return styleManager.removeStyleImport(forImportId: id) + } + } + + /// Removes an existing style import. + /// + /// - Parameters: + /// - importId: Identifier of the style import to remove. + /// + /// - Throws: + /// - An error describing why the operation was unsuccessful. + @available(*, deprecated, renamed: "removeStyleImport(withId:)", message: "Please use the removeStyleImport(withId:) version.") + public func removeStyleImport(for importId: String) throws { + try handleExpected { + styleManager.removeStyleImport(forImportId: importId) + } + } + } extension StyleManagerProtocol { diff --git a/Sources/MapboxMaps/Style/StyleManagerProtocol.swift b/Sources/MapboxMaps/Style/StyleManagerProtocol.swift index f041c531e90..86a4fac6cb7 100644 --- a/Sources/MapboxMaps/Style/StyleManagerProtocol.swift +++ b/Sources/MapboxMaps/Style/StyleManagerProtocol.swift @@ -27,8 +27,6 @@ internal protocol StyleManagerProtocol { func getStyleTransition() -> MapboxCoreMaps.TransitionOptions func setStyleTransitionFor(_ transitionOptions: MapboxCoreMaps.TransitionOptions) - func getStyleImports() -> [StyleObjectInfo] - func removeStyleImport(forImportId importId: String) -> Expected func getStyleImportSchema(forImportId importId: String) -> Expected func getStyleImportConfigProperties(forImportId importId: String) -> Expected func getStyleImportConfigProperty( @@ -37,6 +35,14 @@ internal protocol StyleManagerProtocol { func setStyleImportConfigPropertiesForImportId(_ importId: String, configs: [String: Any]) -> Expected func setStyleImportConfigPropertyForImportId(_ importId: String, config: String, value: Any) -> Expected + func getStyleImports() -> [StyleObjectInfo] + func addStyleImportFromJSON(forImportId: String, json: String, config: [String: Any]?, importPosition: CoreImportPosition?) -> Expected + func addStyleImportFromURI(forImportId: String, uri: String, config: [String: Any]?, importPosition: CoreImportPosition?) -> Expected + func updateStyleImportWithURI(forImportId: String, uri: String, config: [String: Any]?) -> Expected + func updateStyleImportWithJSON(forImportId: String, json: String, config: [String: Any]?) -> Expected + func moveStyleImport(forImportId: String, importPosition: CoreImportPosition?) -> Expected + func removeStyleImport(forImportId importId: String) -> Expected + func styleLayerExists(forLayerId layerId: String) -> Bool func getStyleLayers() -> [MapboxCoreMaps.StyleObjectInfo] diff --git a/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift b/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift index 6abbbd3ee59..494620d9a39 100644 --- a/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift +++ b/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift @@ -699,6 +699,84 @@ class MockStyleManager: StyleManagerProtocol { func removeGeoJSONSourceFeatures(forSourceId sourceId: String, dataId: String, featureIds: [String]) -> Expected { removeGeoJSONSourceFeaturesStub.call(with: .init(sourceId: sourceId, featureIds: featureIds, dataId: dataId)) } + + // MARK: - Import + + struct AddStyleImportFromJSONParams { + let forImportId: String + let json: String, config: [String: Any]? + let importPosition: MapboxMaps.CoreImportPosition? + } + let addStyleImportFromJSONStub = Stub>( + defaultReturnValue: .init(value: NSNull())) + + func addStyleImportFromJSON( + forImportId: String, + json: String, config: [String: Any]?, + importPosition: MapboxMaps.CoreImportPosition? + ) -> Expected { + addStyleImportFromJSONStub.call(with: AddStyleImportFromJSONParams(forImportId: forImportId, json: json, config: config, importPosition: importPosition)) + } + + struct AddStyleImportFromURIParams { + let forImportId: String + let uri: String + let importPosition: MapboxMaps.CoreImportPosition? + } + let addStyleImportFromURIStub = Stub>( + defaultReturnValue: .init(value: NSNull())) + + func addStyleImportFromURI( + forImportId: String, + uri: String, + config: [String: Any]?, + importPosition: MapboxMaps.CoreImportPosition? + ) -> Expected { + addStyleImportFromURIStub.call(with: AddStyleImportFromURIParams(forImportId: forImportId, uri: uri, importPosition: importPosition)) + } + + struct UpdateStyleImportWithURIParams { + let forImportId: String + let uri: String + let config: [String: Any]? + } + let updateStyleImportWithURIStub = Stub>( + defaultReturnValue: .init(value: NSNull())) + + func updateStyleImportWithURI( + forImportId: String, + uri: String, + config: [String: Any]? + ) -> Expected { + updateStyleImportWithURIStub.call(with: UpdateStyleImportWithURIParams(forImportId: forImportId, uri: uri, config: config)) + } + + struct UpdateStyleImportWithJSONParams { + let forImportId: String + let json: String + let config: [String: Any]? + } + let updateStyleImportWithJSONStub = Stub>( + defaultReturnValue: .init(value: NSNull())) + + func updateStyleImportWithJSON( + forImportId: String, + json: String, + config: [String: Any]? + ) -> Expected { + updateStyleImportWithJSONStub.call(with: UpdateStyleImportWithJSONParams(forImportId: forImportId, json: json, config: config)) + } + + struct MoveStyleImportParams { + let forImportId: String + let importPosition: MapboxMaps.CoreImportPosition? + } + let moveStyleImportStub = Stub>( + defaultReturnValue: .init(value: NSNull())) + + func moveStyleImport(forImportId: String, importPosition: MapboxMaps.CoreImportPosition?) -> Expected { + moveStyleImportStub.call(with: MoveStyleImportParams(forImportId: forImportId, importPosition: importPosition)) + } } struct NonEncodableLayer: Layer { diff --git a/Tests/MapboxMapsTests/Foundation/Style/MapStyleReconcilerTests.swift b/Tests/MapboxMapsTests/Foundation/Style/MapStyleReconcilerTests.swift index 854c90936c8..40334ff55c2 100644 --- a/Tests/MapboxMapsTests/Foundation/Style/MapStyleReconcilerTests.swift +++ b/Tests/MapboxMapsTests/Foundation/Style/MapStyleReconcilerTests.swift @@ -56,11 +56,7 @@ final class MapStyleReconcilerTests: XCTestCase { let json = """ {"foo": "bar"} """ - me.mapStyle = .init(json: json, importConfigurations: [ - .init(importId: "foo", config: [ - "bar": "baz" - ]) - ]) + me.mapStyle = .init(json: json, configuration: JSONObject(rawValue: ["bar": "baz"])!) XCTAssertEqual(styleManager.setStyleJSONStub.invocations.count, 1) let params = try XCTUnwrap(styleManager.setStyleJSONStub.invocations.last).parameters @@ -73,7 +69,7 @@ final class MapStyleReconcilerTests: XCTestCase { let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") XCTAssertEqual(inv.last?.parameters.config, "bar") XCTAssertEqual(inv.last?.parameters.value as? String, "baz") } @@ -83,11 +79,7 @@ final class MapStyleReconcilerTests: XCTestCase { self.styleManager.isStyleLoadedStub.defaultReturnValue = false } - me.mapStyle = .init(uri: .streets, importConfigurations: [ - .init(importId: "foo", config: [ - "bar": "baz" - ]) - ]) + me.mapStyle = .init(uri: .streets, configuration: JSONObject(rawValue: ["bar": "baz"])!) XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1) let params = try XCTUnwrap(styleManager.setStyleURIStub.invocations.last).parameters @@ -100,7 +92,7 @@ final class MapStyleReconcilerTests: XCTestCase { let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") XCTAssertEqual(inv.last?.parameters.config, "bar") XCTAssertEqual(inv.last?.parameters.value as? String, "baz") } @@ -115,12 +107,8 @@ final class MapStyleReconcilerTests: XCTestCase { callbacks = invoc.parameters.callbacks } - me.mapStyle = MapStyle(uri: .outdoors, importConfigurations: [ - .init(importId: "foo-1", config: ["k-1": "v-1", "a": "b"]) - ]) - me.mapStyle = MapStyle(uri: .streets, importConfigurations: [ - .init(importId: "foo-2", config: ["k-2": "v-2"]) - ]) + me.mapStyle = .init(uri: .outdoors, configuration: JSONObject(rawValue: ["k-1": "v-1", "a": "b"])!) + me.mapStyle = .init(uri: .streets, configuration: JSONObject(rawValue: ["k-2": "v-2"])!) XCTAssertEqual(styleManager.setStyleURIStub.invocations.map(\.parameters.value), [ StyleURI.outdoors.rawValue, @@ -133,7 +121,7 @@ final class MapStyleReconcilerTests: XCTestCase { // the first style update is skipped. let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo-2") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") XCTAssertEqual(inv.last?.parameters.config, "k-2") XCTAssertEqual(inv.last?.parameters.value as? String, "v-2") } @@ -204,13 +192,9 @@ final class MapStyleReconcilerTests: XCTestCase { self.simulateLoad(callbacks: invoc.parameters.callbacks, result: .success) self.styleManager.setStyleImportConfigPropertyForImportIdStub.reset() } - me.mapStyle = MapStyle(uri: .outdoors, importConfigurations: [ - .init(importId: "foo-1", config: ["k-1": "v-1", "a": "b"]) - ]) + me.mapStyle = MapStyle(uri: .outdoors, configuration: JSONObject(rawValue: ["k-1": "v-1", "a": "b"])!) - let s2 = MapStyle(uri: .outdoors, importConfigurations: [ - .init(importId: "foo-1", config: ["k-2": "v-2"]) - ]) + let s2 = MapStyle(uri: .outdoors, configuration: JSONObject(rawValue: ["k-2": "v-2"])!) var count = 0 me.loadStyle(s2, transition: nil) { error in @@ -221,53 +205,41 @@ final class MapStyleReconcilerTests: XCTestCase { let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo-1") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") XCTAssertEqual(inv.last?.parameters.config, "k-2") XCTAssertEqual(inv.last?.parameters.value as? String, "v-2") } func testStyleImportsReconcileFromNil() { - MapStyleReconciler.reconcileStyleImports( + MapStyleReconciler.reconcileBasemapConfiguration( from: nil, - to: [ - StyleImportConfiguration( - importId: "foo", - config: ["bar": "baz"]) - ], + to: .init(rawValue: ["bar": "baz"])!, styleManager: styleManager) let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") XCTAssertEqual(inv.last?.parameters.config, "bar") XCTAssertEqual(inv.last?.parameters.value as? String, "baz") } func testStyleImportsReconcilePartialUpdate() { - MapStyleReconciler.reconcileStyleImports( - from: [ - StyleImportConfiguration( - importId: "foo", - config: ["bar": "baz"]), - StyleImportConfiguration( - importId: "x", - config: ["y": "z"]) - ], - to: [ - StyleImportConfiguration( - importId: "foo", - config: [ - "bar": "baz", - "qux": "quux" - ]) - ], + MapStyleReconciler.reconcileBasemapConfiguration( + from: JSONObject(rawValue: [ + "bar": "baz", + "x": "y" + ])!, + to: JSONObject(rawValue: [ + "bar": "foo", + "x": "y" + ])!, styleManager: styleManager) let inv = styleManager.setStyleImportConfigPropertyForImportIdStub.invocations XCTAssertEqual(inv.count, 1) - XCTAssertEqual(inv.last?.parameters.importId, "foo") - XCTAssertEqual(inv.last?.parameters.config, "qux") - XCTAssertEqual(inv.last?.parameters.value as? String, "quux") + XCTAssertEqual(inv.last?.parameters.importId, "basemap") + XCTAssertEqual(inv.last?.parameters.config, "bar") + XCTAssertEqual(inv.last?.parameters.value as? String, "foo") } func testIsStyleRootLoaded() { diff --git a/Tests/MapboxMapsTests/Integration Tests/Style/StyleDSLIntegrationTests.swift b/Tests/MapboxMapsTests/Integration Tests/Style/StyleDSLIntegrationTests.swift index 53c0423ca77..d3d9b491b5c 100644 --- a/Tests/MapboxMapsTests/Integration Tests/Style/StyleDSLIntegrationTests.swift +++ b/Tests/MapboxMapsTests/Integration Tests/Style/StyleDSLIntegrationTests.swift @@ -470,4 +470,53 @@ internal class StyleDSLIntegrationTests: MapViewIntegrationTestCase { wait(for: [expectation], timeout: 5) } + + func testStyleImports() { + let expectation = self.expectation(description: "Wait for mapStyle to load") + expectation.expectedFulfillmentCount = 1 + var showFirst = false + + mapView.mapboxMap.mapStyle = .standard + mapView.mapboxMap.setMapStyleContent { + FillLayer(id: "first", source: "test-source") + if showFirst { + StyleImport(id: "import1", json: .emptyStyle) + } else { + StyleImport(id: "import2", json: .emptyStyle) + } + LineLayer(id: "second", source: "test-source") + StyleImport(id: "import3", json: .emptyStyle) + } + + didFinishLoadingStyle = { mapView in + XCTAssertEqual(mapView.mapboxMap.allLayerIdentifiers.map(\.id), ["first", "second"]) + XCTAssertEqual(mapView.mapboxMap.allImportIdentifiers, ["basemap", "import2", "import3"]) + expectation.fulfill() + } + + showFirst = true + + mapView.mapboxMap.setMapStyleContent { + FillLayer(id: "first", source: "test-source") + StyleImport(id: "import3", json: .emptyStyle) + if showFirst { + StyleImport(id: "import1", json: .emptyStyle) + } else { + StyleImport(id: "import2", json: .emptyStyle) + } + LineLayer(id: "second", source: "test-source") + } + + didFinishLoadingStyle = { mapView in + XCTAssertEqual(mapView.mapboxMap.allLayerIdentifiers.map(\.id), ["first", "second"]) + XCTAssertEqual(mapView.mapboxMap.allImportIdentifiers, ["basemap", "import3", "import1"]) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5) + } +} + +private extension String { + static let emptyStyle = "{ \"layers\": [], \"sources\": {} }" } diff --git a/Tests/MapboxMapsTests/SwiftUI/MapBasicCoordinatorTests.swift b/Tests/MapboxMapsTests/SwiftUI/MapBasicCoordinatorTests.swift index 765cb5fad27..96d5e648a9b 100644 --- a/Tests/MapboxMapsTests/SwiftUI/MapBasicCoordinatorTests.swift +++ b/Tests/MapboxMapsTests/SwiftUI/MapBasicCoordinatorTests.swift @@ -32,16 +32,16 @@ final class MapBasicCoordinatorTests: XCTestCase { func testStyleURI() { update(with: MapDependencies(mapStyle: .light)) - var loadMethod = mapView.style.mapStyle?.loadMethod - if case let .uri(styleURI) = loadMethod { + var styleData = mapView.style.mapStyle?.data + if case let .uri(styleURI) = styleData { XCTAssertEqual(styleURI.rawValue, "mapbox://styles/mapbox/light-v11") } else { XCTFail("Failed to update mapStyle") } update(with: MapDependencies(mapStyle: .dark)) - loadMethod = mapView.style.mapStyle?.loadMethod - if case let .uri(styleURI) = loadMethod { + styleData = mapView.style.mapStyle?.data + if case let .uri(styleURI) = styleData { XCTAssertEqual(styleURI.rawValue, "mapbox://styles/mapbox/dark-v11") } else { XCTFail("Failed to update mapStyle") @@ -260,7 +260,13 @@ extension PerformanceStatistics { PerformanceStatistics( collectionDurationMillis: 1000, mapRenderDurationStatistics: DurationStatistics(maxMillis: 0, medianMillis: 0), - cumulativeStatistics: CumulativeRenderingStatistics(drawCalls: nil, textureBytes: nil, vertexBytes: nil), + cumulativeStatistics: CumulativeRenderingStatistics( + drawCalls: nil, + textureBytes: nil, + vertexBytes: nil, + graphicsPrograms: nil, + graphicsProgramsCreationTimeMillis: nil + ), perFrameStatistics: PerFrameRenderingStatistics( topRenderGroups: [], topRenderLayers: [], diff --git a/scripts/api-compatibility-check/breakage_allowlist.txt b/scripts/api-compatibility-check/breakage_allowlist.txt index 9b81231d43e..854b4a108b5 100644 --- a/scripts/api-compatibility-check/breakage_allowlist.txt +++ b/scripts/api-compatibility-check/breakage_allowlist.txt @@ -1374,4 +1374,21 @@ Func MapContentBuilder.buildEither(second:) has return type change from MapboxMa Func MapContentBuilder.buildOptional(_:) has parameter 0 type change from (MapboxMaps.MapContent)? to T? Func MapContentBuilder.buildOptional(_:) has return type change from MapboxMaps.MapContent to MapboxMaps.OptionalMapContent AssociatedType MapContent.Body has been added as a protocol requirement -Var MapContent.body has been added as a protocol requirement \ No newline at end of file +Var MapContent.body has been added as a protocol requirement + +// Style Import API +Constructor StyleImportConfiguration.init(importId:config:) has been removed +Constructor MapStyle.init(json:importConfigurations:) has been removed +Constructor MapStyle.init(uri:importConfigurations:) has been removed +Func StyleImportConfiguration.==(_:_:) has been removed +Var StyleImportConfiguration.config has been removed +Var StyleImportConfiguration.importId has been removed +Func StyleImportConfiguration.standard(importId:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) has been renamed to Func standard(lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) +Constructor MapStyle.init(json:importConfigurations:) has parameter 1 type change from [MapboxMaps.StyleImportConfiguration] to [Swift.String : MapboxMaps.StyleImportConfiguration] +Constructor MapStyle.init(uri:importConfigurations:) has parameter 1 type change from [MapboxMaps.StyleImportConfiguration] to [Swift.String : MapboxMaps.StyleImportConfiguration] +Func StyleImportConfiguration.standard(importId:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) has parameter 0 type change from Swift.String? to MapboxMaps.StandardLightPreset? +Func StyleImportConfiguration.standard(importId:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) has parameter 1 type change from MapboxMaps.StandardLightPreset? to Swift.String? +Func StyleImportConfiguration.standard(importId:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) has parameter 2 type change from Swift.String? to Swift.Bool? +Func StyleImportConfiguration.standard(importId:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:) has been removed +Struct StyleImportConfiguration has removed conformance to Equatable +Struct StyleImportConfiguration has been renamed to Struct StyleImport \ No newline at end of file diff --git a/scripts/release/packager/versions.json b/scripts/release/packager/versions.json index c63d8b43992..d5a539f8437 100644 --- a/scripts/release/packager/versions.json +++ b/scripts/release/packager/versions.json @@ -1,5 +1,5 @@ { - "MapboxCoreMaps": "11.3.0", + "MapboxCoreMaps": "11.4.0-SNAPSHOT.0417T1228Z.b5204fc", "MapboxCommon": "24.3.1", "Turf": "2.8.0" }