In the third part of this series where I’m building a block plugin, I focussed on adding configurable attributes after adding a Mapbox map. To finish off the plugin my goal was to be able to integrate with Garmin so that I could display the locations of outdoor activities on the map. As the Garmin API won’t allow for the whole route of an activity to be shared, I focussed on adding start locations, as well as info boxes with information about each activity. As a fall-back I also included the option to upload and share GPX routes on a map as well.
Adding Garmin activity start locations
Thanks to an open-source adapter to help connect to the Garmin Connect API, making the connection itself was easier. I made use of several files, for the most part stripped down as needed. To pull activity data within a date range needed an additional function added to a stripped down garmin-connect.php
file (see the original file is here).
Here is the newly added function:
/**
* Gets a list of activities within a date range
*
* @param string $startDate
* @param string $endDate
* @param string $strActivityType
* @return mixed
* @throws Exception
*/
public function getActivityListByDate($startDate = '', $endDate = '', $strActivityType = null)
{
$arrParams = array();
if ('' !== $startDate) {
$arrParams['startDate'] = $startDate;
}
else {
return;
}
if ('' !== $endDate) {
$arrParams['endDate'] = $endDate;
}
if (null !== $strActivityType) {
$arrParams['activityType'] = $strActivityType;
}
$strResponse = $this->objConnector->get(
'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities',
$arrParams,
true
);
if ($this->objConnector->getLastResponseCode() != 200) {
throw new Exception($this->objConnector->getLastResponseCode());
}
$objResponse = json_decode($strResponse);
return $objResponse;
}
For the other files added, see the files in the garmin-connect
directory in the Github repository for this plugin.
To retrieve the data and make use of it within the plugin, I added a function in the main plugin file. The purpose of it is primarily to make sure we have the Garmin account login details, then use that to create a new GarminConnect object. We then pull the getActivityListByDate
data from that. A lot of the code in the below snippet is based on converting the data to strings to be used in the info boxes that will appear when clicking on the location markers in the map. If a Garmin activity is set to public, then we link to the Garmin page itself with all the relavant charts and information there.
/**
* Connects to Garmin and retrieves relevant activity details
*
* @param array $new_arr The array with up-to-date attribute values.
*
* @return array|null
*/
function get_garmin_activity_details( $new_arr ) {
$garmin_account_password = get_post_meta( get_the_ID(), 'garmin_account_password_field', true ) ? get_post_meta( get_the_ID(), 'garmin_account_password_field', true ) : null;
$garmin_account_email = get_post_meta( get_the_ID(), 'garmin_account_email_field', true ) ? get_post_meta( get_the_ID(), 'garmin_account_email_field', true ) : null;
if ( $garmin_account_password && $garmin_account_email && $new_arr['showStartLocations'] ) {
require_once MGA_PLUGIN_PATH . 'garmin-connect.php';
$arr_credentials = array(
'username' => $garmin_account_email,
'password' => $garmin_account_password
);
$garmin_activity_array = array(
'type' => 'FeatureCollection',
'features' => array(),
);
try {
$obj_garmin_connect = new \Garmin_Connect( $arr_credentials );
$obj_results = $obj_garmin_connect->getActivityListByDate( $new_arr['dateFrom'], $new_arr['dateTo'], $new_arr['activityType'] );
if ( $obj_results !== null ) {
foreach( $obj_results as $obj_activity ) {
$distance = round( ( $obj_activity->distance / 1000 ), 2 );
if ( $new_arr['distanceMeasurement'] === 'miles' ) {
$distance = round( $distance / 1.609, 2 );
}
$moving_duration = $obj_activity->movingDuration;
$moving_hours = floor( $moving_duration / 3600 );
$moving_minutes = floor(( $moving_duration / 60 ) % 60 );
$moving_seconds = $moving_duration % 60;
$duration_string = $moving_minutes . __( ' mins, ', 'map-garmin-activities' ) . $moving_seconds . __( ' seconds.', 'map-garmin-activities' );
if ($moving_hours > 0 ) {
if ( $moving_hours > 1 ) {
$duration_string = $moving_hours . __( ' hrs, ', 'map-garmin-activities' ) . $duration_string;
}
else {
$duration_string = $moving_hours . __( ' hr, ', 'map-garmin-activities' ) . $duration_string;
}
}
$description_title = '<h6 class="map-garmin-activities-popup-title-text">' . $obj_activity->activityName . '</h6>';
$description_content = sprintf(
__( '%1$sThis Garmin activity is not publicly viewable.
Some available key stats: %2$s Local start time and date: %3$s %4$s Total distance: %5$s Total moving time: %6$s', 'map-garmin-activities' ),
'<p class="map-garmin-activities-popup-text">',
'</p><ul class="map-garmin-activities-popup-text"><li>',
date_i18n('d F Y, h:i:s A', strtotime( $obj_activity->startTimeLocal ) ),
'</li><li>',
$distance . ' ' . $new_arr['distanceMeasurement'] . '.</li><li>',
$duration_string . '</li></ul>'
);
if ( $obj_activity->privacy->typeKey === 'public' ) {
$description_content = sprintf(
__( '%1$s View activity details on %2$s', 'map-garmin-activities' ),
'<p><a href="https://connect.garmin.com/modern/activity/' . (string) $obj_activity->activityId . '" target="_blank" rel="noreferrer">',
'connect.garmin.com </a></p>'
);
}
$garmin_activity_array['features'][] = array(
'type' => 'Feature',
'properties'=> array(
'description' => $description_title . $description_content
),
'geometry' => array(
'type' => 'Point',
'coordinates' => array(
$obj_activity->startLongitude,
$obj_activity->startLatitude
)
)
);
}
}
} catch ( Exception $obj_exception ) {
$garmin_activity_array[] = array( 'error' => $obj_exception->getMessage() );
echo 'Oops: ' . $obj_exception->getMessage();
}
return $garmin_activity_array;
}
else {
return;
}
}
To make sure that data is also available on the front-end, further up in the main plugin file where we add the inline script for the myBlockLocalize variable, we also add a similar line of code to create and pass a new variable called garminActivityData
:
wp_add_inline_script( 'map-garmin-activities-build', 'let garminActivityData =' . json_encode( get_garmin_activity_details( $new_arr ) ), 'before' );
Adding Garmin GPX routes
To make the most of the map, I also included the option to upload GPX routes. A toggle in the settings sidebar allows switching between showing live locations and showing a GPX route, with relevant attributes added as needed.
To add the GPX functionality, converting the GPX data to a more usable geoJSON format was made a lot easier thanks to the @tmcw/togeojson
NPM package. This was imported into the edit.js
file: import { gpx } from '@tmcw/togeojson';
.
This is then used within a useEffect hook in edit.js
, which has a new trackUrl
attribute as it’s dependency. Once that attribute changes (which will happen when a new GPX track is uploaded), the useEffect hook runs, first checking that a trackUrl
exists and another new attribute showStartLocations
is false, then fetching and parsing the GPX data, setting a new gpxAsJSON
attribute and also a new trackCoords
attribute with that data. Here is the code:
useEffect( () => {
if ( attributes.trackUrl && ! attributes.showStartLocations ) {
fetch( attributes.trackUrl )
.then( res => res.text())
.then( str => new DOMParser().parseFromString( str, 'text/xml' ) )
.then( data => gpx( data ) )
.then (gpxAsJSON => {
let coordinates = gpxAsJSON.features[0].geometry.coordinates;
setAttributes({ gpxAsJSON: gpxAsJSON });
setAttributes({ trackCoords: coordinates });
} );
}
}, [attributes.trackUrl] );
Map changes
Another file with significant changes is the map-block.js
file. The entire file is viewable on the Github repository, but here is a quick explanation of the main changes.
If a GPX trackURL is defined and showStartLocations
is set to false, then on map load a new getGeoJSONfromGPX
function is called. This function makes use of the trackCoords
defined earlier, adding a route based on those coordinates.
If showStartLocations
is true and the Garmin activity data is valid, each valid activity’s start location is added using the Mapbox addsource property. As well as that, the info boxes are added for each marker. These are visible when markers are clicked on, as popups.
Editor additions
With the new additions, there are now several new options available in the settings sidebar. Similarly there are new fields in the plugin sidebar to allow for the Garmin login credentials. To prevent filling up this article, those changes are available in the edit.js
and plugin-sidebar.js
files available in the Github repository. Those new attributes are also included in the index.js
file, block.json
, and also in the main plugin file (added to the $default_attributes
variable).
Final thoughts on building this plugin
This was an interesting and fun challenge. In terms of compromises, one was using third-party code in a few places rather than coding those features from scratch. Working with open-source code is a huge benefit when it comes to building new features – I could have spent a lot longer building everything from scratch but this wasn’t (necessarily) needed. However the downside is that the code being imported is not necessarily WordPress specific, and therefore won’t make use / take consideration of WordPress specific options and functions and environments in which WordPress sites will run. An example is the use of PHP cURL functions to connect to the Garmin API – in retrospect a complete rewrite would have been the best approach, using the WordPress HTTP API, as cURL isn’t necessarily enabled on all servers.
Another compromise was in terms of the functionality I would have preferred which was being able to embed entire routes on the map based on Garmin API data. Unfortunately this is not yet possible with the Garmin API, so displaying start locations was the next best option. Adding the info boxes provided a way to share a bit more information about the activities, and if those activities were public then they include a link to the Garmin page which includes a map with the route as well. Including GPX routes was something I wanted to add as showing a full route on a map makes great use of the map itself.
All the code for this plugin is available in this Github repository, and of course open to any feature additions / suggestions for further improvement!
Here are some screenshots of the final product: