Skip to content

Commit

Permalink
Enable Expression to be created without an operator (#1855)
Browse files Browse the repository at this point in the history
* Change clusterProperties to accept array

* Break up expression and reassemble for clusterProperties

* Adjust example so app can run

* Introduce operatorless expression

* Add additional tests, update comments

* Enable operatorless encoding, add tests

* Add property back, update filtering

* Remove generated update

* Fix argument and operator access

* Update Sources/MapboxMaps/Style/Types/Expression.swift

Co-authored-by: Mai Mai <mai.mai@mapbox.com>

* Add changelog, update expression operator to simplify

Co-authored-by: Mai Mai <mai.mai@mapbox.com>
  • Loading branch information
pjleonard37 and maios committed Jan 24, 2023
1 parent 4b54016 commit f99d7b0
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,30 @@ class PointAnnotationClusteringExample: UIViewController, ExampleProtocol {
UIColor.lightPink
}

// Create a cluster property to add to each cluster feature
// This will be added to the cluster textField below
let clusterProperty: [String: Expression] = ["pointString": Exp(.string) { "Count:\n" }]
// Create expression to get the total count of hydrants in a cluster
let sumExpression = Exp {
Exp(.sum) {
Exp(.accumulated)
Exp(.get) { "sum" }
}
1
}

// Create a cluster property to add to each cluster
let clusterProperties: [String: Expression] = [
"sum": sumExpression
]

// Select the options for clustering and pass them to the PointAnnotationManager to display
let clusterOptions = ClusterOptions(circleRadius: .expression(circleRadiusExpression),
circleColor: .expression(circleColorExpression),
textColor: .constant(StyleColor(.black)),
textField: .expression(Exp(.concat) {
Exp(.get) {"pointString"}
Exp(.get) {"point_count"}
Exp(.string) { "Count:\n" }
Exp(.get) {"sum"} // alternatively, you can use the built-in "point_count" property: Exp(.get) {"point_count"}
}),
clusterRadius: 75,
clusterProperties: clusterProperty)
clusterProperties: clusterProperties)
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager(id: clusterLayerID, clusterOptions: clusterOptions)
pointAnnotationManager.annotations = annotations
pointAnnotationManager.delegate = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ class SymbolClusteringExample: UIViewController, ExampleProtocol {
// Enable clustering for this source.
source.cluster = true
source.clusterRadius = 75

// Create expression to identify the max flow rate of one hydrant in the cluster
// ["max", ["get", "FLOW"]]
let maxExpression = Exp(.max) {Exp(.get) { "FLOW" }}

// Create expression to determine if a hydrant with EngineID E-9 is in the cluster
// ["any", ["==", ["get", "ENGINEID"], "E-9"]]
let ine9Expression = Exp(.any) {
Exp(.eq) {
Exp(.get) { "ENGINEID" }
"E-9"
}
}

// Create expression to get the sum of all of the flow rates in the cluster
// [["+", ["accumulated"], ["get", "sum"]], ["get", "FLOW"]]
let sumExpression = Exp {
Exp(.sum) {
Exp(.accumulated)
Exp(.get) { "sum" }
}
Exp(.get) { "FLOW" }
}

// Add the expressions to the cluster as ClusterProperties so they can be accessed below
let clusterProperties: [String: Expression] = [
"max": maxExpression,
"in_e9": ine9Expression,
"sum": sumExpression
]
source.clusterProperties = clusterProperties

let sourceID = "fire-hydrant-source"

var clusteredLayer = createClusteredLayer()
Expand Down Expand Up @@ -153,10 +185,14 @@ class SymbolClusteringExample: UIViewController, ExampleProtocol {
// If the feature is a cluster, it will have `point_count` and `cluster_id` properties. These are assigned
// when the cluster is created.
} else if let selectedFeatureProperties = queriedFeatures.first?.feature.properties,
case let .number(pointCount) = selectedFeatureProperties["point_count"],
case let .number(clusterId) = selectedFeatureProperties["cluster_id"] {
// If the tap landed on a cluster, pass the cluster ID and point count to the alert.
self?.showAlert(withTitle: "Cluster ID \(Int(clusterId))", and: "There are \(Int(pointCount)) points in this cluster")
case let .number(pointCount) = selectedFeatureProperties["point_count"],
case let .number(clusterId) = selectedFeatureProperties["cluster_id"],
case let .number(maxFlow) = selectedFeatureProperties["max"],
case let .number(sum) = selectedFeatureProperties["sum"],
case let .boolean(in_e9) = selectedFeatureProperties["in_e9"] {
// If the tap landed on a cluster, pass the cluster ID and point count to the alert.
let inEngineNine = in_e9 ? "Some hydrants belong to Engine 9." : "No hydrants belong to Engine 9."
self?.showAlert(withTitle: "Cluster ID \(Int(clusterId))", and: "There are \(Int(pointCount)) hydrants in this cluster. The highest water flow is \(Int(maxFlow)) and the collective flow is \(Int(sum)). \(inEngineNine)")
}
case .failure(let error):
self?.showAlert(withTitle: "An error occurred: \(error.localizedDescription)", and: "Please try another hydrant")
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone.
## main

* Improve stability of attribution parsing. ([#1849](https://github.com/mapbox/mapbox-maps-ios/pull/1849))
* Enable `Expression` to be created without an operator so `clusterProperties` can support advanced use cases. ([#1855](https://github.com/mapbox/mapbox-maps-ios/pull/1855))

## 10.11.0-beta.1 - January 12, 2023

Expand Down
30 changes: 26 additions & 4 deletions Sources/MapboxMaps/Annotations/ClusterOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,34 @@ public struct ClusterOptions: Equatable {
/// clustered points. Has the form `{"property_name": [operator, map_expression]}`.
/// `operator` is any expression function that accepts at
/// least 2 operands (e.g. `"+"` or `"max"`) — it accumulates the property value from clusters/points the
/// cluster contains; `map_expression` produces the value of a single point.
/// cluster contains; `map_expression` produces the value of a single point. Example:
///
/// Example: `{"sum": ["+", ["get", "scalerank"]]}`.
/// ``Expression`` syntax:
/// ```
/// let expression = Exp(.sum) {
/// Exp(.get) { "scalerank" }
/// }
/// clusterProperties: ["sum": expression]
/// ```
///
/// For more advanced use cases, in place of `operator`, you can use a custom reduce expression
/// that references a special `["accumulated"]` value, e.g.:
/// JSON syntax:
/// `{"sum": ["+", ["get", "scalerank"]]}`
///
/// For more advanced use cases, in place of `operator`, you can use a custom reduce expression that references a special `["accumulated"]` value. Example:
///
/// ``Expression`` syntax:
/// ```
/// let expression = Exp {
/// Exp(.sum) {
/// Exp(.accumulated)
/// Exp(.get) { "sum" }
/// }
/// Exp(.get) { "scalerank" }
/// }
/// clusterProperties: ["sum": expression]
/// ```
///
/// JSON syntax:
/// `{"sum": [["+", ["accumulated"], ["get", "sum"]], ["get", "scalerank"]]}`
var clusterProperties: [String: Expression]?

Expand Down
48 changes: 35 additions & 13 deletions Sources/MapboxMaps/Style/Types/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,32 @@ public struct Expression: Codable, CustomStringConvertible, Equatable {
internal var elements: [Element]

/// The operator of this expression
/// If the expression starts with an argument instead of an operator
/// then return the first operator of a contained expression if available.
public var `operator`: Operator {
guard let first = elements.first, case Element.operator(let op) = first else {
fatalError("First element of the expression is not an operator.")
switch elements.first {
case .operator(let op): return op
case .argument(.expression(let expression)): return expression.operator
default:
fatalError("First element of the expression is not an operator nor another expression.")
}
return op
}

/// The arguments contained in this expression
public var arguments: [Argument] {
return elements.dropFirst().map { (element) -> Argument in
guard case Element.argument(let arg) = element else {
fatalError("All elements after the first element in the expression must be arguments.")
}
return arg
/// If the expression starts with an argument instead of an operator, return all of the arguments
if case .argument = elements.first {
return elements.map(returnArgument)
}
return elements.dropFirst().map(returnArgument)
}

/// Check if element is argument and return, fatalError if not
internal func returnArgument(element: Element) -> Argument {
guard case Element.argument(let arg) = element else {
fatalError("All elements after the first element in the expression must be arguments.")
}
return arg
}

public init(_ op: Operator,
Expand All @@ -48,6 +59,16 @@ public struct Expression: Codable, CustomStringConvertible, Equatable {
self.elements = [.operator(op)] + arguments.map { Element.argument($0) }
}

/// Initialize an expression with only arguments
public init(@ExpressionArgumentBuilder content: () -> [Expression.Argument]) {
self.init(arguments: content())
}

/// Initialize an expression with only arguments
public init(arguments: [Argument]) {
self.elements = arguments.map { Element.argument($0) }
}

public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()

Expand All @@ -64,12 +85,13 @@ public struct Expression: Codable, CustomStringConvertible, Equatable {
var container = try decoder.unkeyedContainer()
elements = []
guard !container.isAtEnd else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expression requires an operator, but no operator was present.")
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expression requires an operator or argument, but neither was present.")
}
// First element can be an operator or argument
if let decodedOperator = try? container.decode(Operator.self) {
elements.append(.operator(decodedOperator))
}
// First element must be an operator
let decodedOperator = try container.decode(Operator.self)
elements.append(.operator(decodedOperator))
// Subsequent elemenets must be arguments
// Subsequent elements must be arguments
while !container.isAtEnd {
let decodedArgument = try container.decode(Argument.self)
elements.append(.argument(decodedArgument))
Expand Down
Loading

0 comments on commit f99d7b0

Please sign in to comment.