Skip to content

Commit

Permalink
Google Map: Combine markers in the same location for better UX
Browse files Browse the repository at this point in the history
If we did nothing, then the markers would overlap and only 1 would be accessible. Spreading the markers out is a common solution, but then the user would have to click on each one to know which ones are relevant to them. In many cases, it's a recurring event, so they really only need to know about the next one.

This way the user only has to click once, and they can see all of the info they need.

See https://ux.stackexchange.com/a/112281/13828
  • Loading branch information
iandunn committed Oct 13, 2023
1 parent ca79eb7 commit 60da31e
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 15 deletions.
31 changes: 25 additions & 6 deletions mu-plugins/blocks/google-map/src/components/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import GoogleMapReact from 'google-map-react';
/**
* WordPress dependencies
*/
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { Spinner } from '@wordpress/components';

/**
Expand All @@ -16,6 +16,7 @@ import { mapStyles } from '../utilities/map-styles';
import {
assignMarkerReferences,
clusterMarkers,
combineDuplicateLocations,
panToCenter,
updateMapMarkers,
} from '../utilities/google-maps-api';
Expand All @@ -32,11 +33,13 @@ import {
*
* @return {JSX.Element}
*/
export default function Map( { apiKey, markers, icon } ) {
export default function Map( { apiKey, markers: rawMarkers, icon } ) {
const [ loaded, setLoaded ] = useState( false );
const [ clusterer, setClusterer ] = useState( null );
const [ googleMap, setGoogleMap ] = useState( null );
const [ googleMapsApi, setGoogleMapsApi ] = useState( null );
const infoWindow = useRef( null );
let combinedMarkers = [];

const options = {
zoomControl: true,
Expand All @@ -58,13 +61,18 @@ export default function Map( { apiKey, markers, icon } ) {
setGoogleMap( map );
setGoogleMapsApi( maps );

markers = assignMarkerReferences( map, maps, markers, icon );
infoWindow.current = new maps.InfoWindow( {
pixelOffset: new maps.Size( -icon.markerIconAnchorXOffset, 0 ),
} );

combinedMarkers = combineDuplicateLocations( rawMarkers );
combinedMarkers = assignMarkerReferences( map, maps, infoWindow.current, combinedMarkers, icon );

setClusterer(
clusterMarkers(
map,
maps,
markers.map( ( marker ) => marker.markerRef ),
combinedMarkers.map( ( marker ) => marker.markerRef ),
icon
)
);
Expand All @@ -80,11 +88,22 @@ export default function Map( { apiKey, markers, icon } ) {
return;
}

const markerObjects = markers.map( ( marker ) => marker.markerRef );
infoWindow.current.close();

combinedMarkers = combineDuplicateLocations( rawMarkers );
combinedMarkers = assignMarkerReferences(
googleMap,
googleMapsApi,
infoWindow.current,
combinedMarkers,
icon
);

const markerObjects = combinedMarkers.map( ( marker ) => marker.markerRef );

updateMapMarkers( clusterer, markerObjects, googleMap );
panToCenter( markerObjects, googleMap, googleMapsApi );
}, [ clusterer, markers ] );
}, [ clusterer, rawMarkers ] );

return (
<div className="wporg-google-map__container">
Expand Down
37 changes: 37 additions & 0 deletions mu-plugins/blocks/google-map/src/components/marker-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default function MarkerContent( props ) {
case 'meetup':
return <MeetupMarker { ...props } />;

case 'combined':
return <CombinedMarker { ...props } />;

default:
throw 'Component not defined for marker type: ' + type;
}
Expand Down Expand Up @@ -82,3 +85,37 @@ function MeetupMarker( { id, title, url, meetup, timestamp, location } ) {
</div>
);
}

/**
* Render a marker that combines multiple events.
*
* This currently assumes that all of the events are from the same meetup group, but could be made more flexible
* in the future if needed.
*
* @param {Object} props
* @param {Array} props.events
*/
function CombinedMarker( { events } ) {
const combinedId = events.map( ( { id } ) => id ).join( '-' );

return (
<div id={ 'wporg-map-marker__id-' + combinedId } className="wporg-map-marker">
<h3 className="wporg-map-marker__title">{ events[ 0 ].meetup }</h3>
<ul>
{ events.map( ( { id, url, title, location, timestamp } ) => {
return (
<li key={ id }>
<p className="wporg-map-marker__url">
<a href={ url }>{ title }</a>
</p>

<p className="wporg-map-marker__location">{ formatLocation( location ) }</p>

<p className="wporg-map-marker__date-time">{ getEventDateTime( timestamp ) }</p>
</li>
);
} ) }
</ul>
</div>
);
}
55 changes: 46 additions & 9 deletions mu-plugins/blocks/google-map/src/utilities/google-maps-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,66 @@ export function getValidMarkers( markers ) {
return markers;
}

/**
* Combine markers that share the exact same location into a single marker.
*
* @param {Array} rawMarkers
*
* @return {Array}
*/
export function combineDuplicateLocations( rawMarkers ) {
const combinedMarkers = {};

rawMarkers.forEach( ( rawMarker ) => {
const index = rawMarker.latitude + '|' + rawMarker.longitude;

if ( combinedMarkers[ index ] ) {
const alreadyConvertedIntoContainer = combinedMarkers[ index ].hasOwnProperty( 'events' );

if ( ! alreadyConvertedIntoContainer ) {
combinedMarkers[ index ] = {
type: 'combined',
events: [ combinedMarkers[ index ] ],
latitude: rawMarker.latitude,
longitude: rawMarker.longitude,
};
}

combinedMarkers[ index ].events.push( rawMarker );
} else {
combinedMarkers[ index ] = rawMarker;
}
} );

return Object.values( combinedMarkers );
}

/**
* Create Marker objects and save to references to them on the corresponding event.
*
* Creating the markers implicitly adds them to the map. The shared InfoWindow is assigned during creation.
*
* @param {google.maps.Map} map
* @param {google.maps} maps
* @param {Array} wpEvents
* @param {Object} rawIcon
* @param {google.maps.Map} map
* @param {google.maps} maps
* @param {google.maps.InfoWindow} infoWindow
* @param {Array} wpEvents
* @param {Object} rawIcon
*/
export function assignMarkerReferences( map, maps, wpEvents, rawIcon ) {
export function assignMarkerReferences( map, maps, infoWindow, wpEvents, rawIcon ) {
const icon = {
url: rawIcon.markerUrl,
size: new maps.Size( rawIcon.markerHeight, rawIcon.markerWidth ),
anchor: new maps.Point( 34, rawIcon.markerWidth / 2 ),
scaledSize: new maps.Size( rawIcon.markerHeight / 2, rawIcon.markerWidth / 2 ),
};

const infoWindow = new maps.InfoWindow( {
pixelOffset: new maps.Size( -rawIcon.markerIconAnchorXOffset, 0 ),
} );

wpEvents.forEach( ( wpEvent ) => {
// Only the combined markers will need new marker refs, the regular ones will still have the ones they got
// when the map was first loaded.
if ( wpEvent.markerRef ) {
return;
}

const marker = new maps.Marker( {
position: {
lat: parseFloat( wpEvent.latitude ),
Expand Down

0 comments on commit 60da31e

Please sign in to comment.