Skip to content

Commit 7f41284

Browse files
authored
Add PointAnnotationManager component with slot support (#4209)
* feat: add PointAnnotationManager component with slot support New component that configures the shared point annotation manager. Supports slot prop for positioning annotations in the Standard style layer stack. * Add example and fix interface test * fix: manager.slot is String? not Slot? * fix: render children as siblings, not nested in native view * fix: add missing import and correct variable names in ComponentView * Remove debug logs * fix: regenerate docs, simplify applySlot, reset slot on removeFromMap * fix: type-only import for ExampleWithMetadata, remove unused Mapbox import * fix(android): use Dynamic for setSlot to match codegen interface signature * fix(ios): use top-level RemovalReason, not nested type
1 parent 8953929 commit 7f41284

16 files changed

Lines changed: 452 additions & 0 deletions

__tests__/interface.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('Public Interface', () => {
99
'StyleSheet',
1010
'Light',
1111
'PointAnnotation',
12+
'PointAnnotationManager',
1213
'MarkerView',
1314
'Annotation',
1415
'Callout',

android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.rnmapbox.rnmbx.components.annotation.RNMBXCalloutManager
1111
import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewContentManager
1212
import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewManager
1313
import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManager
14+
import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManagerViewManager
1415
import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationModule
1516
import com.rnmapbox.rnmbx.components.camera.RNMBXCameraManager
1617
import com.rnmapbox.rnmbx.components.camera.RNMBXCameraModule
@@ -135,6 +136,7 @@ class RNMBXPackage : TurboReactPackage() {
135136
managers.add(RNMBXMarkerViewManager(reactApplicationContext))
136137
managers.add(RNMBXMarkerViewContentManager(reactApplicationContext))
137138
managers.add(RNMBXPointAnnotationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXPointAnnotationManager")))
139+
managers.add(RNMBXPointAnnotationManagerViewManager(reactApplicationContext))
138140
managers.add(RNMBXCalloutManager())
139141
managers.add(RNMBXNativeUserLocationManager())
140142
managers.add(RNMBXCustomLocationProviderManager())
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.rnmapbox.rnmbx.components.annotation
2+
3+
import android.content.Context
4+
import com.rnmapbox.rnmbx.components.AbstractMapFeature
5+
import com.rnmapbox.rnmbx.components.RemovalReason
6+
import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView
7+
8+
class RNMBXPointAnnotationManagerView(context: Context) : AbstractMapFeature(context) {
9+
var slot: String? = null
10+
set(value) {
11+
field = value
12+
applySlot()
13+
}
14+
15+
private fun applySlot() {
16+
withMapView { mapView ->
17+
mapView.pointAnnotations?.manager?.slot = slot
18+
}
19+
}
20+
21+
override fun addToMap(mapView: RNMBXMapView) {
22+
super.addToMap(mapView)
23+
applySlot()
24+
}
25+
26+
override fun removeFromMap(mapView: RNMBXMapView, reason: RemovalReason): Boolean {
27+
mapView.pointAnnotations?.manager?.slot = null
28+
return super.removeFromMap(mapView, reason)
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.rnmapbox.rnmbx.components.annotation
2+
3+
import com.facebook.react.bridge.Dynamic
4+
import com.facebook.react.bridge.ReactApplicationContext
5+
import com.facebook.react.common.MapBuilder
6+
import com.facebook.react.uimanager.ThemedReactContext
7+
import com.facebook.react.uimanager.annotations.ReactProp
8+
import com.facebook.react.viewmanagers.RNMBXPointAnnotationManagerManagerInterface
9+
import com.rnmapbox.rnmbx.components.AbstractEventEmitter
10+
11+
class RNMBXPointAnnotationManagerViewManager(context: ReactApplicationContext) :
12+
AbstractEventEmitter<RNMBXPointAnnotationManagerView>(context),
13+
RNMBXPointAnnotationManagerManagerInterface<RNMBXPointAnnotationManagerView> {
14+
override fun customEvents(): Map<String, String>? {
15+
return MapBuilder.builder<String, String>().build()
16+
}
17+
18+
override fun getName(): String {
19+
return REACT_CLASS
20+
}
21+
22+
override fun createViewInstance(context: ThemedReactContext): RNMBXPointAnnotationManagerView {
23+
return RNMBXPointAnnotationManagerView(context)
24+
}
25+
26+
companion object {
27+
const val REACT_CLASS = "RNMBXPointAnnotationManager"
28+
}
29+
30+
@ReactProp(name = "slot")
31+
override fun setSlot(view: RNMBXPointAnnotationManagerView, value: Dynamic) {
32+
view.slot = if (value.isNull) null else value.asString()
33+
}
34+
}

docs/docs.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6550,6 +6550,30 @@
65506550
"relPath": "src/components/PointAnnotation.tsx",
65516551
"name": "PointAnnotation"
65526552
},
6553+
"PointAnnotationManager": {
6554+
"description": "Configures the shared PointAnnotation manager for the parent MapView.\nWrap PointAnnotation components as children.",
6555+
"displayName": "PointAnnotationManager",
6556+
"methods": [],
6557+
"props": [
6558+
{
6559+
"name": "slot",
6560+
"required": false,
6561+
"type": "Slot \\| (string & {})",
6562+
"default": "none",
6563+
"description": "The slot in the style layer stack to position the annotation layer.\nUse with Mapbox Standard style to control layer ordering."
6564+
},
6565+
{
6566+
"name": "children",
6567+
"required": false,
6568+
"type": "ReactNode",
6569+
"default": "none",
6570+
"description": "FIX ME NO DESCRIPTION"
6571+
}
6572+
],
6573+
"fileNameWithExt": "PointAnnotationManager.tsx",
6574+
"relPath": "src/components/PointAnnotationManager.tsx",
6575+
"name": "PointAnnotationManager"
6576+
},
65536577
"Rain": {
65546578
"description": "",
65556579
"displayName": "Rain",

docs/examples.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,20 @@
673673
"relPath": "Annotations/PointAnnotationAnchors.js",
674674
"name": "PointAnnotationAnchors"
675675
},
676+
{
677+
"metadata": {
678+
"title": "PointAnnotationManager Slot",
679+
"tags": [
680+
"PointAnnotationManager",
681+
"PointAnnotation",
682+
"slot"
683+
],
684+
"docs": "\nDemonstrates using PointAnnotationManager to position annotations\nin different slots of the Mapbox Standard style.\n "
685+
},
686+
"fullPath": "example/src/examples/Annotations/PointAnnotationManagerSlot.tsx",
687+
"relPath": "Annotations/PointAnnotationManagerSlot.tsx",
688+
"name": "PointAnnotationManagerSlot"
689+
},
676690
{
677691
"metadata": {
678692
"title": "Show Point Annotations",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Camera, MapView, MarkerView } from '@rnmapbox/maps';
2+
import { useEffect, useState } from 'react';
3+
import { Platform, Pressable, StyleSheet, Text, View } from 'react-native';
4+
5+
const POSITIONS = Object.fromEntries([
6+
['id1', [-1.202582, 43.36005] as [number, number]],
7+
['id2', [-1.303701, 43.384357] as [number, number]],
8+
]);
9+
10+
type Rider = {
11+
id: string;
12+
name: string;
13+
order: number;
14+
};
15+
16+
const RIDERS: Rider[] = [
17+
{ id: 'id2', name: 'Favorite Rider', order: 100 },
18+
{ id: 'id1', name: 'Normal Rider', order: 10 },
19+
];
20+
21+
export const Test = () => {
22+
const [selectedId, setSelectedId] = useState<string | null>(null);
23+
const [positions, setPositions] = useState<Record<string, [number, number]>>({});
24+
25+
useEffect(() => {
26+
const interval = setInterval(() => {
27+
setPositions(POSITIONS);
28+
}, 1000);
29+
return () => clearInterval(interval);
30+
}, []);
31+
32+
const labels = RIDERS.map(rider => ({
33+
...rider,
34+
order: rider.id === selectedId ? 10000 : rider.order,
35+
selected: rider.id === selectedId,
36+
})).sort((a, b) => a.order - b.order);
37+
38+
return (
39+
<View style={StyleSheet.absoluteFill}>
40+
<MapView
41+
style={{ flex: 1 }}
42+
onPress={() => {
43+
if (Platform.OS !== 'web') setSelectedId(null);
44+
}}>
45+
<Camera centerCoordinate={[-1.21, 43.38]} zoomLevel={10} />
46+
47+
{labels.map(label => {
48+
const position = positions[label.id];
49+
if (!position) return null;
50+
51+
return (
52+
<MarkerView
53+
key={label.id}
54+
id={label.id}
55+
coordinate={position}
56+
anchor={{ x: 0, y: 1 }}
57+
allowOverlap={false}
58+
isSelected={label.selected}>
59+
<Pressable onPress={() => setSelectedId(label.id)}>
60+
<View
61+
style={{
62+
borderRadius: 16,
63+
borderWidth: 2,
64+
borderColor: label.selected ? '#fff' : '#4CAF50',
65+
backgroundColor: label.selected ? '#fff' : '#4CAF50',
66+
}}
67+
collapsable={false}>
68+
<Text style={{ fontSize: 16, color: label.selected ? '#4CAF50' : '#fff' }}>
69+
{label.name}
70+
</Text>
71+
</View>
72+
</Pressable>
73+
</MarkerView>
74+
);
75+
})}
76+
</MapView>
77+
</View>
78+
);
79+
};
80+
81+
export default Test;
82+
83+
/** @type ExampleWithMetadata['metadata'] */
84+
const metadata = {
85+
title: 'MarkerView Disappear (Issue 4206)',
86+
tags: ['MarkerView', 'bug', 'isSelected'],
87+
docs: 'Exact reproducer from issue #4206: MarkerViews disappear on Android when width/height is 0 during addViewAnnotation.',
88+
};
89+
Test.metadata = metadata;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { useState } from 'react';
2+
import { View, Text, StyleSheet } from 'react-native';
3+
import {
4+
Camera,
5+
MapView,
6+
PointAnnotation,
7+
PointAnnotationManager,
8+
} from '@rnmapbox/maps';
9+
import { Button } from '@rneui/base';
10+
11+
import type { ExampleWithMetadata } from '../common/ExampleMetadata'; // exclude-from-doc
12+
13+
const styles = StyleSheet.create({
14+
map: { flex: 1 },
15+
pin: {
16+
width: 30,
17+
height: 30,
18+
borderRadius: 15,
19+
justifyContent: 'center',
20+
alignItems: 'center',
21+
},
22+
label: { color: 'white', fontWeight: 'bold', fontSize: 12 },
23+
buttons: {
24+
flexDirection: 'row',
25+
justifyContent: 'center',
26+
padding: 8,
27+
gap: 8,
28+
},
29+
});
30+
31+
const COORDS: [number, number][] = [
32+
[-74.00597, 40.71427],
33+
[-74.0065, 40.7128],
34+
[-74.0045, 40.7155],
35+
];
36+
37+
const PointAnnotationManagerSlot = () => {
38+
const [slot, setSlot] = useState<string>('middle');
39+
40+
return (
41+
<>
42+
<MapView style={styles.map} styleURL="mapbox://styles/mapbox/standard">
43+
<Camera
44+
defaultSettings={{
45+
centerCoordinate: [-74.00597, 40.71427],
46+
zoomLevel: 15,
47+
pitch: 45,
48+
}}
49+
/>
50+
<PointAnnotationManager slot={slot}>
51+
{COORDS.map((coord, i) => (
52+
<PointAnnotation key={`pin-${i}`} id={`pin-${i}`} coordinate={coord}>
53+
<View style={[styles.pin, { backgroundColor: 'dodgerblue' }]}>
54+
<Text style={styles.label}>{i + 1}</Text>
55+
</View>
56+
</PointAnnotation>
57+
))}
58+
</PointAnnotationManager>
59+
</MapView>
60+
<View style={styles.buttons}>
61+
<Button
62+
title="bottom"
63+
onPress={() => setSlot('bottom')}
64+
color={slot === 'bottom' ? 'primary' : 'grey'}
65+
/>
66+
<Button
67+
title="middle"
68+
onPress={() => setSlot('middle')}
69+
color={slot === 'middle' ? 'primary' : 'grey'}
70+
/>
71+
<Button
72+
title="top"
73+
onPress={() => setSlot('top')}
74+
color={slot === 'top' ? 'primary' : 'grey'}
75+
/>
76+
</View>
77+
</>
78+
);
79+
};
80+
81+
export default PointAnnotationManagerSlot;
82+
83+
const metadata: ExampleWithMetadata['metadata'] = {
84+
title: 'PointAnnotationManager Slot',
85+
tags: ['PointAnnotationManager', 'PointAnnotation', 'slot'],
86+
docs: `
87+
Demonstrates using PointAnnotationManager to position annotations
88+
in different slots of the Mapbox Standard style.
89+
`,
90+
};
91+
PointAnnotationManagerSlot.metadata = metadata;

example/src/examples/Annotations/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as CustomCallout } from './CustomCallout';
22
export { default as Heatmap } from './Heatmap';
33
export { default as MarkerView } from './MarkerView';
44
export { default as PointAnnotationAnchors } from './PointAnnotationAnchors';
5+
export { default as PointAnnotationManagerSlot } from './PointAnnotationManagerSlot';
56
export { default as ShowPointAnnotation } from './ShowPointAnnotation';
67

78
export const metadata = {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#ifdef __cplusplus
2+
3+
#import <UIKit/UIKit.h>
4+
5+
#import <React/RCTUIManager.h>
6+
#import <React/RCTViewComponentView.h>
7+
8+
NS_ASSUME_NONNULL_BEGIN
9+
10+
@interface RNMBXPointAnnotationManagerComponentView : RCTViewComponentView
11+
12+
@end
13+
14+
NS_ASSUME_NONNULL_END
15+
#endif // __cplusplus

0 commit comments

Comments
 (0)