Thursday, November 26, 2009

An address to point (lat,lng) google maps converting class with mootools

Hi,
today I'll post here the code of a class useful to obtain a conversion of a string address into geographical coordinates latitude and longitude.
Maybe sometimes you have to represent some points (i.e. hotels) on a map, dinamically, that is getting datas from a database. That means that you need to insert the hotel geographical coordinates in the database, as it's surely better to make the conversion before inserting data in the DB than on the fly when showing the map, that because the conversion can take a bit of time, or even fail.
So the situation I considered is the following one: we have a form with some fields like address, cap and city, and two fields hidden or readonly that contains the latitude and longitude values. The action of this form inserts this values in the DB, so these latitude and longitude are the same that will be used to generate the map in the public view (soon another post about that).
So the html part of the form could be something like
<input type="text" name="address" id="address" />
<input type="text" name="city" id="city" />
<input type="text" name="cap" id="cap" />
<input type="button" value="convert" onclick="[see more over]" />
<input type="text" name="lat" id="lat" readonly="readonly" />
<input type="text" name="lng" id="lng" readonly="readonly" />
Now the goal was to write a javascript class that shows a map with a marker in the position associated to the address already inserted in the form (that is make the conversion), make the marker draggable and  insert the latitude and longitude values into the 'lat' and 'lng' input fields, so that when submitting the form they could be written in the database.
So here goes my javascript class that clearly requires mootools, I omit here mootools inclusion.
/*
 * Class AddressToPointConverter, requires mootools v>=1.2
 * Written by abidibo, 26/11/2009
 * Copyright: what fuck is copyright?!
 *
 * Converts an address in geographical coordinates (lat,lng) using
 * the googlemaps geocoder, and inserts these values in form fields
 * defined in the constructor
 *
 */
var AddressToPointConverter = new Class({
  
    Implements: [Options],
    options: {
        canvasPosition: 'over', // over | inside
        canvasW: '400px',
        canvasH: '300px',
        zoom: '13',
        noResZoom: '5',
        dftLat: '45',
        dftLng: '7'
    },
    initialize: function(element, latField, lngField, address, options) {
  
        if($defined(options)) this.setOptions(options);
        this.checkOptions();

        this.element = $type(element)=='element'? element:$(element);
        this.latField = $type(latField)=='element'? latField:$(latField);
        this.lngField = $type(lngField)=='element'? lngField:$(lngField);
        this.address = address;

    },
    checkOptions: function() {
        if(this.options.canvasPosition == 'over') {
            var rexp = /[0-9]+px/;
            if(!rexp.test(this.options.canvasW)) this.options.canvasW = '400px';
            if(!rexp.test(this.options.canvasH)) this.options.canvasW = '300px';
        }
    },
    showMap: function() {
        this.renderContainer();
        this.renderCanvas();
        this.canvasContainer.setStyle('width', (this.canvas.getCoordinates().width)+'px');
        this.renderCtrl();
        this.renderMap();
    },
    renderContainer: function() {
        this.canvasContainer = new Element('div', {'id':'map_canvas_container'});
        this.canvasContainer.setStyles({
                'padding': '1px',
                'background-color': '#000',
                'border': '1px solid #000'
            })
        if(this.options.canvasPosition == 'inside') {
            this.canvasContainer.inject(this.element);
        }
        else { // over
            var elementCoord = this.element.getCoordinates();
            this.canvasContainer.setStyles({
                'position': 'absolute',
                'top': elementCoord.top+'px',
                'left':elementCoord.left+'px'
            })
            this.canvasContainer.inject(document.body);
        }
        document.body.addEvent('mousedown', this.checkDisposeContainer.bind(this));  
    },
    renderCanvas: function() {
        this.canvas = new Element('div', {'id':'map_canvas'});
        this.canvas.setStyles({
            'width': this.options.canvasW,
            'height': this.options.canvasH
        })
        this.canvas.inject(this.canvasContainer, 'top');
    },
    renderCtrl: function() {
        var divCtrl = new Element('div').setStyles({'background-color': '#ccc', 'padding': '2px 0px', 'text-align': 'center'});
        var convertButton = new Element('input', {'type':'button', 'value':'convert'});
        convertButton.setStyles({'cursor': 'pointer', 'border': '1px solid #999', 'margin-top': '2px'});
        divCtrl.inject(this.canvasContainer, 'bottom');      
        convertButton.inject(divCtrl, 'top');
        convertButton.addEvent('click', function() {
            this.latField.value = this.point.lat();
            this.lngField.value = this.point.lng();
            this.canvasContainer.dispose();
        }.bind(this));
    },
    checkDisposeContainer: function(evt) {
        if(evt.page.x<this.canvasContainer.getCoordinates().left ||
           evt.page.x>this.canvasContainer.getCoordinates().right ||
           evt.page.y<this.canvasContainer.getCoordinates().top ||
           evt.page.y>this.canvasContainer.getCoordinates().bottom) {
            this.canvasContainer.dispose();
            for(var prop in this) this[prop] = null;
            document.body.removeEvent('mousedown', this.checkDisposeContainer);
        }
         
    },
    renderMap: function() {
        var mapOptions = {
                  zoom: this.options.zoom.toInt(),
                  mapTypeId: google.maps.MapTypeId.ROADMAP
            };
        var map = new google.maps.Map(this.canvas, mapOptions);
        var point;
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode({'address':this.address}, function(results, status) {
            if(status == google.maps.GeocoderStatus.OK) {
                this.point = results[0].geometry.location;
                this.insertMarker(map);
            }  
            else {
                alert("Geocode was not successfull for the following reason: "+status);
                this.point = new google.maps.LatLng(this.options.dftLat, this.options.dftLng);
                map.setZoom(this.options.noResZoom.toInt());
                this.insertMarker(map);
            }
        }.bind(this))

    },
    insertMarker: function(map) {
        map.setCenter(this.point);    
        var marker = new google.maps.Marker({
                      map: map,
                      position: this.point,
            draggable: true,
            title: this.point.lat()+' - '+this.point.lng()
              });
        google.maps.event.addListener(marker, 'mouseup', function() {this.point = marker.getPosition();}.bind(this))

    }
})
Some annotations:
HOW TO CONSTRUCT IT
var myConverter = new AddressToPointConverter(element, latField, lngField, address, options);
  • element: the DOM element or its id of the referer, the element from which we inject the map into the document, taking care of some options explained below.
  • latField: the DOM element or its id of the input field we want to write the latitude to
  • lngField: the DOM element or its id of the input field we want to write the longitude to
  • address: (string) the address to convert
  • options: some options in the form {'opt1':'val1', 'opt2':'val2'...}
OPTIONS
  • (string) canvasPosition : inside | over,  default over. Is the position of the map object we create. inside: the map container is inserted into the element defined in the constructor. over: the map container is positioned absolutely, its top left corner in the same position as the top left corner of element
  • (string) canvasW: default '400px'. The style width of the map (i.e. 400px, 100%) . The % values are allowed only for canvasPosition='inside'.
  • (string) canvasH: default '300px'. The style height of the map.
  • (int) zoom: default '13'. The zoom value when a result is found
  • (int) noResZoom: default '7'. The zoom when no result is found (with the conversion)
  • (float) dftLat: default: '45'. The default latitude of the center of the map when no result is found
  • (float) dftLng: default '7'. The default longitude of the center of the map when no result is found
The conversion may give a result or not. If not the map is centered in a point defined by (dftLat, dftLng) and with zoom = noResZoom.
In this case but even if the conversion is not exactly what we expect, we may drag the marker in order to positioning it in the desired position. Than click on the button convert and the computed point will be the point represented by the marker new position.

That was the class code, let's we see now an example about its usage, always considering our form written above
<input type="text" name="address" id="address" />
<input type="text" name="city" id="city" />
<input type="text" name="cap" id="cap" />
<script type="text/javascript">
function convert() {
                var addressConverter = new AddressToPointConverter('converter', 'lat', 'lng', $('address').value+','+$('cap').value+' '+$('city').value, {'canvasPosition':'over', 'canvasW':'600px', 'canvasH':'400px'});
                addressConverter.showMap();
}
</script>
<input type="button" value="convert" onclick="Asset.javascript('http://maps.google.com/maps/api/js?sensor=true&callback=convert')">
<input type="text" name="lat" id="lat" readonly="readonly" />
<input type="text" name="lng" id="lng" readonly="readonly" />
The map is generated when clicking the "convert" button, first the google library is charged, passing a reference to our function as callback. In our function convert() the object is constructed and the method showMap is invocated. The map is shown. To dispose it it's enough to click outside the map object.
That's all for now, soon a class to show a map with many markers and info windows.