import * as React from 'react';
import {
StyleSheet,
Animated,
Platform,
type ViewProps,
type ImageURISource,
type ImageRequireSource,
} from 'react-native';
import decorateMapComponent, {
ProviderContext,
SUPPORTED,
USES_DEFAULT_IMPLEMENTATION,
type MapManagerCommand,
type NativeComponent,
type UIManagerCommand,
} from './decorateMapComponent';
import {
Commands,
type MapMarkerNativeComponentType,
} from './MapMarkerNativeComponent';
import {Commands as FabricCommands} from './specs/NativeComponentMarker';
import type {AppleMarkerPriority} from './specs/NativeComponentMarker';
import type {
CalloutPressEvent,
LatLng,
MarkerDeselectEvent,
MarkerDragEvent,
MarkerDragStartEndEvent,
MarkerPressEvent,
MarkerSelectEvent,
Point,
} from './sharedTypes';
import type {Modify} from './sharedTypesInternal';
import {PROVIDER_GOOGLE} from './ProviderConstants';
import {fixImageProp} from './fixImageProp';
type AppleMarkerVisibility = 'hidden' | 'adaptive' | 'visible';
export type MapMarkerProps = ViewProps & {
/**
* Sets the anchor point for the marker.
* The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
*
* The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0],
* where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner.
*
* The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding.
* For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).
*
* @default {x: 0.5, y: 1.0}
* @platform iOS: Google Maps only. For Apple Maps, see the `centerOffset` prop
* @platform Android: Supported
*/
anchor?: Point;
/**
* Specifies the point in the marker image at which to anchor the callout when it is displayed.
* This is specified in the same coordinate system as the anchor.
*
* See the `anchor` prop for more details.
*
* @default {x: 0.5, y: 0.0}
* @platform iOS: Google Maps only. For Apple Maps, see the `calloutOffset` prop
* @platform Android: Supported
*/
calloutAnchor?: Point;
/**
* The offset (in points) at which to place the callout bubble.
* When this property is set to (0, 0),
* the anchor point of the callout bubble is placed on the top-center point of the marker view’s frame.
*
* Specifying positive offset values moves the callout bubble down and to the right,
* while specifying negative values moves it up and to the left
*
* @default {x: 0.0, y: 0.0}
* @platform iOS: Apple Maps only. For Google Maps, see the `calloutAnchor` prop
* @platform Android: Not supported. see the `calloutAnchor` prop
*/
calloutOffset?: Point;
/**
* The offset (in points) at which to display the annotation view.
*
* By default, the center point of an annotation view is placed at the coordinate point of the associated annotation.
*
* Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.
*
* @default {x: 0.0, y: 0.0}
* @platform iOS: Apple Maps only. For Google Maps, see the `anchor` prop
* @platform Android: Not supported. see the `anchor` prop
*/
centerOffset?: Point;
/**
* The coordinate for the marker.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
coordinate: LatLng;
/**
* The description of the marker.
*
* This is only used if the component has no children that are a ``,
* in which case the default callout behavior will be used,
* which will show both the `title` and the `description`, if provided.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
description?: string;
/**
* if `true` allows the marker to be draggable (re-positioned).
*
* @default false
* @platform iOS: Supported
* @platform Android: Supported
*/
draggable?: boolean;
/**
* Sets whether this marker should be flat against the map true or a billboard facing the camera.
*
* @default false
* @platform iOS: Google Maps only
* @platform Android: Supported
*/
flat?: boolean;
/**
* Marker icon to render (equivalent to `icon` property of GMSMarker Class).
* Only local image resources are allowed to be used.
*
* @platform iOS: Google Maps only
* @platform Android: Supported
*/
icon?: ImageURISource | ImageRequireSource;
/**
* A string that can be used to identify this marker.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
identifier?: string;
/**
* A custom image to be used as the marker's icon. Only local image resources are allowed to be used.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
image?: ImageURISource | ImageRequireSource;
/**
* When true, the marker will be pre-selected.
* Setting this to true allows the user to drag the marker without needing to tap on it first to focus it.
*
* @default false
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
isPreselected?: boolean;
/**
* Callback that is called when the user taps the callout view.
*
* @platform iOS: Apple Maps only
* @platform Android: Supported
*/
onCalloutPress?: (event: CalloutPressEvent) => void;
/**
* Callback that is called when the marker is deselected, before the callout is hidden.
*
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
onDeselect?: (event: MarkerDeselectEvent) => void;
/**
* Callback called continuously as the marker is dragged
*
* @platform iOS: Apple Maps only
* @platform Android: Supported
*/
onDrag?: (event: MarkerDragEvent) => void;
/**
* Callback that is called when a drag on the marker finishes.
* This is usually the point you will want to setState on the marker's coordinate again
*
* @platform iOS: Apple Maps only
* @platform Android: Supported
*/
onDragEnd?: (event: MarkerDragStartEndEvent) => void;
/**
* Callback that is called when the user initiates a drag on the marker (if it is draggable)
*
* @platform iOS: Apple Maps only
* @platform Android: Supported
*/
onDragStart?: (event: MarkerDragStartEndEvent) => void;
/**
* Callback that is called when the marker is tapped by the user.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
onPress?: (event: MarkerPressEvent) => void;
/**
* Callback that is called when the marker becomes selected.
* This will be called when the callout for that marker is about to be shown.
*
* @platform iOS: Supported.
* @platform Android: Supported
*/
onSelect?: (event: MarkerSelectEvent) => void;
/**
* The marker's opacity between 0.0 and 1.0.
*
* @default 1.0
* @platform iOS: Supported
* @platform Android: Supported
*/
opacity?: number;
/**
* If no custom marker view or custom image is provided, the platform default pin will be used, which can be customized by this color.
* Ignored if a custom marker is being used.
* For Android, the set of available colors is limited. Unsupported colors will fall back to red.
* See [#887](https://github.com/react-community/react-native-maps/issues/887) for more information.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
pinColor?: string;
/**
* A float number indicating marker's rotation angle, in degrees.
*
* @default 0
* @platform iOS: Google Maps only
* @platform Android: Supported
*/
rotation?: number;
/**
* Sets whether this marker should propagate `onPress` events.
* Enabling it will stop the parent `MapView`'s `onPress` from being called.
*
* Android does not propagate `onPress` events.
*
* See [#1132](https://github.com/react-community/react-native-maps/issues/1132) for more information.
*
* @default false
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
stopPropagation?: boolean;
/**
* Sets whether marker should be tappable.
* If set to false, the marker will not have onPress events.
*
* @default true
* @platform iOS: Google Maps only
* @platform Android: Not supported
*/
tappable?: boolean;
/**
* The title of the marker.
* This is only used if the component has no `` children.
*
* If the marker has children, default callout behavior will be used,
* which will show both the `title` and the `description`, if provided.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
title?: string;
/**
* Sets whether this marker should track view changes in info window.
* Enabling it will let marker change content of info window after first render pass, but will lead to decreased performance,
* so it's recommended to disable it whenever you don't need it.
* **Note**: iOS Google Maps only.
*
* @default false
* @platform iOS: Google Maps only
* @platform Android: Not supported
*/
tracksInfoWindowChanges?: boolean;
/**
* Sets whether this marker should track view changes.
* It's recommended to turn it off whenever it's possible to improve custom marker performance.
*
* @default true
* @platform iOS: Google Maps only
* @platform Android: Supported
*/
tracksViewChanges?: boolean;
/**
* The order in which this tile overlay is drawn with respect to other overlays.
* An overlay with a larger z-index is drawn over overlays with smaller z-indices.
* The order of overlays with the same z-index is arbitrary.
*
* @platform iOS: Supported
* @platform Android: Supported
*/
zIndex?: number;
/**
Constants that indicates the display priority for annotations.
@default required
@platform iOS: Apple Maps only.
@platform Android: Not supported
Required: A constant indicating that the item is required.
High: A constant indicating that the item’s display priority is high.
Low: A constant indicating that the item’s display priority is Low.
*/
displayPriority?: AppleMarkerPriority;
/**
* Visibility of the title text rendered beneath Marker balloon
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
titleVisibility?: AppleMarkerVisibility;
/**
* Visibility of the subtitle text rendered beneath Marker balloon
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
subtitleVisibility?: AppleMarkerVisibility;
/**
* Indicate type of default markers if it's true MKPinAnnotationView will be used and MKMarkerAnnotationView if it's false
* It doesn't change anything if you are using custom Markers
* @platform iOS: Apple Maps only
* @platform Android: Not supported
*/
useLegacyPinView?: boolean;
};
type OmittedProps = Omit;
export type NativeProps = Modify<
OmittedProps,
{icon?: string; image?: MapMarkerProps['image'] | string}
> & {
ref: React.RefObject;
};
export class MapMarker extends React.Component {
// declaration only, as they are set through decorateMap
/// @ts-ignore
context!: React.ContextType;
getNativeComponent!: () => NativeComponent;
getMapManagerCommand!: (name: string) => MapManagerCommand;
getUIManagerCommand!: (name: string) => UIManagerCommand;
static Animated: Animated.AnimatedComponent;
private marker: NativeProps['ref'];
private fabricMarker?: Boolean = undefined;
constructor(props: MapMarkerProps) {
super(props);
this.marker = React.createRef();
this.showCallout = this.showCallout.bind(this);
this.hideCallout = this.hideCallout.bind(this);
this.setCoordinates = this.setCoordinates.bind(this);
this.redrawCallout = this.redrawCallout.bind(this);
this.animateMarkerToCoordinate = this.animateMarkerToCoordinate.bind(this);
}
setNativeProps(props: Partial) {
// @ts-ignore
this.marker.current?.setNativeProps(props);
}
showCallout() {
if (this.marker.current) {
if (this.fabricMarker) {
// @ts-ignore
FabricCommands.showCallout(this.marker.current);
} else {
Commands.showCallout(this.marker.current);
}
}
}
hideCallout() {
if (this.marker.current) {
if (this.fabricMarker) {
// @ts-ignore
FabricCommands.hideCallout(this.marker.current);
} else {
Commands.hideCallout(this.marker.current);
}
}
}
setCoordinates(coordinate: LatLng) {
if (this.marker.current) {
if (this.fabricMarker) {
FabricCommands.setCoordinates(
// @ts-ignore
this.marker.current,
coordinate.latitude,
coordinate.longitude,
);
} else {
Commands.setCoordinates(this.marker.current, coordinate);
}
}
}
redrawCallout() {
if (this.marker.current) {
if (this.fabricMarker) {
// @ts-ignore
FabricCommands.redrawCallout(this.marker.current);
} else {
Commands.redrawCallout(this.marker.current);
}
}
}
animateMarkerToCoordinate(coordinate: LatLng, duration: number = 500) {
if (this.marker.current) {
if (this.fabricMarker) {
FabricCommands.animateToCoordinates(
// @ts-ignore
this.marker.current,
coordinate.latitude,
coordinate.longitude,
duration,
);
} else {
Commands.animateMarkerToCoordinate(
this.marker.current,
coordinate,
duration,
);
}
}
}
redraw() {
if (this.marker.current) {
if (this.fabricMarker) {
// @ts-ignore
FabricCommands.redraw(this.marker.current);
} else {
Commands.redraw(this.marker.current);
}
}
}
render() {
const {stopPropagation = false} = this.props;
if (this.fabricMarker === undefined) {
const provider = this.context;
this.fabricMarker = !(
Platform.OS === 'ios' && provider === PROVIDER_GOOGLE
);
}
let icon: any = this.props.icon;
let image: any = this.props.image;
if (this.fabricMarker) {
if (this.props.image) {
image = fixImageProp(this.props.image);
}
if (this.props.icon) {
icon = fixImageProp(this.props.icon);
}
} else {
if (this.props.image) {
image = fixImageProp(this.props.image);
if (image.uri) {
image = image.uri;
}
}
if (this.props.icon) {
icon = fixImageProp(this.props.icon);
if (icon.uri) {
icon = icon.uri;
}
}
}
const AIRMapMarker = this.getNativeComponent();
return (
{
if (stopPropagation) {
event.stopPropagation();
}
if (this.props.onPress) {
this.props.onPress(event);
}
}}
/>
);
}
}
const styles = StyleSheet.create({
marker: {
position: 'absolute',
backgroundColor: 'transparent',
},
});
MapMarker.Animated = Animated.createAnimatedComponent(MapMarker);
export default decorateMapComponent(MapMarker, 'Marker', {
google: {
ios: SUPPORTED,
android: USES_DEFAULT_IMPLEMENTATION,
},
});