Proximity by City or Zip Code in Drupal 6 with Location and Views

The location module for Drupal 6 is a robust module. On most projects, it gives us 100 percent of what we need. Proximity searching based on zip code is built-in and fairly painless to setup. I’ve recently been tasked to also allow proximity searching based on City. As you might imagine, the request is very similar. Both methods require the use of latitude and longitude (decimals). The difference is that instead of querying the database for latitude and longitude based on zip code (it’s usually termed postal code in Views, Location, etc.), we’re asking for the coordinates of a city. You’ll find that many cities have multiple zip codes, each of which is a row in the location module’s zipcodes database table. In this example, I’m not giving a real honest attempt at solving this issue, but rather I just return the first coordinate that matches the city.

I am going to assume that you have a some understanding of Views exposed filters and the Distance/Proximity filter, which comes from the location module. If not, please search the web for a good tutorial about this. We are basically going to use the same type of setup, with some modifications. The modifications we’re making will happen in a custom module. If you’ve never setup a module, please see Creating a Module for a very basic introduction.

In a nutshell, we’re going to add a few exposed filters to the view, and process them manually in order to set a Distance/Proximity filter. Before you get started, make sure you have some nodes in the system for testing. In the examples below, the content type that has associated ​location data is called a “Walk.” Think of it as a group nature walk. This could be an event, store, or anything else that might have a location. Also, make sure you have imported thezipcodes database table as described in the README for the location module.

To get started, create a view of type location​. Next, create a display within this view (I’m using Page for this tutorial). The view in my demo has the following filters. I’m only mentioning the settings that truly matter or are non-default.

  1. Node: Published = Yes
  2. Node: Type = Walk
  3. Location: City
    1. Exposed
    2. Filter identifier = city
    3. Optional
  4. Location: Postal Code
    1. Exposed
    2. Filter identifier = zip
    3. Optional

Now that you have all of your filters configured, it’d be a good idea to test them. Please test the City and Postal Code filters. They should both be working at this point. More on this in a bit.

NOTE: the order of the filters matters given the way I coded this. If you insert an exposed “Title” filter above the “City” filter, for example, it may break the output if the title filter is left empty. I don’t really have time to provide another example of this, but I did want to mention it. If you put that same exposed “Title” filter BELOW all the fields in this tutorial, it should work fine. If the problem arises in a project, I’ll post the solution I come up with.

Now, for the code. Here’s what it took to get this working in my site. All of this exists within the same module, which I’ve renamed “mymodule” for the purposes of this demo.

First, let’s make sure we validate the exposed filter text inputs. On this site, we’d like to require either a city or a zip code value, but not both. Also, we only want to match zip codes of cities/towns within Connecticut. We’ll use the same zipcodes table for this. This should look familiar to you if you’ve ever used ​hook_form_alter()I should note, another requirement on this site was a customized distance field. You’ll see, in the example below, how to make the distance text input a select list, with a set of specific options. We haven’t added this filter yet, but when we do, this will clean it up a bit.

You should now be able to test the validation. Please test this out a bit to make sure it’s suiting your needs.

So, up to now you should have a working locations view that allows you to get nodes that match a city or zip code. Let’s add the proximity filter now.

Location: Distance / Proximity

  1. Exposed
  2. Operator = Proximity (Rectangular)
  3. Filter identifier = distance
  4. Optional
  5. Origin = Use PHP code to determine latitude/longitude
    1. PHP code for latitude, longitude (make sure you leave the line commented out, exactly as shown)
Now, lets jump back into the code. First, we’ll create the function that we just called in our Distance / Proximity filter. This used to be a single function, but it made more sense to make a helper function that could be used elsewhere. The helper function, called ​mymodule_get_latlong​ returns the latitude and longitude of a city or zip code. The ​mymodule_exposed_filter_proximity​ function passes the exposed filter input values to this helper function and returns the results to the Distance / Proximity exposed filter. Make sense? Maybe the code will make it more clear.
Now make sure you enable the PHP code we added in the last step by uncommenting it. Then, try searching for a city again and notice the SQL query. Here’s what the output of my view looks like at this point (using Hartford​ as a city search term).
 

So we’re almost there. Notice that we’re now matching, hopefully accurately, nodes within 10 miles of Hartford. Also, note that during this test I had the Case sensitive option disabled for the Location: City input. That is the reason for the UPPER within the query. If you notice in the recent code addition, we implemented the same case-insensitivity. It’ll give us a better shot at an exact match. Lastly, let’s talk about what’s actually going on here: We’re getting

  1. all nodes where node is published
  2. all nodes that are of type walk
  3. all nodes where location.city = Hartford
  4. and all nodes within 10 miles of Hartford

Can you see the problem here? The trouble is #3. We do NOT want to limit our search to Hartford because maybe there is a city 8 miles outside of Hartford. The user would expect this to show up in the search results, given that we asked for nodes within 10 miles of Hartford. Please understand that the same issue will happen if we try the zip code field instead. We’ll get an exact zip code match. How do we solve this? Views provides a hook that lets a module alter the views query. This hook, named hook_views_query_alter()​is very powerful. It provides you with full access to the view object and lets you modify the query as desired. In the function below, you will see how to alter the query to remove the effect of the city and zip code exposed filters. Note that this does NOT affect our ability to see the user’s input in your functions; it’s simply letting us remove the exacting WHERE clause additions we do not want. Unfortunately, this isn’t as easy as just removing the WHERE clause parts. You must also be sure to clear out the associated args so that the remaining parts of the WHERE clause receive the appropriate values. Read my comments, maybe they’ll clear this up.

Let’s take one more look at the resulting SQL query, using the same Hartford city search with a 10-mile proximity filter.

Well, there you have it. This is one method of letting users search for nodes within a certain proximity of a city or zip code. There are a few other approaches I can think of, most of which are similar to this one; this is certainly not the only way to do this.  Lastly, I suggest you read through the code to make sure you fully understand the implementation. Happy coding!

Tagged with: , , , , , , ,
Posted in Development

Leave a Reply

Your email address will not be published. Required fields are marked *

*