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

Expose setFeatureState / getFeatureState / removeFeatureState and add associated example #611

Merged
merged 9 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 4 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
07DC84422538B1F100F4AF14 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07DC84412538B1F100F4AF14 /* Assets.xcassets */; };
0C52BA9825AF8C880054ECA8 /* Custom3DPuckExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C52BA9725AF8C880054ECA8 /* Custom3DPuckExample.swift */; };
0C52BA9C25AFB5940054ECA8 /* arrow.gltf in Resources */ = {isa = PBXBuildFile; fileRef = 0C52BA9B25AFB5940054ECA8 /* arrow.gltf */; };
0C784D1126D002DC004AE7D0 /* FeatureStateExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C784D1026D002DC004AE7D0 /* FeatureStateExample.swift */; };
0C78AC2925BF70E40057F570 /* LineGradientExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78AC2825BF70E40057F570 /* LineGradientExample.swift */; };
0C78AC2F25BF72C70057F570 /* GradientLine.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 0C78AC2C25BF71E40057F570 /* GradientLine.geojson */; };
0CC4ECEA25B8AD3000F998B8 /* Custom2DPuckExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC4ECE925B8AD3000F998B8 /* Custom2DPuckExample.swift */; };
Expand Down Expand Up @@ -144,6 +145,7 @@
07DC84412538B1F100F4AF14 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0C52BA9725AF8C880054ECA8 /* Custom3DPuckExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Custom3DPuckExample.swift; sourceTree = "<group>"; };
0C52BA9B25AFB5940054ECA8 /* arrow.gltf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = arrow.gltf; sourceTree = "<group>"; };
0C784D1026D002DC004AE7D0 /* FeatureStateExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureStateExample.swift; sourceTree = "<group>"; };
0C78AC2825BF70E40057F570 /* LineGradientExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGradientExample.swift; sourceTree = "<group>"; };
0C78AC2C25BF71E40057F570 /* GradientLine.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = GradientLine.geojson; sourceTree = "<group>"; };
0CC4ECE925B8AD3000F998B8 /* Custom2DPuckExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Custom2DPuckExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -262,6 +264,7 @@
C69F01EC2543646A001AB49B /* DataDrivenSymbolsExample.swift */,
077E3B2B2581810600564A3E /* ExternalVectorSourceExample.swift */,
07B0715F254789D6007F2865 /* FeaturesAtPointExample.swift */,
0C784D1026D002DC004AE7D0 /* FeatureStateExample.swift */,
077E3B8F2581966300564A3E /* FitCameraToGeometryExample.swift */,
0CE3D3C225818786000585A2 /* FlyToExample.swift */,
07B071CD2547CF2B007F2865 /* MultipleGeometriesExample.swift */,
Expand Down Expand Up @@ -576,6 +579,7 @@
CADCF7252584990E0065C51B /* CameraAnimationExample.swift in Sources */,
CADCF72A2584990E0065C51B /* CustomStyleURLExample.swift in Sources */,
177C9D47269CA61100D13A2D /* ShowHideLayerExample.swift in Sources */,
0C784D1126D002DC004AE7D0 /* FeatureStateExample.swift in Sources */,
CADCF7222584990E0065C51B /* ColorExpressionExample.swift in Sources */,
17E28C5C2672A1160033DF0F /* SymbolClusteringExample.swift in Sources */,
CADCF733258499130065C51B /* Examples.swift in Sources */,
Expand Down
321 changes: 321 additions & 0 deletions Apps/Examples/Examples/All Examples/FeatureStateExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import UIKit
import MapboxMaps

@objc(FeatureStateExample)

nishant-karajgikar marked this conversation as resolved.
Show resolved Hide resolved
public class FeatureStateExample: UIViewController, ExampleProtocol {
nishant-karajgikar marked this conversation as resolved.
Show resolved Hide resolved

private var mapView: MapView!
fileprivate var descriptionView: EarthquakeDescriptionView!
nishant-karajgikar marked this conversation as resolved.
Show resolved Hide resolved
static let earthquakeSourceId: String = "earthquakes"
static let earthquakeLayerId: String = "earthquake-viz"
private var previouslyTappedEarthquakeId: String = ""

private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = DateFormatter.Style.medium //Set time style
dateFormatter.dateStyle = DateFormatter.Style.medium //Set date style
dateFormatter.timeZone = .current

return dateFormatter
}()

override public func viewDidLoad() {
super.viewDidLoad()

// Center the map over the United States.
let centerCoordinate = CLLocationCoordinate2D(latitude: 39.368279,
longitude: -97.646484)
let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 2.4))

// Set up map view
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.ornaments.options.scaleBar.visibility = .hidden
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)

// Set up description view
descriptionView = EarthquakeDescriptionView(frame: .zero)
view.addSubview(descriptionView)
descriptionView.translatesAutoresizingMaskIntoConstraints = false
descriptionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 75.0).isActive = true
descriptionView.heightAnchor.constraint(equalToConstant: 100).isActive = true
descriptionView.widthAnchor.constraint(equalToConstant: 200).isActive = true
descriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2.0).isActive = true

mapView.mapboxMap.onNext(.mapLoaded) { [weak self] _ in
guard let self = self else { return }

self.setupSourceAndLayer()

// Set up tap gesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.findFeatures))
self.mapView.addGestureRecognizer(tapGesture)

// The below lines are used for internal testing purposes only.
DispatchQueue.main.asyncAfter(deadline: .now()+3.0) { [weak self] in
self?.finish()
}
}
}

public func setupSourceAndLayer() {

// Create a new GeoJSON data source which gets its data from an external URL.
guard let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) else {
preconditionFailure("Could not calculate date for seven days ago.")
}

// Format the date to ISO8601 as required by the earthquakes API
let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let startTime = iso8601DateFormatter.string(from: sevenDaysAgo)

// Create the url required for the GeoJSONSource
guard let earthquakeURL = URL(string: "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&eventtype=earthquake&minmagnitude=1&starttime=" + startTime) else {
preconditionFailure("URL is not valid")
}

var earthquakeSource = GeoJSONSource()
earthquakeSource.data = .url(earthquakeURL)
earthquakeSource.generateId = true

do {
try mapView.mapboxMap.style.addSource(earthquakeSource, id: Self.earthquakeSourceId)
} catch {
print("Ran into an error adding a source: \(error)")
}

// Add earthquake-viz layer
var earthquakeVizLayer = CircleLayer(id: Self.earthquakeLayerId)
earthquakeVizLayer.source = Self.earthquakeSourceId

// The feature-state dependent circle-radius expression will render
// the radius size according to its magnitude when
// a feature's selected state is set to true
earthquakeVizLayer.circleRadius = .expression(
Exp(.switchCase) {
Exp(.boolean) {
Exp(.featureState) { "selected" }
false
}
Exp(.interpolate) {
Exp(.linear)
Exp(.get) { "mag" }
1
8
1.5
10
2
12
2.5
14
3
16
3.5
18
4.5
20
6.5
22
8.5
24
10.5
26
}
5
}
)
earthquakeVizLayer.circleRadiusTransition = StyleTransition(duration: 0.5, delay: 0)
earthquakeVizLayer.circleStrokeColor = .constant(.init(color: .black))
earthquakeVizLayer.circleStrokeWidth = .constant(1)

// The feature-state dependent circle-color expression will render
// the color according to its magnitude when
// a feature's hover state is set to true
earthquakeVizLayer.circleColor = .expression(
Exp(.switchCase) {
Exp(.boolean) {
Exp(.featureState) { "selected" }
false
}
Exp(.interpolate) {
Exp(.linear)
Exp(.get) { "mag" }
1
"#fff7ec"
1.5
"#fee8c8"
2
"#fdd49e"
2.5
"#fdbb84"
3
"#fc8d59"
3.5
"#ef6548"
4.5
"#d7301f"
6.5
"#b30000"
8.5
"#7f0000"
10.5
"#000"
}
"#000"
}
)
earthquakeVizLayer.circleColorTransition = StyleTransition(duration: 0.5, delay: 0)

do {
try mapView.mapboxMap.style.addLayer(earthquakeVizLayer)
} catch {
print("Ran into an error adding a layer: \(error)")
}
}

/**
Use the tap point received from the gesture recognizer to query
the map for rendered features at the given point within the layer specified.
*/
@objc public func findFeatures(_ sender: UITapGestureRecognizer) {
let tapPoint = sender.location(in: mapView)

mapView.mapboxMap.queryRenderedFeatures(
at: tapPoint,
options: RenderedQueryOptions(layerIds: ["earthquake-viz"], filter: nil)) { [weak self] result in

guard let self = self else { return }

switch result {
case .success(let queriedfeatures):

// Extract the earthquake feature from the queried features
if let earthquakeFeature = queriedfeatures.first?.feature,
let earthquakeId = (earthquakeFeature.identifier as? NSNumber)?.stringValue,
let point = earthquakeFeature.geometry.extractLocations()?.cgPointValue,
let magnitude = earthquakeFeature.properties["mag"] as? NSNumber,
let place = earthquakeFeature.properties["place"] as? String,
let timestamp = earthquakeFeature.properties["time"] as? NSNumber {

// Set the description of the earthquake from the `properties` object
self.setDescription(magnitude: magnitude.doubleValue, timeStamp: timestamp.doubleValue, location: place)

// Set the earthquake to be "selected"
self.setSelectedState(earthquakeId: earthquakeId)

// Reset a previously tapped earthquake to be "unselected".
self.resetPreviouslySelectedStateIfNeeded(currentTappedEarthquakeId: earthquakeId)

// Store the currently tapped earthquake so it can be reset when another earthquake is tapped.
self.previouslyTappedEarthquakeId = earthquakeId

// Center the selected earthquake on the screen
let coord = CLLocationCoordinate2D(latitude: CLLocationDegrees(point.x), longitude: CLLocationDegrees(point.y))
self.mapView.camera.fly(to: CameraOptions(center: coord, zoom: 10))
}
case .failure(let error):
self.showAlert(with: "An error occurred: \(error.localizedDescription)")
}
}
}

func setDescription(magnitude: Double, timeStamp: Double, location: String) {
self.descriptionView.magnitudeLabel.text = "Magnitude: \(magnitude)"
self.descriptionView.locationLabel.text = "Location: \(location)"
self.descriptionView.dateLabel.text = "Date: " + self.dateFormatter.string(
from: Date(timeIntervalSince1970: timeStamp / 1000.0))
}

// Sets a particular earthquake to be selected
func setSelectedState(earthquakeId: String) {
self.mapView.mapboxMap.setFeatureState(sourceId: Self.earthquakeSourceId,
sourceLayerId: nil,
featureId: earthquakeId,
state: ["selected": true])
}

// Resets the previously selected earthquake to be "unselected" if needed.
func resetPreviouslySelectedStateIfNeeded(currentTappedEarthquakeId: String) {

if self.previouslyTappedEarthquakeId != ""
&& currentTappedEarthquakeId != self.previouslyTappedEarthquakeId {
// Reset a previously tapped earthquake to be "unselected".
self.mapView.mapboxMap.setFeatureState(sourceId: Self.earthquakeSourceId,
sourceLayerId: nil,
featureId: self.previouslyTappedEarthquakeId,
state: ["selected": false])
}
}

// Present an alert with a given title.
public func showAlert(with title: String) {
let alertController = UIAlertController(title: title,
message: nil,
preferredStyle: .alert)

alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))

present(alertController, animated: true, completion: nil)
}
}

private class EarthquakeDescriptionView: UIView {

var magnitudeLabel: UILabel!
var locationLabel: UILabel!
var dateLabel: UILabel!
var stackView: UIStackView!

override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.opacity = 0.7
layer.borderWidth = 0.5
layer.borderColor = UIColor.black.cgColor
createSubviews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func createSubviews() {

func createLabel(placeholder: String) -> UILabel {
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 10)
label.numberOfLines = 0
label.text = placeholder
label.textColor = .black
return label
}

magnitudeLabel = createLabel(placeholder: "Magnitude: ---")
locationLabel = createLabel(placeholder: "Location: ---")
dateLabel = createLabel(placeholder: "Date: ---")

let stackview = UIStackView()
stackview.axis = .vertical
stackview.spacing = 1
stackview.alignment = .leading
stackview.distribution = .fillEqually
stackview.translatesAutoresizingMaskIntoConstraints = false
stackview.addArrangedSubview(magnitudeLabel)
magnitudeLabel.widthAnchor.constraint(equalTo: stackview.widthAnchor).isActive = true

stackview.addArrangedSubview(locationLabel)
locationLabel.widthAnchor.constraint(equalTo: stackview.widthAnchor).isActive = true

stackview.addArrangedSubview(dateLabel)
dateLabel.widthAnchor.constraint(equalTo: stackview.widthAnchor).isActive = true

self.addSubview(stackview)
stackview.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10).isActive = true
stackview.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
stackview.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stackview.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true

}
}
3 changes: 3 additions & 0 deletions Apps/Examples/Examples/Models/Examples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ public struct Examples {
Example(title: "Find features at a point",
description: "Query the map for rendered features belonging to a specific layer.",
type: FeaturesAtPointExample.self),
Example(title: "Use Feature State",
description: "Manipulate map styling with feature states and expressions.",
type: FeatureStateExample.self),
Example(title: "Restrict the map's coordinate bounds",
description: "Prevent the map from panning outside the specified coordinate bounds.",
type: RestrictCoordinateBoundsExample.self),
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Mapbox welcomes participation and contributions from everyone.

## main

### Features ✨ and improvements 🏁

* Add support for `FeatureState` in GeoJSON sources. ([#611](https://github.com/mapbox/mapbox-maps-ios/pull/611))
* `setFeatureState(sourceId:sourceLayerId:featureId:state:)` is used to associate a `stateMap` for a particular feature
* `getFeatureState(sourceId:sourceLayerId:featureId:callback:)` is used to retrieve a previously stored `stateMap` for a feature
* `removeFeatureState(sourceId:sourceLayerId:featureId:stateKey:)` is used to remove a previously stored `stateMap` for a feature

### Breaking changes ⚠️

* Removed GeoJSONManager. Please use Turf directly instead to serialize and deserialize GeoJSON. ([#603](https://github.com/mapbox/mapbox-maps-ios/pull/603))
Expand Down
Loading