This tutorial will use JetSmartFilters to create an address search input and radius / distance dropdown for visitor to find nearby branches (Custom Post Type) in a map listing. The built-in JetEngine can only show the map listing in fixed query builder settings or user’s device geo location which cannot fulfill my client’s requirement.
You must follow this tutorial if trying to create a map listing like find best developers in certain location, find nearby coffee shops, find best X in Y location etc.
The final result will be like this.
Tutorial Environment Setup
- WordPress 6.0.1
- Elementor 3.7.2
- Elementor Pro 3.7.3 (Optional, I just use it to style some widgets.)
- JetEngine 3.0.3.1
- JetSmartFilters 2.3.13
- PHP 7.4
- Open LiteSpeed Server
- Google API for Geocoding provider (I prefer Google due to it’s accuracy of address search)
- All custom codes in this tutorial place in theme / child theme functions.php
Plan / Expected Result
- A custom post type Branches to save all branches’ address and phone in the website.
- A map listing display the headquarter only branch with default 10KM distance around it. (Query builder)
- An address search input to allow visitor find nearby branches. (Not using device geo location)
- A dropdown to allow visitor set the search distance / radius in KM.
- Hide the map and show some text if the search result is empty.
Understand Basic Concept
- You can choose different provider for map rendering and geocoding. In this tutorial, I will use leaflet for map rendering, and Google for geocoding. Make sure you tick the Separate Geocoding API key in Maps Settings.
- For map rendering, your CPT only needs to provide latitude and longitude, the map provider will then populate the markers automatically.
- In this tutorial, I just use address field and let the geocoding provider (Google) to extract it’s latitude and longitude automatically. Remember to enable Preload coordinates by address in Maps Settings, so JetEngine will store the latitude and longitude in a hidden custom field instead of keep calling API on each render.
- For the search functionality, we will use code to let JetEngine use geocoding provider to transform visitor’s input address into latitude and longitude data, and do a SQL search (handled by JetEngine) from our CPT filter by distance / radius setting.
- Example: Visitor input “Changi Air Port, Singapore”. We will first use Google to translate this address into latitude and longitude data (Data A). Then search from our CPT where their distance / radius within 10 KM from the Data A. If has result, JetEngine will erase current map markers and replace them with new results.
- Please be noticed that the distance calculation in JetEngine is the radius in KM or Mile from point A to point B without considering the “travel possibility”.
Step 1: Custom Post Type
Very simple, I just add 2 fields for my Branches CPT, Address (text) and Phone (text). I don’t like to use the map field because it’s not user friendly, I don’t even able to enter the address to get latitude and longitude. You could create 2 separate fields to store latitude and longitude as well, this might save your API calls.
Once done, build your branches data.
Tips: If you want to check your location’s latitude and longitude in Google Map, just right click and you will be able to copy them easily.
Step 2: Query Builder
Make sure Map Listings activated in JetEngine Modules settings.
My Query Builder important settings:
- Query Type: Posts Query (Branches is CPT)
- General > Post Type: Branches
- General > Post Status: Publish (So I can use draft to hide certain branches when needed)
- Pagination > Posts Per Page: -1
- Geo Search > Select location: On the map, I pin on my HQ branch location. Honestly, this step really sucks and I hope JetEngine will improve so I could just enter an address or coordinate instead of find my location on the map. (This setting instructs JetEngine set a WHERE lat = y, lng = x clause)
- Geo Search > Address Field: address (Use your field name. Use comma to join your separated latitude and longitude field if you store the coordinates in individual field)
- Geo Search > Distance: 10
- Geo Search > Unites: Kilometers
After creation, turn on the Preview results and you should get some results. In my example, I only get 1 post because there is no other branch within 10KM based on my pinned location. You could play around with the Distance setting and the result should be changed accordingly.
Important – Ensure there is a geo_query_distance
key in each post result. This represents the distance from your pinned location to the branch location. If you don’t see this, probably your Address Field configured wrongly.
Step 3: Listing Grid For Map Marker Popup
Heads to JetEngine > Listings > Add New. Please choose the query in Step 2 as Listing source.
Create a simple popup card design for each map marker (branch) to be used on Map Listing later. You should use Dynamic Field widget to show CPT’s data.
Step 4: Map Listing Widget
Create a page and insert the Map Listing widget. Assign a unique CSS ID for it so we can use JetSmartFilter later.
Important settings on Map Listing widget:
- General > Listing: Choose the designed listing grid in Step 3.
- General > Address Meta Field: address. Please choose Use Lat Lng Address Meta Field if you use lat lng field.
- General > Automatically detect map center: Yes
- Marker > Choose your desire icon, you can turn on Marker Clustering if you like.
- Popup > Add popup preloader: Yes (For better UX)
- Custom Query > Use Custom Query: Yes and choose the query in Step 2
If all configured correctly, the map should rendering well. You can view your page in frontend and clicking the marker should show your branch details dynamically. The total markers should tally with your query builder preview result based on the settings in Step 2. Mine only shows 1 branch on the map no matter how much zoom in or out on the map.
Step 5: Create Search By Address Input Field
Install and activate JetSmartFilters plugin. Add a new Search type filter and follow my settings.
Filter Settings > Search by > By Custom Field (from Query Variable)
Query Settings > Query Variable > use a non valid meta key name. I use itchy_search_by_address
and 100% sure this is not a valid custom field in entire website (current + future).
Next, add a Search Filter widget into the Map Listing page. Select the newly created search filter. Style it nicely and make sure the Query ID targeting the Map Listing widget’s CSS ID in Step 4.
As of now, your page should similar to mine. And if you do a search on a valid address, you will notice the map just rendering without any markers. Nothing wrong with your setup and great that you follow until this step!
Step 6: Use jet-smart-filters/query/final-query Filter
We can use jet-smart-filters/query/final-query
filter to amend the query JetSmartFilters query. In the return $query, just need to add a geo_query
array key with latitude, longitude and distance values so JetEngine can execute the geo search function. We will also use \Jet_Engine\Query_Builder\Manager::instance()
to get our query builder default latitude, longitude and distance settings.
Apply these code in your child theme functions.php
<?php
add_filter( 'jet-smart-filters/query/final-query', 'itchy_amend_map_query', 20 );
function itchy_amend_map_query( $query) {
// My filter Query ID is branch-map, only target this filter query, replace this with yours.
if( $query['jet_smart_filters'] !== 'jet-engine-maps/branch-map' ) return $query;
if( isset( $query['meta_query'] ) ) {
$query_builder = \Jet_Engine\Query_Builder\Manager::instance();
// My Geo Search query builder ID is 2, replace this with yours.
$branch_geo_search_query = $query_builder->get_query_by_id( 2 );
if( !$branch_geo_search_query->query ) return $query;
// Set default query builder lat, lng and radius, make sure the geo search query has these values.
$default = $branch_geo_search_query->query;
$lat = ( isset( $default['geosearch_location']['lat'] ) )? $default['geosearch_location']['lat'] : 4.6170665820643;
$lng = ( isset( $default['geosearch_location']['lng'] ) )? $default['geosearch_location']['lng'] : 101.12098574638;
$radius = ( isset( $default['geosearch_distance'] ) )? $default['geosearch_distance'] : 10;
$address = '';
// Now let's get the address entered by visitor, we use the itchy_search_by_address key defined in Step 4.
foreach( $query['meta_query'] as $meta_query ) {
if( $meta_query['key'] === 'itchy_search_by_address' ) {
$address = sanitize_text_field( trim( $meta_query['value'] ) );
break;
}
}
// Make sure clear the meta query because we really dont want to search branch by any fields.
// If you don't do this, the query will return empty result because no itchy_search_by_address meta key is found.
unset( $query['meta_query'] );
// Only search by address if address is not empty
if( $address != '' ) {
$provider_id = \Jet_Engine\Modules\Maps_Listings\Module::instance()->settings->get( 'geocode_provider' );
$geocode_provider = \Jet_Engine\Modules\Maps_Listings\Module::instance()->providers->get_providers( 'geocode', $provider_id );
// This function will help us retrieve the lat and lng of the address, suggest use Google API to get more accurate data
$latlng = $geocode_provider->get_location_data( $address );
if( $latlng ) {
$lat = $latlng['lat'];
$lng = $latlng['lng'];
}
}
// Set the new geo search query
$query['geo_query'] = array(
'latitude' => $lat,
'longitude' => $lng,
'distance' => $radius,
);
}
return $query;
}
?>
Read my comment, adjust to suit your project. Line 12 – 24 is my way to get some default values for lat
, lng
and radius
from JetEngine Query Builder. You can just hardcode them directly if you want. Line 40 – 49 playing important role which instructs JetEngine to get the address’s coordinate based on your geocoding provider setting. Once again, I recommend to use Google as Geocoding provider, this is because I randomly get false result when using OpenStreetMap, maybe my country’s addresses not fully covered in their database?
Alright, now let’s do a search by using the filter. You should get branches within 10KM (my distance setting in query builder) from the address. Clicking the markers populate the branch’s details as expected!
Step 7: Allow Visitor Set Search Distance
Currently, visitor will still see empty marker result if no branch within 10KM based on the search address. I am going to add 1 dropdown field for them to choose predefined distance for each search.
Let’s create a new Select Type filter and follow my settings.
- Data Source > Manual Input
- Options List > Make sure value is number, label is human readable text
- Query Settings > Query Variable: use a non valid meta key name. I use itchy_search_by_radius
- Query Settings > Comparison operator: anything, or just leave it as Equals
Insert a Select Filter widget in the Map Listing page. Remember to set the Query ID to target the Map Listing correctly.
Okay, now let’s modify the itchy_amend_map_query
function to dynamically set the distance / radius.
<?php
function itchy_amend_map_query( $query) {
// My filter Query ID is branch-map, only target this filter query, replace this with yours.
if( $query['jet_smart_filters'] !== 'jet-engine-maps/branch-map' ) return $query;
if( isset( $query['meta_query'] ) ) {
$query_builder = \Jet_Engine\Query_Builder\Manager::instance();
// My Geo Search query builder ID is 2, replace this with yours.
$branch_geo_search_query = $query_builder->get_query_by_id( 2 );
if( !$branch_geo_search_query->query ) return $query;
// Set default query builder lat, lng and radius, make sure the geo search query has these values.
$default = $branch_geo_search_query->query;
$lat = ( isset( $default['geosearch_location']['lat'] ) )? $default['geosearch_location']['lat'] : 4.6170665820643;
$lng = ( isset( $default['geosearch_location']['lng'] ) )? $default['geosearch_location']['lng'] : 101.12098574638;
$radius = ( isset( $default['geosearch_distance'] ) )? $default['geosearch_distance'] : 10;
$address = '';
foreach( $query['meta_query'] as $meta_query ) {
// Get the address entered by visitor, we use the itchy_search_by_address key defined in Step 4.
if( $meta_query['key'] === 'itchy_search_by_address' ) {
$address = sanitize_text_field( trim( $meta_query['value'] ) );
}
// Get the radius selected by visitor, we use the itchy_search_by_radius key defined in Step 7.
if( $meta_query['key'] === 'itchy_search_by_radius' ) {
$radius = absint( sanitize_text_field( trim( $meta_query['value'] ) ) );
}
}
// Make sure clear the meta query because we really dont want to search branch by any fields.
// If you don't do this, the query will return empty result because no itchy_search_by_address or itchy_search_by_radius meta key is found.
unset( $query['meta_query'] );
// Only search by address if address is not empty
if( $address != '' ) {
$provider_id = \Jet_Engine\Modules\Maps_Listings\Module::instance()->settings->get( 'geocode_provider' );
$geocode_provider = \Jet_Engine\Modules\Maps_Listings\Module::instance()->providers->get_providers( 'geocode', $provider_id );
// This function will help us retrieve the lat and lng of the address, suggest use Google API to get more accurate data
$latlng = $geocode_provider->get_location_data( $address );
if( $latlng ) {
$lat = $latlng['lat'];
$lng = $latlng['lng'];
}
}
// Set the new geo search query
$query['geo_query'] = array(
'latitude' => $lat,
'longitude' => $lng,
'distance' => $radius,
);
}
return $query;
}
?>
The amendment on Line 25 – 36, we retrieve the distance / radius value from the itchy_search_by_radius key
defined earlier in JetSmartFilters.
Play in frontend, you will see it’s working good even without typing in address. Which means, it’s using the default location (My HQ branch in this example) as the search address, and find all nearby branches within the distance from the Search by radius dropdown field.
Step 8: Hide The Map If Result Is Empty
The Widget Visibility option on Map Listing only works on page load. If the new search (AJAX) has no result, the Map will be showing the default location without any branch marker and without any error message to indicate that. I will use JavaScript to fix this.
We can listen on jet-filter-custom-content-render jQuery event to check response result to decide whether show or hide certain elements. If you would like to know more JetSmartFilters events, kindly read this article.
I create a static text using Heading element, give it a CSS ID (branch-not-found
) and some padding to make it looks better. Use custom CSS to set it display:none
because I want this Heading widget only shows if no result from the filter.
JavaScript time! As usual, I place my JavaScript in functions.php for tutorials.
<?php
add_action( 'wp_footer', 'itchy_map_javascript' );
function itchy_map_javascript() {
// Only generate my javascript on the map page
if( !is_page('search-branches') ) return; ?>
<script>
(($)=>{
document.addEventListener('DOMContentLoaded', ()=>{
//We must wait for the jQuery ready event because JetSmartFilters is not initialized yet
$(document).ready(()=>{
// Replace with your actual element IDs
const branchMap = $('#branch-map');
const branchNotFound = $('#branch-not-found');
if( branchMap.length && branchNotFound.length ) {
// Listen on JetSemartFilters result event
branchMap.on('jet-filter-custom-content-render', (evt, response)=>{
if(response.markers.length < 1)
branchMap.hide()
branchNotFound.show()
else
branchMap.show()
branchNotFound.hide()
})
}
})//end jQuery document ready
})//DOMContentLoaded
})(jQuery)
</script>
<?php
}
?>
The code very easy to understand. Try in frontend and it’s now working perfectly.
Conclusion
I like JetEngine Geo Search function and it truly save my time when creating such feature on client’s website. Hope they can improve the Map type field and also Geo Search Select location way in Query Builder. It’s a disaster if keep using map to pin where I want. MetaBox did this very well when handling map type field.
Hope you like this tutorial.
Useful resources used in this tutorial:
jet-smart-filters/query/final-query
filter hook\Jet_Engine\Query_Builder\Manager::instance()
jet-filter-custom-content-render
jQuery event
First off thank you! Is there a way to make so that the Map widget shows all of the listings (CPT's) until a user enters an address/radius to search by?