﻿(function($) {

    // fireEvent(opts, fn, self, arg)
    //     opts:    (json) jQuery options for this plugin
    //     fn:      (function) function to run
    //     self:    (object) this
    //     arg:     (object) argument to feed to function (fn)
    //
    // note: fn should always return true on successful runs, otherwise return
    //       false
    function fireEvent(opts, fn, self, arg) {
        if ($.isFunction(fn)) {
            try {
                return fn.call(self, arg);
            } catch (error) {
                if (opts.debug) {
                    alert("Error calling googlemaps." + fn + ": " + error);
                } else {
                    throw error;
                }
                return false;
            }
        }
        return true;
    }

    var current = null;

    function Googlemap(root, conf) {
        // current instance
        var self = this;
        if (!current) {
            current = self;
        }

        // internal variables
        var map;
        var geo;
        var bounds;
        var markers;
        //var index = 0;

        // configuration (comments show default values)
        var height = conf.height;
        var width = conf.width;
        var latitude = conf.latitude;    // -35
        var longitude = conf.longitude;   // 150
        var zoom = conf.zoom;        // 4
        var controls = conf.controls;    // true
        var smallcontrols = conf.smallcontrols;
        var labels = conf.labels;      // true
        var html = conf.html;        // null
        var anchor = conf.anchor;      // null
        var addresses = conf.addresses;   // null
        var debug = conf.debug;       // false
        var onMapLoad = conf.onMapLoad;

        // methods
        $.extend(self, {
            // plugin specific
            getVersion: function() { return [1, 0, 0]; },
            getRoot: function() { return root; },

            // google maps specific
            getMap: function() { return map; },
            getGeo: function() { return geo; },
            getAddresses: function() { return addresses; },
            getBounds: function() { return bounds; },
            //getIndex: function() { return index; },
            getMarkers: function() { return markers; },

            // api
            isBrowserCompatible: function() {
                if ($.isFunction(GBrowserIsCompatible))
                    return GBrowserIsCompatible();

                return false;
            },
            initialise: function() {
                self.trace("initialising: " + this);
                if (self.isBrowserCompatible()) {
                    if (onMapLoad != null)
                        self.mapLoad(onMapLoad);

                    size = new GSize(width, height);
                    map = map || new GMap2($(root)[0], { size: size });

                    geo = geo || new GClientGeocoder();
                    bounds = bounds || new GLatLngBounds();
                    markers = markers || new Array();

                    GEvent.addListener(map, 'load', function() {
                        jQuery.event.trigger('mapload', map, self, false);
                    });

                    GEvent.addListener(map, 'click', function() { jQuery.event.trigger('mapclick', map, self, false); });
                    GEvent.addListener(map, 'movestart', function() { jQuery.event.trigger('mapmovestart', map, self, false); });
                    GEvent.addListener(map, 'zoomend', function(oldLevel, newLevel) { jQuery.event.trigger('mapzoomend', { map: map, oldLevel: oldLevel, newLevel: newLevel }, self, false); });

                    // set the map center
                    map.setCenter(new GLatLng(latitude, longitude), zoom);

                    // mark addresses on the map
                    if (addresses) {
                        if (addresses.length > 0) {
                            var i = 0;
                            while (i < addresses.length) {
                                self.geocode(i++);
                            }
                        }
                    }

                    // map type
                    map.setMapType(G_HYBRID_MAP);

                    // map ui
                    if (controls) {
                        var customUI = map.getDefaultUI();
                        customUI.controls.scalecontrol = false;
                        customUI.maptypes.normal = false;
                        customUI.maptypes.satellite = false;
                        customUI.maptypes.physical = false;
                        customUI.controls.largemapcontrol3d = true;
                        map.setUI(customUI);
                    }
                    if (smallcontrols) {
                        var customUI = map.getDefaultUI();
                        customUI.controls.scalecontrol = false;
                        customUI.maptypes.normal = false;
                        customUI.maptypes.satellite = false;
                        customUI.maptypes.physical = false;
                        customUI.controls.largemapcontrol3d = false;
                        customUI.controls.smallzoomcontrol3d = true;
                        customUI.controls.maptypecontrol = false;
                        customUI.controls.menumaptypecontrol = false;
                        map.setUI(customUI);
                    }
                }
            },

            loadXML: function(xml) {
                // markers
                $(xml).find('marker').each(function() {
                    var markerXml = $(this);
                    self.createMarker(new GLatLng(markerXml.attr('latitude'), markerXml.attr('longitude')), markerXml.attr('icon'), markerXml.attr('title'));
                });

                // zoom to fit
                self.optimiseZoomLevel();
            },

            setCenter: function(point, zoom) {
                map.setCenter(point, zoom);
            },

            panTo: function(point) {
                map.panTo(point);
            },

            clearOverlays: function() {
                bounds = new GLatLngBounds();
                map.clearOverlays();
            },

            // geocode(index, address, html, anchor) :
            //     index:     (number) index of the marker (obsolete when label == false)
            //     address:   (string) human readable address to query
            //     html:      [array] what to display on marker's "click" event
            //     anchor:    [array] simulate marker's "click" event outside the map via a link
            geocode: function(index) {
                geo = (geo == null) ? new GClientGeocoder() : geo;
                if (addresses && index >= 0) {
                    self.trace("processing address: [" + addresses[i] + "] (" + index + ")");
                    markers = markers || new Array();

                    // safer way of geocoding - avoids G_GEO_TOO_MANY_QUERIES
                    geo.getLocations(addresses[index], function(response) {
                        var statuscode = response.Status.code;

                        if (statuscode == G_GEO_SUCCESS) {
                            // success!
                            self.trace(response.Placemark);
                            var point = new GLatLng(response.Placemark[0].Point.coordinates[1], response.Placemark[0].Point.coordinates[0], true);

                            // extend bounds
                            bounds = bounds || new GLatLngBounds();
                            //bounds.extend(point); self.trace("bounds extended");

                            // marker
                            var marker = self.createMarker(index, point);
                            self.trace(marker); self.trace("marker created");

                            // marker events
                            GEvent.addListener(marker, "click", function() {
                                zoom = 15;
                                map.setCenter(marker.getLatLng(), zoom);
                            });

                            // add marker to array and display
                            markers[index] = marker;
                            map.addOverlay(marker);

                            // onMarkerLoaded
                            if (fireEvent(conf, self.onMarkerLoaded, self, index) === false) {
                                return self;
                            }
                        } else {
                            if (statuscode == G_GEO_TOO_MANY_QUERIES) {
                                // retry again after a short while
                                var delay = 600;
                                self.trace("index " + index + " will begin retry in " + delay + "ms")
                                setTimeout(function() {
                                    self.geocode(index);
                                }, delay);
                            } else {
                                self.trace("unknown error code: " + statuscode);
                                marker[index] = null;
                            }
                        }
                    });
                }
            },

            // onMarkerLoaded(index)
            //     internal function : DO NOT MODIFY
            onMarkerLoaded: function(index) {
                // set map bounds and zoom level to optimal level so all marker can fit
                return self.optimiseZoomLevel();
            },

            // optimiseZoomLevel()
            optimiseZoomLevel: function() {
                zoom = map.getBoundsZoomLevel(bounds);
                map.setZoom(zoom - 1);
                map.setCenter(bounds.getCenter());
                return true;
            },

            createMarker: function(point, icon, title) {
                // create icon
                var baseIcon = new GIcon(); //(G_DEFAULT_ICON);
                baseIcon.image = "/images/" + icon;
                baseIcon.iconSize = new GSize(31, 32);
                baseIcon.iconAnchor = new GPoint(15, 15);
                baseIcon.infoWindowAnchor = new GPoint(15, 0);

                var markerOptions = {
                    icon: baseIcon,
                    bouncy: true,
                    title: title
                };

                bounds.extend(point);
                self.trace("bounds extended");

                var marker = (labels) ? new GMarker(point, markerOptions) : new GMarker(point);
                map.addOverlay(marker);

                // bubble
                GEvent.addListener(marker, "click", function() {
                    jQuery.event.trigger('markerclick', marker, self, false);
                });
                GEvent.addListener(marker, "mouseover", function() {
                    jQuery.event.trigger('markermouseover', marker, self, false);
                });
                GEvent.addListener(marker, "mouseout", function() {
                    jQuery.event.trigger('markermouseout', marker, self, false);
                });

                return marker;
            },

            /* map events */

            mapLoad: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'mapload', fn, null);
                return self;
            },

            mapClick: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'mapclick', fn, null);
                return self;
            },

            mapMoveStart: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'mapmovestart', fn, null);
                return self;
            },

            mapZoomEnd: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'mapzoomend', fn, null);
                return self;
            },

            /* marker events */

            markerClick: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'markerclick', fn, null);
                return self;
            },

            markerMouseOver: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'markermouseover', fn, null);
                return self;
            },

            markerMouseOut: function(fn) {
                if (fn)
                    jQuery.event.add(self, 'markermouseout', fn, null);
                return self;
            },

            // trace(arg, [args...]) : print everything in the arguments array
            trace: function() {
                if (!debug) return;

                var caller = arguments.caller || "self";
                for (i = 0; i < arguments.length; i++) {
                    var argument = arguments[i]; // print object as it is
                    var line = argument;
                    try {
                        // Firefox, Safari, Opera
                        console.debug(line);
                    } catch (error) {
                        // fails gracefully on IE, Chrome
                        alert(line);
                    }
                }
            }
        });

        function load() {
            self.initialise();
            return self;
        }

        load();
    }


    // jQuery plugin implementation
    jQuery.prototype.googlemap = function(conf) {
        // already constructed --> return API
        var api = this.eq(typeof conf == 'number' ? conf : 0).data("googlemap");
        if (api) { return api; }

        var opts = {
            height: 400,
            width: 615,
            latitude: -23,
            longitude: 133,
            zoom: 4,
            labels: true,
            controls: true,
            smallcontrols: false,
            html: null,
            anchor: null,
            addresses: null,
            debug: false,
            onMapLoad: null
        };

        $.extend(opts, conf);

        this.each(function() {
            var el = new Googlemap($(this), opts);
            $(this).data("googlemap", el);
        });

        return this;
    };

})(jQuery);

