In this project that I am working on, we needed to have a page with an interactive map (aka. Google Maps) with a list of locations below it.
My first stab at this was to look for WordPress plugs for this. My first criteria was to look for the plugins with large number of active installs, with good ratings, and recently updated. I narrowed it down to:
- WP Google Maps: https://wordpress.org/plugins/wp-google-maps/
- Comprehensive Google Map Plugin: https://wordpress.org/plugins/comprehensive-google-map-plugin/
- Google Maps Widget: https://wordpress.org/plugins/google-maps-widget/
- MapPress Easy Google Maps: https://wordpress.org/plugins/mappress-google-maps-for-wordpress/
- Some plugins are no longer supported.
- Many plugins offered options to support styled maps; often paid upgrades.
- All the plugins provided an opinionated user interface for marker lists (i.e., limited styling).
- All the plugins had complex administrative screens.
- The plugin had a complex administrative screen; we need to hand over the administration of the locations (markers) to the client.
- The plugin provided an opinionated presentation of the marker list that would be incompatible without our highly styled site (deal breaker).
1. First, I created a custom post type: https://codex.wordpress.org/Post_Types
wp-content/mu-plugins/locations.php
<?php
add_action( 'init', 'create_post_type' );
function create_post_type() {
register_post_type( 'locations',
array(
'labels' => array(
'name' => __( 'Locations' ),
'singular_name' => __( 'Location' )
),
'public' => false,
'has_archive' => false,
'show_ui' => true,
)
);
}
By adding this custom post type, I was able to create a top level menu in the administrative screen to manage locations that were only available through PHP code, e.g., no routes are created for viewing an individual or archive of them.
2. Created Custom Fields for Locations Posts
Because we want to show a lot of information (in addition to latitude and longitude) for each location, used the Advanced Custom Fields plugin to create a field group (show for the custom locations post type), e.g.,
- type - list
- latitude - text
- longitude - text
- continent - list
- country - list
- address 1 - text
- address 2 - text
- address 3 - text
- phone - text
Create a page to display the location and a custom template for it, e.g., copy page.php to page-locations.php
The overall strategy that I employed here is to quickly get the data out of WordPress (and PHP) and then use client-side JavaScript to build the user interface from the data. To get the custom location posts, we use:
<?php
$args = array('post_type' => 'locations');
$loop = new WP_Query($args);
$locations = array();
while ($loop->have_posts()) : $loop->the_post();
$fields = get_fields();
$fields['title'] = get_the_title();
$locations[] = $fields;
endwhile;
?>
Then we can use the following to output the data as a JSON object for our JavaScript.
<?php echo json_encode($locations);
3. Work JavaScript Magic
Now that we have the data available on to the JavaScript on the page, we can work now in JavaScript to render the map and locations. This example was created using the jQuery JavaScript library; any more complicated I would likely want to use the more structured AngularJS JavaScript library.
note: Again, I am assuming that the reader is proficient in JavaScript.
wp-content/themes/kss/page-locations.php
<?php while (have_posts()) : the_post(); ?>
<?php get_template_part('templates/page', 'header'); ?>
<?php get_template_part('templates/content', 'page'); ?>
<?php endwhile; ?>
<?php
$args = array(
'post_type' => 'locations',
'nopaging' => 'true'
);
$loop = new WP_Query($args);
$locations = array();
while ($loop->have_posts()) : $loop->the_post();
$fields = get_fields();
$fields['title'] = get_the_title();
$locations[] = $fields;
endwhile;
?>
<div id="error"></div>
<div id="map" style="width: 100%; height: 400px;"></div>
<div id="locations"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=OBMITTED"></script>
<script>
(function() {
var i, j, k, continents, conLocations, countries, couLocations;
var $locations = jQuery('#locations');
var $error = jQuery('#error');
var locations = <?php echo json_encode($locations); ?>;
// DISPLAY MAP
var zoom = jQuery(window).width() < 768 ? 1 : 2;
var mapOptions = {
center: {
lat : 25.3130239,
lng : -117
},
zoom: zoom,
disableDefaultUI: true,
zoomControl: true,
zoomControlOptions: {
style: window.google.maps.ZoomControlStyle.DEFAULT,
position: window.google.maps.ControlPosition.BOTTOM_LEFT
},
scaleControl: false,
styles:
[{"featureType":"administrative","elementType":"labels.text.fill","stylers":[{"color":"#444444"}]},{"featureType":"landscape","elementType":"all","stylers":[{"color":"#f2f2f2"}]},{"featureType":"poi","elementType":"all","stylers":[{"visibility":"off"}]},{"featureType":"road","elementType":"all","stylers":[{"saturation":-100},{"lightness":45}]},{"featureType":"road.highway","elementType":"all","stylers":[{"visibility":"simplified"}]},{"featureType":"road.arterial","elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"featureType":"transit","elementType":"all","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"all","stylers":[{"color":"#46bcec"},{"visibility":"on"}]}]
};
var map = new window.google.maps.Map(window.document.getElementById('map'),
mapOptions);
// DISPLAY MARKERS
for (i = 0; i < locations.length; i++) {
locations[i].lat = Number(locations[i].lat);
locations[i].lng = Number(locations[i].lng);
try {
new window.google.maps.Marker({
position: locations[i],
map: map
});
} catch(err) {
$error.append('location with invalid latitude or longitude');
return;
}
}
// CONTINENTS
var detail = [];
detail.push('<ul class="continents">');
continents = locations.map(getContinent);
continents = jQuery.grep(continents, isFirstContinent).sort();
for (i = 0; i < continents.length; i++) {
detail.push('<li>');
detail.push('<div class="continent-title">' + htmlEncode(continents[i]) + '</div>');
conLocations = locations.filter(withContinent, i);
// COUNTRIES
countries = conLocations.map(getCountry);
countries = jQuery.grep(countries, isFirstCountry).sort();
detail.push('<ul class="countries">');
for (j = 0; j < countries.length; j++) {
detail.push('<li>');
detail.push('<div class="country-title">' + htmlEncode(countries[j]) + '</div>');
// LOCATIONS
couLocations = conLocations.filter(withCountry, j);
detail.push('<ul class="locations">');
for (k = 0; k < couLocations.length; k++) {
if (!/^[a-zA-Z]+$/.test(couLocations[k].type)) {
$error.append('location with invalid type');
return;
}
detail.push('<li class="' + htmlEncode(couLocations[k].type) + '">');
detail.push('<div class="location-title">' + htmlEncode(couLocations[k].title) + '</div>');
if (couLocations[k].address_1) {
detail.push('<div class="address">' + htmlEncode(couLocations[k].address_1) + '</div>');
}
if (couLocations[k].address_2) {
detail.push('<div class="address">' + htmlEncode(couLocations[k].address_2) + '</div>');
}
if (couLocations[k].address_3) {
detail.push('<div class="address">' + htmlEncode(couLocations[k].address_3) + '</div>');
}
if (couLocations[k].phone) {
detail.push('<div class="phone">' + htmlEncode(couLocations[k].phone) + '</div>');
}
detail.push('</li>');
}
detail.push('</ul>');
detail.push('</li>');
}
detail.push('</ul>');
detail.push('</li>');
}
detail.push('</ul>');
$locations.append(detail.join('\n'));
function htmlEncode(text) {
return jQuery('<div/>').text(text).html();
}
function getContinent(location) {
return location.continent;
}
function isFirstContinent(element, index) {
return jQuery.inArray(element, continents) === index;
}
function withContinent(element) {
return element.continent === continents[this];
}
function getCountry(location) {
return location.country;
}
function isFirstCountry(element, index) {
return jQuery.inArray(element, countries) === index;
}
function withCountry(element) {
return element.country === countries[this];
}
})();
</script>