Skip to content

Commit

Permalink
iOS V10 PointAnnotation Drag and Selection Support (#2077)
Browse files Browse the repository at this point in the history
* Feature: migrateOfflineCache

- Added the migrate function for iOS and Android from the official Mapbox 'Migrate to v10' guides

* iOS V10 PointAnnotation Build Out

- Added draggable support for PointAnnotation
- Added support for onDrag, onDragEnd, onDragStart, onDeselected, and onSelected

* Update android/rctmgl/src/main/java-v10/com/mapbox/rctmgl/modules/RCTMGLOfflineModule.kt

Co-authored-by: Miklós Fazekas <[email protected]>

* Update android/rctmgl/src/main/java-v10/com/mapbox/rctmgl/modules/RCTMGLOfflineModule.kt

Co-authored-by: Miklós Fazekas <[email protected]>

* RCTMGLInteractiveComponent

- Created new subclass RCTMGLInteractiveComponent that extracts commonality between RCTMGLSource and RCTMGLPointAnnotation

* Renamed RCTMGLInteractiveComponent to RCTMGLInteractiveElement

* Remove extra migrateOfflineCache method

* Feature: migrateOfflineCache

- Added the migrate function for iOS and Android from the official Mapbox 'Migrate to v10' guides

* iOS V10 PointAnnotation Build Out

- Added draggable support for PointAnnotation
- Added support for onDrag, onDragEnd, onDragStart, onDeselected, and onSelected

* Update android/rctmgl/src/main/java-v10/com/mapbox/rctmgl/modules/RCTMGLOfflineModule.kt

Co-authored-by: Miklós Fazekas <[email protected]>

* Update android/rctmgl/src/main/java-v10/com/mapbox/rctmgl/modules/RCTMGLOfflineModule.kt

Co-authored-by: Miklós Fazekas <[email protected]>

* RCTMGLInteractiveComponent

- Created new subclass RCTMGLInteractiveComponent that extracts commonality between RCTMGLSource and RCTMGLPointAnnotation

* Renamed RCTMGLInteractiveComponent to RCTMGLInteractiveElement

* Remove extra migrateOfflineCache method

* Remove extra isDraggable function from RCTMGLSource

Co-authored-by: Miklós Fazekas <[email protected]>
  • Loading branch information
mysport12 and mfazekas authored Aug 21, 2022
1 parent eb436ea commit ce75129
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 109 deletions.
57 changes: 57 additions & 0 deletions ios/RCTMGL-v10/RCTMGLInteractiveElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@_spi(Experimental) import MapboxMaps

@objc
class RCTMGLInteractiveElement : UIView, RCTMGLMapComponent {

var map : RCTMGLMapView? = nil

var layers: [RCTMGLSourceConsumer] = []

static let hitboxDefault = 44.0

@objc var draggable: Bool = false

@objc var hasPressListener: Bool = false

@objc var hitbox : [String:NSNumber] = [
"width": NSNumber(value: hitboxDefault),
"height": NSNumber(value: hitboxDefault)
]

@objc var id: String! = nil

@objc var onDragStart: RCTBubblingEventBlock? = nil

@objc var onPress: RCTBubblingEventBlock? = nil

func getLayerIDs() -> [String] {
layers.compactMap {
if let layer = $0 as? RCTMGLLayer {
return layer.id
} else {
return nil
}
}
}

func isDraggable() -> Bool {
return draggable
}

func isTouchable() -> Bool {
return hasPressListener
}

// MARK: - RCTMGLMapComponent
func addToMap(_ map: RCTMGLMapView, style: Style) {
self.map = map
}

func removeFromMap(_ map: RCTMGLMapView) {
self.map = nil
}

func waitForStyleLoad() -> Bool {
return true
}
}
217 changes: 199 additions & 18 deletions ios/RCTMGL-v10/RCTMGLMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ open class RCTMGLMapView : MapView {

var reactCamera : RCTMGLCamera?
var images : [RCTMGLImages] = []
var sources : [RCTMGLSource] = []
var sources : [RCTMGLInteractiveElement] = []

var handleMapChangedEvents = Set<RCTMGLEvent.EventType>()

Expand Down Expand Up @@ -54,7 +54,7 @@ open class RCTMGLMapView : MapView {
} else {
subview.reactSubviews()?.forEach { addToMap($0) }
}
if let source = subview as? RCTMGLSource {
if let source = subview as? RCTMGLInteractiveElement {
sources.append(source)
}
}
Expand All @@ -68,7 +68,7 @@ open class RCTMGLMapView : MapView {
} else {
subview.reactSubviews()?.forEach { removeFromMap($0) }
}
if let source = subview as? RCTMGLSource {
if let source = subview as? RCTMGLInteractiveElement {
sources.removeAll { $0 == source }
}
}
Expand Down Expand Up @@ -438,17 +438,20 @@ extension RCTMGLMapView {
}

extension RCTMGLMapView: GestureManagerDelegate {
private func touchableSources() -> [RCTMGLSource] {
private func draggableSources() -> [RCTMGLInteractiveElement] {
return sources.filter { $0.isDraggable() }
}
private func touchableSources() -> [RCTMGLInteractiveElement] {
return sources.filter { $0.isTouchable() }
}

private func doHandleTapInSources(sources: [RCTMGLSource], tapPoint: CGPoint, hits: [String: [QueriedFeature]], touchedSources: [RCTMGLSource], callback: @escaping (_ hits: [String: [QueriedFeature]], _ touchedSources: [RCTMGLSource]) -> Void) {
private func doHandleTapInSources(sources: [RCTMGLInteractiveElement], tapPoint: CGPoint, hits: [String: [QueriedFeature]], touchedSources: [RCTMGLInteractiveElement], callback: @escaping (_ hits: [String: [QueriedFeature]], _ touchedSources: [RCTMGLInteractiveElement]) -> Void) {
DispatchQueue.main.async {
if let source = sources.first {
let hitbox = source.hitbox;

let halfWidth = (hitbox["width"]?.doubleValue ?? RCTMGLSource.hitboxDefault) / 2.0;
let halfHeight = (hitbox["height"]?.doubleValue ?? RCTMGLSource.hitboxDefault) / 2.0;
let halfWidth = (hitbox["width"]?.doubleValue ?? RCTMGLInteractiveElement.hitboxDefault) / 2.0;
let halfHeight = (hitbox["height"]?.doubleValue ?? RCTMGLInteractiveElement.hitboxDefault) / 2.0;

let top = tapPoint.y - halfHeight;
let left = tapPoint.x - halfWidth;
Expand Down Expand Up @@ -483,7 +486,7 @@ extension RCTMGLMapView: GestureManagerDelegate {
}
}

func highestZIndex(sources: [RCTMGLSource]) -> RCTMGLSource? {
func highestZIndex(sources: [RCTMGLInteractiveElement]) -> RCTMGLInteractiveElement? {
return sources.first
}

Expand Down Expand Up @@ -541,16 +544,49 @@ extension RCTMGLMapView: GestureManagerDelegate {
@objc
func doHandleLongPress(_ sender: UILongPressGestureRecognizer) {
let position = sender.location(in: self)

if let reactOnLongPress = self.reactOnLongPress, sender.state == .began {
let coordinate = self.mapboxMap.coordinate(for: position)
var geojson = Feature(geometry: .point(Point(coordinate)));
geojson.properties = [
"screenPointX": .number(Double(position.x)),
"screenPointY": .number(Double(position.y))
]
let event = RCTMGLEvent(type:.longPress, payload: logged("doHandleLongPress") { try geojson.toJSON() })
self.fireEvent(event: event, callback: reactOnLongPress)
pointAnnotationManager.handleLongPress(sender) { (_: UILongPressGestureRecognizer) in
DispatchQueue.main.async {
let draggableSources = self.draggableSources()
self.doHandleTapInSources(sources: draggableSources, tapPoint: position, hits: [:], touchedSources: []) { (hits, draggedSources) in
if let source = self.highestZIndex(sources: draggedSources),
source.draggable,
let onDragStart = source.onDragStart {
guard let hitFeatures = hits[source.id] else {
Logger.log(level:.error, message: "doHandleLongPress, no hits found when it should have")
return
}
let features = hitFeatures.compactMap { queriedFeature in
logged("doHandleTap.hitFeatures") { try queriedFeature.feature.toJSON() } }
let location = self.mapboxMap.coordinate(for: position)
let event = RCTMGLEvent(
type: .longPress,
payload: [
"features": features,
"point": [
"x": Double(position.x),
"y": Double(position.y),
],
"coordinates": [
"latitude": Double(location.latitude),
"longitude": Double(location.longitude),
]
]
)
self.fireEvent(event: event, callback: onDragStart)
} else {
if let reactOnLongPress = self.reactOnLongPress, sender.state == .began {
let coordinate = self.mapboxMap.coordinate(for: position)
var geojson = Feature(geometry: .point(Point(coordinate)));
geojson.properties = [
"screenPointX": .number(Double(position.x)),
"screenPointY": .number(Double(position.y))
]
let event = RCTMGLEvent(type:.longPress, payload: logged("doHandleLongPress") { try geojson.toJSON() })
self.fireEvent(event: event, callback: reactOnLongPress)
}
}
}
}
}
}

Expand Down Expand Up @@ -637,6 +673,7 @@ extension RCTMGLMapView {

class PointAnnotationManager : AnnotationInteractionDelegate {
weak var selected : RCTMGLPointAnnotation? = nil
private var draggedAnnotation: PointAnnotation?

func annotationManager(_ manager: AnnotationManager, didDetectTappedAnnotations annotations: [Annotation]) {
guard annotations.count > 0 else {
Expand All @@ -649,9 +686,25 @@ class PointAnnotationManager : AnnotationInteractionDelegate {

if let rctmglPointAnnotation = userInfo[RCTMGLPointAnnotation.key] as? WeakRef<RCTMGLPointAnnotation> {
if let pt = rctmglPointAnnotation.object {
let position = pt.superview?.convert(pt.layer.position, to: nil)
let location = pt.map?.mapboxMap.coordinate(for: position!)
var geojson = Feature(geometry: .point(Point(location!)));
geojson.properties = [
"screenPointX": .number(Double(position!.x)),
"screenPointY": .number(Double(position!.y))
]
let event = RCTMGLEvent(type:.tap, payload: logged("doHandleTap") { try geojson.toJSON() })
if let selected = selected {
guard let onDeselected = pt.onDeselected else {
return
}
onDeselected(event.toJSON())
selected.onDeselect()
}
guard let onSelected = pt.onSelected else {
return
}
onSelected(event.toJSON())
pt.onSelect()
selected = pt
}
Expand Down Expand Up @@ -720,6 +773,134 @@ class PointAnnotationManager : AnnotationInteractionDelegate {
manager.delegate = self
self.mapView = mapView
}

func onDragHandler(_ manager: AnnotationManager, didDetectDraggedAnnotations annotations: [Annotation], dragState: UILongPressGestureRecognizer.State, targetPoint: CLLocationCoordinate2D) {
guard annotations.count > 0 else {
fatalError("didDetectDraggedAnnotations: No annotations found")
}

for annotation in annotations {
if let pointAnnotation = annotation as? PointAnnotation,
let userInfo = pointAnnotation.userInfo {

if let rctmglPointAnnotation = userInfo[RCTMGLPointAnnotation.key] as? WeakRef<RCTMGLPointAnnotation> {
if let pt = rctmglPointAnnotation.object {
let position = pt.superview?.convert(pt.layer.position, to: nil)
var geojson = Feature(geometry: .point(Point(targetPoint)));
geojson.properties = [
"screenPointX": .number(Double(position!.x)),
"screenPointY": .number(Double(position!.y))
]
let event = RCTMGLEvent(type:.longPress, payload: logged("doHandleLongPress") { try geojson.toJSON() })
switch (dragState) {
case .began:
guard let onDragStart = pt.onDragStart else {
return
}
onDragStart(event.toJSON())
case .changed:
guard let onDrag = pt.onDrag else {
return
}
onDrag(event.toJSON())
return
case .ended:
guard let onDragEnd = pt.onDragEnd else {
return
}
onDragEnd(event.toJSON())
return
default:
return
}
}
}
}
/*

let rctmglPointAnnotation = userInfo[RCTMGLPointAnnotation.key] as? WeakRef<RCTMGLPointAnnotation>,
let rctmglPointAnnotation = rctmglPointAnnotation.object {
rctmglPointAnnotation.didTap()
}*/
}
}

// Used for handling panning to detect annotation dragging
func handleLongPress(_ sender: UILongPressGestureRecognizer, noAnnotationFound: @escaping (UILongPressGestureRecognizer) -> Void) {
let layerId = manager.layerId
guard let mapFeatureQueryable = mapView?.mapboxMap else {
noAnnotationFound(sender)
return
}
let options = RenderedQueryOptions(layerIds: [layerId], filter: nil)
guard let targetPoint = self.mapView?.mapboxMap.coordinate(for: sender.location(in: sender.view)) else {
return
}
switch sender.state {
case .began:
mapFeatureQueryable.queryRenderedFeatures(
at: sender.location(in: sender.view),
options: options) { [weak self] (result) in

guard let self = self else { return }
switch result {
case .success(let queriedFeatures):
// Get the identifiers of all the queried features
let queriedFeatureIds: [String] = queriedFeatures.compactMap {
guard case let .string(featureId) = $0.feature.identifier else {
return nil
}
return featureId
}

// Find if any `queriedFeatureIds` match an annotation's `id`
let draggedAnnotations = self.manager.annotations.filter { queriedFeatureIds.contains($0.id) }
let enabledAnnotations = draggedAnnotations.filter { ($0.userInfo?[RCTMGLPointAnnotation.key] as? WeakRef<RCTMGLPointAnnotation>)?.object?.draggable ?? false }
// If `tappedAnnotations` is not empty, call delegate
if !enabledAnnotations.isEmpty {
self.draggedAnnotation = enabledAnnotations.first!
self.onDragHandler(self.manager, didDetectDraggedAnnotations: enabledAnnotations, dragState: .began, targetPoint: targetPoint)
} else {
noAnnotationFound(sender)
}
case .failure(let error):
noAnnotationFound(sender)
Logger.log(level:.warn, message:"Failed to query map for annotations due to error: \(error)")
}
}

case .changed:
guard let annotation = self.draggedAnnotation else {
return
}

self.onDragHandler(self.manager, didDetectDraggedAnnotations: [annotation], dragState: .changed, targetPoint: targetPoint)

// For some reason Mapbox doesn't let us update the geometry of an existing annotation
// so we have to create a whole new one.
var newAnnotation = PointAnnotation(id: annotation.id, coordinate: targetPoint)
newAnnotation.image = annotation.image
newAnnotation.userInfo = annotation.userInfo

var newAnnotations = self.manager.annotations.filter { an in
return an.id != annotation.id
}
newAnnotations.append(newAnnotation)
manager.annotations = newAnnotations
case .cancelled, .ended:
guard let annotation = self.draggedAnnotation else {
return
}
// Optionally notify some other delegate to tell them the drag finished.
self.onDragHandler(self.manager, didDetectDraggedAnnotations: [annotation], dragState: .ended, targetPoint: targetPoint)
// Reset our global var containing the annotation currently being dragged
self.draggedAnnotation = nil
return
default:
return
}
}


func remove(_ annotation: PointAnnotation) {
manager.annotations.removeAll(where: {$0.id == annotation.id})
Expand Down
Loading

0 comments on commit ce75129

Please sign in to comment.