Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More SymbolLayer properties + sourceLayerIdentifier support + zoom levels + local image support #42

Merged
merged 8 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Sources/MapLibreSwiftDSL/Style Layers/Circle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MapLibreSwiftMacros
@MLNStyleProperty<UIColor>("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
Expand All @@ -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 {
Expand Down Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion Sources/MapLibreSwiftDSL/Style Layers/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MapLibreSwiftMacros
@MLNStyleProperty<Float>("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
Expand All @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public protocol StyleLayerDefinition {

public protocol SourceBoundStyleLayerDefinition: StyleLayerDefinition {
var source: StyleLayerSource { get set }

var sourceLayerIdentifier: String? { get }
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
}

/// Based on MLNVectorStyleLayer
Expand Down Expand Up @@ -161,3 +163,13 @@ public extension StyleLayer {
modified(self) { $0.insertionPosition = .belowOthers }
}
}

public extension StyleLayerDefinition {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review if this how you picture its usage, as I see other zoom level code in this spm already, but didn't find a way to access it from the MapView view builder.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Archdoog on our review call: "I think he is on to something fantastic" 😂 We definitely missed exposing this idiomatically in the high-level StyleLayerDefinition!

Capturing another random thought: it might make sense to split this file up since it's now got a bunch of random crap in it :) Especially for things like modifiers which might not be as obvious/discoverable. (Can do this later.)

func minimumZoomLevel(_ value: Float) -> Self {
modified(self) { $0.minimumZoomLevel = value }
}

func maximumZoomLevel(_ value: Float) -> Self {
modified(self) { $0.maximumZoomLevel = value }
}
}
87 changes: 69 additions & 18 deletions Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ import MapLibreSwiftMacros

@MLNStyleProperty<Double>("iconRotation", supportsInterpolation: true)
@MLNStyleProperty<UIColor>("iconColor", supportsInterpolation: true)
@MLNStyleProperty<Bool>("iconAllowsOverlap", supportsInterpolation: false)

@MLNStyleProperty<UIColor>("textColor", supportsInterpolation: true)
@MLNStyleProperty<Double>("textFontSize", supportsInterpolation: true)
@MLNStyleProperty<String>("text", supportsInterpolation: false)
@MLNStyleProperty<Bool>("iconAllowsOverlap", supportsInterpolation: false)
// An enum would probably be better?
@MLNStyleProperty<String>("textAnchor", supportsInterpolation: false)
@MLNStyleProperty<CGVector>("textOffset", supportsInterpolation: true)
@MLNStyleProperty<Double>("maximumTextWidth", supportsInterpolation: true)

@MLNStyleProperty<UIColor>("textHaloColor", supportsInterpolation: true)
@MLNStyleProperty<Double>("textHaloWidth", supportsInterpolation: true)
@MLNStyleProperty<Double>("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
Expand All @@ -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)
}

Expand All @@ -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]()
Comment on lines -44 to +57
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made these two public, because this allows devs to make their own extensions for the SymbolStyleLayer. For example, we created a helper extension for ourselves like this, but needed these properties exposed to do so:

public extension SymbolStyleLayer {

    func iconImage(mappings: [AnyHashable: UIImage], default defaultImage: UIImage) -> Self {
        return modified(self) { it in

            let expression1 = NSExpression(forKeyPath: "ios_category_icon_name")
            let expression2 = NSExpression(forKeyPath: "ios_category_icon_color")

            // Create an NSExpression that concatenates the two key paths
            let attributeExpression = expression1.mgl_appending(expression2)
            let mappingExpressions = mappings.mapValues { image in
                NSExpression(forConstantValue: image.sha256())
            }
            let mappingDictionary = NSDictionary(dictionary: mappingExpressions)
            let defaultExpression = NSExpression(forConstantValue: defaultImage.sha256())

            // swiftlint:disable force_cast
            it.iconImageName = NSExpression(
                forMLNMatchingKey: attributeExpression,
                in: mappingDictionary as! [NSExpression: NSExpression],
                default: defaultExpression
            )
            // swiftlint:enable force_cast
            it.iconImages = mappings.values + [defaultImage]
        }
    }

}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Thanks for the detailed example clarifying the use case!


// MARK: - Modifiers

Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
11 changes: 6 additions & 5 deletions Sources/MapLibreSwiftUI/Examples/Layers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// }
Loading