Category Archives: User Experience

2014-07-07 14_18_06-Clipboard

How does your Mobile Strategy support your Customer Experience?

A recent telephone survey conducted by Braun Research on behalf of Bank of America provided a rather candid look at how our mobile phones have become an indispensable companion and personal assistant for most Americans. When asked to rank their mobile devices importance in their daily life, the survey respondents ranked their mobile phone as equally important as their car or (gulp), even their deodorant! Among the Millennials surveyed, 96% said that their mobile phone was THE MOST IMPORTANT part of their daily life, ahead of such basic hygiene tasks as brushing their teeth and deodorant.

While these statistics are shocking (and quite possibly, smelly!), they do shed light on the changing expectations for customer service. This same survey provided evidence that HOW customers are interacting with banks is changing, and rapidly! Bank branches with ATMs and Drive Through Windows are quickly becoming less important, as online and mobile banking applications become more widely used. According to the survey, only 23% of the respondents conduct the majority of their banking transactions through a branch versus 47% conducting the majority of transactions online or through their mobile device.

The picture in this tweet provides a humorous illustration of how mobile technology has changed over the past 3 decades. Our mobile phone has evolved from the need to talk “on the go”, to rapidly becoming an indispensable companion, as confirmed within this telephone survey. When it comes to the options we provide to our customers for interacting with us, are we considering the options we can provide through their mobile device? Or, are we still under the outdated assumption when it comes to our mobile strategy that “smaller is better”? If so, what impact does that assumption have on your overall Customer Experience?

VA Google Glass

Technology can either bring you closer to the customer or send you farther adrift, farther away from the people you’re striving to serve

Virgin Atlantic recently completed a 6-week Proof of Concept aimed at enhancing the customer experience for its Upper Class passengers. Customer Service Representatives were outfitted with Google Glass and Sony Smartwatches. The concept was to enable the representatives to provide service to the customer, without the need to break eye contact. The fashion-forward company has declared the leading-edge technology POC a raging success.

While this POC utilized technology to enhance the customer experience, it did so in a way that is unique. Virgin Atlantic didn’t just try this concept to provide better tools to their Customer Service Representatives; they did it with the entire goal of allowing the reps to establish an emotional connection with the customer. They know the value of the human side of customer experience, and this POC helped them take that emotional connection one step further.

But, before you rush to order the latest tech gadget for your CSRs, be sure you know how you can connect with your customers on an emotional level. Only once you know how to enhance the human experience, can you identify tools to support that effort.

To read more about Virgin Atlantic’s Proof of Concept, click here:

http://onforb.es/1pPmn71 via @forbes

Geolocation in WordPress

The explosion of geolocation services has made it relatively easy to do custom geolocation of just about any dataset you want.

In this post, I’ll walk through setting up a “Location” custom-post type in WordPress. We’ll calculate a latitude and longitude for each location, include a search function, and then display the resulting locations in a table and on a map.

Create latitude/longitude database table

We want the location data to be searchable, so we can show people the closest locations. In order to avoid WordPress’s complex metadata relationships and simplify the MYSQL calls, I want to put my latitude/longitude data into a separate table in the WordPress database. That way I can use WordPress’s database functions to access the table, but keep the queries themselves simple.

In your WordPress database, create a table named “lat_lng_post” and give it the following columns:

post_id(number), lat(number), lng(number).

Create the custom post type

Next, let’s create a custom “Location” post type with address information. I’ve built this as a plugin so it can be easily added/removed to any website. If you’ve never created a plugin before, you can check out my post on the subject.

<?php
/*
Plugin Name: Locations
Plugin URI: 
Description: Adds custom post type for locations
Version: 1.0
Author: Steven Ray
Author URI: 
License: GPL
*/

/**** LOCATION POST TYPE ****/

// setup the location custom post type
add_action( 'init', 'srd_locations_register_post_type' );

// register the location post type
function srd_locations_register_post_type() {

    // setup the arguments for the location post type
    $locations_args = array(
        'public' => true,
        'query_var' => 'location',
        'rewrite' => array(
            'slug' => 'location',
            'with_front' => false
        ),
        'supports' => array(
            'title',
            'editor',
            'excerpt',
            'thumbnail'
        ),
        'labels' => array(
            'name' => 'Locations',
            'singular_name' => 'Location',
            'add_new' => 'Add New Location',
            'add_new_item' => 'Add New Location',
            'edit_item' => 'Edit Location',
            'new_item' => 'New Location',
            'view_item' => 'View Location',
            'search_items' => 'Search Locations',
            'not_found' => 'No Locations Found',
            'not_found_in_trash' => 'No Locations Found in Trash'
        ),
        'register_meta_box_cb' => 'add_location_metaboxes'
    );

    //register the post type
    register_post_type( 'location', $locations_args );

    // Add the location metaboxes
    function add_location_metaboxes() {
        add_meta_box('location-details', 'location details', 'location_details', 'location', 'normal', 'default');
    }
}
//Get rid of the content editor
add_action( 'init', 'srd_locations_init' );
function srd_locations_init() {
    remove_post_type_support( 'location', 'editor' );
}

/*** location METABOXES ***/
//Render the metabox
function location_details() {
    global $post;
    // Noncename needed to verify where the data originated
    echo '<input type="hidden" name="eventmeta_noncename" id="eventmeta_noncename" value="' .
    wp_create_nonce( plugin_basename(__FILE__) ) . '" />';
    // Get the field data if it has already been entered
    $address = get_post_meta($post->ID, 'location-street-address', true);
    $city = get_post_meta($post->ID, 'location-city', true);
    $state = get_post_meta($post->ID, 'location-state', true);
    $ZIP = get_post_meta($post->ID, 'location-ZIP', true);

    $latitude = get_post_meta($post->ID, 'location-latitude', true);
    $longitude = get_post_meta($post->ID, 'location-longitude', true);

    // Echo out the fields

    echo '<p><label>Street address:</label> <input type="text" name="location-street-address" value="' . $address  . '" /></p>';
    echo '<p><label>City:</label> <input type="text" name="location-city" value="' . $city  . '" /></p>';
    echo '<p><label>State:</label> <input type="text" name="location-state" value="' . $state . '" /></p>';
    echo '<p><label>ZIP code:</label> <input type="text" name="location-ZIP" value="' . $ZIP  . '" /></p>';
    echo '<p><label>Latitude (calculated):</label> <input type="text" disabled="disabled" name="location-latitude" value="' . $latitude  .'" /></p>';
    echo '<p><label>Longitude (calculated):</label> <input type="text" disabled="disabled" name="location-longitude" value="' . $longitude  .'" /></p>';
}

// Save the Metabox Data
function srd_save_location_meta($post_id, $post) {
    // verify this came from the our screen and with proper authorization,
    // because save_post can be triggered at other times
    if ( !wp_verify_nonce( $_POST['eventmeta_noncename'], plugin_basename(__FILE__) )) {
    return $post->ID;
    }
    // Is the user allowed to edit the post or page?
    if ( !current_user_can( 'edit_post', $post->ID ))
        return $post->ID;
    // OK, we're authenticated: we need to find and save the data
    // We'll put it into an array to make it easier to loop though.
    $location_meta['location-street-address'] = $_POST['location-street-address'];
    $location_meta['location-city'] = $_POST['location-city'];
    $location_meta['location-state'] = $_POST['location-state'];
    $location_meta['location-ZIP'] = $_POST['location-ZIP'];

    //Get Lat/Long from address
        $address = $_POST['location-street-address']." ".$_POST['location-city']." ".$_POST['location-state']. " ".$_POST['location-ZIP'];
        $prepAddr = str_replace(' ','+',$address);
        $geocode=file_get_contents('http://maps.google.com/maps/api/geocode/json?address='.$prepAddr.'&sensor=false');
        $output= json_decode($geocode);
        $latitude = $output->results[0]->geometry->location->lat;
        $longitude = $output->results[0]->geometry->location->lng;

    $location_meta['location-latitude'] = $latitude;
    $location_meta['location-longitude'] = $longitude;

    // Add values of $location_meta as custom fields
    foreach ($location_meta as $key => $value) { // Cycle through the $location_meta array!
        if( $post->post_type == 'revision' ) return; // Don't store custom data twice
        $value = implode(',', (array)$value); // If $value is an array, make it a CSV (unlikely)
        if(get_post_meta($post->ID, $key, FALSE)) { // If the custom field already has a value
            update_post_meta($post->ID, $key, $value);
        } else { // If the custom field doesn't have a value
            add_post_meta($post->ID, $key, $value);
        }
        if(!$value) delete_post_meta($post->ID, $key); // Delete if blank
    }

    //Call function to save lat/long to custom table
    save_lat_lng($post->ID, $latitude, $longitude);

}
add_action('save_post', 'srd_save_location_meta', 1, 2); // save the custom fields
/*** END METABOXES ***/

/*** DISPLAY COLUMNS ON MANAGE POSTS PAGE ***/
add_filter( 'manage_edit-location_columns', 'set_custom_edit_location_columns' );
add_action( 'manage_location_posts_custom_column' , 'custom_location_column', 10, 2 );

//Define columns to show
function set_custom_edit_location_columns($columns) {
    unset($columns['date']);
    $columns['location-street-address'] = __( 'Address' );
    $columns['location-city'] = __( 'City' );
    $columns['location-state'] = __( 'State' );
    $columns['location-ZIP'] = __( 'ZIP code' );
    $columns['location-latitude'] = __( 'Latitude' );
    $columns['location-longitude'] = __( 'Longitude' );
    return $columns;
}
//Show the columns
function custom_location_column( $column, $post_id ) {
    echo get_post_meta( $post_id , $column , true );
}
/*** END COLUMN DISPLAY ***/

/*** SAVE LAT/LONG TO CUSTOM TABLE ON SAVE ***/
function save_lat_lng( $post_id, $latitude, $longitude )   
{  
    global $wpdb;  

    // Check that we are editing the right post type  
    if ( 'location' != $_POST['post_type'] )   
    {  
        return;  
    }  

    // Check if we have a lat/lng stored for this property already  
    $check_link = $wpdb->get_row("SELECT * FROM lat_lng_post WHERE post_id = '" . $post_id . "'");  
    if ($check_link != null)   
    {  
        // We already have a lat lng for this post. Update row  
        $wpdb->update(   
        'lat_lng_post',   
        array(   
            'lat' => $latitude,  
            'lng' => $longitude  
        ),   
        array( 'post_id' => $post_id ),   
        array(   
            '%f',  
            '%f'  
        )  
        );  
    }  
    else  
    {  
        // We do not already have a lat lng for this post. Insert row  
        $wpdb->insert(   
        'lat_lng_post',   
        array(   
            'post_id' => $post_id,  
            'lat' => $latitude,  
            'lng' => $longitude  
        ),   
        array(   
            '%d',   
            '%f',  
            '%f'  
        )   
        );  
    }  
} 
/*** END SAVE TO CUSTOM TABLE ***/

/*** ADD location SEARCH VARIABLES TO QUERY VARS ***/
function add_query_vars_filter( $vars ){
  $vars[] = "user_ZIP";
  $vars[] = "user_radius";
  return $vars;
}
add_filter( 'query_vars', 'add_query_vars_filter' );
/*** END QUERY VARS UPDATE ***/

/**** END location POST TYPE ****/

?>

Most of the code is basic “custom-post-type” code. The things to notice are:

Metaboxes: We’re just asking for basic address info. The State input is a textbox just to keep things simple; you’ll probably want to turn that into a select dropdown. There are also read-only fields for the latitude/longitude, so users can see the values and check them if necessary.

Getting latitude/longitude: When the metabox data is saved, you’ll see the following code:

    //Get Lat/Long from address
        $address = $_POST['location-street-address']." ".$_POST['location-city']." ".$_POST['location-state']. " ".$_POST['location-ZIP'];
        $prepAddr = str_replace(' ','+',$address);
        $geocode=file_get_contents('http://maps.google.com/maps/api/geocode/json?address='.$prepAddr.'&sensor=false');
        $output= json_decode($geocode);
        $latitude = $output->results[0]->geometry->location->lat;
        $longitude = $output->results[0]->geometry->location->lng;

    $location_meta['location-latitude'] = $latitude;
    $location_meta['location-longitude'] = $longitude;

This gets the address info, puts it together into a URL-friendly string, and passes it to Google’s geocode service, which returns a JSON object for that location. We extract the latitude and longitude values and save them with the metadata for that location. We can now grab those values any time we want, without having to hit Google or parse JSON every time. They’re happy, we’re happy. Win-win!

This might be obvious, but if you enter a street address, Google Maps can only provide a location if that address is valid. If you give it a bad location, you’ll get a bad result. Double check that your location actually exists!

Also, you don’t need to provide a street address unless you want that level of detail. If all you need is a city/state, or a ZIP code, modify the $address string appropriately.

Get rid of the content editor: This custom post-type doesn’t have a need for a rich-text editor, so to simplify the interface, I’ve removed it:

//Get rid of the content editor
add_action( 'init', 'srd_locations_init' );
function srd_locations_init() {
    remove_post_type_support( 'location', 'editor' );
}

Add search criteria to query_vars: In order to allow users to search on my custom data, I need to add the search terms to WordPress’ query_vars global variable.

/*** ADD location SEARCH VARIABLES TO QUERY VARS ***/
function add_query_vars_filter( $vars ){
  $vars[] = "user_ZIP";
  $vars[] = "user_radius";
  return $vars;
}
add_filter( 'query_vars', 'add_query_vars_filter' );
/*** END QUERY VARS UPDATE ***/

Search box

Having created the Location post type, Let’s build a search-box widget that lets users enter their ZIP code and search for locations within a certain radius of them.

I like to use plugins to create widgets, so that I can add/remove them easily. So to create the search widget, create a new plugin and paste the following code into it:

<?php
/*
Plugin Name: Location search widget
Plugin URI: 
Description: Adds location search box
Version: 1.0
Author: Steven Ray
Author URI: 
License: GPL
*/

/**** FIND A LOCATION WIDGET ****/

// Creating the widget 
class srd_find_location_widget extends WP_Widget {

function __construct() {
parent::__construct(
// Base ID of your widget
'srd_find_location_widget', 

// Widget name will appear in UI
__('Find location', ''), 

// Widget description
array( 'description' => __( 'Displays find a location form.' ), ) 
);
}

// Creating widget front-end
// This is where the action happens
public function widget( $args, $instance ) {
$title = apply_filters( 'widget_title', $instance['title'] );
// before and after widget arguments are defined by themes
echo $args['before_widget'];
if ( ! empty( $title ) )
echo $args['before_title'] . $title . $args['after_title'];

// This is where you run the code and display the output
    echo do_shortcode('[findalocation]');
}

// Widget Backend 
public function form( $instance ) {
if ( isset( $instance[ 'title' ] ) ) {
$title = $instance[ 'title' ];
}
else {
$title = __( 'New title', '' );
}
// Widget admin form
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 
<input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
</p>
<?php 
}

// Updating widget replacing old instances with new
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
return $instance;
}
} // Class srd_find_location_widget ends here

// Register and load the widget
function srd_find_location_load_widget() {
    register_widget( 'srd_find_location_widget' );
}
add_action( 'widgets_init', 'srd_find_location_load_widget' );

/**** END FIND LOCATION WIDGET ****/
?>

You may have noticed that all this widget does is execute a shortcode called [‘findalocation’]. I set it up this way so a user could place the search form in a page if they wanted to. So let’s create the shortcode now.

Create search box shortcode

In functions.php, add the following code:

function find_location_form() {'
    include(ABSPATH."/wp-content/themes/YOUR_THEME_NAME/PATH_TO_FORM_FILE/findalocation.php");
    return $form;}
add_shortcode('findalocation','find_location_form');

Again, pretty simple: all it does is display a PHP file called “findalocation.php“. That file is where the actual form resides. This lets you update the form without having to dig through your function and plugin files (and risking your whole website breaking because of a typo).

Create the search form

We’re now ready to create the search form. Create a file named “findalocation.php” and paste the following code into it:

<?php 

ob_start(); ?>
<div>
<form method="get" action="PATH_TO_RESULTS_PAGE/location_search.php">
<input type="hidden" name="locationsearch" value="Y" />
<div>
    <div>
    <label>Your ZIP code</label>
    <input type="number" min="5" max="5" name="user_ZIP" id="user_ZIP" value="<?php echo get_query_var('user_ZIP');?>" />
    </div>
    <div>
    <label>Locations within:</label>
    <select name="user_radius" id="user_radius">
        <option<?php if(get_query_var('user_radius') == 25000) {echo ' selected="selected"';}?> value="25000">Any</option>
        <option<?php if(get_query_var('user_radius') == 5) {echo ' selected="selected"';}?> value="5">5 miles</option>
        <option<?php if(get_query_var('user_radius') == 10 || !get_query_var('user_radius')) {echo ' selected="selected"';}?> value="10">10 miles</option>
        <option<?php if(get_query_var('user_radius') == 20) {echo ' selected="selected"';}?> value="20">20 miles</option>
        <option<?php if(get_query_var('user_radius') == 50) {echo ' selected="selected"';}?> value="50">50 miles</option>
        <option<?php if(get_query_var('user_radius') == 100) {echo ' selected="selected"';}?> value="100">100 miles</option>
    </select>
    </div>
</div>
<div>
    <input type="submit" value="Find locations" />
</div>
</form>
</div>
<?php $form = ob_get_clean(); ?>

It’s a straightforward HTML form, with a few tweaks:

ob_start() and _ob_get_clean(): Because WordPress’ shortcode function requires output to be returned as a variable, I use output-buffering to capture the form output, and then empty the buffer into the variable $form using ob_get_clean().

get_query_var: The widget checks to see if any of the values are present in the query_var global. If I include the box on the search-results page, it will retain the user’s search values, rather than requiring users to re-enter everything if they want to refine the search.

The hidden input field: This isn’t crucial, but I like to pass the name of the form to my form-parsing scripts, in case I want to use a single script to handle multiple forms, or handle common variables differently depending on the context.

Google Maps API Key

Because the search-results page is going to include a custom embedded Google Map, you need to get an API key from Google. It’s free and fairly easy; Google provides detailed instructions.

Search results page

All right, we now have our custom-post type, and a widget containing our search form. Now let’s add the search-results page. This page is set up so if a user comes to the page directly, they will see a table of all locations. If they come to the page through a search form, it will display both a table and a map.

Create a page called “location_results.php” and make sure the search form’s “action” property points to it. For the demo this is a regular PHP page, but you could also put this code inside a WordPress page template (or call it using a shortcode).

 <?php 
//Check if this is a search-form submission
$locationsearch = $_GET['locationsearch'];

//If it is a search, grab the form variables
if($locationsearch) {
    $user_ZIP = $_GET['user_ZIP'];
    $user_radius = $_GET['user_radius'];

    //Check that user ZIP code is a 5-digit number between 10001 and 99999. If not, display error message.
    if($user_ZIP) {
        if(99999 < $user_ZIP || $user_ZIP < 10001) {$outofrange = 'y';}
        if(!is_numeric($user_ZIP) || $outofrange == 'y') {$form .= '<div>You did not enter a valid 5-digit ZIP code, so we do not know your location.</div>';unset($user_ZIP);unset($locationsearch);}
    }
    //If it's a blank form, act like it's not a search
    if(!$user_ZIP) { unset($locationsearch);}
    //If it's still a search, load Google Map code and div
    if($locationsearch) {
        $form .= '<script src="http://maps.google.com/maps/api/js?key=YOUR_GOOGLE_API_KEY&sensor=false" type="text/javascript"></script>';
        $form .= '<div id="map" style="width: 100%; height: 400px; margin:20px 0;"></div>';
    }

    if($user_ZIP) {
    //Get user lat/long from ZIP
    $geocode=file_get_contents('http://maps.google.com/maps/api/geocode/json?address='.$user_ZIP.'&sensor=false');
    $output= json_decode($geocode);
    $lat = $output->results[0]->geometry->location->lat;
    $lng = $output->results[0]->geometry->location->lng;
    }
}
$homeurl = home_url();

//If a search, display "cancel search" button that takes user back to plain "find a location" page
if($locationsearch) {$form .= '<a href="'.$homeurl.'/PATH_TO_FIND_LOCATION_PAGE/">Clear search</a>';}

//Generate table from database

//If it's a search, execute the search query
if($locationsearch) {
    // WP_Query arguments
    $args = array (
        'post_type'              => 'location',
        'post_status'            => 'published',
        'posts_per_page'        =>    5000,
        'order'                  => 'ASC',
        'orderby'                => 'title',
        'meta_query'             => array(),
    );

    //If distance is a factor, add the meta_query to $args
    if($user_ZIP) {
    //Add filter to compare Locations to user location and radius
    add_filter( 'posts_where' , 'location_posts_where' );  
    }
    // The Query
    $custom_posts = new WP_Query( $args );
    // Remove the filter after executing the query
    remove_filter( 'posts_where' , 'location_posts_where' ); 

}
//Otherwise do the default query
else {
$custom_posts = new WP_Query('post_type=location&orderby=title&order=ASC&posts_per_page=5000');
}

//Begin building results table
$form .= '<table><thead>';
$form .= '<tr><th>Name</th><th>Address</th><th>City</th><th>State</th>';

if($user_ZIP) {$form .= '<th>Miles</th>';}
$form .= '</tr></thead><tbody>';

global $post;
while ($custom_posts->have_posts()) : $custom_posts->the_post();
    $title = get_the_title();
    $street = get_post_meta($post->ID, 'location-street-address', true);
    $city = get_post_meta($post->ID, 'location-city', true);
    $state = get_post_meta($post->ID, 'location-state', true);

    //If street address exists, make it a link to Google Maps
    if($street) {
        $streetplain = $street;
        $mapquery = str_replace(' ','+',$titletext).'+';
        $mapquery = str_replace('UCC','',$mapquery);
        $mapquery .= str_replace(' ','+',$street).'+';
        $mapquery .= str_replace('','+',$city).'+'.$state;
        $street = '<a target="_blank" href="https://www.google.com/maps/search/'.$mapquery.'/">'.$street.'</a>';      
    }

    $form .= '<tr><td>'.$title.'</td><td>'.$street.'</td><td>'.$city.'</td><td>'.$state.'</td>';

    if($locationsearch) {
        //Get location of location
        $latitude = get_post_meta($post->ID, 'location-latitude', true);
        $longitude = get_post_meta($post->ID, 'location-longitude', true);

        //Add location to the Map array
        $locations .= "['<div style=\"line-height:1.35; overflow:hidden; white-space:nowrap;\"><p>$title<br/>$streetplain<br/>$city, $state</p></div>',$latitude,$longitude],";

        if($user_ZIP) {
            //Calculate distance from user ZIP
            $distance = number_format(round(distance($lat,$lng,$latitude,$longitude),1),1);
            $form .= "<td>$distance</td>";    
        }
    }

    $form .= '</tr>';

endwhile;
wp_reset_postdata();

$form .= '</tbody></table>';

if($locationsearch) {
    //If no user location provided, use center of Minnesota to center map
    if(!$user_ZIP) {
        $lat = '45.7326';
        $lng = '-93.9196';
    }
    //Add Google Map init script
    $form .= "  <script type='text/javascript'>
        var locations = [$locations];

        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 10,
          center: new google.maps.LatLng($lat, $lng),
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });

        var infowindow = new google.maps.InfoWindow();

        var marker, i;
        var bounds = new google.maps.LatLngBounds();
        for (i = 0; i < locations.length; i++) {  
          marker = new google.maps.Marker({
            position: new google.maps.LatLng(locations[i][1], locations[i][2]),
            map: map
          });
          bounds.extend(marker.position);

          google.maps.event.addListener(marker, 'click', (function(marker, i) {
            return function() {
              infowindow.setContent(locations[i][0]);
              infowindow.open(map, marker);
            }
          })(marker, i));
        }
        map.fitBounds(bounds);
      </script>";
}
?>

There’s a lot to see here, so let’s go through it piece by piece.

The first part of the code is explained in the comments. It checks to see if the page has been reached through a “search locations” submission. If so, it grabs the user’s ZIP code and chosen radius.

If the ZIP code is valid, it loads an embedded Google Map that we will later populate with data. It then calculates the latitude and longitude of the user’s ZIP code, using the same basic code we used to calculate the latitude/longitude of each location.

Next, we write the MYSQL query. If it’s a search, we do a custom query that takes the user’s location and chosen radius into account. Otherwise, we just grab all the locations:

//If it's a search, execute the search query
if($locationsearch) {
    // WP_Query arguments
    $args = array (
        'post_type'              => 'location',
        'post_status'            => 'published',
        'posts_per_page'        =>    5000,
        'order'                  => 'ASC',
        'orderby'                => 'title',
        'meta_query'             => array(),
    );

    //If distance is a factor, add the meta_query to $args
    if($user_ZIP) {
    //Add filter to compare Locations to user location and radius
    add_filter( 'posts_where' , 'location_posts_where' );  
    }
    // The Query
    $custom_posts = new WP_Query( $args );
    // Remove the filter after executing the query
    remove_filter( 'posts_where' , 'location_posts_where' ); 

}
//Otherwise do the default query
else {
$custom_posts = new WP_Query('post_type=location&orderby=title&order=ASC&posts_per_page=5000');
}

In the search query, you’ll notice that we added a filter called “location_posts_where”. This function is shown and discussed in detail in the next section, but basically it modifies the query to add a WHERE clause that only returns locations within the given radius of the user’s location.

Once we have the list of returned locations, we start building the table.  We set up the table headers, and then use a loop to build each row. If a street address is provided, we automatically turn it into a Google Maps link. We could also have just grabbed the stored latitude/longitude for each location and used them to build the link:

//Begin building results table
$form .= '<table><thead>';
$form .= '<tr><th>Name</th><th>Address</th><th>City</th><th>State</th>';

if($user_ZIP) {$form .= '<th>Miles</th>';}
$form .= '</tr></thead><tbody>';

global $post;
while ($custom_posts->have_posts()) : $custom_posts->the_post();
    $title = get_the_title();
    $street = get_post_meta($post->ID, 'location-street-address', true);
    $city = get_post_meta($post->ID, 'location-city', true);
    $state = get_post_meta($post->ID, 'location-state', true);

    //If street address exists, make it a link to Google Maps
    if($street) {
        $streetplain = $street;
        $mapquery = str_replace(' ','+',$titletext).'+';
        $mapquery = str_replace('UCC','',$mapquery);
        $mapquery .= str_replace(' ','+',$street).'+';
        $mapquery .= str_replace('','+',$city).'+'.$state;
        $street = '<a target="_blank" href="https://www.google.com/maps/search/'.$mapquery.'/">'.$street.'</a>';      
    }

    $form .= '<tr><td>'.$title.'</td><td>'.$street.'</td><td>'.$city.'</td><td>'.$state.'</td>';

If this is a search result, we add a column called “distance” that will show each location’s distance from the user’s location. We also add each location’s information to an array called “$locations” that will be used to populate the embedded map. Note that I’m able to put arbitrary HTML into $locations; this HTML is what will be shown in the pop-up window for each location:

    if($locationsearch) {
        //Get location of location
        $latitude = get_post_meta($post->ID, 'location-latitude', true);
        $longitude = get_post_meta($post->ID, 'location-longitude', true);

        //Add location to the Map array
        $locations .= "['<div style=\"line-height:1.35; overflow:hidden; white-space:nowrap;\"><p>$title<br/>$streetplain<br/>$city, $state</p></div>',$latitude,$longitude],";

        if($user_ZIP) {
            //Calculate distance from user ZIP
            $distance = number_format(round(distance($lat,$lng,$latitude,$longitude),1),1);
            $form .= "<td>$distance</td>";    
        }
    }

    $form .= '</tr>';

endwhile;
wp_reset_postdata();

$form .= '</tbody></table>';

Note that we have called a function called “distance” that calculates the distance between two points (in this case, the user’s ZIP code and the latitude/longitude of each location). That function is explained in detail in the next section.

With the table built, we now turn to populating the embedded map. First, we define a default location — in this case, the center of Minnesota. This code should never actually run, since further up I declared that if there is not a valid ZIP code, then no map is shown. I show it here in case you want to modify the code to show a map even if there is no search.

if($locationsearch) {
    //If no user location provided, use center of Minnesota to center map
    if(!$user_ZIP) {
        $lat = '45.7326';
        $lng = '-93.9196';
    }

Next, we initialize the Google Maps script, and pass it the $locations array containing information on all the returned locations:

    //Add Google Map init script
    $form .= "  <script type='text/javascript'>
        var locations = [$locations];

Next, we define the map, telling the script to use the div with id of "map" that we created earlier, give it an initial zoom, center it on the user's location, and make it a standard "road view" map.
        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 10,
          center: new google.maps.LatLng($lat, $lng),
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });

Next, we define a few variables. “infowindow” is the pop-up you get when you click on a marker in the embedded map; We’re going to fill it with custom content. “marker” is the marker itself; and “bounds” will be used to ensure the map displays all the returned locations.

        var infowindow = new google.maps.InfoWindow();

        var marker, i;
        var bounds = new google.maps.LatLngBounds();

Next, we loop through all the locations. For each one we place a marker, using the latitude/longitude values for that location. We also add the location’s position to “bounds”. This object uses Google’s LatLngBounds() class to calculate the smallest rectangle containing all the positions inside it. Every time we add a location to it, it adjusts the rectangle to include that location.

        for (i = 0; i < locations.length; i++) {  
          marker = new google.maps.Marker({
            position: new google.maps.LatLng(locations[i][1], locations[i][2]),
            map: map
          });
          bounds.extend(marker.position);

Next, we add an event listener to the marker so that when it’s clicked, an Info Window displays. Using Google’s InfoWindow() class, we populate the Info Window with the custom HTML from our locations array.

          google.maps.event.addListener(marker, 'click', (function(marker, i) {
            return function() {
              infowindow.setContent(locations[i][0]);
              infowindow.open(map, marker);
            }
          })(marker, i));
        }

Finally, we reset the zoom level of the embedded map to include all of the returned locations:

        map.fitBounds(bounds);

 

Create latitude/longitude calculation functions

As noted above, the search-results page uses a couple of functions to calculate the distance between two points. Add these functions to functions.php:

<?php
/*** CALCULATE DISTANCE USING LAT/LONG, GIVEN A ZIP CODE ***/
    function location_posts_where( $where )  
    {  
        global $wpdb;
        //Get user location from ZIP
        $geocode=file_get_contents('http://maps.google.com/maps/api/geocode/json?address='.get_query_var('user_ZIP').'&sensor=false');
        $output= json_decode($geocode);
        $lat = $output->results[0]->geometry->location->lat;
        $lng = $output->results[0]->geometry->location->lng;
    ?>
    <?php

        $radius = get_query_var('user_radius'); // (in miles)  

        // Append our radius calculation to the WHERE  
        $where .= " AND $wpdb->posts.ID IN (SELECT post_id FROM lat_lng_post WHERE 
             ( 3959 * acos( cos( radians(" . $lat . ") ) 
                            * cos( radians( lat ) ) 
                            * cos( radians( lng ) 
                            - radians(" . $lng . ") ) 
                            + sin( radians(" . $lat . ") ) 
                            * sin( radians( lat ) ) ) ) <= " . $radius . ")";

        // Return the updated WHERE part of the query  
        return $where;  
    }

/*** CALCULATE DISTANCE BETWEEN TWO POINTS OF LATITUDE/LONGITUDE ***/
function distance($lat1, $lon1, $lat2, $lon2) {
     $theta = $lon1 - $lon2;
     $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) +  cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
     $dist = acos($dist);
     $dist = rad2deg($dist);
     $miles = $dist * 60 * 1.1515;
    return $miles;
}
?>

The first function modifies the WordPress query that is looking for locations. It adds a WHERE clause, so that it only returns locations within the chosen radius of the user.

The second function calculates the distance between each returned location and the user’s ZIP code, so that we can display it in the results table. It’s a general function that can calculate the distance between any two points.

Summary

Phew! That was a lot of detail, but I hope it was worth it. Using the above code, we have done the following:

1. Created a custom post type called “Locations” that automatically calculates the latitude/longitude of each location.

2. Created a search form that can be rendered as either a widget or a shortcode;

3. Created a results page that displays a table of locations;

4. If a user supplies a ZIP code, the results page also returns an embedded map showing all locations within the chosen distance from the user. Clicking on a location shows a popup containing that location’s name and address.