//Author:   mark.sharman
//Created:  10/15/2009 2:12:01 PM
//Amended: Matt Brunsdon, MapData Sciences - 31/03/2010
//Added the ability to search by area type, added jquery tabs
var detail = document.createElement("div");
detail.id = "info";
detail.style.fontFamily = "Arial";
detail.style.color = "#00000F";
detail.style.backgroundColor = "#FFFFFF";
detail.style.border = "1px solid #00000F";
detail.style.width = "300px";
detail.style.left = "50px";
detail.style.top = "47px";
detail.style.visibility = "hidden";


var Locator = {
    InitialZoom: 6,
    InitialCentre: new VELatLong(-38.0, 180.0),
    FindAddressZoom: 12,
    // this needs to change if the zoom level groupings change
    PolygonTransparency: 0.47,
    LineTransparency: 0.8,
    MaximumZoom: 18,
    MinimumZoom: 6,

    Strings: {
        InvalidSearch: 'Please enter a Town/Suburb or double click the map to zoom in to a selected area',
        InvalidSuburb: 'The value of Suburb is not valid',
        MultipleAddressDialogTitle: 'Multiple Address Matches Found',
        MultipleAddressPrompt: 'Choose an address',
        Searching: 'Searching...',
        NoAddressFound: 'No address was found'
    },

    ElementIDs: {
        Map: 'map',
        Boundaries: 'chkBoundaries',
        StatId: 'hdStatId',
        MultipleAddressDialog: 'multipleAddressDialog',
        MultipleAddresses: 'ddlMultipleAddresses',
        Search: 'search',
        SearchArea: 'txtArea',
        SearchStreet: 'txtStreet',
        SearchSuburb: 'txtSuburb',
        SearchState: 'ddlState',
        SearchButton: 'btnFindAddress',
        ResetButton: 'btnReset',
        SearchButtonArea: 'btnFindArea',
        ResetButtonArea: 'btnResetArea',
        ExceptionDialog: 'exception-dialog',
        SearchAreaType: 'ddlArea'
    },

    /// This method is used to validate the search fields. The following function has code
    /// to perform validation on the standard address fields. Use it as a guide for your
    /// custom search fields.
    validateSearchFields: function() {
        var haveValidSuburb = false;
        var txtSuburb = $(this.getElementID(this.ElementIDs.SearchSuburb));

        if (txtSuburb.attr("value") != '' && txtSuburb.attr("value").toLowerCase() != txtSuburb.attr("title").toLowerCase()) {
            haveValidSuburb = true;
        }

        if (!haveValidSuburb) {
            this.displayException(this.Strings.InvalidSearch);
            return false;
        }

        return true;
    },

    clearSearchAddressFields: function() {
        var _SELF = this;

        $(this.getElementID(this.ElementIDs.SearchStreet)).attr("value", "");
        $(this.getElementID(this.ElementIDs.SearchSuburb)).attr("value", "");
        $(this.getElementID(this.ElementIDs.SearchState)).attr("value", "");

        // now restore default state showing the blurred hint
        $(this.getElementID(this.ElementIDs.SearchStreet)).hint();
        $(this.getElementID(this.ElementIDs.SearchSuburb)).hint();
        $(this.getElementID(this.ElementIDs.SearchState)).attr("selectedIndex", 0);

        _SELF._lastFieldFocussed.focus();
    },

    clearSearchAreaField: function() {
        var _SELF = this;

        $(this.getElementID(this.ElementIDs.SearchArea)).attr("value", "");
        // now restore default state showing the blurred hint
        $(this.getElementID(this.ElementIDs.SearchArea)).hint();

        _SELF._lastFieldFocussed.focus();
    },

    /// Reset the search fields back to default values.
    /// Here we set the textfields to the value in the title attribute and then call the blur method to
    /// trigger jQuery hint code.
    resetSearchFields: function() {
        $(this.getElementID(this.ElementIDs.Search) + ' input[title!=""]').attr("value", "").blur();
        $(this.getElementID(this.ElementIDs.Search) + ' input[type="checkbox"]').attr("checked", "");
        $(this.getElementID(this.ElementIDs.SearchState)).attr("selectedIndex", 0);
        $(this.getElementID(this.ElementIDs.SearchAreaType)).attr("selectedIndex", 0);

    },

    /// Builds a JSON representation of the address to be geocoded. This method is 
    /// automatically called, it should return an object that contains the following
    buildAddressSearch: function() {
        // load suburb information
        var txtSuburb = $(this.getElementID(this.ElementIDs.SearchSuburb));
        var suburb = txtSuburb.attr("value");
        if (txtSuburb.attr("value").toLowerCase() == txtSuburb.attr("title").toLowerCase()) {
            suburb = '';
        }

        // load street information
        var txtStreet = $(this.getElementID(this.ElementIDs.SearchStreet));
        var street = txtStreet.attr("value");
        if (txtStreet.attr("value").toLowerCase() == txtStreet.attr("title").toLowerCase()) {
            street = '';
        }

        // load selected state value
        var state = '';
        var ddlState = $(this.getElementID(this.ElementIDs.SearchState));
        if (ddlState.attr("selectedIndex") != 0) {
            state = ddlState.attr("value");
        }

        var _SELF = this;
        Logger.log("buildAddressSearch :: ... ending:" + "," + street + "," + suburb + "," + state + "," + _SELF._country);

        return new MapDS.Address(street, suburb, null, null, state, _SELF._country);
    },

    /// Builds a JSON representation of the search filters available.
    /// MUST return either an object or null. Last line should always be "return null;"
    /// instead of removing, return your object before this line. A return is immediate
    /// and the rest of the code within the method will not execute.
    getSearchFiltersJSON: function() {
        return null;
    },

    /// This method is called from within findNearest. The purpose of this function is to allow
    /// for control of how the marker is created and how the extra POI information is displayed.
    processPOI: function(id, poi) {
        Logger.debug(poi);
        var _SELF = this;
        var mkr = new VEShape(VEShapeType.Pushpin, new VELatLong(poi.Position.Latitude, poi.Position.Longitude));

        var icon = "<img src='images/Orange-callout.png'/>";
        mkr.SetCustomIcon(icon);

        mkr.SetDescription('<div class="popup-content"><h6>{0}</h6><p>{1}{2}</p><p>{3}</p><p>{4}</p></div>'.format(
        poi.Description, ((poi.AddressLine != '' && !_SELF.isUndefined(poi.AddressLine)) ? poi.AddressLine + ', ' : ''),
        ((poi.Locality != '' && !_SELF.isUndefined(poi.Locality)) ? poi.Locality + ', ' : ''),
        poi.Region, poi.Description, poi.RadialDistance, id));

        return mkr;
    },

    /// This method is called from within findNearest. The purpose of this function is to allow
    /// for control of how the searched location marker is created and how the extra information is displayed.
    processLocationPOI: function(poi) {
        var _SELF = this;

        _SELF._pPointLayer.DeleteAllShapes();
        var mkr = new VEShape(VEShapeType.Pushpin, new VELatLong(poi.Position.Latitude, poi.Position.Longitude));
        var icon = "<img src='images/Orange-callout.png'/>";
        mkr.SetCustomIcon(icon);

        mkr.SetDescription('<div class="popup-content"><h5>Your searched location</h5></br><p>{0}</p><p>{1}</p><p>{2}</p></div>'.format((poi.AddressLine != '' ? poi.AddressLine + ', ' : ''), (poi.Locality != '' ? poi.Locality + ', ' : ''), poi.Region));

        _SELF._pPointLayer.AddShape(mkr);

        // DO NOT REMOVE THE FOLLOWING LINE - It is needed for routing purposes.
        _SELF.locationMarker = mkr;
    },


    /// Start of core Locator code
    map: null,
    // object that contains the VEMap instance
    locationMarker: null,

    _foundLocations: null,
    // private
    _lastSearchedLocation: null,
    // private
    _lastFieldFocussed: null,
    // private, the last search field to have focus
    _country: 'NZ',
    _pPointLayer: new VEShapeLayer(),
    _pPolygonLayer: new VEShapeLayer(),
    _featuresClient: null,
    _infoWindow: null,
    _lastCentre: null,
    _lastZoom: null,

    // this is updated if out of range when details are created
    _zoomingFrom: null,
    _detailZoomLevels: null,
    _detailZoom: null,
    _zoomIndex: 0,
    // default
    _polyHiFill: new VEColor(37, 25, 255, 0.3),
    //_polyHiLine: new VEColor(37, 25, 255, 1.0),
    _features: null,
    _target: null,
    _statId: null,
    _NZ06type: null,
    _NZ06code: null,

    _rcZoomLevel: 7,
    _taZoomLevel: 10,
    _auZoomLevel: 14,
    _mbZoomLevel: 17,
    _doFeatures: false,

    _browser: null,
    _browser_version: null,

    // hack for IE z-index issue
    _dropdown_bottom: null,


    /**
    * Returns true if position is a valid sidebar position
    */
    isValidSidebarPosition: function(position) {
        if (position == this.SidebarPositions.Left || position == this.SidebarPositions.Bottom || position == this.SidebarPositions.Right || position == this.SidebarPositions.Top) {
            return true;
        }
        return false;
    },

    /**
    * Returns true if object is undefined
    */
    isUndefined: function(object) {
        return typeof object === "undefined";
    },

    /**
    * Private method
    * Returns true if object is an Image
    * IE doesn't support "instanceof Image" so instead we test that the object is in the DOM then test the tagName is 'IMG'
    */
    isImage: function(object) {
        return this.isHTMLElement(object, 'IMG');
    },

    /**
    * Private method
    * Returns true if object is an HTML Element
    * {type} is a string that represents the type of HTML element that the object should be.
    *      for example. if [type] is "div" then the element should be a div.
    */
    isHTMLElement: function(object, type) {
        if (!type || !(typeof type === "string")) {
            return false;
        }
        if (object && object.nodeType) {
            return (object.tagName === type.toUpperCase() ? true : false);
        }
        return false;
    },

    /**
    * Private method
    * Returns true if input is a number
    */
    isNumeric: function(input) {
        return (input - 0) == input && input.length > 0;
    },

    /**
    * Private method
    * Returns true if object is a function
    */
    isFunction: function(object) {
        return typeof object === "function";
    },

    // CAUTION - changes to this switch         
    // returns the appropriate feature code for the given zoom level.
    getFeatureCode: function(zoomLevel) {

        var _SELF = this;
        if (_SELF._showCensusBoundaries == false) return null;

        switch (zoomLevel) {

            //4, 5, 6, 7                                                                                                          
            case 6:
                return "Census_RC06NZ_L06";
                break;
            case 7:
                return "Census_RC06NZ_L07";
                break;

            //8, 9, 10, 11                                                                                                        
            case 8:
                return "Census_TA06NZ_L09";
                break;
            case 9:
                return "Census_TA06NZ_L09";
                break;
            case 10:
                return "Census_TA06NZ_L10";
                break;
            case 11:
                return "Census_TA06NZ_L10";
                break;

            //12, 13, 14, 15                                                                                                         
            case 12:
                return "Census_AU06NZ_L13";
                break;
            case 13:
                return "Census_AU06NZ_L13";
                break;
            case 14:
                return "Census_AU06NZ_L13";
                break;
            case 15:
                return "Census_AU06NZ_L13";
                break;
            //16, 17, 18                                                                                                         
            case 16:
                return "Census_MB06NZ_L16";
                break;
            case 17:
                return "Census_MB06NZ_L17";
                break;
            case 18:
                return "Census_MB06NZ_L18";
                break;

            default:
                return "Census_RC06NZ_L06";
        }
        return null;
    },

    // returns an appropriate fill colour given the zoom level
    getFeatureColour: function(currentZoom) {
        var _SELF = this;
        var zooms = null;

        // Regional Council zoomlevels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[0], currentZoom)) {
            return new VEColor(51, 102, 102, _SELF.PolygonTransparency);
        }

        // Territorial Authorities zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[1], currentZoom)) {
            return new VEColor(255, 133, 0, _SELF.PolygonTransparency);
        }

        // Area Unit zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[2], currentZoom)) {
            return new VEColor(255, 255, 51, _SELF.PolygonTransparency);
        }

        // Meshblock zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[3], currentZoom)) {
            return new VEColor(102, 51, 0, _SELF.PolygonTransparency);
        }

        // level 5 and below
        return new VEColor(51, 102, 102, _SELF.LineTransparency);
    },

    // returns an appropriate line colour given the zoom level
    getFeatureLineColour: function(currentZoom) {
        var _SELF = this;
        var zooms = null;

        // Regional Council zoomlevels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[0], currentZoom)) {
            return new VEColor(51, 102, 102, _SELF.LineTransparency);
        }

        // Territorial Authorities zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[1], currentZoom)) {
            return new VEColor(255, 133, 0, _SELF.LineTransparency);
        }

        // Area Unit zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[2], currentZoom)) {
            return new VEColor(255, 210, 0, _SELF.LineTransparency);
        }

        // Meshblock zoom levels
        if (_SELF.arrayContains(_SELF._detailZoomLevels[3], currentZoom)) {
            return new VEColor(102, 51, 0, _SELF.LineTransparency);
        }

        // level 5 and below
        return new VEColor(51, 102, 102, _SELF.LineTransparency);
    },

    // Sets up and stores the target feature. Since a target feature may be a multi polygon
    // this may mean setting hilite colour to fill and line of 1..n shapes
    hiliteShape: function(shape) {
        var _SELF = this;
        var features = _SELF._features;

        try {
            for (var i = 0; i < features.length; i++) {
                var context = features[i].Context;
                var selected_polygon_found = false;

                for (var p = 0; p < context.Polygons.length; p++) {
                    // a polygon in this instance is 1..n polygons, where each can have 1..n shapes
                    for (var s = 0; s < context.Polygons[p].Shapes.length; s++) {
                        var shp = context.Polygons[p].Shapes[s];
                        if (shp.iid == shape.iid) {
                            // we found the shape we want hilited
                            // any associate shapes will also need to be hilited
                            // ... so we start going through these polygons and therefore shapes again
                            selected_polygon_found = true;
                            break;
                        }
                    }
                    if (selected_polygon_found) {
                        break;
                    }
                }

                if (selected_polygon_found) {
                    for (var q = 0; q < context.Polygons.length; q++) {
                        for (var r = 0; r < context.Polygons[q].Shapes.length; r++) {
                            var hshp = context.Polygons[q].Shapes[r];
                            hshp.SetFillColor(_SELF._polyHiFill);
                            //hshp.SetLineColor(_SELF._polyHiLine);
                            //hshp.SetLineColor(new VEColor(37, 25, 255, 0.0));
                            hshp.SetLineColor(_SELF.getFeatureLineColour(_SELF._lastZoom));

                            if (context.Polygons[q].ShapeType == 'Donut') {
                                // dial down line colour for donuts to make the extra lines invisible
                                hshp.SetLineColor(new VEColor(37, 25, 255, 0.0));
                            }
                        }
                    }

                    _SELF._target = features[i];

                    // do not precess any further features
                    break;
                }
            }
            showDetails(shape);

        } catch (ex) {
            Logger.log(ex);
        }

        // Logger.log("number of shapes hilited=" + shapes_hilited.toString());
    },

    // Since a target feature may be a multi polygon this may mean setting 
    // the appropriate colour (given the zoom level) to fill and line of 1..n shapes
    unhiliteShape: function(zoom) {
        var _SELF = this;
        var features = _SELF._features; // array:[{context, feature}]
        if (!_SELF.isUndefined(_SELF._target) && _SELF._target != null) {
            try {
                var tDisplayName = rtrim(_SELF._target.Feature.properties.DisplayName);
                for (var i = 0; i < features.length; i++) {
                    var fDisplayName = rtrim(features[i].Feature.properties.DisplayName);

                    if (tDisplayName == fDisplayName) {
                        var context = features[i].Context;
                        for (var p = 0; p < context.Polygons.length; p++) {
                            var found = false;
                            for (var s = 0; s < context.Polygons[p].Shapes.length; s++) {
                                var shp = context.Polygons[p].Shapes[s];
                                shp.SetFillColor(_SELF.getFeatureColour(zoom));
                                shp.SetLineColor(_SELF.getFeatureLineColour(zoom));

                                if (context.Polygons[p].ShapeType == "Donut") {
                                    // dial down line colour for donuts to hide the extra lines
                                    var veColor = _SELF.getFeatureColour(zoom);
                                    shp.SetLineColor(new VEColor(veColor.R, veColor.G, veColor.B, 0.0));
                                }
                            }
                        }
                    }
                }
            } catch (ex) {
                Logger.log(ex);
            }
        }
    },

    // BING - passes the doubleclick event first through the click event
    // ... when clicking on the map, only, both events fire ...
    // when clicking on a VESahpe or VEShapeLayer object on the onclick fires.
    mouseClickHandler: function(e) {
        try {
            var _SELF = Locator;
            if (e.eventName == 'onclick') {
                if (!_SELF.isUndefined(e) && e.elementID != null) {

                    // highlite the target polygon
                    var shape = _SELF.map.GetShapeByID(e.elementID);

                    _SELF.unhiliteShape(e.zoomLevel);
                    _SELF.hiliteShape(shape);

                    // set the current zoom index
                    if (_SELF.arrayContains(_SELF._detailZoomLevels[0], e.zoomLeve)) {
                        _SELF._zoomIndex = 0;
                    }
                    else if (_SELF.arrayContains(_SELF._detailZoomLevels[1], e.zoomLeve)) {
                        _SELF._zoomIndex = 1;
                    }
                    else if (_SELF.arrayContains(_SELF._detailZoomLevels[2], e.zoomLeve)) {
                        _SELF._zoomIndex = 2;
                    }
                    else if (_SELF.arrayContains(_SELF._detailZoomLevels[3], e.zoomLeve)) {
                        _SELF._zoomIndex = 3;
                    }

                    // zoom levels higher than 3 are not managed
                }
            }
            else { //if (e.eventName == 'oncdoublelick') {
                return false;
            }
        }
        catch (ex) {
            Logger.log(ex.message);
        }
    },

    commaFormatted: function(amount) {
        var delimiter = ",";
        var a = amount.split('.', 2);
        var i = parseInt(a[0]);
        if (isNaN(i)) {
            return '';
        }

        var newamount = 0;
        var minus = '';
        if (i < 0) {
            minus = '-';
        }

        i = Math.abs(i);
        var n = new String(i);
        var a = [];
        while (n.length > 3) {
            var nn = n.substr(n.length - 3);
            a.unshift(nn);
            n = n.substr(0, n.length - 3);
        }

        if (n.length > 0) {
            a.unshift(n);
        }

        n = a.join(delimiter);
        newamount = minus + n;
        return newamount;
    },

    formatShapeDetails: function(properties, zoom) {

        // NB - MS - Fields that can contain ..C are MedianAge & MedianIncome
        // 1) MedianAge is a vchar value and ..C values are correctly represented 
        // in the database andin the application
        // 2) MedianIncome is a Money and incorrectly represented as 0 
        // in the database and has to be formatted to ..C here in the application
        var _SELF = this;
        var p = properties;
        var desc = "<div>";

        try {
            desc += "<table id='detail_table' cell-padding=0px >";
            desc += "<th style='width:80%; padding-left:.9em; padding-top:.5em; padding-bottom:.2em; font-size:1.2em; font-weight:bold; color:Teal;'>";

            // HACK where displaynames are not stored with council type they must be appended here...
            if (!_SELF.isUndefined(p.DisplayName)) {

                // set the current zoom index
                if (_SELF.arrayContains(_SELF._detailZoomLevels[0], zoom)) {
                    desc += p.DisplayName;
                }
                else if (_SELF.arrayContains(_SELF._detailZoomLevels[1], zoom)) {
                    desc += p.DisplayName + " territorial authority";
                }
                else if (_SELF.arrayContains(_SELF._detailZoomLevels[2], zoom)) {
                    desc += p.DisplayName + " area unit";
                }
                else if (_SELF.arrayContains(_SELF._detailZoomLevels[3], zoom)) {
                    desc += "Meshblock " + p.DisplayName;
                }
                else {
                    desc += p.DisplayName;
                }
            }

            desc += "</th>";
            desc += "<th style='width:20%;'>";
            desc += "<img src='images/close_button_grey.png' alt='Close' onclick='closeDetail(); return false;' style='float:right; padding-right:.5em; padding-top:.5em; padding-bottom:.2em;'/>";
            desc += "</th>";

            if (!_SELF.isUndefined(p.UsuallyResidentPop)) {
                var pop = _SELF.commaFormatted(p.UsuallyResidentPop);
                desc += "<tr><td class='desc_td'>Number of people:</td><td >" + pop.toString() + "</td></tr>";
            }

            if (!_SELF.isUndefined(p.MedianAge)) {
                var age = p.MedianAge;
                desc += "<tr><td class='desc_td'>Median age:</td><td >" + age.toString() + "</td></tr>";
            }

            // if not at Meshblock level or greater
            if (zoom < _SELF._mbZoomLevel) {
                if (!_SELF.isUndefined(p.MedianIncome)) {
                    var income = p.MedianIncome.toString();

                    // see item 2) ... above
                    if (income != 0) {
                        income = _SELF.commaFormatted(income);
                        desc += "<tr><td class='desc_td'>Median personal income:</td><td >$" + income + "</td></tr>";
                    } else {
                        desc += "<tr><td class='desc_td'>Median personal income:</td><td >..C</td></tr>";
                    }
                }
            }

            if (!_SELF.isUndefined(p.TotalHouseholds)) {
                var households = _SELF.commaFormatted(p.TotalHouseholds);
                desc += "<tr><td class='desc_td'>Number of households:</td><td >" + households.toString() + "</td></tr>";
            }



            if (!_SELF.isUndefined(p.ExternalUri)) {
                desc += "<tr><td class='desc_td'><a href='" + p.ExternalUri.toString() + "' rel='external' target='_blank'>View QuickStats</a></td></tr>";
            }

            desc += "<tr><td class='detail_census'>Source: 2006 Census</td></tr>";

            if (!_SELF.isUndefined(p.Population1) || !_SELF.isUndefined(p.Population2)) {

                desc += "<tr><td colspan='2'><hr/></td></tr>";
            }

            if (!_SELF.isUndefined(p.Population1)) {
                var population1 = _SELF.commaFormatted(p.Population1);
                desc += "<tr><td class='desc_td'>2010 estimated population:</td><td >" + population1.toString() + "</td></tr>";
            }

            if (!_SELF.isUndefined(p.Population2)) {
                var population2 = _SELF.commaFormatted(p.Population2);
                desc += "<tr><td class='desc_td'>2031 projected population:</td><td >" + population2.toString() + "</td></tr>";
            }

            if (!_SELF.isUndefined(p.Population1) || !_SELF.isUndefined(p.Population2)) {

                desc += "<tr><td colspan='2'><a href='http://www.stats.govt.nz/browse_for_stats/population/estimates_and_projections.aspx' target='_blank'> More on estimates and projections</td></tr>";
            }

            desc += '</table>';

            desc += "</div>";
        } catch (ex) {
            alert(ex.message);
        }

        return desc;
    },


    doFeatures: function(mapzoom, newcentre) {
        var _SELF = this;
        var boundaries = $(_SELF.getElementID(_SELF.ElementIDs.Boundaries), false);
        if (boundaries[0].checked == false) {
            //alert("2006 Census Boundaries checked box is currently unchecked.");
            return;
        }
        if (mapzoom >= 5 && mapzoom <= 18) {
            _SELF._lastZoom = mapzoom;
            _SELF.getFeatures(mapzoom);
        }
        else if (mapzoom > 18) {
            _SELF._lastZoom = mapzoom;
        }
        else if (mapzoom <= 4) {
            _SELF._lastZoom = mapzoom;
            _SELF.getFeatures(6);
        }
        else {
            //alert('doFeatures: else');
        }
    },

    validateNZStatsRequest: function(sid) {

        var _SELF = this;
        var featureCode = "";
        var id = parseInt(sid) + 0;

        if (isNaN(id)) {
            alert("NZ Statistics id=" + sid.toString() + ", does not validate.");
        }

        var zooms = null;
        // NZStats feature ranges are determined from id code range...
        if (id >= 1000000 && id < 2000000) {
            _SELF._NZ06code = id.toString().substring(5);
            _SELF._NZ06type = "NZ_RC06";
            _SELF._lastZoom = _SELF._detailZoomLevels[0][0];
        }
        else if (id >= 2000000 && id < 3000000) {
            _SELF._NZ06code = id.toString().substring(4);
            _SELF._NZ06type = "NZ_TA06";
            _SELF._lastZoom = _SELF._detailZoomLevels[1][0];
        }
        else if (id >= 3000000 && id < 4000000) {
            _SELF._NZ06code = id.toString().substring(1);
            _SELF._NZ06type = "NZ_AU06";
            _SELF._lastZoom = _SELF._detailZoomLevels[2][0];
        }
        else { //if (id >= 4000000)  .. or other
            alert("NZ Statistics ID (" + sid.toString() + ") is either invalid or not supported.");
            return false;
        }

        Logger.log(_SELF._NZ06type + "=" + _SELF._NZ06code);
        return true;
    },

    // NB - does not handle concave polygons all that well but is generally pretty good
    centroid: function(pts) {

        var points = new Array();
        for (var p = 0; p < pts.length; p++) {
            points.push(pts[p]);
        }

        var sumY = 0;
        var sumX = 0;
        var partialSum = 0;
        var sum = 0;

        //close polygon
        points.push(points[0]);
        var n = points.length;

        for (var i = 0; i < n - 1; i++) {
            partialSum = points[i].Longitude * points[i + 1].Latitude - points[i + 1].Longitude * points[i].Latitude;
            sum += partialSum;
            sumX += (points[i].Longitude + points[i + 1].Longitude) * partialSum;
            sumY += (points[i].Latitude + points[i + 1].Latitude) * partialSum;
        }

        delete points;

        var area = 0.5 * sum;
        return new VELatLong(sumY / 6 / area, sumX / 6 / area);
    },

    //this finds a feature filter attribute for a passed area category
    //returns string
    getFeatureFilters: function(category) {
        switch (category) {
            case 'Region':
                return "[DisplayName]";
                break;
            case 'TerritoryAuthority':
                return "[DisplayName]";
                break;
            case 'AreaUnit':
                return "[DisplayName]";
                break;
            case 'Meshblock':
                return "[DisplayName]";
                break;
            default:
                return "";
        }
        return null;
    },

    //this finds a default zoom level for a passed area category
    //returns int
    getFeatureZoom: function(category) {
        switch (category) {
            case 'Region':
                return 4;
                break;
            case 'TerritoryAuthority':
                return 8;
                break;
            case 'AreaUnit':
                return 12;
                break;
            case 'Meshblock':
                return 16;
                break;
            default:
                return "";
        }
        return null;
    },

    // this finds a feature based on area name and type then locates on the map 
    // with this feature roughly centred.
    findArea: function(areaName) {

        var _SELF = this;

        //get the area type and query
        var txtAreaType = $(this.getElementID(this.ElementIDs.SearchAreaType));
        txtAreaType = txtAreaType.attr("value");

        var msg = [];
        //do query cleaning and validation if search category is meshblock
        if (txtAreaType == "Meshblock") {
            var meshb_ = areaName;
            if (meshb_.toLowerCase().indexOf("mb") != -1) {
                // trim the MB of the left side
                meshb_ = meshb_.substr(2, meshb_.length - 2);
            }
            var mb = parseInt(meshb_, 10);
            if (isNaN(mb)) {
                msg.push("No feature has been found with :" + areaName.toString());
                _SELF.displayInformation.apply(_SELF, msg);
                return false;
            }
        }

        //determine the feature filter for the selected area category
        var txtFilter = _SELF.getFeatureFilters(txtAreaType);

        //determine the default zoom level for the selected area category
        var defaultZoom = _SELF.getFeatureZoom(txtAreaType);
        Logger.log("Default Zoom Level: " + defaultZoom);

        //determine feature code to use in query
        var featureCode = _SELF.getFeatureCode(defaultZoom);
        var featuresClient = new MapDS.Features.ServiceClient();
        var _filter = '(' + txtFilter + ' like "%' + areaName + '%")';

        Logger.log("findArea: filter: " + _filter);

        // should this be done here??
        $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', '');

        var p = new MapDS.Features.FeatureQuery({
            filter: _filter,
            items: 1
        });
        if (p == null) {
            msg.push("No feature has been found with :" + meshblock_id.toString());
            _SELF.displayInformation.apply(_SELF, msg);
            return false;
        }

        featuresClient.setEncoded('false');

        return featuresClient.getFeatures(p, featureCode, function(results) {

            //Logger.log("Features call results name: " + results.features[0].properties.DisplayName);
            if (!_SELF.isUndefined(results.features) && results.features.length > 0) {

                //_SELF._lastZoom = _SELF._detailZoomLevels[2][0];
                _SELF._lastZoom = defaultZoom;

                try {
                    var type = "";
                    if (results.features[0].geometry.type == 'MultiPolygon') type = 'MultiPolygon';
                    else type = 'Polygon';

                    //Unfortunately we have determined that meshblocks can be multi polygons too 
                    var polygons = null;
                    //context:[  "Polygon", polygons:[ item:["Simple",coords{}  ]  ]  ] ... '1' items element
                    if (type == 'Polygon' && results.features[0].geometry.coordinates.length == 1) {
                        polygons = new Array();
                        polygons.push(_SELF.simplePolygon(results.features[0].geometry.coordinates));
                    }

                    //context:[  "Polygon", polygons:[ item:["Donut",coords{{},{},{} ...}  ]  ]  ] ... 'n' items element
                    //context:[  "MultiPolygon", polygons:[ {item:[{"Simple",coords{}}]}, {item:[{"Donut",coords{}}], ... or more}   ]  ]... 'n; polygons with '1' items element
                    if ((type == 'Polygon' && results.features[0].geometry.coordinates.length > 1) || (type == 'MultiPolygon')) {
                        // this function passes back an array of polygons of length 1..2
                        polygons = _SELF.complexPolygon(_SELF._lastZoom, type, results.features[0].geometry.coordinates);
                    }

                    // this is slightly over kill because meshblocks cannot be multi polygons ...
                    // however its consistent        
                    for (var p = 0; p < polygons.length; p++) {
                        for (var s = 0; s < polygons[p].Shapes.length; s++) {
                            var pdesc = _SELF.formatShapeDetails(results.features[0].properties, _SELF._lastZoom);
                            var shape = polygons[p].Shapes[s];
                            shape.SetDescription(pdesc);
                        }
                    }

                    var context = new Object();
                    context.Polygons = polygons;
                    context.FeatureType = 'Polygon';

                    if (!_SELF.isUndefined(_SELF._target)) {
                        delete _SELF._target;
                    }

                    // set up the target as best we can ... note no shape at this stage
                    _SELF._target = new Object();
                    _SELF._target.Context = context;
                    _SELF._target.Feature = results.features[0];


                    var length = 0;
                    var shape = null;
                    for (var q = 0; q < context.Polygons.length; q++) {
                        for (var s = 0; s < context.Polygons[q].Shapes.length; s++) {
                            if (context.Polygons[q].Shapes[s].GetPoints().length > length) {
                                length = context.Polygons[q].Shapes[s].GetPoints().length;
                                shape = context.Polygons[q].Shapes[s];
                            }
                        }
                    }

                    // ... roughly calculate polygon centre coordinates
                    _SELF._lastCentre = _SELF.centroid(shape.GetPoints());

                }
                catch (ex) {
                    Logger.log(ex.Message);
                }

                $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', '');

                //do features
                //_SELF._doFeatures = false;
                // zoom to the chosen level .. this triggers a more complete getfeatures
                // which populates the map with complete features/shapes                
                var r = _SELF.map.SetCenterAndZoom(_SELF._lastCentre, _SELF._lastZoom);

                // if a user is doing several meshblock searches they may never change zoom level
                // ... therefore we do features explicitly. 
                Logger.log("last center: " + _SELF._lastCentre);
                Logger.log("last zoom: " + _SELF._lastZoom);

                _SELF.doFeatures(_SELF._lastZoom, _SELF._lastZoom);

                return;
            } else {
                Logger.log("Features are undefined ... where filter=" + _filter);
                var msg = new Array();
                msg.push("The query you entered: " + areaName.toString() + " is not valid or does not exist. Please try new information.");
                _SELF.displayInformation.apply(_SELF, msg);
            }
            return;
        });
    },


    // this handles command line request("id") and is intended to work for 
    // any level other than Meshblock.
    processNZStatsRequest: function() {
        var _SELF = this;
        var featuresClient = new MapDS.Features.ServiceClient();
        var _filter = "(" + _SELF._NZ06type + " = " + _SELF._NZ06code + ")";

        Logger.log("processNZStatsRequest: filter: " + _filter);

        var p = new MapDS.Features.FeatureQuery({
            filter: _filter,
            items: 1
        });
        if (p == null) {
            alert("No feature has been found with " + _SELF._NZ06type + ": " + _SELF._NZ06code.toString());
            return false;
        }

        featuresClient.setEncoded('false');
        //... execute query on service, to get a feature so that an approximation
        // of centre and zoom can be made. Given this the zoom event will complete
        // all correct feature management.
        return featuresClient.getFeatures(p, "ALL", function(results) {
            if (!_SELF.isUndefined(results.features) && results.features.length > 0) {

                //Logger.log("processNZStatsRequest: resultsfeature.length:" + results.features.length.toString());
                var type = "";
                if (results.features[0].geometry.type == 'MultiPolygon') type = 'MultiPolygon';
                else type = 'Polygon';

                polygons = new Array();
                polygons = _SELF.complexPolygon(_SELF._lastZoom, type, results.features[0].geometry.coordinates);

                // this is slightly over kill because meshblocks cannot be multi polygons ...
                // however its consistent        
                for (var p = 0; p < polygons.length; p++) {
                    for (var s = 0; s < polygons[p].Shapes.length; s++) {
                        var pdesc = _SELF.formatShapeDetails(results.features[0].properties, _SELF._lastZoom);
                        var shape = polygons[p].Shapes[s];
                        shape.SetDescription(pdesc);
                    }
                }

                var context = new Object();
                context.Polygons = polygons;
                context.FeatureType = type;

                // set up the target as best we can ... note no shape at this stage
                _SELF._target = new Object();
                _SELF._target.Context = null;
                _SELF._target.Feature = results.features[0];

                // to ensure we have the majority of the polygon(s) at centre we need
                // to determine which of a possible 'n' polygons to centre ... this can
                // easily be done by selecting the one with the most points.
                var length = 0;
                var shape = null;
                for (var q = 0; q < context.Polygons.length; q++) {
                    for (var s = 0; s < context.Polygons[q].Shapes.length; s++) {
                        if (context.Polygons[q].Shapes[s].GetPoints().length > length) {
                            length = context.Polygons[q].Shapes[s].GetPoints().length;
                            shape = context.Polygons[q].Shapes[s];
                        }
                    }
                }

                // ... roughly calculate polygon centre coordinates
                _SELF._lastCentre = _SELF.centroid(shape.GetPoints());

                _SELF._doFeatures = true;
                // zoom to the chosen level .. this triggers a more complete getfeatures
                // which populates the map with complete features/shapes
                return _SELF.map.SetCenterAndZoom(_SELF._lastCentre, _SELF._lastZoom);

            } else {

                _SELF._lastZoom = _SELF.InitialZoom;
                _SELF._lastCentre = _SELF.InitialCentre;

                Logger.log("Features are undefined ... where filter=" + _filter);
                alert("Boundary " + _filter + " does not exist. Please correct this and try again.");
            }
            return;
        });
    },

    // the parameter is a polygon
    getVECoordinates: function(coords) {
        var ve_coords = new Array();
        try {
            for (var i = 0; i < coords.length; i++) {
                var ve_latlong = new VELatLong(coords[i][1], coords[i][0]);
                ve_coords.push(ve_latlong);
            }
        } catch (ex) {
            Logger.log(ex);
        }

        return ve_coords;
    },

    // This does the job of stitching together all the polygons into one single VEShape.
    // CAUTION - extra lines are created to join them all together.
    createAdvanceShape: function(polyPoints) {
        if (polyPoints.length > 0) {
            var anchor = polyPoints[0][0];
            var points = polyPoints[0].concat(anchor);
            var lines = new Array();
            var line = new VEShape(VEShapeType.Polyline, points);
            lines.push(line);

            for (var i = 1; i < polyPoints.length; i++) {
                points = points.concat(polyPoints[i], polyPoints[i][0], anchor);
                var line = new VEShape(VEShapeType.Polyline, polyPoints[i].concat(polyPoints[i][0]));
                lines.push(line);
            }

            var polygon = new VEShape(VEShapeType.Polygon, points);
            return lines.concat(polygon);
        }
        return null;
    },


    // Polygon/Simple
    simplePolygon: function(coords) {
        var _SELF = this;

        try {
            var polygon = new Object();
            polygon.ShapeType = 'Simple';
            polygon.Coordinates = new Array();

            var veCoordinates = _SELF.getVECoordinates(coords[0]);
            polygon.Coordinates.push(veCoordinates);

            // store the shape
            var shape = new VEShape(VEShapeType.Polygon, veCoordinates);
            shape.HideIcon();

            polygon.Shapes = new Array();
            polygon.Shapes.push(shape);
        } catch (ex) {
            Logger.log(ex);
        }
        return polygon;
    },

    // Polygon/Donut
    donutPolygon: function(coords) {
        var _SELF = this;
        try {
            var polygon = new Object();
            polygon.ShapeType = 'Donut';
            polygon.Coordinates = new Array();

            var veCoordinates = null;
            for (var j = 0; j < coords.length; j++) {
                veCoordinates = _SELF.getVECoordinates(coords[j]);
                polygon.Coordinates.push(veCoordinates);
            }

            // this method returns a array of shapes...
            var s = _SELF.createAdvanceShape(polygon.Coordinates);

            // for simplicity sake we are only interested in rendering the last shape...
            // which is a combination all shape coordinates.
            var pts = s[s.length - 1].GetPoints();

            // we still want to create this as a Polygon however ...
            shape = new VEShape(VEShapeType.Polygon, pts);
            shape.HideIcon();

            polygon.Shapes = new Array();
            polygon.Shapes.push(shape);
        } catch (ex) {
            Logger.log(ex);
        }
        return polygon;
    },


    // HACK ALERT - NZ Census data has been horrifically thinned for optimization.
    // Thinning has caused all kinds of geographical anomilies to be introduced,
    // such that we daren't build all donut polygons because some are invalid splinters.
    // Currently we only allow the creation of donut polygons in the Area Unit levels.
    complexPolygon: function(currentZoom, featureType, coords) {
        var _SELF = this;
        var zooms = _SELF._detailZoomLevels[2]; // Area Unit zoomlevels
        var polygon = null;
        var polygons = new Array();

        if (featureType == 'Polygon') {
            //Polygon/Donut
            if (_SELF.arrayContains(zooms, currentZoom)) {
                polygon = _SELF.donutPolygon(coords);
            } else {
                polygon = _SELF.simplePolygon(coords);
            }
            polygons.push(polygon);
        } else { // multiPolygon
            for (var j = 0; j < coords.length; j++) {
                if (coords[j].length == 1) {
                    // MultiPolygon/Simple
                    polygon = _SELF.simplePolygon(coords[j]);
                    polygons.push(polygon);
                    delete polygon;
                } else {
                    // MultiPolygon/Donut
                    if (_SELF.arrayContains(zooms, currentZoom)) {
                        polygon = _SELF.donutPolygon(coords[j]);
                    } else {
                        polygon = _SELF.simplePolygon(coords[j]);
                    }
                    polygons.push(polygon);
                    delete polygon;
                }
            }
        }

        // this function always passed back an array of polygons of length 1..n
        return polygons;
    },

    // calls to service to get the features.
    getFeatures: function(currentZoom) {
        var _SELF = this;

        if (_SELF.map.GetZoomLevel() != currentZoom) {
            return;
        }

        var featureCode = _SELF.getFeatureCode(currentZoom);
        if (!featureCode) {
            return;
        }

        // ... VELatLongRectangle
        var bound = _SELF.map.GetMapView();
        var topleft = bound.TopLeftLatLong;
        var bottomright = bound.BottomRightLatLong;

        // ... build a MapDS.Bounds rectangle from the VELatLongRectangle to plug into the features API
        var mapds_bounds = new MapDS.Bounds(topleft.Longitude, bottomright.Latitude, bottomright.Longitude, topleft.Latitude);
        var b = new MapDS.Features.BBOXQuery(mapds_bounds); //, sortby: "lastUpdated" , , { items: 50 }
        _SELF._infoWindow.css('display', 'block');
        _SELF._featuresClient.setEncoded('false');
        _SELF._featuresClient.getFeatures(b, featureCode, function(results) {

            if (_SELF.isUndefined(results.features)) {
                Logger.log("code:" + results.code.toString() + ", message:" + results.message);
                alert("code:" + results.code.toString() + ", message:" + results.message);
                return;
            } else {
                if (!_SELF.isUndefined(_SELF._features) && _SELF._features != null) {
                    delete _SELF._features;
                }
                _SELF._features = new Array();

                var polygon_colour = _SELF.getFeatureColour(currentZoom);
                var line_colour = _SELF.getFeatureLineColour(currentZoom);
                var coords = new Array();

                if (!_SELF.isUndefined(_SELF._pPolygonLayer) && _SELF._pPolygonLayer != null) {
                    _SELF._pPolygonLayer.DeleteAllShapes();
                }

                try {

                    for (var i = 0; i < results.features.length; i++) {
                        // a feature is either Polygon or MultiPolygon
                        var featuretype = 'Polygon';
                        if (results.features[i].geometry.type == 'MultiPolygon') {
                            featuretype = 'MultiPolygon';
                        }

                        var polygons = null; // there may be 1 .. n polygons, this is always an array
                        //context:[  "Polygon", polygons:[ item:["Simple",coords{}  ]  ]  ] ... '1' items element
                        if (featuretype == 'Polygon' && results.features[i].geometry.coordinates.length == 1) {
                            polygons = new Array();
                            polygons.push(_SELF.simplePolygon(results.features[i].geometry.coordinates));
                        }

                        //context:[  "Polygon", polygons:[ item:["Donut",coords{{},{},{} ...}  ]  ]  ] ... 'n' items element
                        //context:[  "MultiPolygon", polygons:[ {item:[{"Simple",coords{}}]}, {item:[{"Donut",coords{}}], ... or more}   ]  ]... 'n; polygons with '1' items element
                        if ((featuretype == 'Polygon' && results.features[i].geometry.coordinates.length > 1) || (featuretype == 'MultiPolygon')) {
                            // this function passes back an array of polygons of length 1..2
                            polygons = _SELF.complexPolygon(currentZoom, featuretype, results.features[i].geometry.coordinates);
                        }

                        // DONUT POLYGONS
                        // to create the complex shape of a donut polygon requires that we we combine
                        // all component coordinates in an appropriate order to represent a single shape
                        // that is a conbination of all shapes...
                        // the coordinates will be an array of coordintate pairs 
                        // the first array of coordinate pairs is the external array
                        // ... all following arrays will be holes in the donut
                        // this algorythm takes the coordinate pairs, knowing that the first is the
                        // external coordinate set ... and therefore contains starting and ending point
                        // of any single polygon that is generated.
                        // LIMITATION - the resulting polygon will contain joining lines
                        // set VEShape properties ... note that this may be for multiple shapes
                        for (var p = 0; p < polygons.length; p++) {

                            // identify whether we are looking at the target feature
                            var hiliteTarget = false;
                            var fDisplayName = rtrim(results.features[i].properties.DisplayName);

                            if (!_SELF.isUndefined(_SELF._target) && _SELF._target != null) {
                                var tDisplayName = rtrim(_SELF._target.Feature.properties.DisplayName);
                                if (fDisplayName == tDisplayName) {
                                    hiliteTarget = true;

                                    // complete the target data if we are doing a getFeatures as a result
                                    // of an NzStats ID query
                                    if (_SELF._target.Context == null) {
                                        var context = new Object();
                                        context.Polygons = polygons;
                                        _SELF._target.Context
                                    }
                                }
                            }

                            for (var s = 0; s < polygons[p].Shapes.length; s++) {
                                try {
                                    var shape = polygons[p].Shapes[s];

                                    // if a target is already selected ensure that it gets the correct
                                    // fill and line setting
                                    if (!hiliteTarget) {
                                        shape.SetFillColor(polygon_colour);
                                        shape.SetLineColor(line_colour);

                                        // reset line colour and transparency if this is a donut polygon
                                        if (polygons[p].ShapeType == 'Donut') {
                                            // dial down line colour for donuts to hide the extra lines
                                            shape.SetLineColor(new VEColor(polygon_colour.R, polygon_colour.G, polygon_colour.B, 0.0));
                                        }

                                    } else { // this is the target feature
                                        shape.SetFillColor(_SELF._polyHiFill);
                                        //shape.SetLineColor(_SELF._polyHiLine);
                                        //shape.SetLineColor(new VEColor(37, 25, 255, 0.0));
                                        shape.SetLineColor(line_colour);

                                        // reset line colour and transparency if this is a donut polygon
                                        if (polygons[p].ShapeType == 'Donut') {
                                            // dial down line colour for donuts to hide the extra lines
                                            shape.SetLineColor(new VEColor(37, 25, 255, 0.0));
                                        }
                                    }

                                    var pdesc = _SELF.formatShapeDetails(results.features[i].properties, currentZoom);
                                    shape.SetDescription(pdesc);

                                    // add the shape to the map
                                    _SELF._pPolygonLayer.AddShape(shape);
                                } catch (ex) {
                                    Logger.log(ex);
                                }
                            }
                        }

                        // the bing data
                        var context = new Object();
                        context.FeatureType = featuretype;
                        context.Polygons = polygons;
                        delete polygons;

                        // the feature data
                        var f = new Object();
                        f.Context = context;
                        f.Feature = results.features[i];
                        _SELF._features.push(f);
                        delete f;

                        // accrue this locally only  for the life time of the function                        
                        coords.push(context);
                        delete context;
                    }

                } catch (ex) {
                    Logger.log(ex);
                }

                if (!_SELF.isUndefined(_SELF._target)) {
                    //showDetails(shape here);
                }

                delete coords;
            }

            _SELF._infoWindow.css('display', 'none');
            _SELF._doFeatures = false;
        });
    },

    /**
    * Returns the id string asked for, will prepend # unless the second parameter is false.
    */
    getElementID: function(id, withHash) {
        if (this.isUndefined(withHash)) {
            withHash = true;
        }
        return (withHash ? "#" : "") + id;
    },

    findElement: function(id) {
        var str = '';
        for (var i = 0; i < document.forms[0].length; i++) {
            if (!this.isUndefined(document.forms[0][i].name) && document.forms[0][i].name != "") {
                var c = $(this.getElementID(id, false));
                if (document.forms[0][i].name.indexOf(c.selector) != -1) {
                    return document.forms[0][i];
                }
            }
        }
        return null;
    },

    /**
    * Will fire a call to load up the exception dialog, the HTML will be set using the message parameter.
    */
    displayException: function(errorMessage) {
        $(this.getElementID(this.ElementIDs.ExceptionDialog)).html(errorMessage).dialog('open');
    },


    displayInformation: function(errorMessage) {
        $(this.getElementID(this.ElementIDs.ExceptionDialog)).html(errorMessage).dialog('open');
    },

    /**
    * Will save a POI as the last location.
    */
    saveLocationPOI: function(poi) {
        this._lastSearchedLocation = poi;
    },

    /**
    * Private method
    * Assumption is made that at least the first parameter exists and that it is a number. 
    * The same number assumption is made for precision if it is given.
    * Default for precision is 1, therefore no change.
    */
    round: function(val, precision) {
        precision = (arguments.length > 1) ? Math.pow(10, arguments[1]) : 1;
        return Math.round(val * precision) / precision;
    },

    /**
    * Will return the full bearing from the shortened string.
    */
    expandBearing: function(bearing) {
        switch (bearing) {
            case 'N':
                return 'North';
                break;
            case 'S':
                return 'South';
                break;
            case 'E':
                return 'East';
                break;
            case 'W':
                return 'West';
                break;
            case 'NE':
                return 'North East';
                break;
            case 'NW':
                return 'North West';
                break;
            case 'SE':
                return 'South East';
                break;
            case 'SW':
                return 'South West';
                break;
            default:
                return '';
        }
    },

    /**
    * Will convert seconds to a formatted time string
    */
    convertToTime: function(seconds) {
        var hrs = Math.floor(seconds / 3600);
        var mins = Math.floor(seconds / 60) - (hrs * 60);
        var secs = seconds - ((hrs * 3600) + (mins * 60));
        var msg = 'about ';
        if (hrs > 0) {
            msg = hrs + 'hrs ';
        }
        if (mins > 0) {
            msg += mins + 'mins ';
        }
        if (hrs == 0 && mins == 0) {
            msg = 'less than 1 minute';
        }
        return msg;
    },

    /**
    * Resets everything!  ... to default settings
    */

    reset: function() {

        var _SELF = this;

        closeDetail();

        _SELF._foundLocations = null;
        _SELF._lastSearchedLocation = null;
        _SELF.locationMarker = null;
        _SELF._features = new Array();
        _SELF.resetSearchFields();

        var boundaries = $(_SELF.getElementID(_SELF.ElementIDs.Boundaries));
        boundaries[0].checked = true;

        if (!_SELF.isUndefined(_SELF._target) && _SELF._target != null) {
            delete _SELF._target;
        }

        if (!_SELF.isUndefined(_SELF._pPolygonLayer) && _SELF._pPolygonLayer != null) {
            _SELF._pPolygonLayer.DeleteAllShapes();
        }

        if (!_SELF.isUndefined(_SELF._pPointLayer) && _SELF._pPointLayer != null) {
            _SELF._pPointLayer.DeleteAllShapes();
        }

        if (_SELF.map != null) _SELF.map.SetCenterAndZoom(_SELF.InitialCentre, _SELF.InitialZoom);

        // since the default is to use 2006 census data ... we need to get features again
        if (_SELF.map != null) _SELF.doFeatures(_SELF.InitialZoom, _SELF.InitialCentre);
    },

    /**
    * Performs a call to geocode in order to geocode an address. 
    */
    findAddress: function() {
        Logger.log("findAddress: ... starting");
        var _SELF = this;

        // check the findArea field
        var doingFindArea = false;
        var txtArea = $(this.getElementID(this.ElementIDs.SearchArea));
        var area = rtrim(txtArea.attr("value"));

        if (txtArea.attr("value").toLowerCase() != txtArea.attr("title").toLowerCase() && area.length > 0) {
            doingFindArea = true;
        }

        // adjust validation to accept findArea values
        if (!doingFindArea) {
            var areSearchFieldsValid = _SELF.validateSearchFields();
            if (!areSearchFieldsValid) {
                return;
            }
        }

        // lock down the find button
        $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', 'true');

        Logger.log("findAddress: ... ending with geocode call");
        closeDetail();

        // if this is an address search use this
        if (!doingFindArea) {
            var g = new MapDS.Geocode.Geocoder();
            g.getLocations(_SELF.buildAddressSearch(), function(results) {
                _SELF.geocodeCallback(results);
            });
        } else {
            // else it's a findArea search 
            _SELF.findArea(area);
        }
    },

    // this is more or less where the polygon stuff should be done based on a correct geocodet
    findAddressCallback: function(address) {

        // have to handle multiple addresses here now ... 
        Logger.log("findAddressCallback: ... starting");
        var _SELF = this;

        _SELF.saveLocationPOI.apply(_SELF, [address]);
        _SELF.processLocationPOI(address);

        $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', '');
        Logger.log("findAddressCallback: ... ending");

        _SELF._lastZoom = _SELF.FindAddressZoom;
        _SELF._lastCentre = new VELatLong(address.Position.Latitude, address.Position.Longitude);
        _SELF.map.SetCenterAndZoom(_SELF._lastCentre, _SELF._lastZoom);

        _SELF.doFeatures(_SELF._lastZoom);
    },

    /**
    * Performs a call to Geocode an address. If a single match is found then the method 
    * will automatically call findNearest. Multiple matches are displayed within a 
    * dialog for the user to select.
    */

    formatGeocodingError: function(code) {
        var descr = "";
        switch (code) {
            case 0:
                descr = "Address not found";
                break;
            case 110:
                descr = "Could not find address";
                break;
            case 120:
                descr = "Invalid postcode";
                break;
            case 888:
                descr = "Manual positioning, not an exact match";
                break;
            case 999:
                descr = "Manual positioning, exact match";
                break;
            default:
                descr = "Unknown error";
        }
        return descr;
    },

    geocodeCallback: function(data) {
        // make jQuery Ajax call...
        var _SELF = this;
        Logger.log("geocodeCallback ... starting ");

        if (data.Code != '1' && data.Code != '100') {
            // unlock down the find button
            $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', '');
            var error_message = new Array();
            error_message.push(_SELF.formatGeocodingError(data.Code));
            _SELF.displayException.apply(_SELF, error_message);
            return;
        } else {

            _SELF._foundLocations = data.Locations;

            // need to display selection if multiple
            if (_SELF._foundLocations.length > 1) {
                // display modal window
                $(_SELF.getElementID(_SELF.ElementIDs.MultipleAddressDialog)).find(
                _SELF.getElementID(_SELF.ElementIDs.MultipleAddresses)).html('');

                $("<option/>").attr('value', '0').html(_SELF.Strings.MultipleAddressPrompt).appendTo(
                $(_SELF.getElementID(_SELF.ElementIDs.MultipleAddressDialog)).find(
                _SELF.getElementID(_SELF.ElementIDs.MultipleAddresses)));

                $.each(_SELF._foundLocations, function(i, address) {
                    $("<option/>").html((address.AddressLine != '' ? address.AddressLine + ', ' : '') + address.Locality + ', ' + address.Region).appendTo(
                    $(_SELF.getElementID(_SELF.ElementIDs.MultipleAddressDialog)).find(
                    _SELF.getElementID(_SELF.ElementIDs.MultipleAddresses)));
                });

                // unlock down the find button
                $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).attr('disabled', '');
                $(_SELF.getElementID(_SELF.ElementIDs.MultipleAddressDialog)).dialog('open');

            } else {
                _SELF.findAddressCallback(_SELF._foundLocations[0]);
            }
        }
    },

    arrayContains: function(a, v) {
        for (var i = 0; i < a.length; i++) {
            if (a[i] == v) {
                return true;
            }
        }
        return false;
    },

    /**
    * This method is hooked into the window.onload automatically.
    */
    initMap: function(lat, lng, zoom) {
        var _SELF = this;

        _SELF._infoWindow = $("#info");

        // default is checked
        this.map = new VEMap(this.getElementID(this.ElementIDs.Map, false));
        this.map.SetCredentials("AqLB8facMvXsgikxcMEtnMfxlH3qraxNngVkowSfFtxo9B1bFzJzEYk8FpwDW3v5");
        this.map.MaxZoomLevel = 17;
        this.map.MinZoomLevel = 5;

        var mopt = new VEMapOptions();
        mopt.EnableBirdseye = false;
        mopt.EnableDashboardLabels = false;

        this.map.LoadMap(_SELF.InitialCentre, _SELF.InitialZoom, VEMapStyle.Road, false, VEMapMode.Mode2D, false, 0, mopt);

        // this setting fixed the issue where a default polygon coordinate clipping was going on
        // ... the effect of which was that the polygon boundaries didn't touch very well.
        this.map.EnableShapeDisplayThreshold(false);

        this.map.SetScaleBarDistanceUnit(VEDistanceUnit.Kilometers);

        this.map.AddShapeLayer(_SELF._pPointLayer);
        this.map.AddShapeLayer(_SELF._pPolygonLayer);

        this.map.AddControl(detail);

        _SELF._lastCentre = _SELF.InitialCentre;
        _SELF._lastZoom = _SELF.InitialZoom;

        this.map.AttachEvent("onendpan", function(e) {

            var previous = _SELF._lastCentre;

            _SELF._lastCentre = _SELF.map.GetCenter();
            _SELF._lastZoom = _SELF.map.GetZoomLevel();

            if (_SELF._lastCentre.Longitude < 159 || _SELF._lastCentre.Longitude > 179) {
                _SELF._lastCentre.Longitude = previous.Longitude;
                _SELF.map.SetCenter(_SELF._lastCentre);
            }

            if (_SELF._lastCentre.Latitude > -30.0 || _SELF._lastCentre.Latitude < -50) {
                _SELF._lastCentre.Latitude = previous.Latitude;
                _SELF.map.SetCenter(_SELF._lastCentre);
            }

            // PAN DISTANCE RATIOS 
            // This is an attempt to provide a distance ratio to determine whether a pan
            // has significantly changed the set of features. At certain zoomlevels, given
            // the feature type (see feature type zoom level grouping discussions), a given
            // distance change may mean the previously recovered feature set is now no longer
            // applicable, and therefore we need to get features again.
            //... these zoomlevel/distance ratios have been determined imperically
            // CAUTION changes to these ratios are permitted but should then be tested.
            // Note that changing them may have an impact on pan/highlighted polygon functionality
            var dist = 0.0;

            switch (_SELF._lastZoom) {
                case 18:
                    dist = 0.003;
                    break;
                case 17:
                    dist = 0.004;
                    break;
                case 16:
                    dist = 0.005;
                    break;
                case 15:
                    dist = 0.006;
                    break;
                case 14:
                    dist = 0.007;
                    break;
                case 13:
                    dist = 0.008;
                    break;
                case 12:
                    dist = 0.009;
                    break;
                case 11:
                    dist = 0.01;
                    break;
                case 10:
                    dist = 0.02;
                    break;
                case 9:
                    dist = 0.03;
                    break;
                case 8:
                    dist = 0.04;
                    break;
                default:
                    dist = 0.05;
            }

            var hasMoved = (Math.abs(previous.Longitude - _SELF._lastCentre.Longitude) > dist || Math.abs(previous.Latitude - _SELF._lastCentre.Latitude) > dist) ? true : false;

            if (hasMoved || _SELF._doFeatures) {
                _SELF.doFeatures(_SELF._lastZoom, _SELF._lastCentre);
            }
        });

        this.map.AttachEvent("onclick", _SELF.mouseClickHandler);
        this.map.AttachEvent("ondoubleclick", _SELF.mouseClickHandler);

        this.map.AttachEvent("onstartzoom", function(e) {
            _SELF._zoomingFrom = e.zoomLevel;
            //_SELF.displayZooms("onstartzoom", _SELF._zoomingFrom.toString());
        });

        this.map.AttachEvent("onendzoom", function(e) {

            if (e.zoomLevel <= _SELF.MinimumZoom) _SELF.map.SetZoomLevel(_SELF.MinimumZoom);
            if (e.zoomLevel >= _SELF.MaximumZoom) _SELF.map.SetZoomLevel(_SELF.MaximumZoom);

            //_SELF.displayZooms("onendzoom", e.zoomLevel.toString());
            var displayDetails = false;

            var zooms = _SELF._detailZoomLevels[_SELF._zoomIndex];
            if (_SELF.arrayContains(zooms, e.zoomLevel)) {
                //showDetails(some shape here);
            } else if (!_SELF.arrayContains(zooms, e.zoomLevel)) {
                // time to change the range
                closeDetail();
            } else {
                var somethingelse = 0;
            }

            var zoomMoved = e.zoomLevel !== _SELF._lastZoom ? true : false;

            if (zoomMoved || _SELF._doFeatures) {
                // if we are about to do features we are due to also reset zoom index
                _SELF.doFeatures(e.zoomLevel, _SELF._lastCentre);
            }
        });

        // this will get done once here ... hereafter it is done by the reset or the events above
        if (_SELF._statId != "" && _SELF.validateNZStatsRequest(_SELF._statId)) {
            _SELF.processNZStatsRequest();
        } else {
            _SELF.doFeatures(_SELF._lastZoom, _SELF._lastCentre);
        }
    },


    /**
    * The main initialisation method for the Locator object.
    */
    init: function(options) {
        var _SELF = this;

        $().ready(function() {
            //add tabs to page
            $("#search").tabs();

            _SELF._featuresClient = new MapDS.Features.ServiceClient();

            // Grab all anchor elements on the page that have an href and target is external. Set their target to blank.
            $('a[rel="external"]').filter('[href!=""]').each(function() {
                this.target = "_blank";
            });

            // recover the request Statics NZ ID ... if provided
            var s = _SELF.findElement(_SELF.ElementIDs.StatId);
            _SELF._statId = s.value;

            $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).click(function() {
                _SELF.findAddress();
                return false;
            });
            $(_SELF.getElementID(_SELF.ElementIDs.SearchButtonArea)).click(function() {
                _SELF.findAddress();
                return false;
            });
            $(_SELF.getElementID(_SELF.ElementIDs.ResetButton)).click(function() {
                _SELF.reset();
                return false;
            });
            $(_SELF.getElementID(_SELF.ElementIDs.ResetButtonArea)).click(function() {
                _SELF.reset();
                return false;
            });

            // manage search fields ... we have either an address or area ... search
            $(_SELF.getElementID(_SELF.ElementIDs.SearchArea)).focus(function() {
                _SELF._lastFieldFocussed = this;
                _SELF.clearSearchAddressFields();
            })
            $(_SELF.getElementID(_SELF.ElementIDs.SearchStreet)).focus(function() {
                _SELF._lastFieldFocussed = this;
                _SELF.clearSearchAreaField();
            })
            $(_SELF.getElementID(_SELF.ElementIDs.SearchSuburb)).focus(function() {
                _SELF._lastFieldFocussed = this;
                _SELF.clearSearchAreaField();
            })
            $(_SELF.getElementID(_SELF.ElementIDs.SearchState)).focus(function() {
                _SELF._lastFieldFocussed = this;
                _SELF.clearSearchAreaField();
            })


            // ... toggles the census boundary flag
            $(_SELF.getElementID(_SELF.ElementIDs.Boundaries)).click(function() {
                if (this.checked == false) {
                    _SELF._pPolygonLayer.DeleteAllShapes();
                    closeDetail();
                } else {
                    if (_SELF.isUndefined(_SELF._lastZoom)) {
                        _SELF._lastzoom = _SELF.InitialZoom;
                    }
                    _SELF.doFeatures(_SELF._lastZoom, _SELF._lastCentre);
                }
            })

            // need to have the current text blanked on focus, reset on blur...
            $(_SELF.getElementID(_SELF.ElementIDs.Search) + ' input[title!=""]').keypress(

            function(evt) {
                _SELF._lastFieldFocussed = this;
                if (evt.keyCode == 13) {
                    $(_SELF.getElementID(_SELF.ElementIDs.SearchButton)).click();
                }
            }).hint();

            // ensure that Enter presses on the form don't cause a submit
            $("form").keypress(function(e) {
                if (e.keyCode == 13) {
                    return false;
                }
            });

            $(window).resize(function() {
                if (!_SELF.isUndefined(_SELF.map) && _SELF.map != null) {

                    // for some reason in anything other than IE this does not
                    // seem to change the settings.
                    detail.style.top = (_SELF.map.GetTop() + 50).toString() + "px";
                    detail.style.left = (_SELF.map.GetLeft() + 50).toString() + "px";

                    Logger.log("map top=" + _SELF.map.GetTop().toString());
                    Logger.log("map left=" + _SELF.map.GetLeft().toString());
                }
            });

            // attach the exception dialog
            $(_SELF.getElementID(_SELF.ElementIDs.ExceptionDialog)).dialog({
                bgiframe: true,
                modal: true,
                draggable: false,
                resizable: false,
                autoOpen: false,
                zIndex: 99999,
                close: function(evt, ui) {
                    if (_SELF._lastFieldFocussed) {
                        _SELF._lastFieldFocussed.focus();
                    }
                },
                buttons: {
                    Ok: function() {
                        $(this).dialog('close');
                    }
                }
            });

            // attach the multiple results dialog.
            $(_SELF.getElementID(_SELF.ElementIDs.MultipleAddressDialog)).dialog({
                bgiframe: true,
                height: 300,
                width: 400,
                modal: true,
                draggable: false,
                resizable: false,
                autoOpen: false,
                zIndex: 99999,
                buttons: {
                    'Use Address': function() {
                        var idx = $(this).find(_SELF.getElementID(_SELF.ElementIDs.MultipleAddresses)).attr('selectedIndex');
                        if (idx != 0) {
                            // go back one to get correct array location.
                            idx--;

                            // do callback on selected location...    
                            _SELF.findAddressCallback(_SELF._foundLocations[idx]);
                        }
                        $(this).dialog('close');
                    },
                    Cancel: function() {
                        $(this).dialog('close');
                    }
                }
            });

            // ATTENTION to group zoomlevels with feature type
            // Edit here and it will flow through the code logic
            // CAUTION changing these grouping means that feature types
            // also must be re-aligned. Check getFeatureCodes and make appropriate changes.
            // Making changes may require creation of a feature type. If this happens
            // permission in the database have to be set and testing has to consider
            // resulting application speed and load on the features service. From this
            // thinning may be required. If this happens testing must also include
            // polygon quality/accuracy.
            // CAUTION any changes to features data (thinning) may have further donut 
            // polygon complications. Check this.
            // CAUTION if you change these please update the default.aspx 
            // zoomTo entry zoom levels.
            // set up the zoom levels
            _SELF._detailZoomLevels = new Array();
            _SELF._detailZoomLevels.push(new Array(4, 5, 6, 7)); // Regional Council levels
            _SELF._detailZoomLevels.push(new Array(8, 9, 10, 11)); // Territorial Authorities levels  
            _SELF._detailZoomLevels.push(new Array(12, 13, 14, 15)); // Area Unit levels
            _SELF._detailZoomLevels.push(new Array(16, 17, 18)); // Meshblock levels
            _SELF.reset();


            // load up options
            if (options) {

            }

            // need to wrap the call in a function so we can maintain scope
            window.onload = function() {
                _SELF.initMap.apply(_SELF);
            };

        });
    }
};

Locator.init();

function zoomTo(level) {
    var _SELF = Locator;
    if (_SELF.isUndefined(_SELF._lastCentre)) return;

    _SELF.map.SetZoomLevel(level);
}

function rtrim(value) {

    var re = /((\s*\S+)*)\s*/;
    return value.replace(re, "$1");
}

function closeDetail() {
    detail.style.visibility = "hidden";
}

function showDetails(shape) {
    var _SELF = Locator;

    detail.style.top = _SELF.map.GetTop() + 50;
    detail.style.left = _SELF.map.GetLeft() + 50;
    detail.style.opacity = 0.8;
    detail.style.filter = "alpha(opacity=80)";
    detail.innerHTML = shape.GetDescription();

    detail.style.visibility = "visible";
}
