I’m sketching this one out because it’s a fun mix of things. I’m going to use Gravity Forms to accept submissions, the Open Street Map API will convert the address into latitude/longitude, and then we’ll display the data on a Leaflet JS map. I could have done it all in Google but I was irritated by their credit card requirement.
Gravity Forms
I built a pretty typical form in Gravity Forms to collect some information. I used the standard address field and removed the more detailed street address pieces. That preserved privacy and made the searches more likely to work.
Getting the Lat/Long from Open Street Map API
What I needed to do then was pass the address variables to the Open Street Map API to get lat/long coordinates. Since I was creating a post via Gravity Forms, I could use the gform_after_submission hook to use the form information to add info to the post we just made.
The function below is a bit long but it’s not fancy. It is called right after the form gets submitted.
Section 1 (see code comments) gets any/all of the address information from the form and formats it in the way that the API wants. I push it into an array because using PHP’s implode function is easier than checking for the field, appending it, and adding & for me. I can just let it happen automatically this way.
Section 2 gets the data from the API with the parameters we’re passing from the form. The URL ends up looking something like https://nominatim.openstreetmap.org/search?&city=Richmond&state=Virginia&country=united%20states%20of%20america&limit=1&format=json We’re limiting our responses to one and we’re asking for the JSON format.
You’ll also note that if the country is “United States” I replace it with “United States of America.” I don’t know why that gets better search responses but it does. It took me a while to figure that out. It was also easier to do this than figure out how to change it in the Gravity Forms list.
Section 3 takes the API data and writes it to the custom fields. You’ll notice I do that for the latitude, longitude, and the featured image URL.
function dlinq_mapit_get_latlng($entry, $form){ $post_id = $entry['post_id'];//get the id of the post that was just created $args = array(); //SECTION 1: get the form address values if they exist and put them in an array if(rgar($entry, '2.3')){ $city = 'city=' . rgar($entry, '2.3'); //these line up to the field ID and subfield array_push($args, $city); } if(rgar($entry, '2.4')){ $state = 'state=' . rgar($entry, '2.4'); array_push($args, $state); } if(rgar($entry, '2.5')){ $postalcode = 'postalcode=' . rgar($entry, '2.5'); array_push($args, $postalcode); } if(rgar($entry, '2.6')){ if (rgar($entry, '2.6') == "United States"){ $clean_country = 'United States of America'; } else { $clean_country = rgar($entry, '2.6'); } $country = 'country=' . $clean_country; array_push($args, $country); } $addy = implode('&', $args); $clean = str_replace(' ', '%20', $addy);//replace spaces with %20 for URL kindness urlencode was too much //SECTION 2: Get content from API $resp = dlinq_mapit_fetch($clean); $json = json_decode($resp); $lat = $json[0]->lat; $lng = $json[0]->lon; //SECTION 3: Update the post custom fields update_post_meta( $post_id, 'lat', $lat, '' ); update_post_meta( $post_id, 'lng', $lng, '' ); $img_url = get_the_post_thumbnail_url( $post_id, 'medium' ); update_post_meta( $post_id, 'f_img', $img_url, ''); } //get our API data function dlinq_mapit_fetch($clean){ $url = "https://nominatim.openstreetmap.org/search?{$clean}&limit=1&format=json"; $curl = curl_init($url); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); $headers = array( "Accept: application/json", ); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl,CURLOPT_USERAGENT,'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13');//need a user agent for the API $resp = curl_exec($curl); curl_close($curl); return $resp; }
Adding Custom Fields to the API
Since I have the title and content of the post in the API already, it seemed easiest just to make the custom fields visible as well. I have to write the code below for each custom field I want to expose in the rest API.
function lat_get_post_meta_cb($object, $field_name, $request){ return get_post_meta($object['id'], $field_name, true); } function lat_update_post_meta_cb($value, $object, $field_name){ return update_post_meta($object['id'], $field_name, $value); } add_action('rest_api_init', function(){ register_rest_field('post', 'lat', array( 'get_callback' => 'lat_get_post_meta_cb', 'update_callback' => 'lat_update_post_meta_cb', 'schema' => null ) ); });
Making the Map and Markers
We’ll make a template for the map.
<?php /** * Template Name: Map Display * * Template for displaying a map. * * @package understrap */ get_header(); while ( have_posts() ) : the_post(); echo "<div id='map'></div>"; endwhile; get_footer();?>
Now since this is a plugin, adding page templates is kind of a hassle. I found this solution after trying a few others that didn’t work. So now we have a page.
function wpse255804_add_page_template ($templates) { $templates['map-display.php'] = 'Map Display'; return $templates; } add_filter ('theme_page_templates', 'wpse255804_add_page_template'); function wpse255804_redirect_page_template ($template) { $post = get_post(); $page_template = get_post_meta( $post->ID, '_wp_page_template', true ); if ('map-display.php' == basename ($page_template)) $template = WP_PLUGIN_DIR . '/dlinq_map_it/inc/map-display.php'; return $template; } add_filter ('page_template', 'wpse255804_redirect_page_template');
Now we move from PHP to javascript. This code gets the WordPress JSON and loops through it to write the markers to the map.
const wpJson = document.querySelector('link[rel="https://api.w.org/"]').href + 'wp/v2/posts?per_page=99'; fetch(wpJson).then(response => response.json()).then(data => markerMaker(data)); function markerMaker(data){ data.forEach((item, index) => { const title = item.title.rendered; const lat = item.lat; const long = item.lng; const marker = L.marker([lat, long]).addTo(map); let imgHtml = ''; let bioHtml = ''; if(item.f_img){ const imgUrl = item.f_img; imgHtml = `<img src="${imgUrl}" alt="Picture of the home town for ${title}" width="150px" height="auto">`; } if(item.content.rendered){ const content = item.content.rendered; bioHtml = `<div class="bio">${content}</div>`; } if (lat != '' && long != ''){ marker.bindPopup(`<h2 class="popup-name">${title}</h2> ${imgHtml}${bioHtml}`); } }); }
We can enqueue the script for just the page template using the code below. I had to bump leaflet up to 1.8 due to an issue with clicking on markers in Safari.
add_action('wp_enqueue_scripts', 'dlinq_mapit_load_scripts'); function dlinq_mapit_load_scripts() { $deps = array('jquery'); $version= '1.0'; $in_footer = true; if ( is_page( 'map-display' ) ) { wp_enqueue_script('leaflet', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.8.0-beta.1/leaflet.js', '', '1', $in_footer); wp_enqueue_script('dlinq-map-js', plugin_dir_url( __FILE__) . 'js/dlinq-map.js', 'leaflet', $version, $in_footer); } wp_enqueue_style( 'dlinq-map-css', plugin_dir_url( __FILE__) . 'css/dlinq-map.css'); wp_enqueue_style('leaflet', 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css'); }
Now you end up with something like this.
See the Pen
BL embed example by Tom (@twwoodward)
on CodePen.
This is awesome work, Tom. Thanks for sharing.
Any chance you could chuck the files you’ve used in a git, or package it up if it’s a plugin? Keen to have a fiddle with this, but uncertain where each bit of code has been put (functions.php / plugin folder / new files / somewhere else?) so can’t easily replicate it.
Cheers!
For sure. I have it up as a plugin. The form fields are hard coded so I put an export of the form I used in the form directory for import. You’ll need to change the access token to work with your site.
Thanks so much, Tom. Really appreciate it. 🙂