2
\$\begingroup\$

If you notice, there are variables of street_address, route, intersection re-used in the entire code base. They are actually from Google Maps geocoder address components.

How can I refactor this one so that I don't have to declare them one by one?

Note that in the geocoder address components, those 3 mentioned above are just part of the 20+ components. I just removed the rest for simplicity sake.

I have tried declaring the address components in a new array, and using for loop, but it gets messy and more complicated.

$(function() {
  $('#shop_formatted_address').autocomplete({
    // This bit uses the geocoder to fetch address values
    source: function(request, response) {
      geocoder.geocode( {'address': request.term }, function(results, status) {
        response($.map(results, function(item) {
          // Get address_components
          for (var i = 0; i < item.address_components.length; i++)
          {
            var addr = item.address_components[i];
            var get_street_address, get_route, get_intersection;
            if (addr.types[0] == 'street_address')
              get_street_address = addr.long_name;
            if (addr.types[0] == 'route')
              get_route = addr.long_name;
            if (addr.types[0] == 'intersection')
              get_intersection = addr.long_name;
          }
          return {
            label: item.formatted_address,
            value: item.formatted_address,
            lat: item.geometry.location.lat(),
            lng: item.geometry.location.lng(),
            street_address: get_street_address,
            route: get_route,
            intersection: get_intersection
          }
        }));
      })
    },

    // This bit is executed upon selection of an address
    select: function(event, ui) {
      clearValue();
      // Get values
      $('#shop_lat').val(ui.item.lat);
      $('#shop_lng').val(ui.item.lng);
      $('#shop_street_address').val(ui.item.street_address);
      $('#shop_route').val(ui.item.route);
      $('#shop_intersection').val(ui.item.intersection);
      var location = new google.maps.LatLng(ui.item.lat, ui.item.lng);
      marker.setPosition(location);
      map.setCenter(location);
    },

    // Changes the current marker when autocomplete dropdown list is focused
    focus: function(event, ui) {
      var location = new google.maps.LatLng(ui.item.lat, ui.item.lng);
      marker.setPosition(location);
      map.setCenter(location);
    }
  });        
});

// Add listener to marker for reverse geocoding
google.maps.event.addListener(marker, 'drag', function() {
  geocoder.geocode({'latLng': marker.getPosition()}, function(results, status) {
    if (status == google.maps.GeocoderStatus.OK) {
      clearValue();
      if (results[0]) {
        // Get address_components
        for (var i = 0; i < results[0].address_components.length; i++)
        {
          var addr = results[0].address_components[i];
          if (addr.types[0] == 'street_address')
            $('#shop_street_address').val(addr.long_name);
          if (addr.types[0] == 'route')
            $('#shop_route').val(addr.long_name);
          if (addr.types[0] == 'intersection')
            $('#shop_intersection').val(addr.long_name);
        }

        $('#shop_formatted_address').val(results[0].formatted_address);
        $('#shop_lat').val(marker.getPosition().lat());
        $('#shop_lng').val(marker.getPosition().lng());
      } else {
        alert('No results found. Please revise the address or adjust the map marker.');
      }
    }
  });
});
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I was about to write up an answer, but then I noticed that inside the $.map(results, ... you're declaring these variables var get_street_address, get_route, get_intersection; but you're overwriting them in the for loop (assuming the loop runs more than once) so when the values are used in the returned object, you're only getting the latest values. Are you certain that's what you want? \$\endgroup\$ Commented Oct 3, 2012 at 15:54
  • \$\begingroup\$ Actually I could have put it outside the loop. \$\endgroup\$
    – Victor
    Commented Oct 3, 2012 at 23:23
  • \$\begingroup\$ Whether the declaration is inside or outside the loop won't make a difference. Variables are scoped to the function they're in. Either way, you're just overwriting the values in each iteration, and returning whatever the values were after the last iteration. To use all the values, you'd need to assign some sort of collection type to each variable, and add items to the collections in each iteration. \$\endgroup\$ Commented Oct 3, 2012 at 23:25

2 Answers 2

1
\$\begingroup\$

You could refactor this using a few arrays to avoid as much typing, but I think readability is paramount, and I don't see many ways for it to be more readable than what you have submitted.

You could try using filter instead, but it will be over 3 times slower (most of the time this won't matter), (N times slower where N is the number of components):

      function getValueOfAddressType(values, addressType) {
        var candidates = $.filter(values, function(i) {
            return values[i].types[0] === addressType;
        });
        return candidates[0].long_name;
      }


$('#shop_formatted_address').autocomplete({
This bit uses the geocoder to fetch address values
source: function(request, response) {
  geocoder.geocode( {'address': request.term }, function(results, status) {
    response($.map(results, function(item) {

      return {
        label: item.formatted_address,
        value: item.formatted_address,
        lat: item.geometry.location.lat(),
        lng: item.geometry.location.lng(),
        street_address: getValueOfAddressType(item.address_components, 'street_address'),
        route: getValueOfAddressType(item.address_components, 'route'),
        intersection: getValueOfAddressType(item.address_components, 'intersection')
      }
\$\endgroup\$
1
  • \$\begingroup\$ You are one crazy guy! I will use your approach. Thanks. \$\endgroup\$
    – Victor
    Commented Oct 5, 2012 at 8:34
1
\$\begingroup\$

Got a little carried away, but here's my take. Haven't been able to test it though, so consider it more as a draft of a different approach, than a drop-in replacement (although if it just works, that's cool too).

Basically, I've DRY'ed the code as much as I could, breaking the logic into several functions, so the final event handlers are as straightforward as possible. I also ran with the fact that you've ID'ed your elements with the names of the values you want them to display, just prefixed with shop_.

$(function () {
  // Get all the elements once and for all
  var elements = (function () {
    var elements = {};
    $.each(["lat", "lng", "street_address", "route", "intersection", "formatted_address"], function (id) {
      elements[id] = $("#shop_" + id);
    });
    return elements;
  }());


  // Generic result-extraction function
  // Takes an array of results, and an array of the address component
  // types it should collect
  function extract(results, types) {
    // Internal function to extract the address components
    function extractAddressComponents(result) {
      var i, l, address, type, extracted = {};
      for( i = 0, l = result.address_components.length ; i < l ; i++ ) {
        address = result.address_components[i];
        type    = address.types[0];
        // Uncomment this next line, if you want to avoid what user1689607 mentions in the comments
        // if( extracted[type] ) { continue }
        if( types.indexOf(type) !== -1 ) {
          extracted[type] = address.long_name;
        }
      }
      return extracted;
    }

    return $.map(results, function (result) {
      $.extend({
        label:  result.formatted_address,
        value:  result.formatted_address,
        lat:    result.geometry.location.lat(),
        lng:    result.geometry.location.lng(),
        latlng: result.geometry.location,
        formatted_address: result.formatted_address
      }, extractAddressComponents(result));
    });
  }

  // Function to set the values of the elements
  function populate(obj) {
    var id;
    for( id in elements ) {
      if( !elements.hasOwnProperty(id) ) { continue; }
      if( obj.hasOwnProperty(id) ) {
        elements[id].val(obj[id]);
      } else {
        elements[id].val("");
      }
    }
  }

  // Center the map and marker
  function center(latlng) {
    if( latlng ) {
      marker.setPosition(latlng);
      map.setCenter(latlng);
    }
  }

  // A little convenience wrapping of the geocoding functions
  var geocode = (function () {
    function geocode(query, callback) {
      geocoder.geocode(query, function (results, status) {
        if( status === google.maps.GeocoderStatus.OK ) {
          callback(extract(results, ["street_address", "route", "intersection"]));
        } else {
          callback([]);
        }
      });
    }

    return {
      address: function (address, callback) { geocode({address: address}, callback); },
      reverse: function (latlng, callback) { geocode({latLng: latlng}, callback); }
    };
  }());

  // Hook it all up here
  elements.formatted_address.autocomplete({
    source: function(request, response) {
      response(geocode.address(request.term));
    },

    select: function (event, ui) {
      clearValue(); // Don't know what this one does, but I left it in
      delete ui.item.formatted_address;
      populate(ui.item);
      center(ui.item.latlng);
    },

    focus: function (event, ui) {
      center(ui.item.latlng);
    }
  });

  google.maps.event.addListener(marker, 'drag', function() {
    var latlng  = marker.getPosition(),
        results = geocode.reverse(latlng),
        result;

    clearValue(); // Don't know what this one does, but I left it in

    if( results.length ) {
      result = results[0];
      $.extend(result, {
        lat: latlng.lat(),
        lng: latlng.lng()
      });
      populate(result);
    } else {
      alert('No results found. Please revise the address or adjust the map marker.');
    }
  });
});

In the end there's more code, but hopefully it'll seem cleaner, and be more maintainable. And there's very, very little repetition, which I believe is what you originally asked for (as I said, I got carried away).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.