diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js
index f169ff2cf1b14..c4b10aea57303 100644
--- a/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js
+++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js
@@ -1,101 +1,10 @@
-import { BaseControl, TextControl } from '@wordpress/components';
-import { Component, createRef } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-import Lookup from '../lookup';
+import MapboxLocationSearch from './mapbox';
+import MapkitLocationSearch from './mapkit';
-const placeholderText = __( 'Add a marker…', 'jetpack' );
-
-export class LocationSearch extends Component {
- constructor() {
- super( ...arguments );
-
- this.textRef = createRef();
- this.containerRef = createRef();
- this.state = {
- isEmpty: true,
- };
- this.autocompleter = {
- name: 'placeSearch',
- options: this.search,
- isDebounced: true,
- getOptionLabel: option =>
{ option.place_name },
- getOptionKeywords: option => [ option.place_name ],
- getOptionCompletion: this.getOptionCompletion,
- };
- }
- componentDidMount() {
- setTimeout( () => {
- this.containerRef.current.querySelector( 'input' ).focus();
- }, 50 );
- }
- getOptionCompletion = option => {
- const { value } = option;
- const point = {
- placeTitle: value.text,
- title: value.text,
- caption: value.place_name,
- id: value.id,
- coordinates: {
- longitude: value.geometry.coordinates[ 0 ],
- latitude: value.geometry.coordinates[ 1 ],
- },
- };
- this.props.onAddPoint( point );
- return value.text;
- };
-
- search = value => {
- const { apiKey, onError } = this.props;
- const url =
- 'https://api.mapbox.com/geocoding/v5/mapbox.places/' +
- encodeURI( value ) +
- '.json?access_token=' +
- apiKey;
- return new Promise( function ( resolve, reject ) {
- const xhr = new XMLHttpRequest();
- xhr.open( 'GET', url );
- xhr.onload = function () {
- if ( xhr.status === 200 ) {
- const res = JSON.parse( xhr.responseText );
- resolve( res.features );
- } else {
- const res = JSON.parse( xhr.responseText );
- onError( res.statusText, res.responseJSON.message );
- reject( new Error( 'Mapbox Places Error' ) );
- }
- };
- xhr.send();
- } );
- };
- onReset = () => {
- this.textRef.current.value = null;
- };
- render() {
- const { label } = this.props;
- return (
-
-
-
- { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => (
-
- ) }
-
-
-
- );
- }
-}
-
-LocationSearch.defaultProps = {
- onError: () => {},
+const LocationSearch = props => {
+ const LocationSearchComponent =
+ props.mapProvider === 'mapbox' ? MapboxLocationSearch : MapkitLocationSearch;
+ return
;
};
export default LocationSearch;
diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js
new file mode 100644
index 0000000000000..75ebd48cb1f37
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js
@@ -0,0 +1,101 @@
+import { BaseControl, TextControl } from '@wordpress/components';
+import { Component, createRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import Lookup from '../lookup';
+
+const placeholderText = __( 'Add a marker…', 'jetpack' );
+
+export class MapboxLocationSearch extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.textRef = createRef();
+ this.containerRef = createRef();
+ this.state = {
+ isEmpty: true,
+ };
+ this.autocompleter = {
+ name: 'placeSearch',
+ options: this.search,
+ isDebounced: true,
+ getOptionLabel: option =>
{ option.place_name },
+ getOptionKeywords: option => [ option.place_name ],
+ getOptionCompletion: this.getOptionCompletion,
+ };
+ }
+ componentDidMount() {
+ setTimeout( () => {
+ this.containerRef.current.querySelector( 'input' ).focus();
+ }, 50 );
+ }
+ getOptionCompletion = option => {
+ const { value } = option;
+ const point = {
+ placeTitle: value.text,
+ title: value.text,
+ caption: value.place_name,
+ id: value.id,
+ coordinates: {
+ longitude: value.geometry.coordinates[ 0 ],
+ latitude: value.geometry.coordinates[ 1 ],
+ },
+ };
+ this.props.onAddPoint( point );
+ return value.text;
+ };
+
+ search = value => {
+ const { apiKey, onError } = this.props;
+ const url =
+ 'https://api.mapbox.com/geocoding/v5/mapbox.places/' +
+ encodeURI( value ) +
+ '.json?access_token=' +
+ apiKey;
+ return new Promise( function ( resolve, reject ) {
+ const xhr = new XMLHttpRequest();
+ xhr.open( 'GET', url );
+ xhr.onload = function () {
+ if ( xhr.status === 200 ) {
+ const res = JSON.parse( xhr.responseText );
+ resolve( res.features );
+ } else {
+ const res = JSON.parse( xhr.responseText );
+ onError( res.statusText, res.responseJSON.message );
+ reject( new Error( 'Mapbox Places Error' ) );
+ }
+ };
+ xhr.send();
+ } );
+ };
+ onReset = () => {
+ this.textRef.current.value = null;
+ };
+ render() {
+ const { label } = this.props;
+ return (
+
+
+
+ { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => (
+
+ ) }
+
+
+
+ );
+ }
+}
+
+MapboxLocationSearch.defaultProps = {
+ onError: () => {},
+};
+
+export default MapboxLocationSearch;
diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js
new file mode 100644
index 0000000000000..74a2c6edcb6bf
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js
@@ -0,0 +1,95 @@
+import { BaseControl, TextControl } from '@wordpress/components';
+import { useEffect, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import Lookup from '../lookup';
+import { useMapkit } from '../mapkit/hooks';
+
+const placeholderText = __( 'Add a marker…', 'jetpack' );
+
+const MapkitLocationSearch = ( { label, onAddPoint } ) => {
+ const containerRef = useRef();
+ const textRef = useRef();
+ const { mapkit } = useMapkit();
+
+ const autocompleter = {
+ name: 'placeSearch',
+ options: async value => {
+ return new Promise( function ( resolve, reject ) {
+ const search = new mapkit.Search( {
+ includePointsOfInterest: false,
+ } );
+ search.autocomplete( value, ( err, results ) => {
+ if ( err ) {
+ reject( err );
+ return;
+ }
+ // filter out results without coordinates
+ const filtered = results?.results.filter( result => result.coordinate ) ?? [];
+
+ // add placeName
+ const withPlaceName = filtered.map( result => ( {
+ ...result,
+ placeName: result.displayLines?.join( ', ' ),
+ } ) );
+
+ resolve( withPlaceName );
+ } );
+ } );
+ },
+ isDebounced: true,
+ getOptionLabel: option => {
+ return
{ option.placeName };
+ },
+ getOptionKeywords: option => [ option.placeName ],
+ getOptionCompletion: option => {
+ const { value } = option;
+ const point = {
+ placeTitle: value.placeName,
+ title: value.placeName,
+ caption: value.placeName,
+ coordinates: {
+ longitude: value.coordinate.longitude,
+ latitude: value.coordinate.latitude,
+ },
+ // mapkit doesn't give us an id, so we'll make one containing the place name and coordinates
+ id: `${ value.placeName } ${ Number( value.coordinate.latitude ).toFixed( 2 ) } ${ Number(
+ value.coordinate.longitude
+ ).toFixed( 2 ) }`,
+ };
+ onAddPoint( point );
+ return value.placeName;
+ },
+ };
+
+ const onReset = () => {
+ textRef.current.value = '';
+ };
+
+ useEffect( () => {
+ setTimeout( () => {
+ containerRef.current.querySelector( 'input' ).focus();
+ }, 50 );
+ }, [] );
+
+ return (
+
+
+
+ { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => (
+
+ ) }
+
+
+
+ );
+};
+
+export default MapkitLocationSearch;
diff --git a/projects/plugins/jetpack/extensions/blocks/map/map.php b/projects/plugins/jetpack/extensions/blocks/map/map.php
index f6ce0365952cb..7f351e95ed181 100644
--- a/projects/plugins/jetpack/extensions/blocks/map/map.php
+++ b/projects/plugins/jetpack/extensions/blocks/map/map.php
@@ -58,6 +58,31 @@ function wpcom_load_event( $access_token_source ) {
}
}
+/**
+ * Function to determine which map provider to choose
+ *
+ * @param array $html The block's HTML - needed for the class name.
+ *
+ * @return string The name of the map provider.
+ */
+function get_map_provider( $html ) {
+ $mapbox_styles = array( 'is-style-terrain' );
+ // return mapbox if html contains one of the mapbox styles
+ foreach ( $mapbox_styles as $style ) {
+ if ( strpos( $html, $style ) !== false ) {
+ return 'mapbox';
+ }
+ }
+
+ // you can override the map provider with a cookie
+ if ( isset( $_COOKIE['map_provider'] ) ) {
+ return sanitize_text_field( wp_unslash( $_COOKIE['map_provider'] ) );
+ }
+
+ // if we don't apply the filters & default to mapbox
+ return apply_filters( 'wpcom_map_block_map_provider', 'mapbox' );
+}
+
/**
* Map block registration/dependency declaration.
*
@@ -68,7 +93,6 @@ function wpcom_load_event( $access_token_source ) {
*/
function load_assets( $attr, $content ) {
$access_token = Jetpack_Mapbox_Helper::get_access_token();
-
wpcom_load_event( $access_token['source'] );
if ( Blocks::is_amp_request() ) {
@@ -101,7 +125,12 @@ function load_assets( $attr, $content ) {
Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME );
- return preg_replace( '/
-1
- ? window.innerHeight
- : window.innerHeight * 0.8;
- const blockHeight = Math.min( blockWidth * ( 3 / 4 ), maxHeight );
- container.style.height = blockHeight + 'px';
- }
-}
-
export function setMarkerHTML( el, markerColor ) {
el.innerHTML = `
diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js
new file mode 100644
index 0000000000000..604443b76a251
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js
@@ -0,0 +1,141 @@
+import { waitForObject } from '../../../shared/block-editor-asset-loader';
+
+const earthRadius = 6.371e6;
+
+function getMetersPerPixel( latitude ) {
+ return Math.abs( ( earthRadius * Math.cos( ( latitude * Math.PI ) / 180 ) * 2 * Math.PI ) / 256 );
+}
+
+function convertZoomLevelToCameraDistance( zoomLevel, latitude ) {
+ const altitude = ( 512 / Math.pow( 2, zoomLevel ) ) * 0.5; // altitude in pixels
+ return altitude * getMetersPerPixel( latitude );
+}
+
+function convertCameraDistanceToZoomLevel( cameraDistance, latitude ) {
+ const altitude = cameraDistance / getMetersPerPixel( latitude );
+ return Math.log2( 512 / ( altitude / 0.5 ) );
+}
+
+function pointsToMapRegion( mapkit, points ) {
+ if ( points.length === 0 ) {
+ return null;
+ }
+
+ const topLeftCoord = new mapkit.Coordinate( -90, 180 );
+ const bottomRightCoord = new mapkit.Coordinate( 90, -180 );
+
+ points.forEach( point => {
+ topLeftCoord.latitude = Math.max( topLeftCoord.latitude, point.coordinates.latitude );
+ topLeftCoord.longitude = Math.min( topLeftCoord.longitude, point.coordinates.longitude );
+ bottomRightCoord.latitude = Math.min( bottomRightCoord.latitude, point.coordinates.latitude );
+ bottomRightCoord.longitude = Math.max(
+ bottomRightCoord.longitude,
+ point.coordinates.longitude
+ );
+ } );
+
+ const center = new mapkit.Coordinate(
+ topLeftCoord.latitude - ( topLeftCoord.latitude - bottomRightCoord.latitude ) * 0.5,
+ topLeftCoord.longitude + ( bottomRightCoord.longitude - topLeftCoord.longitude ) * 0.5
+ );
+
+ const span = new mapkit.CoordinateSpan(
+ Math.abs( topLeftCoord.latitude - bottomRightCoord.latitude ) * 1.3,
+ Math.abs( bottomRightCoord.longitude - topLeftCoord.longitude ) * 1.3
+ );
+
+ return new mapkit.CoordinateRegion( center, span );
+}
+
+function createCalloutElementCallback( currentDoc, callback ) {
+ return () => {
+ const element = currentDoc.createElement( 'div' );
+ element.classList.add( 'mapkit-popup-content' );
+ callback( element );
+ return element;
+ };
+}
+
+function waitUntilMapkitIsInitialized( currentWindow ) {
+ return new Promise( ( resolve, reject ) => {
+ const check = () => {
+ if ( typeof currentWindow.mapkitIsInitializing === 'undefined' ) {
+ reject();
+ } else if ( currentWindow.mapkitIsInitializing === false ) {
+ resolve();
+ } else {
+ currentWindow.requestAnimationFrame( check );
+ }
+ };
+ check();
+ } );
+}
+
+function loadMapkitLibrary( currentDoc, currentWindow ) {
+ return new Promise( resolve => {
+ if ( currentWindow.mapkitScriptIsLoading ) {
+ waitForObject( currentWindow, 'mapkit' ).then( mapkitObj => {
+ resolve( mapkitObj );
+ } );
+ } else {
+ currentWindow.mapkitScriptIsLoading = true;
+
+ const element = currentDoc.createElement( 'script' );
+ element.addEventListener(
+ 'load',
+ async () => {
+ const mapkitObj = await waitForObject( currentWindow, 'mapkit' );
+ currentWindow.mapkitScriptIsLoading = false;
+
+ resolve( mapkitObj );
+ },
+ { once: true }
+ );
+ element.src = 'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js';
+ element.crossOrigin = 'anonymous';
+ currentDoc.head.appendChild( element );
+ }
+ } );
+}
+
+function fetchMapkitKey( mapkitObj, blogId, currentWindow ) {
+ return new Promise( ( resolve, reject ) => {
+ if ( currentWindow.mapkitIsInitialized ) {
+ resolve();
+ } else if ( currentWindow.mapkitIsInitializing ) {
+ waitUntilMapkitIsInitialized( currentWindow ).then( () => {
+ resolve();
+ } );
+ } else {
+ currentWindow.mapkitIsInitializing = true;
+ currentWindow.mapkitIsInitialized = false;
+ mapkitObj.init( {
+ authorizationCallback: async done => {
+ try {
+ const response = await fetch( `https://public-api.wordpress.com/wpcom/v2/mapkit` );
+ if ( response.status === 200 ) {
+ const data = await response.json();
+ done( data.wpcom_mapkit_access_token );
+ } else {
+ reject();
+ }
+ currentWindow.mapkitIsInitializing = false;
+ currentWindow.mapkitIsInitialized = true;
+ resolve();
+ } catch ( error ) {
+ reject();
+ }
+ },
+ } );
+ }
+ } );
+}
+
+export {
+ convertZoomLevelToCameraDistance,
+ convertCameraDistanceToZoomLevel,
+ createCalloutElementCallback,
+ fetchMapkitKey,
+ loadMapkitLibrary,
+ pointsToMapRegion,
+};
diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js
new file mode 100644
index 0000000000000..475ad2a2aa670
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js
@@ -0,0 +1,38 @@
+import { createContext, useState } from '@wordpress/element';
+
+const MapkitContext = createContext( {
+ map: null,
+ mapkit: null,
+ loaded: false,
+ activeMarker: null,
+ calloutReference: null,
+ currentDoc: null,
+ currentWindow: null,
+ admin: false,
+ setPoints: () => {},
+ points: [],
+ previousCenter: null,
+} );
+
+const MapkitProvider = ( { value, children } ) => {
+ const [ activeMarker, setActiveMarker ] = useState( null );
+ const [ calloutReference, setCalloutReference ] = useState( null );
+ const [ previousCenter, setPreviousCenter ] = useState( null );
+ return (
+
+ { children }
+
+ );
+};
+
+export { MapkitContext, MapkitProvider };
diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js
new file mode 100644
index 0000000000000..84ca2f9eb36d1
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js
@@ -0,0 +1,285 @@
+import { CONNECTION_STORE_ID } from '@automattic/jetpack-connection';
+import { select } from '@wordpress/data';
+import { useContext, useEffect, useRef, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { debounce } from 'lodash';
+import { getLoadContext } from '../../../../shared/block-editor-asset-loader';
+import {
+ convertZoomLevelToCameraDistance,
+ convertCameraDistanceToZoomLevel,
+ fetchMapkitKey,
+ loadMapkitLibrary,
+ pointsToMapRegion,
+} from '../../mapkit-utils';
+import { MapkitContext } from '../context';
+
+const useMapkit = () => {
+ return useContext( MapkitContext );
+};
+
+// mapRef can be a ref to the element that will render the map
+// or a ref to the element that will be on the page when the map is rendered.
+// It is only used here to determine the document and window to use.
+const useMapkitSetup = mapRef => {
+ const [ loaded, setLoaded ] = useState( false );
+ const [ error, setError ] = useState( false );
+ const [ mapkit, setMapkit ] = useState( null );
+ const [ _currentWindow, setCurrentWindow ] = useState( null );
+ const [ _currentDoc, setCurrentDoc ] = useState( null );
+
+ useEffect( () => {
+ const blog_id = select( CONNECTION_STORE_ID ).getBlogId();
+ const { currentDoc, currentWindow } = getLoadContext( mapRef.current );
+
+ if ( mapRef.current ) {
+ setCurrentWindow( currentWindow );
+ setCurrentDoc( currentDoc );
+
+ const fetchMapkitKeyErrorMessage = __(
+ 'Failed to retrieve a Mapkit API token. Please try refreshing.',
+ 'jetpack'
+ );
+
+ // If mapkit is already loaded, reuse it.
+ if ( currentWindow.mapkit ) {
+ setMapkit( currentWindow.mapkit );
+ // Fetch API key in the off chance that mapkit is available but not initialized for some reason
+ // It will just resolve in case it is already initialized.
+ fetchMapkitKey( currentWindow.mapkit, blog_id, currentWindow ).then(
+ () => {
+ setLoaded( true );
+ },
+ () => {
+ setError( fetchMapkitKeyErrorMessage );
+ }
+ );
+ } else {
+ loadMapkitLibrary( currentDoc, currentWindow ).then( mapkitObj => {
+ setMapkit( mapkitObj );
+
+ fetchMapkitKey( mapkitObj, blog_id, currentWindow ).then(
+ () => {
+ setLoaded( true );
+ },
+ () => {
+ setError( fetchMapkitKeyErrorMessage );
+ }
+ );
+ } );
+ }
+ }
+ }, [ mapRef ] );
+
+ return { loaded, error, mapkit, currentDoc: _currentDoc, currentWindow: _currentWindow };
+};
+
+const useMapkitInit = ( mapkit, loaded, mapRef ) => {
+ const [ map, setMap ] = useState( null );
+ useEffect( () => {
+ if ( mapkit && loaded ) {
+ setMap( new mapkit.Map( mapRef.current ) );
+ }
+ }, [ mapkit, loaded, mapRef ] );
+ return { map };
+};
+
+const useMapkitCenter = ( center, setCenter ) => {
+ const { mapkit, map } = useMapkit();
+ const memoizedCenter = useRef( center );
+ const memoizedSetCenter = useRef( setCenter );
+
+ useEffect( () => {
+ if ( ! mapkit || ! map || ! memoizedCenter.current ) {
+ return;
+ }
+
+ const lat = memoizedCenter.current?.lat ?? memoizedCenter.current?.latitude;
+ const lng = memoizedCenter.current?.lng ?? memoizedCenter.current?.longitude;
+
+ if ( typeof lat === 'number' && typeof lng === 'number' ) {
+ map.center = new mapkit.Coordinate( lat, lng );
+ }
+ }, [ mapkit, map, memoizedCenter ] );
+
+ useEffect( () => {
+ if ( ! mapkit || ! map ) {
+ return;
+ }
+
+ const changeRegion = () => {
+ if ( map.center ) {
+ const { latitude, longitude } = map.center;
+ memoizedSetCenter.current( { lat: latitude, lng: longitude } );
+ }
+ };
+
+ map.addEventListener( 'region-change-end', debounce( changeRegion, 1000 ) );
+
+ return () => {
+ map.removeEventListener( 'region-change-end', changeRegion );
+ };
+ }, [ mapkit, map, memoizedSetCenter ] );
+};
+
+const useMapkitType = mapStyle => {
+ const { mapkit, map } = useMapkit();
+
+ useEffect( () => {
+ if ( ! mapkit || ! map ) {
+ return;
+ }
+ map.mapType = ( () => {
+ switch ( mapStyle ) {
+ case 'satellite':
+ return mapkit.Map.MapTypes.Satellite;
+ case 'black_and_white':
+ return mapkit.Map.MapTypes.MutedStandard;
+ case 'hybrid':
+ return mapkit.Map.MapTypes.Hybrid;
+ default:
+ return mapkit.Map.MapTypes.Standard;
+ }
+ } )();
+ }, [ mapkit, map, mapStyle ] );
+};
+
+const useMapkitZoom = ( zoom, setZoom ) => {
+ const { mapkit, map, points } = useMapkit();
+
+ useEffect( () => {
+ if ( mapkit && map ) {
+ if ( points && points.length <= 1 ) {
+ const defaultCameraDistance = convertZoomLevelToCameraDistance( 13, map.center.latitude );
+
+ if ( zoom ) {
+ const cameraDistance = convertZoomLevelToCameraDistance( zoom, map.center.latitude );
+ if ( cameraDistance !== map.cameraDistance ) {
+ map.cameraDistance = cameraDistance;
+ }
+ } else if ( defaultCameraDistance !== map.cameraDistance ) {
+ map.cameraDistance = defaultCameraDistance;
+ }
+ // Zooming and scrolling are enabled when there are 0 or 1 points.
+ map.isZoomEnabled = true;
+ map.isScrollEnabled = true;
+ } else {
+ map.region = pointsToMapRegion( mapkit, points );
+ // Zooming and scrolling are disabled when there are multiple points.
+ map.isZoomEnabled = false;
+ map.isScrollEnabled = false;
+ }
+ }
+ }, [ mapkit, map, zoom, points ] );
+
+ useEffect( () => {
+ const changeZoom = () => {
+ setZoom( convertCameraDistanceToZoomLevel( map.cameraDistance, map.center.latitude ) );
+ };
+
+ map.addEventListener( 'zoom-end', changeZoom );
+
+ return () => {
+ map.removeEventListener( 'zoom-end', changeZoom );
+ };
+ }, [ mapkit, map, setZoom ] );
+};
+
+const useMapkitPoints = ( points, markerColor, callOutElement = null, onSelect = null ) => {
+ const { mapkit, map, loaded } = useMapkit();
+
+ // avoid rerenders by making these refs
+ const callOutElementRef = useRef( callOutElement );
+ const onSelectRef = useRef( onSelect );
+
+ useEffect( () => {
+ if ( loaded ) {
+ map.removeAnnotations( map.annotations );
+ const annotations = points.map( point => {
+ const marker = new mapkit.MarkerAnnotation(
+ new mapkit.Coordinate( point.coordinates.latitude, point.coordinates.longitude ),
+ { color: markerColor }
+ );
+ marker.calloutEnabled = true;
+ marker.title = point.title;
+ if ( callOutElementRef.current ) {
+ marker.callout = {
+ calloutElementForAnnotation: callOutElementRef.current,
+ };
+ }
+ if ( onSelectRef.current ) {
+ marker.addEventListener( 'select', () => onSelectRef.current( point, map ) );
+ }
+ return marker;
+ } );
+ map.showItems( annotations );
+ }
+ }, [ points, loaded, map, mapkit, markerColor, callOutElementRef, onSelectRef ] );
+};
+
+const useMapkitOnMapLoad = onMapLoad => {
+ const { map, loaded } = useMapkit();
+ const onMapLoadRef = useRef( onMapLoad );
+
+ useEffect( () => {
+ if ( loaded ) {
+ onMapLoadRef.current( map );
+ }
+ }, [ loaded, map, onMapLoadRef ] );
+};
+
+const useMapkitOnMapTap = onMapTap => {
+ const { map, previousCenter, loaded } = useMapkit();
+ const onMapTapRef = useRef( onMapTap );
+
+ useEffect( () => {
+ if ( loaded ) {
+ map.addEventListener( 'single-tap', () => {
+ onMapTapRef.current( previousCenter );
+ } );
+ }
+ }, [ loaded, map, previousCenter, onMapTapRef ] );
+};
+
+const useMapkitAddressLookup = ( address, onSetPointsRef ) => {
+ const { mapkit, map } = useMapkit();
+
+ useEffect( () => {
+ if ( mapkit && map && address?.length ) {
+ const geocoder = new mapkit.Geocoder();
+ geocoder.lookup( address, ( error, data ) => {
+ if ( data?.results?.length ) {
+ const place = data.results[ 0 ];
+ const title = place.formattedAddress;
+ const point = {
+ placeTitle: title,
+ title: title,
+ caption: title,
+ coordinates: {
+ longitude: place.coordinate.longitude,
+ latitude: place.coordinate.latitude,
+ },
+ // mapkit doesn't give us an id, so we'll make one containing the place name and coordinates
+ id: `${ title } ${ Number( place.coordinate.latitude ).toFixed( 2 ) } ${ Number(
+ place.coordinate.longitude
+ ).toFixed( 2 ) }`,
+ };
+
+ onSetPointsRef.current( [ point ] );
+ }
+ } );
+ }
+ }, [ mapkit, map, address, onSetPointsRef ] );
+};
+
+export {
+ useMapkit,
+ useMapkitSetup,
+ useMapkitInit,
+ useMapkitZoom,
+ useMapkitType,
+ useMapkitCenter,
+ useMapkitPoints,
+ useMapkitOnMapLoad,
+ useMapkitOnMapTap,
+ useMapkitAddressLookup,
+};
diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_black_and_white.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white.jpg
similarity index 100%
rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_black_and_white.jpg
rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white.jpg
diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg
new file mode 100644
index 0000000000000..5cb7f513b90e1
Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg differ
diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_default.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default.jpg
similarity index 100%
rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_default.jpg
rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default.jpg
diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg
new file mode 100644
index 0000000000000..fff7a89ae2c85
Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg differ
diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_satellite.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite.jpg
similarity index 100%
rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_satellite.jpg
rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite.jpg
diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg
new file mode 100644
index 0000000000000..30f90a70cf719
Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg differ
diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_terrain.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_terrain.jpg
similarity index 100%
rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_terrain.jpg
rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_terrain.jpg
diff --git a/projects/plugins/jetpack/extensions/blocks/map/settings.js b/projects/plugins/jetpack/extensions/blocks/map/settings.js
index 1f09c3c6211df..82d63740ee180 100644
--- a/projects/plugins/jetpack/extensions/blocks/map/settings.js
+++ b/projects/plugins/jetpack/extensions/blocks/map/settings.js
@@ -1,10 +1,16 @@
// Disable forbidden