Filter wordpress posts by distance between coordinates

What I’m trying to do is filter a bunch of wordpress posts by distance between 2 coordinates. There is coordinates, a range and a category inputted by the user that are passed in the URL like this:

/?cat=0&s=5041GW&range=250&lat=51.5654368&lon=5.071263999999928

Then there are posts(not all of them) that have a lat and long field which I created using the plugin Advanced Custom Fields. These are the arguments I pass to get_posts to get the posts that are filtered by category:

Read More
   $args = array(
        'posts_per_page'   => 24,
        'category'         => $_GET["cat"],
        'orderby'          => 'post_date',
        'order'            => 'DESC',
        'post_type'        => 'adressen',
        'post_status'      => 'publish',
    );

Now what I’m trying to do is modify this so that when a range and location are actually passed, the posts will be filtered to only return posts with a location within the range(in kilometers) of the location the user searched for. I can’t seem to be able to find a good solution for this as I’m having a hard time working with wordpress and the plugins it has. I would really appreciate a solution I can understand.

Related posts

Leave a Reply

1 comment

  1. This could be fairly expensive computationally. The straightforward way to do it would be to get all the posts that otherwise fit the criteria, and then loop through them all, discarding posts outside the specified range.

    Difficulties arise because there is not a linear mapping between meters and lat/long. It depends on where you are on the earth. See this question for details. The PHPcoord library exists to make this calculation for you, but due to the slightly approximate nature of my proposed answer, I shall be using approximate methods described on this website using the Haversine formula.

    I shall be using the following formulae:

    • To calculate the distance in km between two lat/lng coords:

      x = Δλ ⋅ cos φm
      y = Δφ
      d = R ⋅ √(x² + y²)
      

      where φ is latitude in radians, λ is longitude in radians, R is earth’s radius (mean radius = 6,371km)

    • To calculate the destination given a starting latlng, distance, and bearing:

      φ2 = asin( sin φ1 ⋅ cos δ + cos φ1 ⋅ sin δ ⋅ cos θ )
      λ2 = λ1 + atan2( sin θ ⋅ sin δ ⋅ cos φ1, cos δ − sin φ1 ⋅ sin φ2 )
      

      where θ is the bearing (clockwise from north), δ is the angular distance d/R, and d is the distance travelled. See atan2.


    We shall therefore define the following helper functions:

    const R = 6371; // km
    
    function distance_between_points_rad($lat1, $lng1, $lat2, $lng2){
        // latlng in radians
        $x = ($lng2-$lng1) * cos(($lat1+$lat2)/2);
        $y = ($lat2-$lat1);
        // return distance in km
        return sqrt($x*$x + $y*$y) * R;
    }
    
    function get_destination_lat_rad($lat1, $lng1, $d, $brng){
        return asin( sin($lat1)*cos($d/R) +
                        cos($lat1)*sin($d/R)*cos($brng) );
    }
    
    function get_destination_lng_rad($lat1, $lng1, $d, $brng){
        $lat2 = get_destination_lat_rad($lat1, $lng1, $d, $brng);
        return $lng1 + atan2(sin($brng)*sin($d/R)*cos($lat1),
                             cos($d/R)-sin($lat1)*sin($lat2));
    }
    
    function get_bounding_box_rad($lat, $lng, $range){
        // latlng in radians, $range in km
        $latmin = get_destination_lat_rad($lat, $lng, $range, 0);
        $latmax = get_destination_lat_rad($lat, $lng, $range, deg2rad(180));
        $lngmax = get_destination_lng_rad($lat, $lng, $range, deg2rad(90));
        $lngmin = get_destination_lng_rad($lat, $lng, $range, deg2rad(270));
        // return approx bounding latlng in radians
        return array($latmin, $latmax, $lngmin, $lngmax);
    }
    
    function distance_between_points_deg($lat1, $lng1, $lat2, $lng2){
        // latlng in degrees
        // return distance in km
        return distance_between_points_rad(
            deg2rad($lat1), deg2rad($lng1), deg2rad($lat2), deg2rad($lng2) );
    }
    
    function get_bounding_box_deg($lat, $lng, $range){
        // latlng in degrees, $range in km
        return array_map(rad2deg,
            get_bounding_box_rad(deg2rad($lat), deg2rad($lng), $range));
    }
    

    (Runnable in ideone)

    Now, the general process shall be:

    1. Create a bounding square-ish box to filter down posts into just a
      few that are about right. This should not be too computationally
      expensive, but is an approximation which might leave some edge posts
      out, and include some posts that do not fit.
    2. Refine the returned
      posts to just those that fit the bill. This is a computationally
      expensive process, hence the first stage. The few posts excluded in
      the first step will still be excluded. The bounding box could
      potentially be made larger to accommodate.

    The query you want to use shall include meta information:
    see here for a useful guide to some of these meta queries

    $lat1 = $_GET['lat']; // degrees
    $lng1 = $_GET['lng']; // degrees
    $range = $_GET['range']; // km
    
    // get the approximate bounding box
    $bbox = get_bounding_box_deg($lat1, $lng1, $range);
    
    // query the posts
    $args = array(
        'posts_per_page'   => 24,
        'category'         => $_GET["cat"],
        'orderby'          => 'post_date',
        'order'            => 'DESC',
        'post_type'        => 'adressen',
        'post_status'      => 'publish',
        'meta_query' => array(
            'relation' => 'AND',
            array(
                'key' => 'lat',
                'value' => array( $bbox[0], $bbox[1] ),
                'type' => 'numeric',
                'compare' => 'BETWEEN'
            ),
            array(
                'key' => 'lng',
                'value' => array( $bbox[2], $bbox[3] ),
                'type' => 'numeric',
                'compare' => 'BETWEEN'
            )
        )
    );
    $the_query = new WP_Query( $args );
    

    Then the posts are filtered down in the loop:

    // Then filter the posts down in the loop
    if ( $the_query->have_posts() ) {
        while ( $the_query->have_posts() ) {
            $the_query->the_post();
            $custom_fields = get_post_custom();
            if (isset($custom_fields['lat']) && isset($custom_fields['lng'])){
                 $lat2 = $custom_fields['lat'];
                 $lng2 = $custom_fields['lng'];
                 $dist = distance_between_points_deg($lat1, $lng1, $lat2, $lng2);
                 if ($dist <= $range){
                     // post is in range
                 } else {
                     // post out of range, discard
                 }
            } else {
                // post has no latlng coords
            }
        }
    } else {
        // no posts found
    }
    /* Restore original Post Data */
    wp_reset_postdata();
    

    The WordPress code is untested, so apologies if errors remain. The general concept is correct though.