diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift b/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift index 02d49fa..040065e 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift @@ -9,6 +9,7 @@ import MapLibreSwiftMacros @MLNStyleProperty("strokeColor", supportsInterpolation: false) public struct CircleStyleLayer: SourceBoundVectorStyleLayerDefinition { public let identifier: String + public let sourceLayerIdentifier: String? public var insertionPosition: LayerInsertionPosition = .aboveOthers public var isVisible: Bool = true public var maximumZoomLevel: Float? = nil @@ -20,11 +21,13 @@ public struct CircleStyleLayer: SourceBoundVectorStyleLayerDefinition { public init(identifier: String, source: Source) { self.identifier = identifier self.source = .source(source) + sourceLayerIdentifier = nil } - public init(identifier: String, source: MLNSource) { + public init(identifier: String, source: MLNSource, sourceLayerIdentifier: String? = nil) { self.identifier = identifier self.source = .mglSource(source) + self.sourceLayerIdentifier = sourceLayerIdentifier } public func makeStyleLayer(style: MLNStyle) -> StyleLayer { @@ -69,6 +72,7 @@ private struct CircleStyleLayerInternal: StyleLayer { public func makeMLNStyleLayer() -> MLNStyleLayer { let result = MLNCircleStyleLayer(identifier: identifier, source: mglSource) + result.sourceLayerIdentifier = definition.sourceLayerIdentifier result.circleRadius = definition.radius result.circleColor = definition.color diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Line.swift b/Sources/MapLibreSwiftDSL/Style Layers/Line.swift index c6d65cb..1c10508 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Line.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Line.swift @@ -10,6 +10,7 @@ import MapLibreSwiftMacros @MLNStyleProperty("lineWidth", supportsInterpolation: true) public struct LineStyleLayer: SourceBoundVectorStyleLayerDefinition { public let identifier: String + public let sourceLayerIdentifier: String? public var insertionPosition: LayerInsertionPosition = .aboveOthers public var isVisible: Bool = true public var maximumZoomLevel: Float? = nil @@ -21,11 +22,13 @@ public struct LineStyleLayer: SourceBoundVectorStyleLayerDefinition { public init(identifier: String, source: Source) { self.identifier = identifier self.source = .source(source) + sourceLayerIdentifier = nil } - public init(identifier: String, source: MLNSource) { + public init(identifier: String, source: MLNSource, sourceLayerIdentifier: String? = nil) { self.identifier = identifier self.source = .mglSource(source) + self.sourceLayerIdentifier = sourceLayerIdentifier } public func makeStyleLayer(style: MLNStyle) -> StyleLayer { diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift b/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift index 814f754..435d562 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift @@ -75,6 +75,8 @@ public protocol StyleLayerDefinition { public protocol SourceBoundStyleLayerDefinition: StyleLayerDefinition { var source: StyleLayerSource { get set } + + var sourceLayerIdentifier: String? { get } } /// Based on MLNVectorStyleLayer @@ -161,3 +163,13 @@ public extension StyleLayer { modified(self) { $0.insertionPosition = .belowOthers } } } + +public extension StyleLayerDefinition { + func minimumZoomLevel(_ value: Float) -> Self { + modified(self) { $0.minimumZoomLevel = value } + } + + func maximumZoomLevel(_ value: Float) -> Self { + modified(self) { $0.maximumZoomLevel = value } + } +} diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift b/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift index 45338cf..2db5f1b 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift @@ -5,13 +5,23 @@ import MapLibreSwiftMacros @MLNStyleProperty("iconRotation", supportsInterpolation: true) @MLNStyleProperty("iconColor", supportsInterpolation: true) +@MLNStyleProperty("iconAllowsOverlap", supportsInterpolation: false) + @MLNStyleProperty("textColor", supportsInterpolation: true) @MLNStyleProperty("textFontSize", supportsInterpolation: true) @MLNStyleProperty("text", supportsInterpolation: false) -@MLNStyleProperty("iconAllowsOverlap", supportsInterpolation: false) +// An enum would probably be better? +@MLNStyleProperty("textAnchor", supportsInterpolation: false) +@MLNStyleProperty("textOffset", supportsInterpolation: true) +@MLNStyleProperty("maximumTextWidth", supportsInterpolation: true) + +@MLNStyleProperty("textHaloColor", supportsInterpolation: true) +@MLNStyleProperty("textHaloWidth", supportsInterpolation: true) +@MLNStyleProperty("textHaloBlur", supportsInterpolation: true) public struct SymbolStyleLayer: SourceBoundVectorStyleLayerDefinition { public let identifier: String + public let sourceLayerIdentifier: String? public var insertionPosition: LayerInsertionPosition = .aboveOthers public var isVisible: Bool = true public var maximumZoomLevel: Float? = nil @@ -22,11 +32,13 @@ public struct SymbolStyleLayer: SourceBoundVectorStyleLayerDefinition { public init(identifier: String, source: Source) { self.identifier = identifier + sourceLayerIdentifier = nil self.source = .source(source) } - public init(identifier: String, source: MLNSource) { + public init(identifier: String, source: MLNSource, sourceLayerIdentifier: String? = nil) { self.identifier = identifier + self.sourceLayerIdentifier = sourceLayerIdentifier self.source = .mglSource(source) } @@ -40,10 +52,9 @@ public struct SymbolStyleLayer: SourceBoundVectorStyleLayerDefinition { return SymbolStyleLayerInternal(definition: self, mglSource: styleSource) } - // TODO: Other properties and their modifiers - fileprivate var iconImageName: NSExpression? + public var iconImageName: NSExpression? - private var iconImages = [UIImage]() + public var iconImages = [UIImage]() // MARK: - Modifiers @@ -54,18 +65,42 @@ public struct SymbolStyleLayer: SourceBoundVectorStyleLayerDefinition { } } - // FIXME: This appears to be broken upstream; waiting for a new release -// public func iconImage(attribute: String, mappings: [AnyHashable: UIImage], default defaultImage: UIImage) -> Self -// { -// return modified(self) { it in -// it.iconImageName = NSExpression(forMLNMatchingKey: NSExpression(forConstantValue: attribute), -// in: Dictionary(uniqueKeysWithValues: mappings.map({ (k, v) in -// (NSExpression(forConstantValue: k), NSExpression(forConstantValue: v.sha256())) -// })), -// default: NSExpression(forConstantValue: defaultImage.sha256())) -// it.iconImages = mappings.values + [defaultImage] -// } -// } + public func iconImage(featurePropertyNamed keyPath: String) -> Self { + var copy = self + copy.iconImageName = NSExpression(forKeyPath: keyPath) + return copy + } + + /// Add an icon image that can be dynamic and use UIImages in your app, based on a feature property of the source. + /// For example, your feature could have a property called "icon-name". This name is then resolved against the key + /// in the mappings dictionary and used to find a UIImage to display on the map for that feature. + /// - Parameters: + /// - keyPath: The keypath to the feature property containing the icon to use, for example "icon-name". + /// - mappings: A lookup dictionary containing the keys found in "keyPath" and a UIImage for each keyPath. The key + /// of the mappings dictionary needs to match the value type stored at keyPath, for example `String`. + /// - defaultImage: A UIImage that MapLibre should fall back to if the key in your feature is not found in the + /// mappings table + public func iconImage( + featurePropertyNamed keyPath: String, + mappings: [AnyHashable: UIImage], + default defaultImage: UIImage + ) -> Self { + modified(self) { it in + let attributeExpression = NSExpression(forKeyPath: keyPath) + let mappingExpressions = mappings.mapValues { image in + NSExpression(forConstantValue: image.sha256()) + } + let mappingDictionary = NSDictionary(dictionary: mappingExpressions) + let defaultExpression = NSExpression(forConstantValue: defaultImage.sha256()) + + it.iconImageName = NSExpression( + forMLNMatchingKey: attributeExpression, + in: mappingDictionary as! [NSExpression: NSExpression], + default: defaultExpression + ) + it.iconImages = mappings.values + [defaultImage] + } + } } private struct SymbolStyleLayerInternal: StyleLayer { @@ -100,18 +135,34 @@ private struct SymbolStyleLayerInternal: StyleLayer { public func makeMLNStyleLayer() -> MLNStyleLayer { let result = MLNSymbolStyleLayer(identifier: identifier, source: mglSource) + result.sourceLayerIdentifier = definition.sourceLayerIdentifier result.iconImageName = definition.iconImageName result.iconRotation = definition.iconRotation + result.iconAllowsOverlap = definition.iconAllowsOverlap result.iconColor = definition.iconColor + result.text = definition.text result.textColor = definition.textColor result.textFontSize = definition.textFontSize + result.maximumTextWidth = definition.maximumTextWidth + result.textAnchor = definition.textAnchor + result.textOffset = definition.textOffset - result.iconAllowsOverlap = definition.iconAllowsOverlap + result.textHaloColor = definition.textHaloColor + result.textHaloWidth = definition.textHaloWidth + result.textHaloBlur = definition.textHaloBlur result.predicate = definition.predicate + if let minimumZoomLevel = definition.minimumZoomLevel { + result.minimumZoomLevel = minimumZoomLevel + } + + if let maximumZoomLevel = definition.maximumZoomLevel { + result.maximumZoomLevel = maximumZoomLevel + } + return result } } diff --git a/Sources/MapLibreSwiftUI/Examples/Layers.swift b/Sources/MapLibreSwiftUI/Examples/Layers.swift index bbd40fd..dac3096 100644 --- a/Sources/MapLibreSwiftUI/Examples/Layers.swift +++ b/Sources/MapLibreSwiftUI/Examples/Layers.swift @@ -133,17 +133,18 @@ let clustered = ShapeSource(identifier: "points", options: [.clustered: true, .c .ignoresSafeArea(.all) } -// TODO: Fixme +// This example does not work within a package? But it does work when in a real app // #Preview("Multiple Symbol Icons") { // MapView(styleURL: demoTilesURL) { // // Simple symbol layer demonstration with an icon // SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) -// .iconImage(attribute: "icon", +// .iconImage(featurePropertyNamed: "icon", // mappings: [ -// "missing": UIImage(systemName: "mappin.slash")!, -// "club": UIImage(systemName: "figure.dance")! +// "missing": UIImage(systemName: "mappin.slash")!, +// "club": UIImage(systemName: "figure.dance")!, // ], // default: UIImage(systemName: "mappin")!) +// .iconColor(.red) // } -// .edgesIgnoringSafeArea(.all) +// .ignoresSafeArea(.all) // }